rice: AwesomeWM Night
21
bin/awesomefetch
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
# Author: https://github.com/rxyhn
|
||||
|
||||
user="${USER}"
|
||||
shell="$(basename ${SHELL})"
|
||||
distro=$(. /etc/os-release ; echo "$ID")
|
||||
wm="$(xprop -id $(xprop -root -notype | awk '$1=="_NET_SUPPORTING_WM_CHECK:"{print $5}') -notype -f _NET_WM_NAME 8t | grep "WM_NAME" | cut -f2 -d \")"
|
||||
kernel="$(uname -r | cut -d '-' -f1)"
|
||||
|
||||
white='\033[37m'
|
||||
bold='\033[1m'
|
||||
end='\033[0m'
|
||||
|
||||
printf '%b' "
|
||||
${bold}${white} __________ ${end} ${bold}welcome,${user}${end}
|
||||
${bold}${white} |______ |${end}
|
||||
${bold}${white} ______| |${end} ${bold}os${end} ${distro}
|
||||
${bold}${white} | ____ |${end} ${bold}sh${end} ${shell}
|
||||
${bold}${white} | |__ | |${end} ${bold}wm${end} ${wm}
|
||||
${bold}${white} |_____| |__|${end} ${bold}kr${end} ${kernel}
|
||||
"
|
133
bin/screensht
Executable file
|
@ -0,0 +1,133 @@
|
|||
#!/bin/bash
|
||||
|
||||
<<screensht
|
||||
|
||||
_____ _____ _____ _____ _____ _____ _____ _____ _____
|
||||
| __| | __ | __| __| | | __| | |_ _|
|
||||
|__ | --| -| __| __| | | |__ | | | |
|
||||
|_____|_____|__|__|_____|_____|_|___|_____|__|__| |_|
|
||||
~ Script to take screenshots with maim ~
|
||||
|
||||
screensht
|
||||
|
||||
|
||||
# =============================================
|
||||
|
||||
# Directory
|
||||
_SCREENSHOT_DIR_=$HOME/Pictures/Screenshots
|
||||
_ORIGINAL_DIR_=$_SCREENSHOT_DIR_/Original
|
||||
|
||||
# Color
|
||||
_FG_COLOR_='#1d1f21'
|
||||
_BG_COLOR_='#c5c8c6'
|
||||
_BG_SIZE_=10
|
||||
|
||||
# Border Size Applied when value greater than or equal 3
|
||||
_BORDER_SIZE_=0
|
||||
_SHADOW_SIZE_='50x10+0+10' # [ weight ] x [ radius ] + [ horizontal ] x [ vertical ]
|
||||
_ROUNDED_CORNER_=4
|
||||
|
||||
# =============================================
|
||||
|
||||
# List of Colors
|
||||
Light_Red="\033[1;31m"
|
||||
Light_Green="\033[1;32m"
|
||||
Yellow="\033[1;33m"
|
||||
Light_Blue="\033[1;34m"
|
||||
Light_Purple="\033[1;35m"
|
||||
Light_Cyan="\033[1;36m"
|
||||
NoColor="\033[0m"
|
||||
|
||||
|
||||
function check() {
|
||||
if [[ $? -eq 1 && ${PIPESTATUS[0]} -eq 1 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function get_latest_img() {
|
||||
_LATEST_IMAGE_=$(/bin/ls -th $_SCREENSHOT_DIR_ | grep -vE '.screensht.png$' | grep -E '.png$' | head -n 1)
|
||||
|
||||
if [[ $( echo "$_LATEST_IMAGE_" | wc -w ) -eq 0 ]]; then
|
||||
exit 1
|
||||
else
|
||||
_LATEST_IMAGE_="$_SCREENSHOT_DIR_/$_LATEST_IMAGE_"
|
||||
fi
|
||||
}
|
||||
|
||||
function convert() {
|
||||
_target_file_=$( echo "$_LATEST_IMAGE_" | sed 's/.png/.screensht.png/g' )
|
||||
|
||||
if [[ $_BORDER_SIZE_ -ge 3 ]]; then
|
||||
magick convert "$_LATEST_IMAGE_" \
|
||||
-format 'roundrectangle 1,1 %[fx:w+4],%[fx:h+4] '"$_ROUNDED_CORNER_"','"$_ROUNDED_CORNER_"''\
|
||||
info: > $_SCREENSHOT_DIR_/_rounded_.mvg
|
||||
check
|
||||
|
||||
magick convert "$_LATEST_IMAGE_" -border $_BORDER_SIZE_ -alpha transparent \
|
||||
-background none -fill white -stroke none -strokewidth 0 \
|
||||
-draw "@"$_SCREENSHOT_DIR_"/_rounded_.mvg" $_SCREENSHOT_DIR_/_rounded_mask_.png
|
||||
check
|
||||
|
||||
magick convert "$_LATEST_IMAGE_" -border $_BORDER_SIZE_ -alpha transparent \
|
||||
-background none -fill none -stroke $_FG_COLOR_ -strokewidth $_BORDER_SIZE_ \
|
||||
-draw "@"$_SCREENSHOT_DIR_"/_rounded_.mvg" $_SCREENSHOT_DIR_/_rounded_overlay_.png
|
||||
check
|
||||
|
||||
magick convert "$_LATEST_IMAGE_" -alpha set -bordercolor none -border $_BORDER_SIZE_ \
|
||||
$_SCREENSHOT_DIR_/_rounded_mask_.png -compose DstIn -composite \
|
||||
$_SCREENSHOT_DIR_/_rounded_overlay_.png -compose Over -composite \
|
||||
"$_target_file_" && \
|
||||
rm -f $_SCREENSHOT_DIR_/_rounded_*
|
||||
check
|
||||
else
|
||||
magick convert "$_LATEST_IMAGE_" \( +clone -alpha extract -draw 'fill black polygon 0,0 0,'"$_ROUNDED_CORNER_"' '"$_ROUNDED_CORNER_"',0 fill white circle '"$_ROUNDED_CORNER_"','"$_ROUNDED_CORNER_"' '"$_ROUNDED_CORNER_"',0' \
|
||||
\( +clone -flip \) -compose Multiply -composite \
|
||||
\( +clone -flop \) -compose Multiply -composite \
|
||||
\) -alpha off -compose CopyOpacity -composite -compose over "$_target_file_"
|
||||
check
|
||||
fi
|
||||
|
||||
magick convert "$_target_file_" \( +clone -background black -shadow $_SHADOW_SIZE_ \) +swap -background none -layers merge +repage "$_target_file_" \
|
||||
&& magick convert "$_target_file_" -bordercolor $_BG_COLOR_ -border $_BG_SIZE_ "$_target_file_"
|
||||
check
|
||||
|
||||
magick convert "$_target_file_" -gravity North -background $_BG_COLOR_ -splice 0x$(( $_BG_SIZE_ / 2 )) "$_target_file_"
|
||||
check
|
||||
|
||||
magick convert "$_target_file_" -profile /usr/share/color/icc/colord/sRGB.icc "$_target_file_"
|
||||
check
|
||||
}
|
||||
|
||||
function summary() {
|
||||
_runtime_job_=$(($2-$1))
|
||||
hours=$((_runtime_job_ / 3600)); minutes=$(( (_runtime_job_ % 3600) / 60 )); seconds=$(( (_runtime_job_ % 3600) % 60 ))
|
||||
|
||||
if [[ $3 != "failed" ]]; then
|
||||
xclip -selection clipboard -t image/png -i $_target_file_ && notify-send -u normal -t 3000 "Awesome-Maim: $_target_file_ Copied"
|
||||
fi
|
||||
}
|
||||
|
||||
function main() {
|
||||
_start_job_=$(date +%Y.%m.%d-%H.%M.%S)
|
||||
|
||||
maim -u -b 3 -m 5 -s ~/Pictures/Screenshots/$_start_job_.png> /dev/null 2>&1
|
||||
check
|
||||
|
||||
get_latest_img
|
||||
|
||||
convert
|
||||
|
||||
mv $_LATEST_IMAGE_ ~/Pictures/Screenshots/Original/
|
||||
notify-send -i ~/Pictures/Screenshots/Original/$_start_job_.png "Screenshot Taken" "saved to ~/Pictures/Screenshots"
|
||||
_end_job_=$(date +%s)
|
||||
}
|
||||
|
||||
|
||||
if [[ ! -d "$_SCREENSHOT_DIR_" || ! -d "$_ORIGINAL_DIR_" ]]; then
|
||||
mkdir -p "$_SCREENSHOT_DIR_"
|
||||
mkdir -p "$_ORIGINAL_DIR_"
|
||||
fi
|
||||
|
||||
clear
|
||||
main
|
28
config/alacritty/aesthetic/colors.yml
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Aesthetic Color
|
||||
# Created by https://github.com/rxyhn
|
||||
|
||||
colors:
|
||||
primary:
|
||||
background: '#061115'
|
||||
foreground: '#D9D7D6'
|
||||
cursor:
|
||||
text: CellForeground
|
||||
cursor: '#D9D7D6'
|
||||
bright:
|
||||
black: '#1C252C'
|
||||
red: '#DF5B61'
|
||||
green: '#78B892'
|
||||
yellow: '#DE8F78'
|
||||
blue: '#6791C9'
|
||||
magenta: '#BC83E3'
|
||||
cyan: '#67AFC1'
|
||||
white: '#D9D7D6'
|
||||
normal:
|
||||
black: '#1C252C'
|
||||
red: '#DF5B61'
|
||||
green: '#78B892'
|
||||
yellow: '#DE8F78'
|
||||
blue: '#6791C9'
|
||||
magenta: '#BC83E3'
|
||||
cyan: '#67AFC1'
|
||||
white: '#D9D7D6'
|
89
config/alacritty/alacritty.yml
Normal file
|
@ -0,0 +1,89 @@
|
|||
## Import files (Colors,Etc)
|
||||
import:
|
||||
- ~/.config/alacritty/aesthetic/colors.yml
|
||||
|
||||
## Set environment variables
|
||||
env:
|
||||
TERM: alacritty
|
||||
WINIT_X11_SCALE_FACTOR: '1.0'
|
||||
|
||||
## Font
|
||||
font:
|
||||
normal:
|
||||
family: Iosevka
|
||||
style: Medium
|
||||
|
||||
bold:
|
||||
family: Iosevka
|
||||
style: Bold
|
||||
|
||||
italic:
|
||||
family: Iosevka
|
||||
style: Italic
|
||||
|
||||
bold_italic:
|
||||
family: Iosevka
|
||||
style: Bold Italic
|
||||
|
||||
size: 11
|
||||
|
||||
offset:
|
||||
x: 0
|
||||
y: 0
|
||||
|
||||
glyph_offset:
|
||||
x: 0
|
||||
y: 0
|
||||
|
||||
draw_bold_text_with_bright_colors: true
|
||||
|
||||
## Terminal window settings
|
||||
window:
|
||||
opacity: 1.0
|
||||
padding:
|
||||
x: 20
|
||||
y: 20
|
||||
|
||||
dynamic_padding: true
|
||||
decorations: full
|
||||
startup_mode: Windowed
|
||||
dynamic_title: true
|
||||
title: Alacritty
|
||||
|
||||
## Live config reload
|
||||
live_config_reload: true
|
||||
|
||||
class:
|
||||
instance: Alacritty
|
||||
general: Alacritty
|
||||
|
||||
## Scroll
|
||||
scrolling:
|
||||
history: 10000
|
||||
|
||||
# Visual
|
||||
visual_bell:
|
||||
animation: EaseOutExpo
|
||||
duration: 0.5
|
||||
color: "#ffffff"
|
||||
|
||||
##Selection
|
||||
selection:
|
||||
semantic_escape_chars: ",│`|:\"' ()[]{}<>\t"
|
||||
save_to_clipboard: true
|
||||
|
||||
## Cs
|
||||
cursor:
|
||||
style: 'Underline'
|
||||
thickness: 0.20
|
||||
|
||||
## Shell
|
||||
shell:
|
||||
program: /usr/bin/zsh
|
||||
|
||||
## Mouse
|
||||
mouse:
|
||||
double_click: { threshold: 300 }
|
||||
triple_click: { threshold: 300 }
|
||||
|
||||
hide_when_typing: true
|
24
config/awesome/configuration/autorun.sh
Executable file
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
function run {
|
||||
if ! pgrep -f $1 ;
|
||||
then
|
||||
$@&
|
||||
fi
|
||||
}
|
||||
|
||||
# music
|
||||
run mpd
|
||||
run mpDris2 # add playerctl support to mpd
|
||||
|
||||
# compositor
|
||||
run picom --config $HOME/.config/picom/picom.conf
|
||||
|
||||
# redshift
|
||||
run redshift
|
||||
|
||||
# power manager
|
||||
run xfce4-power-manager
|
||||
|
||||
# auth
|
||||
run /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1
|
44
config/awesome/configuration/bling.lua
Normal file
|
@ -0,0 +1,44 @@
|
|||
local awful = require("awful")
|
||||
local wibox = require("wibox")
|
||||
local beautiful = require("beautiful")
|
||||
local bling = require("module.bling")
|
||||
|
||||
|
||||
bling.widget.tag_preview.enable {
|
||||
show_client_content = false,
|
||||
placement_fn = function(c)
|
||||
awful.placement.left(c, {
|
||||
margins = {
|
||||
-- left = beautiful.wibar_width + beautiful.useless_gap * 2,
|
||||
left = beautiful.wibar_width + 11
|
||||
}
|
||||
})
|
||||
end,
|
||||
scale = 0.15,
|
||||
honor_padding = true,
|
||||
honor_workarea = false,
|
||||
background_widget = wibox.widget {
|
||||
bg = beautiful.xbackground,
|
||||
widget = wibox.widget.background
|
||||
}
|
||||
}
|
||||
|
||||
bling.widget.task_preview.enable {
|
||||
placement_fn = function(c)
|
||||
awful.placement.top_left(c, {
|
||||
margins = {
|
||||
-- bottom = beautiful.wibar_height + beautiful.useless_gap * 2,
|
||||
-- left = beautiful.useless_gap * 2
|
||||
top = 19,
|
||||
left = beautiful.wibar_width + 11
|
||||
}
|
||||
})
|
||||
end
|
||||
}
|
||||
|
||||
awful.keyboard.append_global_keybindings({
|
||||
awful.key({modkey}, "d", function() awful.spawn(launcher) end,
|
||||
{description = "show app launcher", group = "launcher"})
|
||||
})
|
||||
|
||||
require('ui.pop.window_switcher').enable()
|
65
config/awesome/configuration/init.lua
Normal file
|
@ -0,0 +1,65 @@
|
|||
-- Standard Awesome Library
|
||||
local awful = require("awful")
|
||||
local beautiful = require("beautiful")
|
||||
local gears = require("gears")
|
||||
|
||||
-- Bling
|
||||
local bling = require("module.bling")
|
||||
bling.module.flash_focus.enable()
|
||||
|
||||
-- Autostart
|
||||
awful.spawn.with_shell("~/.config/awesome/configuration/autorun.sh")
|
||||
|
||||
-- Default Applications
|
||||
terminal = "alacritty"
|
||||
browser = "firefox"
|
||||
filemanager = "thunar"
|
||||
vscode = "code"
|
||||
editor = os.getenv("EDITOR") or "nvim"
|
||||
editor_cmd = terminal .. " -e " .. editor
|
||||
discord = "discord"
|
||||
launcher = "rofi -show drun"
|
||||
|
||||
-- Weather API
|
||||
openweathermap_key = ""
|
||||
openweathermap_city_id = ""
|
||||
weather_units = ""
|
||||
|
||||
-- Global Vars
|
||||
screen_width = awful.screen.focused().geometry.width
|
||||
screen_height = awful.screen.focused().geometry.height
|
||||
|
||||
-- Default modkey.
|
||||
modkey = "Mod4"
|
||||
altkey = "Mod1"
|
||||
shift = "Shift"
|
||||
ctrl = "Control"
|
||||
|
||||
-- Set Wallpaper
|
||||
gears.wallpaper.maximized(beautiful.wallpaper, s, false, nil)
|
||||
|
||||
-- bling.module.tiled_wallpaper("", s, {
|
||||
-- fg = beautiful.xcolor8,
|
||||
-- bg = beautiful.xbackground,
|
||||
-- offset_y = 20,
|
||||
-- offset_x = 20,
|
||||
-- font = "Iosevka",
|
||||
-- font_size = 13,
|
||||
-- padding = 100,
|
||||
-- zickzack = true
|
||||
-- })
|
||||
|
||||
-- Get Bling Config
|
||||
require("configuration.bling")
|
||||
|
||||
-- Get Keybinds
|
||||
require("configuration.keys")
|
||||
|
||||
-- Get Rules
|
||||
require("configuration.ruled")
|
||||
|
||||
-- Layouts and Window Stuff
|
||||
require("configuration.window")
|
||||
|
||||
-- Scratchpad
|
||||
require("configuration.scratchpad")
|
342
config/awesome/configuration/keys.lua
Normal file
|
@ -0,0 +1,342 @@
|
|||
-- keys.lua
|
||||
-- Contains Global Keys
|
||||
local gears = require("gears")
|
||||
local awful = require("awful")
|
||||
local hotkeys_popup = require("awful.hotkeys_popup")
|
||||
local helpers = require("helpers")
|
||||
-- Custom modules
|
||||
local machi = require("module.layout-machi")
|
||||
local bling = require("module.bling")
|
||||
-- Theme library
|
||||
local beautiful = require("beautiful")
|
||||
local xresources = require("beautiful.xresources")
|
||||
local dpi = xresources.apply_dpi
|
||||
|
||||
local keys = {}
|
||||
|
||||
-- Mouse Bindings
|
||||
awful.mouse.append_global_mousebindings({
|
||||
awful.button({}, 4, awful.tag.viewprev),
|
||||
awful.button({}, 5, awful.tag.viewnext)
|
||||
})
|
||||
|
||||
-- Client and Tabs Bindings
|
||||
awful.keyboard.append_global_keybindings({
|
||||
awful.key({"Mod1"}, "a",
|
||||
function() bling.module.tabbed.pick_with_dmenu() end,
|
||||
{description = "pick client to add to tab group", group = "tabs"}),
|
||||
awful.key({"Mod1"}, "s", function() bling.module.tabbed.iter() end,
|
||||
{description = "iterate through tabbing group", group = "tabs"}),
|
||||
awful.key({"Mod1"}, "d", function() bling.module.tabbed.pop() end, {
|
||||
description = "remove focused client from tabbing group",
|
||||
group = "tabs"
|
||||
}), awful.key({modkey}, "Down", function()
|
||||
awful.client.focus.bydirection("down")
|
||||
bling.module.flash_focus.flashfocus(client.focus)
|
||||
end, {description = "focus down", group = "client"}),
|
||||
awful.key({modkey}, "Up", function()
|
||||
awful.client.focus.bydirection("up")
|
||||
bling.module.flash_focus.flashfocus(client.focus)
|
||||
end, {description = "focus up", group = "client"}),
|
||||
awful.key({modkey}, "Left", function()
|
||||
awful.client.focus.bydirection("left")
|
||||
bling.module.flash_focus.flashfocus(client.focus)
|
||||
end, {description = "focus left", group = "client"}),
|
||||
awful.key({modkey}, "Right", function()
|
||||
awful.client.focus.bydirection("right")
|
||||
bling.module.flash_focus.flashfocus(client.focus)
|
||||
end, {description = "focus right", group = "client"}),
|
||||
awful.key({modkey}, "j", function() awful.client.focus.byidx(1) end,
|
||||
{description = "focus next by index", group = "client"}),
|
||||
awful.key({modkey}, "k", function() awful.client.focus.byidx(-1) end,
|
||||
{description = "focus previous by index", group = "client"}),
|
||||
awful.key({modkey, "Shift"}, "j", function() awful.client.swap.byidx(1) end,
|
||||
{description = "swap with next client by index", group = "client"}),
|
||||
awful.key({modkey, "Shift"}, "k",
|
||||
function() awful.client.swap.byidx(-1) end, {
|
||||
description = "swap with previous client by index",
|
||||
group = "client"
|
||||
}), awful.key({modkey}, "u", awful.client.urgent.jumpto,
|
||||
{description = "jump to urgent client", group = "client"}),
|
||||
awful.key({altkey}, "Tab", function()
|
||||
awesome.emit_signal("bling::window_switcher::turn_on")
|
||||
end, {description = "Window Switcher", group = "client"})
|
||||
})
|
||||
|
||||
-- Awesomewm
|
||||
awful.keyboard.append_global_keybindings({
|
||||
-- Volume control
|
||||
awful.key({}, "XF86AudioRaiseVolume",
|
||||
function() awful.spawn("amixer -D pulse set Master 5%+") end,
|
||||
{description = "increase volume", group = "awesome"}),
|
||||
awful.key({}, "XF86AudioLowerVolume",
|
||||
function() awful.spawn("amixer -D pulse set Master 5%-") end,
|
||||
{description = "decrease volume", group = "awesome"}),
|
||||
awful.key({}, "XF86AudioMute", function() awful.spawn("amixer -D pulse set Master 1+ toggle") end,
|
||||
{description = "mute volume", group = "awesome"}), -- Media Control
|
||||
awful.key({}, "XF86AudioPlay",
|
||||
function() awful.spawn("playerctl play-pause") end,
|
||||
{description = "toggle playerctl", group = "awesome"}),
|
||||
awful.key({}, "XF86AudioPrev",
|
||||
function() awful.spawn("playerctl previous") end,
|
||||
{description = "playerctl previous", group = "awesome"}),
|
||||
awful.key({}, "XF86AudioNext", function() awful.spawn("playerctl next") end,
|
||||
{description = "playerctl next", group = "awesome"}),
|
||||
|
||||
-- Screenshots
|
||||
awful.key({}, "Print",
|
||||
function() awful.spawn.with_shell("screensht") end,
|
||||
{description = "take a screenshot", group = "awesome"}),
|
||||
|
||||
-- Brightness
|
||||
awful.key({}, "XF86MonBrightnessUp",
|
||||
function() awful.spawn("brightnessctl set 5%+ -q") end,
|
||||
{description = "increase brightness", group = "awesome"}),
|
||||
awful.key({}, "XF86MonBrightnessDown",
|
||||
function() awful.spawn("brightnessctl set 5%- -q") end,
|
||||
{description = "decrease brightness", group = "awesome"}),
|
||||
|
||||
-- Awesome stuff
|
||||
awful.key({modkey}, "F1", hotkeys_popup.show_help,
|
||||
{description = "show help", group = "awesome"}),
|
||||
awful.key({modkey}, "Escape", awful.tag.history.restore,
|
||||
{description = "go back", group = "tag"}),
|
||||
awful.key({modkey}, "x",
|
||||
function() require("ui.pop.exitscreen").exit_screen_show() end,
|
||||
{description = "show exit screen", group = "awesome"}),
|
||||
awful.key({modkey, "Control"}, "r", awesome.restart,
|
||||
{description = "reload awesome", group = "awesome"}),
|
||||
awful.key({modkey, "Shift"}, "q", awesome.quit,
|
||||
{description = "quit awesome", group = "awesome"})
|
||||
|
||||
})
|
||||
|
||||
-- Layout Machi
|
||||
awful.keyboard.append_global_keybindings({
|
||||
awful.key({modkey}, ".",
|
||||
function() machi.default_editor.start_interactive() end, {
|
||||
description = "edit the current layout if it is a machi layout",
|
||||
group = "layout"
|
||||
}),
|
||||
awful.key({modkey}, "/", function() machi.switcher.start(client.focus) end,
|
||||
{
|
||||
description = "switch between windows for a machi layout",
|
||||
group = "layout"
|
||||
})
|
||||
})
|
||||
|
||||
-- Launcher and screen
|
||||
awful.keyboard.append_global_keybindings({
|
||||
awful.key({modkey, "Control"}, "j",
|
||||
function() awful.screen.focus_relative(1) end,
|
||||
{description = "focus the next screen", group = "screen"}),
|
||||
awful.key({modkey, "Control"}, "k",
|
||||
function() awful.screen.focus_relative(-1) end,
|
||||
{description = "focus the previous screen", group = "screen"}),
|
||||
|
||||
awful.key({modkey}, "Return", function() awful.spawn(terminal) end,
|
||||
{description = "open a terminal", group = "launcher"}),
|
||||
|
||||
awful.key({modkey}, "s",
|
||||
function() awesome.emit_signal("scratch::music") end,
|
||||
{description = "open music", group = "scratchpad"}),
|
||||
|
||||
awful.key({modkey}, "z", function()
|
||||
dash_toggle() end,
|
||||
{description = "Toggle dashboard", group = "launcher"}),
|
||||
|
||||
awful.key({modkey}, "f", function() awful.spawn(filemanager) end,
|
||||
{description = "open file browser", group = "launcher"}),
|
||||
|
||||
awful.key({modkey}, "v",
|
||||
function() awesome.emit_signal("scratch::chat") end,
|
||||
{description = "open chats", group = "scratchpad"}),
|
||||
|
||||
awful.key({modkey}, "w", function() awful.spawn.with_shell(browser) end,
|
||||
{description = "open firefox", group = "launcher"}),
|
||||
|
||||
awful.key({modkey}, "l", function() awful.tag.incmwfact(0.05) end,
|
||||
{description = "increase master width factor", group = "layout"}),
|
||||
awful.key({modkey}, "h", function() awful.tag.incmwfact(-0.05) end,
|
||||
{description = "decrease master width factor", group = "layout"}),
|
||||
|
||||
awful.key({modkey, "Shift"}, "h",
|
||||
function() awful.tag.incnmaster(1, nil, true) end, {
|
||||
description = "increase the number of master clients",
|
||||
group = "layout"
|
||||
}), awful.key({modkey, "Shift"}, "l",
|
||||
function() awful.tag.incnmaster(-1, nil, true) end, {
|
||||
description = "decrease the number of master clients",
|
||||
group = "layout"
|
||||
}), awful.key({modkey, "Control"}, "h",
|
||||
function() awful.tag.incncol(1, nil, true) end, {
|
||||
description = "increase the number of columns",
|
||||
group = "layout"
|
||||
}), awful.key({modkey, "Control"}, "l",
|
||||
function() awful.tag.incncol(-1, nil, true) end, {
|
||||
description = "decrease the number of columns",
|
||||
group = "layout"
|
||||
}),
|
||||
|
||||
awful.key({modkey}, "space", function() awful.layout.inc(1) end,
|
||||
{description = "select next", group = "layout"}),
|
||||
awful.key({modkey, "Shift"}, "space", function() awful.layout.inc(-1) end,
|
||||
{description = "select previous", group = "layout"}), -- Set Layout
|
||||
awful.key({modkey, "Control"}, "w",
|
||||
function() awful.layout.set(awful.layout.suit.max) end,
|
||||
{description = "set max layout", group = "tag"}),
|
||||
awful.key({modkey}, "s",
|
||||
function() awful.layout.set(awful.layout.suit.tile) end,
|
||||
{description = "set tile layout", group = "tag"}),
|
||||
awful.key({modkey, shift}, "s",
|
||||
function() awful.layout.set(awful.layout.suit.floating) end,
|
||||
{description = "set floating layout", group = "tag"}),
|
||||
|
||||
awful.key({modkey, "Control"}, "n", function()
|
||||
local c = awful.client.restore()
|
||||
-- Focus restored client
|
||||
if c then
|
||||
c:emit_signal("request::activate", "key.unminimize", {raise = true})
|
||||
end
|
||||
end, {description = "restore minimized", group = "client"})
|
||||
})
|
||||
|
||||
-- Client management keybinds
|
||||
client.connect_signal("request::default_keybindings", function()
|
||||
awful.keyboard.append_client_keybindings({
|
||||
awful.key({modkey, "Shift"}, "f", function(c)
|
||||
c.fullscreen = not c.fullscreen
|
||||
c:raise()
|
||||
end, {description = "toggle fullscreen", group = "client"}),
|
||||
awful.key({modkey}, "q", function(c) c:kill() end,
|
||||
{description = "close", group = "client"}),
|
||||
awful.key({modkey, "Control"}, "space", awful.client.floating.toggle,
|
||||
{description = "toggle floating", group = "client"}),
|
||||
awful.key({modkey, "Control"}, "Return",
|
||||
function(c) c:swap(awful.client.getmaster()) end,
|
||||
{description = "move to master", group = "client"}),
|
||||
awful.key({modkey}, "o", function(c) c:move_to_screen() end,
|
||||
{description = "move to screen", group = "client"}),
|
||||
awful.key({modkey, shift}, "b", function(c)
|
||||
c.floating = not c.floating
|
||||
c.width = 400
|
||||
c.height = 200
|
||||
awful.placement.bottom_right(c)
|
||||
c.sticky = not c.sticky
|
||||
end, {description = "toggle keep on top", group = "client"}),
|
||||
|
||||
awful.key({modkey}, "n", function(c)
|
||||
-- The client currently has the input focus, so it cannot be
|
||||
-- minimized, since minimized clients can't have the focus.
|
||||
c.minimized = true
|
||||
end, {description = "minimize", group = "client"}),
|
||||
awful.key({modkey}, "m", function(c)
|
||||
c.maximized = not c.maximized
|
||||
c:raise()
|
||||
end, {description = "(un)maximize", group = "client"}),
|
||||
awful.key({modkey, "Control"}, "m", function(c)
|
||||
c.maximized_vertical = not c.maximized_vertical
|
||||
c:raise()
|
||||
end, {description = "(un)maximize vertically", group = "client"}),
|
||||
awful.key({modkey, "Shift"}, "m", function(c)
|
||||
c.maximized_horizontal = not c.maximized_horizontal
|
||||
c:raise()
|
||||
end, {description = "(un)maximize horizontally", group = "client"}),
|
||||
|
||||
-- On the fly padding change
|
||||
awful.key({modkey, shift}, "=",
|
||||
function() helpers.resize_padding(5) end,
|
||||
{description = "add padding", group = "screen"}),
|
||||
awful.key({modkey, shift}, "-",
|
||||
function() helpers.resize_padding(-5) end,
|
||||
{description = "subtract padding", group = "screen"}),
|
||||
|
||||
-- On the fly useless gaps change
|
||||
awful.key({modkey}, "=", function() helpers.resize_gaps(5) end,
|
||||
{description = "add gaps", group = "screen"}),
|
||||
|
||||
awful.key({modkey}, "-", function() helpers.resize_gaps(-5) end,
|
||||
{description = "subtract gaps", group = "screen"}),
|
||||
-- Single tap: Center client
|
||||
-- Double tap: Center client + Floating + Resize
|
||||
awful.key({modkey}, "c", function(c)
|
||||
awful.placement.centered(c, {
|
||||
honor_workarea = true,
|
||||
honor_padding = true
|
||||
})
|
||||
helpers.single_double_tap(nil, function()
|
||||
helpers.float_and_resize(c, screen_width * 0.25,
|
||||
screen_height * 0.28)
|
||||
end)
|
||||
end)
|
||||
})
|
||||
end)
|
||||
|
||||
-- Num row keybinds
|
||||
awful.keyboard.append_global_keybindings({
|
||||
awful.key {
|
||||
modifiers = {modkey},
|
||||
keygroup = "numrow",
|
||||
description = "only view tag",
|
||||
group = "tag",
|
||||
on_press = function(index)
|
||||
local screen = awful.screen.focused()
|
||||
local tag = screen.tags[index]
|
||||
if tag then tag:view_only() end
|
||||
end
|
||||
}, awful.key {
|
||||
modifiers = {modkey, "Control"},
|
||||
keygroup = "numrow",
|
||||
description = "toggle tag",
|
||||
group = "tag",
|
||||
on_press = function(index)
|
||||
local screen = awful.screen.focused()
|
||||
local tag = screen.tags[index]
|
||||
if tag then awful.tag.viewtoggle(tag) end
|
||||
end
|
||||
}, awful.key {
|
||||
modifiers = {modkey, "Shift"},
|
||||
keygroup = "numrow",
|
||||
description = "move focused client to tag",
|
||||
group = "tag",
|
||||
on_press = function(index)
|
||||
if client.focus then
|
||||
local tag = client.focus.screen.tags[index]
|
||||
if tag then client.focus:move_to_tag(tag) end
|
||||
end
|
||||
end
|
||||
}, awful.key {
|
||||
modifiers = {modkey, "Control", "Shift"},
|
||||
keygroup = "numrow",
|
||||
description = "toggle focused client on tag",
|
||||
group = "tag",
|
||||
on_press = function(index)
|
||||
if client.focus then
|
||||
local tag = client.focus.screen.tags[index]
|
||||
if tag then client.focus:toggle_tag(tag) end
|
||||
end
|
||||
end
|
||||
}, awful.key {
|
||||
modifiers = {modkey},
|
||||
keygroup = "numpad",
|
||||
description = "select layout directly",
|
||||
group = "layout",
|
||||
on_press = function(index)
|
||||
local t = awful.screen.focused().selected_tag
|
||||
if t then t.layout = t.layouts[index] or t.layout end
|
||||
end
|
||||
}
|
||||
})
|
||||
|
||||
client.connect_signal("request::default_mousebindings", function()
|
||||
awful.mouse.append_client_mousebindings({
|
||||
awful.button({}, 1, function(c)
|
||||
c:activate{context = "mouse_click"}
|
||||
end), awful.button({modkey}, 1, function(c)
|
||||
c:activate{context = "mouse_click", action = "mouse_move"}
|
||||
end), awful.button({modkey}, 3, function(c)
|
||||
c:activate{context = "mouse_click", action = "mouse_resize"}
|
||||
end)
|
||||
})
|
||||
end)
|
||||
-- EOF ------------------------------------------------------------------------
|
82
config/awesome/configuration/ruled.lua
Normal file
|
@ -0,0 +1,82 @@
|
|||
local awful = require("awful")
|
||||
local beautiful = require("beautiful")
|
||||
local ruled = require("ruled")
|
||||
|
||||
ruled.client.connect_signal("request::rules", function()
|
||||
|
||||
-- Global
|
||||
ruled.client.append_rule {
|
||||
id = "global",
|
||||
rule = {},
|
||||
properties = {
|
||||
focus = awful.client.focus.filter,
|
||||
raise = true,
|
||||
size_hints_honor = false,
|
||||
screen = awful.screen.preferred,
|
||||
placement = awful.placement.no_overlap+awful.placement.no_offscreen
|
||||
}
|
||||
}
|
||||
|
||||
-- tasklist order
|
||||
ruled.client.append_rule {
|
||||
id = "tasklist_order",
|
||||
rule = {},
|
||||
properties = {},
|
||||
callback = awful.client.setslave
|
||||
}
|
||||
|
||||
-- Float em
|
||||
ruled.client.append_rule {
|
||||
id = "floating",
|
||||
rule_any = {
|
||||
class = {"Arandr", "Blueman-manager", "Sxiv", "fzfmenu"},
|
||||
role = {
|
||||
"pop-up" -- e.g. Google Chrome's (detached) Developer Tools.
|
||||
},
|
||||
name = {"Friends List", "Steam - News"},
|
||||
instance = {"spad", "discord", "music"}
|
||||
},
|
||||
properties = {floating = true, placement = awful.placement.centered}
|
||||
}
|
||||
|
||||
-- Borders
|
||||
ruled.client.append_rule {
|
||||
id = "borders",
|
||||
rule_any = {type = {"normal", "dialog"}},
|
||||
except_any = {
|
||||
role = {"Popup"},
|
||||
type = {"splash"},
|
||||
name = {"^discord.com is sharing your screen.$"}
|
||||
},
|
||||
properties = {
|
||||
border_width = beautiful.border_width,
|
||||
border_color = beautiful.border_normal
|
||||
}
|
||||
}
|
||||
|
||||
-- Center Placement
|
||||
ruled.client.append_rule {
|
||||
id = "center_placement",
|
||||
rule_any = {
|
||||
type = {"dialog"},
|
||||
class = {"Steam", "discord", "markdown_input", "scratchpad"},
|
||||
instance = {"markdown_input", "scratchpad"},
|
||||
role = {"GtkFileChooserDialog", "conversation"}
|
||||
},
|
||||
properties = {placement = awful.placement.center}
|
||||
}
|
||||
|
||||
-- Titlebar rules
|
||||
ruled.client.append_rule {
|
||||
id = "titlebars",
|
||||
rule_any = {type = {"normal", "dialog"}},
|
||||
except_any = {
|
||||
class = {"Steam", "zoom", "jetbrains-studio", "chat", "Org.gnome.Nautilus", "Firefox", "Google-chrome", "Brave-browser"},
|
||||
type = {"splash"},
|
||||
instance = {"onboard"},
|
||||
name = {"^discord.com is sharing your screen.$"}
|
||||
},
|
||||
properties = {titlebars_enabled = true}
|
||||
}
|
||||
end)
|
||||
|
55
config/awesome/configuration/scratchpad.lua
Normal file
|
@ -0,0 +1,55 @@
|
|||
local bling = require("module.bling")
|
||||
local beautiful = require("beautiful")
|
||||
local dpi = beautiful.xresources.apply_dpi
|
||||
local rubato = require("module.rubato")
|
||||
|
||||
local music_anim = {
|
||||
x = rubato.timed {
|
||||
pos = -970,
|
||||
rate = 120,
|
||||
easing = rubato.quadratic,
|
||||
intro = 0.1,
|
||||
duration = 0.3,
|
||||
awestore_compat = true
|
||||
}
|
||||
}
|
||||
|
||||
local music_scratch = bling.module.scratchpad:new{
|
||||
command = music,
|
||||
rule = {instance = "music"},
|
||||
sticky = false,
|
||||
autoclose = false,
|
||||
floating = true,
|
||||
geometry = {x = dpi(10), y = dpi(580), height = dpi(460), width = dpi(960)},
|
||||
reapply = true,
|
||||
rubato = music_anim
|
||||
}
|
||||
|
||||
awesome.connect_signal("scratch::music", function() music_scratch:toggle() end)
|
||||
|
||||
local chat_anim = {
|
||||
y = rubato.timed {
|
||||
pos = 1090,
|
||||
rate = 120,
|
||||
easing = rubato.quadratic,
|
||||
intro = 0.1,
|
||||
duration = 0.3,
|
||||
awestore_compat = true
|
||||
}
|
||||
}
|
||||
|
||||
local chat_scratch = bling.module.scratchpad:new{
|
||||
command = "Discord",
|
||||
rule = {
|
||||
-- class = "chat"
|
||||
class = "discord"
|
||||
},
|
||||
sticky = false,
|
||||
autoclose = false,
|
||||
floating = true,
|
||||
geometry = {x = dpi(460), y = dpi(90), height = dpi(900), width = dpi(1000)},
|
||||
reapply = true,
|
||||
rubato = chat_anim
|
||||
}
|
||||
|
||||
awesome.connect_signal("scratch::chat", function() chat_scratch:toggle() end)
|
170
config/awesome/configuration/window.lua
Normal file
|
@ -0,0 +1,170 @@
|
|||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
local gfs = gears.filesystem
|
||||
local wibox = require("wibox")
|
||||
local beautiful = require("beautiful")
|
||||
local dpi = require("beautiful.xresources").apply_dpi
|
||||
local helpers = require("helpers")
|
||||
|
||||
-- Bling Module
|
||||
local bling = require("module.bling")
|
||||
|
||||
-- Layout Machi
|
||||
local machi = require("module.layout-machi")
|
||||
beautiful.layout_machi = machi.get_icon()
|
||||
|
||||
-- This is to slave windows' positions in floating layout
|
||||
require("module.savefloats")
|
||||
|
||||
-- Better mouse resizing on tiled
|
||||
require("module.better-resize")
|
||||
|
||||
client.connect_signal("request::manage", function(c)
|
||||
if not c.icon then
|
||||
local i = gears.surface(gfs.get_configuration_dir() ..
|
||||
"icons/awesome.png")
|
||||
c.icon = i._native
|
||||
end
|
||||
|
||||
-- Set the windows at the slave,
|
||||
if awesome.startup and not c.size_hints.user_position and
|
||||
not c.size_hints.program_position then
|
||||
-- Prevent clients from being unreachable after screen count changes.
|
||||
awful.placement.no_offscreen(c)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Enable sloppy focus, so that focus follows mouse.
|
||||
client.connect_signal("mouse::enter", function(c)
|
||||
c:emit_signal("request::activate", "mouse_enter", {raise = false})
|
||||
end)
|
||||
|
||||
client.connect_signal("focus",
|
||||
function(c) c.border_color = beautiful.border_focus end)
|
||||
|
||||
client.connect_signal("unfocus",
|
||||
function(c) c.border_color = beautiful.border_normal end)
|
||||
|
||||
-- Custom Layouts -------------------------------------------------------------
|
||||
|
||||
local mstab = bling.layout.mstab
|
||||
local centered = bling.layout.centered
|
||||
local horizontal = bling.layout.horizontal
|
||||
local equal = bling.layout.equalarea
|
||||
local deck = bling.layout.deck
|
||||
|
||||
machi.editor.nested_layouts = {
|
||||
["0"] = deck,
|
||||
["1"] = awful.layout.suit.spiral,
|
||||
["2"] = awful.layout.suit.fair,
|
||||
["3"] = awful.layout.suit.fair.horizontal
|
||||
}
|
||||
|
||||
-- Set the layouts
|
||||
|
||||
tag.connect_signal("request::default_layouts", function()
|
||||
awful.layout.append_default_layouts({
|
||||
awful.layout.suit.tile, awful.layout.suit.floating, centered, mstab,
|
||||
horizontal, machi.default_layout, equal, deck
|
||||
})
|
||||
end)
|
||||
|
||||
-- Layout List Widget ---------------------------------------------------------
|
||||
|
||||
-- List
|
||||
local ll = awful.widget.layoutlist {
|
||||
source = awful.widget.layoutlist.source.default_layouts, -- DOC_HIDE
|
||||
spacing = dpi(24),
|
||||
base_layout = wibox.widget {
|
||||
spacing = dpi(24),
|
||||
forced_num_cols = 4,
|
||||
layout = wibox.layout.grid.vertical
|
||||
},
|
||||
widget_template = {
|
||||
{
|
||||
{
|
||||
id = "icon_role",
|
||||
forced_height = dpi(68),
|
||||
forced_width = dpi(68),
|
||||
widget = wibox.widget.imagebox
|
||||
},
|
||||
margins = dpi(24),
|
||||
widget = wibox.container.margin
|
||||
},
|
||||
id = "background_role",
|
||||
forced_width = dpi(68),
|
||||
forced_height = dpi(68),
|
||||
widget = wibox.container.background
|
||||
}
|
||||
}
|
||||
|
||||
-- Popup
|
||||
local layout_popup = awful.popup {
|
||||
widget = wibox.widget {
|
||||
{ll, margins = dpi(24), widget = wibox.container.margin},
|
||||
bg = beautiful.xbackground,
|
||||
shape = helpers.rrect(beautiful.border_radius),
|
||||
border_color = beautiful.widget_border_color,
|
||||
border_width = beautiful.widget_border_width,
|
||||
widget = wibox.container.background
|
||||
},
|
||||
placement = awful.placement.centered,
|
||||
ontop = true,
|
||||
visible = false,
|
||||
bg = beautiful.xbackground .. "00"
|
||||
}
|
||||
|
||||
-- Key Bindings for Widget ----------------------------------------------------
|
||||
|
||||
function gears.table.iterate_value(t, value, step_size, filter, start_at)
|
||||
local k = gears.table.hasitem(t, value, true, start_at)
|
||||
if not k then return end
|
||||
|
||||
step_size = step_size or 1
|
||||
local new_key = gears.math.cycle(#t, k + step_size)
|
||||
|
||||
if filter and not filter(t[new_key]) then
|
||||
for i = 1, #t do
|
||||
local k2 = gears.math.cycle(#t, new_key + i)
|
||||
if filter(t[k2]) then return t[k2], k2 end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
return t[new_key], new_key
|
||||
end
|
||||
|
||||
awful.keygrabber {
|
||||
start_callback = function() layout_popup.visible = true end,
|
||||
stop_callback = function() layout_popup.visible = false end,
|
||||
export_keybindings = true,
|
||||
stop_event = "release",
|
||||
stop_key = {"Escape", "Super_L", "Super_R", "Mod4"},
|
||||
keybindings = {
|
||||
{
|
||||
{modkey, "Shift"}, " ", function()
|
||||
awful.layout.set(gears.table.iterate_value(ll.layouts,
|
||||
ll.current_layout, -1),
|
||||
nil)
|
||||
end
|
||||
}, {
|
||||
{modkey}, " ", function()
|
||||
awful.layout.set(gears.table.iterate_value(ll.layouts,
|
||||
ll.current_layout, 1),
|
||||
nil)
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-- Hide all windows when a splash is shown
|
||||
awesome.connect_signal("widgets::splash::visibility", function(vis)
|
||||
local t = screen.primary.selected_tag
|
||||
if vis then
|
||||
for idx, c in ipairs(t:clients()) do c.hidden = true end
|
||||
else
|
||||
for idx, c in ipairs(t:clients()) do c.hidden = false end
|
||||
end
|
||||
end)
|
||||
|
||||
-- EOF ------------------------------------------------------------------------
|
550
config/awesome/helpers.lua
Normal file
|
@ -0,0 +1,550 @@
|
|||
-- helpers.lua
|
||||
-- Functions that you use more than once and in different files would
|
||||
-- be nice to define here.
|
||||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
local beautiful = require("beautiful")
|
||||
local xresources = require("beautiful.xresources")
|
||||
local dpi = xresources.apply_dpi
|
||||
local wibox = require("wibox")
|
||||
local naughty = require("naughty")
|
||||
local helpers = {}
|
||||
|
||||
function helpers.contains(_table, _c)
|
||||
for _, c in ipairs(_table) do
|
||||
if _c == c then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function helpers.find(rule)
|
||||
local function matcher(c) return awful.rules.match(c, rule) end
|
||||
local clients = client.get()
|
||||
local findex = gears.table.hasitem(clients, client.focus) or 1
|
||||
local start = gears.math.cycle(#clients, findex + 1)
|
||||
|
||||
local matches = {}
|
||||
for c in awful.client.iterate(matcher, start) do
|
||||
matches[#matches + 1] = c
|
||||
end
|
||||
|
||||
return matches
|
||||
end
|
||||
|
||||
-- Adds a maximized mask to a screen
|
||||
function helpers.screen_mask(s, bg)
|
||||
local mask = wibox({
|
||||
visible = false,
|
||||
ontop = true,
|
||||
type = "splash",
|
||||
screen = s
|
||||
})
|
||||
awful.placement.maximize(mask)
|
||||
mask.bg = bg
|
||||
return mask
|
||||
end
|
||||
|
||||
function helpers.custom_shape(cr, width, height)
|
||||
cr:move_to(0, height / 25)
|
||||
cr:line_to(height / 25, 0)
|
||||
cr:line_to(width, 0)
|
||||
cr:line_to(width, height - height / 25)
|
||||
cr:line_to(width - height / 25, height)
|
||||
cr:line_to(0, height)
|
||||
cr:close_path()
|
||||
end
|
||||
|
||||
-- Resize gaps on the fly
|
||||
|
||||
helpers.resize_gaps = function(amt)
|
||||
local t = awful.screen.focused().selected_tag
|
||||
t.gap = t.gap + tonumber(amt)
|
||||
awful.layout.arrange(awful.screen.focused())
|
||||
end
|
||||
|
||||
-- Resize padding on the fly
|
||||
|
||||
helpers.resize_padding = function(amt)
|
||||
local s = awful.screen.focused()
|
||||
local l = s.padding.left
|
||||
local r = s.padding.right
|
||||
local t = s.padding.top
|
||||
local b = s.padding.bottom
|
||||
s.padding = {
|
||||
left = l + amt,
|
||||
right = r + amt,
|
||||
top = t + amt,
|
||||
bottom = b + amt
|
||||
}
|
||||
awful.layout.arrange(awful.screen.focused())
|
||||
end
|
||||
|
||||
-- Create rounded rectangle shape (in one line)
|
||||
|
||||
helpers.rrect = function(radius)
|
||||
return function(cr, width, height)
|
||||
gears.shape.rounded_rect(cr, width, height, radius)
|
||||
end
|
||||
end
|
||||
|
||||
-- Create pi
|
||||
|
||||
helpers.pie = function(width, height, start_angle, end_angle, radius)
|
||||
return function(cr)
|
||||
gears.shape.pie(cr, width, height, start_angle, end_angle, radius)
|
||||
end
|
||||
end
|
||||
|
||||
-- Create parallelogram
|
||||
|
||||
helpers.prgram = function(height, base)
|
||||
return function(cr, width)
|
||||
gears.shape.parallelogram(cr, width, height, base)
|
||||
end
|
||||
end
|
||||
|
||||
-- Create partially rounded rect
|
||||
|
||||
helpers.prrect = function(radius, tl, tr, br, bl)
|
||||
return function(cr, width, height)
|
||||
gears.shape.partially_rounded_rect(cr, width, height, tl, tr, br, bl,
|
||||
radius)
|
||||
end
|
||||
end
|
||||
|
||||
-- Create rounded bar
|
||||
|
||||
helpers.rbar = function(width, height)
|
||||
return function(cr)
|
||||
gears.shape.rounded_bar(cr, width, height)
|
||||
end
|
||||
end
|
||||
|
||||
-- Markup helper
|
||||
|
||||
function helpers.colorize_text(txt, fg)
|
||||
return "<span foreground='" .. fg .. "'>" .. txt .. "</span>"
|
||||
end
|
||||
|
||||
function helpers.client_menu_toggle()
|
||||
local instance = nil
|
||||
|
||||
return function()
|
||||
if instance and instance.wibox.visible then
|
||||
instance:hide()
|
||||
instance = nil
|
||||
else
|
||||
instance = awful.menu.clients({theme = {width = dpi(250)}})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Escapes a string so that it can be displayed inside pango markup
|
||||
-- tags. Modified from:
|
||||
-- https://github.com/kernelsauce/turbo/blob/master/turbo/escape.lua
|
||||
function helpers.pango_escape(s)
|
||||
return (string.gsub(s, "[&<>]",
|
||||
{["&"] = "&", ["<"] = "<", [">"] = ">"}))
|
||||
end
|
||||
|
||||
function helpers.vertical_pad(height)
|
||||
return wibox.widget {
|
||||
forced_height = height,
|
||||
layout = wibox.layout.fixed.vertical
|
||||
}
|
||||
end
|
||||
|
||||
function helpers.horizontal_pad(width)
|
||||
return wibox.widget {
|
||||
forced_width = width,
|
||||
layout = wibox.layout.fixed.horizontal
|
||||
}
|
||||
end
|
||||
|
||||
-- Maximizes client and also respects gaps
|
||||
function helpers.maximize(c)
|
||||
c.maximized = not c.maximized
|
||||
if c.maximized then
|
||||
awful.placement.maximize(c, {
|
||||
honor_padding = true,
|
||||
honor_workarea = true,
|
||||
margins = beautiful.useless_gap * 2
|
||||
})
|
||||
|
||||
end
|
||||
c:raise()
|
||||
end
|
||||
|
||||
function helpers.move_to_edge(c, direction)
|
||||
-- local workarea = awful.screen.focused().workarea
|
||||
-- local client_geometry = c:geometry()
|
||||
if direction == "up" then
|
||||
local old_x = c:geometry().x
|
||||
awful.placement.top(c, {
|
||||
honor_padding = true,
|
||||
honor_workarea = true,
|
||||
honor_padding = true
|
||||
})
|
||||
c.x = old_x
|
||||
-- c:geometry({ nil, y = workarea.y + beautiful.screen_margin * 2, nil, nil })
|
||||
elseif direction == "down" then
|
||||
local old_x = c:geometry().x
|
||||
awful.placement.bottom(c, {
|
||||
honor_padding = true,
|
||||
honor_workarea = true,
|
||||
honor_padding = true
|
||||
})
|
||||
c.x = old_x
|
||||
-- c:geometry({ nil, y = workarea.height + workarea.y - client_geometry.height - beautiful.screen_margin * 2 - beautiful.border_width * 2, nil, nil })
|
||||
elseif direction == "left" then
|
||||
local old_y = c:geometry().y
|
||||
awful.placement.left(c, {
|
||||
honor_padding = true,
|
||||
honor_workarea = true,
|
||||
honor_padding = true
|
||||
})
|
||||
c.y = old_y
|
||||
-- c:geometry({ x = workarea.x + beautiful.screen_margin * 2, nil, nil, nil })
|
||||
elseif direction == "right" then
|
||||
local old_y = c:geometry().y
|
||||
awful.placement.right(c, {
|
||||
honor_padding = true,
|
||||
honor_workarea = true,
|
||||
honor_padding = true
|
||||
})
|
||||
c.y = old_y
|
||||
-- c:geometry({ x = workarea.width + workarea.x - client_geometry.width - beautiful.screen_margin * 2 - beautiful.border_width * 2, nil, nil, nil })
|
||||
end
|
||||
end
|
||||
|
||||
local double_tap_timer = nil
|
||||
function helpers.single_double_tap(single_tap_function, double_tap_function)
|
||||
if double_tap_timer then
|
||||
double_tap_timer:stop()
|
||||
double_tap_timer = nil
|
||||
double_tap_function()
|
||||
-- naughty.notify({text = "We got a double tap"})
|
||||
return
|
||||
end
|
||||
|
||||
double_tap_timer = gears.timer.start_new(0.20, function()
|
||||
double_tap_timer = nil
|
||||
-- naughty.notify({text = "We got a single tap"})
|
||||
if single_tap_function then single_tap_function() end
|
||||
return false
|
||||
end)
|
||||
end
|
||||
|
||||
-- Used as a custom command in rofi to move a window into the current tag
|
||||
-- instead of following it.
|
||||
-- Rofi has access to the X window id of the client.
|
||||
function helpers.rofi_move_client_here(window)
|
||||
local win = function(c) return awful.rules.match(c, {window = window}) end
|
||||
|
||||
for c in awful.client.iterate(win) do
|
||||
c.minimized = false
|
||||
c:move_to_tag(mouse.screen.selected_tag)
|
||||
client.focus = c
|
||||
c:raise()
|
||||
end
|
||||
end
|
||||
|
||||
-- Add a hover cursor to a widget by changing the cursor on
|
||||
-- mouse::enter and mouse::leave
|
||||
-- You can find the names of the available cursors by opening any
|
||||
-- cursor theme and looking in the "cursors folder"
|
||||
-- For example: "hand1" is the cursor that appears when hovering over
|
||||
-- links
|
||||
function helpers.add_hover_cursor(w, hover_cursor)
|
||||
local original_cursor = "left_ptr"
|
||||
|
||||
w:connect_signal("mouse::enter", function()
|
||||
local w = _G.mouse.current_wibox
|
||||
if w then w.cursor = hover_cursor end
|
||||
end)
|
||||
|
||||
w:connect_signal("mouse::leave", function()
|
||||
local w = _G.mouse.current_wibox
|
||||
if w then w.cursor = original_cursor end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Tag back and forth:
|
||||
-- If you try to focus the tag you are already at, go back to the previous tag.
|
||||
-- Useful for quick switching after for example checking an incoming chat
|
||||
-- message at tag 2 and coming back to your work at tag 1 with the same
|
||||
-- keypress.
|
||||
-- Also focuses urgent clients if they exist in the tag. This fixes the issue
|
||||
-- (visual mismatch) where after switching to a tag which includes an urgent
|
||||
-- client, the urgent client is unfocused but still covers all other windows
|
||||
-- (even the currently focused window).
|
||||
function helpers.tag_back_and_forth(tag_index)
|
||||
local s = mouse.screen
|
||||
local tag = s.tags[tag_index]
|
||||
if tag then
|
||||
if tag == s.selected_tag then
|
||||
awful.tag.history.restore()
|
||||
else
|
||||
tag:view_only()
|
||||
end
|
||||
|
||||
local urgent_clients = function(c)
|
||||
return awful.rules.match(c, {urgent = true, first_tag = tag})
|
||||
end
|
||||
|
||||
for c in awful.client.iterate(urgent_clients) do
|
||||
client.focus = c
|
||||
c:raise()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Resize DWIM (Do What I Mean)
|
||||
-- Resize client or factor
|
||||
-- Constants --
|
||||
local floating_resize_amount = dpi(20)
|
||||
local tiling_resize_factor = 0.05
|
||||
---------------
|
||||
function helpers.resize_dwim(c, direction)
|
||||
if awful.layout.get(mouse.screen) == awful.layout.suit.floating or
|
||||
(c and c.floating) then
|
||||
if direction == "up" then
|
||||
c:relative_move(0, 0, 0, -floating_resize_amount)
|
||||
elseif direction == "down" then
|
||||
c:relative_move(0, 0, 0, floating_resize_amount)
|
||||
elseif direction == "left" then
|
||||
c:relative_move(0, 0, -floating_resize_amount, 0)
|
||||
elseif direction == "right" then
|
||||
c:relative_move(0, 0, floating_resize_amount, 0)
|
||||
end
|
||||
else
|
||||
if direction == "up" then
|
||||
awful.client.incwfact(-tiling_resize_factor)
|
||||
elseif direction == "down" then
|
||||
awful.client.incwfact(tiling_resize_factor)
|
||||
elseif direction == "left" then
|
||||
awful.tag.incmwfact(-tiling_resize_factor)
|
||||
elseif direction == "right" then
|
||||
awful.tag.incmwfact(tiling_resize_factor)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Move client to screen edge, respecting the screen workarea
|
||||
function helpers.move_to_edge(c, direction)
|
||||
local workarea = awful.screen.focused().workarea
|
||||
if direction == "up" then
|
||||
c:geometry({nil, y = workarea.y + beautiful.useless_gap * 2, nil, nil})
|
||||
elseif direction == "down" then
|
||||
c:geometry({
|
||||
nil,
|
||||
y = workarea.height + workarea.y - c:geometry().height -
|
||||
beautiful.useless_gap * 2 - beautiful.border_width * 2,
|
||||
nil,
|
||||
nil
|
||||
})
|
||||
elseif direction == "left" then
|
||||
c:geometry({x = workarea.x + beautiful.useless_gap * 2, nil, nil, nil})
|
||||
elseif direction == "right" then
|
||||
c:geometry({
|
||||
x = workarea.width + workarea.x - c:geometry().width -
|
||||
beautiful.useless_gap * 2 - beautiful.border_width * 2,
|
||||
nil,
|
||||
nil,
|
||||
nil
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Move client DWIM (Do What I Mean)
|
||||
-- Move to edge if the client / layout is floating
|
||||
-- Swap by index if maximized
|
||||
-- Else swap client by direction
|
||||
function helpers.move_client_dwim(c, direction)
|
||||
if c.floating or
|
||||
(awful.layout.get(mouse.screen) == awful.layout.suit.floating) then
|
||||
helpers.move_to_edge(c, direction)
|
||||
elseif awful.layout.get(mouse.screen) == awful.layout.suit.max then
|
||||
if direction == "up" or direction == "left" then
|
||||
awful.client.swap.byidx(-1, c)
|
||||
elseif direction == "down" or direction == "right" then
|
||||
awful.client.swap.byidx(1, c)
|
||||
end
|
||||
else
|
||||
awful.client.swap.bydirection(direction, c, nil)
|
||||
end
|
||||
end
|
||||
|
||||
-- Make client floating and snap to the desired edge
|
||||
function helpers.float_and_edge_snap(c, direction)
|
||||
-- if not c.floating then
|
||||
-- c.floating = true
|
||||
-- end
|
||||
naughty.notify({text = "double tap"})
|
||||
c.floating = true
|
||||
local workarea = awful.screen.focused().workarea
|
||||
if direction == "up" then
|
||||
local axis = 'horizontally'
|
||||
local f = awful.placement.scale + awful.placement.top +
|
||||
(axis and awful.placement['maximize_' .. axis] or nil)
|
||||
local geo = f(client.focus, {
|
||||
honor_padding = true,
|
||||
honor_workarea = true,
|
||||
to_percent = 0.5
|
||||
})
|
||||
elseif direction == "down" then
|
||||
local axis = 'horizontally'
|
||||
local f = awful.placement.scale + awful.placement.bottom +
|
||||
(axis and awful.placement['maximize_' .. axis] or nil)
|
||||
local geo = f(client.focus, {
|
||||
honor_padding = true,
|
||||
honor_workarea = true,
|
||||
to_percent = 0.5
|
||||
})
|
||||
elseif direction == "left" then
|
||||
local axis = 'vertically'
|
||||
local f = awful.placement.scale + awful.placement.left +
|
||||
(axis and awful.placement['maximize_' .. axis] or nil)
|
||||
local geo = f(client.focus, {
|
||||
honor_padding = true,
|
||||
honor_workarea = true,
|
||||
to_percent = 0.5
|
||||
})
|
||||
elseif direction == "right" then
|
||||
local axis = 'vertically'
|
||||
local f = awful.placement.scale + awful.placement.right +
|
||||
(axis and awful.placement['maximize_' .. axis] or nil)
|
||||
local geo = f(client.focus, {
|
||||
honor_padding = true,
|
||||
honor_workarea = true,
|
||||
to_percent = 0.5
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Rounds a number to any number of decimals
|
||||
function helpers.round(number, decimals)
|
||||
local power = 10 ^ decimals
|
||||
return math.floor(number * power) / power
|
||||
end
|
||||
|
||||
function helpers.fake_escape()
|
||||
root.fake_input('key_press', "Escape")
|
||||
root.fake_input('key_release', "Escape")
|
||||
end
|
||||
|
||||
function helpers.run_or_raise(match, move, spawn_cmd, spawn_args)
|
||||
local matcher = function(c) return awful.rules.match(c, match) end
|
||||
|
||||
-- Find and raise
|
||||
local found = false
|
||||
for c in awful.client.iterate(matcher) do
|
||||
found = true
|
||||
c.minimized = false
|
||||
if move then
|
||||
c:move_to_tag(mouse.screen.selected_tag)
|
||||
client.focus = c
|
||||
c:raise()
|
||||
else
|
||||
c:jump_to()
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
-- Spawn if not found
|
||||
if not found then awful.spawn(spawn_cmd, spawn_args) end
|
||||
end
|
||||
|
||||
function helpers.pad(size)
|
||||
local str = ""
|
||||
for i = 1, size do str = str .. " " end
|
||||
local pad = wibox.widget.textbox(str)
|
||||
return pad
|
||||
end
|
||||
|
||||
function helpers.float_and_resize(c, width, height)
|
||||
c.width = width
|
||||
c.height = height
|
||||
awful.placement.centered(c, {honor_workarea = true, honor_padding = true})
|
||||
awful.client.property.set(c, 'floating_geometry', c:geometry())
|
||||
c.floating = true
|
||||
c:raise()
|
||||
end
|
||||
|
||||
-- Useful for periodically checking the output of a command that
|
||||
-- requires internet access.
|
||||
-- Ensures that `command` will be run EXACTLY once during the desired
|
||||
-- `interval`, even if awesome restarts multiple times during this time.
|
||||
-- Saves output in `output_file` and checks its last modification
|
||||
-- time to determine whether to run the command again or not.
|
||||
-- Passes the output of `command` to `callback` function.
|
||||
function helpers.remote_watch(command, interval, output_file, callback)
|
||||
local run_the_thing = function()
|
||||
-- Pass output to callback AND write it to file
|
||||
awful.spawn.easy_async_with_shell(command.." | tee "..output_file, function(out) callback(out) end)
|
||||
end
|
||||
|
||||
local timer
|
||||
timer = gears.timer {
|
||||
timeout = interval,
|
||||
call_now = true,
|
||||
autostart = true,
|
||||
single_shot = false,
|
||||
callback = function()
|
||||
awful.spawn.easy_async_with_shell("date -r "..output_file.." +%s", function(last_update, _, __, exitcode)
|
||||
-- Probably the file does not exist yet (first time
|
||||
-- running after reboot)
|
||||
if exitcode == 1 then
|
||||
run_the_thing()
|
||||
return
|
||||
end
|
||||
|
||||
local diff = os.time() - tonumber(last_update)
|
||||
if diff >= interval then
|
||||
run_the_thing()
|
||||
else
|
||||
-- Pass the date saved in the file since it is fresh enough
|
||||
awful.spawn.easy_async_with_shell("cat "..output_file, function(out) callback(out) end)
|
||||
|
||||
-- Schedule an update for when the remaining time to complete the interval passes
|
||||
timer:stop()
|
||||
gears.timer.start_new(interval - diff, function()
|
||||
run_the_thing()
|
||||
timer:again()
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
-- Volume Control
|
||||
function helpers.volume_control(step)
|
||||
local cmd
|
||||
if step == 0 then
|
||||
cmd = "pactl set-sink-mute @DEFAULT_SINK@ toggle"
|
||||
else
|
||||
sign = step > 0 and "+" or ""
|
||||
cmd = "pactl set-sink-mute @DEFAULT_SINK@ 0 && pactl set-sink-volume @DEFAULT_SINK@ "..sign..tostring(step).."%"
|
||||
end
|
||||
awful.spawn.with_shell(cmd)
|
||||
end
|
||||
|
||||
function helpers.music_control(state)
|
||||
local cmd
|
||||
if state == "toggle" then
|
||||
cmd = "playerctl -p spotify,mpd play-pause"
|
||||
elseif state == "prev" then
|
||||
cmd = "playerctl -p spotify,mpd previous"
|
||||
elseif state == "next" then
|
||||
cmd = "playerctl -p spotify,mpd next"
|
||||
end
|
||||
awful.spawn.with_shell(cmd)
|
||||
end
|
||||
|
||||
|
||||
return helpers
|
||||
|
||||
-- EOF ------------------------------------------------------------------------
|
BIN
config/awesome/icons/awesome.png
Normal file
After Width: | Height: | Size: 403 B |
BIN
config/awesome/icons/notification.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
config/awesome/icons/taglist/dot.png
Normal file
After Width: | Height: | Size: 519 B |
45
config/awesome/icons/taglist/dot.svg
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 31.955 31.955" style="enable-background:new 0 0 31.955 31.955;" xml:space="preserve">
|
||||
<g>
|
||||
<path style="fill:#030104;" d="M27.25,4.655C20.996-1.571,10.88-1.546,4.656,4.706C-1.571,10.96-1.548,21.076,4.705,27.3
|
||||
c6.256,6.226,16.374,6.203,22.597-0.051C33.526,20.995,33.505,10.878,27.25,4.655z"/>
|
||||
<path style="fill:#030104;" d="M13.288,23.896l-1.768,5.207c2.567,0.829,5.331,0.886,7.926,0.17l-0.665-5.416
|
||||
C17.01,24.487,15.067,24.5,13.288,23.896z M8.12,13.122l-5.645-0.859c-0.741,2.666-0.666,5.514,0.225,8.143l5.491-1.375
|
||||
C7.452,17.138,7.426,15.029,8.12,13.122z M28.763,11.333l-4.965,1.675c0.798,2.106,0.716,4.468-0.247,6.522l5.351,0.672
|
||||
C29.827,17.319,29.78,14.193,28.763,11.333z M11.394,2.883l1.018,5.528c2.027-0.954,4.356-1.05,6.442-0.288l1.583-5.137
|
||||
C17.523,1.94,14.328,1.906,11.394,2.883z"/>
|
||||
<circle style="fill:#030104;" cx="15.979" cy="15.977" r="6.117"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
BIN
config/awesome/icons/taglist/ghost.png
Normal file
After Width: | Height: | Size: 508 B |
45
config/awesome/icons/taglist/ghost.svg
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 333.561 333.561" style="enable-background:new 0 0 333.561 333.561;" xml:space="preserve">
|
||||
<path d="M295.023,70.021c-10.653-20.379-25.378-36.617-43.763-48.265C228.47,7.32,200.047,0,166.78,0s-61.69,7.32-84.48,21.757
|
||||
c-18.386,11.647-33.11,27.886-43.763,48.265c-17.745,33.944-18.257,67.888-18.257,71.646v182.064c0,1.4,0.001,2.673,0.036,3.596
|
||||
c0.214,5.784,4.052,6.233,5.223,6.233c2.327,0,3.61-1.173,7.316-4.88l0.213-0.214l34.969-44.641l34.895,44.274l0.392,0.441
|
||||
c2.747,2.745,7.7,4.738,11.779,4.738h3.416c4.08,0,9.033-1.993,11.779-4.739l0.21-0.21l35.064-44.56l34.909,44.311l0.393,0.441
|
||||
c2.756,2.756,7.71,4.756,11.779,4.756h3.416c4.08,0,9.034-1.993,11.78-4.739l0.21-0.21l35.063-44.559l34.906,44.444l0.396,0.447
|
||||
c1.47,1.47,5.23,4.888,8.402,4.888c3.98,0,6.452-3.763,6.452-9.82V141.668C313.28,137.909,312.768,103.966,295.023,70.021z
|
||||
M109.354,170.146c-17.999,0-32.59-14.591-32.59-32.59s14.591-32.59,32.59-32.59s32.591,14.591,32.591,32.59
|
||||
S127.353,170.146,109.354,170.146z M223.354,170.146c-17.999,0-32.59-14.591-32.59-32.59s14.591-32.59,32.59-32.59
|
||||
s32.591,14.591,32.591,32.59S241.353,170.146,223.354,170.146z"/>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
BIN
config/awesome/icons/taglist/pacman.png
Normal file
After Width: | Height: | Size: 634 B |
4
config/awesome/icons/taglist/pacman.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="571.11" width="541.6">
|
||||
<path style="fill:#ffcc00" d="M535.441,412.339A280.868,280.868 0 1,1 536.186,161.733L284.493,286.29Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 238 B |
3
config/awesome/icons/titlebar/close.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="48" height="64" viewBox="0 0 48 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="32.4492" y="20" width="8" height="28" rx="2" transform="rotate(50 32.4492 20)" fill="#DF5B61"/>
|
||||
</svg>
|
After Width: | Height: | Size: 208 B |
3
config/awesome/icons/titlebar/default.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="48" height="64" viewBox="0 0 48 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="32.4492" y="20" width="8" height="28" rx="2" transform="rotate(50 32.4492 20)" fill="#1C252C"/>
|
||||
</svg>
|
After Width: | Height: | Size: 208 B |
3
config/awesome/icons/titlebar/maximized.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="48" height="64" viewBox="0 0 48 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="32.4492" y="20" width="8" height="28" rx="2" transform="rotate(50 32.4492 20)" fill="#BC83E3"/>
|
||||
</svg>
|
After Width: | Height: | Size: 208 B |
3
config/awesome/icons/titlebar/minimize.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="48" height="64" viewBox="0 0 48 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="32.4492" y="20" width="8" height="28" rx="2" transform="rotate(50 32.4492 20)" fill="#6791C9"/>
|
||||
</svg>
|
After Width: | Height: | Size: 208 B |
BIN
config/awesome/images/bg.png
Normal file
After Width: | Height: | Size: 5.6 MiB |
BIN
config/awesome/images/no_music.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
config/awesome/images/pfp.png
Normal file
After Width: | Height: | Size: 12 KiB |
135
config/awesome/module/better-resize.lua
Normal file
|
@ -0,0 +1,135 @@
|
|||
local capi = {
|
||||
client = client,
|
||||
mouse = mouse,
|
||||
screen = screen,
|
||||
mousegrabber = mousegrabber
|
||||
}
|
||||
local awful = require("awful")
|
||||
|
||||
local function mouse_resize_handler(m, c)
|
||||
awful.client.incwfact(0, c) -- needed to fix normalization at start
|
||||
local start = m(capi.mouse.coords())
|
||||
local x, y = start.x, start.y
|
||||
local wa = m(c.screen.workarea)
|
||||
local idx = awful.client.idx(c)
|
||||
local c_above, c_below
|
||||
local idx_above, idx_below
|
||||
local wfact_above, wfact_below
|
||||
local jump_to = {x = x, y = y}
|
||||
local move_mwfact = false
|
||||
|
||||
do
|
||||
local g = m(c:geometry())
|
||||
|
||||
local v_border = 0.2 * g.height
|
||||
|
||||
if idx.idx > 1 and y >= g.y and y <= g.y + v_border then
|
||||
-- we are near the top edge of the window
|
||||
c_above = awful.client.next(-1, c)
|
||||
c_below = c
|
||||
jump_to.y = g.y
|
||||
idx_above = idx.idx - 1
|
||||
idx_below = idx.idx
|
||||
elseif idx.idx < (idx.num) and y >= g.y + g.height - v_border then
|
||||
-- we are near the bottom edge of the window
|
||||
c_above = c
|
||||
c_below = awful.client.next(1, c)
|
||||
idx_above = idx.idx
|
||||
idx_below = idx.idx + 1
|
||||
jump_to.y = g.y + g.height
|
||||
end
|
||||
|
||||
local mw_split = wa.x + wa.width *
|
||||
c.screen.selected_tag.master_width_factor
|
||||
|
||||
if math.abs(mw_split - x) > wa.width / 6 then
|
||||
move_mwfact = false
|
||||
else
|
||||
move_mwfact = true
|
||||
jump_to.x = mw_split
|
||||
end
|
||||
end
|
||||
|
||||
if idx_above then
|
||||
local t = c.screen.selected_tag
|
||||
local data = t.windowfact or {}
|
||||
local colfact = data[idx.col] or {}
|
||||
wfact_above = colfact[idx_above] or 1
|
||||
wfact_below = colfact[idx_below] or 1
|
||||
end
|
||||
|
||||
if idx_above and move_mwfact then
|
||||
cursor = "cross"
|
||||
elseif idx_above then
|
||||
cursor = m({y = "sb_v_double_arrow", x = "sb_h_double_arrow"}).y
|
||||
elseif move_mwfact then
|
||||
cursor = m({y = "sb_v_double_arrow", x = "sb_h_double_arrow"}).x
|
||||
else
|
||||
return false
|
||||
end
|
||||
|
||||
capi.mouse.coords(m(jump_to))
|
||||
|
||||
capi.mousegrabber.run(function(_mouse)
|
||||
if not c.valid then return false end
|
||||
|
||||
local pressed = false
|
||||
for _, v in ipairs(_mouse.buttons) do
|
||||
if v then
|
||||
pressed = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
_mouse = m(_mouse)
|
||||
|
||||
if pressed then
|
||||
if move_mwfact then
|
||||
c.screen.selected_tag.master_width_factor =
|
||||
math.min(math.max((_mouse.x - wa.x) / wa.width, 0.01), 0.99)
|
||||
end
|
||||
|
||||
if idx_above then
|
||||
local factor_delta = (_mouse.y - jump_to.y) / wa.height
|
||||
|
||||
if factor_delta < 0 then
|
||||
factor_delta = math.max(factor_delta, -(wfact_above - 0.05))
|
||||
else
|
||||
factor_delta = math.min(factor_delta, wfact_below - 0.05)
|
||||
end
|
||||
|
||||
local t = c.screen.selected_tag
|
||||
local data = t.windowfact or {}
|
||||
local colfact = data[idx.col] or {}
|
||||
colfact[idx_above] = wfact_above + factor_delta
|
||||
colfact[idx_below] = wfact_below - factor_delta
|
||||
awful.client.incwfact(0, c_above) -- just in case
|
||||
end
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end, cursor)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
awful.layout.suit.tile.mouse_resize_handler =
|
||||
function(c) return mouse_resize_handler(function(x) return x end, c) end
|
||||
awful.layout.suit.tile.bottom.mouse_resize_handler =
|
||||
function(c)
|
||||
return mouse_resize_handler(function(q)
|
||||
return {x = q.y, y = q.x, width = q.height, height = q.width}
|
||||
end, c)
|
||||
end
|
||||
|
||||
-- local old_coords = mouse.coords
|
||||
|
||||
-- mouse.coords = function(...)
|
||||
-- if select(1, ...) and not(select(1, ...).blah) then
|
||||
-- print("set mouse!!!")
|
||||
-- print(debug.traceback())
|
||||
|
||||
-- end
|
||||
-- return old_coords(...)
|
||||
-- end
|
24
config/awesome/module/bling/.editorconfig
Normal file
|
@ -0,0 +1,24 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.lua]
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
max_line_length = 80
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[*.{html,css}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
9
config/awesome/module/bling/AUTHORS.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
The following developers have contributed major code to bling:
|
||||
|
||||
* [Nooo37](https://github.com/Nooo37)
|
||||
* [JavaCafe01](https://github.com/JavaCafe01)
|
||||
* [Grumph](https://github.com/Grumph)
|
||||
* [Bysmutheye](https://github.com/Bysmutheye)
|
||||
* [HumblePresent](https://github.com/HumblePresent)
|
||||
* [Kasper24](https://github.com/Kasper24)
|
||||
* [undefinedDarkness](https://github.com/undefinedDarkness)
|
2
config/awesome/module/bling/CODEOWNERS
Normal file
|
@ -0,0 +1,2 @@
|
|||
/module/* @Nooo37
|
||||
/widget/* @JavaCafe01
|
21
config/awesome/module/bling/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 BlingCorp
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
41
config/awesome/module/bling/README.md
Normal file
|
@ -0,0 +1,41 @@
|
|||
<p align="center">
|
||||
<img src="/images/bling_banner.png" />
|
||||
</p>
|
||||
<h1 align="center"></h1>
|
||||
|
||||
All documentation, instructions, and previews are [here](https://blingcorp.github.io/bling/).
|
||||
|
||||
## Features
|
||||
|
||||
- Layouts
|
||||
- mstab (master-slave tab layout)
|
||||
- centered
|
||||
- vertical
|
||||
- horizontal
|
||||
- equalarea
|
||||
- Modules
|
||||
- Flash Focus
|
||||
- Tabbed container
|
||||
- Tiled Wallpaper
|
||||
- Wallpaper Easy Setup
|
||||
- Window Swallowing
|
||||
- Scratchpad
|
||||
- Signals
|
||||
- Playerctl
|
||||
- Widgets
|
||||
- Tag Preview
|
||||
- Task Preview
|
||||
|
||||
All naming credit goes to [JavaCafe01](https://github.com/JavaCafe01).
|
||||
|
||||
### Dependencies
|
||||
|
||||
In order to use the `tabbed` modules `pick` function, you need to install `xwininfo`.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome 💛
|
||||
|
||||
Before requesting changes, makes sure that your editor has an "editorconfig" extension installed, this will use our code style everytime when you edit in the `bling` folder.
|
||||
|
||||
When adding a layout/module/signal/widget, please add theme variables for customization and add the according documentation under `docs`.
|
63
config/awesome/module/bling/bling-dev-1.rockspec
Normal file
|
@ -0,0 +1,63 @@
|
|||
package = "bling"
|
||||
version = "dev-1"
|
||||
|
||||
source = {
|
||||
url = "git://github.com/BlingCorp/bling",
|
||||
branch = "master",
|
||||
}
|
||||
|
||||
description = {
|
||||
summary = "Utilities for the AwesomeWM",
|
||||
detailed = [[
|
||||
This module extends the Awesome window manager with alternative layouts,
|
||||
flash focus, tabbing, a simple tiling wallpaper generator, a declarative
|
||||
wallpaper setter, window swallowing and a playerctl signal.
|
||||
]],
|
||||
homepage = "https://github.com/BlingCorp/bling",
|
||||
license = "MIT",
|
||||
}
|
||||
|
||||
dependencies = {
|
||||
"lua >= 5.1",
|
||||
}
|
||||
|
||||
build = {
|
||||
type = "builtin",
|
||||
modules = {
|
||||
["bling"] = "init.lua",
|
||||
["bling.helpers"] = "helpers/init.lua",
|
||||
["bling.helpers.client"] = "helpers/client.lua",
|
||||
["bling.helpers.color"] = "helpers/color.lua",
|
||||
["bling.helpers.filesystem"] = "helpers/filesystem.lua",
|
||||
["bling.helpers.shape"] = "helpers/shape.lua",
|
||||
["bling.helpers.time"] = "helpers/time.lua",
|
||||
["bling.layout"] = "layout/init.lua",
|
||||
["bling.layout.centered"] = "layout/centered.lua",
|
||||
["bling.layout.deck"] = "layout/deck.lua",
|
||||
["bling.layout.equalarea"] = "layout/equalarea.lua",
|
||||
["bling.layout.horizontal"] = "layout/horizontal.lua",
|
||||
["bling.layout.mstab"] = "layout/mstab.lua",
|
||||
["bling.layout.vertical"] = "layout/vertical.lua",
|
||||
["bling.module"] = "module/init.lua",
|
||||
["bling.module.flash_focus"] = "module/flash_focus.lua",
|
||||
["bling.module.scratchpad"] = "module/scratchpad.lua",
|
||||
["bling.module.tabbed"] = "module/tabbed.lua",
|
||||
["bling.module.tiled_wallpaper"] = "module/tiled_wallpaper.lua",
|
||||
["bling.module.wallpaper"] = "module/wallpaper.lua",
|
||||
["bling.module.window_swallowing"] = "module/window_swallowing.lua",
|
||||
["bling.signal"] = "signal/init.lua",
|
||||
["bling.signal.playerctl"] = "signal/playerctl/init.lua",
|
||||
["bling.signal.playerctl.playerctl_cli"] = "signal/playerctl/playerctl_cli.lua",
|
||||
["bling.signal.playerctl.playerctl_lib"] = "signal/playerctl/playerctl_lib.lua",
|
||||
["bling.widget"] = "widget/init.lua",
|
||||
["bling.widget.tabbar.boxes"] = "widget/tabbar/boxes.lua",
|
||||
["bling.widget.tabbar.default"] = "widget/tabbar/default.lua",
|
||||
["bling.widget.tabbar.modern"] = "widget/tabbar/modern.lua",
|
||||
["bling.widget.tabbed_misc"] = "widget/tabbed_misc/init.lua",
|
||||
["bling.widget.tabbed_misc.custom_tasklist"] = "widget/tabbed_misc/custom_tasklist.lua",
|
||||
["bling.widget.tabbed_misc.titlebar_indicator"] = "widget/tabbed_misc/titlebar_indicator.lua",
|
||||
["bling.widget.tag_preview"] = "widget/tag_preview.lua",
|
||||
["bling.widget.task_preview"] = "widget/task_preview.lua",
|
||||
["bling.widget.window_switcher"] = "widget/window_switcher.lua",
|
||||
},
|
||||
}
|
127
config/awesome/module/bling/helpers/client.lua
Normal file
|
@ -0,0 +1,127 @@
|
|||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
|
||||
local _client = {}
|
||||
|
||||
--- Turn off passed client
|
||||
-- Remove current tag from window's tags
|
||||
--
|
||||
-- @param c A client
|
||||
function _client.turn_off(c, current_tag)
|
||||
if current_tag == nil then
|
||||
current_tag = c.screen.selected_tag
|
||||
end
|
||||
local ctags = {}
|
||||
for k, tag in pairs(c:tags()) do
|
||||
if tag ~= current_tag then
|
||||
table.insert(ctags, tag)
|
||||
end
|
||||
end
|
||||
c:tags(ctags)
|
||||
c.sticky = false
|
||||
end
|
||||
|
||||
--- Turn on passed client (add current tag to window's tags)
|
||||
--
|
||||
-- @param c A client
|
||||
function _client.turn_on(c)
|
||||
local current_tag = c.screen.selected_tag
|
||||
ctags = { current_tag }
|
||||
for k, tag in pairs(c:tags()) do
|
||||
if tag ~= current_tag then
|
||||
table.insert(ctags, tag)
|
||||
end
|
||||
end
|
||||
c:tags(ctags)
|
||||
c:raise()
|
||||
client.focus = c
|
||||
end
|
||||
|
||||
--- Sync two clients
|
||||
--
|
||||
-- @param to_c The client to which to write all properties
|
||||
-- @param from_c The client from which to read all properties
|
||||
function _client.sync(to_c, from_c)
|
||||
if not from_c or not to_c then
|
||||
return
|
||||
end
|
||||
if not from_c.valid or not to_c.valid then
|
||||
return
|
||||
end
|
||||
if from_c.modal then
|
||||
return
|
||||
end
|
||||
to_c.floating = from_c.floating
|
||||
to_c.maximized = from_c.maximized
|
||||
to_c.above = from_c.above
|
||||
to_c.below = from_c.below
|
||||
to_c:geometry(from_c:geometry())
|
||||
-- TODO: Should also copy over the position in a tiling layout
|
||||
end
|
||||
|
||||
--- Checks whether the passed client is a childprocess of a given process ID
|
||||
--
|
||||
-- @param c A client
|
||||
-- @param pid The process ID
|
||||
-- @return True if the passed client is a childprocess of the given PID otherwise false
|
||||
function _client.is_child_of(c, pid)
|
||||
-- io.popen is normally discouraged. Should probably be changed
|
||||
if not c or not c.valid then
|
||||
return false
|
||||
end
|
||||
if tostring(c.pid) == tostring(pid) then
|
||||
return true
|
||||
end
|
||||
local pid_cmd = [[pstree -T -p -a -s ]]
|
||||
.. tostring(c.pid)
|
||||
.. [[ | sed '2q;d' | grep -o '[0-9]*$' | tr -d '\n']]
|
||||
local handle = io.popen(pid_cmd)
|
||||
local parent_pid = handle:read("*a")
|
||||
handle:close()
|
||||
return tostring(parent_pid) == tostring(pid)
|
||||
or tostring(parent_pid) == tostring(c.pid)
|
||||
end
|
||||
|
||||
--- Finds all clients that satisfy the passed rule
|
||||
--
|
||||
-- @param rule The rule to be searched for
|
||||
-- @retrun A list of clients that match the given rule
|
||||
function _client.find(rule)
|
||||
local function matcher(c)
|
||||
return awful.rules.match(c, rule)
|
||||
end
|
||||
local clients = client.get()
|
||||
local findex = gears.table.hasitem(clients, client.focus) or 1
|
||||
local start = gears.math.cycle(#clients, findex + 1)
|
||||
|
||||
local matches = {}
|
||||
for c in awful.client.iterate(matcher, start) do
|
||||
matches[#matches + 1] = c
|
||||
end
|
||||
|
||||
return matches
|
||||
end
|
||||
|
||||
--- Gets the next client by direction from the focused one
|
||||
--
|
||||
-- @param direction it the direction as a string ("up", "down", "left" or "right")
|
||||
-- @retrun the client in the given direction starting at the currently focused one, nil otherwise
|
||||
function _client.get_by_direction(direction)
|
||||
local sel = client.focus
|
||||
if not sel then
|
||||
return nil
|
||||
end
|
||||
local cltbl = sel.screen:get_clients()
|
||||
local geomtbl = {}
|
||||
for i, cl in ipairs(cltbl) do
|
||||
geomtbl[i] = cl:geometry()
|
||||
end
|
||||
local target = gears.geometry.rectangle.get_in_direction(
|
||||
direction,
|
||||
geomtbl,
|
||||
sel:geometry()
|
||||
)
|
||||
return cltbl[target]
|
||||
end
|
||||
|
||||
return _client
|
158
config/awesome/module/bling/helpers/color.lua
Normal file
|
@ -0,0 +1,158 @@
|
|||
local tonumber = tonumber
|
||||
local string = string
|
||||
local math = math
|
||||
local floor = math.floor
|
||||
local max = math.max
|
||||
local min = math.min
|
||||
local abs = math.abs
|
||||
local format = string.format
|
||||
|
||||
local _color = {}
|
||||
|
||||
--- Try to guess if a color is dark or light.
|
||||
--
|
||||
-- @string color The color with hexadecimal HTML format `"#RRGGBB"`.
|
||||
-- @treturn bool `true` if the color is dark, `false` if it is light.
|
||||
function _color.is_dark(color)
|
||||
-- Try to determine if the color is dark or light
|
||||
local numeric_value = 0
|
||||
for s in color:gmatch("[a-fA-F0-9][a-fA-F0-9]") do
|
||||
numeric_value = numeric_value + tonumber("0x" .. s)
|
||||
end
|
||||
return (numeric_value < 383)
|
||||
end
|
||||
|
||||
function _color.is_opaque(color)
|
||||
if type(color) == "string" then
|
||||
color = _color.hex_to_rgba(color)
|
||||
end
|
||||
|
||||
return color.a < 0.01
|
||||
end
|
||||
|
||||
--- Lighten a color.
|
||||
--
|
||||
-- @string color The color to lighten with hexadecimal HTML format `"#RRGGBB"`.
|
||||
-- @int[opt=26] amount How much light from 0 to 255. Default is around 10%.
|
||||
-- @treturn string The lighter color
|
||||
function _color.lighten(color, amount)
|
||||
amount = amount or 26
|
||||
local c = {
|
||||
r = tonumber("0x" .. color:sub(2, 3)),
|
||||
g = tonumber("0x" .. color:sub(4, 5)),
|
||||
b = tonumber("0x" .. color:sub(6, 7)),
|
||||
}
|
||||
|
||||
c.r = c.r + amount
|
||||
c.r = c.r < 0 and 0 or c.r
|
||||
c.r = c.r > 255 and 255 or c.r
|
||||
c.g = c.g + amount
|
||||
c.g = c.g < 0 and 0 or c.g
|
||||
c.g = c.g > 255 and 255 or c.g
|
||||
c.b = c.b + amount
|
||||
c.b = c.b < 0 and 0 or c.b
|
||||
c.b = c.b > 255 and 255 or c.b
|
||||
|
||||
return string.format("#%02x%02x%02x", c.r, c.g, c.b)
|
||||
end
|
||||
|
||||
--- Darken a color.
|
||||
--
|
||||
-- @string color The color to darken with hexadecimal HTML format `"#RRGGBB"`.
|
||||
-- @int[opt=26] amount How much dark from 0 to 255. Default is around 10%.
|
||||
-- @treturn string The darker color
|
||||
function _color.darken(color, amount)
|
||||
amount = amount or 26
|
||||
return _color.lighten(color, -amount)
|
||||
end
|
||||
|
||||
-- Returns a value that is clipped to interval edges if it falls outside the interval
|
||||
function _color.clip(num, min_num, max_num)
|
||||
return max(min(num, max_num), min_num)
|
||||
end
|
||||
|
||||
-- Converts the given hex color to rgba
|
||||
function _color.hex_to_rgba(color)
|
||||
color = color:gsub("#", "")
|
||||
return { r = tonumber("0x" .. color:sub(1, 2)),
|
||||
g = tonumber("0x" .. color:sub(3, 4)),
|
||||
b = tonumber("0x" .. color:sub(5, 6)),
|
||||
a = #color == 8 and tonumber("0x" .. color:sub(7, 8)) or 255 }
|
||||
end
|
||||
|
||||
-- Converts the given rgba color to hex
|
||||
function _color.rgba_to_hex(color)
|
||||
local r = _color.clip(color.r or color[1], 0, 255)
|
||||
local g = _color.clip(color.g or color[2], 0, 255)
|
||||
local b = _color.clip(color.b or color[3], 0, 255)
|
||||
local a = _color.clip(color.a or color[4] or 255, 0, 255)
|
||||
return "#" .. format("%02x%02x%02x%02x",
|
||||
floor(r),
|
||||
floor(g),
|
||||
floor(b),
|
||||
floor(a))
|
||||
end
|
||||
|
||||
-- Converts the given hex color to hsv
|
||||
function _color.hex_to_hsv(color)
|
||||
local color = _color.hex2rgb(color)
|
||||
local C_max = max(color.r, color.g, color.b)
|
||||
local C_min = min(color.r, color.g, color.b)
|
||||
local delta = C_max - C_min
|
||||
local H, S, V
|
||||
if delta == 0 then
|
||||
H = 0
|
||||
elseif C_max == color.r then
|
||||
H = 60 * (((color.g - color.b) / delta) % 6)
|
||||
elseif C_max == color.g then
|
||||
H = 60 * (((color.b - color.r) / delta) + 2)
|
||||
elseif C_max == color.b then
|
||||
H = 60 * (((color.r - color.g) / delta) + 4)
|
||||
end
|
||||
if C_max == 0 then
|
||||
S = 0
|
||||
else
|
||||
S = delta / C_max
|
||||
end
|
||||
V = C_max
|
||||
|
||||
return { h = H,
|
||||
s = S * 100,
|
||||
v = V * 100 }
|
||||
end
|
||||
|
||||
-- Converts the given hsv color to hex
|
||||
function _color.hsv_to_hex(H, S, V)
|
||||
S = S / 100
|
||||
V = V / 100
|
||||
if H > 360 then H = 360 end
|
||||
if H < 0 then H = 0 end
|
||||
local C = V * S
|
||||
local X = C * (1 - abs(((H / 60) % 2) - 1))
|
||||
local m = V - C
|
||||
local r_, g_, b_ = 0, 0, 0
|
||||
if H >= 0 and H < 60 then
|
||||
r_, g_, b_ = C, X, 0
|
||||
elseif H >= 60 and H < 120 then
|
||||
r_, g_, b_ = X, C, 0
|
||||
elseif H >= 120 and H < 180 then
|
||||
r_, g_, b_ = 0, C, X
|
||||
elseif H >= 180 and H < 240 then
|
||||
r_, g_, b_ = 0, X, C
|
||||
elseif H >= 240 and H < 300 then
|
||||
r_, g_, b_ = X, 0, C
|
||||
elseif H >= 300 and H < 360 then
|
||||
r_, g_, b_ = C, 0, X
|
||||
end
|
||||
local r, g, b = (r_ + m) * 255, (g_ + m) * 255, (b_ + m) * 255
|
||||
return ("#%02x%02x%02x"):format(floor(r), floor(g), floor(b))
|
||||
end
|
||||
|
||||
function _color.multiply(color, amount)
|
||||
return { _color.clip(color.r * amount, 0, 255),
|
||||
_color.clip(color.g * amount, 0, 255),
|
||||
_color.clip(color.b * amount, 0, 255),
|
||||
255 }
|
||||
end
|
||||
|
||||
return _color
|
62
config/awesome/module/bling/helpers/filesystem.lua
Normal file
|
@ -0,0 +1,62 @@
|
|||
local Gio = require("lgi").Gio
|
||||
local awful = require("awful")
|
||||
local string = string
|
||||
|
||||
local _filesystem = {}
|
||||
|
||||
--- Get a list of files from a given directory.
|
||||
-- @string path The directory to search.
|
||||
-- @tparam[opt] table exts Specific extensions to limit the search to. eg:`{ "jpg", "png" }`
|
||||
-- If ommited, all files are considered.
|
||||
-- @bool[opt=false] recursive List files from subdirectories
|
||||
-- @staticfct bling.helpers.filesystem.get_random_file_from_dir
|
||||
function _filesystem.list_directory_files(path, exts, recursive)
|
||||
recursive = recursive or false
|
||||
local files, valid_exts = {}, {}
|
||||
|
||||
-- Transforms { "jpg", ... } into { [jpg] = #, ... }
|
||||
if exts then
|
||||
for i, j in ipairs(exts) do
|
||||
valid_exts[j:lower()] = i
|
||||
end
|
||||
end
|
||||
|
||||
-- Build a table of files from the path with the required extensions
|
||||
local file_list = Gio.File.new_for_path(path):enumerate_children(
|
||||
"standard::*",
|
||||
0
|
||||
)
|
||||
if file_list then
|
||||
for file in function()
|
||||
return file_list:next_file()
|
||||
end do
|
||||
local file_type = file:get_file_type()
|
||||
if file_type == "REGULAR" then
|
||||
local file_name = file:get_display_name()
|
||||
if
|
||||
not exts
|
||||
or valid_exts[file_name:lower():match(".+%.(.*)$") or ""]
|
||||
then
|
||||
table.insert(files, file_name)
|
||||
end
|
||||
elseif recursive and file_type == "DIRECTORY" then
|
||||
local file_name = file:get_display_name()
|
||||
files = gears.table.join(
|
||||
files,
|
||||
list_directory_files(file_name, exts, recursive)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return files
|
||||
end
|
||||
|
||||
function _filesystem.save_image_async_curl(url, filepath, callback)
|
||||
awful.spawn.with_line_callback(string.format("curl -L -s %s -o %s", url, filepath),
|
||||
{
|
||||
exit=callback
|
||||
})
|
||||
end
|
||||
|
||||
return _filesystem
|
134
config/awesome/module/bling/helpers/icon_theme.lua
Normal file
|
@ -0,0 +1,134 @@
|
|||
local Gio = require("lgi").Gio
|
||||
local Gtk = require("lgi").Gtk
|
||||
local gobject = require("gears.object")
|
||||
local gtable = require("gears.table")
|
||||
local helpers = require("helpers")
|
||||
local setmetatable = setmetatable
|
||||
local ipairs = ipairs
|
||||
|
||||
local icon_theme = { mt = {} }
|
||||
|
||||
function icon_theme:get_client_icon_path(client)
|
||||
local function find_icon(class)
|
||||
if self._private.client_icon_cache[class] ~= nil then
|
||||
return self._private.client_icon_cache[class]
|
||||
end
|
||||
|
||||
for _, app in ipairs(Gio.AppInfo.get_all()) do
|
||||
local id = Gio.AppInfo.get_id(app)
|
||||
if id:match(helpers.misc.case_insensitive_pattern(class)) then
|
||||
self._private.client_icon_cache[class] = self:get_gicon_path(Gio.AppInfo.get_icon(app))
|
||||
return self._private.client_icon_cache[class]
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local class = client.class
|
||||
if class == "jetbrains-studio" then
|
||||
class = "android-studio"
|
||||
end
|
||||
|
||||
local icon = self:get_icon_path("gnome-window-manager")
|
||||
|
||||
if class ~= nil then
|
||||
class = class:gsub("[%-]", "%%%0")
|
||||
icon = find_icon(class) or icon
|
||||
|
||||
class = client.class
|
||||
class = class:gsub("[%-]", "")
|
||||
icon = find_icon(class) or icon
|
||||
|
||||
class = client.class
|
||||
class = class:gsub("[%-]", ".")
|
||||
icon = find_icon(class) or icon
|
||||
|
||||
class = client.class
|
||||
class = class:match("(.-)-") or class
|
||||
class = class:match("(.-)%.") or class
|
||||
class = class:match("(.-)%s+") or class
|
||||
class = class:gsub("[%-]", "%%%0")
|
||||
icon = find_icon(class) or icon
|
||||
end
|
||||
|
||||
return icon
|
||||
end
|
||||
|
||||
function icon_theme:choose_icon(icons_names)
|
||||
local icon_info = Gtk.IconTheme.choose_icon(self.gtk_theme, icons_names, self.icon_size, 0);
|
||||
if icon_info then
|
||||
local icon_path = Gtk.IconInfo.get_filename(icon_info)
|
||||
if icon_path then
|
||||
return icon_path
|
||||
end
|
||||
end
|
||||
|
||||
return ""
|
||||
end
|
||||
|
||||
|
||||
function icon_theme:get_gicon_path(gicon)
|
||||
if gicon == nil then
|
||||
return ""
|
||||
end
|
||||
|
||||
if self._private.icon_cache[gicon] ~= nil then
|
||||
return self._private.icon_cache[gicon]
|
||||
end
|
||||
|
||||
local icon_info = Gtk.IconTheme.lookup_by_gicon(self.gtk_theme, gicon, self.icon_size, 0);
|
||||
if icon_info then
|
||||
local icon_path = Gtk.IconInfo.get_filename(icon_info)
|
||||
if icon_path then
|
||||
self._private.icon_cache[gicon] = icon_path
|
||||
return icon_path
|
||||
end
|
||||
end
|
||||
|
||||
return ""
|
||||
end
|
||||
|
||||
function icon_theme:get_icon_path(icon_name)
|
||||
if self._private.icon_cache[icon_name] ~= nil then
|
||||
return self._private.icon_cache[icon_name]
|
||||
end
|
||||
|
||||
local icon_info = Gtk.IconTheme.lookup_icon(self.gtk_theme, icon_name, self.icon_size, 0);
|
||||
if icon_info then
|
||||
local icon_path = Gtk.IconInfo.get_filename(icon_info)
|
||||
if icon_path then
|
||||
self._private.icon_cache[icon_name] = icon_path
|
||||
return icon_path
|
||||
end
|
||||
end
|
||||
|
||||
return ""
|
||||
end
|
||||
|
||||
local function new(theme_name, icon_size)
|
||||
local ret = gobject{}
|
||||
gtable.crush(ret, icon_theme, true)
|
||||
|
||||
ret._private = {}
|
||||
ret._private.client_icon_cache = {}
|
||||
ret._private.icon_cache = {}
|
||||
|
||||
ret.name = theme_name or nil
|
||||
ret.icon_size = icon_size or 48
|
||||
|
||||
if theme_name then
|
||||
ret.gtk_theme = Gtk.IconTheme.new()
|
||||
Gtk.IconTheme.set_custom_theme(ret.gtk_theme, theme_name);
|
||||
else
|
||||
ret.gtk_theme = Gtk.IconTheme.get_default()
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
function icon_theme.mt:__call(...)
|
||||
return new(...)
|
||||
end
|
||||
|
||||
return setmetatable(icon_theme, icon_theme.mt)
|
7
config/awesome/module/bling/helpers/init.lua
Normal file
|
@ -0,0 +1,7 @@
|
|||
return {
|
||||
client = require(... .. ".client"),
|
||||
color = require(... .. ".color"),
|
||||
filesystem = require(... .. ".filesystem"),
|
||||
shape = require(... .. ".shape"),
|
||||
time = require(... .. ".time"),
|
||||
}
|
30
config/awesome/module/bling/helpers/shape.lua
Normal file
|
@ -0,0 +1,30 @@
|
|||
local gears = require("gears")
|
||||
|
||||
shape = {}
|
||||
|
||||
-- Create rounded rectangle shape (in one line)
|
||||
|
||||
function shape.rrect(radius)
|
||||
return function(cr, width, height)
|
||||
gears.shape.rounded_rect(cr, width, height, radius)
|
||||
end
|
||||
end
|
||||
|
||||
-- Create partially rounded rect
|
||||
|
||||
function shape.prrect(radius, tl, tr, br, bl)
|
||||
return function(cr, width, height)
|
||||
gears.shape.partially_rounded_rect(
|
||||
cr,
|
||||
width,
|
||||
height,
|
||||
tl,
|
||||
tr,
|
||||
br,
|
||||
bl,
|
||||
radius
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return shape
|
24
config/awesome/module/bling/helpers/time.lua
Normal file
|
@ -0,0 +1,24 @@
|
|||
local time = {}
|
||||
|
||||
--- Parse a time string to seconds (from midnight)
|
||||
--
|
||||
-- @string time The time (`HH:MM:SS`)
|
||||
-- @treturn int The number of seconds since 00:00:00
|
||||
function time.hhmmss_to_seconds(time)
|
||||
hour_sec = tonumber(string.sub(time, 1, 2)) * 3600
|
||||
min_sec = tonumber(string.sub(time, 4, 5)) * 60
|
||||
get_sec = tonumber(string.sub(time, 7, 8))
|
||||
return (hour_sec + min_sec + get_sec)
|
||||
end
|
||||
|
||||
--- Get time difference in seconds.
|
||||
--
|
||||
-- @tparam string base The time to compare from (`HH:MM:SS`).
|
||||
-- @tparam string base The time to compare to (`HH:MM:SS`).
|
||||
-- @treturn int Number of seconds between the two times.
|
||||
function time.time_diff(base, compare)
|
||||
local diff = time.hhmmss_to_seconds(base) - time.hhmmss_to_seconds(compare)
|
||||
return diff
|
||||
end
|
||||
|
||||
return time
|
BIN
config/awesome/module/bling/icons/layouts/centered.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
config/awesome/module/bling/icons/layouts/deck.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
config/awesome/module/bling/icons/layouts/equalarea.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
config/awesome/module/bling/icons/layouts/horizontal.png
Normal file
After Width: | Height: | Size: 768 B |
BIN
config/awesome/module/bling/icons/layouts/mstab.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
config/awesome/module/bling/icons/layouts/vertical.png
Normal file
After Width: | Height: | Size: 767 B |
BIN
config/awesome/module/bling/images/bling_banner-2x.png
Normal file
After Width: | Height: | Size: 248 KiB |
BIN
config/awesome/module/bling/images/bling_banner.png
Normal file
After Width: | Height: | Size: 76 KiB |
11
config/awesome/module/bling/init.lua
Normal file
|
@ -0,0 +1,11 @@
|
|||
--[[
|
||||
Bling
|
||||
Layouts, widgets and utilities for Awesome WM
|
||||
--]]
|
||||
return {
|
||||
layout = require(... .. ".layout"),
|
||||
module = require(... .. ".module"),
|
||||
helpers = require(... .. ".helpers"),
|
||||
signal = require(... .. ".signal"),
|
||||
widget = require(... .. ".widget"),
|
||||
}
|
84
config/awesome/module/bling/layout/centered.lua
Normal file
|
@ -0,0 +1,84 @@
|
|||
local awful = require("awful")
|
||||
local math = math
|
||||
|
||||
local mylayout = {}
|
||||
|
||||
mylayout.name = "centered"
|
||||
|
||||
function mylayout.arrange(p)
|
||||
local area = p.workarea
|
||||
local t = p.tag or screen[p.screen].selected_tag
|
||||
local mwfact = t.master_width_factor
|
||||
local nmaster = math.min(t.master_count, #p.clients)
|
||||
local nslaves = #p.clients - nmaster
|
||||
|
||||
local master_area_width = area.width * mwfact
|
||||
local slave_area_width = area.width - master_area_width
|
||||
local master_area_x = area.x + 0.5 * slave_area_width
|
||||
|
||||
local number_of_left_sided_slaves = math.floor(nslaves / 2)
|
||||
local number_of_right_sided_slaves = nslaves - number_of_left_sided_slaves
|
||||
local left_iterator = 0
|
||||
local right_iterator = 0
|
||||
|
||||
-- Special case: no maters -> rrelapse into awesomes fair layout
|
||||
if t.master_count == 0 then
|
||||
awful.layout.suit.fair.arrange(p)
|
||||
return
|
||||
end
|
||||
|
||||
-- Special case: one slave -> relapse into awesomes masterstack tile layout
|
||||
if nslaves == 1 then
|
||||
awful.layout.suit.tile.right.arrange(p)
|
||||
return
|
||||
end
|
||||
|
||||
-- Special case: no slaves -> fullscreen master area
|
||||
if nslaves < 1 then
|
||||
master_area_width = area.width
|
||||
master_area_x = area.x
|
||||
end
|
||||
|
||||
-- iterate through masters
|
||||
for idx = 1, nmaster do
|
||||
local c = p.clients[idx]
|
||||
local g
|
||||
g = {
|
||||
x = master_area_x,
|
||||
y = area.y + (nmaster - idx) * (area.height / nmaster),
|
||||
width = master_area_width,
|
||||
height = area.height / nmaster,
|
||||
}
|
||||
p.geometries[c] = g
|
||||
end
|
||||
|
||||
-- iterate through slaves
|
||||
for idx = 1, nslaves do -- idx=nmaster+1,#p.clients do
|
||||
local c = p.clients[idx + nmaster]
|
||||
local g
|
||||
if idx % 2 == 0 then
|
||||
g = {
|
||||
x = area.x,
|
||||
y = area.y
|
||||
+ left_iterator
|
||||
* (area.height / number_of_left_sided_slaves),
|
||||
width = slave_area_width / 2,
|
||||
height = area.height / number_of_left_sided_slaves,
|
||||
}
|
||||
left_iterator = left_iterator + 1
|
||||
else
|
||||
g = {
|
||||
x = area.x + master_area_width + slave_area_width / 2,
|
||||
y = area.y
|
||||
+ right_iterator
|
||||
* (area.height / number_of_right_sided_slaves),
|
||||
width = slave_area_width / 2,
|
||||
height = area.height / number_of_right_sided_slaves,
|
||||
}
|
||||
right_iterator = right_iterator + 1
|
||||
end
|
||||
p.geometries[c] = g
|
||||
end
|
||||
end
|
||||
|
||||
return mylayout
|
37
config/awesome/module/bling/layout/deck.lua
Normal file
|
@ -0,0 +1,37 @@
|
|||
local mylayout = {}
|
||||
|
||||
mylayout.name = "deck"
|
||||
|
||||
function mylayout.arrange(p)
|
||||
local area = p.workarea
|
||||
local t = p.tag or screen[p.screen].selected_tag
|
||||
local client_count = #p.clients
|
||||
|
||||
if client_count == 1 then
|
||||
local c = p.clients[1]
|
||||
local g = {
|
||||
x = area.x,
|
||||
y = area.y,
|
||||
width = area.width,
|
||||
height = area.height,
|
||||
}
|
||||
p.geometries[c] = g
|
||||
return
|
||||
end
|
||||
|
||||
local xoffset = area.width * 0.1 / (client_count - 1)
|
||||
local yoffset = area.height * 0.1 / (client_count - 1)
|
||||
|
||||
for idx = 1, client_count do
|
||||
local c = p.clients[idx]
|
||||
local g = {
|
||||
x = area.x + (idx - 1) * xoffset,
|
||||
y = area.y + (idx - 1) * yoffset,
|
||||
width = area.width - (xoffset * (client_count - 1)),
|
||||
height = area.height - (yoffset * (client_count - 1)),
|
||||
}
|
||||
p.geometries[c] = g
|
||||
end
|
||||
end
|
||||
|
||||
return mylayout
|
77
config/awesome/module/bling/layout/equalarea.lua
Normal file
|
@ -0,0 +1,77 @@
|
|||
local math = math
|
||||
local screen = screen
|
||||
local mylayout = {}
|
||||
mylayout.name = "equalarea"
|
||||
|
||||
local function divide(p, g, low, high, cls, mwfact, mcount)
|
||||
if low == high then
|
||||
p.geometries[cls[low]] = g
|
||||
else
|
||||
local masters = math.max(0, math.min(mcount, high) - low + 1)
|
||||
local numblock = high - low + 1
|
||||
local slaves = numblock - masters
|
||||
local smalldiv
|
||||
if numblock > 5 and (numblock % 5) == 0 then
|
||||
smalldiv = math.floor(numblock / 5)
|
||||
else
|
||||
if (numblock % 3) == 0 then
|
||||
smalldiv = math.floor(numblock / 3)
|
||||
else
|
||||
smalldiv = math.floor(numblock / 2)
|
||||
end
|
||||
end
|
||||
local bigdiv = numblock - smalldiv
|
||||
local smallmasters = math.min(masters, smalldiv)
|
||||
local bigmasters = masters - smallmasters
|
||||
local smallg = {}
|
||||
local bigg = {}
|
||||
smallg.x = g.x
|
||||
smallg.y = g.y
|
||||
if g.width > (g.height * 1.3) then
|
||||
smallg.height = g.height
|
||||
bigg.height = g.height
|
||||
bigg.width = math.floor(
|
||||
g.width
|
||||
* (bigmasters * (mwfact - 1) + bigdiv)
|
||||
/ (slaves + mwfact * masters)
|
||||
)
|
||||
smallg.width = g.width - bigg.width
|
||||
bigg.y = g.y
|
||||
bigg.x = g.x + smallg.width
|
||||
else
|
||||
smallg.width = g.width
|
||||
bigg.width = g.width
|
||||
bigg.height = math.floor(
|
||||
g.height
|
||||
* (bigmasters * (mwfact - 1) + bigdiv)
|
||||
/ (slaves + mwfact * masters)
|
||||
)
|
||||
smallg.height = g.height - bigg.height
|
||||
bigg.x = g.x
|
||||
bigg.y = g.y + smallg.height
|
||||
end
|
||||
divide(p, smallg, low, high - bigdiv, cls, mwfact, mcount)
|
||||
divide(p, bigg, low + smalldiv, high, cls, mwfact, mcount)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function mylayout.arrange(p)
|
||||
local t = p.tag or screen[p.screen].selected_tag
|
||||
local wa = p.workarea
|
||||
local cls = p.clients
|
||||
|
||||
if #cls == 0 then
|
||||
return
|
||||
end
|
||||
local mwfact = t.master_width_factor * 2
|
||||
local mcount = t.master_count
|
||||
local g = {}
|
||||
g.height = wa.height
|
||||
g.width = wa.width
|
||||
g.x = wa.x
|
||||
g.y = wa.y
|
||||
divide(p, g, 1, #cls, cls, mwfact, mcount)
|
||||
end
|
||||
|
||||
return mylayout
|
56
config/awesome/module/bling/layout/horizontal.lua
Normal file
|
@ -0,0 +1,56 @@
|
|||
local math = math
|
||||
|
||||
local mylayout = {}
|
||||
|
||||
mylayout.name = "horizontal"
|
||||
|
||||
function mylayout.arrange(p)
|
||||
local area = p.workarea
|
||||
local t = p.tag or screen[p.screen].selected_tag
|
||||
local mwfact = t.master_width_factor
|
||||
local nmaster = math.min(t.master_count, #p.clients)
|
||||
local nslaves = #p.clients - nmaster
|
||||
|
||||
local master_area_height = area.height * mwfact
|
||||
local slave_area_height = area.height - master_area_height
|
||||
|
||||
-- Special case: no slaves
|
||||
if nslaves == 0 then
|
||||
master_area_height = area.height
|
||||
slave_area_height = 0
|
||||
end
|
||||
|
||||
-- Special case: no masters
|
||||
if nmaster == 0 then
|
||||
master_area_height = 0
|
||||
slave_area_height = area.height
|
||||
end
|
||||
|
||||
-- itearte through masters
|
||||
for idx = 1, nmaster do
|
||||
local c = p.clients[idx]
|
||||
local g = {
|
||||
x = area.x + (idx - 1) * (area.width / nmaster),
|
||||
y = area.y,
|
||||
width = area.width / nmaster,
|
||||
height = master_area_height,
|
||||
}
|
||||
p.geometries[c] = g
|
||||
end
|
||||
|
||||
-- iterate through slaves
|
||||
for idx = 1, nslaves do
|
||||
local c = p.clients[idx + nmaster]
|
||||
local g = {
|
||||
x = area.x,
|
||||
y = area.y
|
||||
+ master_area_height
|
||||
+ (idx - 1) * (slave_area_height / nslaves),
|
||||
width = area.width,
|
||||
height = slave_area_height / nslaves,
|
||||
}
|
||||
p.geometries[c] = g
|
||||
end
|
||||
end
|
||||
|
||||
return mylayout
|
44
config/awesome/module/bling/layout/init.lua
Normal file
|
@ -0,0 +1,44 @@
|
|||
local beautiful = require("beautiful")
|
||||
local gears = require("gears")
|
||||
|
||||
local M = {}
|
||||
local relative_lua_path = tostring(...)
|
||||
|
||||
local function get_layout_icon_path(name)
|
||||
local relative_icon_path = relative_lua_path
|
||||
:match("^.*bling"):gsub("%.", "/")
|
||||
.. "/icons/layouts/" .. name .. ".png"
|
||||
|
||||
for p in package.path:gmatch('([^;]+)') do
|
||||
p = p:gsub("?.*", "")
|
||||
local absolute_icon_path = p .. relative_icon_path
|
||||
if gears.filesystem.file_readable(absolute_icon_path) then
|
||||
return absolute_icon_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function get_icon(icon_raw)
|
||||
if icon_raw ~= nil then
|
||||
return gears.color.recolor_image(icon_raw, beautiful.fg_normal)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local layouts = {
|
||||
"mstab",
|
||||
"vertical",
|
||||
"horizontal",
|
||||
"centered",
|
||||
"equalarea",
|
||||
"deck"
|
||||
}
|
||||
|
||||
for _, layout_name in ipairs(layouts) do
|
||||
local icon_raw = get_layout_icon_path(layout_name)
|
||||
beautiful["layout_" .. layout_name] = get_icon(icon_raw)
|
||||
M[layout_name] = require(... .. "." .. layout_name)
|
||||
end
|
||||
|
||||
return M
|
244
config/awesome/module/bling/layout/mstab.lua
Normal file
|
@ -0,0 +1,244 @@
|
|||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
local wibox = require("wibox")
|
||||
local beautiful = require("beautiful")
|
||||
|
||||
local mylayout = {}
|
||||
|
||||
mylayout.name = "mstab"
|
||||
|
||||
local tabbar_ontop = beautiful.mstab_bar_ontop or false
|
||||
local tabbar_padding = beautiful.mstab_bar_padding or "default"
|
||||
local border_radius = beautiful.mstab_border_radius
|
||||
or beautiful.border_radius
|
||||
or 0
|
||||
local tabbar_position = beautiful.mstab_tabbar_position
|
||||
or beautiful.tabbar_position
|
||||
or "top"
|
||||
|
||||
local bar_style = beautiful.mstab_tabbar_style
|
||||
or beautiful.tabbar_style
|
||||
or "default"
|
||||
local bar = require(
|
||||
tostring(...):match(".*bling") .. ".widget.tabbar." .. bar_style
|
||||
)
|
||||
local tabbar_size = bar.size
|
||||
or beautiful.mstab_bar_height
|
||||
or beautiful.tabbar_size
|
||||
or 40
|
||||
local dont_resize_slaves = beautiful.mstab_dont_resize_slaves or false
|
||||
|
||||
-- The top_idx is the idx of the slave clients (excluding all master clients)
|
||||
-- that should be on top of all other slave clients ("the focused slave")
|
||||
-- by creating a variable outside of the arrange function, this layout can "remember" that client
|
||||
-- by creating it as a new property of every tag, this layout can be active on different tags and
|
||||
-- still have different "focused slave clients"
|
||||
for idx, tag in ipairs(root.tags()) do
|
||||
tag.top_idx = 1
|
||||
end
|
||||
|
||||
-- Haven't found a signal that is emitted when a new tag is added. That should work though
|
||||
-- since you can't use a layout on a tag that you haven't selected previously
|
||||
tag.connect_signal("property::selected", function(t)
|
||||
if not t.top_idx then
|
||||
t.top_idx = 1
|
||||
end
|
||||
end)
|
||||
|
||||
function update_tabbar(
|
||||
clients,
|
||||
t,
|
||||
top_idx,
|
||||
area,
|
||||
master_area_width,
|
||||
slave_area_width
|
||||
)
|
||||
local s = t.screen
|
||||
|
||||
-- create the list of clients for the tabbar
|
||||
local clientlist = bar.layout()
|
||||
for idx, c in ipairs(clients) do
|
||||
-- focus with right click, kill with mid click, minimize with left click
|
||||
local buttons = gears.table.join(
|
||||
awful.button({}, 1, function()
|
||||
c:raise()
|
||||
client.focus = c
|
||||
end),
|
||||
awful.button({}, 2, function()
|
||||
c:kill()
|
||||
end),
|
||||
awful.button({}, 3, function()
|
||||
c.minimized = true
|
||||
end)
|
||||
)
|
||||
local client_box = bar.create(c, (idx == top_idx), buttons)
|
||||
clientlist:add(client_box)
|
||||
end
|
||||
|
||||
-- if no tabbar exists, create one
|
||||
if not s.tabbar then
|
||||
s.tabbar = wibox({
|
||||
ontop = tabbar_ontop,
|
||||
shape = function(cr, width, height)
|
||||
gears.shape.rounded_rect(cr, width, height, border_radius)
|
||||
end,
|
||||
bg = bar.bg_normal,
|
||||
visible = true,
|
||||
})
|
||||
|
||||
-- Change visibility of the tab bar when layout, selected tag or number of clients (visible, master, slave) changes
|
||||
local function adjust_visiblity(t)
|
||||
s.tabbar.visible = (#t:clients() - t.master_count > 1)
|
||||
and (t.layout.name == mylayout.name)
|
||||
end
|
||||
|
||||
tag.connect_signal("property::selected", function(t)
|
||||
adjust_visiblity(t)
|
||||
end)
|
||||
tag.connect_signal("property::layout", function(t, layout)
|
||||
adjust_visiblity(t)
|
||||
end)
|
||||
tag.connect_signal("tagged", function(t, c)
|
||||
adjust_visiblity(t)
|
||||
end)
|
||||
tag.connect_signal("untagged", function(t, c)
|
||||
adjust_visiblity(t)
|
||||
end)
|
||||
tag.connect_signal("property::master_count", function(t)
|
||||
adjust_visiblity(t)
|
||||
end)
|
||||
client.connect_signal("property::minimized", function(c)
|
||||
local t = c.first_tag
|
||||
adjust_visiblity(t)
|
||||
end)
|
||||
end
|
||||
|
||||
-- update the tabbar size and position (to support gap size change on the fly)
|
||||
if tabbar_position == "top" then
|
||||
s.tabbar.x = area.x + master_area_width + t.gap
|
||||
s.tabbar.y = area.y + t.gap
|
||||
s.tabbar.width = slave_area_width - 2 * t.gap
|
||||
s.tabbar.height = tabbar_size
|
||||
elseif tabbar_position == "bottom" then
|
||||
s.tabbar.x = area.x + master_area_width + t.gap
|
||||
s.tabbar.y = area.y + area.height - tabbar_size - t.gap
|
||||
s.tabbar.width = slave_area_width - 2 * t.gap
|
||||
s.tabbar.height = tabbar_size
|
||||
elseif tabbar_position == "left" then
|
||||
s.tabbar.x = area.x + master_area_width + t.gap
|
||||
s.tabbar.y = area.y + t.gap
|
||||
s.tabbar.width = tabbar_size
|
||||
s.tabbar.height = area.height - 2 * t.gap
|
||||
elseif tabbar_position == "right" then
|
||||
s.tabbar.x = area.x
|
||||
+ master_area_width
|
||||
+ slave_area_width
|
||||
- tabbar_size
|
||||
- t.gap
|
||||
s.tabbar.y = area.y + t.gap
|
||||
s.tabbar.width = tabbar_size
|
||||
s.tabbar.height = area.height - 2 * t.gap
|
||||
end
|
||||
|
||||
-- update clientlist
|
||||
s.tabbar:setup({ layout = wibox.layout.flex.horizontal, clientlist })
|
||||
end
|
||||
|
||||
function mylayout.arrange(p)
|
||||
local area = p.workarea
|
||||
local t = p.tag or screen[p.screen].selected_tag
|
||||
local s = t.screen
|
||||
local mwfact = t.master_width_factor
|
||||
local nmaster = math.min(t.master_count, #p.clients)
|
||||
local nslaves = #p.clients - nmaster
|
||||
|
||||
local master_area_width = area.width * mwfact
|
||||
local slave_area_width = area.width - master_area_width
|
||||
|
||||
-- "default" means that it uses standard useless gap size
|
||||
if tabbar_padding == "default" then
|
||||
tabbar_padding = 2 * t.gap
|
||||
end
|
||||
|
||||
-- Special case: No masters -> full screen slave width
|
||||
if nmaster == 0 then
|
||||
master_area_width = 1
|
||||
slave_area_width = area.width
|
||||
end
|
||||
|
||||
-- Special case: One or zero slaves -> no tabbar (essentially tile right)
|
||||
if nslaves <= 1 then
|
||||
-- since update_tabbar isnt called that way we have to hide it manually
|
||||
if s.tabbar then
|
||||
s.tabbar.visible = false
|
||||
end
|
||||
-- otherwise just do tile right
|
||||
awful.layout.suit.tile.right.arrange(p)
|
||||
return
|
||||
end
|
||||
|
||||
-- Iterate through masters
|
||||
for idx = 1, nmaster do
|
||||
local c = p.clients[idx]
|
||||
local g = {
|
||||
x = area.x,
|
||||
y = area.y + (idx - 1) * (area.height / nmaster),
|
||||
width = master_area_width,
|
||||
height = area.height / nmaster,
|
||||
}
|
||||
p.geometries[c] = g
|
||||
end
|
||||
|
||||
local tabbar_size_change = 0
|
||||
local tabbar_width_change = 0
|
||||
local tabbar_y_change = 0
|
||||
local tabbar_x_change = 0
|
||||
if tabbar_position == "top" then
|
||||
tabbar_size_change = tabbar_size + tabbar_padding
|
||||
tabbar_y_change = tabbar_size + tabbar_padding
|
||||
elseif tabbar_position == "bottom" then
|
||||
tabbar_size_change = tabbar_size + tabbar_padding
|
||||
elseif tabbar_position == "left" then
|
||||
tabbar_width_change = tabbar_size + tabbar_padding
|
||||
tabbar_x_change = tabbar_size + tabbar_padding
|
||||
elseif tabbar_position == "right" then
|
||||
tabbar_width_change = tabbar_size + tabbar_padding
|
||||
end
|
||||
|
||||
-- Iterate through slaves
|
||||
-- (also creates a list of all slave clients for update_tabbar)
|
||||
local slave_clients = {}
|
||||
for idx = 1, nslaves do
|
||||
local c = p.clients[idx + nmaster]
|
||||
slave_clients[#slave_clients + 1] = c
|
||||
if c == client.focus then
|
||||
t.top_idx = #slave_clients
|
||||
end
|
||||
local g = {
|
||||
x = area.x + master_area_width + tabbar_x_change,
|
||||
y = area.y + tabbar_y_change,
|
||||
width = slave_area_width - tabbar_width_change,
|
||||
height = area.height - tabbar_size_change,
|
||||
}
|
||||
if not dont_resize_slaves and idx ~= t.top_idx then
|
||||
g = {
|
||||
x = area.x + master_area_width + slave_area_width / 4,
|
||||
y = area.y + tabbar_size + area.height / 4,
|
||||
width = slave_area_width / 2,
|
||||
height = area.height / 4 - tabbar_size,
|
||||
}
|
||||
end
|
||||
p.geometries[c] = g
|
||||
end
|
||||
|
||||
update_tabbar(
|
||||
slave_clients,
|
||||
t,
|
||||
t.top_idx,
|
||||
area,
|
||||
master_area_width,
|
||||
slave_area_width
|
||||
)
|
||||
end
|
||||
|
||||
return mylayout
|
56
config/awesome/module/bling/layout/vertical.lua
Normal file
|
@ -0,0 +1,56 @@
|
|||
local math = math
|
||||
|
||||
local mylayout = {}
|
||||
|
||||
mylayout.name = "vertical"
|
||||
|
||||
function mylayout.arrange(p)
|
||||
local area = p.workarea
|
||||
local t = p.tag or screen[p.screen].selected_tag
|
||||
local mwfact = t.master_width_factor
|
||||
local nmaster = math.min(t.master_count, #p.clients)
|
||||
local nslaves = #p.clients - nmaster
|
||||
|
||||
local master_area_width = area.width * mwfact
|
||||
local slave_area_width = area.width - master_area_width
|
||||
|
||||
-- Special case: no slaves
|
||||
if nslaves == 0 then
|
||||
master_area_width = area.width
|
||||
slave_area_width = 0
|
||||
end
|
||||
|
||||
-- Special case: no masters
|
||||
if nmaster == 0 then
|
||||
master_area_width = 0
|
||||
slave_area_width = area.width
|
||||
end
|
||||
|
||||
-- iterate through masters
|
||||
for idx = 1, nmaster do
|
||||
local c = p.clients[idx]
|
||||
local g = {
|
||||
x = area.x,
|
||||
y = area.y + (idx - 1) * (area.height / nmaster),
|
||||
width = master_area_width,
|
||||
height = area.height / nmaster,
|
||||
}
|
||||
p.geometries[c] = g
|
||||
end
|
||||
|
||||
-- itearte through slaves
|
||||
for idx = 1, nslaves do
|
||||
local c = p.clients[idx + nmaster]
|
||||
local g = {
|
||||
x = area.x
|
||||
+ master_area_width
|
||||
+ (idx - 1) * (slave_area_width / nslaves),
|
||||
y = area.y,
|
||||
width = slave_area_width / nslaves,
|
||||
height = area.height,
|
||||
}
|
||||
p.geometries[c] = g
|
||||
end
|
||||
end
|
||||
|
||||
return mylayout
|
44
config/awesome/module/bling/module/flash_focus.lua
Normal file
|
@ -0,0 +1,44 @@
|
|||
local gears = require("gears")
|
||||
local beautiful = require("beautiful")
|
||||
|
||||
local op = beautiful.flash_focus_start_opacity or 0.6
|
||||
local stp = beautiful.flash_focus_step or 0.01
|
||||
|
||||
local flashfocus = function(c)
|
||||
if c then
|
||||
c.opacity = op
|
||||
local q = op
|
||||
local g = gears.timer({
|
||||
timeout = stp,
|
||||
call_now = false,
|
||||
autostart = true,
|
||||
})
|
||||
|
||||
g:connect_signal("timeout", function()
|
||||
if not c.valid then
|
||||
return
|
||||
end
|
||||
if q >= 1 then
|
||||
c.opacity = 1
|
||||
g:stop()
|
||||
else
|
||||
c.opacity = q
|
||||
q = q + stp
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Bring the focused client to the top
|
||||
if c then
|
||||
c:raise()
|
||||
end
|
||||
end
|
||||
|
||||
local enable = function()
|
||||
client.connect_signal("focus", flashfocus)
|
||||
end
|
||||
local disable = function()
|
||||
client.disconnect_signal("focus", flashfocus)
|
||||
end
|
||||
|
||||
return { enable = enable, disable = disable, flashfocus = flashfocus }
|
8
config/awesome/module/bling/module/init.lua
Normal file
|
@ -0,0 +1,8 @@
|
|||
return {
|
||||
window_swallowing = require(... .. ".window_swallowing"),
|
||||
tiled_wallpaper = require(... .. ".tiled_wallpaper"),
|
||||
wallpaper = require(... .. ".wallpaper"),
|
||||
flash_focus = require(... .. ".flash_focus"),
|
||||
tabbed = require(... .. ".tabbed"),
|
||||
scratchpad = require(... .. ".scratchpad"),
|
||||
}
|
357
config/awesome/module/bling/module/scratchpad.lua
Normal file
|
@ -0,0 +1,357 @@
|
|||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
local naughty = require("naughty")
|
||||
|
||||
local ruled
|
||||
if awesome.version ~= "v4.3" then
|
||||
ruled = require("ruled")
|
||||
end
|
||||
|
||||
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
|
||||
|
||||
local Scratchpad = { mt = {} }
|
||||
|
||||
--- Creates a new scratchpad object based on the argument
|
||||
--
|
||||
-- @param args A table of possible arguments
|
||||
-- @return The new scratchpad object
|
||||
function Scratchpad:new(args)
|
||||
args = args or {}
|
||||
if args.awestore then
|
||||
naughty.notify({
|
||||
title = "Bling Error",
|
||||
text = "Awestore is no longer supported! Please take a look at the scratchpad documentation and use rubato for animations instead.",
|
||||
})
|
||||
end
|
||||
|
||||
args.rubato = args.rubato or {}
|
||||
args.in_anim = false
|
||||
local ret = gears.object({})
|
||||
gears.table.crush(ret, Scratchpad)
|
||||
gears.table.crush(ret, args)
|
||||
return ret
|
||||
end
|
||||
|
||||
--- Find all clients that satisfy the the rule
|
||||
--
|
||||
-- @return A list of all clients that satisfy the rule
|
||||
function Scratchpad:find()
|
||||
return helpers.client.find(self.rule)
|
||||
end
|
||||
|
||||
--- Applies the objects scratchpad properties to a given client
|
||||
--
|
||||
-- @param c A client to which to apply the properties
|
||||
function Scratchpad:apply(c)
|
||||
if not c or not c.valid then
|
||||
return
|
||||
end
|
||||
c.floating = self.floating
|
||||
c.sticky = self.sticky
|
||||
c.fullscreen = false
|
||||
c.maximized = false
|
||||
c:geometry({
|
||||
x = self.geometry.x + awful.screen.focused().geometry.x,
|
||||
y = self.geometry.y + awful.screen.focused().geometry.y,
|
||||
width = self.geometry.width,
|
||||
height = self.geometry.height,
|
||||
})
|
||||
if self.autoclose then
|
||||
c:connect_signal("unfocus", function(c1)
|
||||
c1.sticky = false -- client won't turn off if sticky
|
||||
helpers.client.turn_off(c1)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--- The turn on animation
|
||||
local function animate_turn_on(self, c, anim, axis)
|
||||
-- Check for the following scenerio:
|
||||
-- Toggle on scratchpad at tag 1
|
||||
-- Toggle on scratchpad at tag 2
|
||||
-- The animation will instantly end
|
||||
-- as the timer pos is already at the on position
|
||||
-- from toggling on the scratchpad at tag 1
|
||||
if axis == "x" and anim.pos == self.geometry.x then
|
||||
anim.pos = anim:initial()
|
||||
else
|
||||
if anim.pos == self.geometry.y then
|
||||
anim.pos = anim:initial()
|
||||
end
|
||||
end
|
||||
|
||||
anim:subscribe(function(pos)
|
||||
if c and c.valid then
|
||||
if axis == "x" then
|
||||
c.x = pos
|
||||
else
|
||||
c.y = pos
|
||||
end
|
||||
end
|
||||
self.in_anim = true
|
||||
end)
|
||||
|
||||
if axis == "x" then
|
||||
anim:set(self.geometry.x)
|
||||
else
|
||||
anim:set(self.geometry.y)
|
||||
end
|
||||
|
||||
anim.ended:subscribe(function()
|
||||
self.in_anim = false
|
||||
anim:unsubscribe()
|
||||
anim.ended:unsubscribe()
|
||||
end)
|
||||
end
|
||||
|
||||
--- Turns the scratchpad on
|
||||
function Scratchpad:turn_on()
|
||||
local c = self:find()[1]
|
||||
local anim_x = self.rubato.x
|
||||
local anim_y = self.rubato.y
|
||||
|
||||
if c and not self.in_anim and c.first_tag and c.first_tag.selected then
|
||||
c:raise()
|
||||
client.focus = c
|
||||
return
|
||||
end
|
||||
if c and not self.in_anim then
|
||||
-- if a client was found, turn it on
|
||||
if self.reapply then
|
||||
self:apply(c)
|
||||
end
|
||||
-- c.sticky was set to false in turn_off so it has to be reapplied anyway
|
||||
c.sticky = self.sticky
|
||||
|
||||
if anim_x then
|
||||
animate_turn_on(self, c, anim_x, "x")
|
||||
end
|
||||
if anim_y then
|
||||
animate_turn_on(self, c, anim_y, "y")
|
||||
end
|
||||
|
||||
helpers.client.turn_on(c)
|
||||
self:emit_signal("turn_on", c)
|
||||
|
||||
return
|
||||
end
|
||||
if not c then
|
||||
-- if no client was found, spawn one, find the corresponding window,
|
||||
-- apply the properties only once (until the next closing)
|
||||
local pid = awful.spawn.with_shell(self.command)
|
||||
if awesome.version ~= "v4.3" then
|
||||
ruled.client.append_rule({
|
||||
id = "scratchpad",
|
||||
rule = self.rule,
|
||||
properties = {
|
||||
-- If a scratchpad is opened it should spawn at the current tag
|
||||
-- the same way it will behave if the client was already open
|
||||
tag = awful.screen.focused().selected_tag,
|
||||
switch_to_tags = false,
|
||||
-- Hide the client until the gemoetry rules are applied
|
||||
hidden = true,
|
||||
minimized = true,
|
||||
},
|
||||
callback = function(c)
|
||||
-- For a reason I can't quite get the gemotery rules will fail to apply unless we use this timer
|
||||
gears.timer({
|
||||
timeout = 0.15,
|
||||
autostart = true,
|
||||
single_shot = true,
|
||||
callback = function()
|
||||
self:apply(c)
|
||||
c.hidden = false
|
||||
c.minimized = false
|
||||
-- Some clients fail to gain focus
|
||||
c:activate({})
|
||||
|
||||
if anim_x then
|
||||
animate_turn_on(self, c, anim_x, "x")
|
||||
end
|
||||
if anim_y then
|
||||
animate_turn_on(self, c, anim_y, "y")
|
||||
end
|
||||
|
||||
self:emit_signal("inital_apply", c)
|
||||
|
||||
-- Discord spawns 2 windows, so keep the rule until the 2nd window shows
|
||||
if c.name ~= "Discord Updater" then
|
||||
ruled.client.remove_rule("scratchpad")
|
||||
end
|
||||
-- In a case Discord is killed before the second window spawns
|
||||
c:connect_signal("request::unmanage", function()
|
||||
ruled.client.remove_rule("scratchpad")
|
||||
end)
|
||||
end,
|
||||
})
|
||||
end,
|
||||
})
|
||||
else
|
||||
local function inital_apply(c1)
|
||||
if helpers.client.is_child_of(c1, pid) then
|
||||
self:apply(c1)
|
||||
if anim_x then
|
||||
animate_turn_on(self, c1, anim_x, "x")
|
||||
end
|
||||
if anim_y then
|
||||
animate_turn_on(self, c1, anim_y, "y")
|
||||
end
|
||||
self:emit_signal("inital_apply", c1)
|
||||
client.disconnect_signal("manage", inital_apply)
|
||||
end
|
||||
end
|
||||
client.connect_signal("manage", inital_apply)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Called when the turn off animation has ended
|
||||
local function on_animate_turn_off_end(self, c, anim, tag, turn_off_on_end)
|
||||
anim:unsubscribe()
|
||||
anim.ended:unsubscribe()
|
||||
|
||||
if turn_off_on_end then
|
||||
-- When toggling off a scratchpad that's present on multiple tags
|
||||
-- depsite still being unminizmied on the other tags it will become invisible
|
||||
-- as it's position could be outside the screen from the animation
|
||||
c:geometry({
|
||||
x = self.geometry.x + c.screen.geometry.x,
|
||||
y = self.geometry.y + c.screen.geometry.y,
|
||||
width = self.geometry.width,
|
||||
height = self.geometry.height,
|
||||
})
|
||||
helpers.client.turn_off(c, tag)
|
||||
|
||||
self:emit_signal("turn_off", c)
|
||||
|
||||
self.in_anim = false
|
||||
end
|
||||
end
|
||||
|
||||
--- The turn off animation
|
||||
local function animate_turn_off(self, c, anim, axis, turn_off_on_end)
|
||||
local screen_on_toggled_scratchpad = c.screen
|
||||
local tag_on_toggled_scratchpad = screen_on_toggled_scratchpad.selected_tag
|
||||
|
||||
if c.floating == false then
|
||||
-- Save the client geometry before floating it
|
||||
local non_floating_x = c.x
|
||||
local non_floating_y = c.y
|
||||
local non_floating_width = c.width
|
||||
local non_floating_height = c.height
|
||||
|
||||
-- Can't animate non floating clients
|
||||
c.floating = true
|
||||
|
||||
-- Set the client geometry back to what it was before floating it
|
||||
c:geometry({
|
||||
x = non_floating_x,
|
||||
y = non_floating_y,
|
||||
width = non_floating_width,
|
||||
height = non_floating_height,
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
if axis == "x" then
|
||||
anim.pos = c.x
|
||||
else
|
||||
anim.pos = c.y
|
||||
end
|
||||
|
||||
anim:subscribe(function(pos)
|
||||
if c and c.valid then
|
||||
if axis == "x" then
|
||||
c.x = pos
|
||||
else
|
||||
c.y = pos
|
||||
end
|
||||
end
|
||||
self.in_anim = true
|
||||
|
||||
-- Handles changing tag mid animation
|
||||
-- Check for the following scenerio:
|
||||
-- Toggle on scratchpad at tag 1
|
||||
-- Toggle on scratchpad at tag 2
|
||||
-- Toggle off scratchpad at tag 1
|
||||
-- Switch to tag 2
|
||||
-- Outcome: The client will remain on tag 1 and will instead be removed from tag 2
|
||||
if screen_on_toggled_scratchpad.selected_tag ~= tag_on_toggled_scratchpad then
|
||||
on_animate_turn_off_end(self, c, anim, tag_on_toggled_scratchpad, true)
|
||||
end
|
||||
end)
|
||||
|
||||
anim:set(anim:initial())
|
||||
|
||||
anim.ended:subscribe(function()
|
||||
on_animate_turn_off_end(self, c, anim, nil, turn_off_on_end)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Turns the scratchpad off
|
||||
function Scratchpad:turn_off()
|
||||
local c = self:find()[1]
|
||||
if c and not self.in_anim then
|
||||
-- Get the tweens
|
||||
local anim_x = self.rubato.x
|
||||
local anim_y = self.rubato.y
|
||||
|
||||
local anim_x_duration = (anim_x and anim_x.duration) or 0
|
||||
local anim_y_duration = (anim_y and anim_y.duration) or 0
|
||||
|
||||
local turn_off_on_end = (anim_x_duration >= anim_y_duration) and true or false
|
||||
if anim_x then
|
||||
animate_turn_off(self, c, anim_x, "x", turn_off_on_end)
|
||||
end
|
||||
if anim_y then
|
||||
animate_turn_off(self, c, anim_y, "y", not turn_off_on_end)
|
||||
end
|
||||
|
||||
if not anim_x and not anim_y then
|
||||
helpers.client.turn_off(c)
|
||||
self:emit_signal("turn_off", c)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Turns the scratchpad off if it is focused otherwise it raises the scratchpad
|
||||
function Scratchpad:toggle()
|
||||
local is_turn_off = false
|
||||
local c = self:find()[1]
|
||||
if self.dont_focus_before_close then
|
||||
if c then
|
||||
if c.sticky and #c:tags() > 0 then
|
||||
is_turn_off = true
|
||||
else
|
||||
local current_tag = c.screen.selected_tag
|
||||
for k, tag in pairs(c:tags()) do
|
||||
if tag == current_tag then
|
||||
is_turn_off = true
|
||||
break
|
||||
else
|
||||
is_turn_off = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
is_turn_off = client.focus
|
||||
and awful.rules.match(client.focus, self.rule)
|
||||
end
|
||||
|
||||
if is_turn_off then
|
||||
self:turn_off()
|
||||
else
|
||||
self:turn_on()
|
||||
end
|
||||
end
|
||||
|
||||
--- Make the module callable without putting a `:new` at the end of it
|
||||
--
|
||||
-- @param args A table of possible arguments
|
||||
-- @return The new scratchpad object
|
||||
function Scratchpad.mt:__call(...)
|
||||
return Scratchpad:new(...)
|
||||
end
|
||||
|
||||
return setmetatable(Scratchpad, Scratchpad.mt)
|
274
config/awesome/module/bling/module/tabbed.lua
Normal file
|
@ -0,0 +1,274 @@
|
|||
--[[
|
||||
|
||||
This module currently works by adding a new property to each client that is tabbed.
|
||||
That new property is called bling_tabbed.
|
||||
So each client in a tabbed state has the property "bling_tabbed" which is a table.
|
||||
Each client that is not tabbed doesn't have that property.
|
||||
In the function themselves, the same object is refered to as "tabobj" which is why
|
||||
you will often see something like: "local tabobj = some_client.bling_tabbed" at the beginning
|
||||
of a function.
|
||||
|
||||
--]]
|
||||
|
||||
local awful = require("awful")
|
||||
local wibox = require("wibox")
|
||||
local gears = require("gears")
|
||||
local beautiful = require("beautiful")
|
||||
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
|
||||
|
||||
local bar_style = beautiful.tabbar_style or "default"
|
||||
local bar = require(
|
||||
tostring(...):match(".*bling") .. ".widget.tabbar." .. bar_style
|
||||
)
|
||||
|
||||
tabbed = {}
|
||||
|
||||
-- helper function to connect to the (un)focus signals
|
||||
local function update_tabbar_from(c)
|
||||
if not c or not c.bling_tabbed then
|
||||
return
|
||||
end
|
||||
tabbed.update_tabbar(c.bling_tabbed)
|
||||
end
|
||||
|
||||
-- used to change focused tab relative to the currently focused one
|
||||
tabbed.iter = function(idx)
|
||||
if not idx then
|
||||
idx = 1
|
||||
end
|
||||
if not client.focus or not client.focus.bling_tabbed then
|
||||
return
|
||||
end
|
||||
local tabobj = client.focus.bling_tabbed
|
||||
local new_idx = (tabobj.focused_idx + idx) % #tabobj.clients
|
||||
if new_idx == 0 then
|
||||
new_idx = #tabobj.clients
|
||||
end
|
||||
tabbed.switch_to(tabobj, new_idx)
|
||||
end
|
||||
|
||||
-- removes a given client from its tab object
|
||||
tabbed.remove = function(c)
|
||||
if not c or not c.bling_tabbed then
|
||||
return
|
||||
end
|
||||
local tabobj = c.bling_tabbed
|
||||
table.remove(tabobj.clients, tabobj.focused_idx)
|
||||
if not beautiful.tabbar_disable then
|
||||
awful.titlebar.hide(c, bar.position)
|
||||
end
|
||||
c.bling_tabbed = nil
|
||||
c:disconnect_signal("focus", update_tabbar_from)
|
||||
c:disconnect_signal("unfocus", update_tabbar_from)
|
||||
awesome.emit_signal("bling::tabbed::client_removed", tabobj, c)
|
||||
tabbed.switch_to(tabobj, 1)
|
||||
end
|
||||
|
||||
-- removes the currently focused client from the tab object
|
||||
tabbed.pop = function()
|
||||
if not client.focus or not client.focus.bling_tabbed then
|
||||
return
|
||||
end
|
||||
tabbed.remove(client.focus)
|
||||
end
|
||||
|
||||
-- adds a client to a given tabobj
|
||||
tabbed.add = function(c, tabobj)
|
||||
if c.bling_tabbed then
|
||||
tabbed.remove(c)
|
||||
end
|
||||
c:connect_signal("focus", update_tabbar_from)
|
||||
c:connect_signal("unfocus", update_tabbar_from)
|
||||
helpers.client.sync(c, tabobj.clients[tabobj.focused_idx])
|
||||
tabobj.clients[#tabobj.clients + 1] = c
|
||||
tabobj.focused_idx = #tabobj.clients
|
||||
-- calls update even though switch_to calls update again
|
||||
-- but the new client needs to have the tabobj property
|
||||
-- before a clean switch can happen
|
||||
tabbed.update(tabobj)
|
||||
awesome.emit_signal("bling::tabbed::client_added", tabobj, c)
|
||||
tabbed.switch_to(tabobj, #tabobj.clients)
|
||||
end
|
||||
|
||||
-- use xwininfo to select one client and make it tab in the currently focused tab
|
||||
tabbed.pick = function()
|
||||
if not client.focus then
|
||||
return
|
||||
end
|
||||
-- this function uses xwininfo to grab a client window id which is then
|
||||
-- compared to all other clients window ids
|
||||
|
||||
local xwininfo_cmd =
|
||||
[[ xwininfo | grep 'xwininfo: Window id:' | cut -d " " -f 4 ]]
|
||||
awful.spawn.easy_async_with_shell(xwininfo_cmd, function(output)
|
||||
for _, c in ipairs(client.get()) do
|
||||
if tonumber(c.window) == tonumber(output) then
|
||||
if not client.focus.bling_tabbed and not c.bling_tabbed then
|
||||
tabbed.init(client.focus)
|
||||
tabbed.add(c, client.focus.bling_tabbed)
|
||||
end
|
||||
if not client.focus.bling_tabbed and c.bling_tabbed then
|
||||
tabbed.add(client.focus, c.bling_tabbed)
|
||||
end
|
||||
if client.focus.bling_tabbed and not c.bling_tabbed then
|
||||
tabbed.add(c, client.focus.bling_tabbed)
|
||||
end
|
||||
-- TODO: Should also merge tabs when focus and picked
|
||||
-- both are tab groups
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- select a client by direction and make it tab in the currently focused tab
|
||||
tabbed.pick_by_direction = function(direction)
|
||||
local sel = client.focus
|
||||
if not sel then
|
||||
return
|
||||
end
|
||||
if not sel.bling_tabbed then
|
||||
tabbed.init(sel)
|
||||
end
|
||||
local c = helpers.client.get_by_direction(direction)
|
||||
if not c then
|
||||
return
|
||||
end
|
||||
tabbed.add(c, sel.bling_tabbed)
|
||||
end
|
||||
|
||||
-- use dmenu to select a client and make it tab in the currently focused tab
|
||||
tabbed.pick_with_dmenu = function(dmenu_command)
|
||||
if not client.focus then
|
||||
return
|
||||
end
|
||||
|
||||
if not dmenu_command then
|
||||
dmenu_command = "rofi -dmenu -i"
|
||||
end
|
||||
|
||||
-- get all clients from the current tag
|
||||
-- ignores the case where multiple tags are selected
|
||||
local t = awful.screen.focused().selected_tag
|
||||
local list_clients = {}
|
||||
local list_clients_string = ""
|
||||
for idx, c in ipairs(t:clients()) do
|
||||
if c.window ~= client.focus.window then
|
||||
list_clients[#list_clients + 1] = c
|
||||
if #list_clients ~= 1 then
|
||||
list_clients_string = list_clients_string .. "\\n"
|
||||
end
|
||||
list_clients_string = list_clients_string
|
||||
.. tostring(c.window)
|
||||
.. " "
|
||||
.. c.name
|
||||
end
|
||||
end
|
||||
|
||||
if #list_clients == 0 then
|
||||
return
|
||||
end
|
||||
-- calls the actual dmenu
|
||||
local xprop_cmd = [[ echo -e "]]
|
||||
.. list_clients_string
|
||||
.. [[" | ]]
|
||||
.. dmenu_command
|
||||
.. [[ | awk '{ print $1 }' ]]
|
||||
awful.spawn.easy_async_with_shell(xprop_cmd, function(output)
|
||||
for _, c in ipairs(list_clients) do
|
||||
if tonumber(c.window) == tonumber(output) then
|
||||
if not client.focus.bling_tabbed then
|
||||
tabbed.init(client.focus)
|
||||
end
|
||||
local tabobj = client.focus.bling_tabbed
|
||||
tabbed.add(c, tabobj)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- update everything about one tab object
|
||||
tabbed.update = function(tabobj)
|
||||
local currently_focused_c = tabobj.clients[tabobj.focused_idx]
|
||||
-- update tabobj of each client and other things
|
||||
for idx, c in ipairs(tabobj.clients) do
|
||||
if c.valid then
|
||||
c.bling_tabbed = tabobj
|
||||
helpers.client.sync(c, currently_focused_c)
|
||||
-- the following handles killing a client while the client is tabbed
|
||||
c:connect_signal("unmanage", function(c)
|
||||
tabbed.remove(c)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Maybe remove if I'm the only one using it?
|
||||
awesome.emit_signal("bling::tabbed::update", tabobj)
|
||||
if not beautiful.tabbar_disable then
|
||||
tabbed.update_tabbar(tabobj)
|
||||
end
|
||||
end
|
||||
|
||||
-- change focused tab by absolute index
|
||||
tabbed.switch_to = function(tabobj, new_idx)
|
||||
local old_focused_c = tabobj.clients[tabobj.focused_idx]
|
||||
tabobj.focused_idx = new_idx
|
||||
for idx, c in ipairs(tabobj.clients) do
|
||||
if idx ~= new_idx then
|
||||
helpers.client.turn_off(c)
|
||||
else
|
||||
helpers.client.turn_on(c)
|
||||
c:raise()
|
||||
if old_focused_c and old_focused_c.valid then
|
||||
c:swap(old_focused_c)
|
||||
end
|
||||
helpers.client.sync(c, old_focused_c)
|
||||
end
|
||||
end
|
||||
awesome.emit_signal("bling::tabbed::changed_focus", tabobj)
|
||||
tabbed.update(tabobj)
|
||||
end
|
||||
|
||||
tabbed.update_tabbar = function(tabobj)
|
||||
local flexlist = bar.layout()
|
||||
local tabobj_focused_client = tabobj.clients[tabobj.focused_idx]
|
||||
local tabobj_is_focused = (client.focus == tabobj_focused_client)
|
||||
-- itearte over all tabbed clients to create the widget tabbed list
|
||||
for idx, c in ipairs(tabobj.clients) do
|
||||
local buttons = gears.table.join(awful.button({}, 1, function()
|
||||
tabbed.switch_to(tabobj, idx)
|
||||
end))
|
||||
local wid_temp = bar.create(c, (idx == tabobj.focused_idx), buttons,
|
||||
not tabobj_is_focused)
|
||||
flexlist:add(wid_temp)
|
||||
end
|
||||
-- add tabbar to each tabbed client (clients will be hided anyway)
|
||||
for _, c in ipairs(tabobj.clients) do
|
||||
local titlebar = awful.titlebar(c, {
|
||||
bg = bar.bg_normal,
|
||||
size = bar.size,
|
||||
position = bar.position,
|
||||
})
|
||||
titlebar:setup({ layout = wibox.layout.flex.horizontal, flexlist })
|
||||
end
|
||||
end
|
||||
|
||||
tabbed.init = function(c)
|
||||
local tabobj = {}
|
||||
tabobj.clients = { c }
|
||||
c:connect_signal("focus", update_tabbar_from)
|
||||
c:connect_signal("unfocus", update_tabbar_from)
|
||||
tabobj.focused_idx = 1
|
||||
tabbed.update(tabobj)
|
||||
end
|
||||
|
||||
if beautiful.tabbed_spawn_in_tab then
|
||||
client.connect_signal("manage", function(c)
|
||||
local s = awful.screen.focused()
|
||||
local previous_client = awful.client.focus.history.get(s, 1)
|
||||
if previous_client and previous_client.bling_tabbed then
|
||||
tabbed.add(c, previous_client.bling_tabbed)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return tabbed
|
56
config/awesome/module/bling/module/tiled_wallpaper.lua
Normal file
|
@ -0,0 +1,56 @@
|
|||
--[[
|
||||
This module makes use of cairo surfaces
|
||||
For documentation take a look at the C docs:
|
||||
https://www.cairographics.org/
|
||||
They can be applied to lua by changing the naming conventions
|
||||
and adjusting for the missing namespaces (and classes)
|
||||
for example:
|
||||
cairo_rectangle(cr, 1, 1, 1, 1) in C would be written as
|
||||
cr:rectangle(1, 1, 1, 1) in lua
|
||||
and
|
||||
cairo_fill(cr) in C would be written as
|
||||
cr:fill() in lua
|
||||
--]]
|
||||
|
||||
local cairo = require("lgi").cairo
|
||||
local gears = require("gears")
|
||||
|
||||
function create_tiled_wallpaper(str, s, args_table)
|
||||
-- user input
|
||||
args_table = args_table or {}
|
||||
local fg = args_table.fg or "#ff0000"
|
||||
local bg = args_table.bg or "#00ffff"
|
||||
local offset_x = args_table.offset_x
|
||||
local offset_y = args_table.offset_y
|
||||
local font = args_table.font or "Hack"
|
||||
local font_size = tonumber(args_table.font_size) or 16
|
||||
local zickzack_bool = args_table.zickzack or false
|
||||
local padding = args_table.padding or 100
|
||||
|
||||
-- create cairo image wallpaper
|
||||
local img = cairo.ImageSurface(cairo.Format.RGB24, padding, padding)
|
||||
cr = cairo.Context(img)
|
||||
|
||||
cr:set_source(gears.color(bg))
|
||||
cr:paint()
|
||||
|
||||
cr:set_source(gears.color(fg))
|
||||
|
||||
cr:set_font_size(font_size)
|
||||
cr:select_font_face(font)
|
||||
|
||||
if zickzack_bool then
|
||||
cr:set_source(gears.color(fg))
|
||||
cr:move_to(padding / 2 + font_size, padding / 2 + font_size)
|
||||
cr:show_text(str)
|
||||
end
|
||||
|
||||
cr:set_source(gears.color(fg))
|
||||
cr:move_to(font_size, font_size)
|
||||
cr:show_text(str)
|
||||
|
||||
-- tile cairo image
|
||||
gears.wallpaper.tiled(img, s, { x = offset_x, y = offset_y })
|
||||
end
|
||||
|
||||
return create_tiled_wallpaper
|
339
config/awesome/module/bling/module/wallpaper.lua
Normal file
|
@ -0,0 +1,339 @@
|
|||
---------------------------------------------------------------------------
|
||||
-- High-level declarative function for setting your wallpaper.
|
||||
--
|
||||
--
|
||||
-- An easy way to setup a complex wallpaper with slideshow, random, schedule, extensibility.
|
||||
--
|
||||
-- @usage
|
||||
-- local wallpaper = require("wallpaper")
|
||||
-- -- A silly example
|
||||
-- wallpaper.setup { -- I want a wallpaper
|
||||
-- change_timer = 500, -- changing every 5 minutes
|
||||
-- set_function = wallpaper.setters.random, -- in a random way
|
||||
-- wallpaper = {"#abcdef",
|
||||
-- "~/Pictures",
|
||||
-- wallpaper.setters.awesome}, -- from this list (a color, a directory with pictures and the Awesome wallpaper)
|
||||
-- recursive = false, -- do not read subfolders of "~/Pictures"
|
||||
-- position = "centered", -- center it on the screen (for pictures)
|
||||
-- scale = 2, -- 2 time bigger (for pictures)
|
||||
-- }
|
||||
--
|
||||
-- @author Grumph
|
||||
-- @copyright 2021 Grumph
|
||||
--
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
local awful = require("awful")
|
||||
local beautiful = require("beautiful")
|
||||
local gears = require("gears")
|
||||
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
|
||||
|
||||
local setters = {}
|
||||
|
||||
--- Apply a wallpaper.
|
||||
--
|
||||
-- This function is a helper that will apply a wallpaper_object,
|
||||
-- either using gears.wallpaper.set or gears.wallpaper.* higher level functions when applicable.
|
||||
-- @param wallpaper_object A wallpaper object, either
|
||||
-- a `pattern` (see `gears.wallpaper.set`)
|
||||
-- a `surf` (see `gears.wallpaper.centered`)
|
||||
-- a function that actually sets the wallpaper.
|
||||
-- @tparam table args The argument table containing any of the arguments below.
|
||||
-- @int[opt=nil] args.screen The screen to use (as used in `gears.wallpaper` functions)
|
||||
-- @string[opt=nil or "centered"] args.position The `gears.wallpaper` position function to use.
|
||||
-- Must be set when wallpaper is a file.
|
||||
-- It can be `"centered"`, `"fit"`, `"tiled"` or `"maximized"`.
|
||||
-- @string[opt=beautiful.bg_normal or "black"] args.background See `gears.wallpaper`.
|
||||
-- @bool[opt=false] args.ignore_aspect See `gears.wallpaper`.
|
||||
-- @tparam[opt={x=0,y=0}] table args.offset See `gears.wallpaper`.
|
||||
-- @int[opt=1] args.scale See `gears.wallpaper`.
|
||||
function apply(wallpaper_object, args)
|
||||
args.background = args.background or beautiful.bg_normal or "black"
|
||||
args.ignore_aspect = args.ignore_aspect or false -- false = keep aspect ratio
|
||||
args.offset = args.offset or { x = 0, y = 0 }
|
||||
args.scale = args.scale or 1
|
||||
local positions = {
|
||||
["centered"] = function()
|
||||
gears.wallpaper.centered(
|
||||
wallpaper_object,
|
||||
args.screen,
|
||||
args.background,
|
||||
args.scale
|
||||
)
|
||||
end,
|
||||
["tiled"] = function()
|
||||
gears.wallpaper.tiled(wallpaper_object, args.screen, args.offset)
|
||||
end,
|
||||
["maximized"] = function()
|
||||
gears.wallpaper.maximized(
|
||||
wallpaper_object,
|
||||
args.screen,
|
||||
args.ignore_aspect,
|
||||
args.offset
|
||||
)
|
||||
end,
|
||||
["fit"] = function()
|
||||
gears.wallpaper.fit(wallpaper_object, args.screen, args.background)
|
||||
end,
|
||||
}
|
||||
if
|
||||
type(wallpaper_object) == "string"
|
||||
and gears.filesystem.file_readable(wallpaper_object)
|
||||
then
|
||||
-- path of an image file, we use a position function
|
||||
local p = args.position or "centered"
|
||||
positions[p]()
|
||||
elseif type(wallpaper_object) == "function" then
|
||||
-- function
|
||||
wallpaper_object(args)
|
||||
elseif
|
||||
(not gears.color.ensure_pango_color(wallpaper_object, nil))
|
||||
and args.position
|
||||
then
|
||||
-- if the user sets a position function, wallpaper_object should be a cairo surface
|
||||
positions[args.position]()
|
||||
else
|
||||
gears.wallpaper.set(wallpaper_object)
|
||||
end
|
||||
end
|
||||
|
||||
--- Converts `args.wallpaper` to a list of `wallpaper_objects` readable by `apply` function).
|
||||
--
|
||||
-- @tparam table args The argument table containing the argument below.
|
||||
-- @param[opt=`beautiful.wallpaper_path` or `"black"`] args.wallpaper A wallpaper object.
|
||||
-- It can be a color or a cairo pattern (what `gears.wallpaper.set` understands),
|
||||
-- a cairo suface (set with gears.wallpaper.set if `args.position` is nil, or with
|
||||
-- `gears.wallpaper` position functions, see `args.position`),
|
||||
-- a function similar to args.set_function that will effectively set a wallpaper (usually
|
||||
-- with `gears.wallpaper` functions),
|
||||
-- a path to a file,
|
||||
-- path to a directory containing images,
|
||||
-- or a list with any of the previous choices.
|
||||
-- @tparam[opt=`{"jpg", "jpeg", "png", "bmp"}`] table args.image_formats A list of
|
||||
-- file extensions to filter when `args.wallpaper` is a directory.
|
||||
-- @bool[opt=true] args.recursive Either to recurse or not when `args.wallpaper` is a directory.
|
||||
-- @treturn table A list of `wallpaper_objects` (what `apply` can read).
|
||||
-- @see apply
|
||||
function prepare_list(args)
|
||||
args.image_formats = args.image_formats or { "jpg", "jpeg", "png", "bmp" }
|
||||
args.recursive = args.recursive or true
|
||||
|
||||
local wallpapers = (args.wallpaper or beautiful.wallpaper_path or "black")
|
||||
local res = {}
|
||||
if type(wallpapers) ~= "table" then
|
||||
wallpapers = { wallpapers }
|
||||
end
|
||||
for _, w in ipairs(wallpapers) do
|
||||
-- w is either:
|
||||
-- - a directory path (string)
|
||||
-- - an image path or a color (string)
|
||||
-- - a cairo surface or a cairo pattern
|
||||
-- - a function for setting the wallpaper
|
||||
if type(w) == "string" and gears.filesystem.dir_readable(w) then
|
||||
local file_list = helpers.filesystem.list_directory_files(
|
||||
w,
|
||||
args.image_formats,
|
||||
args.recursive
|
||||
)
|
||||
for _, f in ipairs(file_list) do
|
||||
res[#res + 1] = w .. "/" .. f
|
||||
end
|
||||
else
|
||||
res[#res + 1] = w
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local simple_index = 0
|
||||
--- Set the next wallpaper in a list.
|
||||
--
|
||||
-- @tparam table args See `prepare_list` and `apply` arguments
|
||||
-- @see apply
|
||||
-- @see prepare_list
|
||||
function setters.simple(args)
|
||||
local wallpapers = prepare_list(args)
|
||||
simple_index = (simple_index % #wallpapers) + 1
|
||||
apply(wallpapers[simple_index], args)
|
||||
end
|
||||
|
||||
--- Set a random wallpaper from a list.
|
||||
--
|
||||
-- @tparam table args See `prepare_list` and `apply` arguments
|
||||
-- @see apply
|
||||
-- @see prepare_list
|
||||
function setters.random(args)
|
||||
local wallpapers = prepare_list(args)
|
||||
apply(wallpapers[math.random(#wallpapers)], args)
|
||||
end
|
||||
|
||||
local simple_schedule_object = nil
|
||||
--- A schedule setter.
|
||||
--
|
||||
-- This simple schedule setter was freely inspired by [dynamic-wallpaper](https://github.com/manilarome/awesome-glorious-widgets/blob/master/dynamic-wallpaper/init.lua).
|
||||
-- @tparam table args The argument table containing any of the arguments below.
|
||||
-- @tparam table args.wallpaper The schedule table, with the form
|
||||
-- {
|
||||
-- ["HH:MM:SS"] = wallpaper,
|
||||
-- ["HH:MM:SS"] = wallpaper2,
|
||||
-- }
|
||||
-- The wallpapers definition can be anything the `schedule_set_function` can read
|
||||
-- (what you would place in `args.wallpaper` for this function),
|
||||
-- @tparam[opt=`setters.simple`] function args.wallpaper_set_function The set_function used by default
|
||||
function setters.simple_schedule(args)
|
||||
local function update_wallpaper()
|
||||
local fake_args = gears.table.join(args, {
|
||||
wallpaper = args.wallpaper[simple_schedule_object.closest_lower_time],
|
||||
})
|
||||
simple_schedule_object.schedule_set_function(fake_args)
|
||||
end
|
||||
if not simple_schedule_object then
|
||||
simple_schedule_object = {}
|
||||
-- initialize the schedule object, so we don't do it for every call
|
||||
simple_schedule_object.schedule_set_function = args.schedule_set_function
|
||||
or setters.simple
|
||||
-- we get the sorted time keys
|
||||
simple_schedule_object.times = {}
|
||||
for k in pairs(args.wallpaper) do
|
||||
table.insert(simple_schedule_object.times, k)
|
||||
end
|
||||
table.sort(simple_schedule_object.times)
|
||||
-- now we get the closest time which is below current time (the current applicable period)
|
||||
local function update_timer()
|
||||
local current_time = os.date("%H:%M:%S")
|
||||
local next_time = simple_schedule_object.times[1]
|
||||
simple_schedule_object.closest_lower_time =
|
||||
simple_schedule_object.times[#simple_schedule_object.times]
|
||||
for _, k in ipairs(simple_schedule_object.times) do
|
||||
if k > current_time then
|
||||
next_time = k
|
||||
break
|
||||
end
|
||||
simple_schedule_object.closest_lower_time = k
|
||||
end
|
||||
simple_schedule_object.timer.timeout = helpers.time.time_diff(
|
||||
next_time,
|
||||
current_time
|
||||
)
|
||||
if simple_schedule_object.timer.timeout < 0 then
|
||||
-- the next_time is the day after, so we add 24 hours to the timer
|
||||
simple_schedule_object.timer.timeout = simple_schedule_object.timer.timeout
|
||||
+ 86400
|
||||
end
|
||||
simple_schedule_object.timer:again()
|
||||
update_wallpaper()
|
||||
end
|
||||
simple_schedule_object.timer = gears.timer({
|
||||
callback = update_timer,
|
||||
})
|
||||
update_timer()
|
||||
else
|
||||
-- if called again (usually when the change_timer is set), we just change the wallpaper depending on current parameters
|
||||
update_wallpaper()
|
||||
end
|
||||
end
|
||||
|
||||
--- Set the AWESOME wallpaper.
|
||||
--
|
||||
-- @tparam table args The argument table containing the argument below.
|
||||
-- @param[opt=`beautiful.bg_normal`] args.colors.bg The bg color.
|
||||
-- If the default is used, the color is darkened if `beautiful.bg_normal` is light
|
||||
-- or lightned if `beautiful.bg_normal` is dark.
|
||||
-- @param[opt=`beautiful.fg_normal`] args.colors.fg The fg color.
|
||||
-- @param[opt=`beautiful.fg_focus`] args.colors.alt_fg The alt_fg color.
|
||||
--
|
||||
-- see beautiful.theme_assets.wallpaper
|
||||
function setters.awesome_wallpaper(args)
|
||||
local colors = {
|
||||
bg = beautiful.bg_normal,
|
||||
fg = beautiful.fg_normal,
|
||||
alt_fg = beautiful.bg_focus,
|
||||
}
|
||||
colors.bg = helpers.color.is_dark(beautiful.bg_normal)
|
||||
and helpers.color.lighten(colors.bg)
|
||||
or helpers.color.darken(colors.bg)
|
||||
if type(args.colors) == "table" then
|
||||
colors.bg = args.colors.bg or colors.bg
|
||||
colors.fg = args.colors.fg or colors.fg
|
||||
colors.alt_fg = args.colors.alt_fg or colors.alt_fg
|
||||
end
|
||||
-- Generate wallpaper:
|
||||
if not args.screen then
|
||||
for s in screen do
|
||||
gears.wallpaper.set(
|
||||
beautiful.theme_assets.wallpaper(
|
||||
colors.bg,
|
||||
colors.fg,
|
||||
colors.alt_fg,
|
||||
s
|
||||
)
|
||||
)
|
||||
end
|
||||
else
|
||||
gears.wallpaper.set(
|
||||
beautiful.theme_assets.wallpaper(
|
||||
colors.bg,
|
||||
colors.fg,
|
||||
colors.alt_fg,
|
||||
args.screen
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
--- Setup a wallpaper.
|
||||
--
|
||||
-- @tparam table args Parameters for the wallpaper. It may also contain all parameters your `args.set_function` needs
|
||||
-- @int[opt=nil] args.screen The screen to use (as used in `gears.wallpaper` functions)
|
||||
-- @int[opt=nil] args.change_timer Time in seconds for wallpaper changes
|
||||
-- @tparam[opt=`setters.awesome` or `setters.simple`] function args.set_function A function to set the wallpaper
|
||||
-- It takes args as parameter (the same args as the setup function).
|
||||
-- This function is called at `"request::wallpaper"` `screen` signals and at `args.change_timer` timeouts.
|
||||
-- There is no obligation, but for consistency, the function should use `args.wallpaper` as a feeder.
|
||||
-- If `args.wallpaper` is defined, the default function is `setters.simple`, else it will be `setters.awesome`.
|
||||
--
|
||||
-- @usage
|
||||
-- local wallpaper = require("wallpaper")
|
||||
-- wallpaper.setup {
|
||||
-- change_timer = 631, -- Prime number is better
|
||||
-- set_function = wallpaper.setters.random,
|
||||
-- -- parameters for the random setter
|
||||
-- wallpaper = '/data/pictures/wallpapers',
|
||||
-- position = "maximized",
|
||||
-- }
|
||||
--
|
||||
-- @see apply
|
||||
-- @see prepare_list
|
||||
-- @see setters.simple
|
||||
function setup(args)
|
||||
local config = args or {}
|
||||
config.set_function = config.set_function
|
||||
or (config.wallpaper and setters.simple or setters.awesome_wallpaper)
|
||||
local function set_wallpaper(s)
|
||||
config.screen = s or config.screen
|
||||
config.set_function(config)
|
||||
end
|
||||
|
||||
if config.change_timer and config.change_timer > 0 then
|
||||
gears.timer({
|
||||
timeout = config.change_timer,
|
||||
call_now = false,
|
||||
autostart = true,
|
||||
callback = function()
|
||||
set_wallpaper()
|
||||
end,
|
||||
})
|
||||
end
|
||||
if awesome.version == "v4.3" then
|
||||
awful.screen.connect_for_each_screen(set_wallpaper)
|
||||
else
|
||||
screen.connect_signal("request::wallpaper", set_wallpaper)
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
setup = setup,
|
||||
setters = setters,
|
||||
apply = apply,
|
||||
prepare_list = prepare_list,
|
||||
}
|
88
config/awesome/module/bling/module/window_swallowing.lua
Normal file
|
@ -0,0 +1,88 @@
|
|||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
local beautiful = require("beautiful")
|
||||
|
||||
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
|
||||
|
||||
-- It might actually swallow too much, that's why there is a filter option by classname
|
||||
-- without the don't-swallow-list it would also swallow for example
|
||||
-- file pickers or new firefox windows spawned by an already existing one
|
||||
|
||||
local window_swallowing_activated = false
|
||||
|
||||
-- you might want to add or remove applications here
|
||||
local dont_swallow_classname_list = beautiful.dont_swallow_classname_list
|
||||
or { "firefox", "Gimp", "Google-chrome" }
|
||||
local activate_dont_swallow_filter = beautiful.dont_swallow_filter_activated
|
||||
or true
|
||||
|
||||
-- checks if client classname matches with any entry of the dont-swallow-list
|
||||
local function check_if_swallow(c)
|
||||
if not activate_dont_swallow_filter then
|
||||
return true
|
||||
end
|
||||
for _, classname in ipairs(dont_swallow_classname_list) do
|
||||
if classname == c.class then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- the function that will be connected to / disconnected from the spawn client signal
|
||||
local function manage_clientspawn(c)
|
||||
-- get the last focused window to check if it is a parent window
|
||||
local parent_client = awful.client.focus.history.get(c.screen, 1)
|
||||
if not parent_client then
|
||||
return
|
||||
end
|
||||
|
||||
-- io.popen is normally discouraged. Should probably be changed
|
||||
local handle = io.popen(
|
||||
[[pstree -T -p -a -s ]]
|
||||
.. tostring(c.pid)
|
||||
.. [[ | sed '2q;d' | grep -o '[0-9]*$' | tr -d '\n']]
|
||||
)
|
||||
local parent_pid = handle:read("*a")
|
||||
handle:close()
|
||||
|
||||
if
|
||||
(tostring(parent_pid) == tostring(parent_client.pid))
|
||||
and check_if_swallow(c)
|
||||
then
|
||||
c:connect_signal("unmanage", function()
|
||||
helpers.client.turn_on(parent_client)
|
||||
helpers.client.sync(parent_client, c)
|
||||
end)
|
||||
|
||||
helpers.client.sync(c, parent_client)
|
||||
helpers.client.turn_off(parent_client)
|
||||
end
|
||||
end
|
||||
|
||||
-- without the following functions that module would be autoloaded by require("bling")
|
||||
-- a toggle window swallowing hotkey is also possible that way
|
||||
|
||||
local function start()
|
||||
client.connect_signal("manage", manage_clientspawn)
|
||||
window_swallowing_activated = true
|
||||
end
|
||||
|
||||
local function stop()
|
||||
client.disconnect_signal("manage", manage_clientspawn)
|
||||
window_swallowing_activated = false
|
||||
end
|
||||
|
||||
local function toggle()
|
||||
if window_swallowing_activated then
|
||||
stop()
|
||||
else
|
||||
start()
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
start = start,
|
||||
stop = stop,
|
||||
toggle = toggle,
|
||||
}
|
3
config/awesome/module/bling/signal/init.lua
Normal file
|
@ -0,0 +1,3 @@
|
|||
return {
|
||||
playerctl = require(... .. ".playerctl"),
|
||||
}
|
46
config/awesome/module/bling/signal/playerctl/init.lua
Normal file
|
@ -0,0 +1,46 @@
|
|||
local awful = require("awful")
|
||||
local gtimer = require("gears.timer")
|
||||
local beautiful = require("beautiful")
|
||||
local naughty = require("naughty")
|
||||
|
||||
-- Use CLI backend as default as it is supported on most if not all systems
|
||||
local backend_config = beautiful.playerctl_backend or "playerctl_cli"
|
||||
local backends = {
|
||||
playerctl_cli = require(... .. ".playerctl_cli"),
|
||||
playerctl_lib = require(... .. ".playerctl_lib"),
|
||||
}
|
||||
|
||||
local backend = nil
|
||||
|
||||
local function enable_wrapper(args)
|
||||
local open = naughty.action { name = "Open" }
|
||||
|
||||
open:connect_signal("invoked", function()
|
||||
awful.spawn("xdg-open https://blingcorp.github.io/bling/#/signals/pctl")
|
||||
end)
|
||||
|
||||
gtimer.delayed_call(function()
|
||||
naughty.notify({
|
||||
title = "Bling Error",
|
||||
text = "Global signals are deprecated! Please take a look at the playerctl documentation.",
|
||||
app_name = "Bling Error",
|
||||
app_icon = "system-error",
|
||||
actions = { open }
|
||||
})
|
||||
end)
|
||||
|
||||
backend_config = (args and args.backend) or backend_config
|
||||
backend = backends[backend_config](args)
|
||||
return backend
|
||||
end
|
||||
|
||||
local function disable_wrapper()
|
||||
backend:disable()
|
||||
end
|
||||
|
||||
return {
|
||||
lib = backends.playerctl_lib,
|
||||
cli = backends.playerctl_cli,
|
||||
enable = enable_wrapper,
|
||||
disable = disable_wrapper
|
||||
}
|
348
config/awesome/module/bling/signal/playerctl/playerctl_cli.lua
Normal file
|
@ -0,0 +1,348 @@
|
|||
-- Playerctl signals
|
||||
--
|
||||
-- Provides:
|
||||
-- metadata
|
||||
-- title (string)
|
||||
-- artist (string)
|
||||
-- album_path (string)
|
||||
-- album (string)
|
||||
-- player_name (string)
|
||||
-- position
|
||||
-- interval_sec (number)
|
||||
-- length_sec (number)
|
||||
-- playback_status
|
||||
-- playing (boolean)
|
||||
-- volume
|
||||
-- volume (number)
|
||||
-- loop_status
|
||||
-- loop_status (string)
|
||||
-- shuffle
|
||||
-- shuffle (bool)
|
||||
-- no_players
|
||||
-- (No parameters)
|
||||
|
||||
local awful = require("awful")
|
||||
local gobject = require("gears.object")
|
||||
local gtable = require("gears.table")
|
||||
local gtimer = require("gears.timer")
|
||||
local gstring = require("gears.string")
|
||||
local beautiful = require("beautiful")
|
||||
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
|
||||
local setmetatable = setmetatable
|
||||
local tonumber = tonumber
|
||||
local ipairs = ipairs
|
||||
local type = type
|
||||
local capi = { awesome = awesome }
|
||||
|
||||
local playerctl = { mt = {} }
|
||||
|
||||
function playerctl:disable()
|
||||
self._private.metadata_timer:stop()
|
||||
self._private.metadata_timer = nil
|
||||
awful.spawn.with_shell("killall playerctl")
|
||||
end
|
||||
|
||||
function playerctl:pause(player)
|
||||
if player ~= nil then
|
||||
awful.spawn.with_shell("playerctl --player=" .. player .. " pause")
|
||||
else
|
||||
awful.spawn.with_shell(self._private.cmd .. "pause")
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:play(player)
|
||||
if player ~= nil then
|
||||
awful.spawn.with_shell("playerctl --player=" .. player .. " play")
|
||||
else
|
||||
awful.spawn.with_shell(self._private.cmd .. "play")
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:stop(player)
|
||||
if player ~= nil then
|
||||
awful.spawn.with_shell("playerctl --player=" .. player .. " stop")
|
||||
else
|
||||
awful.spawn.with_shell(self._private.cmd .. "stop")
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:play_pause(player)
|
||||
if player ~= nil then
|
||||
awful.spawn.with_shell("playerctl --player=" .. player .. " play-pause")
|
||||
else
|
||||
awful.spawn.with_shell(self._private.cmd .. "play-pause")
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:previous(player)
|
||||
if player ~= nil then
|
||||
awful.spawn.with_shell("playerctl --player=" .. player .. " previous")
|
||||
else
|
||||
awful.spawn.with_shell(self._private.cmd .. "previous")
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:next(player)
|
||||
if player ~= nil then
|
||||
awful.spawn.with_shell("playerctl --player=" .. player .. " next")
|
||||
else
|
||||
awful.spawn.with_shell(self._private.cmd .. "next")
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:set_loop_status(loop_status, player)
|
||||
if player ~= nil then
|
||||
awful.spawn.with_shell("playerctl --player=" .. player .. " loop " .. loop_status)
|
||||
else
|
||||
awful.spawn.with_shell(self._private.cmd .. "loop " .. loop_status)
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:cycle_loop_status(player)
|
||||
local function set_loop_status(loop_status)
|
||||
if loop_status == "None" then
|
||||
self:set_loop_status("Track")
|
||||
elseif loop_status == "Track" then
|
||||
self:set_loop_status("Playlist")
|
||||
elseif loop_status == "Playlist" then
|
||||
self:set_loop_status("None")
|
||||
end
|
||||
end
|
||||
|
||||
if player ~= nil then
|
||||
awful.spawn.easy_async_with_shell("playerctl --player=" .. player .. " loop", function(stdout)
|
||||
set_loop_status(stdout)
|
||||
end)
|
||||
else
|
||||
set_loop_status(self._private.loop_status)
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:set_position(position, player)
|
||||
if player ~= nil then
|
||||
awful.spawn.with_shell("playerctl --player=" .. player .. " position " .. position)
|
||||
else
|
||||
awful.spawn.with_shell(self._private.cmd .. "position " .. position)
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:set_shuffle(shuffle, player)
|
||||
shuffle = shuffle and "on" or "off"
|
||||
|
||||
if player ~= nil then
|
||||
awful.spawn.with_shell("playerctl --player=" .. player .. " shuffle " .. shuffle)
|
||||
else
|
||||
awful.spawn.with_shell(self._private.cmd .. "shuffle " .. shuffle)
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:cycle_shuffle(player)
|
||||
if player ~= nil then
|
||||
awful.spawn.easy_async_with_shell("playerctl --player=" .. player .. " shuffle", function(stdout)
|
||||
local shuffle = stdout == "on" and true or false
|
||||
self:set_shuffle(not self._private.shuffle)
|
||||
end)
|
||||
else
|
||||
self:set_shuffle(not self._private.shuffle)
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:set_volume(volume, player)
|
||||
if player ~= nil then
|
||||
awful.spawn.with_shell("playerctl --player=" .. player .. " volume " .. volume)
|
||||
else
|
||||
awful.spawn.with_shell(self._private.cmd .. "volume " .. volume)
|
||||
end
|
||||
end
|
||||
|
||||
local function emit_player_metadata(self)
|
||||
local metadata_cmd = self._private.cmd .. "metadata --format 'title_{{title}}artist_{{artist}}art_url_{{mpris:artUrl}}player_name_{{playerName}}album_{{album}}' -F"
|
||||
|
||||
awful.spawn.with_line_callback(metadata_cmd, {
|
||||
stdout = function(line)
|
||||
local title = gstring.xml_escape(line:match('title_(.*)artist_')) or ""
|
||||
local artist = gstring.xml_escape(line:match('artist_(.*)art_url_')) or ""
|
||||
local art_url = line:match('art_url_(.*)player_name_') or ""
|
||||
local player_name = line:match('player_name_(.*)album_') or ""
|
||||
local album = gstring.xml_escape(line:match('album_(.*)')) or ""
|
||||
|
||||
art_url = art_url:gsub('%\n', '')
|
||||
if player_name == "spotify" then
|
||||
art_url = art_url:gsub("open.spotify.com", "i.scdn.co")
|
||||
end
|
||||
|
||||
if self._private.metadata_timer
|
||||
and self._private.metadata_timer.started
|
||||
then
|
||||
self._private.metadata_timer:stop()
|
||||
end
|
||||
|
||||
self._private.metadata_timer = gtimer {
|
||||
timeout = self.debounce_delay,
|
||||
autostart = true,
|
||||
single_shot = true,
|
||||
callback = function()
|
||||
if title and title ~= "" then
|
||||
if art_url ~= "" then
|
||||
local art_path = os.tmpname()
|
||||
helpers.filesystem.save_image_async_curl(art_url, art_path, function()
|
||||
self:emit_signal("metadata", title, artist, art_path, album, player_name)
|
||||
capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, art_path)
|
||||
end)
|
||||
else
|
||||
self:emit_signal("metadata", title, artist, "", album, player_name)
|
||||
capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, "")
|
||||
end
|
||||
else
|
||||
self:emit_signal("no_players")
|
||||
capi.awesome.emit_signal("bling::playerctl::no_players")
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
collectgarbage("collect")
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local function emit_player_position(self)
|
||||
local position_cmd = self._private.cmd .. "position"
|
||||
local length_cmd = self._private.cmd .. "metadata mpris:length"
|
||||
|
||||
awful.widget.watch(position_cmd, self.interval, function(_, interval)
|
||||
awful.spawn.easy_async_with_shell(length_cmd, function(length)
|
||||
local length_sec = tonumber(length) -- in microseconds
|
||||
local interval_sec = tonumber(interval) -- in seconds
|
||||
if length_sec and interval_sec then
|
||||
if interval_sec >= 0 and length_sec > 0 then
|
||||
self:emit_signal("position", interval_sec, length_sec / 1000000)
|
||||
capi.awesome.emit_signal("bling::playerctl::position", interval_sec, length_sec / 1000000)
|
||||
end
|
||||
end
|
||||
end)
|
||||
collectgarbage("collect")
|
||||
end)
|
||||
end
|
||||
|
||||
local function emit_player_playback_status(self)
|
||||
local status_cmd = self._private.cmd .. "status -F"
|
||||
|
||||
awful.spawn.with_line_callback(status_cmd, {
|
||||
stdout = function(line)
|
||||
if line:find("Playing") then
|
||||
self:emit_signal("playback_status", true)
|
||||
capi.awesome.emit_signal("bling::playerctl::status", true)
|
||||
else
|
||||
self:emit_signal("playback_status", false)
|
||||
capi.awesome.emit_signal("bling::playerctl::status", false)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local function emit_player_volume(self)
|
||||
local volume_cmd = self._private.cmd .. "volume -F"
|
||||
|
||||
awful.spawn.with_line_callback(volume_cmd, {
|
||||
stdout = function(line)
|
||||
self:emit_signal("volume", tonumber(line))
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local function emit_player_loop_status(self)
|
||||
local loop_status_cmd = self._private.cmd .. "loop -F"
|
||||
|
||||
awful.spawn.with_line_callback(loop_status_cmd, {
|
||||
stdout = function(line)
|
||||
self._private.loop_status = line
|
||||
self:emit_signal("loop_status", line:lower())
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local function emit_player_shuffle(self)
|
||||
local shuffle_cmd = self._private.cmd .. "shuffle -F"
|
||||
|
||||
awful.spawn.with_line_callback(shuffle_cmd, {
|
||||
stdout = function(line)
|
||||
if line:find("On") then
|
||||
self._private.shuffle = true
|
||||
self:emit_signal("shuffle", true)
|
||||
else
|
||||
self._private.shuffle = false
|
||||
self:emit_signal("shuffle", false)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local function parse_args(self, args)
|
||||
if args.player then
|
||||
self._private.cmd = self._private.cmd .. "--player="
|
||||
|
||||
if type(args.player) == "string" then
|
||||
self._private.cmd = self._private.cmd .. args.player .. " "
|
||||
elseif type(args.player) == "table" then
|
||||
for index, player in ipairs(args.player) do
|
||||
self._private.cmd = self._private.cmd .. player
|
||||
if index < #args.player then
|
||||
self._private.cmd = self._private.cmd .. ","
|
||||
else
|
||||
self._private.cmd = self._private.cmd .. " "
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if args.ignore then
|
||||
self._private.cmd = self._private.cmd .. "--ignore-player="
|
||||
|
||||
if type(args.ignore) == "string" then
|
||||
self._private.cmd = self._private.cmd .. args.ignore .. " "
|
||||
elseif type(args.ignore) == "table" then
|
||||
for index, player in ipairs(args.ignore) do
|
||||
self._private.cmd = self._private.cmd .. player
|
||||
if index < #args.ignore then
|
||||
self._private.cmd = self._private.cmd .. ","
|
||||
else
|
||||
self._private.cmd = self._private.cmd .. " "
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function new(args)
|
||||
args = args or {}
|
||||
|
||||
local ret = gobject{}
|
||||
gtable.crush(ret, playerctl, true)
|
||||
|
||||
ret.interval = args.interval or beautiful.playerctl_position_update_interval or 1
|
||||
ret.debounce_delay = args.debounce_delay or beautiful.playerctl_debounce_delay or 0.35
|
||||
|
||||
ret._private = {}
|
||||
ret._private.metadata_timer = nil
|
||||
ret._private.cmd = "playerctl "
|
||||
parse_args(ret, args)
|
||||
|
||||
emit_player_metadata(ret)
|
||||
emit_player_position(ret)
|
||||
emit_player_playback_status(ret)
|
||||
emit_player_volume(ret)
|
||||
emit_player_loop_status(ret)
|
||||
emit_player_shuffle(ret)
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
function playerctl.mt:__call(...)
|
||||
return new(...)
|
||||
end
|
||||
|
||||
-- On startup instead of on playerctl object init to make it
|
||||
-- possible to have more than one of these running
|
||||
awful.spawn.with_shell("killall playerctl")
|
||||
|
||||
return setmetatable(playerctl, playerctl.mt)
|
560
config/awesome/module/bling/signal/playerctl/playerctl_lib.lua
Normal file
|
@ -0,0 +1,560 @@
|
|||
-- Playerctl signals
|
||||
--
|
||||
-- Provides:
|
||||
-- metadata
|
||||
-- title (string)
|
||||
-- artist (string)
|
||||
-- album_path (string)
|
||||
-- album (string)
|
||||
-- new (bool)
|
||||
-- player_name (string)
|
||||
-- position
|
||||
-- interval_sec (number)
|
||||
-- length_sec (number)
|
||||
-- player_name (string)
|
||||
-- playback_status
|
||||
-- playing (boolean)
|
||||
-- player_name (string)
|
||||
-- seeked
|
||||
-- position (number)
|
||||
-- player_name (string)
|
||||
-- volume
|
||||
-- volume (number)
|
||||
-- player_name (string)
|
||||
-- loop_status
|
||||
-- loop_status (string)
|
||||
-- player_name (string)
|
||||
-- shuffle
|
||||
-- shuffle (boolean)
|
||||
-- player_name (string)
|
||||
-- exit
|
||||
-- player_name (string)
|
||||
-- no_players
|
||||
-- (No parameters)
|
||||
|
||||
local awful = require("awful")
|
||||
local gobject = require("gears.object")
|
||||
local gtable = require("gears.table")
|
||||
local gtimer = require("gears.timer")
|
||||
local gstring = require("gears.string")
|
||||
local beautiful = require("beautiful")
|
||||
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
|
||||
local setmetatable = setmetatable
|
||||
local ipairs = ipairs
|
||||
local pairs = pairs
|
||||
local type = type
|
||||
local capi = { awesome = awesome }
|
||||
|
||||
local playerctl = { mt = {} }
|
||||
|
||||
function playerctl:disable()
|
||||
-- Restore default settings
|
||||
self.ignore = {}
|
||||
self.priority = {}
|
||||
self.update_on_activity = true
|
||||
self.interval = 1
|
||||
self.debounce_delay = 0.35
|
||||
|
||||
-- Reset timers
|
||||
self._private.manager = nil
|
||||
self._private.metadata_timer:stop()
|
||||
self._private.metadata_timer = nil
|
||||
self._private.position_timer:stop()
|
||||
self._private.position_timer = nil
|
||||
|
||||
-- Reset default values
|
||||
self._private.last_position = -1
|
||||
self._private.last_length = -1
|
||||
self._private.last_player = nil
|
||||
self._private.last_title = ""
|
||||
self._private.last_artist = ""
|
||||
self._private.last_artUrl = ""
|
||||
end
|
||||
|
||||
function playerctl:pause(player)
|
||||
player = player or self._private.manager.players[1]
|
||||
if player then
|
||||
player:pause()
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:play(player)
|
||||
player = player or self._private.manager.players[1]
|
||||
if player then
|
||||
player:play()
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:stop(player)
|
||||
player = player or self._private.manager.players[1]
|
||||
if player then
|
||||
player:stop()
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:play_pause(player)
|
||||
player = player or self._private.manager.players[1]
|
||||
if player then
|
||||
player:play_pause()
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:previous(player)
|
||||
player = player or self._private.manager.players[1]
|
||||
if player then
|
||||
player:previous()
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:next(player)
|
||||
player = player or self._private.manager.players[1]
|
||||
if player then
|
||||
player:next()
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:set_loop_status(loop_status, player)
|
||||
player = player or self._private.manager.players[1]
|
||||
if player then
|
||||
player:set_loop_status(loop_status)
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:cycle_loop_status(player)
|
||||
player = player or self._private.manager.players[1]
|
||||
if player then
|
||||
if player.loop_status == "NONE" then
|
||||
player:set_loop_status("TRACK")
|
||||
elseif player.loop_status == "TRACK" then
|
||||
player:set_loop_status("PLAYLIST")
|
||||
elseif player.loop_status == "PLAYLIST" then
|
||||
player:set_loop_status("NONE")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:set_position(position, player)
|
||||
player = player or self._private.manager.players[1]
|
||||
if player then
|
||||
player:set_position(position * 1000000)
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:set_shuffle(shuffle, player)
|
||||
player = player or self._private.manager.players[1]
|
||||
if player then
|
||||
player:set_shuffle(shuffle)
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:cycle_shuffle(player)
|
||||
player = player or self._private.manager.players[1]
|
||||
if player then
|
||||
player:set_shuffle(not player.shuffle)
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:set_volume(volume, player)
|
||||
player = player or self._private.manager.players[1]
|
||||
if player then
|
||||
player:set_volume(volume)
|
||||
end
|
||||
end
|
||||
|
||||
function playerctl:get_manager()
|
||||
return self._private.manager
|
||||
end
|
||||
|
||||
function playerctl:get_active_player()
|
||||
return self._private.manager.players[1]
|
||||
end
|
||||
|
||||
function playerctl:get_player_of_name(name)
|
||||
for _, player in ipairs(self._private.manager.players[1]) do
|
||||
if player.name == name then
|
||||
return player
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function emit_metadata_signal(self, title, artist, artUrl, album, new, player_name)
|
||||
title = gstring.xml_escape(title)
|
||||
artist = gstring.xml_escape(artist)
|
||||
album = gstring.xml_escape(album)
|
||||
|
||||
-- Spotify client doesn't report its art URL's correctly...
|
||||
if player_name == "spotify" then
|
||||
artUrl = artUrl:gsub("open.spotify.com", "i.scdn.co")
|
||||
end
|
||||
|
||||
if artUrl ~= "" then
|
||||
local art_path = os.tmpname()
|
||||
helpers.filesystem.save_image_async_curl(artUrl, art_path, function()
|
||||
self:emit_signal("metadata", title, artist, art_path, album, new, player_name)
|
||||
capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, art_path, player_name)
|
||||
end)
|
||||
else
|
||||
capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, "", player_name)
|
||||
self:emit_signal("metadata", title, artist, "", album, new, player_name)
|
||||
end
|
||||
end
|
||||
|
||||
local function metadata_cb(self, player, metadata)
|
||||
if self.update_on_activity then
|
||||
self._private.manager:move_player_to_top(player)
|
||||
end
|
||||
|
||||
local data = metadata.value
|
||||
|
||||
local title = data["xesam:title"] or ""
|
||||
local artist = data["xesam:artist"][1] or ""
|
||||
for i = 2, #data["xesam:artist"] do
|
||||
artist = artist .. ", " .. data["xesam:artist"][i]
|
||||
end
|
||||
local artUrl = data["mpris:artUrl"] or ""
|
||||
local album = data["xesam:album"] or ""
|
||||
|
||||
if player == self._private.manager.players[1] then
|
||||
self._private.active_player = player
|
||||
|
||||
-- Callback can be called even though values we care about haven't
|
||||
-- changed, so check to see if they have
|
||||
if
|
||||
player ~= self._private.last_player
|
||||
or title ~= self._private.last_title
|
||||
or artist ~= self._private.last_artist
|
||||
or artUrl ~= self._private.last_artUrl
|
||||
then
|
||||
if (title == "" and artist == "" and artUrl == "") then return end
|
||||
|
||||
if self._private.metadata_timer ~= nil and self._private.metadata_timer.started then
|
||||
self._private.metadata_timer:stop()
|
||||
end
|
||||
|
||||
self._private.metadata_timer = gtimer {
|
||||
timeout = self.debounce_delay,
|
||||
autostart = true,
|
||||
single_shot = true,
|
||||
callback = function()
|
||||
emit_metadata_signal(self, title, artist, artUrl, album, true, player.player_name)
|
||||
end
|
||||
}
|
||||
|
||||
-- Re-sync with position timer when track changes
|
||||
self._private.position_timer:again()
|
||||
self._private.last_player = player
|
||||
self._private.last_title = title
|
||||
self._private.last_artist = artist
|
||||
self._private.last_artUrl = artUrl
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function position_cb(self)
|
||||
local player = self._private.manager.players[1]
|
||||
if player then
|
||||
|
||||
local position = player:get_position() / 1000000
|
||||
local length = (player.metadata.value["mpris:length"] or 0) / 1000000
|
||||
if position ~= self._private.last_position or length ~= self._private.last_length then
|
||||
capi.awesome.emit_signal("bling::playerctl::position", position, length, player.player_name)
|
||||
self:emit_signal("position", position, length, player.player_name)
|
||||
self._private.last_position = position
|
||||
self._private.last_length = length
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function playback_status_cb(self, player, status)
|
||||
if self.update_on_activity then
|
||||
self._private.manager:move_player_to_top(player)
|
||||
end
|
||||
|
||||
if player == self._private.manager.players[1] then
|
||||
self._private.active_player = player
|
||||
|
||||
-- Reported as PLAYING, PAUSED, or STOPPED
|
||||
if status == "PLAYING" then
|
||||
self:emit_signal("playback_status", true, player.player_name)
|
||||
capi.awesome.emit_signal("bling::playerctl::status", true, player.player_name)
|
||||
else
|
||||
self:emit_signal("playback_status", false, player.player_name)
|
||||
capi.awesome.emit_signal("bling::playerctl::status", false, player.player_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function seeked_cb(self, player, position)
|
||||
if self.update_on_activity then
|
||||
self._private.manager:move_player_to_top(player)
|
||||
end
|
||||
|
||||
if player == self._private.manager.players[1] then
|
||||
self._private.active_player = player
|
||||
self:emit_signal("seeked", position / 1000000, player.player_name)
|
||||
end
|
||||
end
|
||||
|
||||
local function volume_cb(self, player, volume)
|
||||
if self.update_on_activity then
|
||||
self._private.manager:move_player_to_top(player)
|
||||
end
|
||||
|
||||
if player == self._private.manager.players[1] then
|
||||
self._private.active_player = player
|
||||
self:emit_signal("volume", volume, player.player_name)
|
||||
end
|
||||
end
|
||||
|
||||
local function loop_status_cb(self, player, loop_status)
|
||||
if self.update_on_activity then
|
||||
self._private.manager:move_player_to_top(player)
|
||||
end
|
||||
|
||||
if player == self._private.manager.players[1] then
|
||||
self._private.active_player = player
|
||||
self:emit_signal("loop_status", loop_status:lower(), player.player_name)
|
||||
end
|
||||
end
|
||||
|
||||
local function shuffle_cb(self, player, shuffle)
|
||||
if self.update_on_activity then
|
||||
self._private.manager:move_player_to_top(player)
|
||||
end
|
||||
|
||||
if player == self._private.manager.players[1] then
|
||||
self._private.active_player = player
|
||||
self:emit_signal("shuffle", shuffle, player.player_name)
|
||||
end
|
||||
end
|
||||
|
||||
local function exit_cb(self, player)
|
||||
if player == self._private.manager.players[1] then
|
||||
self:emit_signal("exit", player.player_name)
|
||||
end
|
||||
end
|
||||
|
||||
-- Determine if player should be managed
|
||||
local function name_is_selected(self, name)
|
||||
if self.ignore[name.name] then
|
||||
return false
|
||||
end
|
||||
|
||||
if #self.priority > 0 then
|
||||
for _, arg in pairs(self.priority) do
|
||||
if arg == name.name or arg == "%any" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Create new player and connect it to callbacks
|
||||
local function init_player(self, name)
|
||||
if name_is_selected(self, name) then
|
||||
local player = self._private.lgi_Playerctl.Player.new_from_name(name)
|
||||
self._private.manager:manage_player(player)
|
||||
player.on_metadata = function(player, metadata)
|
||||
metadata_cb(self, player, metadata)
|
||||
end
|
||||
player.on_playback_status = function(player, playback_status)
|
||||
playback_status_cb(self, player, playback_status)
|
||||
end
|
||||
player.on_seeked = function(player, position)
|
||||
seeked_cb(self, player, position)
|
||||
end
|
||||
player.on_volume = function(player, volume)
|
||||
volume_cb(self, player, volume)
|
||||
end
|
||||
player.on_loop_status = function(player, loop_status)
|
||||
loop_status_cb(self, player, loop_status)
|
||||
end
|
||||
player.on_shuffle = function(player, shuffle_status)
|
||||
shuffle_cb(self, player, shuffle_status)
|
||||
end
|
||||
player.on_exit = function(player, shuffle_status)
|
||||
exit_cb(self, player)
|
||||
end
|
||||
|
||||
-- Start position timer if its not already running
|
||||
if not self._private.position_timer.started then
|
||||
self._private.position_timer:again()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Determine if a player name comes before or after another according to the
|
||||
-- priority order
|
||||
local function player_compare_name(self, name_a, name_b)
|
||||
local any_index = math.huge
|
||||
local a_match_index = nil
|
||||
local b_match_index = nil
|
||||
|
||||
if name_a == name_b then
|
||||
return 0
|
||||
end
|
||||
|
||||
for index, name in ipairs(self.priority) do
|
||||
if name == "%any" then
|
||||
any_index = (any_index == math.huge) and index or any_index
|
||||
elseif name == name_a then
|
||||
a_match_index = a_match_index or index
|
||||
elseif name == name_b then
|
||||
b_match_index = b_match_index or index
|
||||
end
|
||||
end
|
||||
|
||||
if not a_match_index and not b_match_index then
|
||||
return 0
|
||||
elseif not a_match_index then
|
||||
return (b_match_index < any_index) and 1 or -1
|
||||
elseif not b_match_index then
|
||||
return (a_match_index < any_index) and -1 or 1
|
||||
elseif a_match_index == b_match_index then
|
||||
return 0
|
||||
else
|
||||
return (a_match_index < b_match_index) and -1 or 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Sorting function used by manager if a priority order is specified
|
||||
local function player_compare(self, a, b)
|
||||
local player_a = self._private.lgi_Playerctl.Player(a)
|
||||
local player_b = self._private.lgi_Playerctl.Player(b)
|
||||
return player_compare_name(self, player_a.player_name, player_b.player_name)
|
||||
end
|
||||
|
||||
local function get_current_player_info(self, player)
|
||||
local title = player:get_title() or ""
|
||||
local artist = player:get_artist() or ""
|
||||
local artUrl = player:print_metadata_prop("mpris:artUrl") or ""
|
||||
local album = player:get_album() or ""
|
||||
|
||||
emit_metadata_signal(self, title, artist, artUrl, album, false, player.player_name)
|
||||
playback_status_cb(self, player, player.playback_status)
|
||||
volume_cb(self, player, player.volume)
|
||||
loop_status_cb(self, player, player.loop_status)
|
||||
shuffle_cb(self, player, player.shuffle)
|
||||
end
|
||||
|
||||
local function start_manager(self)
|
||||
self._private.manager = self._private.lgi_Playerctl.PlayerManager()
|
||||
|
||||
if #self.priority > 0 then
|
||||
self._private.manager:set_sort_func(function(a, b)
|
||||
return player_compare(self, a, b)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Timer to update track position at specified interval
|
||||
self._private.position_timer = gtimer {
|
||||
timeout = self.interval,
|
||||
callback = function()
|
||||
position_cb(self)
|
||||
end,
|
||||
}
|
||||
|
||||
-- Manage existing players on startup
|
||||
for _, name in ipairs(self._private.manager.player_names) do
|
||||
init_player(self, name)
|
||||
end
|
||||
|
||||
if self._private.manager.players[1] then
|
||||
get_current_player_info(self, self._private.manager.players[1])
|
||||
end
|
||||
|
||||
local _self = self
|
||||
|
||||
-- Callback to manage new players
|
||||
function self._private.manager:on_name_appeared(name)
|
||||
init_player(_self, name)
|
||||
end
|
||||
|
||||
function self._private.manager:on_player_appeared(player)
|
||||
if player == self.players[1] then
|
||||
_self._private.active_player = player
|
||||
end
|
||||
end
|
||||
|
||||
function self._private.manager:on_player_vanished(player)
|
||||
if #self.players == 0 then
|
||||
_self._private.metadata_timer:stop()
|
||||
_self._private.position_timer:stop()
|
||||
_self:emit_signal("no_players")
|
||||
capi.awesome.emit_signal("bling::playerctl::no_players")
|
||||
elseif player == _self._private.active_player then
|
||||
_self._private.active_player = self.players[1]
|
||||
get_current_player_info(_self, self.players[1])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function parse_args(self, args)
|
||||
self.ignore = {}
|
||||
if type(args.ignore) == "string" then
|
||||
self.ignore[args.ignore] = true
|
||||
elseif type(args.ignore) == "table" then
|
||||
for _, name in pairs(args.ignore) do
|
||||
self.ignore[name] = true
|
||||
end
|
||||
end
|
||||
|
||||
self.priority = {}
|
||||
if type(args.player) == "string" then
|
||||
self.priority[1] = args.player
|
||||
elseif type(args.player) == "table" then
|
||||
self.priority = args.player
|
||||
end
|
||||
end
|
||||
|
||||
local function new(args)
|
||||
args = args or {}
|
||||
|
||||
local ret = gobject{}
|
||||
gtable.crush(ret, playerctl, true)
|
||||
|
||||
-- Grab settings from beautiful variables if not set explicitly
|
||||
args.ignore = args.ignore or beautiful.playerctl_ignore
|
||||
args.player = args.player or beautiful.playerctl_player
|
||||
ret.update_on_activity = args.update_on_activity or
|
||||
beautiful.playerctl_update_on_activity or true
|
||||
ret.interval = args.interval or beautiful.playerctl_position_update_interval or 1
|
||||
ret.debounce_delay = args.debounce_delay or beautiful.playerctl_debounce_delay or 0.35
|
||||
parse_args(ret, args)
|
||||
|
||||
ret._private = {}
|
||||
|
||||
-- Metadata callback for title, artist, and album art
|
||||
ret._private.last_player = nil
|
||||
ret._private.last_title = ""
|
||||
ret._private.last_artist = ""
|
||||
ret._private.last_artUrl = ""
|
||||
|
||||
-- Track position callback
|
||||
ret._private.last_position = -1
|
||||
ret._private.last_length = -1
|
||||
|
||||
-- Grab playerctl library
|
||||
ret._private.lgi_Playerctl = require("lgi").Playerctl
|
||||
ret._private.manager = nil
|
||||
ret._private.metadata_timer = nil
|
||||
ret._private.position_timer = nil
|
||||
|
||||
-- Ensure main event loop has started before starting player manager
|
||||
gtimer.delayed_call(function()
|
||||
start_manager(ret)
|
||||
end)
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
function playerctl.mt:__call(...)
|
||||
return new(...)
|
||||
end
|
||||
|
||||
return setmetatable(playerctl, playerctl.mt)
|
104
config/awesome/module/bling/theme-var-template.lua
Normal file
|
@ -0,0 +1,104 @@
|
|||
--[[ Bling theme variables template
|
||||
|
||||
This file has all theme variables of the bling module.
|
||||
Every variable has a small comment on what it does.
|
||||
You might just want to copy that whole part into your theme.lua and start adjusting from there.
|
||||
|
||||
--]]
|
||||
-- LuaFormatter off
|
||||
-- window swallowing
|
||||
theme.dont_swallow_classname_list = { "firefox", "Gimp" } -- list of class names that should not be swallowed
|
||||
theme.dont_swallow_filter_activated = true -- whether the filter above should be active
|
||||
|
||||
-- flash focus
|
||||
theme.flash_focus_start_opacity = 0.6 -- the starting opacity
|
||||
theme.flash_focus_step = 0.01 -- the step of animation
|
||||
|
||||
-- playerctl signal
|
||||
theme.playerctl_backend = "playerctl_cli" -- backend to use
|
||||
theme.playerctl_ignore = {} -- list of players to be ignored
|
||||
theme.playerctl_player = {} -- list of players to be used in priority order
|
||||
theme.playerctl_update_on_activity = true -- whether to prioritize the most recently active players or not
|
||||
theme.playerctl_position_update_interval = 1 -- the update interval for fetching the position from playerctl
|
||||
|
||||
-- tabbed
|
||||
theme.tabbed_spawn_in_tab = false -- whether a new client should spawn into the focused tabbing container
|
||||
|
||||
-- tabbar general
|
||||
theme.tabbar_disable = false -- disable the tab bar entirely
|
||||
theme.tabbar_ontop = false
|
||||
theme.tabbar_radius = 0 -- border radius of the tabbar
|
||||
theme.tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern")
|
||||
theme.tabbar_font = "Sans 11" -- font of the tabbar
|
||||
theme.tabbar_size = 40 -- size of the tabbar
|
||||
theme.tabbar_position = "top" -- position of the tabbar
|
||||
theme.tabbar_bg_normal = "#000000" -- background color of the focused client on the tabbar
|
||||
theme.tabbar_fg_normal = "#ffffff" -- foreground color of the focused client on the tabbar
|
||||
theme.tabbar_bg_focus = "#1A2026" -- background color of unfocused clients on the tabbar
|
||||
theme.tabbar_fg_focus = "#ff0000" -- foreground color of unfocused clients on the tabbar
|
||||
theme.tabbar_bg_focus_inactive = nil -- background color of the focused client on the tabbar when inactive
|
||||
theme.tabbar_fg_focus_inactive = nil -- foreground color of the focused client on the tabbar when inactive
|
||||
theme.tabbar_bg_normal_inactive = nil -- background color of unfocused clients on the tabbar when inactive
|
||||
theme.tabbar_fg_normal_inactive = nil -- foreground color of unfocused clients on the tabbar when inactive
|
||||
|
||||
-- mstab
|
||||
theme.mstab_bar_ontop = false -- whether you want to allow the bar to be ontop of clients
|
||||
theme.mstab_dont_resize_slaves = false -- whether the tabbed stack windows should be smaller than the
|
||||
-- currently focused stack window (set it to true if you use
|
||||
-- transparent terminals. False if you use shadows on solid ones
|
||||
theme.mstab_bar_padding = "default" -- how much padding there should be between clients and your tabbar
|
||||
-- by default it will adjust based on your useless gaps.
|
||||
-- If you want a custom value. Set it to the number of pixels (int)
|
||||
theme.mstab_border_radius = 0 -- border radius of the tabbar
|
||||
theme.mstab_bar_height = 40 -- height of the tabbar
|
||||
theme.mstab_tabbar_position = "top" -- position of the tabbar (mstab currently does not support left,right)
|
||||
theme.mstab_tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern")
|
||||
-- defaults to the tabbar_style so only change if you want a
|
||||
-- different style for mstab and tabbed
|
||||
|
||||
-- the following variables are currently only for the "modern" tabbar style
|
||||
theme.tabbar_color_close = "#f9929b" -- changes the color of the close button
|
||||
theme.tabbar_color_min = "#fbdf90" -- changes the color of the minimize button
|
||||
theme.tabbar_color_float = "#ccaced" -- changes the color of the float button
|
||||
|
||||
-- tag preview widget
|
||||
theme.tag_preview_widget_border_radius = 0 -- Border radius of the widget (With AA)
|
||||
theme.tag_preview_client_border_radius = 0 -- Border radius of each client in the widget (With AA)
|
||||
theme.tag_preview_client_opacity = 0.5 -- Opacity of each client
|
||||
theme.tag_preview_client_bg = "#000000" -- The bg color of each client
|
||||
theme.tag_preview_client_border_color = "#ffffff" -- The border color of each client
|
||||
theme.tag_preview_client_border_width = 3 -- The border width of each client
|
||||
theme.tag_preview_widget_bg = "#000000" -- The bg color of the widget
|
||||
theme.tag_preview_widget_border_color = "#ffffff" -- The border color of the widget
|
||||
theme.tag_preview_widget_border_width = 3 -- The border width of the widget
|
||||
theme.tag_preview_widget_margin = 0 -- The margin of the widget
|
||||
|
||||
-- task preview widget
|
||||
theme.task_preview_widget_border_radius = 0 -- Border radius of the widget (With AA)
|
||||
theme.task_preview_widget_bg = "#000000" -- The bg color of the widget
|
||||
theme.task_preview_widget_border_color = "#ffffff" -- The border color of the widget
|
||||
theme.task_preview_widget_border_width = 3 -- The border width of the widget
|
||||
theme.task_preview_widget_margin = 0 -- The margin of the widget
|
||||
|
||||
-- window switcher
|
||||
theme.window_switcher_widget_bg = "#000000" -- The bg color of the widget
|
||||
theme.window_switcher_widget_border_width = 3 -- The border width of the widget
|
||||
theme.window_switcher_widget_border_radius = 0 -- The border radius of the widget
|
||||
theme.window_switcher_widget_border_color = "#ffffff" -- The border color of the widget
|
||||
theme.window_switcher_clients_spacing = 20 -- The space between each client item
|
||||
theme.window_switcher_client_icon_horizontal_spacing = 5 -- The space between client icon and text
|
||||
theme.window_switcher_client_width = 150 -- The width of one client widget
|
||||
theme.window_switcher_client_height = 250 -- The height of one client widget
|
||||
theme.window_switcher_client_margins = 10 -- The margin between the content and the border of the widget
|
||||
theme.window_switcher_thumbnail_margins = 10 -- The margin between one client thumbnail and the rest of the widget
|
||||
theme.thumbnail_scale = false -- If set to true, the thumbnails fit policy will be set to "fit" instead of "auto"
|
||||
theme.window_switcher_name_margins = 10 -- The margin of one clients title to the rest of the widget
|
||||
theme.window_switcher_name_valign = "center" -- How to vertically align one clients title
|
||||
theme.window_switcher_name_forced_width = 200 -- The width of one title
|
||||
theme.window_switcher_name_font = "Sans 11" -- The font of all titles
|
||||
theme.window_switcher_name_normal_color = "#ffffff" -- The color of one title if the client is unfocused
|
||||
theme.window_switcher_name_focus_color = "#ff0000" -- The color of one title if the client is focused
|
||||
theme.window_switcher_icon_valign = "center" -- How to vertically align the one icon
|
||||
theme.window_switcher_icon_width = 40 -- The width of one icon
|
||||
|
||||
-- LuaFormatter on
|
1053
config/awesome/module/bling/widget/app_launcher/init.lua
Normal file
656
config/awesome/module/bling/widget/app_launcher/prompt.lua
Normal file
|
@ -0,0 +1,656 @@
|
|||
---------------------------------------------------------------------------
|
||||
--- Modified Prompt module.
|
||||
-- @author Julien Danjou <julien@danjou.info>
|
||||
-- @copyright 2008 Julien Danjou
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
local akey = require("awful.key")
|
||||
local keygrabber = require("awful.keygrabber")
|
||||
local gobject = require("gears.object")
|
||||
local gdebug = require('gears.debug')
|
||||
local gtable = require("gears.table")
|
||||
local gcolor = require("gears.color")
|
||||
local gstring = require("gears.string")
|
||||
local gfs = require("gears.filesystem")
|
||||
local wibox = require("wibox")
|
||||
local beautiful = require("beautiful")
|
||||
local io = io
|
||||
local table = table
|
||||
local math = math
|
||||
local ipairs = ipairs
|
||||
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
|
||||
local capi = { selection = selection }
|
||||
|
||||
local prompt = { mt = {} }
|
||||
|
||||
--- Private data
|
||||
local data = {}
|
||||
data.history = {}
|
||||
|
||||
local function itera(inc,a, i)
|
||||
i = i + inc
|
||||
local v = a[i]
|
||||
if v then return i,v end
|
||||
end
|
||||
|
||||
local function history_check_load(id, max)
|
||||
if id and id ~= "" and not data.history[id] then
|
||||
data.history[id] = { max = 50, table = {} }
|
||||
|
||||
if max then
|
||||
data.history[id].max = max
|
||||
end
|
||||
|
||||
local f = io.open(id, "r")
|
||||
if not f then return end
|
||||
|
||||
-- Read history file
|
||||
for line in f:lines() do
|
||||
if gtable.hasitem(data.history[id].table, line) == nil then
|
||||
table.insert(data.history[id].table, line)
|
||||
if #data.history[id].table >= data.history[id].max then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
|
||||
local function is_word_char(c)
|
||||
if string.find(c, "[{[(,.:;_-+=@/ ]") then
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function cword_start(s, pos)
|
||||
local i = pos
|
||||
if i > 1 then
|
||||
i = i - 1
|
||||
end
|
||||
while i >= 1 and not is_word_char(s:sub(i, i)) do
|
||||
i = i - 1
|
||||
end
|
||||
while i >= 1 and is_word_char(s:sub(i, i)) do
|
||||
i = i - 1
|
||||
end
|
||||
if i <= #s then
|
||||
i = i + 1
|
||||
end
|
||||
return i
|
||||
end
|
||||
|
||||
local function cword_end(s, pos)
|
||||
local i = pos
|
||||
while i <= #s and not is_word_char(s:sub(i, i)) do
|
||||
i = i + 1
|
||||
end
|
||||
while i <= #s and is_word_char(s:sub(i, i)) do
|
||||
i = i + 1
|
||||
end
|
||||
return i
|
||||
end
|
||||
|
||||
local function history_save(id)
|
||||
if data.history[id] then
|
||||
gfs.make_parent_directories(id)
|
||||
local f = io.open(id, "w")
|
||||
if not f then
|
||||
gdebug.print_warning("Failed to write the history to "..id)
|
||||
return
|
||||
end
|
||||
for i = 1, math.min(#data.history[id].table, data.history[id].max) do
|
||||
f:write(data.history[id].table[i] .. "\n")
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
|
||||
local function history_items(id)
|
||||
if data.history[id] then
|
||||
return #data.history[id].table
|
||||
else
|
||||
return -1
|
||||
end
|
||||
end
|
||||
|
||||
local function history_add(id, command)
|
||||
if data.history[id] and command ~= "" then
|
||||
local index = gtable.hasitem(data.history[id].table, command)
|
||||
if index == nil then
|
||||
table.insert(data.history[id].table, command)
|
||||
|
||||
-- Do not exceed our max_cmd
|
||||
if #data.history[id].table > data.history[id].max then
|
||||
table.remove(data.history[id].table, 1)
|
||||
end
|
||||
|
||||
history_save(id)
|
||||
else
|
||||
-- Bump this command to the end of history
|
||||
table.remove(data.history[id].table, index)
|
||||
table.insert(data.history[id].table, command)
|
||||
history_save(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function have_multibyte_char_at(text, position)
|
||||
return text:sub(position, position):wlen() == -1
|
||||
end
|
||||
|
||||
local function prompt_text_with_cursor(args)
|
||||
local char, spacer, text_start, text_end, ret
|
||||
local text = args.text or ""
|
||||
local _prompt = args.prompt or ""
|
||||
local underline = args.cursor_ul or "none"
|
||||
|
||||
if args.select_all then
|
||||
if #text == 0 then char = " " else char = gstring.xml_escape(text) end
|
||||
spacer = " "
|
||||
text_start = ""
|
||||
text_end = ""
|
||||
elseif #text < args.cursor_pos then
|
||||
char = " "
|
||||
spacer = ""
|
||||
text_start = gstring.xml_escape(text)
|
||||
text_end = ""
|
||||
else
|
||||
local offset = 0
|
||||
if have_multibyte_char_at(text, args.cursor_pos) then
|
||||
offset = 1
|
||||
end
|
||||
char = gstring.xml_escape(text:sub(args.cursor_pos, args.cursor_pos + offset))
|
||||
spacer = " "
|
||||
text_start = gstring.xml_escape(text:sub(1, args.cursor_pos - 1))
|
||||
text_end = gstring.xml_escape(text:sub(args.cursor_pos + 1 + offset))
|
||||
end
|
||||
|
||||
local cursor_color = gcolor.ensure_pango_color(args.cursor_color)
|
||||
local text_color = gcolor.ensure_pango_color(args.text_color)
|
||||
|
||||
if args.highlighter then
|
||||
text_start, text_end = args.highlighter(text_start, text_end)
|
||||
end
|
||||
|
||||
ret = _prompt .. text_start .. "<span background=\"" .. cursor_color ..
|
||||
"\" foreground=\"" .. text_color .. "\" underline=\"" .. underline ..
|
||||
"\">" .. char .. "</span>" .. text_end .. spacer
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
local function update(self)
|
||||
self.textbox:set_font(self.font)
|
||||
self.textbox:set_markup(prompt_text_with_cursor{
|
||||
text = self.command, text_color = self.fg_cursor, cursor_color = self.bg_cursor,
|
||||
cursor_pos = self._private_cur_pos, cursor_ul = self.ul_cursor, select_all = self.select_all,
|
||||
prompt = self.prompt, highlighter = self.highlighter })
|
||||
end
|
||||
|
||||
local function exec(self, cb, command_to_history)
|
||||
self.textbox:set_markup("")
|
||||
history_add(self.history_path, command_to_history)
|
||||
keygrabber.stop(self._private.grabber)
|
||||
if cb then cb(self.command) end
|
||||
if self.done_callback then
|
||||
self.done_callback()
|
||||
end
|
||||
end
|
||||
|
||||
function prompt:start()
|
||||
-- The cursor position
|
||||
if self.reset_on_stop == true or self._private_cur_pos == nil then
|
||||
self._private_cur_pos = (self.select_all and 1) or self.text:wlen() + 1
|
||||
end
|
||||
if self.reset_on_stop == true then self.text = "" self.command = "" end
|
||||
|
||||
self.textbox:set_font(self.font)
|
||||
self.textbox:set_markup(prompt_text_with_cursor{
|
||||
text = self.reset_on_stop and self.text or self.command, text_color = self.fg_cursor, cursor_color = self.bg_cursor,
|
||||
cursor_pos = self._private_cur_pos, cursor_ul = self.ul_cursor, select_all = self.select_all,
|
||||
prompt = self.prompt, highlighter = self.highlighter})
|
||||
|
||||
self._private.search_term = nil
|
||||
|
||||
history_check_load(self.history_path, self.history_max)
|
||||
local history_index = history_items(self.history_path) + 1
|
||||
|
||||
-- The completion element to use on completion request.
|
||||
local ncomp = 1
|
||||
|
||||
local command_before_comp
|
||||
local cur_pos_before_comp
|
||||
|
||||
self._private.grabber = keygrabber.run(function(modifiers, key, event)
|
||||
-- Convert index array to hash table
|
||||
local mod = {}
|
||||
for _, v in ipairs(modifiers) do mod[v] = true end
|
||||
|
||||
if event ~= "press" then
|
||||
if self.keyreleased_callback then
|
||||
self.keyreleased_callback(mod, key, self.command)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Call the user specified callback. If it returns true as
|
||||
-- the first result then return from the function. Treat the
|
||||
-- second and third results as a new command and new prompt
|
||||
-- to be set (if provided)
|
||||
if self.keypressed_callback then
|
||||
local user_catched, new_command, new_prompt =
|
||||
self.keypressed_callback(mod, key, self.command)
|
||||
if new_command or new_prompt then
|
||||
if new_command then
|
||||
self.command = new_command
|
||||
end
|
||||
if new_prompt then
|
||||
self.prompt = new_prompt
|
||||
end
|
||||
update(self)
|
||||
end
|
||||
if user_catched then
|
||||
if self.changed_callback then
|
||||
self.changed_callback(self.command)
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local filtered_modifiers = {}
|
||||
|
||||
-- User defined cases
|
||||
if self.hooks[key] then
|
||||
-- Remove caps and num lock
|
||||
for _, m in ipairs(modifiers) do
|
||||
if not gtable.hasitem(akey.ignore_modifiers, m) then
|
||||
table.insert(filtered_modifiers, m)
|
||||
end
|
||||
end
|
||||
|
||||
for _,v in ipairs(self.hooks[key]) do
|
||||
if #filtered_modifiers == #v[1] then
|
||||
local match = true
|
||||
for _,v2 in ipairs(v[1]) do
|
||||
match = match and mod[v2]
|
||||
end
|
||||
if match then
|
||||
local cb
|
||||
local ret, quit = v[3](self.command)
|
||||
local original_command = self.command
|
||||
|
||||
-- Support both a "simple" and a "complex" way to
|
||||
-- control if the prompt should quit.
|
||||
quit = quit == nil and (ret ~= true) or (quit~=false)
|
||||
|
||||
-- Allow the callback to change the command
|
||||
self.command = (ret ~= true) and ret or self.command
|
||||
|
||||
-- Quit by default, but allow it to be disabled
|
||||
if ret and type(ret) ~= "boolean" then
|
||||
cb = self.exe_callback
|
||||
if not quit then
|
||||
self._private_cur_pos = ret:wlen() + 1
|
||||
update(self)
|
||||
end
|
||||
elseif quit then
|
||||
-- No callback.
|
||||
cb = function() end
|
||||
end
|
||||
|
||||
-- Execute the callback
|
||||
if cb then
|
||||
exec(self, cb, original_command)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Get out cases
|
||||
if (mod.Control and (key == "c" or key == "g"))
|
||||
or (not mod.Control and key == "Escape") then
|
||||
self:stop()
|
||||
return false
|
||||
elseif (mod.Control and (key == "j" or key == "m"))
|
||||
-- or (not mod.Control and key == "Return")
|
||||
-- or (not mod.Control and key == "KP_Enter")
|
||||
then
|
||||
exec(self, self.exe_callback, self.command)
|
||||
-- We already unregistered ourselves so we don't want to return
|
||||
-- true, otherwise we may unregister someone else.
|
||||
return
|
||||
end
|
||||
|
||||
-- Control cases
|
||||
if mod.Control then
|
||||
self.select_all = nil
|
||||
if key == "v" then
|
||||
local selection = capi.selection()
|
||||
if selection then
|
||||
-- Remove \n
|
||||
local n = selection:find("\n")
|
||||
if n then
|
||||
selection = selection:sub(1, n - 1)
|
||||
end
|
||||
self.command = self.command:sub(1, self._private_cur_pos - 1) .. selection .. self.command:sub(self._private_cur_pos)
|
||||
self._private_cur_pos = self._private_cur_pos + #selection
|
||||
end
|
||||
elseif key == "a" then
|
||||
self._private_cur_pos = 1
|
||||
elseif key == "b" then
|
||||
if self._private_cur_pos > 1 then
|
||||
self._private_cur_pos = self._private_cur_pos - 1
|
||||
if have_multibyte_char_at(self.command, self._private_cur_pos) then
|
||||
self._private_cur_pos = self._private_cur_pos - 1
|
||||
end
|
||||
end
|
||||
elseif key == "d" then
|
||||
if self._private_cur_pos <= #self.command then
|
||||
self.command = self.command:sub(1, self._private_cur_pos - 1) .. self.command:sub(self._private_cur_pos + 1)
|
||||
end
|
||||
elseif key == "p" then
|
||||
if history_index > 1 then
|
||||
history_index = history_index - 1
|
||||
|
||||
self.command = data.history[self.history_path].table[history_index]
|
||||
self._private_cur_pos = #self.command + 2
|
||||
end
|
||||
elseif key == "n" then
|
||||
if history_index < history_items(self.history_path) then
|
||||
history_index = history_index + 1
|
||||
|
||||
self.command = data.history[self.history_path].table[history_index]
|
||||
self._private_cur_pos = #self.command + 2
|
||||
elseif history_index == history_items(self.history_path) then
|
||||
history_index = history_index + 1
|
||||
|
||||
self.command = ""
|
||||
self._private_cur_pos = 1
|
||||
end
|
||||
elseif key == "e" then
|
||||
self._private_cur_pos = #self.command + 1
|
||||
elseif key == "r" then
|
||||
self._private.search_term = self._private.search_term or self.command:sub(1, self._private_cur_pos - 1)
|
||||
for i,v in (function(a,i) return itera(-1,a,i) end), data.history[self.history_path].table, history_index do
|
||||
if v:find(self._private.search_term,1,true) ~= nil then
|
||||
self.command=v
|
||||
history_index=i
|
||||
self._private_cur_pos=#self.command+1
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif key == "s" then
|
||||
self._private.search_term = self._private.search_term or self.command:sub(1, self._private_cur_pos - 1)
|
||||
for i,v in (function(a,i) return itera(1,a,i) end), data.history[self.history_path].table, history_index do
|
||||
if v:find(self._private.search_term,1,true) ~= nil then
|
||||
self.command=v
|
||||
history_index=i
|
||||
self._private_cur_pos=#self.command+1
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif key == "f" then
|
||||
if self._private_cur_pos <= #self.command then
|
||||
if have_multibyte_char_at(self.command, self._private_cur_pos) then
|
||||
self._private_cur_pos = self._private_cur_pos + 2
|
||||
else
|
||||
self._private_cur_pos = self._private_cur_pos + 1
|
||||
end
|
||||
end
|
||||
elseif key == "h" then
|
||||
if self._private_cur_pos > 1 then
|
||||
local offset = 0
|
||||
if have_multibyte_char_at(self.command, self._private_cur_pos - 1) then
|
||||
offset = 1
|
||||
end
|
||||
self.command = self.command:sub(1, self._private_cur_pos - 2 - offset) .. self.command:sub(self._private_cur_pos)
|
||||
self._private_cur_pos = self._private_cur_pos - 1 - offset
|
||||
end
|
||||
elseif key == "k" then
|
||||
self.command = self.command:sub(1, self._private_cur_pos - 1)
|
||||
elseif key == "u" then
|
||||
self.command = self.command:sub(self._private_cur_pos, #self.command)
|
||||
self._private_cur_pos = 1
|
||||
elseif key == "Prior" then
|
||||
self._private.search_term = self.command:sub(1, self._private_cur_pos - 1) or ""
|
||||
for i,v in (function(a,i) return itera(-1,a,i) end), data.history[self.history_path].table, history_index do
|
||||
if v:find(self._private.search_term,1,true) == 1 then
|
||||
self.command=v
|
||||
history_index=i
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif key == "Next" then
|
||||
self._private.search_term = self.command:sub(1, self._private_cur_pos - 1) or ""
|
||||
for i,v in (function(a,i) return itera(1,a,i) end), data.history[self.history_path].table, history_index do
|
||||
if v:find(self._private.search_term,1,true) == 1 then
|
||||
self.command=v
|
||||
history_index=i
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif key == "w" or key == "BackSpace" then
|
||||
local wstart = 1
|
||||
local wend = 1
|
||||
local cword_start_pos = 1
|
||||
local cword_end_pos = 1
|
||||
while wend < self._private_cur_pos do
|
||||
wend = self.command:find("[{[(,.:;_-+=@/ ]", wstart)
|
||||
if not wend then wend = #self.command + 1 end
|
||||
if self._private_cur_pos >= wstart and self._private_cur_pos <= wend + 1 then
|
||||
cword_start_pos = wstart
|
||||
cword_end_pos = self._private_cur_pos - 1
|
||||
break
|
||||
end
|
||||
wstart = wend + 1
|
||||
end
|
||||
self.command = self.command:sub(1, cword_start_pos - 1) .. self.command:sub(cword_end_pos + 1)
|
||||
self._private_cur_pos = cword_start_pos
|
||||
elseif key == "Delete" then
|
||||
-- delete from history only if:
|
||||
-- we are not dealing with a new command
|
||||
-- the user has not edited an existing entry
|
||||
if self.command == data.history[self.history_path].table[history_index] then
|
||||
table.remove(data.history[self.history_path].table, history_index)
|
||||
if history_index <= history_items(self.history_path) then
|
||||
self.command = data.history[self.history_path].table[history_index]
|
||||
self._private_cur_pos = #self.command + 2
|
||||
elseif history_index > 1 then
|
||||
history_index = history_index - 1
|
||||
|
||||
self.command = data.history[self.history_path].table[history_index]
|
||||
self._private_cur_pos = #self.command + 2
|
||||
else
|
||||
self.command = ""
|
||||
self._private_cur_pos = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif mod.Mod1 or mod.Mod3 then
|
||||
if key == "b" then
|
||||
self._private_cur_pos = cword_start(self.command, self._private_cur_pos)
|
||||
elseif key == "f" then
|
||||
self._private_cur_pos = cword_end(self.command, self._private_cur_pos)
|
||||
elseif key == "d" then
|
||||
self.command = self.command:sub(1, self._private_cur_pos - 1) .. self.command:sub(cword_end(self.command, self._private_cur_pos))
|
||||
elseif key == "BackSpace" then
|
||||
local wstart = cword_start(self.command, self._private_cur_pos)
|
||||
self.command = self.command:sub(1, wstart - 1) .. self.command:sub(self._private_cur_pos)
|
||||
self._private_cur_pos = wstart
|
||||
end
|
||||
else
|
||||
if self.completion_callback then
|
||||
if key == "Tab" or key == "ISO_Left_Tab" then
|
||||
if key == "ISO_Left_Tab" or mod.Shift then
|
||||
if ncomp == 1 then return end
|
||||
if ncomp == 2 then
|
||||
self.command = command_before_comp
|
||||
self.textbox:set_font(self.font)
|
||||
self.textbox:set_markup(prompt_text_with_cursor{
|
||||
text = command_before_comp, text_color = self.fg_cursor, cursor_color = self.bg_cursor,
|
||||
cursor_pos = self._private_cur_pos, cursor_ul = self.ul_cursor, select_all = self.select_all,
|
||||
prompt = self.prompt })
|
||||
self._private_cur_pos = cur_pos_before_comp
|
||||
ncomp = 1
|
||||
return
|
||||
end
|
||||
|
||||
ncomp = ncomp - 2
|
||||
elseif ncomp == 1 then
|
||||
command_before_comp = self.command
|
||||
cur_pos_before_comp = self._private_cur_pos
|
||||
end
|
||||
local matches
|
||||
self.command, self._private_cur_pos, matches = self.completion_callback(command_before_comp, cur_pos_before_comp, ncomp)
|
||||
ncomp = ncomp + 1
|
||||
key = ""
|
||||
-- execute if only one match found and autoexec flag set
|
||||
if matches and #matches == 1 and args.autoexec then
|
||||
exec(self, self.exe_callback)
|
||||
return
|
||||
end
|
||||
elseif key ~= "Shift_L" and key ~= "Shift_R" then
|
||||
ncomp = 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Typin cases
|
||||
if mod.Shift and key == "Insert" then
|
||||
local selection = capi.selection()
|
||||
if selection then
|
||||
-- Remove \n
|
||||
local n = selection:find("\n")
|
||||
if n then
|
||||
selection = selection:sub(1, n - 1)
|
||||
end
|
||||
self.command = self.command:sub(1, self._private_cur_pos - 1) .. selection .. self.command:sub(self._private_cur_pos)
|
||||
self._private_cur_pos = self._private_cur_pos + #selection
|
||||
end
|
||||
elseif key == "Home" then
|
||||
self._private_cur_pos = 1
|
||||
elseif key == "End" then
|
||||
self._private_cur_pos = #self.command + 1
|
||||
elseif key == "BackSpace" then
|
||||
if self._private_cur_pos > 1 then
|
||||
local offset = 0
|
||||
if have_multibyte_char_at(self.command, self._private_cur_pos - 1) then
|
||||
offset = 1
|
||||
end
|
||||
self.command = self.command:sub(1, self._private_cur_pos - 2 - offset) .. self.command:sub(self._private_cur_pos)
|
||||
self._private_cur_pos = self._private_cur_pos - 1 - offset
|
||||
end
|
||||
elseif key == "Delete" then
|
||||
self.command = self.command:sub(1, self._private_cur_pos - 1) .. self.command:sub(self._private_cur_pos + 1)
|
||||
elseif key == "Left" then
|
||||
self._private_cur_pos = self._private_cur_pos - 1
|
||||
elseif key == "Right" then
|
||||
self._private_cur_pos = self._private_cur_pos + 1
|
||||
elseif key == "Prior" then
|
||||
if history_index > 1 then
|
||||
history_index = history_index - 1
|
||||
|
||||
self.command = data.history[self.history_path].table[history_index]
|
||||
self._private_cur_pos = #self.command + 2
|
||||
end
|
||||
elseif key == "Next" then
|
||||
if history_index < history_items(self.history_path) then
|
||||
history_index = history_index + 1
|
||||
|
||||
self.command = data.history[self.history_path].table[history_index]
|
||||
self._private_cur_pos = #self.command + 2
|
||||
elseif history_index == history_items(self.history_path) then
|
||||
history_index = history_index + 1
|
||||
|
||||
self.command = ""
|
||||
self._private_cur_pos = 1
|
||||
end
|
||||
else
|
||||
-- wlen() is UTF-8 aware but #key is not,
|
||||
-- so check that we have one UTF-8 char but advance the cursor of # position
|
||||
if key:wlen() == 1 then
|
||||
if self.select_all then self.command = "" end
|
||||
self.command = self.command:sub(1, self._private_cur_pos - 1) .. key .. self.command:sub(self._private_cur_pos)
|
||||
self._private_cur_pos = self._private_cur_pos + #key
|
||||
end
|
||||
end
|
||||
if self._private_cur_pos < 1 then
|
||||
self._private_cur_pos = 1
|
||||
elseif self._private_cur_pos > #self.command + 1 then
|
||||
self._private_cur_pos = #self.command + 1
|
||||
end
|
||||
self.select_all = nil
|
||||
end
|
||||
|
||||
update(self)
|
||||
if self.changed_callback then
|
||||
self.changed_callback(self.command)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function prompt:stop()
|
||||
keygrabber.stop(self._private.grabber)
|
||||
history_save(self.history_path)
|
||||
if self.done_callback then self.done_callback() end
|
||||
return false
|
||||
end
|
||||
|
||||
local function new(args)
|
||||
args = args or {}
|
||||
|
||||
args.command = args.text or ""
|
||||
args.prompt = args.prompt or ""
|
||||
args.text = args.text or ""
|
||||
args.font = args.font or beautiful.prompt_font or beautiful.font
|
||||
args.bg_cursor = args.bg_cursor or beautiful.prompt_bg_cursor or beautiful.bg_focus or "white"
|
||||
args.fg_cursor = args.fg_cursor or beautiful.prompt_fg_cursor or beautiful.fg_focus or "black"
|
||||
args.ul_cursor = args.ul_cursor or nil
|
||||
args.reset_on_stop = args.reset_on_stop == nil and true or args.reset_on_stop
|
||||
args.select_all = args.select_all or nil
|
||||
args.highlighter = args.highlighter or nil
|
||||
args.hooks = args.hooks or {}
|
||||
args.keypressed_callback = args.keypressed_callback or nil
|
||||
args.changed_callback = args.changed_callback or nil
|
||||
args.done_callback = args.done_callback or nil
|
||||
args.history_max = args.history_max or nil
|
||||
args.history_path = args.history_path or nil
|
||||
args.completion_callback = args.completion_callback or nil
|
||||
args.exe_callback = args.exe_callback or nil
|
||||
args.textbox = args.textbox or wibox.widget.textbox()
|
||||
|
||||
-- Build the hook map
|
||||
local hooks = {}
|
||||
for _,v in ipairs(args.hooks) do
|
||||
if #v == 3 then
|
||||
local _,key,callback = unpack(v)
|
||||
if type(callback) == "function" then
|
||||
hooks[key] = hooks[key] or {}
|
||||
hooks[key][#hooks[key]+1] = v
|
||||
else
|
||||
gdebug.print_warning("The hook's 3rd parameter has to be a function.")
|
||||
end
|
||||
else
|
||||
gdebug.print_warning("The hook has to have 3 parameters.")
|
||||
end
|
||||
end
|
||||
args.hooks = hooks
|
||||
|
||||
local ret = gobject({})
|
||||
ret._private = {}
|
||||
gtable.crush(ret, prompt)
|
||||
gtable.crush(ret, args)
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
function prompt.mt:__call(...)
|
||||
return new(...)
|
||||
end
|
||||
|
||||
return setmetatable(prompt, prompt.mt)
|
7
config/awesome/module/bling/widget/init.lua
Normal file
|
@ -0,0 +1,7 @@
|
|||
return {
|
||||
tag_preview = require(... .. ".tag_preview"),
|
||||
task_preview = require(... .. ".task_preview"),
|
||||
window_switcher = require(... .. ".window_switcher"),
|
||||
tabbed_misc = require(... .. ".tabbed_misc"),
|
||||
app_launcher = require(... .. ".app_launcher"),
|
||||
}
|
57
config/awesome/module/bling/widget/tabbar/boxes.lua
Normal file
|
@ -0,0 +1,57 @@
|
|||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
local wibox = require("wibox")
|
||||
|
||||
local beautiful = require("beautiful")
|
||||
|
||||
local bg_normal = beautiful.tabbar_bg_normal or beautiful.bg_normal or "#ffffff"
|
||||
local fg_normal = beautiful.tabbar_fg_normal or beautiful.fg_normal or "#000000"
|
||||
local bg_focus = beautiful.tabbar_bg_focus or beautiful.bg_focus or "#000000"
|
||||
local fg_focus = beautiful.tabbar_fg_focus or beautiful.fg_focus or "#ffffff"
|
||||
local bg_focus_inactive = beautiful.tabbar_bg_focus_inactive or bg_focus
|
||||
local fg_focus_inactive = beautiful.tabbar_fg_focus_inactive or fg_focus
|
||||
local bg_normal_inactive = beautiful.tabbar_bg_normal_inactive or bg_normal
|
||||
local fg_normal_inactive = beautiful.tabbar_fg_normal_inactive or fg_normal
|
||||
local font = beautiful.tabbar_font or beautiful.font or "Hack 15"
|
||||
local size = beautiful.tabbar_size or 40
|
||||
local position = beautiful.tabbar_position or "bottom"
|
||||
|
||||
local function create(c, focused_bool, buttons, inactive_bool)
|
||||
local bg_temp = inactive_bool and bg_normal_inactive or bg_normal
|
||||
local fg_temp = inactive_bool and fg_normal_inactive or fg_normal
|
||||
if focused_bool then
|
||||
bg_temp = inactive_bool and bg_focus_inactive or bg_focus
|
||||
fg_temp = inactive_bool and fg_focus_inactive or fg_focus
|
||||
end
|
||||
local wid_temp = wibox.widget({
|
||||
{
|
||||
{
|
||||
awful.widget.clienticon(c),
|
||||
left = 10,
|
||||
right = 10,
|
||||
bottom = 10,
|
||||
top = 10,
|
||||
widget = wibox.container.margin(),
|
||||
},
|
||||
widget = wibox.container.place(),
|
||||
},
|
||||
buttons = buttons,
|
||||
bg = bg_temp,
|
||||
widget = wibox.container.background(),
|
||||
})
|
||||
return wid_temp
|
||||
end
|
||||
|
||||
local layout = wibox.layout.fixed.horizontal
|
||||
if position == "left" or position == "right" then
|
||||
layout = wibox.layout.fixed.vertical
|
||||
end
|
||||
|
||||
return {
|
||||
layout = layout,
|
||||
create = create,
|
||||
position = position,
|
||||
size = size,
|
||||
bg_normal = bg_normal,
|
||||
bg_focus = bg_normal,
|
||||
}
|
60
config/awesome/module/bling/widget/tabbar/default.lua
Normal file
|
@ -0,0 +1,60 @@
|
|||
local gears = require("gears")
|
||||
local wibox = require("wibox")
|
||||
|
||||
local beautiful = require("beautiful")
|
||||
|
||||
local bg_normal = beautiful.tabbar_bg_normal or beautiful.bg_normal or "#ffffff"
|
||||
local fg_normal = beautiful.tabbar_fg_normal or beautiful.fg_normal or "#000000"
|
||||
local bg_focus = beautiful.tabbar_bg_focus or beautiful.bg_focus or "#000000"
|
||||
local fg_focus = beautiful.tabbar_fg_focus or beautiful.fg_focus or "#ffffff"
|
||||
local bg_focus_inactive = beautiful.tabbar_bg_focus_inactive or bg_focus
|
||||
local fg_focus_inactive = beautiful.tabbar_fg_focus_inactive or fg_focus
|
||||
local bg_normal_inactive = beautiful.tabbar_bg_normal_inactive or bg_normal
|
||||
local fg_normal_inactive = beautiful.tabbar_fg_normal_inactive or fg_normal
|
||||
local font = beautiful.tabbar_font or beautiful.font or "Hack 15"
|
||||
local size = beautiful.tabbar_size or 20
|
||||
local position = beautiful.tabbar_position or "top"
|
||||
|
||||
local function create(c, focused_bool, buttons, inactive_bool)
|
||||
local flexlist = wibox.layout.flex.horizontal()
|
||||
local title_temp = c.name or c.class or "-"
|
||||
local bg_temp = inactive_bool and bg_normal_inactive or bg_normal
|
||||
local fg_temp = inactive_bool and fg_normal_inactive or fg_normal
|
||||
if focused_bool then
|
||||
bg_temp = inactive_bool and bg_focus_inactive or bg_focus
|
||||
fg_temp = inactive_bool and fg_focus_inactive or fg_focus
|
||||
end
|
||||
local text_temp = wibox.widget.textbox()
|
||||
text_temp.align = "center"
|
||||
text_temp.valign = "center"
|
||||
text_temp.font = font
|
||||
text_temp.markup = "<span foreground='"
|
||||
.. fg_temp
|
||||
.. "'>"
|
||||
.. title_temp
|
||||
.. "</span>"
|
||||
c:connect_signal("property::name", function(_)
|
||||
local title_temp = c.name or c.class or "-"
|
||||
text_temp.markup = "<span foreground='"
|
||||
.. fg_temp
|
||||
.. "'>"
|
||||
.. title_temp
|
||||
.. "</span>"
|
||||
end)
|
||||
local wid_temp = wibox.widget({
|
||||
text_temp,
|
||||
buttons = buttons,
|
||||
bg = bg_temp,
|
||||
widget = wibox.container.background(),
|
||||
})
|
||||
return wid_temp
|
||||
end
|
||||
|
||||
return {
|
||||
layout = wibox.layout.flex.horizontal,
|
||||
create = create,
|
||||
position = position,
|
||||
size = size,
|
||||
bg_normal = bg_normal,
|
||||
bg_focus = bg_focus,
|
||||
}
|
271
config/awesome/module/bling/widget/tabbar/modern.lua
Normal file
|
@ -0,0 +1,271 @@
|
|||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
local wibox = require("wibox")
|
||||
local beautiful = require("beautiful")
|
||||
local xresources = require("beautiful.xresources")
|
||||
local dpi = xresources.apply_dpi
|
||||
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
|
||||
|
||||
local bg_normal = beautiful.tabbar_bg_normal or beautiful.bg_normal or "#ffffff"
|
||||
local fg_normal = beautiful.tabbar_fg_normal or beautiful.fg_normal or "#000000"
|
||||
local bg_focus = beautiful.tabbar_bg_focus or beautiful.bg_focus or "#000000"
|
||||
local fg_focus = beautiful.tabbar_fg_focus or beautiful.fg_focus or "#ffffff"
|
||||
local bg_focus_inactive = beautiful.tabbar_bg_focus_inactive or bg_focus
|
||||
local fg_focus_inactive = beautiful.tabbar_fg_focus_inactive or fg_focus
|
||||
local bg_normal_inactive = beautiful.tabbar_bg_normal_inactive or bg_normal
|
||||
local fg_normal_inactive = beautiful.tabbar_fg_normal_inactive or fg_normal
|
||||
local font = beautiful.tabbar_font or beautiful.font or "Hack 15"
|
||||
local size = beautiful.tabbar_size or dpi(40)
|
||||
local border_radius = beautiful.mstab_border_radius
|
||||
or beautiful.border_radius
|
||||
or 6
|
||||
local position = beautiful.tabbar_position or "top"
|
||||
local close_color = beautiful.tabbar_color_close
|
||||
or beautiful.xcolor1
|
||||
or "#f9929b"
|
||||
local min_color = beautiful.tabbar_color_min or beautiful.xcolor3 or "#fbdf90"
|
||||
local float_color = beautiful.tabbar_color_float
|
||||
or beautiful.xcolor5
|
||||
or "#ccaced"
|
||||
|
||||
-- Helper to create buttons
|
||||
local function create_title_button(c, color_focus, color_unfocus)
|
||||
local tb_color = wibox.widget({
|
||||
wibox.widget.textbox(),
|
||||
forced_width = dpi(8),
|
||||
forced_height = dpi(8),
|
||||
bg = color_focus,
|
||||
shape = gears.shape.circle,
|
||||
widget = wibox.container.background,
|
||||
})
|
||||
|
||||
local tb = wibox.widget({
|
||||
tb_color,
|
||||
width = dpi(25),
|
||||
height = dpi(25),
|
||||
strategy = "min",
|
||||
layout = wibox.layout.constraint,
|
||||
})
|
||||
|
||||
local function update()
|
||||
if client.focus == c then
|
||||
tb_color.bg = color_focus
|
||||
else
|
||||
tb_color.bg = color_unfocus
|
||||
end
|
||||
end
|
||||
update()
|
||||
c:connect_signal("focus", update)
|
||||
c:connect_signal("unfocus", update)
|
||||
|
||||
tb:connect_signal("mouse::enter", function()
|
||||
tb_color.bg = color_focus .. "70"
|
||||
end)
|
||||
|
||||
tb:connect_signal("mouse::leave", function()
|
||||
tb_color.bg = color_focus
|
||||
end)
|
||||
|
||||
tb.visible = true
|
||||
return tb
|
||||
end
|
||||
|
||||
local function create(c, focused_bool, buttons, inactive_bool)
|
||||
-- local flexlist = wibox.layout.flex.horizontal()
|
||||
local title_temp = c.name or c.class or "-"
|
||||
local bg_temp = inactive_bool and bg_normal_inactive or bg_normal
|
||||
local fg_temp = inactive_bool and fg_normal_inactive or fg_normal
|
||||
if focused_bool then
|
||||
bg_temp = inactive_bool and bg_focus_inactive or bg_focus
|
||||
fg_temp = inactive_bool and fg_focus_inactive or fg_focus
|
||||
end
|
||||
local text_temp = wibox.widget.textbox()
|
||||
text_temp.align = "center"
|
||||
text_temp.valign = "center"
|
||||
text_temp.font = font
|
||||
text_temp.markup = "<span foreground='"
|
||||
.. fg_temp
|
||||
.. "'>"
|
||||
.. title_temp
|
||||
.. "</span>"
|
||||
c:connect_signal("property::name", function(_)
|
||||
local title_temp = c.name or c.class or "-"
|
||||
text_temp.markup = "<span foreground='"
|
||||
.. fg_temp
|
||||
.. "'>"
|
||||
.. title_temp
|
||||
.. "</span>"
|
||||
end)
|
||||
|
||||
local tab_content = wibox.widget({
|
||||
{
|
||||
awful.widget.clienticon(c),
|
||||
top = dpi(6),
|
||||
left = dpi(15),
|
||||
bottom = dpi(6),
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
text_temp,
|
||||
nill,
|
||||
expand = "none",
|
||||
layout = wibox.layout.align.horizontal,
|
||||
})
|
||||
|
||||
local close = create_title_button(c, close_color, bg_normal)
|
||||
close:connect_signal("button::press", function()
|
||||
c:kill()
|
||||
end)
|
||||
|
||||
local floating = create_title_button(c, float_color, bg_normal)
|
||||
floating:connect_signal("button::press", function()
|
||||
c.floating = not c.floating
|
||||
end)
|
||||
|
||||
local min = create_title_button(c, min_color, bg_normal)
|
||||
min:connect_signal("button::press", function()
|
||||
c.minimized = true
|
||||
end)
|
||||
|
||||
if focused_bool then
|
||||
tab_content = wibox.widget({
|
||||
{
|
||||
awful.widget.clienticon(c),
|
||||
top = dpi(10),
|
||||
left = dpi(15),
|
||||
bottom = dpi(10),
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
text_temp,
|
||||
{
|
||||
{ min, floating, close, layout = wibox.layout.fixed.horizontal },
|
||||
top = dpi(10),
|
||||
right = dpi(10),
|
||||
bottom = dpi(10),
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
expand = "none",
|
||||
layout = wibox.layout.align.horizontal,
|
||||
})
|
||||
end
|
||||
|
||||
local main_content = nil
|
||||
local left_shape = nil
|
||||
local right_shape = nil
|
||||
|
||||
if position == "top" then
|
||||
main_content = wibox.widget({
|
||||
{
|
||||
tab_content,
|
||||
bg = bg_temp,
|
||||
shape = helpers.shape.prrect(
|
||||
border_radius,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
),
|
||||
widget = wibox.container.background,
|
||||
},
|
||||
top = dpi(8),
|
||||
widget = wibox.container.margin,
|
||||
})
|
||||
|
||||
left_shape = helpers.shape.prrect(
|
||||
border_radius,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
)
|
||||
right_shape = helpers.shape.prrect(
|
||||
border_radius,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
)
|
||||
else
|
||||
main_content = wibox.widget({
|
||||
{
|
||||
tab_content,
|
||||
bg = bg_temp,
|
||||
shape = helpers.shape.prrect(
|
||||
border_radius,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
),
|
||||
widget = wibox.container.background,
|
||||
},
|
||||
bottom = dpi(8),
|
||||
widget = wibox.container.margin,
|
||||
})
|
||||
|
||||
left_shape = helpers.shape.prrect(
|
||||
border_radius,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
)
|
||||
right_shape = helpers.shape.prrect(
|
||||
border_radius,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
)
|
||||
end
|
||||
|
||||
local wid_temp = wibox.widget({
|
||||
buttons = buttons,
|
||||
{
|
||||
{
|
||||
{
|
||||
wibox.widget.textbox(),
|
||||
bg = bg_normal,
|
||||
shape = left_shape,
|
||||
widget = wibox.container.background,
|
||||
},
|
||||
bg = bg_temp,
|
||||
shape = gears.rectangle,
|
||||
widget = wibox.container.background,
|
||||
},
|
||||
width = border_radius + (border_radius / 2),
|
||||
height = size,
|
||||
strategy = "exact",
|
||||
layout = wibox.layout.constraint,
|
||||
},
|
||||
main_content,
|
||||
{
|
||||
{
|
||||
{
|
||||
wibox.widget.textbox(),
|
||||
bg = bg_normal,
|
||||
shape = right_shape,
|
||||
widget = wibox.container.background,
|
||||
},
|
||||
bg = bg_temp,
|
||||
shape = gears.rectangle,
|
||||
widget = wibox.container.background,
|
||||
},
|
||||
width = border_radius + (border_radius / 2),
|
||||
height = size,
|
||||
strategy = "exact",
|
||||
layout = wibox.layout.constraint,
|
||||
},
|
||||
|
||||
layout = wibox.layout.align.horizontal,
|
||||
})
|
||||
return wid_temp
|
||||
end
|
||||
|
||||
return {
|
||||
layout = wibox.layout.flex.horizontal,
|
||||
create = create,
|
||||
position = position,
|
||||
size = size,
|
||||
bg_normal = bg_normal,
|
||||
bg_focus = bg_focus,
|
||||
}
|
81
config/awesome/module/bling/widget/tabbar/pure.lua
Normal file
|
@ -0,0 +1,81 @@
|
|||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
local wibox = require("wibox")
|
||||
local gcolor = require("gears.color")
|
||||
local beautiful = require("beautiful")
|
||||
|
||||
local bg_normal = beautiful.tabbar_bg_normal or beautiful.bg_normal or "#ffffff"
|
||||
local fg_normal = beautiful.tabbar_fg_normal or beautiful.fg_normal or "#000000"
|
||||
local bg_focus = beautiful.tabbar_bg_focus or beautiful.bg_focus or "#000000"
|
||||
local fg_focus = beautiful.tabbar_fg_focus or beautiful.fg_focus or "#ffffff"
|
||||
local bg_focus_inactive = beautiful.tabbar_bg_focus_inactive or bg_focus
|
||||
local fg_focus_inactive = beautiful.tabbar_fg_focus_inactive or fg_focus
|
||||
local bg_normal_inactive = beautiful.tabbar_bg_normal_inactive or bg_normal
|
||||
local fg_normal_inactive = beautiful.tabbar_fg_normal_inactive or fg_normal
|
||||
local font = beautiful.tabbar_font or beautiful.font or "Hack 15"
|
||||
local size = beautiful.tabbar_size or 20
|
||||
local position = beautiful.tabbar_position or "top"
|
||||
|
||||
local function create(c, focused_bool, buttons, inactive_bool)
|
||||
local bg_temp = inactive_bool and bg_normal_inactive or bg_normal
|
||||
local fg_temp = inactive_bool and fg_normal_inactive or fg_normal
|
||||
if focused_bool then
|
||||
bg_temp = inactive_bool and bg_focus_inactive or bg_focus
|
||||
fg_temp = inactive_bool and fg_focus_inactive or fg_focus
|
||||
end
|
||||
|
||||
local wid_temp = wibox.widget({
|
||||
{
|
||||
{ -- Left
|
||||
wibox.widget.base.make_widget(
|
||||
awful.titlebar.widget.iconwidget(c)
|
||||
),
|
||||
buttons = buttons,
|
||||
layout = wibox.layout.fixed.horizontal,
|
||||
},
|
||||
{ -- Title
|
||||
wibox.widget.base.make_widget(
|
||||
awful.titlebar.widget.titlewidget(c)
|
||||
),
|
||||
buttons = buttons,
|
||||
widget = wibox.container.place,
|
||||
},
|
||||
{ -- Right
|
||||
focused_bool and wibox.widget.base.make_widget(
|
||||
awful.titlebar.widget.floatingbutton(c)
|
||||
) or nil,
|
||||
focused_bool and wibox.widget.base.make_widget(
|
||||
awful.titlebar.widget.stickybutton(c)
|
||||
) or nil,
|
||||
focused_bool and wibox.widget.base.make_widget(
|
||||
awful.titlebar.widget.ontopbutton(c)
|
||||
) or nil,
|
||||
focused_bool and wibox.widget.base.make_widget(
|
||||
awful.titlebar.widget.maximizedbutton(c)
|
||||
) or nil,
|
||||
focused_bool and wibox.widget.base.make_widget(
|
||||
awful.titlebar.widget.minimizebutton(c)
|
||||
) or nil,
|
||||
focused_bool and wibox.widget.base.make_widget(
|
||||
awful.titlebar.widget.closebutton(c)
|
||||
) or nil,
|
||||
layout = wibox.layout.fixed.horizontal,
|
||||
},
|
||||
layout = wibox.layout.align.horizontal,
|
||||
},
|
||||
bg = bg_temp,
|
||||
fg = fg_temp,
|
||||
widget = wibox.container.background,
|
||||
})
|
||||
|
||||
return wid_temp
|
||||
end
|
||||
|
||||
return {
|
||||
layout = wibox.layout.flex.horizontal,
|
||||
create = create,
|
||||
position = position,
|
||||
size = size,
|
||||
bg_normal = bg_normal,
|
||||
bg_focus = bg_focus,
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
local wibox = require("wibox")
|
||||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
local beautiful = require("beautiful")
|
||||
local dpi = require("beautiful.xresources").apply_dpi
|
||||
|
||||
local function tabobj_support(self, c, index, clients)
|
||||
-- Self is the background widget in this context
|
||||
if not c.bling_tabbed and #c.bling_tabbed.clients > 1 then
|
||||
return
|
||||
end
|
||||
|
||||
local group = c.bling_tabbed
|
||||
|
||||
-- TODO: Allow customization here
|
||||
local layout_v = wibox.widget {
|
||||
vertical_spacing = dpi(2),
|
||||
horizontal_spacing = dpi(2),
|
||||
layout = wibox.layout.grid.horizontal,
|
||||
forced_num_rows = 2,
|
||||
forced_num_cols = 2,
|
||||
homogeneous = true
|
||||
}
|
||||
|
||||
local wrapper = wibox.widget({
|
||||
layout_v,
|
||||
id = "click_role",
|
||||
widget = wibox.container.margin,
|
||||
margins = dpi(5),
|
||||
})
|
||||
|
||||
-- To get the ball rolling.
|
||||
for idx, c in ipairs(group.clients) do
|
||||
if not (c and c.icon) then goto skip end
|
||||
|
||||
-- Add to the last layout
|
||||
layout_v:add(wibox.widget {
|
||||
{
|
||||
widget = awful.widget.clienticon,
|
||||
client = c
|
||||
},
|
||||
widget = wibox.container.constraint,
|
||||
width = dpi(24),
|
||||
height = dpi(24)
|
||||
})
|
||||
::skip::
|
||||
end
|
||||
self.widget = wrapper
|
||||
end
|
||||
|
||||
return tabobj_support
|
9
config/awesome/module/bling/widget/tabbed_misc/init.lua
Normal file
|
@ -0,0 +1,9 @@
|
|||
return {
|
||||
titlebar_indicator = require(
|
||||
tostring(...):match(".*bling")
|
||||
.. ".widget.tabbed_misc.titlebar_indicator"
|
||||
),
|
||||
custom_tasklist = require(
|
||||
tostring(...):match(".*bling") .. ".widget.tabbed_misc.custom_tasklist"
|
||||
),
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
local wibox = require("wibox")
|
||||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
local beautiful = require("beautiful")
|
||||
local dpi = require("beautiful.xresources").apply_dpi
|
||||
local tabbed_module = require(
|
||||
tostring(...):match(".*bling") .. ".module.tabbed"
|
||||
)
|
||||
|
||||
-- Just check if a table contains a value.
|
||||
local function tbl_contains(tbl, item)
|
||||
for _, v in ipairs(tbl) do
|
||||
if v == item then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Needs to be run, every time a new titlbear is created
|
||||
return function(c, opts)
|
||||
-- Args & Fallback -- Widget templates are in their original loactions
|
||||
opts = gears.table.crush({
|
||||
layout_spacing = dpi(4),
|
||||
icon_size = dpi(20),
|
||||
icon_margin = dpi(4),
|
||||
bg_color_focus = "#ff0000",
|
||||
bg_color = "#00000000",
|
||||
fg_color = "#fafafa",
|
||||
fg_color_focus = "#e0e0e0",
|
||||
icon_shape = function(cr, w, h)
|
||||
gears.shape.rounded_rect(cr, w, h, 0)
|
||||
end,
|
||||
layout = wibox.layout.fixed.horizontal,
|
||||
}, gears.table.join(
|
||||
opts,
|
||||
beautiful.bling_tabbed_misc_titlebar_indicator
|
||||
))
|
||||
|
||||
-- Container to store icons
|
||||
local tabbed_icons = wibox.widget({
|
||||
layout = opts.layout,
|
||||
spacing = opts.layout_spacing,
|
||||
})
|
||||
|
||||
awesome.connect_signal("bling::tabbed::client_removed", function(_, removed_c)
|
||||
-- Remove from list
|
||||
for idx, icon in ipairs(tabbed_icons.children) do
|
||||
if icon._client == removed_c then
|
||||
tabbed_icons:remove(idx)
|
||||
end
|
||||
end
|
||||
|
||||
-- Empty list
|
||||
if removed_c == c then
|
||||
tabbed_icons:reset()
|
||||
end
|
||||
end)
|
||||
|
||||
local function recreate(group)
|
||||
if tbl_contains(group.clients, c) then
|
||||
tabbed_icons:reset()
|
||||
local focused = group.clients[group.focused_idx]
|
||||
|
||||
-- Autohide?
|
||||
if #group.clients == 1 then
|
||||
return
|
||||
end
|
||||
|
||||
for idx, client in ipairs(group.clients) do
|
||||
local widget = wibox.widget(
|
||||
opts.widget_template or {
|
||||
{
|
||||
{
|
||||
{
|
||||
id = "icon_role",
|
||||
forced_width = opts.icon_size,
|
||||
forced_height = opts.icon_size,
|
||||
widget = awful.widget.clienticon,
|
||||
},
|
||||
margins = opts.icon_margin,
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
shape = opts.icon_shape,
|
||||
id = "bg_role",
|
||||
widget = wibox.container.background,
|
||||
},
|
||||
halign = "center",
|
||||
valign = "center",
|
||||
widget = wibox.container.place,
|
||||
})
|
||||
|
||||
widget._client = client
|
||||
|
||||
-- No creation call back since this would be called on creation & every time the widget updated.
|
||||
if opts.widget_template and opts.widget_template.update_callback then
|
||||
opts.widget_template.update_callback(widget, client, group)
|
||||
end
|
||||
|
||||
-- Add icons & etc
|
||||
for _, w in ipairs(widget:get_children_by_id("icon_role")) do
|
||||
-- TODO: Allow fallback icon?
|
||||
w.image = client.icon
|
||||
w.client = client
|
||||
end
|
||||
|
||||
for _, w in ipairs(widget:get_children_by_id("bg_role")) do
|
||||
w:add_button(awful.button({}, 1, function()
|
||||
tabbed_module.switch_to(group, idx)
|
||||
end))
|
||||
if client == focused then
|
||||
w.bg = opts.bg_color_focus
|
||||
w.fg = opts.fg_color_focus
|
||||
else
|
||||
w.bg = opts.bg_color
|
||||
w.fg = opts.fg_color
|
||||
end
|
||||
end
|
||||
|
||||
for _, w in ipairs(widget:get_children_by_id("text_role")) do
|
||||
w.text = client.name
|
||||
end
|
||||
|
||||
tabbed_icons:add(widget)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
awesome.connect_signal("bling::tabbed::client_added", recreate)
|
||||
awesome.connect_signal("bling::tabbed::changed_focus", recreate)
|
||||
|
||||
return tabbed_icons
|
||||
end
|
246
config/awesome/module/bling/widget/tag_preview.lua
Normal file
|
@ -0,0 +1,246 @@
|
|||
--
|
||||
-- Provides:
|
||||
-- bling::tag_preview::update -- first line is the signal
|
||||
-- t (tag) -- indented lines are function parameters
|
||||
-- bling::tag_preview::visibility
|
||||
-- s (screen)
|
||||
-- v (boolean)
|
||||
--
|
||||
local awful = require("awful")
|
||||
local wibox = require("wibox")
|
||||
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
|
||||
local gears = require("gears")
|
||||
local beautiful = require("beautiful")
|
||||
local dpi = beautiful.xresources.apply_dpi
|
||||
local cairo = require("lgi").cairo
|
||||
|
||||
local function draw_widget(
|
||||
t,
|
||||
tag_preview_image,
|
||||
scale,
|
||||
screen_radius,
|
||||
client_radius,
|
||||
client_opacity,
|
||||
client_bg,
|
||||
client_border_color,
|
||||
client_border_width,
|
||||
widget_bg,
|
||||
widget_border_color,
|
||||
widget_border_width,
|
||||
geo,
|
||||
margin,
|
||||
background_image
|
||||
)
|
||||
local client_list = wibox.layout.manual()
|
||||
client_list.forced_height = geo.height
|
||||
client_list.forced_width = geo.width
|
||||
local tag_screen = t.screen
|
||||
for i, c in ipairs(t:clients()) do
|
||||
if not c.hidden and not c.minimized then
|
||||
|
||||
|
||||
local img_box = wibox.widget ({
|
||||
resize = true,
|
||||
forced_height = 100 * scale,
|
||||
forced_width = 100 * scale,
|
||||
widget = wibox.widget.imagebox,
|
||||
})
|
||||
|
||||
-- If fails to set image, fallback to a awesome icon
|
||||
if not pcall(function() img_box.image = gears.surface.load(c.icon) end) then
|
||||
img_box.image = beautiful.theme_assets.awesome_icon (24, "#222222", "#fafafa")
|
||||
end
|
||||
|
||||
if tag_preview_image then
|
||||
if c.prev_content or t.selected then
|
||||
local content
|
||||
if t.selected then
|
||||
content = gears.surface(c.content)
|
||||
else
|
||||
content = gears.surface(c.prev_content)
|
||||
end
|
||||
local cr = cairo.Context(content)
|
||||
local x, y, w, h = cr:clip_extents()
|
||||
local img = cairo.ImageSurface.create(
|
||||
cairo.Format.ARGB32,
|
||||
w - x,
|
||||
h - y
|
||||
)
|
||||
cr = cairo.Context(img)
|
||||
cr:set_source_surface(content, 0, 0)
|
||||
cr.operator = cairo.Operator.SOURCE
|
||||
cr:paint()
|
||||
|
||||
img_box = wibox.widget({
|
||||
image = gears.surface.load(img),
|
||||
resize = true,
|
||||
opacity = client_opacity,
|
||||
forced_height = math.floor(c.height * scale),
|
||||
forced_width = math.floor(c.width * scale),
|
||||
widget = wibox.widget.imagebox,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local client_box = wibox.widget({
|
||||
{
|
||||
nil,
|
||||
{
|
||||
nil,
|
||||
img_box,
|
||||
nil,
|
||||
expand = "outside",
|
||||
layout = wibox.layout.align.horizontal,
|
||||
},
|
||||
nil,
|
||||
expand = "outside",
|
||||
widget = wibox.layout.align.vertical,
|
||||
},
|
||||
forced_height = math.floor(c.height * scale),
|
||||
forced_width = math.floor(c.width * scale),
|
||||
bg = client_bg,
|
||||
shape_border_color = client_border_color,
|
||||
shape_border_width = client_border_width,
|
||||
shape = helpers.shape.rrect(client_radius),
|
||||
widget = wibox.container.background,
|
||||
})
|
||||
|
||||
client_box.point = {
|
||||
x = math.floor((c.x - geo.x) * scale),
|
||||
y = math.floor((c.y - geo.y) * scale),
|
||||
}
|
||||
|
||||
client_list:add(client_box)
|
||||
end
|
||||
end
|
||||
|
||||
return wibox.widget {
|
||||
{
|
||||
background_image,
|
||||
{
|
||||
{
|
||||
{
|
||||
{
|
||||
client_list,
|
||||
forced_height = geo.height,
|
||||
forced_width = geo.width,
|
||||
widget = wibox.container.place,
|
||||
},
|
||||
layout = wibox.layout.align.horizontal,
|
||||
},
|
||||
layout = wibox.layout.align.vertical,
|
||||
},
|
||||
margins = margin,
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
layout = wibox.layout.stack
|
||||
},
|
||||
bg = widget_bg,
|
||||
shape_border_width = widget_border_width,
|
||||
shape_border_color = widget_border_color,
|
||||
shape = helpers.shape.rrect(screen_radius),
|
||||
widget = wibox.container.background,
|
||||
}
|
||||
end
|
||||
|
||||
local enable = function(opts)
|
||||
local opts = opts or {}
|
||||
|
||||
local tag_preview_image = opts.show_client_content or false
|
||||
local widget_x = opts.x or dpi(20)
|
||||
local widget_y = opts.y or dpi(20)
|
||||
local scale = opts.scale or 0.2
|
||||
local work_area = opts.honor_workarea or false
|
||||
local padding = opts.honor_padding or false
|
||||
local placement_fn = opts.placement_fn or nil
|
||||
local background_image = opts.background_widget or nil
|
||||
|
||||
local margin = beautiful.tag_preview_widget_margin or dpi(0)
|
||||
local screen_radius = beautiful.tag_preview_widget_border_radius or dpi(0)
|
||||
local client_radius = beautiful.tag_preview_client_border_radius or dpi(0)
|
||||
local client_opacity = beautiful.tag_preview_client_opacity or 0.5
|
||||
local client_bg = beautiful.tag_preview_client_bg or "#000000"
|
||||
local client_border_color = beautiful.tag_preview_client_border_color
|
||||
or "#ffffff"
|
||||
local client_border_width = beautiful.tag_preview_client_border_width
|
||||
or dpi(3)
|
||||
local widget_bg = beautiful.tag_preview_widget_bg or "#000000"
|
||||
local widget_border_color = beautiful.tag_preview_widget_border_color
|
||||
or "#ffffff"
|
||||
local widget_border_width = beautiful.tag_preview_widget_border_width
|
||||
or dpi(3)
|
||||
|
||||
local tag_preview_box = awful.popup({
|
||||
type = "dropdown_menu",
|
||||
visible = false,
|
||||
ontop = true,
|
||||
placement = placement_fn,
|
||||
widget = wibox.container.background,
|
||||
input_passthrough = true,
|
||||
bg = "#00000000",
|
||||
})
|
||||
|
||||
tag.connect_signal("property::selected", function(t)
|
||||
-- Awesome switches up tags on startup really fast it seems, probably depends on what rules you have set
|
||||
-- which can cause the c.content to not show the correct image
|
||||
gears.timer
|
||||
{
|
||||
timeout = 0.1,
|
||||
call_now = false,
|
||||
autostart = true,
|
||||
single_shot = true,
|
||||
callback = function()
|
||||
if t.selected == true then
|
||||
for _, c in ipairs(t:clients()) do
|
||||
c.prev_content = gears.surface.duplicate_surface(c.content)
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
end)
|
||||
|
||||
awesome.connect_signal("bling::tag_preview::update", function(t)
|
||||
local geo = t.screen:get_bounding_geometry({
|
||||
honor_padding = padding,
|
||||
honor_workarea = work_area,
|
||||
})
|
||||
|
||||
tag_preview_box.maximum_width = scale * geo.width + margin * 2
|
||||
tag_preview_box.maximum_height = scale * geo.height + margin * 2
|
||||
|
||||
|
||||
tag_preview_box.widget = draw_widget(
|
||||
t,
|
||||
tag_preview_image,
|
||||
scale,
|
||||
screen_radius,
|
||||
client_radius,
|
||||
client_opacity,
|
||||
client_bg,
|
||||
client_border_color,
|
||||
client_border_width,
|
||||
widget_bg,
|
||||
widget_border_color,
|
||||
widget_border_width,
|
||||
geo,
|
||||
margin,
|
||||
background_image
|
||||
)
|
||||
end)
|
||||
|
||||
awesome.connect_signal("bling::tag_preview::visibility", function(s, v)
|
||||
if not placement_fn then
|
||||
tag_preview_box.x = s.geometry.x + widget_x
|
||||
tag_preview_box.y = s.geometry.y + widget_y
|
||||
end
|
||||
|
||||
if v == false then
|
||||
tag_preview_box.widget = nil
|
||||
collectgarbage("collect")
|
||||
end
|
||||
|
||||
tag_preview_box.visible = v
|
||||
end)
|
||||
end
|
||||
|
||||
return {enable = enable, draw_widget = draw_widget}
|
199
config/awesome/module/bling/widget/task_preview.lua
Normal file
|
@ -0,0 +1,199 @@
|
|||
--
|
||||
-- Provides:
|
||||
-- bling::task_preview::visibility
|
||||
-- s (screen)
|
||||
-- v (boolean)
|
||||
-- c (client)
|
||||
--
|
||||
local awful = require("awful")
|
||||
local wibox = require("wibox")
|
||||
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
|
||||
local gears = require("gears")
|
||||
local beautiful = require("beautiful")
|
||||
local dpi = beautiful.xresources.apply_dpi
|
||||
local cairo = require("lgi").cairo
|
||||
|
||||
-- TODO: rename structure to something better?
|
||||
local function draw_widget(
|
||||
c,
|
||||
widget_template,
|
||||
screen_radius,
|
||||
widget_bg,
|
||||
widget_border_color,
|
||||
widget_border_width,
|
||||
margin,
|
||||
widget_width,
|
||||
widget_height
|
||||
)
|
||||
if not pcall(function()
|
||||
return type(c.content)
|
||||
end) then
|
||||
return
|
||||
end
|
||||
|
||||
local content = nil
|
||||
if c.active then
|
||||
content = gears.surface(c.content)
|
||||
elseif c.prev_content then
|
||||
content = gears.surface(c.prev_content)
|
||||
end
|
||||
|
||||
local img = nil
|
||||
if content ~= nil then
|
||||
local cr = cairo.Context(content)
|
||||
local x, y, w, h = cr:clip_extents()
|
||||
img = cairo.ImageSurface.create(cairo.Format.ARGB32, w - x, h - y)
|
||||
cr = cairo.Context(img)
|
||||
cr:set_source_surface(content, 0, 0)
|
||||
cr.operator = cairo.Operator.SOURCE
|
||||
cr:paint()
|
||||
end
|
||||
|
||||
local widget = wibox.widget({
|
||||
(widget_template or {
|
||||
{
|
||||
{
|
||||
{
|
||||
{
|
||||
id = "icon_role",
|
||||
resize = true,
|
||||
forced_height = dpi(20),
|
||||
forced_width = dpi(20),
|
||||
widget = wibox.widget.imagebox,
|
||||
},
|
||||
{
|
||||
{
|
||||
id = "name_role",
|
||||
align = "center",
|
||||
widget = wibox.widget.textbox,
|
||||
},
|
||||
left = dpi(4),
|
||||
right = dpi(4),
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
layout = wibox.layout.align.horizontal,
|
||||
},
|
||||
{
|
||||
{
|
||||
{
|
||||
id = "image_role",
|
||||
resize = true,
|
||||
clip_shape = helpers.shape.rrect(screen_radius),
|
||||
widget = wibox.widget.imagebox,
|
||||
},
|
||||
valign = "center",
|
||||
halign = "center",
|
||||
widget = wibox.container.place,
|
||||
},
|
||||
top = margin * 0.25,
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
fill_space = true,
|
||||
layout = wibox.layout.fixed.vertical,
|
||||
},
|
||||
margins = margin,
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
bg = widget_bg,
|
||||
shape_border_width = widget_border_width,
|
||||
shape_border_color = widget_border_color,
|
||||
shape = helpers.shape.rrect(screen_radius),
|
||||
widget = wibox.container.background,
|
||||
}),
|
||||
width = widget_width,
|
||||
height = widget_height,
|
||||
widget = wibox.container.constraint,
|
||||
})
|
||||
|
||||
-- TODO: have something like a create callback here?
|
||||
|
||||
for _, w in ipairs(widget:get_children_by_id("image_role")) do
|
||||
w.image = img -- TODO: copy it with gears.surface.xxx or something
|
||||
end
|
||||
|
||||
for _, w in ipairs(widget:get_children_by_id("name_role")) do
|
||||
w.text = c.name
|
||||
end
|
||||
|
||||
for _, w in ipairs(widget:get_children_by_id("icon_role")) do
|
||||
w.image = c.icon -- TODO: detect clienticon
|
||||
end
|
||||
|
||||
return widget
|
||||
end
|
||||
|
||||
local enable = function(opts)
|
||||
local opts = opts or {}
|
||||
|
||||
local widget_x = opts.x or dpi(20)
|
||||
local widget_y = opts.y or dpi(20)
|
||||
local widget_height = opts.height or dpi(200)
|
||||
local widget_width = opts.width or dpi(200)
|
||||
local placement_fn = opts.placement_fn or nil
|
||||
|
||||
local margin = beautiful.task_preview_widget_margin or dpi(0)
|
||||
local screen_radius = beautiful.task_preview_widget_border_radius or dpi(0)
|
||||
local widget_bg = beautiful.task_preview_widget_bg or "#000000"
|
||||
local widget_border_color = beautiful.task_preview_widget_border_color
|
||||
or "#ffffff"
|
||||
local widget_border_width = beautiful.task_preview_widget_border_width
|
||||
or dpi(3)
|
||||
|
||||
local task_preview_box = awful.popup({
|
||||
type = "dropdown_menu",
|
||||
visible = false,
|
||||
ontop = true,
|
||||
placement = placement_fn,
|
||||
widget = wibox.container.background, -- A dummy widget to make awful.popup not scream
|
||||
input_passthrough = true,
|
||||
bg = "#00000000",
|
||||
})
|
||||
|
||||
tag.connect_signal("property::selected", function(t)
|
||||
-- Awesome switches up tags on startup really fast it seems, probably depends on what rules you have set
|
||||
-- which can cause the c.content to not show the correct image
|
||||
gears.timer
|
||||
{
|
||||
timeout = 0.1,
|
||||
call_now = false,
|
||||
autostart = true,
|
||||
single_shot = true,
|
||||
callback = function()
|
||||
if t.selected == true then
|
||||
for _, c in ipairs(t:clients()) do
|
||||
c.prev_content = gears.surface.duplicate_surface(c.content)
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
end)
|
||||
|
||||
awesome.connect_signal("bling::task_preview::visibility", function(s, v, c)
|
||||
if v then
|
||||
-- Update task preview contents
|
||||
task_preview_box.widget = draw_widget(
|
||||
c,
|
||||
opts.structure,
|
||||
screen_radius,
|
||||
widget_bg,
|
||||
widget_border_color,
|
||||
widget_border_width,
|
||||
margin,
|
||||
widget_width,
|
||||
widget_height
|
||||
)
|
||||
else
|
||||
task_preview_box.widget = nil
|
||||
collectgarbage("collect")
|
||||
end
|
||||
|
||||
if not placement_fn then
|
||||
task_preview_box.x = s.geometry.x + widget_x
|
||||
task_preview_box.y = s.geometry.y + widget_y
|
||||
end
|
||||
|
||||
task_preview_box.visible = v
|
||||
end)
|
||||
end
|
||||
|
||||
return { enable = enable, draw_widget = draw_widget }
|
454
config/awesome/module/bling/widget/window_switcher.lua
Normal file
|
@ -0,0 +1,454 @@
|
|||
local cairo = require("lgi").cairo
|
||||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
local wibox = require("wibox")
|
||||
local beautiful = require("beautiful")
|
||||
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
|
||||
local dpi = beautiful.xresources.apply_dpi
|
||||
|
||||
local window_switcher_first_client -- The client that was focused when the window_switcher was activated
|
||||
local window_switcher_minimized_clients = {} -- The clients that were minimized when the window switcher was activated
|
||||
local window_switcher_grabber
|
||||
|
||||
local get_num_clients = function()
|
||||
local minimized_clients_in_tag = 0
|
||||
local matcher = function(c)
|
||||
return awful.rules.match(
|
||||
c,
|
||||
{
|
||||
minimized = true,
|
||||
skip_taskbar = false,
|
||||
hidden = false,
|
||||
first_tag = awful.screen.focused().selected_tag,
|
||||
}
|
||||
)
|
||||
end
|
||||
for c in awful.client.iterate(matcher) do
|
||||
minimized_clients_in_tag = minimized_clients_in_tag + 1
|
||||
end
|
||||
return minimized_clients_in_tag + #awful.screen.focused().clients
|
||||
end
|
||||
|
||||
local window_switcher_hide = function(window_switcher_box)
|
||||
-- Add currently focused client to history
|
||||
if client.focus then
|
||||
local window_switcher_last_client = client.focus
|
||||
awful.client.focus.history.add(window_switcher_last_client)
|
||||
-- Raise client that was focused originally
|
||||
-- Then raise last focused client
|
||||
if
|
||||
window_switcher_first_client and window_switcher_first_client.valid
|
||||
then
|
||||
window_switcher_first_client:raise()
|
||||
window_switcher_last_client:raise()
|
||||
end
|
||||
end
|
||||
|
||||
-- Minimize originally minimized clients
|
||||
local s = awful.screen.focused()
|
||||
for _, c in pairs(window_switcher_minimized_clients) do
|
||||
if c and c.valid and not (client.focus and client.focus == c) then
|
||||
c.minimized = true
|
||||
end
|
||||
end
|
||||
-- Reset helper table
|
||||
window_switcher_minimized_clients = {}
|
||||
|
||||
-- Resume recording focus history
|
||||
awful.client.focus.history.enable_tracking()
|
||||
-- Stop and hide window_switcher
|
||||
awful.keygrabber.stop(window_switcher_grabber)
|
||||
window_switcher_box.visible = false
|
||||
window_switcher_box.widget = nil
|
||||
collectgarbage("collect")
|
||||
end
|
||||
|
||||
local function draw_widget(
|
||||
type,
|
||||
background,
|
||||
border_width,
|
||||
border_radius,
|
||||
border_color,
|
||||
clients_spacing,
|
||||
client_icon_horizontal_spacing,
|
||||
client_width,
|
||||
client_height,
|
||||
client_margins,
|
||||
thumbnail_margins,
|
||||
thumbnail_scale,
|
||||
name_margins,
|
||||
name_valign,
|
||||
name_forced_width,
|
||||
name_font,
|
||||
name_normal_color,
|
||||
name_focus_color,
|
||||
icon_valign,
|
||||
icon_width,
|
||||
mouse_keys
|
||||
)
|
||||
local tasklist_widget = type == "thumbnail"
|
||||
and awful.widget.tasklist({
|
||||
screen = awful.screen.focused(),
|
||||
filter = awful.widget.tasklist.filter.currenttags,
|
||||
buttons = mouse_keys,
|
||||
style = {
|
||||
font = name_font,
|
||||
fg_normal = name_normal_color,
|
||||
fg_focus = name_focus_color,
|
||||
},
|
||||
layout = {
|
||||
layout = wibox.layout.flex.horizontal,
|
||||
spacing = clients_spacing,
|
||||
},
|
||||
widget_template = {
|
||||
widget = wibox.container.background,
|
||||
id = "bg_role",
|
||||
forced_width = client_width,
|
||||
forced_height = client_height,
|
||||
create_callback = function(self, c, _, __)
|
||||
local content = gears.surface(c.content)
|
||||
local cr = cairo.Context(content)
|
||||
local x, y, w, h = cr:clip_extents()
|
||||
local img = cairo.ImageSurface.create(
|
||||
cairo.Format.ARGB32,
|
||||
w - x,
|
||||
h - y
|
||||
)
|
||||
cr = cairo.Context(img)
|
||||
cr:set_source_surface(content, 0, 0)
|
||||
cr.operator = cairo.Operator.SOURCE
|
||||
cr:paint()
|
||||
self:get_children_by_id("thumbnail")[1].image =
|
||||
gears.surface.load(
|
||||
img
|
||||
)
|
||||
end,
|
||||
{
|
||||
{
|
||||
{
|
||||
horizontal_fit_policy = thumbnail_scale == true
|
||||
and "fit"
|
||||
or "auto",
|
||||
vertical_fit_policy = thumbnail_scale == true
|
||||
and "fit"
|
||||
or "auto",
|
||||
id = "thumbnail",
|
||||
widget = wibox.widget.imagebox,
|
||||
},
|
||||
margins = thumbnail_margins,
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
{
|
||||
{
|
||||
{
|
||||
id = "icon_role",
|
||||
widget = wibox.widget.imagebox,
|
||||
},
|
||||
forced_width = icon_width,
|
||||
valign = icon_valign,
|
||||
widget = wibox.container.place,
|
||||
},
|
||||
{
|
||||
{
|
||||
forced_width = name_forced_width,
|
||||
valign = name_valign,
|
||||
id = "text_role",
|
||||
widget = wibox.widget.textbox,
|
||||
},
|
||||
margins = name_margins,
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
spacing = client_icon_horizontal_spacing,
|
||||
layout = wibox.layout.fixed.horizontal,
|
||||
},
|
||||
layout = wibox.layout.flex.vertical,
|
||||
},
|
||||
},
|
||||
})
|
||||
or awful.widget.tasklist({
|
||||
screen = awful.screen.focused(),
|
||||
filter = awful.widget.tasklist.filter.currenttags,
|
||||
buttons = mouse_keys,
|
||||
style = {
|
||||
font = name_font,
|
||||
fg_normal = name_normal_color,
|
||||
fg_focus = name_focus_color,
|
||||
},
|
||||
layout = {
|
||||
layout = wibox.layout.fixed.vertical,
|
||||
spacing = clients_spacing,
|
||||
},
|
||||
widget_template = {
|
||||
widget = wibox.container.background,
|
||||
id = "bg_role",
|
||||
forced_width = client_width,
|
||||
forced_height = client_height,
|
||||
{
|
||||
{
|
||||
{
|
||||
id = "icon_role",
|
||||
widget = wibox.widget.imagebox,
|
||||
},
|
||||
forced_width = icon_width,
|
||||
valign = icon_valign,
|
||||
widget = wibox.container.place,
|
||||
},
|
||||
{
|
||||
{
|
||||
forced_width = name_forced_width,
|
||||
valign = name_valign,
|
||||
id = "text_role",
|
||||
widget = wibox.widget.textbox,
|
||||
},
|
||||
margins = name_margins,
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
spacing = client_icon_horizontal_spacing,
|
||||
layout = wibox.layout.fixed.horizontal,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return wibox.widget({
|
||||
{
|
||||
tasklist_widget,
|
||||
margins = client_margins,
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
shape_border_width = border_width,
|
||||
shape_border_color = border_color,
|
||||
bg = background,
|
||||
shape = helpers.shape.rrect(border_radius),
|
||||
widget = wibox.container.background,
|
||||
})
|
||||
end
|
||||
|
||||
local enable = function(opts)
|
||||
local opts = opts or {}
|
||||
|
||||
local type = opts.type or "thumbnail"
|
||||
local background = beautiful.window_switcher_widget_bg or "#000000"
|
||||
local border_width = beautiful.window_switcher_widget_border_width or dpi(3)
|
||||
local border_radius = beautiful.window_switcher_widget_border_radius
|
||||
or dpi(0)
|
||||
local border_color = beautiful.window_switcher_widget_border_color
|
||||
or "#ffffff"
|
||||
local clients_spacing = beautiful.window_switcher_clients_spacing or dpi(20)
|
||||
local client_icon_horizontal_spacing = beautiful.window_switcher_client_icon_horizontal_spacing
|
||||
or dpi(5)
|
||||
local client_width = beautiful.window_switcher_client_width
|
||||
or dpi(type == "thumbnail" and 150 or 500)
|
||||
local client_height = beautiful.window_switcher_client_height
|
||||
or dpi(type == "thumbnail" and 250 or 50)
|
||||
local client_margins = beautiful.window_switcher_client_margins or dpi(10)
|
||||
local thumbnail_margins = beautiful.window_switcher_thumbnail_margins
|
||||
or dpi(5)
|
||||
local thumbnail_scale = beautiful.thumbnail_scale or false
|
||||
local name_margins = beautiful.window_switcher_name_margins or dpi(10)
|
||||
local name_valign = beautiful.window_switcher_name_valign or "center"
|
||||
local name_forced_width = beautiful.window_switcher_name_forced_width
|
||||
or dpi(type == "thumbnail" and 200 or 550)
|
||||
local name_font = beautiful.window_switcher_name_font or beautiful.font
|
||||
local name_normal_color = beautiful.window_switcher_name_normal_color
|
||||
or "#FFFFFF"
|
||||
local name_focus_color = beautiful.window_switcher_name_focus_color
|
||||
or "#FF0000"
|
||||
local icon_valign = beautiful.window_switcher_icon_valign or "center"
|
||||
local icon_width = beautiful.window_switcher_icon_width or dpi(40)
|
||||
|
||||
local hide_window_switcher_key = opts.hide_window_switcher_key or "Escape"
|
||||
|
||||
local select_client_key = opts.select_client_key or 1
|
||||
local minimize_key = opts.minimize_key or "n"
|
||||
local unminimize_key = opts.unminimize_key or "N"
|
||||
local kill_client_key = opts.kill_client_key or "q"
|
||||
|
||||
local cycle_key = opts.cycle_key or "Tab"
|
||||
|
||||
local previous_key = opts.previous_key or "Left"
|
||||
local next_key = opts.next_key or "Right"
|
||||
|
||||
local vim_previous_key = opts.vim_previous_key or "h"
|
||||
local vim_next_key = opts.vim_next_key or "l"
|
||||
|
||||
local scroll_previous_key = opts.scroll_previous_key or 4
|
||||
local scroll_next_key = opts.scroll_next_key or 5
|
||||
|
||||
local window_switcher_box = awful.popup({
|
||||
bg = "#00000000",
|
||||
visible = false,
|
||||
ontop = true,
|
||||
placement = awful.placement.centered,
|
||||
screen = awful.screen.focused(),
|
||||
widget = wibox.container.background, -- A dummy widget to make awful.popup not scream
|
||||
widget = {
|
||||
{
|
||||
draw_widget(),
|
||||
margins = client_margins,
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
shape_border_width = border_width,
|
||||
shape_border_color = border_color,
|
||||
bg = background,
|
||||
shape = helpers.shape.rrect(border_radius),
|
||||
widget = wibox.container.background,
|
||||
},
|
||||
})
|
||||
|
||||
local mouse_keys = gears.table.join(
|
||||
awful.button({
|
||||
modifiers = { "Any" },
|
||||
button = select_client_key,
|
||||
on_press = function(c)
|
||||
client.focus = c
|
||||
end,
|
||||
}),
|
||||
|
||||
awful.button({
|
||||
modifiers = { "Any" },
|
||||
button = scroll_previous_key,
|
||||
on_press = function()
|
||||
awful.client.focus.byidx(-1)
|
||||
end,
|
||||
}),
|
||||
|
||||
awful.button({
|
||||
modifiers = { "Any" },
|
||||
button = scroll_next_key,
|
||||
on_press = function()
|
||||
awful.client.focus.byidx(1)
|
||||
end,
|
||||
})
|
||||
)
|
||||
|
||||
local keyboard_keys = {
|
||||
[hide_window_switcher_key] = function()
|
||||
window_switcher_hide(window_switcher_box)
|
||||
end,
|
||||
|
||||
[minimize_key] = function()
|
||||
if client.focus then
|
||||
client.focus.minimized = true
|
||||
end
|
||||
end,
|
||||
[unminimize_key] = function()
|
||||
if awful.client.restore() then
|
||||
client.focus = awful.client.restore()
|
||||
end
|
||||
end,
|
||||
[kill_client_key] = function()
|
||||
if client.focus then
|
||||
client.focus:kill()
|
||||
end
|
||||
end,
|
||||
|
||||
[cycle_key] = function()
|
||||
awful.client.focus.byidx(1)
|
||||
end,
|
||||
|
||||
[previous_key] = function()
|
||||
awful.client.focus.byidx(1)
|
||||
end,
|
||||
[next_key] = function()
|
||||
awful.client.focus.byidx(-1)
|
||||
end,
|
||||
|
||||
[vim_previous_key] = function()
|
||||
awful.client.focus.byidx(1)
|
||||
end,
|
||||
[vim_next_key] = function()
|
||||
awful.client.focus.byidx(-1)
|
||||
end,
|
||||
}
|
||||
|
||||
window_switcher_box:connect_signal("property::width", function()
|
||||
if window_switcher_box.visible and get_num_clients() == 0 then
|
||||
window_switcher_hide(window_switcher_box)
|
||||
end
|
||||
end)
|
||||
|
||||
window_switcher_box:connect_signal("property::height", function()
|
||||
if window_switcher_box.visible and get_num_clients() == 0 then
|
||||
window_switcher_hide(window_switcher_box)
|
||||
end
|
||||
end)
|
||||
|
||||
awesome.connect_signal("bling::window_switcher::turn_on", function()
|
||||
local number_of_clients = get_num_clients()
|
||||
if number_of_clients == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
-- Store client that is focused in a variable
|
||||
window_switcher_first_client = client.focus
|
||||
|
||||
-- Stop recording focus history
|
||||
awful.client.focus.history.disable_tracking()
|
||||
|
||||
-- Go to previously focused client (in the tag)
|
||||
awful.client.focus.history.previous()
|
||||
|
||||
-- Track minimized clients
|
||||
-- Unminimize them
|
||||
-- Lower them so that they are always below other
|
||||
-- originally unminimized windows
|
||||
local clients = awful.screen.focused().selected_tag:clients()
|
||||
for _, c in pairs(clients) do
|
||||
if c.minimized then
|
||||
table.insert(window_switcher_minimized_clients, c)
|
||||
c.minimized = false
|
||||
c:lower()
|
||||
end
|
||||
end
|
||||
|
||||
-- Start the keygrabber
|
||||
window_switcher_grabber = awful.keygrabber.run(function(_, key, event)
|
||||
if event == "release" then
|
||||
-- Hide if the modifier was released
|
||||
-- We try to match Super or Alt or Control since we do not know which keybind is
|
||||
-- used to activate the window switcher (the keybind is set by the user in keys.lua)
|
||||
if
|
||||
key:match("Super")
|
||||
or key:match("Alt")
|
||||
or key:match("Control")
|
||||
then
|
||||
window_switcher_hide(window_switcher_box)
|
||||
end
|
||||
-- Do nothing
|
||||
return
|
||||
end
|
||||
|
||||
-- Run function attached to key, if it exists
|
||||
if keyboard_keys[key] then
|
||||
keyboard_keys[key]()
|
||||
end
|
||||
end)
|
||||
|
||||
window_switcher_box.widget = draw_widget(
|
||||
type,
|
||||
background,
|
||||
border_width,
|
||||
border_radius,
|
||||
border_color,
|
||||
clients_spacing,
|
||||
client_icon_horizontal_spacing,
|
||||
client_width,
|
||||
client_height,
|
||||
client_margins,
|
||||
thumbnail_margins,
|
||||
thumbnail_scale,
|
||||
name_margins,
|
||||
name_valign,
|
||||
name_forced_width,
|
||||
name_font,
|
||||
name_normal_color,
|
||||
name_focus_color,
|
||||
icon_valign,
|
||||
icon_width,
|
||||
mouse_keys
|
||||
)
|
||||
window_switcher_box.visible = true
|
||||
end)
|
||||
end
|
||||
|
||||
return { enable = enable }
|
13
config/awesome/module/layout-machi/LICENSE
Normal file
|
@ -0,0 +1,13 @@
|
|||
Copyright 2019 Xinhao Yuan
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
351
config/awesome/module/layout-machi/README.md
Normal file
|
@ -0,0 +1,351 @@
|
|||
# ![machi icon](icon.png) layout-machi
|
||||
|
||||
A manual layout for Awesome with a rapid interactive editor.
|
||||
|
||||
Demos: https://imgur.com/a/OlM60iw
|
||||
|
||||
Draft mode: https://imgur.com/a/BOvMeQL
|
||||
|
||||
__`ng` is merged into `master`. Checkout `legacy` tag for the previous master checkpoint.__
|
||||
|
||||
## Machi-ng
|
||||
|
||||
Machi-ng is a refactoring effort of machi with new features and enhancements.
|
||||
|
||||
### Breaking changes
|
||||
|
||||
1. Added a max split (before merging) of 1,000 for all commands and a global cap of 10,000 areas.
|
||||
2. `t` command now applies to the current area and its further splits, instead of globally.
|
||||
3. `s` command now shifts inside the last group of pending areas that have the same parent, instead of all pending areas.
|
||||
4. There is no more per-layout setting of "draft mode". Every window has its own setting.
|
||||
|
||||
### New features & enhancements
|
||||
|
||||
1. Areas are protected by a minimum size (not configurable for now).
|
||||
2. More tolerating "safer" error handling. If the screen cannot fit the minimum size of the layout, areas out of the screen will be hidden, but it will not crash the layout logic.
|
||||
3. Dynamic size adjustment with propagation.
|
||||
4. Editor can be used on areas instead of entire screens.
|
||||
|
||||
## Why?
|
||||
|
||||
TL;DR --- To bring back the control of the window layout.
|
||||
|
||||
1. Dynamic tiling can be an overkill, since tiling is only useful for persistent windows, and people extensively use hibernate/sleep these days.
|
||||
2. Having window moving around can be annoying whenever a new window shows up.
|
||||
3. I want a flexible layout such that I can quickly adjust to whatever I need.
|
||||
|
||||
## Compatibilities
|
||||
|
||||
I developed it with Awesome git version. Hopefully it works with 4.3 stable.
|
||||
Please let me know if it does not work in 4.3 or older versions.
|
||||
|
||||
## Really quick usage
|
||||
|
||||
See `rc.patch` for adding layout-machi to the default 4.3 config.
|
||||
|
||||
## Quick usage
|
||||
|
||||
Suppose this git is checked out at `~/.config/awesome/layout-machi`
|
||||
|
||||
Use `local machi = require("layout-machi")` to load the module.
|
||||
|
||||
The package provide a default layout `machi.default_layout` and editor `machi.default_editor`, which can be added into the layout list.
|
||||
|
||||
The package comes with the icon for `layoutbox`, which can be set with the following statement (after a theme has been loaded):
|
||||
|
||||
`require("beautiful").layout_machi = machi.get_icon()`
|
||||
|
||||
By default, any machi layout will use the layout command from `machi.layout.default_cmd`, which is initialized as `w66.` (see interpretation below).
|
||||
You can change it after loading the module.
|
||||
|
||||
## Use the layout
|
||||
|
||||
Use `local layout = machi.layout.create(args)` to instantiate the layout with an editor object. `args` is a table of arguments, where the followings can be used:
|
||||
|
||||
- `name`: the constant name of the layout.
|
||||
- `name_func`: a `function(t)` closure that returns a string for tag `t`. `name_func` overrides `name`.
|
||||
- `icon_name`: the "system" name used by Awesome to find the icon. The default value is `machi`.
|
||||
- `persistent`: whether to keep a history of the command for the layout. The default is `true`.
|
||||
- `default_cmd`: the command to use if there is no persistent history for this layout.
|
||||
- `editor`: the editor used for the layout. The default is `machi.default_editor` (or `machi.editor.default_editor`).
|
||||
- `new_placement_cb`: a callback `function(c, instance, areas, geometry)` that fits new client `c` into the areas.
|
||||
This is a new and experimental feature. The interface is subject to changes.
|
||||
|
||||
If `name` and `name_func` are both nil, a default name function will be used, which depends on the tag names, screen geometries, and `icon_name`.
|
||||
|
||||
The function is compatible with the previous `machi.layout.create(name, editor, default_cmd)` calls.
|
||||
|
||||
For `new_placement_cb` the arguments are:
|
||||
- `c`: the new client to be placed.
|
||||
- `instance`: a layout and tag depedent table with the following fields available:
|
||||
- `cmd`: the current layout command.
|
||||
- `client_data`: a mapping from previously managed clients to their layout related settings and assigned areas.
|
||||
Note that it may contain some clients that are no longer in the layout. You can filter them using `screen.tiled_clients`.
|
||||
Each entry is a table with fields:
|
||||
- `.placement`: If true, the client has been placed by the layout, otherwise `new_placement_cb` will be called on the client in the further.
|
||||
- `.area`: If it is non-nil, the window is fit in the area.
|
||||
- `.lu`, `.rd`: If those are non-nil, the window is in draft mode and the fields are for the areas of its corners.
|
||||
- `.draft`: if non-nil, this is the overriding perference of draft mode for the window.
|
||||
- `tag_data`: a mapping from area ids to their fake tag data. This is for nested layouts.
|
||||
- `areas`: the current array of areas produced by `instance.cmd`. Each area is a table with the following fields available:
|
||||
- `id`: self index of the array.
|
||||
- `habitable`: if true, the area is for placing windows. It could be false for a parent area, or an area disabled by command `/`.
|
||||
- `x`, `y`, `width`, `height`: area geometry.
|
||||
- `layout`: the string used to index the nested layout, if any.
|
||||
- `geometry`: the output table the client geometry. Note that the geometry _includes_ the borders.
|
||||
|
||||
The callback places the new client by changing its geometry and client data.
|
||||
Note that after the callback machi will validate the geometry and fit into the areas.
|
||||
So no need to set the `.area`, `.lu`, or `.rd` field of the client data in the callback.
|
||||
For example, to place new client in the largest area among empty areas, create the layout with
|
||||
```
|
||||
machi.layout.create{ new_placement_cb = machi.layout.placement.empty }
|
||||
```
|
||||
|
||||
## The layout editor and commands
|
||||
|
||||
### Starting editor in lua
|
||||
|
||||
Call `local editor = machi.editor.create()` to create an editor.
|
||||
To edit the current machi layout on screen `s`, call `editor.start_interactive(s)`.
|
||||
Calling it with no arguments would be the same as `editor.start_interactive(awful.screen.focused())`.
|
||||
|
||||
### Basic usage
|
||||
|
||||
The editing command starts with the open area of the entire workarea, perform "operations" to split the current area into multiple sub-areas, then recursively edits each of them (by default, the maximum split depth is 2).
|
||||
The layout is defined by a sequence of operations as a layout command.
|
||||
The layout editor allows users to interactively input their commands and shows the resulting layouts on screen, with the following auxiliary functions:
|
||||
|
||||
1. `Up`/`Down`: restore to the history command
|
||||
2. `Backspace`: undo the last command. If `Shift` is hold, restores to the current (maybe transcoded) command of the layout.
|
||||
3. `Escape`: exit the editor without saving the layout.
|
||||
4. `Enter`: when all areas are defined, hit enter will save the layout. If `Shift` is hold, only applies the command without saving it to the history.
|
||||
|
||||
### Layout command
|
||||
|
||||
As aforementioned, command a sequence of operations.
|
||||
There are three kinds of operations:
|
||||
|
||||
1. Operations taking argument string and parsed as multiple numbers.
|
||||
|
||||
- `h`: horizontally split. Splits to two areas evenly without args.
|
||||
- `v`: vertically split. Splits to two areas evenly without args.
|
||||
- `w`: grid split. No splits without args.
|
||||
- `d`: draft split. No splits without args.
|
||||
|
||||
2. Operations taking argument string as a single number or string.
|
||||
|
||||
- `s`: shift open areas within the same parent. Shifts one area without args.
|
||||
- `c`: finish the open areas within the same parent. Finishes all areas with the same parent without args.
|
||||
- `t`: set the number of further split of the curret area. Sets to the default (2) splits without args.
|
||||
- `x`: set the nested layout of the current area. Behaves like `-` without args.
|
||||
|
||||
3. Operation not taking argument.
|
||||
|
||||
- `.`: finish all areas.
|
||||
- `-`: finish the current area
|
||||
- `/`: remove the current area
|
||||
- `;`: no-op
|
||||
|
||||
Argument strings are composed of numbers and `,`. If the string contains `,`, it will be used to split argument into multiple numbers.
|
||||
Otherwise, each digit in the string will be treated as a separated number in type 1 ops.
|
||||
|
||||
Each operation may take argument string either from before (such as `22w`) or after (such as `w22`).
|
||||
When any ambiguity arises, operation before always take the argument after. So `h11v` is interpreted as `h11` and `v`.
|
||||
|
||||
For examples:
|
||||
|
||||
`h-v`
|
||||
|
||||
```
|
||||
11 22
|
||||
11 22
|
||||
11
|
||||
11 33
|
||||
11 33
|
||||
```
|
||||
|
||||
|
||||
`hvv` (or `22w`)
|
||||
|
||||
```
|
||||
11 33
|
||||
11 33
|
||||
|
||||
22 44
|
||||
22 44
|
||||
```
|
||||
|
||||
|
||||
`131h2v-12v`
|
||||
|
||||
Details:
|
||||
|
||||
- `131h`: horizontally split the initial area (entire desktop) to the ratio of 1:3:1
|
||||
- For the first `1` part:
|
||||
- `2v`: vertically split the area to the ratio of 2:1
|
||||
- `-`: skip the editing of the middle `3` part
|
||||
- For the right `1` part:
|
||||
- `12v`: split the right part vertically to the ratio of 1:2
|
||||
|
||||
Tada!
|
||||
|
||||
```
|
||||
11 3333 44
|
||||
11 3333 44
|
||||
11 3333
|
||||
11 3333 55
|
||||
3333 55
|
||||
22 3333 55
|
||||
22 3333 55
|
||||
```
|
||||
|
||||
|
||||
`12210121d`
|
||||
|
||||
```
|
||||
11 2222 3333 44
|
||||
11 2222 3333 44
|
||||
|
||||
55 6666 7777 88
|
||||
55 6666 7777 88
|
||||
55 6666 7777 88
|
||||
55 6666 7777 88
|
||||
|
||||
99 AAAA BBBB CC
|
||||
99 AAAA BBBB CC
|
||||
```
|
||||
|
||||
### Advanced grid layout
|
||||
|
||||
__More document coming soon. For now there is only a running example.__
|
||||
|
||||
Simple grid, `w44`:
|
||||
```
|
||||
0 1 2 3
|
||||
|
||||
4 5 6 7
|
||||
|
||||
8 9 A B
|
||||
|
||||
C D E F
|
||||
```
|
||||
|
||||
Merge grid from the top-left corner, size 3x1, `w4431`:
|
||||
```
|
||||
0-0-0 1
|
||||
|
||||
2 3 4 5
|
||||
|
||||
6 7 8 9
|
||||
|
||||
A B C D
|
||||
```
|
||||
|
||||
Another merge, size 1x3, `w443113`:
|
||||
```
|
||||
0-0-0 1
|
||||
|
|
||||
2 3 4 1
|
||||
|
|
||||
5 6 7 1
|
||||
|
||||
8 9 A B
|
||||
```
|
||||
|
||||
Another merge, size 1x3, `w44311313`:
|
||||
```
|
||||
0-0-0 1
|
||||
|
|
||||
2 3 4 1
|
||||
| |
|
||||
2 5 6 1
|
||||
|
|
||||
2 7 8 9
|
||||
```
|
||||
|
||||
Another merge, size 2x2, `w4431131322`:
|
||||
```
|
||||
0-0-0 1
|
||||
|
|
||||
2 3-3 1
|
||||
| | | |
|
||||
2 3-3 1
|
||||
|
|
||||
2 4 5 6
|
||||
```
|
||||
|
||||
Final merge, size 3x1, `w443113132231`:
|
||||
```
|
||||
0-0-0 1
|
||||
|
|
||||
2 3-3 1
|
||||
| | | |
|
||||
2 3-3 1
|
||||
|
|
||||
2 4-4-4
|
||||
```
|
||||
|
||||
`d` command works similarly after the inital grid is defined, such as `d1221012210221212121222`.
|
||||
|
||||
### Draft mode
|
||||
|
||||
Unlike the regular placement, where a window fits in a single area, windows in draft mode can span across multiple areas.
|
||||
Each drafting window is associated with a upper-left area (UL) and a bottom-right area (BR).
|
||||
The geometry of the window is from the upper-left corner of the UL to the bottom-right corner of the BR.
|
||||
|
||||
Draft mode is suppose to work well with grid areas (produced by `d` or `w` operations), but it is not limited to those.
|
||||
Draft mode is enabled for a newly placed window if
|
||||
(a) `new_placement_cb` returns so, or
|
||||
(b) `new_placement_cb` is unspecified and the window's UL and BR corners fit different areas.
|
||||
Resizing a window to a single area disables drafting, otherwise resizing across areas enables drafting.
|
||||
You can also use `f` or `.` key in switcher UI to manually cycle through modes despit how the window previously spans areas.
|
||||
|
||||
### Nested layouts
|
||||
|
||||
__This feature is a toy. It may come with performance and usability issues - you have been warned.__
|
||||
|
||||
Known caveats include:
|
||||
|
||||
1. `arrange()` of the nested layouts are always called when the machi `arrange()` is called. This could be optimized with caching.
|
||||
2. `client.*wfact` and other layout related operations don't work as machi fakes tag data to the nested layout engine.
|
||||
But it hopefully works if one changes the fields in the faked tag data.
|
||||
|
||||
__This feature is not available for windows in draft mode.__
|
||||
|
||||
To set up nested layouts, you first need to check/modify `machi.editor.nested_layouts` array, which maps an argument string (`[0-9,]+`) to a layout object.
|
||||
In machi command, use the argument string with command `x` will set up the nested layout of the area to the mapped one.
|
||||
|
||||
For example, since by default `machi.editor.nested_layouts["0"]` is `awful.layout.suit.tile` and `machi.editor.nested_layouts["1"]` is `awful.layout.suit.spiral`,
|
||||
the command `11h0x1x` will split the screen horizontally and apply the layouts accordingly - see the figure below.
|
||||
|
||||
![nested layout screenshot](nested_layout_screenshot.png)
|
||||
|
||||
### Persistent history
|
||||
|
||||
By default, the last 100 command sequences are stored in `.cache/awesome/history_machi`.
|
||||
To change that, please refer to `editor.lua`. (XXX more documents)
|
||||
|
||||
## Switcher
|
||||
|
||||
Calling `machi.switcher.start()` will create a switcher supporting the following keys:
|
||||
|
||||
- Arrow keys: move focus into other areas by the direction.
|
||||
- `Shift` + arrow keys: move the focused window to other areas by the direction. In draft mode, move the window while preserving its size.
|
||||
- `Control`[ + `Shift`] + arrow keys: move the bottom-right (or top-left window if `Shift` is pressed) area of the focused window by direction. Only works in draft mode.
|
||||
- `Tab`: switch beteen windows covering the current areas.
|
||||
- `q` or `PageUp` (`Prior`): select the parent of the current area. Hold `Control` to resize the current window accordingly.
|
||||
- `e` or `PageDown` (`Next`): select the previous child of the current area, if `q` or `PageUp` was used. Hold `Control` to resize the current window accordingly.
|
||||
- `f` or `.`: toggle the per-window setting of draft mode.
|
||||
- `/`: open the editor to edit the selected area using the same command interpretation.
|
||||
Note the final command may be transcoded to be embeddable, but the areas shall be the same.
|
||||
|
||||
So far, the key binding is not configurable. One has to modify the source code to change it.
|
||||
|
||||
## Caveats
|
||||
|
||||
A compositor (e.g. picom, compton, xcompmgr) is required. Otherwise switcher and editor will block the clients.
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0 --- See LICENSE
|
569
config/awesome/module/layout-machi/editor.lua
Normal file
|
@ -0,0 +1,569 @@
|
|||
local this_package = ... and (...):match("(.-)[^%.]+$") or ""
|
||||
local machi_engine = require(this_package.."engine")
|
||||
local beautiful = require("beautiful")
|
||||
local awful = require("awful")
|
||||
local wibox = require("wibox")
|
||||
local naughty = require("naughty")
|
||||
local gears = require("gears")
|
||||
local gfs = require("gears.filesystem")
|
||||
local lgi = require("lgi")
|
||||
local dpi = require("beautiful.xresources").apply_dpi
|
||||
|
||||
local ERROR = 2
|
||||
local WARNING = 1
|
||||
local INFO = 0
|
||||
local DEBUG = -1
|
||||
|
||||
local module = {
|
||||
log_level = WARNING,
|
||||
nested_layouts = {
|
||||
["0"] = awful.layout.suit.tile,
|
||||
["1"] = awful.layout.suit.spiral,
|
||||
["2"] = awful.layout.suit.fair,
|
||||
["3"] = awful.layout.suit.fair.horizontal,
|
||||
},
|
||||
}
|
||||
|
||||
local function log(level, msg)
|
||||
if level > module.log_level then
|
||||
print(msg)
|
||||
end
|
||||
end
|
||||
|
||||
local function with_alpha(col, alpha)
|
||||
local r, g, b
|
||||
_, r, g, b, _ = col:get_rgba()
|
||||
return lgi.cairo.SolidPattern.create_rgba(r, g, b, alpha)
|
||||
end
|
||||
|
||||
local function max(a, b)
|
||||
if a < b then return b else return a end
|
||||
end
|
||||
|
||||
local function is_tiling(c)
|
||||
return
|
||||
not (c.tomb_floating or c.floating or c.maximized_horizontal or c.maximized_vertical or c.maximized or c.fullscreen)
|
||||
end
|
||||
|
||||
local function set_tiling(c)
|
||||
c.floating = false
|
||||
c.maximized = false
|
||||
c.maximized_vertical = false
|
||||
c.maximized_horizontal = false
|
||||
c.fullscreen = false
|
||||
end
|
||||
|
||||
local function _area_tostring(wa)
|
||||
return "{x:" .. tostring(wa.x) .. ",y:" .. tostring(wa.y) .. ",w:" .. tostring(wa.width) .. ",h:" .. tostring(wa.height) .. "}"
|
||||
end
|
||||
|
||||
local function shrink_area_with_gap(a, gap)
|
||||
return {
|
||||
x = a.x + gap,
|
||||
y = a.y + gap,
|
||||
width = a.width - gap * 2,
|
||||
height = a.height - gap * 2,
|
||||
}
|
||||
end
|
||||
|
||||
function module.restore_data(data)
|
||||
if data.history_file then
|
||||
local file, err = io.open(data.history_file, "r")
|
||||
if err then
|
||||
log(INFO, "cannot read history from " .. data.history_file)
|
||||
else
|
||||
data.cmds = {}
|
||||
data.last_cmd = {}
|
||||
local last_layout_name
|
||||
for line in file:lines() do
|
||||
if line:sub(1, 1) == "+" then
|
||||
last_layout_name = line:sub(2, #line)
|
||||
else
|
||||
if last_layout_name ~= nil then
|
||||
log(DEBUG, "restore last cmd " .. line .. " for " .. last_layout_name)
|
||||
data.last_cmd[last_layout_name] = line
|
||||
last_layout_name = nil
|
||||
else
|
||||
log(DEBUG, "restore cmd " .. line)
|
||||
data.cmds[#data.cmds + 1] = line
|
||||
end
|
||||
end
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
function module.create(data)
|
||||
if data == nil then
|
||||
data = module.restore_data({
|
||||
history_file = gfs.get_cache_dir() .. "/history_machi",
|
||||
history_save_max = 100,
|
||||
})
|
||||
end
|
||||
|
||||
data.cmds = data.cmds or {}
|
||||
data.last_cmd = data.last_cmd or {}
|
||||
data.minimum_size = data.minimum_size or 100
|
||||
|
||||
|
||||
local function add_cmd(instance_name, cmd)
|
||||
-- remove duplicated entries
|
||||
local j = 1
|
||||
for i = 1, #data.cmds do
|
||||
if data.cmds[i] ~= cmd then
|
||||
data.cmds[j] = data.cmds[i]
|
||||
j = j + 1
|
||||
end
|
||||
end
|
||||
for i = #data.cmds, j, -1 do
|
||||
table.remove(data.cmds, i)
|
||||
end
|
||||
|
||||
data.cmds[#data.cmds + 1] = cmd
|
||||
data.last_cmd[instance_name] = cmd
|
||||
if data.history_file then
|
||||
local file, err = io.open(data.history_file, "w")
|
||||
if err then
|
||||
log(ERROR, "cannot save history to " .. data.history_file)
|
||||
else
|
||||
for i = max(1, #data.cmds - data.history_save_max + 1), #data.cmds do
|
||||
log(DEBUG, "save cmd " .. data.cmds[i])
|
||||
file:write(data.cmds[i] .. "\n")
|
||||
end
|
||||
for name, cmd in pairs(data.last_cmd) do
|
||||
log(DEBUG, "save last cmd " .. cmd .. " for " .. name)
|
||||
file:write("+" .. name .. "\n" .. cmd .. "\n")
|
||||
end
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
local function start_interactive(screen, embed_args)
|
||||
local info_size = dpi(60)
|
||||
-- colors are in rgba
|
||||
local border_color = with_alpha(
|
||||
gears.color(beautiful.machi_editor_border_color or beautiful.border_focus),
|
||||
beautiful.machi_editor_border_opacity or 0.75)
|
||||
local active_color = with_alpha(
|
||||
gears.color(beautiful.machi_editor_active_color or beautiful.bg_focus),
|
||||
beautiful.machi_editor_active_opacity or 0.5)
|
||||
local open_color = with_alpha(
|
||||
gears.color(beautiful.machi_editor_open_color or beautiful.bg_normal),
|
||||
beautiful.machi_editor_open_opacity or 0.5)
|
||||
local closed_color = open_color
|
||||
|
||||
if to_save == nil then
|
||||
to_save = true
|
||||
end
|
||||
|
||||
screen = screen or awful.screen.focused()
|
||||
local tag = screen.selected_tag
|
||||
local gap = tag.gap or 0
|
||||
local layout = tag.layout
|
||||
|
||||
if layout.machi_set_cmd == nil then
|
||||
naughty.notify({
|
||||
text = "The layout to edit is not machi",
|
||||
timeout = 3,
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
local cmd_index = #data.cmds + 1
|
||||
data.cmds[cmd_index] = ""
|
||||
|
||||
local start_x = screen.workarea.x
|
||||
local start_y = screen.workarea.y
|
||||
|
||||
local kg
|
||||
local infobox = wibox({
|
||||
screen = screen,
|
||||
x = screen.workarea.x,
|
||||
y = screen.workarea.y,
|
||||
width = screen.workarea.width,
|
||||
height = screen.workarea.height,
|
||||
bg = "#ffffff00",
|
||||
opacity = 1,
|
||||
ontop = true,
|
||||
type = "dock",
|
||||
})
|
||||
infobox.visible = true
|
||||
|
||||
workarea = embed_args and embed_args.workarea or screen.workarea
|
||||
|
||||
local closed_areas
|
||||
local open_areas
|
||||
local pending_op
|
||||
local current_cmd
|
||||
local to_exit
|
||||
local to_apply
|
||||
|
||||
local key_translate_tab = {
|
||||
["Return"] = ".",
|
||||
[" "] = "-",
|
||||
}
|
||||
|
||||
local function set_cmd(cmd)
|
||||
local new_closed_areas, new_open_areas, new_pending_op = machi_engine.areas_from_command(
|
||||
cmd,
|
||||
{
|
||||
x = workarea.x + gap,
|
||||
y = workarea.y + gap,
|
||||
width = workarea.width - gap * 2,
|
||||
height = workarea.height - gap * 2
|
||||
},
|
||||
gap * 2 + data.minimum_size)
|
||||
if new_closed_areas then
|
||||
closed_areas, open_areas, pending_op =
|
||||
new_closed_areas, new_open_areas, new_pending_op
|
||||
current_cmd = cmd
|
||||
|
||||
if embed_args then
|
||||
current_info =
|
||||
embed_args.cmd_prefix.."["..current_cmd.."]"..embed_args.cmd_suffix
|
||||
else
|
||||
current_info = cmd
|
||||
end
|
||||
|
||||
if #open_areas == 0 and not pending_op then
|
||||
current_info = current_info .. "\n(enter to apply)"
|
||||
end
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_key(key)
|
||||
if key_translate_tab[key] ~= nil then
|
||||
key = key_translate_tab[key]
|
||||
end
|
||||
|
||||
return set_cmd(current_cmd..key)
|
||||
end
|
||||
|
||||
|
||||
local function cleanup()
|
||||
infobox.visible = false
|
||||
end
|
||||
|
||||
local function draw_info(context, cr, width, height)
|
||||
cr:set_source_rgba(0, 0, 0, 0)
|
||||
cr:rectangle(0, 0, width, height)
|
||||
cr:fill()
|
||||
|
||||
local msg, ext
|
||||
|
||||
for i, a in ipairs(closed_areas) do
|
||||
if a.habitable then
|
||||
local sa = shrink_area_with_gap(a, gap)
|
||||
local to_highlight = false
|
||||
if pending_op ~= nil then
|
||||
to_highlight = a.group_id == op_count
|
||||
end
|
||||
cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height)
|
||||
cr:clip()
|
||||
if to_highlight then
|
||||
cr:set_source(active_color)
|
||||
else
|
||||
cr:set_source(closed_color)
|
||||
end
|
||||
cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height)
|
||||
cr:fill()
|
||||
cr:set_source(border_color)
|
||||
cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height)
|
||||
cr:set_line_width(10.0)
|
||||
cr:stroke()
|
||||
cr:reset_clip()
|
||||
end
|
||||
end
|
||||
|
||||
for i, a in ipairs(open_areas) do
|
||||
local sa = shrink_area_with_gap(a, gap)
|
||||
local to_highlight = false
|
||||
if not pending_op then
|
||||
to_highlight = i == #open_areas
|
||||
else
|
||||
to_highlight = a.group_id == op_count
|
||||
end
|
||||
cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height)
|
||||
cr:clip()
|
||||
if i == #open_areas then
|
||||
cr:set_source(active_color)
|
||||
else
|
||||
cr:set_source(open_color)
|
||||
end
|
||||
cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height)
|
||||
cr:fill()
|
||||
|
||||
cr:set_source(border_color)
|
||||
cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height)
|
||||
cr:set_line_width(10.0)
|
||||
if to_highlight then
|
||||
cr:stroke()
|
||||
else
|
||||
cr:set_dash({5, 5}, 0)
|
||||
cr:stroke()
|
||||
cr:set_dash({}, 0)
|
||||
end
|
||||
cr:reset_clip()
|
||||
end
|
||||
|
||||
local pl = lgi.Pango.Layout.create(cr)
|
||||
pl:set_font_description(beautiful.get_merged_font(beautiful.font, info_size))
|
||||
pl:set_alignment("CENTER")
|
||||
pl:set_text(current_info)
|
||||
local w, h = pl:get_size()
|
||||
w = w / lgi.Pango.SCALE
|
||||
h = h / lgi.Pango.SCALE
|
||||
local ext = { width = w, height = h, x_bearing = 0, y_bearing = 0 }
|
||||
cr:move_to(width / 2 - ext.width / 2 - ext.x_bearing, height / 2 - ext.height / 2 - ext.y_bearing)
|
||||
cr:set_source_rgba(1, 1, 1, 1)
|
||||
cr:show_layout(pl)
|
||||
cr:fill()
|
||||
cr:move_to(width / 2 - ext.width / 2 - ext.x_bearing, height / 2 - ext.height / 2 - ext.y_bearing)
|
||||
cr:set_source_rgba(0, 0, 0, 1)
|
||||
cr:set_line_width(2.0)
|
||||
cr:layout_path(pl)
|
||||
cr:stroke()
|
||||
end
|
||||
|
||||
local function refresh()
|
||||
log(DEBUG, "closed areas:")
|
||||
for i, a in ipairs(closed_areas) do
|
||||
log(DEBUG, " " .. _area_tostring(a))
|
||||
end
|
||||
log(DEBUG, "open areas:")
|
||||
for i, a in ipairs(open_areas) do
|
||||
log(DEBUG, " " .. _area_tostring(a))
|
||||
end
|
||||
infobox.bgimage = draw_info
|
||||
end
|
||||
|
||||
local function get_final_cmd()
|
||||
local final_cmd = current_cmd
|
||||
if embed_args then
|
||||
final_cmd = embed_args.cmd_prefix ..
|
||||
machi_engine.areas_to_command(closed_areas, true) ..
|
||||
embed_args.cmd_suffix
|
||||
end
|
||||
return final_cmd
|
||||
end
|
||||
|
||||
log(DEBUG, "interactive layout editing starts")
|
||||
|
||||
set_cmd("")
|
||||
refresh()
|
||||
|
||||
kg = awful.keygrabber.run(
|
||||
function (mod, key, event)
|
||||
if event == "release" then
|
||||
return
|
||||
end
|
||||
|
||||
local ok, err = pcall(
|
||||
function ()
|
||||
if key == "BackSpace" then
|
||||
local alt = false
|
||||
for _, m in ipairs(mod) do
|
||||
if m == "Shift" then
|
||||
alt = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if alt then
|
||||
if embed_args then
|
||||
set_cmd(embed_args.original_cmd or "")
|
||||
else
|
||||
local _cd, _td, areas = layout.machi_get_instance_data(screen, tag)
|
||||
set_cmd(machi_engine.areas_to_command(areas))
|
||||
end
|
||||
else
|
||||
set_cmd(current_cmd:sub(1, #current_cmd - 1))
|
||||
end
|
||||
elseif key == "Escape" then
|
||||
table.remove(data.cmds, #data.cmds)
|
||||
to_exit = true
|
||||
elseif key == "Up" or key == "Down" then
|
||||
if current_cmd ~= data.cmds[cmd_index] then
|
||||
data.cmds[#data.cmds] = current_cmd
|
||||
end
|
||||
|
||||
if key == "Up" and cmd_index > 1 then
|
||||
cmd_index = cmd_index - 1
|
||||
elseif key == "Down" and cmd_index < #data.cmds then
|
||||
cmd_index = cmd_index + 1
|
||||
end
|
||||
|
||||
log(DEBUG, "restore history #" .. tostring(cmd_index) .. ":" .. data.cmds[cmd_index])
|
||||
set_cmd(data.cmds[cmd_index])
|
||||
elseif #open_areas > 0 or pending_op then
|
||||
handle_key(key)
|
||||
else
|
||||
if key == "Return" then
|
||||
local alt = false
|
||||
for _, m in ipairs(mod) do
|
||||
if m == "Shift" then
|
||||
alt = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local instance_name, persistent = layout.machi_get_instance_info(tag)
|
||||
if not alt and persistent then
|
||||
table.remove(data.cmds, #data.cmds)
|
||||
add_cmd(instance_name, get_final_cmd())
|
||||
current_info = "Saved!"
|
||||
else
|
||||
current_info = "Applied!"
|
||||
end
|
||||
|
||||
to_exit = true
|
||||
to_apply = true
|
||||
end
|
||||
end
|
||||
|
||||
refresh()
|
||||
|
||||
if to_exit then
|
||||
log(DEBUG, "interactive layout editing ends")
|
||||
if to_apply then
|
||||
layout.machi_set_cmd(get_final_cmd(), tag)
|
||||
awful.layout.arrange(screen)
|
||||
gears.timer{
|
||||
timeout = 1,
|
||||
autostart = true,
|
||||
singleshot = true,
|
||||
callback = cleanup,
|
||||
}
|
||||
else
|
||||
cleanup()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
log(ERROR, "Getting error in keygrabber: " .. err)
|
||||
to_exit = true
|
||||
cleanup()
|
||||
end
|
||||
|
||||
if to_exit then
|
||||
awful.keygrabber.stop(kg)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
local function run_cmd(cmd, screen, tag)
|
||||
local gap = tag.gap
|
||||
local areas, closed = machi_engine.areas_from_command(
|
||||
cmd,
|
||||
{
|
||||
x = screen.workarea.x + gap,
|
||||
y = screen.workarea.y + gap,
|
||||
width = screen.workarea.width - gap * 2,
|
||||
height = screen.workarea.height - gap * 2
|
||||
},
|
||||
gap * 2 + data.minimum_size)
|
||||
if not areas or #closed > 0 then
|
||||
return nil
|
||||
end
|
||||
for _, a in ipairs(areas) do
|
||||
a.x = a.x + gap
|
||||
a.y = a.y + gap
|
||||
a.width = a.width - gap * 2
|
||||
a.height = a.height - gap * 2
|
||||
end
|
||||
return areas
|
||||
end
|
||||
|
||||
local function get_last_cmd(name)
|
||||
return data.last_cmd[name]
|
||||
end
|
||||
|
||||
function adjust_shares(c, axis, adj)
|
||||
if not c:isvisible() or c.floating or c.immobilized then
|
||||
return
|
||||
end
|
||||
local screen = c.screen
|
||||
local tag = screen.selected_tag
|
||||
local layout = tag.layout
|
||||
if not layout.machi_get_instance_data then return end
|
||||
local cd, _td, areas = layout.machi_get_instance_data(screen, tag)
|
||||
local key_shares = axis.."_shares"
|
||||
local key_spare = axis.."_spare"
|
||||
local key_parent_shares = "parent_"..axis.."_shares"
|
||||
|
||||
if not cd[c] or not cd[c].area then
|
||||
return
|
||||
end
|
||||
|
||||
if adj < 0 then
|
||||
if axis == "x" and c.width + adj < data.minimum_size then
|
||||
adj = data.minimum_size - c.width
|
||||
elseif axis == "y" and c.height + adj < data.minimum_size then
|
||||
adj = data.minimum_size - c.height
|
||||
end
|
||||
end
|
||||
|
||||
local function adjust(parent_id, shares, adj)
|
||||
-- The propagation part is questionable. But it is not critical anyway..
|
||||
if type(shares) ~= "table" then
|
||||
local old = areas[parent_id].split[key_shares][shares][2] or 0
|
||||
areas[parent_id].split[key_shares][shares][2] = old + adj
|
||||
else
|
||||
local acc = 0
|
||||
for i = 1, #shares do
|
||||
local old = areas[parent_id].split[key_shares][shares[i]][2] or 0
|
||||
local adj_split = i == #shares and adj - acc or math.floor(adj * i / #shares - acc + 0.5)
|
||||
areas[parent_id].split[key_shares][shares[i]][2] = old + adj_split
|
||||
acc = acc + adj_split
|
||||
end
|
||||
end
|
||||
if adj <= 0 then
|
||||
return #areas[parent_id].split[key_shares] > 1
|
||||
else
|
||||
return areas[parent_id].split[key_spare] >= adj
|
||||
end
|
||||
end
|
||||
|
||||
local area = cd[c].area
|
||||
while areas[area].parent_id do
|
||||
if adjust(areas[area].parent_id, areas[area][key_parent_shares], adj) then
|
||||
break
|
||||
end
|
||||
area = areas[area].parent_id
|
||||
end
|
||||
|
||||
layout.machi_set_cmd(machi_engine.areas_to_command(areas), tag, true)
|
||||
awful.layout.arrange(screen)
|
||||
end
|
||||
|
||||
function adjust_x_shares(c, adj)
|
||||
adjust_shares(c, "x", adj)
|
||||
end
|
||||
|
||||
function adjust_y_shares(c, adj)
|
||||
adjust_shares(c, "y", adj)
|
||||
end
|
||||
|
||||
return {
|
||||
start_interactive = start_interactive,
|
||||
run_cmd = run_cmd,
|
||||
get_last_cmd = get_last_cmd,
|
||||
adjust_x_shares = adjust_x_shares,
|
||||
adjust_y_shares = adjust_y_shares,
|
||||
}
|
||||
end
|
||||
|
||||
module.default_editor = module.create()
|
||||
|
||||
return module
|
937
config/awesome/module/layout-machi/engine.lua
Normal file
|
@ -0,0 +1,937 @@
|
|||
-- area {
|
||||
-- x, y, width, height
|
||||
-- parent_id
|
||||
-- parent_cid
|
||||
-- parent_x_shares
|
||||
-- parent_y_shares
|
||||
-- habitable
|
||||
-- hole (unique)
|
||||
-- }
|
||||
--
|
||||
-- split {
|
||||
-- method
|
||||
-- x_shares
|
||||
-- y_shares
|
||||
-- children
|
||||
-- }
|
||||
--
|
||||
-- share {weight, adjustment, dynamic, minimum}
|
||||
local in_module = ...
|
||||
|
||||
-- Split a length by `measures`, such that each split respect the
|
||||
-- weight [1], adjustment (user [2] + engine [3]) without breaking the minimum size [4].
|
||||
--
|
||||
-- The split algorithm has a worst case of O(n^2) where n = #shares,
|
||||
-- which should be fine for practical usage of screen partitions.
|
||||
-- Using geometric algorithm this can be optimized to O(n log n), but
|
||||
-- I don't think it is worth.
|
||||
|
||||
-- Returns two values:
|
||||
-- 1. the (accumulative) result if it is possible to give every share its minimum size, otherwise nil.
|
||||
-- 2. any spare space to adjust without capping any share.
|
||||
local function fair_split(length, shares)
|
||||
local ret = {}
|
||||
local normalized_adj = nil
|
||||
local sum_weight
|
||||
local sum_adj
|
||||
local remaining = #shares
|
||||
local spare = nil
|
||||
local need_recompute
|
||||
repeat
|
||||
need_recompute = false
|
||||
sum_weight = 0
|
||||
sum_adj = 0
|
||||
for i = 1, #shares do
|
||||
if ret[i] == nil then
|
||||
sum_weight = sum_weight + shares[i][1]
|
||||
if normalized_adj then
|
||||
sum_adj = sum_adj + normalized_adj[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if normalized_adj == nil then
|
||||
normalized_adj = {}
|
||||
for i = 1, #shares do
|
||||
if sum_weight > shares[i][1] then
|
||||
normalized_adj[i] = ((shares[i][2] or 0) + (shares[i][3] or 0)) * sum_weight / (sum_weight - shares[i][1])
|
||||
else
|
||||
normalized_adj[i] = 0
|
||||
end
|
||||
sum_adj = sum_adj + normalized_adj[i]
|
||||
end
|
||||
|
||||
for i = 1, #shares do
|
||||
local required = (shares[i][4] - normalized_adj[i]) * sum_weight / shares[i][1] + sum_adj
|
||||
if spare == nil or spare > length - required then
|
||||
spare = length - required
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local capped_length = 0
|
||||
for i = 1, #shares do
|
||||
if ret[i] == nil then
|
||||
local split = (length - sum_adj) * shares[i][1] / sum_weight + normalized_adj[i]
|
||||
if split < shares[i][4] then
|
||||
ret[i] = shares[i][4]
|
||||
capped_length = capped_length + shares[i][4]
|
||||
need_recompute = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
length = length - capped_length
|
||||
until not need_recompute
|
||||
|
||||
if #shares == 1 or spare < 0 then
|
||||
spare = 0
|
||||
end
|
||||
|
||||
if remaining == 0 then
|
||||
return nil, spare
|
||||
end
|
||||
|
||||
local acc_weight = 0
|
||||
local acc_adj = 0
|
||||
local acc_ret = 0
|
||||
for i = 1, #shares do
|
||||
if ret[i] == nil then
|
||||
acc_weight = acc_weight + shares[i][1]
|
||||
acc_adj = acc_adj + normalized_adj[i]
|
||||
ret[i] = remaining == 1 and length - acc_ret or math.floor((length - sum_adj) / sum_weight * acc_weight + acc_adj - acc_ret + 0.5)
|
||||
acc_ret = acc_ret + ret[i]
|
||||
remaining = remaining - 1
|
||||
end
|
||||
end
|
||||
|
||||
ret[0] = 0
|
||||
for i = 1, #shares do
|
||||
ret[i] = ret[i - 1] + ret[i]
|
||||
end
|
||||
|
||||
return ret, spare
|
||||
end
|
||||
|
||||
-- Static data
|
||||
|
||||
-- Command character info
|
||||
-- 3 for taking the arg string and an open area
|
||||
-- 2 for taking an open area
|
||||
-- 1 for taking nothing
|
||||
-- 0 for args
|
||||
local ch_info = {
|
||||
["h"] = 3, ["H"] = 3,
|
||||
["v"] = 3, ["V"] = 3,
|
||||
["w"] = 3, ["W"] = 3,
|
||||
["d"] = 3, ["D"] = 3,
|
||||
["s"] = 3,
|
||||
["t"] = 3,
|
||||
["c"] = 3,
|
||||
["x"] = 3,
|
||||
["-"] = 2,
|
||||
["/"] = 2,
|
||||
["."] = 1,
|
||||
[";"] = 1,
|
||||
["0"] = 0, ["1"] = 0, ["2"] = 0, ["3"] = 0, ["4"] = 0,
|
||||
["5"] = 0, ["6"] = 0, ["7"] = 0, ["8"] = 0, ["9"] = 0,
|
||||
["_"] = 0, [","] = 0,
|
||||
}
|
||||
|
||||
local function parse_arg_str(arg_str, default)
|
||||
local ret = {}
|
||||
local current = {}
|
||||
if #arg_str == 0 then return ret end
|
||||
local index = 1
|
||||
local split_mode = arg_str:find("[,_]") ~= nil
|
||||
|
||||
local p = index
|
||||
while index <= #arg_str do
|
||||
local ch = arg_str:sub(index, index)
|
||||
if split_mode then
|
||||
if ch == "_" then
|
||||
local r = tonumber(arg_str:sub(p, index - 1))
|
||||
if r == nil then
|
||||
current[#current + 1] = default
|
||||
else
|
||||
current[#current + 1] = r
|
||||
end
|
||||
p = index + 1
|
||||
elseif ch == "," then
|
||||
local r = tonumber(arg_str:sub(p, index - 1))
|
||||
if r == nil then
|
||||
current[#current + 1] = default
|
||||
else
|
||||
current[#current + 1] = r
|
||||
end
|
||||
ret[#ret + 1] = current
|
||||
current = {}
|
||||
p = index + 1
|
||||
end
|
||||
else
|
||||
local r = tonumber(ch)
|
||||
if r == nil then
|
||||
ret[#ret + 1] = {default}
|
||||
else
|
||||
ret[#ret + 1] = {r}
|
||||
end
|
||||
end
|
||||
index = index + 1
|
||||
end
|
||||
|
||||
if split_mode then
|
||||
local r = tonumber(arg_str:sub(p, index - 1))
|
||||
if r == nil then
|
||||
current[#current + 1] = default
|
||||
else
|
||||
current[#current + 1] = r
|
||||
end
|
||||
ret[#ret + 1] = current
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
if not in_module then
|
||||
print("Testing parse_arg_str")
|
||||
local x = parse_arg_str("1234", 0)
|
||||
assert(#x == 4)
|
||||
assert(#x[1] == 1 and x[1][1] == 1)
|
||||
assert(#x[2] == 1 and x[2][1] == 2)
|
||||
assert(#x[3] == 1 and x[3][1] == 3)
|
||||
assert(#x[4] == 1 and x[4][1] == 4)
|
||||
local x = parse_arg_str("12_34_,", -1)
|
||||
assert(#x == 2)
|
||||
assert(#x[1] == 3 and x[1][1] == 12 and x[1][2] == 34 and x[1][3] == -1)
|
||||
assert(#x[2] == 1 and x[2][1] == -1)
|
||||
local x = parse_arg_str("12_34,56_,78_90_", -1)
|
||||
assert(#x == 3)
|
||||
assert(#x[1] == 2 and x[1][1] == 12 and x[1][2] == 34)
|
||||
assert(#x[2] == 2 and x[2][1] == 56 and x[2][2] == -1)
|
||||
assert(#x[3] == 3 and x[3][1] == 78 and x[3][2] == 90 and x[3][3] == -1)
|
||||
print("Passed.")
|
||||
end
|
||||
|
||||
local max_split = 1000
|
||||
local max_areas = 10000
|
||||
local default_expansion = 2
|
||||
|
||||
-- Execute a (partial) command, returns:
|
||||
-- 1. Closed areas: areas that will not be further partitioned by further input.
|
||||
-- 2. Open areas: areas that can be further partitioned.
|
||||
-- 3. Pending: if the command can take more argument into the last command.
|
||||
local function areas_from_command(command, workarea, minimum)
|
||||
local pending_op = nil
|
||||
local arg_str = ""
|
||||
local closed_areas = {}
|
||||
local open_areas
|
||||
local root = {
|
||||
expansion = default_expansion,
|
||||
x = workarea.x,
|
||||
y = workarea.y,
|
||||
width = workarea.width,
|
||||
height = workarea.height,
|
||||
bl = true,
|
||||
br = true,
|
||||
bu = true,
|
||||
bd = true,
|
||||
}
|
||||
|
||||
local function close_area()
|
||||
local a = open_areas[#open_areas]
|
||||
table.remove(open_areas, #open_areas)
|
||||
local i = #closed_areas + 1
|
||||
closed_areas[i] = a
|
||||
a.id = i
|
||||
a.habitable = true
|
||||
return a, i
|
||||
end
|
||||
|
||||
local function push_open_areas(areas)
|
||||
for i = #areas, 1, -1 do
|
||||
open_areas[#open_areas + 1] = areas[i]
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_op(method)
|
||||
local l = method:lower()
|
||||
local alt = method ~= l
|
||||
method = l
|
||||
|
||||
if method == "h" or method == "v" then
|
||||
|
||||
local args = parse_arg_str(arg_str, 0)
|
||||
if #args == 0 then
|
||||
args = {{1}, {1}}
|
||||
elseif #args == 1 then
|
||||
args[2] = {1}
|
||||
end
|
||||
|
||||
local total = 0
|
||||
local shares = { }
|
||||
for i = 1, #args do
|
||||
local arg
|
||||
if not alt then
|
||||
arg = args[i]
|
||||
else
|
||||
arg = args[#args - i + 1]
|
||||
end
|
||||
if arg[2] == 0 and arg[3] then arg[2], arg[3] = -arg[3], nil end
|
||||
shares[i] = arg
|
||||
end
|
||||
|
||||
if #shares > max_split then
|
||||
return nil
|
||||
end
|
||||
|
||||
local a, area_index = close_area()
|
||||
a.habitable = false
|
||||
a.split = {
|
||||
method = method,
|
||||
x_shares = method == "h" and shares or {{1}},
|
||||
y_shares = method == "v" and shares or {{1}},
|
||||
children = {}
|
||||
}
|
||||
local children = a.split.children
|
||||
|
||||
if method == "h" then
|
||||
for i = 1, #a.split.x_shares do
|
||||
local child = {
|
||||
parent_id = area_index,
|
||||
parent_cid = #children + 1,
|
||||
parent_x_shares = #children + 1,
|
||||
parent_y_shares = 1,
|
||||
expansion = a.expansion - 1,
|
||||
|
||||
bl = i == 1 and a.bl or false,
|
||||
br = i == #a.split.x_shares and a.br or false,
|
||||
bu = a.bu,
|
||||
bd = a.bd,
|
||||
}
|
||||
children[#children + 1] = child
|
||||
end
|
||||
else
|
||||
for i = 1, #a.split.y_shares do
|
||||
local child = {
|
||||
parent_id = area_index,
|
||||
parent_cid = #children + 1,
|
||||
parent_x_shares = 1,
|
||||
parent_y_shares = #children + 1,
|
||||
expansion = a.expansion - 1,
|
||||
|
||||
bl = a.bl,
|
||||
br = a.br,
|
||||
bu = i == 1 and a.bu or false,
|
||||
bd = i == #a.split.y_shares and a.bd or false,
|
||||
}
|
||||
children[#children + 1] = child
|
||||
end
|
||||
end
|
||||
|
||||
push_open_areas(children)
|
||||
|
||||
elseif method == "w" or method == "d" then
|
||||
|
||||
local args = parse_arg_str(arg_str, 0)
|
||||
|
||||
local x_shares = {}
|
||||
local y_shares = {}
|
||||
local m_start = #args + 1
|
||||
|
||||
if method == "w" then
|
||||
if #args == 0 then
|
||||
args = {{1}, {1}}
|
||||
elseif #args == 1 then
|
||||
args[2] = {1}
|
||||
end
|
||||
|
||||
local x_shares_count, y_shares_count
|
||||
if alt then
|
||||
x_shares_count = args[2][1]
|
||||
y_shares_count = args[1][1]
|
||||
else
|
||||
x_shares_count = args[1][1]
|
||||
y_shares_count = args[2][1]
|
||||
end
|
||||
if x_shares_count < 1 then x_shares_count = 1 end
|
||||
if y_shares_count < 1 then y_shares_count = 1 end
|
||||
|
||||
if x_shares_count * y_shares_count > max_split then
|
||||
return nil
|
||||
end
|
||||
|
||||
for i = 1, x_shares_count do x_shares[i] = {1} end
|
||||
for i = 1, y_shares_count do y_shares[i] = {1} end
|
||||
|
||||
m_start = 3
|
||||
else
|
||||
local current = x_shares
|
||||
for i = 1, #args do
|
||||
if not alt then
|
||||
arg = args[i]
|
||||
else
|
||||
arg = args[#args - i + 1]
|
||||
end
|
||||
if arg[1] == 0 then
|
||||
if current == x_shares then current = y_shares else
|
||||
m_start = i + 1
|
||||
break
|
||||
end
|
||||
else
|
||||
if arg[2] == 0 and arg[3] then arg[2], arg[3] = -arg[3], nil end
|
||||
current[#current + 1] = arg
|
||||
end
|
||||
end
|
||||
|
||||
if #x_shares == 0 then
|
||||
x_shares = {{1}}
|
||||
end
|
||||
|
||||
if #y_shares == 0 then
|
||||
y_shares = {{1}}
|
||||
end
|
||||
|
||||
if #x_shares * #y_shares > max_split then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local a, area_index = close_area()
|
||||
a.habitable = false
|
||||
a.split = {
|
||||
method = method,
|
||||
x_shares = x_shares,
|
||||
y_shares = y_shares,
|
||||
children = {},
|
||||
}
|
||||
local children = {}
|
||||
|
||||
for y_index = 1, #a.split.y_shares do
|
||||
for x_index = 1, #a.split.x_shares do
|
||||
local r = {
|
||||
parent_id = area_index,
|
||||
-- parent_cid will be filled later.
|
||||
parent_x_shares = x_index,
|
||||
parent_y_shares = y_index,
|
||||
expansion = a.expansion - 1
|
||||
}
|
||||
if x_index == 1 then r.bl = a.bl else r.bl = false end
|
||||
if x_index == #a.split.x_shares then r.br = a.br else r.br = false end
|
||||
if y_index == 1 then r.bu = a.bu else r.bu = false end
|
||||
if y_index == #a.split.y_shares then r.bd = a.bd else r.bd = false end
|
||||
children[#children + 1] = r
|
||||
end
|
||||
end
|
||||
|
||||
local merged_children = {}
|
||||
local start_index = 1
|
||||
for i = m_start, #args - 1, 2 do
|
||||
-- find the first index that is not merged
|
||||
while start_index <= #children and children[start_index] == false do
|
||||
start_index = start_index + 1
|
||||
end
|
||||
if start_index > #children or children[start_index] == false then
|
||||
break
|
||||
end
|
||||
local x = (start_index - 1) % #x_shares
|
||||
local y = math.floor((start_index - 1) / #x_shares)
|
||||
local w = args[i][1]
|
||||
local h = args[i + 1][1]
|
||||
if w < 1 then w = 1 end
|
||||
if h == nil or h < 1 then h = 1 end
|
||||
if alt then
|
||||
local tmp = w
|
||||
w = h
|
||||
h = tmp
|
||||
end
|
||||
if x + w > #x_shares then w = #x_shares - x end
|
||||
if y + h > #y_shares then h = #y_shares - y end
|
||||
local end_index = start_index
|
||||
for ty = y, y + h - 1 do
|
||||
local succ = true
|
||||
for tx = x, x + w - 1 do
|
||||
if children[ty * #x_shares + tx + 1] == false then
|
||||
succ = false
|
||||
break
|
||||
elseif ty == y then
|
||||
end_index = ty * #x_shares + tx + 1
|
||||
end
|
||||
end
|
||||
|
||||
if not succ then
|
||||
break
|
||||
elseif ty > y then
|
||||
end_index = ty * #x_shares + x + w
|
||||
end
|
||||
end
|
||||
|
||||
local function generate_range(s, e)
|
||||
local r = {} for i = s, e do r[#r+1] = i end return r
|
||||
end
|
||||
|
||||
local r = {
|
||||
bu = children[start_index].bu, bl = children[start_index].bl,
|
||||
bd = children[end_index].bd, br = children[end_index].br,
|
||||
|
||||
parent_id = area_index,
|
||||
-- parent_cid will be filled later.
|
||||
parent_x_shares = generate_range(children[start_index].parent_x_shares, children[end_index].parent_x_shares),
|
||||
parent_y_shares = generate_range(children[start_index].parent_y_shares, children[end_index].parent_y_shares),
|
||||
expansion = a.expansion - 1
|
||||
}
|
||||
merged_children[#merged_children + 1] = r
|
||||
|
||||
for ty = y, y + h - 1 do
|
||||
local succ = true
|
||||
for tx = x, x + w - 1 do
|
||||
local index = ty * #x_shares + tx + 1
|
||||
if index <= end_index then
|
||||
children[index] = false
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, #merged_children do
|
||||
a.split.children[#a.split.children + 1] = merged_children[i]
|
||||
a.split.children[#a.split.children].parent_cid = #a.split.children
|
||||
end
|
||||
|
||||
-- clean up children, remove all `false'
|
||||
for i = 1, #children do
|
||||
if children[i] ~= false then
|
||||
a.split.children[#a.split.children + 1] = children[i]
|
||||
a.split.children[#a.split.children].parent_cid = #a.split.children
|
||||
end
|
||||
end
|
||||
|
||||
push_open_areas(a.split.children)
|
||||
|
||||
elseif method == "s" then
|
||||
|
||||
if #open_areas > 0 then
|
||||
local times = arg_str == "" and 1 or tonumber(arg_str)
|
||||
local t = {}
|
||||
local c = #open_areas
|
||||
local p = open_areas[c].parent_id
|
||||
while c > 0 and open_areas[c].parent_id == p do
|
||||
t[#t + 1] = open_areas[c]
|
||||
open_areas[c] = nil
|
||||
c = c - 1
|
||||
end
|
||||
for i = #t, 1, -1 do
|
||||
open_areas[c + 1] = t[(i + times - 1) % #t + 1]
|
||||
c = c + 1
|
||||
end
|
||||
end
|
||||
|
||||
elseif method == "t" then
|
||||
|
||||
if #open_areas > 0 then
|
||||
open_areas[#open_areas].expansion = tonumber(arg_str) or default_expansion
|
||||
end
|
||||
|
||||
elseif method == "x" then
|
||||
|
||||
local a = close_area()
|
||||
a.layout = arg_str
|
||||
|
||||
elseif method == "-" then
|
||||
|
||||
close_area()
|
||||
|
||||
elseif method == "." then
|
||||
|
||||
while #open_areas > 0 do
|
||||
close_area()
|
||||
end
|
||||
|
||||
elseif method == "c" then
|
||||
|
||||
local limit = tonumber(arg_str)
|
||||
if limit == nil or limit > #open_areas then
|
||||
limit = #open_areas
|
||||
end
|
||||
local p = open_areas[#open_areas].parent_id
|
||||
while limit > 0 and open_areas[#open_areas].parent_id == p do
|
||||
close_area()
|
||||
limit = limit - 1
|
||||
end
|
||||
|
||||
elseif method == "/" then
|
||||
|
||||
close_area().habitable = false
|
||||
|
||||
elseif method == ";" then
|
||||
|
||||
-- nothing
|
||||
|
||||
end
|
||||
|
||||
if #open_areas + #closed_areas > max_areas then
|
||||
return nil
|
||||
end
|
||||
|
||||
while #open_areas > 0 and open_areas[#open_areas].expansion <= 0 do
|
||||
close_area()
|
||||
end
|
||||
|
||||
arg_str = ""
|
||||
return true
|
||||
end
|
||||
|
||||
open_areas = {root}
|
||||
|
||||
for i = 1, #command do
|
||||
local ch = command:sub(i, i)
|
||||
local t = ch_info[ch]
|
||||
local r = true
|
||||
if t == nil then
|
||||
return nil
|
||||
elseif t == 3 then
|
||||
if pending_op ~= nil then
|
||||
r = handle_op(pending_op)
|
||||
pending_op = nil
|
||||
end
|
||||
if #open_areas == 0 then return nil end
|
||||
if arg_str == "" then
|
||||
pending_op = ch
|
||||
else
|
||||
r = handle_op(ch)
|
||||
end
|
||||
elseif t == 2 or t == 1 then
|
||||
if pending_op ~= nil then
|
||||
handle_op(pending_op)
|
||||
pending_op = nil
|
||||
end
|
||||
if #open_areas == 0 and t == 2 then return nil end
|
||||
r = handle_op(ch)
|
||||
elseif t == 0 then
|
||||
arg_str = arg_str..ch
|
||||
end
|
||||
|
||||
if not r then return nil end
|
||||
end
|
||||
|
||||
if pending_op ~= nil then
|
||||
if not handle_op(pending_op) then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
if #closed_areas == 0 then
|
||||
return closed_areas, open_areas, pending_op ~= nil
|
||||
end
|
||||
|
||||
local old_closed_areas = closed_areas
|
||||
closed_areas = {}
|
||||
local function reorder_and_fill_adj_min(old_id)
|
||||
local a = old_closed_areas[old_id]
|
||||
closed_areas[#closed_areas + 1] = a
|
||||
a.id = #closed_areas
|
||||
|
||||
if a.split then
|
||||
for i = 1, #a.split.x_shares do
|
||||
a.split.x_shares[i][3] = 0
|
||||
a.split.x_shares[i][4] = minimum
|
||||
end
|
||||
|
||||
for i = 1, #a.split.y_shares do
|
||||
a.split.y_shares[i][3] = 0
|
||||
a.split.y_shares[i][4] = minimum
|
||||
end
|
||||
|
||||
for _, c in ipairs(a.split.children) do
|
||||
if c.id then
|
||||
reorder_and_fill_adj_min(c.id)
|
||||
end
|
||||
|
||||
local x_minimum, y_minimum
|
||||
if c.split then
|
||||
x_minimum, y_minimum = c.x_minimum, c.y_minimum
|
||||
else
|
||||
x_minimum, y_minimum =
|
||||
minimum, minimum
|
||||
end
|
||||
|
||||
if type(c.parent_x_shares) == "table" then
|
||||
local x_minimum_split = math.ceil(x_minimum / #c.parent_x_shares)
|
||||
for i = 1, #c.parent_x_shares do
|
||||
if a.split.x_shares[c.parent_x_shares[i]][4] < x_minimum_split then
|
||||
a.split.x_shares[c.parent_x_shares[i]][4] = x_minimum_split
|
||||
end
|
||||
end
|
||||
else
|
||||
if a.split.x_shares[c.parent_x_shares][4] < x_minimum then
|
||||
a.split.x_shares[c.parent_x_shares][4] = x_minimum
|
||||
end
|
||||
end
|
||||
|
||||
if type(c.parent_y_shares) == "table" then
|
||||
local y_minimum_split = math.ceil(y_minimum / #c.parent_y_shares)
|
||||
for i = 1, #c.parent_y_shares do
|
||||
if a.split.y_shares[c.parent_y_shares[i]][4] < y_minimum_split then
|
||||
a.split.y_shares[c.parent_y_shares[i]][4] = y_minimum_split
|
||||
end
|
||||
end
|
||||
else
|
||||
if a.split.y_shares[c.parent_y_shares][4] < y_minimum then
|
||||
a.split.y_shares[c.parent_y_shares][4] = y_minimum
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
a.x_minimum = 0
|
||||
a.x_total_weight = 0
|
||||
for i = 1, #a.split.x_shares do
|
||||
a.x_minimum = a.x_minimum + a.split.x_shares[i][4]
|
||||
a.x_total_weight = a.x_total_weight + (a.split.x_shares[i][2] or 0)
|
||||
end
|
||||
a.y_minimum = 0
|
||||
a.y_total_weight = 0
|
||||
for i = 1, #a.split.y_shares do
|
||||
a.y_minimum = a.y_minimum + a.split.y_shares[i][4]
|
||||
a.y_total_weight = a.y_total_weight + (a.split.y_shares[i][2] or 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
reorder_and_fill_adj_min(1)
|
||||
|
||||
-- For debugging
|
||||
-- for i = 1, #closed_areas do
|
||||
-- print(i, closed_areas[i].parent_id, closed_areas[i].parent_x_shares, closed_areas[i].parent_y_shares)
|
||||
-- if closed_areas[i].split then
|
||||
-- print("/", closed_areas[i].split.method, #closed_areas[i].split.x_shares, #closed_areas[i].split.y_shares)
|
||||
-- for j = 1, #closed_areas[i].split.children do
|
||||
-- print("->", closed_areas[i].split.children[j].id)
|
||||
-- end
|
||||
-- end
|
||||
-- end
|
||||
|
||||
local orig_width = root.width
|
||||
if root.x_minimum and root.width < root.x_minimum then
|
||||
root.width = root.x_minimum
|
||||
end
|
||||
local orig_height = root.height
|
||||
if root.y_minimum and root.height < root.y_minimum then
|
||||
root.height = root.y_minimum
|
||||
end
|
||||
|
||||
function split(id)
|
||||
local a = closed_areas[id]
|
||||
if a.split then
|
||||
local x_shares, y_shares
|
||||
x_shares, a.split.x_spare = fair_split(a.width, a.split.x_shares)
|
||||
y_shares, a.split.y_spare = fair_split(a.height, a.split.y_shares)
|
||||
|
||||
for _, c in ipairs(a.split.children) do
|
||||
|
||||
if type(c.parent_x_shares) == "table" then
|
||||
c.x = a.x + x_shares[c.parent_x_shares[1] - 1]
|
||||
c.width = 0
|
||||
for i = 1, #c.parent_x_shares do
|
||||
c.width = c.width + x_shares[c.parent_x_shares[i]] - x_shares[c.parent_x_shares[i] - 1]
|
||||
end
|
||||
else
|
||||
c.x = a.x + x_shares[c.parent_x_shares - 1]
|
||||
c.width = x_shares[c.parent_x_shares] - x_shares[c.parent_x_shares - 1]
|
||||
end
|
||||
|
||||
if type(c.parent_y_shares) == "table" then
|
||||
c.y = a.y + y_shares[c.parent_y_shares[1] - 1]
|
||||
c.height = 0
|
||||
for i = 1, #c.parent_y_shares do
|
||||
c.height = c.height + y_shares[c.parent_y_shares[i]] - y_shares[c.parent_y_shares[i] - 1]
|
||||
end
|
||||
else
|
||||
c.y = a.y + y_shares[c.parent_y_shares - 1]
|
||||
c.height = y_shares[c.parent_y_shares] - y_shares[c.parent_y_shares - 1]
|
||||
end
|
||||
|
||||
if c.id then
|
||||
split(c.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
split(1)
|
||||
|
||||
for i = 1, #closed_areas do
|
||||
if closed_areas[i].x + closed_areas[i].width > root.x + orig_width or
|
||||
closed_areas[i].y + closed_areas[i].height > root.y + orig_height
|
||||
then
|
||||
closed_areas[i].habitable = false
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, #open_areas do
|
||||
if open_areas[i].x + open_areas[i].width > root.x + orig_width or
|
||||
open_areas[i].y + open_areas[i].height > root.y + orig_height
|
||||
then
|
||||
open_areas[i].habitable = false
|
||||
end
|
||||
end
|
||||
|
||||
return closed_areas, open_areas, pending_op ~= nil
|
||||
end
|
||||
|
||||
local function areas_to_command(areas, to_embed, root_area)
|
||||
root_area = root_area or 1
|
||||
if #areas < root_area then return nil end
|
||||
|
||||
local function shares_to_arg_str(shares)
|
||||
local arg_str = ""
|
||||
for _, share in ipairs(shares) do
|
||||
if #arg_str > 0 then arg_str = arg_str.."," end
|
||||
arg_str = arg_str..tostring(share[1])
|
||||
if not share[2] or share[2] == 0 then
|
||||
-- nothing
|
||||
elseif share[2] > 0 then
|
||||
arg_str = arg_str.."_"..tostring(share[2])
|
||||
else
|
||||
arg_str = arg_str.."__"..tostring(-share[2])
|
||||
end
|
||||
end
|
||||
return arg_str
|
||||
end
|
||||
|
||||
local function get_command(area_id)
|
||||
local r
|
||||
local handled_options = {}
|
||||
local a = areas[area_id]
|
||||
|
||||
if a.hole then
|
||||
return "|"
|
||||
end
|
||||
|
||||
if a.split then
|
||||
for i = 1, #a.split.children do
|
||||
if a.split.children[i].hole then
|
||||
a.expansion = default_expansion + 1
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local method = a.split.method
|
||||
if method == "h" then
|
||||
r = shares_to_arg_str(a.split.x_shares)
|
||||
r = "h"..r
|
||||
elseif method == "v" then
|
||||
r = shares_to_arg_str(a.split.y_shares)
|
||||
r = "v"..r
|
||||
elseif method == "d" or method == "w" then
|
||||
local simple = true
|
||||
for _, s in ipairs(a.split.x_shares) do
|
||||
if s[1] ~= 1 or s[2] then simple = false break end
|
||||
end
|
||||
if simple then
|
||||
for _, s in ipairs(a.split.y_shares) do
|
||||
if s[1] ~= 1 or s[2] then simple = false break end
|
||||
end
|
||||
end
|
||||
if method == "w" and simple then
|
||||
r = tostring(#a.split.x_shares)..","..tostring(#a.split.y_shares)
|
||||
else
|
||||
r = shares_to_arg_str(a.split.x_shares)..",,"..shares_to_arg_str(a.split.y_shares)
|
||||
method = "d"
|
||||
end
|
||||
local m = ""
|
||||
for _, c in ipairs(a.split.children) do
|
||||
if type(c.parent_x_shares) == "table" then
|
||||
if #m > 0 then m = m.."," end
|
||||
m = m..tostring(c.parent_x_shares[#c.parent_x_shares] - c.parent_x_shares[1] + 1)..","..
|
||||
tostring(c.parent_y_shares[#c.parent_y_shares] - c.parent_y_shares[1] + 1)
|
||||
end
|
||||
end
|
||||
if method == "d" and r == "1,,1" then
|
||||
r = ""
|
||||
end
|
||||
r = method..r..(#m == 0 and m or (method == "w" and "," or ",,"))..m
|
||||
end
|
||||
local acc_dashes = 0
|
||||
if a.expansion > 1 then
|
||||
for _, c in ipairs(a.split.children) do
|
||||
local cr = get_command(c.id)
|
||||
if cr == "-" then
|
||||
acc_dashes = acc_dashes + 1
|
||||
else
|
||||
if acc_dashes == 0 then
|
||||
elseif acc_dashes == 1 then
|
||||
r = r.."-"
|
||||
else
|
||||
r = r.."c"..tonumber(acc_dashes)
|
||||
end
|
||||
acc_dashes = 0
|
||||
r = r..cr
|
||||
end
|
||||
end
|
||||
if acc_dashes > 0 then
|
||||
r = r.."c"
|
||||
end
|
||||
end
|
||||
|
||||
if area_id ~= root_area then
|
||||
if a.expansion ~= areas[a.parent_id].expansion - 1 then
|
||||
r = "t"..tostring(a.expansion)..r
|
||||
end
|
||||
else
|
||||
if a.expansion ~= default_expansion then
|
||||
r = "t"..tostring(a.expansion)..r
|
||||
end
|
||||
end
|
||||
elseif a.disabled then
|
||||
r = "/"
|
||||
elseif a.layout then
|
||||
r = "x"..a.layout
|
||||
else
|
||||
r = "-"
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
|
||||
local r = get_command(root_area)
|
||||
if not to_embed then
|
||||
if r == "-" then
|
||||
r = "."
|
||||
else
|
||||
-- The last . may be redundant, but it makes sure no pending op.
|
||||
r = r:gsub("[\\c]+$", "").."."
|
||||
end
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
if not in_module then
|
||||
print("Testing areas/command processing")
|
||||
local function check_transcoded_command(command, expectation)
|
||||
local areas, open_areas = areas_from_command(command, {x = 0, y = 0, width = 100, height = 100}, 0)
|
||||
if #open_areas > 0 then
|
||||
print("Found open areas after command "..command)
|
||||
assert(false)
|
||||
end
|
||||
local transcoded = areas_to_command(areas)
|
||||
if transcoded ~= expectation then
|
||||
print("Mismatched transcoding for "..command..": got "..transcoded..", expected "..expectation)
|
||||
assert(false)
|
||||
end
|
||||
end
|
||||
check_transcoded_command(".", ".")
|
||||
check_transcoded_command("3t.", ".")
|
||||
check_transcoded_command("121h.", "h1,2,1.")
|
||||
check_transcoded_command("1_10,2,1h1s131v.", "h1_10,2,1-v1,3,1.")
|
||||
check_transcoded_command("332111w.", "w3,3,2,1,1,1.")
|
||||
check_transcoded_command("1310111d.", "d1,3,1,,1,1,1.")
|
||||
check_transcoded_command("dw66.", "dw6,6.")
|
||||
check_transcoded_command(";dw66.", "dw6,6.")
|
||||
check_transcoded_command("101dw66.", "dw6,6.")
|
||||
check_transcoded_command("3tdw66.", "t3dw6,6.")
|
||||
print("Passed.")
|
||||
end
|
||||
|
||||
return {
|
||||
areas_from_command = areas_from_command,
|
||||
areas_to_command = areas_to_command,
|
||||
}
|
BIN
config/awesome/module/layout-machi/icon.png
Normal file
After Width: | Height: | Size: 988 B |
33
config/awesome/module/layout-machi/init.lua
Normal file
|
@ -0,0 +1,33 @@
|
|||
local engine = require(... .. ".engine")
|
||||
local layout = require(... .. ".layout")
|
||||
local editor = require(... .. ".editor")
|
||||
local switcher = require(... .. ".switcher")
|
||||
local default_editor = editor.default_editor
|
||||
local default_layout = layout.create{ name_func = default_name }
|
||||
local gcolor = require("gears.color")
|
||||
local beautiful = require("beautiful")
|
||||
|
||||
local icon_raw
|
||||
local source = debug.getinfo(1, "S").source
|
||||
if source:sub(1, 1) == "@" then
|
||||
icon_raw = source:match("^@(.-)[^/]+$") .. "icon.png"
|
||||
end
|
||||
|
||||
local function get_icon()
|
||||
if icon_raw ~= nil then
|
||||
return gcolor.recolor_image(icon_raw, beautiful.fg_normal)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
engine = engine,
|
||||
layout = layout,
|
||||
editor = editor,
|
||||
switcher = switcher,
|
||||
default_editor = default_editor,
|
||||
default_layout = default_layout,
|
||||
icon_raw = icon_raw,
|
||||
get_icon = get_icon,
|
||||
}
|
596
config/awesome/module/layout-machi/layout.lua
Normal file
|
@ -0,0 +1,596 @@
|
|||
local this_package = ... and (...):match("(.-)[^%.]+$") or ""
|
||||
local machi_editor = require(this_package.."editor")
|
||||
local awful = require("awful")
|
||||
local gobject = require("gears.object")
|
||||
local capi = {
|
||||
screen = screen
|
||||
}
|
||||
|
||||
local ERROR = 2
|
||||
local WARNING = 1
|
||||
local INFO = 0
|
||||
local DEBUG = -1
|
||||
|
||||
local module = {
|
||||
log_level = WARNING,
|
||||
global_default_cmd = "w66.",
|
||||
allow_shrinking_by_mouse_moving = false,
|
||||
}
|
||||
|
||||
local function log(level, msg)
|
||||
if level > module.log_level then
|
||||
print(msg)
|
||||
end
|
||||
end
|
||||
|
||||
local function min(a, b)
|
||||
if a < b then return a else return b end
|
||||
end
|
||||
|
||||
local function max(a, b)
|
||||
if a < b then return b else return a end
|
||||
end
|
||||
|
||||
local function get_screen(s)
|
||||
return s and capi.screen[s]
|
||||
end
|
||||
|
||||
awful.mouse.resize.add_enter_callback(
|
||||
function (c)
|
||||
c.full_width_before_move = c.width + c.border_width * 2
|
||||
c.full_height_before_move = c.height + c.border_width * 2
|
||||
end, 'mouse.move')
|
||||
|
||||
--- find the best area for the area-like object
|
||||
-- @param c area-like object - table with properties x, y, width, and height
|
||||
-- @param areas array of area objects
|
||||
-- @return the index of the best area
|
||||
local function find_area(c, areas)
|
||||
local choice = 1
|
||||
local choice_value = nil
|
||||
local c_area = c.width * c.height
|
||||
for i, a in ipairs(areas) do
|
||||
if a.habitable then
|
||||
local x_cap = max(0, min(c.x + c.width, a.x + a.width) - max(c.x, a.x))
|
||||
local y_cap = max(0, min(c.y + c.height, a.y + a.height) - max(c.y, a.y))
|
||||
local cap = x_cap * y_cap
|
||||
-- -- a cap b / a cup b
|
||||
-- local cup = c_area + a.width * a.height - cap
|
||||
-- if cup > 0 then
|
||||
-- local itx_ratio = cap / cup
|
||||
-- if choice_value == nil or choice_value < itx_ratio then
|
||||
-- choice_value = itx_ratio
|
||||
-- choice = i
|
||||
-- end
|
||||
-- end
|
||||
-- a cap b
|
||||
if choice_value == nil or choice_value < cap then
|
||||
choice = i
|
||||
choice_value = cap
|
||||
end
|
||||
end
|
||||
end
|
||||
return choice
|
||||
end
|
||||
|
||||
local function distance(x1, y1, x2, y2)
|
||||
-- use d1
|
||||
return math.abs(x1 - x2) + math.abs(y1 - y2)
|
||||
end
|
||||
|
||||
local function find_lu(c, areas, rd)
|
||||
local lu = nil
|
||||
for i, a in ipairs(areas) do
|
||||
if a.habitable then
|
||||
if rd == nil or (a.x < areas[rd].x + areas[rd].width and a.y < areas[rd].y + areas[rd].height) then
|
||||
if lu == nil or distance(c.x, c.y, a.x, a.y) < distance(c.x, c.y, areas[lu].x, areas[lu].y) then
|
||||
lu = i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return lu
|
||||
end
|
||||
|
||||
local function find_rd(c, border_width, areas, lu)
|
||||
local x, y
|
||||
x = c.x + c.width + (border_width or 0) * 2
|
||||
y = c.y + c.height + (border_width or 0) * 2
|
||||
local rd = nil
|
||||
for i, a in ipairs(areas) do
|
||||
if a.habitable then
|
||||
if lu == nil or (a.x + a.width > areas[lu].x and a.y + a.height > areas[lu].y) then
|
||||
if rd == nil or distance(x, y, a.x + a.width, a.y + a.height) < distance(x, y, areas[rd].x + areas[rd].width, areas[rd].y + areas[rd].height) then
|
||||
rd = i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return rd
|
||||
end
|
||||
|
||||
function module.set_geometry(c, area_lu, area_rd, useless_gap, border_width)
|
||||
-- We try to negate the gap of outer layer
|
||||
if area_lu ~= nil then
|
||||
c.x = area_lu.x - useless_gap
|
||||
c.y = area_lu.y - useless_gap
|
||||
end
|
||||
|
||||
if area_rd ~= nil then
|
||||
c.width = area_rd.x + area_rd.width - c.x + useless_gap - border_width * 2
|
||||
c.height = area_rd.y + area_rd.height - c.y + useless_gap - border_width * 2
|
||||
end
|
||||
end
|
||||
|
||||
-- TODO: the string need to be updated when its screen geometry changed.
|
||||
local function get_machi_tag_string(tag)
|
||||
if tag.machi_tag_string == nil then
|
||||
tag.machi_tag_string =
|
||||
tostring(tag.screen.geometry.width) .. "x" .. tostring(tag.screen.geometry.height) .. "+" ..
|
||||
tostring(tag.screen.geometry.x) .. "+" .. tostring(tag.screen.geometry.y) .. '+' .. tag.name
|
||||
end
|
||||
return tag.machi_tag_string
|
||||
end
|
||||
|
||||
function module.create(args_or_name, editor, default_cmd)
|
||||
local args
|
||||
if type(args_or_name) == "string" then
|
||||
args = {
|
||||
name = args_or_name
|
||||
}
|
||||
elseif type(args_or_name) == "function" then
|
||||
args = {
|
||||
name_func = args_or_name
|
||||
}
|
||||
elseif type(args_or_name) == "table" then
|
||||
args = args_or_name
|
||||
else
|
||||
return nil
|
||||
end
|
||||
if args.name == nil and args.name_func == nil then
|
||||
local prefix = args.icon_name and (args.icon_name.."-") or ""
|
||||
args.name_func = function (tag)
|
||||
return prefix..get_machi_tag_string(tag)
|
||||
end
|
||||
end
|
||||
args.editor = args.editor or editor or machi_editor.default_editor
|
||||
args.default_cmd = args.default_cmd or default_cmd or global_default_cmd
|
||||
args.persistent = args.persistent == nil or args.persistent
|
||||
|
||||
local layout = {}
|
||||
local instances = {}
|
||||
|
||||
local function get_instance_info(tag)
|
||||
return (args.name_func and args.name_func(tag) or args.name), args.persistent
|
||||
end
|
||||
|
||||
local function get_instance_(tag)
|
||||
local name, persistent = get_instance_info(tag)
|
||||
if instances[name] == nil then
|
||||
instances[name] = {
|
||||
layout = layout,
|
||||
cmd = persistent and args.editor.get_last_cmd(name) or nil,
|
||||
areas_cache = {},
|
||||
tag_data = {},
|
||||
client_data = setmetatable({}, {__mode="k"}),
|
||||
}
|
||||
if instances[name].cmd == nil then
|
||||
instances[name].cmd = args.default_cmd
|
||||
end
|
||||
end
|
||||
return instances[name]
|
||||
end
|
||||
|
||||
local function get_instance_data(screen, tag)
|
||||
local workarea = screen.workarea
|
||||
local instance = get_instance_(tag)
|
||||
local cmd = instance.cmd or module.global_default_cmd
|
||||
if cmd == nil then return end
|
||||
|
||||
local key = tostring(workarea.width) .. "x" .. tostring(workarea.height) .. "+" .. tostring(workarea.x) .. "+" .. tostring(workarea.y)
|
||||
if instance.areas_cache[key] == nil then
|
||||
instance.areas_cache[key] = args.editor.run_cmd(cmd, screen, tag)
|
||||
if instance.areas_cache[key] == nil then
|
||||
return
|
||||
end
|
||||
end
|
||||
return instance.client_data, instance.tag_data, instance.areas_cache[key], instance, args.new_placement_cb
|
||||
end
|
||||
|
||||
local function set_cmd(cmd, tag, keep_instance_data)
|
||||
local instance = get_instance_(tag)
|
||||
if instance.cmd ~= cmd then
|
||||
instance.cmd = cmd
|
||||
instance.areas_cache = {}
|
||||
for _, tag in pairs(instance.tag_data) do
|
||||
tag:emit_signal("property::layout")
|
||||
end
|
||||
if not keep_instance_data then
|
||||
instance.tag_data = {}
|
||||
instance.client_data = setmetatable({}, {__mode="k"})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local clean_up
|
||||
local tag_data = setmetatable({}, {__mode = "k"})
|
||||
|
||||
clean_up = function (tag)
|
||||
local screen = tag.screen
|
||||
local _cd, _td, _areas, instance, _new_placement_cb = get_instance_data(screen, tag)
|
||||
|
||||
if tag_data[tag].regsitered then
|
||||
tag_data[tag].regsitered = false
|
||||
tag:disconnect_signal("property::layout", clean_up)
|
||||
tag:connect_signal("property::selected", clean_up)
|
||||
for _, tag in pairs(instance.tag_data) do
|
||||
tag:emit_signal("property::layout")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
clean_up_on_selected_change = function (tag)
|
||||
if not tag.selected then clean_up(tag) end
|
||||
end
|
||||
|
||||
local function arrange(p)
|
||||
local useless_gap = p.useless_gap
|
||||
local screen = get_screen(p.screen)
|
||||
local wa = screen.workarea -- get the real workarea without the gap (instead of p.workarea)
|
||||
local cls = p.clients
|
||||
local tag = p.tag or screen.selected_tag
|
||||
local cd, td, areas, instance, new_placement_cb = get_instance_data(screen, tag)
|
||||
|
||||
if not tag_data[tag] then tag_data[tag] = {} end
|
||||
if not tag_data[tag].registered then
|
||||
tag_data[tag].regsitered = true
|
||||
tag:connect_signal("property::layout", clean_up)
|
||||
tag:connect_signal("property::selected", clean_up)
|
||||
end
|
||||
|
||||
if areas == nil then return end
|
||||
local nested_clients = {}
|
||||
|
||||
local function place_client_in_area(c, area)
|
||||
if machi_editor.nested_layouts[areas[area].layout] ~= nil then
|
||||
local clients = nested_clients[area]
|
||||
if clients == nil then clients = {}; nested_clients[area] = clients end
|
||||
clients[#clients + 1] = c
|
||||
else
|
||||
p.geometries[c] = {}
|
||||
module.set_geometry(p.geometries[c], areas[area], areas[area], useless_gap, 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- Make clients calling new_placement_cb appear in the end.
|
||||
local j = 0
|
||||
for i = 1, #cls do
|
||||
cd[cls[i]] = cd[cls[i]] or {}
|
||||
if cd[cls[i]].placement then
|
||||
j = j + 1
|
||||
cls[j], cls[i] = cls[i], cls[j]
|
||||
end
|
||||
end
|
||||
|
||||
for i, c in ipairs(cls) do
|
||||
if c.floating or c.immobilized then
|
||||
log(DEBUG, "Ignore client " .. tostring(c))
|
||||
else
|
||||
local geo = {
|
||||
x = c.x,
|
||||
y = c.y,
|
||||
width = c.width + c.border_width * 2,
|
||||
height = c.height + c.border_width * 2,
|
||||
}
|
||||
|
||||
if not cd[c].placement and new_placement_cb then
|
||||
cd[c].placement = true
|
||||
new_placement_cb(c, instance, areas, geo)
|
||||
end
|
||||
|
||||
local in_draft = cd[c].draft
|
||||
if cd[c].draft ~= nil then
|
||||
in_draft = cd[c].draft
|
||||
elseif cd[c].lu then
|
||||
in_draft = true
|
||||
elseif cd[c].area then
|
||||
in_draft = false
|
||||
else
|
||||
in_draft = nil
|
||||
end
|
||||
|
||||
local skip = false
|
||||
|
||||
if in_draft ~= false then
|
||||
if cd[c].lu ~= nil and cd[c].rd ~= nil and
|
||||
cd[c].lu <= #areas and cd[c].rd <= #areas and
|
||||
areas[cd[c].lu].habitable and areas[cd[c].rd].habitable
|
||||
then
|
||||
if areas[cd[c].lu].x == geo.x and
|
||||
areas[cd[c].lu].y == geo.y and
|
||||
areas[cd[c].rd].x + areas[cd[c].rd].width == geo.x + geo.width and
|
||||
areas[cd[c].rd].y + areas[cd[c].rd].height == geo.y + geo.height
|
||||
then
|
||||
skip = true
|
||||
end
|
||||
end
|
||||
|
||||
local lu = nil
|
||||
local rd = nil
|
||||
if not skip then
|
||||
log(DEBUG, "Compute areas for " .. (c.name or ("<untitled:" .. tostring(c) .. ">")))
|
||||
lu = find_lu(geo, areas)
|
||||
if lu ~= nil then
|
||||
geo.x = areas[lu].x
|
||||
geo.y = areas[lu].y
|
||||
rd = find_rd(geo, 0, areas, lu)
|
||||
end
|
||||
end
|
||||
|
||||
if lu ~= nil and rd ~= nil then
|
||||
if lu == rd and cd[c].lu == nil then
|
||||
cd[c].area = lu
|
||||
place_client_in_area(c, lu)
|
||||
else
|
||||
cd[c].lu = lu
|
||||
cd[c].rd = rd
|
||||
cd[c].area = nil
|
||||
p.geometries[c] = {}
|
||||
module.set_geometry(p.geometries[c], areas[lu], areas[rd], useless_gap, 0)
|
||||
end
|
||||
end
|
||||
else
|
||||
if cd[c].area ~= nil and
|
||||
cd[c].area <= #areas and
|
||||
areas[cd[c].area].habitable and
|
||||
areas[cd[c].area].layout == nil and
|
||||
areas[cd[c].area].x == geo.x and
|
||||
areas[cd[c].area].y == geo.y and
|
||||
areas[cd[c].area].width == geo.width and
|
||||
areas[cd[c].area].height == geo.height
|
||||
then
|
||||
skip = true
|
||||
else
|
||||
log(DEBUG, "Compute areas for " .. (c.name or ("<untitled:" .. tostring(c) .. ">")))
|
||||
local area = find_area(geo, areas)
|
||||
cd[c].area, cd[c].lu, cd[c].rd = area, nil, nil
|
||||
place_client_in_area(c, area)
|
||||
end
|
||||
end
|
||||
|
||||
if skip then
|
||||
if geo.x ~= c.x or geo.y ~= c.y or
|
||||
geo.width ~= c.width + c.border_width * 2 or
|
||||
geo.height ~= c.height + c.border_width * 2 then
|
||||
p.geometries[c] = {}
|
||||
module.set_geometry(p.geometries[c], geo, geo, useless_gap, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local arranged_area = {}
|
||||
local function arrange_nested_layout(area, clients)
|
||||
local nested_layout = machi_editor.nested_layouts[areas[area].layout]
|
||||
if not nested_layout then return end
|
||||
if td[area] == nil then
|
||||
local tag = gobject{}
|
||||
td[area] = tag
|
||||
-- TODO: Make the default more flexible.
|
||||
tag.layout = nested_layout
|
||||
tag.column_count = 1
|
||||
tag.master_count = 1
|
||||
tag.master_fill_policy = "expand"
|
||||
tag.gap = 0
|
||||
tag.master_width_factor = 0.5
|
||||
tag._private = {
|
||||
awful_tag_properties = {
|
||||
},
|
||||
}
|
||||
end
|
||||
local nested_params = {
|
||||
tag = td[area],
|
||||
screen = p.screen,
|
||||
clients = clients,
|
||||
padding = 0,
|
||||
geometry = {
|
||||
x = areas[area].x,
|
||||
y = areas[area].y,
|
||||
width = areas[area].width,
|
||||
height = areas[area].height,
|
||||
},
|
||||
-- Not sure how useless_gap adjustment works here. It seems to work anyway.
|
||||
workarea = {
|
||||
x = areas[area].x - useless_gap,
|
||||
y = areas[area].y - useless_gap,
|
||||
width = areas[area].width + useless_gap * 2,
|
||||
height = areas[area].height + useless_gap * 2,
|
||||
},
|
||||
useless_gap = useless_gap,
|
||||
geometries = {},
|
||||
}
|
||||
nested_layout.arrange(nested_params)
|
||||
for _, c in ipairs(clients) do
|
||||
p.geometries[c] = {
|
||||
x = nested_params.geometries[c].x,
|
||||
y = nested_params.geometries[c].y,
|
||||
width = nested_params.geometries[c].width,
|
||||
height = nested_params.geometries[c].height,
|
||||
}
|
||||
end
|
||||
end
|
||||
for area, clients in pairs(nested_clients) do
|
||||
arranged_area[area] = true
|
||||
arrange_nested_layout(area, clients)
|
||||
end
|
||||
-- Also rearrange empty nested layouts.
|
||||
-- TODO Iterate through only if the area has a nested layout
|
||||
for area, data in pairs(areas) do
|
||||
if not arranged_area[area] and areas[area].layout then
|
||||
arrange_nested_layout(area, {})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function resize_handler (c, context, h)
|
||||
local tag = c.screen.selected_tag
|
||||
local instance = get_instance_(tag)
|
||||
local cd = instance.client_data
|
||||
local cd, td, areas, _placement_cb = get_instance_data(c.screen, tag)
|
||||
|
||||
if areas == nil then return end
|
||||
|
||||
if context == "mouse.move" then
|
||||
local in_draft = cd[c].draft
|
||||
if cd[c].draft ~= nil then
|
||||
in_draft = cd[c].draft
|
||||
elseif cd[c].lu then
|
||||
in_draft = true
|
||||
elseif cd[c].area then
|
||||
in_draft = false
|
||||
else
|
||||
log(ERROR, "Assuming in_draft for unhandled client "..tostring(c))
|
||||
in_draft = true
|
||||
end
|
||||
if in_draft then
|
||||
local lu = find_lu(h, areas)
|
||||
local rd = nil
|
||||
if lu ~= nil then
|
||||
-- Use the initial width and height since it may change in undesired way.
|
||||
local hh = {}
|
||||
hh.x = areas[lu].x
|
||||
hh.y = areas[lu].y
|
||||
hh.width = c.full_width_before_move
|
||||
hh.height = c.full_height_before_move
|
||||
rd = find_rd(hh, 0, areas, lu)
|
||||
|
||||
if rd ~= nil and not module.allowing_shrinking_by_mouse_moving and
|
||||
(areas[rd].x + areas[rd].width - areas[lu].x < c.full_width_before_move or
|
||||
areas[rd].y + areas[rd].height - areas[lu].y < c.full_height_before_move) then
|
||||
hh.x = areas[rd].x + areas[rd].width - c.full_width_before_move
|
||||
hh.y = areas[rd].y + areas[rd].height - c.full_height_before_move
|
||||
lu = find_lu(hh, areas, rd)
|
||||
end
|
||||
|
||||
if lu ~= nil and rd ~= nil then
|
||||
cd[c].lu = lu
|
||||
cd[c].rd = rd
|
||||
cd[c].area = nil
|
||||
module.set_geometry(c, areas[lu], areas[rd], 0, c.border_width)
|
||||
end
|
||||
end
|
||||
else
|
||||
local center_x = h.x + h.width / 2
|
||||
local center_y = h.y + h.height / 2
|
||||
|
||||
local choice = nil
|
||||
local choice_value = nil
|
||||
|
||||
for i, a in ipairs(areas) do
|
||||
if a.habitable then
|
||||
local ac_x = a.x + a.width / 2
|
||||
local ac_y = a.y + a.height / 2
|
||||
local dis = (ac_x - center_x) * (ac_x - center_x) + (ac_y - center_y) * (ac_y - center_y)
|
||||
if choice_value == nil or choice_value > dis then
|
||||
choice = i
|
||||
choice_value = dis
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if choice and cd[c].area ~= choice then
|
||||
cd[c].lu = nil
|
||||
cd[c].rd = nil
|
||||
cd[c].area = choice
|
||||
module.set_geometry(c, areas[choice], areas[choice], 0, c.border_width)
|
||||
end
|
||||
end
|
||||
elseif cd[c].draft ~= false then
|
||||
local lu = find_lu(h, areas)
|
||||
local rd = nil
|
||||
if lu ~= nil then
|
||||
local hh = {}
|
||||
hh.x = h.x
|
||||
hh.y = h.y
|
||||
hh.width = h.width
|
||||
hh.height = h.height
|
||||
rd = find_rd(hh, c.border_width, areas, lu)
|
||||
end
|
||||
|
||||
if lu ~= nil and rd ~= nil then
|
||||
if lu == rd and cd[c].draft ~= true then
|
||||
cd[c].lu = nil
|
||||
cd[c].rd = nil
|
||||
cd[c].area = lu
|
||||
awful.layout.arrange(c.screen)
|
||||
else
|
||||
cd[c].lu = lu
|
||||
cd[c].rd = rd
|
||||
cd[c].area = nil
|
||||
module.set_geometry(c, areas[lu], areas[rd], 0, c.border_width)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
layout.name = args.icon_name or "machi"
|
||||
layout.arrange = arrange
|
||||
layout.resize_handler = resize_handler
|
||||
layout.machi_editor = args.editor
|
||||
layout.machi_get_instance_info = get_instance_info
|
||||
layout.machi_get_instance_data = get_instance_data
|
||||
layout.machi_set_cmd = set_cmd
|
||||
return layout
|
||||
end
|
||||
|
||||
module.placement = {}
|
||||
|
||||
local function empty_then_maybe_fair(c, instance, areas, geometry, do_fair)
|
||||
local area_client_count = {}
|
||||
for _, oc in ipairs(c.screen.tiled_clients) do
|
||||
local cd = instance.client_data[oc]
|
||||
if cd and cd.placement and cd.area then
|
||||
area_client_count[cd.area] = (area_client_count[cd.area] or 0) + 1
|
||||
end
|
||||
end
|
||||
local choice_client_count = nil
|
||||
local choice_spare_score = nil
|
||||
local choice = nil
|
||||
for i = 1, #areas do
|
||||
local a = areas[i]
|
||||
if a.habitable then
|
||||
-- +1 for the new client
|
||||
local client_count = (area_client_count[i] or 0) + 1
|
||||
local spare_score = a.width * a.height / client_count
|
||||
if choice == nil or (choice_client_count > 1 and client_count == 1) then
|
||||
choice_client_count = client_count
|
||||
choice_spare_score = spare_score
|
||||
choice = i
|
||||
elseif (choice_client_count > 1) == (client_count > 1) and choice_spare_score < spare_score then
|
||||
choice_client_count = client_count
|
||||
choice_spare_score = spare_score
|
||||
choice = i
|
||||
end
|
||||
end
|
||||
end
|
||||
if choice_client_count > 1 and not do_fair then
|
||||
return
|
||||
end
|
||||
instance.client_data[c].lu = nil
|
||||
instance.client_data[c].rd = nil
|
||||
instance.client_data[c].area = choice
|
||||
geometry.x = areas[choice].x
|
||||
geometry.y = areas[choice].y
|
||||
geometry.width = areas[choice].width
|
||||
geometry.height = areas[choice].height
|
||||
end
|
||||
|
||||
function module.placement.empty(c, instance, areas, geometry)
|
||||
empty_then_maybe_fair(c, instance, areas, geometry, false)
|
||||
end
|
||||
|
||||
function module.placement.empty_then_fair(c, instance, areas, geometry)
|
||||
empty_then_maybe_fair(c, instance, areas, geometry, true)
|
||||
end
|
||||
|
||||
return module
|
BIN
config/awesome/module/layout-machi/nested_layout_screenshot.png
Normal file
After Width: | Height: | Size: 637 KiB |
38
config/awesome/module/layout-machi/rc.patch
Normal file
|
@ -0,0 +1,38 @@
|
|||
--- /usr/etc/xdg/awesome/rc.lua 2019-10-02 22:20:36.000000000 -0400
|
||||
+++ rc.lua 2019-10-06 12:13:41.090197230 -0400
|
||||
@@ -17,6 +17,7 @@
|
||||
-- Enable hotkeys help widget for VIM and other apps
|
||||
-- when client with a matching name is opened:
|
||||
require("awful.hotkeys_popup.keys")
|
||||
+local machi = require("layout-machi")
|
||||
|
||||
-- {{{ Error handling
|
||||
-- Check if awesome encountered an error during startup and fell back to
|
||||
@@ -34,6 +35,8 @@
|
||||
-- Themes define colours, icons, font and wallpapers.
|
||||
beautiful.init(gears.filesystem.get_themes_dir() .. "default/theme.lua")
|
||||
|
||||
+beautiful.layout_machi = machi.get_icon()
|
||||
+
|
||||
-- This is used later as the default terminal and editor to run.
|
||||
terminal = "xterm"
|
||||
editor = os.getenv("EDITOR") or "nano"
|
||||
@@ -48,6 +51,7 @@
|
||||
|
||||
-- Table of layouts to cover with awful.layout.inc, order matters.
|
||||
awful.layout.layouts = {
|
||||
+ machi.default_layout,
|
||||
awful.layout.suit.floating,
|
||||
awful.layout.suit.tile,
|
||||
awful.layout.suit.tile.left,
|
||||
@@ -262,6 +266,10 @@
|
||||
awful.key({ modkey, "Shift" }, "q", awesome.quit,
|
||||
{description = "quit awesome", group = "awesome"}),
|
||||
|
||||
+ awful.key({ modkey, }, ".", function () machi.default_editor.start_interactive() end,
|
||||
+ {description = "edit the current layout if it is a machi layout", group = "layout"}),
|
||||
+ awful.key({ modkey, }, "/", function () machi.switcher.start(client.focus) end,
|
||||
+ {description = "switch between windows for a machi layout", group = "layout"}),
|
||||
awful.key({ modkey, }, "l", function () awful.tag.incmwfact( 0.05) end,
|
||||
{description = "increase master width factor", group = "layout"}),
|
||||
awful.key({ modkey, }, "h", function () awful.tag.incmwfact(-0.05) end,
|
591
config/awesome/module/layout-machi/switcher.lua
Normal file
|
@ -0,0 +1,591 @@
|
|||
local machi = {
|
||||
layout = require((...):match("(.-)[^%.]+$") .. "layout"),
|
||||
engine = require((...):match("(.-)[^%.]+$") .. "engine"),
|
||||
}
|
||||
|
||||
local capi = {
|
||||
client = client
|
||||
}
|
||||
|
||||
local beautiful = require("beautiful")
|
||||
local wibox = require("wibox")
|
||||
local awful = require("awful")
|
||||
local gears = require("gears")
|
||||
local lgi = require("lgi")
|
||||
local dpi = require("beautiful.xresources").apply_dpi
|
||||
local gtimer = require("gears.timer")
|
||||
|
||||
local ERROR = 2
|
||||
local WARNING = 1
|
||||
local INFO = 0
|
||||
local DEBUG = -1
|
||||
|
||||
local module = {
|
||||
log_level = WARNING,
|
||||
}
|
||||
|
||||
local function log(level, msg)
|
||||
if level > module.log_level then
|
||||
print(msg)
|
||||
end
|
||||
end
|
||||
|
||||
local function min(a, b)
|
||||
if a < b then return a else return b end
|
||||
end
|
||||
|
||||
local function max(a, b)
|
||||
if a < b then return b else return a end
|
||||
end
|
||||
|
||||
local function with_alpha(col, alpha)
|
||||
local r, g, b
|
||||
_, r, g, b, _ = col:get_rgba()
|
||||
return lgi.cairo.SolidPattern.create_rgba(r, g, b, alpha)
|
||||
end
|
||||
|
||||
function module.start(c, exit_keys)
|
||||
local tablist_font_desc = beautiful.get_merged_font(
|
||||
beautiful.font, dpi(10))
|
||||
local font_color = with_alpha(gears.color(beautiful.fg_normal), 1)
|
||||
local font_color_hl = with_alpha(gears.color(beautiful.fg_focus), 1)
|
||||
local label_size = dpi(30)
|
||||
local border_color = with_alpha(
|
||||
gears.color(beautiful.machi_switcher_border_color or beautiful.border_focus),
|
||||
beautiful.machi_switcher_border_opacity or 0.25)
|
||||
local border_color_hl = with_alpha(
|
||||
gears.color(beautiful.machi_switcher_border_hl_color or beautiful.border_focus),
|
||||
beautiful.machi_switcher_border_hl_opacity or 0.75)
|
||||
local fill_color = with_alpha(
|
||||
gears.color(beautiful.machi_switcher_fill_color or beautiful.bg_normal),
|
||||
beautiful.machi_switcher_fill_opacity or 0.25)
|
||||
local box_bg = with_alpha(
|
||||
gears.color(beautiful.machi_switcher_box_bg or beautiful.bg_normal),
|
||||
beautiful.machi_switcher_box_opacity or 0.85)
|
||||
local fill_color_hl = with_alpha(
|
||||
gears.color(beautiful.machi_switcher_fill_color_hl or beautiful.bg_focus),
|
||||
beautiful.machi_switcher_fill_hl_opacity or 1)
|
||||
-- for comparing floats
|
||||
local threshold = 0.1
|
||||
local traverse_radius = dpi(5)
|
||||
|
||||
local screen = c and c.screen or awful.screen.focused()
|
||||
local tag = screen.selected_tag
|
||||
local layout = tag.layout
|
||||
local gap = tag.gap
|
||||
local start_x = screen.workarea.x
|
||||
local start_y = screen.workarea.y
|
||||
|
||||
if (c ~= nil and c.floating) or layout.machi_get_instance_data == nil then return end
|
||||
|
||||
local cd, td, areas, _new_placement_cb = layout.machi_get_instance_data(screen, screen.selected_tag)
|
||||
if areas == nil or #areas == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local infobox = wibox({
|
||||
screen = screen,
|
||||
x = screen.workarea.x,
|
||||
y = screen.workarea.y,
|
||||
width = screen.workarea.width,
|
||||
height = screen.workarea.height,
|
||||
bg = "#ffffff00",
|
||||
opacity = 1,
|
||||
ontop = true,
|
||||
type = "dock",
|
||||
})
|
||||
infobox.visible = true
|
||||
|
||||
local tablist = nil
|
||||
local tablist_index = nil
|
||||
|
||||
local traverse_x, traverse_y
|
||||
if c then
|
||||
traverse_x = c.x + traverse_radius
|
||||
traverse_y = c.y + traverse_radius
|
||||
else
|
||||
traverse_x = screen.workarea.x + screen.workarea.width / 2
|
||||
traverse_y = screen.workarea.y + screen.workarea.height / 2
|
||||
end
|
||||
|
||||
local selected_area_ = nil
|
||||
local function set_selected_area(area)
|
||||
selected_area_ = area
|
||||
if area then
|
||||
traverse_x = max(areas[area].x + traverse_radius, min(areas[area].x + areas[area].width - traverse_radius, traverse_x))
|
||||
traverse_y = max(areas[area].y + traverse_radius, min(areas[area].y + areas[area].height - traverse_radius, traverse_y))
|
||||
end
|
||||
end
|
||||
|
||||
local function selected_area()
|
||||
if selected_area_ == nil then
|
||||
local min_dis = nil
|
||||
for i, a in ipairs(areas) do
|
||||
if a.habitable then
|
||||
local dis =
|
||||
math.abs(a.x + traverse_radius - traverse_x) + math.abs(a.x + a.width - traverse_radius - traverse_x) - a.width +
|
||||
math.abs(a.y + traverse_radius - traverse_y) + math.abs(a.y + a.height - traverse_radius - traverse_y) - a.height +
|
||||
traverse_radius * 4
|
||||
if min_dis == nil or min_dis > dis then
|
||||
min_dis = dis
|
||||
selected_area_ = i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
set_selected_area(selected_area_)
|
||||
end
|
||||
return selected_area_
|
||||
end
|
||||
|
||||
local parent_stack = {}
|
||||
|
||||
local function maintain_tablist()
|
||||
if tablist == nil then
|
||||
tablist = {}
|
||||
|
||||
local active_area = selected_area()
|
||||
for _, tc in ipairs(screen.tiled_clients) do
|
||||
if not (tc.floating or tc.immobilized)
|
||||
then
|
||||
if areas[active_area].x <= tc.x + tc.width + tc.border_width * 2 and tc.x <= areas[active_area].x + areas[active_area].width and
|
||||
areas[active_area].y <= tc.y + tc.height + tc.border_width * 2 and tc.y <= areas[active_area].y + areas[active_area].height
|
||||
then
|
||||
tablist[#tablist + 1] = tc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
tablist_index = 1
|
||||
|
||||
else
|
||||
|
||||
local j = 0
|
||||
for i = 1, #tablist do
|
||||
if tablist[i].valid then
|
||||
j = j + 1
|
||||
tablist[j] = tablist[i]
|
||||
elseif i <= tablist_index and tablist_index > 0 then
|
||||
tablist_index = tablist_index - 1
|
||||
end
|
||||
end
|
||||
|
||||
for i = #tablist, j + 1, -1 do
|
||||
table.remove(tablist, i)
|
||||
end
|
||||
end
|
||||
|
||||
if c and not c.valid then c = nil end
|
||||
if c == nil and #tablist > 0 then
|
||||
c = tablist[tablist_index]
|
||||
end
|
||||
end
|
||||
|
||||
local function draw_info(context, cr, width, height)
|
||||
maintain_tablist()
|
||||
|
||||
cr:set_source_rgba(0, 0, 0, 0)
|
||||
cr:rectangle(0, 0, width, height)
|
||||
cr:fill()
|
||||
|
||||
local msg, ext
|
||||
local active_area = selected_area()
|
||||
for i, a in ipairs(areas) do
|
||||
if a.habitable or i == active_area then
|
||||
cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height)
|
||||
cr:clip()
|
||||
cr:set_source(fill_color)
|
||||
cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height)
|
||||
cr:fill()
|
||||
cr:set_source(i == active_area and border_color_hl or border_color)
|
||||
cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height)
|
||||
cr:set_line_width(10.0)
|
||||
cr:stroke()
|
||||
cr:reset_clip()
|
||||
end
|
||||
end
|
||||
|
||||
if #tablist > 0 then
|
||||
local a = areas[active_area]
|
||||
local pl = lgi.Pango.Layout.create(cr)
|
||||
pl:set_font_description(tablist_font_desc)
|
||||
|
||||
local vpadding = dpi(10)
|
||||
local list_height = vpadding
|
||||
local list_width = 2 * vpadding
|
||||
local exts = {}
|
||||
|
||||
for index, tc in ipairs(tablist) do
|
||||
local label = tc.name or "<unnamed>"
|
||||
pl:set_text(label)
|
||||
local w, h
|
||||
w, h = pl:get_size()
|
||||
w = w / lgi.Pango.SCALE
|
||||
h = h / lgi.Pango.SCALE
|
||||
local ext = { width = w, height = h, x_bearing = 0, y_bearing = 0 }
|
||||
exts[#exts + 1] = ext
|
||||
list_height = list_height + ext.height + vpadding
|
||||
list_width = max(list_width, w + 2 * vpadding)
|
||||
end
|
||||
|
||||
local x_offset = a.x + a.width / 2 - start_x
|
||||
local y_offset = a.y + a.height / 2 - list_height / 2 + vpadding - start_y
|
||||
|
||||
-- cr:rectangle(a.x - start_x, y_offset - vpadding - start_y, a.width, list_height)
|
||||
-- cover the entire area
|
||||
cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height)
|
||||
cr:set_source(fill_color)
|
||||
cr:fill()
|
||||
|
||||
cr:rectangle(a.x + (a.width - list_width) / 2 - start_x, a.y + (a.height - list_height) / 2 - start_y, list_width, list_height)
|
||||
cr:set_source(box_bg)
|
||||
cr:fill()
|
||||
|
||||
for index, tc in ipairs(tablist) do
|
||||
local label = tc.name or "<unnamed>"
|
||||
local ext = exts[index]
|
||||
if index == tablist_index then
|
||||
cr:rectangle(x_offset - ext.width / 2 - vpadding / 2, y_offset - vpadding / 2, ext.width + vpadding, ext.height + vpadding)
|
||||
cr:set_source(fill_color_hl)
|
||||
cr:fill()
|
||||
pl:set_text(label)
|
||||
cr:move_to(x_offset - ext.width / 2 - ext.x_bearing, y_offset - ext.y_bearing)
|
||||
cr:set_source(font_color_hl)
|
||||
cr:show_layout(pl)
|
||||
else
|
||||
pl:set_text(label)
|
||||
cr:move_to(x_offset - ext.width / 2 - ext.x_bearing, y_offset - ext.y_bearing)
|
||||
cr:set_source(font_color)
|
||||
cr:show_layout(pl)
|
||||
end
|
||||
|
||||
y_offset = y_offset + ext.height + vpadding
|
||||
end
|
||||
end
|
||||
|
||||
-- show the traverse point
|
||||
cr:rectangle(traverse_x - start_x - traverse_radius, traverse_y - start_y - traverse_radius, traverse_radius * 2, traverse_radius * 2)
|
||||
cr:set_source_rgba(1, 1, 1, 1)
|
||||
cr:fill()
|
||||
end
|
||||
|
||||
infobox.bgimage = draw_info
|
||||
|
||||
local key_translate_tab = {
|
||||
["w"] = "Up",
|
||||
["a"] = "Left",
|
||||
["s"] = "Down",
|
||||
["d"] = "Right",
|
||||
}
|
||||
|
||||
awful.client.focus.history.disable_tracking()
|
||||
|
||||
local kg
|
||||
local function exit()
|
||||
awful.client.focus.history.enable_tracking()
|
||||
if capi.client.focus then
|
||||
capi.client.emit_signal("focus", capi.client.focus)
|
||||
end
|
||||
infobox.visible = false
|
||||
awful.keygrabber.stop(kg)
|
||||
end
|
||||
|
||||
local function handle_key(mod, key, event)
|
||||
if event == "release" then
|
||||
if exit_keys and exit_keys[key] then
|
||||
exit()
|
||||
end
|
||||
return
|
||||
end
|
||||
if key_translate_tab[key] ~= nil then
|
||||
key = key_translate_tab[key]
|
||||
end
|
||||
|
||||
maintain_tablist()
|
||||
assert(tablist ~= nil)
|
||||
|
||||
local shift = false
|
||||
local ctrl = false
|
||||
for i, m in ipairs(mod) do
|
||||
if m == "Shift" then shift = true
|
||||
elseif m == "Control" then ctrl = true
|
||||
end
|
||||
end
|
||||
|
||||
if key == "Tab" then
|
||||
if #tablist > 0 then
|
||||
tablist_index = tablist_index % #tablist + 1
|
||||
c = tablist[tablist_index]
|
||||
c:emit_signal("request::activate", "mouse.move", {raise=false})
|
||||
c:raise()
|
||||
|
||||
infobox.bgimage = draw_info
|
||||
end
|
||||
elseif key == "Up" or key == "Down" or key == "Left" or key == "Right" then
|
||||
local current_area = selected_area()
|
||||
|
||||
if c and (shift or ctrl) then
|
||||
if shift then
|
||||
if current_area == nil or
|
||||
areas[current_area].x ~= c.x or
|
||||
areas[current_area].y ~= c.y
|
||||
then
|
||||
traverse_x = c.x + traverse_radius
|
||||
traverse_y = c.y + traverse_radius
|
||||
set_selected_area(nil)
|
||||
end
|
||||
elseif ctrl then
|
||||
local ex = c.x + c.width + c.border_width * 2
|
||||
local ey = c.y + c.height + c.border_width * 2
|
||||
if current_area == nil or
|
||||
areas[current_area].x + areas[current_area].width ~= ex or
|
||||
areas[current_area].y + areas[current_area].height ~= ey
|
||||
then
|
||||
traverse_x = ex - traverse_radius
|
||||
traverse_y = ey - traverse_radius
|
||||
set_selected_area(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local choice = nil
|
||||
local choice_value
|
||||
|
||||
current_area = selected_area()
|
||||
|
||||
for i, a in ipairs(areas) do
|
||||
if not a.habitable then goto continue end
|
||||
|
||||
local v
|
||||
if key == "Up" then
|
||||
if a.x < traverse_x + threshold
|
||||
and traverse_x < a.x + a.width + threshold then
|
||||
v = traverse_y - a.y - a.height
|
||||
else
|
||||
v = -1
|
||||
end
|
||||
elseif key == "Down" then
|
||||
if a.x < traverse_x + threshold
|
||||
and traverse_x < a.x + a.width + threshold then
|
||||
v = a.y - traverse_y
|
||||
else
|
||||
v = -1
|
||||
end
|
||||
elseif key == "Left" then
|
||||
if a.y < traverse_y + threshold
|
||||
and traverse_y < a.y + a.height + threshold then
|
||||
v = traverse_x - a.x - a.width
|
||||
else
|
||||
v = -1
|
||||
end
|
||||
elseif key == "Right" then
|
||||
if a.y < traverse_y + threshold
|
||||
and traverse_y < a.y + a.height + threshold then
|
||||
v = a.x - traverse_x
|
||||
else
|
||||
v = -1
|
||||
end
|
||||
end
|
||||
|
||||
if (v > threshold) and (choice_value == nil or choice_value > v) then
|
||||
choice = i
|
||||
choice_value = v
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
|
||||
if choice == nil then
|
||||
choice = current_area
|
||||
if key == "Up" then
|
||||
traverse_y = screen.workarea.y
|
||||
elseif key == "Down" then
|
||||
traverse_y = screen.workarea.y + screen.workarea.height
|
||||
elseif key == "Left" then
|
||||
traverse_x = screen.workarea.x
|
||||
else
|
||||
traverse_x = screen.workarea.x + screen.workarea.width
|
||||
end
|
||||
end
|
||||
|
||||
if choice ~= nil then
|
||||
tablist = nil
|
||||
set_selected_area(choice)
|
||||
|
||||
if c and ctrl and cd[c].draft ~= false then
|
||||
local lu = cd[c].lu or cd[c].area
|
||||
local rd = cd[c].rd or cd[c].area
|
||||
|
||||
if shift then
|
||||
lu = choice
|
||||
if areas[rd].x + areas[rd].width <= areas[lu].x or
|
||||
areas[rd].y + areas[rd].height <= areas[lu].y
|
||||
then
|
||||
rd = nil
|
||||
end
|
||||
else
|
||||
rd = choice
|
||||
if areas[rd].x + areas[rd].width <= areas[lu].x or
|
||||
areas[rd].y + areas[rd].height <= areas[lu].y
|
||||
then
|
||||
lu = nil
|
||||
end
|
||||
end
|
||||
|
||||
if lu ~= nil and rd ~= nil then
|
||||
machi.layout.set_geometry(c, areas[lu], areas[rd], 0, c.border_width)
|
||||
elseif lu ~= nil then
|
||||
machi.layout.set_geometry(c, areas[lu], nil, 0, c.border_width)
|
||||
elseif rd ~= nil then
|
||||
c.x = min(c.x, areas[rd].x)
|
||||
c.y = min(c.y, areas[rd].y)
|
||||
machi.layout.set_geometry(c, nil, areas[rd], 0, c.border_width)
|
||||
end
|
||||
|
||||
if lu == rd and cd[c].draft ~= true then
|
||||
cd[c].lu = nil
|
||||
cd[c].rd = nil
|
||||
cd[c].area = lu
|
||||
else
|
||||
cd[c].lu = lu
|
||||
cd[c].rd = rd
|
||||
cd[c].area = nil
|
||||
end
|
||||
|
||||
c:emit_signal("request::activate", "mouse.move", {raise=false})
|
||||
c:raise()
|
||||
awful.layout.arrange(screen)
|
||||
elseif c and shift then
|
||||
-- move the window
|
||||
local in_draft = cd[c].draft
|
||||
if cd[c].draft ~= nil then
|
||||
in_draft = cd[c].draft
|
||||
elseif cd[c].lu then
|
||||
in_draft = true
|
||||
elseif cd[c].area then
|
||||
in_draft = false
|
||||
else
|
||||
log(ERROR, "Assuming in_draft for unhandled client "..tostring(c))
|
||||
in_draft = true
|
||||
end
|
||||
if in_draft then
|
||||
c.x = areas[choice].x
|
||||
c.y = areas[choice].y
|
||||
else
|
||||
machi.layout.set_geometry(c, areas[choice], areas[choice], 0, c.border_width)
|
||||
cd[c].lu = nil
|
||||
cd[c].rd = nil
|
||||
cd[c].area = choice
|
||||
end
|
||||
c:emit_signal("request::activate", "mouse.move", {raise=false})
|
||||
c:raise()
|
||||
awful.layout.arrange(screen)
|
||||
|
||||
tablist = nil
|
||||
else
|
||||
maintain_tablist()
|
||||
-- move the focus
|
||||
if #tablist > 0 and tablist[1] ~= c then
|
||||
c = tablist[1]
|
||||
capi.client.focus = c
|
||||
end
|
||||
end
|
||||
|
||||
infobox.bgimage = draw_info
|
||||
end
|
||||
elseif (key == "q" or key == "Prior") then
|
||||
local current_area = selected_area()
|
||||
if areas[current_area].parent_id == nil then
|
||||
return
|
||||
end
|
||||
|
||||
tablist = nil
|
||||
set_selected_area(areas[current_area].parent_id)
|
||||
if #parent_stack == 0 or
|
||||
parent_stack[#parent_stack] ~= current_area then
|
||||
parent_stack = {current_area}
|
||||
end
|
||||
parent_stack[#parent_stack + 1] = areas[current_area].parent_id
|
||||
current_area = parent_stack[#parent_stack]
|
||||
|
||||
if c and ctrl and cd[c].draft ~= false then
|
||||
if cd[c].area then
|
||||
cd[c].lu, cd[c].rd, cd[c].area = cd[c].area, cd[c].area, nil
|
||||
end
|
||||
machi.layout.set_geometry(c, areas[current_area], areas[current_area], 0, c.border_width)
|
||||
awful.layout.arrange(screen)
|
||||
end
|
||||
|
||||
infobox.bgimage = draw_info
|
||||
elseif (key =="e" or key == "Next") then
|
||||
local current_area = selected_area()
|
||||
if #parent_stack <= 1 or parent_stack[#parent_stack] ~= current_area then
|
||||
return
|
||||
end
|
||||
|
||||
tablist = nil
|
||||
set_selected_area(parent_stack[#parent_stack - 1])
|
||||
table.remove(parent_stack, #parent_stack)
|
||||
current_area = parent_stack[#parent_stack]
|
||||
|
||||
if c and ctrl then
|
||||
if areas[current_area].habitable and cd[c].draft ~= true then
|
||||
cd[c].lu, cd[c].rd, cd[c].area = nil, nil, current_area
|
||||
end
|
||||
machi.layout.set_geometry(c, areas[current_area], areas[current_area], 0, c.border_width)
|
||||
awful.layout.arrange(screen)
|
||||
end
|
||||
|
||||
infobox.bgimage = draw_info
|
||||
elseif key == "/" then
|
||||
local current_area = selected_area()
|
||||
local original_cmd = machi.engine.areas_to_command(areas, true, current_area)
|
||||
areas[current_area].hole = true
|
||||
local prefix, suffix = machi.engine.areas_to_command(
|
||||
areas, false):match("(.*)|(.*)")
|
||||
areas[current_area].hole = nil
|
||||
|
||||
workarea = {
|
||||
x = areas[current_area].x - gap * 2,
|
||||
y = areas[current_area].y - gap * 2,
|
||||
width = areas[current_area].width + gap * 4,
|
||||
height = areas[current_area].height + gap * 4,
|
||||
}
|
||||
gtimer.delayed_call(
|
||||
function ()
|
||||
layout.machi_editor.start_interactive(
|
||||
screen,
|
||||
{
|
||||
workarea = workarea,
|
||||
original_cmd = original_cmd,
|
||||
cmd_prefix = prefix,
|
||||
cmd_suffix = suffix,
|
||||
}
|
||||
)
|
||||
end
|
||||
)
|
||||
exit()
|
||||
elseif (key == "f" or key == ".") and c then
|
||||
if cd[c].draft == nil then
|
||||
cd[c].draft = true
|
||||
elseif cd[c].draft == true then
|
||||
cd[c].draft = false
|
||||
else
|
||||
cd[c].draft = nil
|
||||
end
|
||||
awful.layout.arrange(screen)
|
||||
elseif key == "Escape" or key == "Return" then
|
||||
exit()
|
||||
else
|
||||
log(DEBUG, "Unhandled key " .. key)
|
||||
end
|
||||
end
|
||||
|
||||
kg = awful.keygrabber.run(
|
||||
function (...)
|
||||
ok, _ = pcall(handle_key, ...)
|
||||
if not ok then exit() end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
return module
|
627
config/awesome/module/overflow.lua
Normal file
|
@ -0,0 +1,627 @@
|
|||
---------------------------------------------------------------------------
|
||||
-- A layout that allows its children to take more space than what's available
|
||||
-- in the surrounding container. If the content does exceed the available
|
||||
-- size, a scrollbar is added and scrolling behavior enabled.
|
||||
--
|
||||
--@DOC_wibox_layout_defaults_overflow_EXAMPLE@
|
||||
-- @author Lucas Schwiderski
|
||||
-- @copyright 2021 Lucas Schwiderski
|
||||
-- @layoutmod wibox.layout.overflow
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
local base = require('wibox.widget.base')
|
||||
local fixed = require('wibox.layout.fixed')
|
||||
local separator = require('wibox.widget.separator')
|
||||
local gtable = require('gears.table')
|
||||
local gshape = require('gears.shape')
|
||||
local gobject = require('gears.object')
|
||||
local mousegrabber = mousegrabber
|
||||
|
||||
local overflow = { mt = {} }
|
||||
|
||||
-- Determine the required space to draw the layout's children and, if necessary,
|
||||
-- the scrollbar.
|
||||
function overflow:fit(context, orig_width, orig_height)
|
||||
local widgets = self._private.widgets
|
||||
local num_widgets = #widgets
|
||||
if num_widgets < 1 then
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
local width, height = orig_width, orig_height
|
||||
local scrollbar_width = self._private.scrollbar_width
|
||||
local scrollbar_enabled = self._private.scrollbar_enabled
|
||||
local used_in_dir, used_max = 0, 0
|
||||
local is_y = self._private.dir == "y"
|
||||
local avail_in_dir = is_y and orig_height or orig_width
|
||||
|
||||
-- Set the direction covered by scrolling to the maximum value
|
||||
-- to allow widgets to take as much space as they want.
|
||||
if is_y then
|
||||
height = math.huge
|
||||
else
|
||||
width = math.huge
|
||||
end
|
||||
|
||||
-- First, determine widget sizes.
|
||||
-- Only when the content doesn't fit and needs scrolling should
|
||||
-- we reduce content size to make space for a scrollbar.
|
||||
for _, widget in pairs(widgets) do
|
||||
local w, h = base.fit_widget(self, context, widget, width, height)
|
||||
|
||||
if is_y then
|
||||
used_max = math.max(used_max, w)
|
||||
used_in_dir = used_in_dir + h
|
||||
else
|
||||
used_in_dir = used_in_dir + w
|
||||
used_max = math.max(used_max, h)
|
||||
end
|
||||
end
|
||||
|
||||
local spacing = self._private.spacing * (num_widgets - 1)
|
||||
used_in_dir = used_in_dir + spacing
|
||||
|
||||
local need_scrollbar = used_in_dir > avail_in_dir and scrollbar_enabled
|
||||
|
||||
-- If the direction perpendicular to scrolling (e.g. width in vertical
|
||||
-- scrolling) is not fully covered by any of the widgets, we can add our
|
||||
-- scrollbar width to that value. Otherwise widget size will be reduced
|
||||
-- during `layout` to make space for the scrollbar.
|
||||
if need_scrollbar
|
||||
and (
|
||||
(is_y and used_max < orig_width)
|
||||
or (not is_y and used_max < orig_height)
|
||||
) then
|
||||
used_max = used_max + scrollbar_width
|
||||
end
|
||||
|
||||
if is_y then
|
||||
return used_max, used_in_dir
|
||||
else
|
||||
return used_in_dir, used_max
|
||||
end
|
||||
end
|
||||
|
||||
-- Layout children, scrollbar and spacing widgets.
|
||||
-- Only those widgets that are currently visible will be placed.
|
||||
function overflow:layout(context, orig_width, orig_height)
|
||||
local result = {}
|
||||
local is_y = self._private.dir == "y"
|
||||
local widgets = self._private.widgets
|
||||
local avail_in_dir = is_y and orig_height or orig_width
|
||||
local scrollbar_width = self._private.scrollbar_width
|
||||
local scrollbar_enabled = self._private.scrollbar_enabled
|
||||
local scrollbar_position = self._private.scrollbar_position
|
||||
local width, height = orig_width, orig_height
|
||||
local widget_x, widget_y = 0, 0
|
||||
local used_in_dir, used_max = 0, 0
|
||||
|
||||
-- Set the direction covered by scrolling to the maximum value
|
||||
-- to allow widgets to take as much space as they want.
|
||||
if is_y then
|
||||
height = math.huge
|
||||
else
|
||||
width = math.huge
|
||||
end
|
||||
|
||||
-- First, determine widget sizes.
|
||||
-- Only when the content doesn't fit and needs scrolling should
|
||||
-- we reduce content size to make space for a scrollbar.
|
||||
for _, widget in pairs(widgets) do
|
||||
local w, h = base.fit_widget(self, context, widget, width, height)
|
||||
|
||||
if is_y then
|
||||
used_max = math.max(used_max, w)
|
||||
used_in_dir = used_in_dir + h
|
||||
else
|
||||
used_in_dir = used_in_dir + w
|
||||
used_max = math.max(used_max, h)
|
||||
end
|
||||
end
|
||||
|
||||
used_in_dir = used_in_dir + self._private.spacing * (#widgets-1)
|
||||
|
||||
-- Save size for scrolling behavior
|
||||
self._private.avail_in_dir = avail_in_dir
|
||||
self._private.used_in_dir = used_in_dir
|
||||
|
||||
local need_scrollbar = used_in_dir > avail_in_dir and scrollbar_enabled
|
||||
|
||||
local scroll_position = self._private.position
|
||||
|
||||
if need_scrollbar then
|
||||
local scrollbar_widget = self._private.scrollbar_widget
|
||||
local bar_x, bar_y = 0, 0
|
||||
local bar_w, bar_h
|
||||
-- The percentage of how much of the content can be visible within
|
||||
-- the available space
|
||||
local visible_percent = avail_in_dir / used_in_dir
|
||||
-- Make scrollbar length reflect `visible_percent`
|
||||
-- TODO: Apply a default minimum length
|
||||
local bar_length = math.floor(visible_percent * avail_in_dir)
|
||||
local bar_pos = (avail_in_dir - bar_length) * self._private.position
|
||||
|
||||
if is_y then
|
||||
bar_w, bar_h = base.fit_widget(self, context, scrollbar_widget, scrollbar_width, bar_length)
|
||||
bar_y = bar_pos
|
||||
|
||||
if scrollbar_position == "left" then
|
||||
widget_x = widget_x + bar_w
|
||||
elseif scrollbar_position == "right" then
|
||||
bar_x = orig_width - bar_w
|
||||
end
|
||||
|
||||
self._private.bar_length = bar_h
|
||||
|
||||
width = width - bar_w
|
||||
else
|
||||
bar_w, bar_h = base.fit_widget(self, context, scrollbar_widget, bar_length, scrollbar_width)
|
||||
bar_x = bar_pos
|
||||
|
||||
if scrollbar_position == "top" then
|
||||
widget_y = widget_y + bar_h
|
||||
elseif scrollbar_position == "bottom" then
|
||||
bar_y = orig_height - bar_h
|
||||
end
|
||||
|
||||
self._private.bar_length = bar_w
|
||||
|
||||
height = height - bar_h
|
||||
end
|
||||
|
||||
table.insert(result, base.place_widget_at(
|
||||
scrollbar_widget,
|
||||
math.floor(bar_x),
|
||||
math.floor(bar_y),
|
||||
math.floor(bar_w),
|
||||
math.floor(bar_h)
|
||||
))
|
||||
end
|
||||
|
||||
local pos, spacing = 0, self._private.spacing
|
||||
local interval = used_in_dir - avail_in_dir
|
||||
|
||||
local spacing_widget = self._private.spacing_widget
|
||||
if spacing_widget then
|
||||
if is_y then
|
||||
local _
|
||||
_, spacing = base.fit_widget(self, context, spacing_widget, width, spacing)
|
||||
else
|
||||
spacing = base.fit_widget(self, context, spacing_widget, spacing, height)
|
||||
end
|
||||
end
|
||||
|
||||
for i, w in pairs(widgets) do
|
||||
local content_x, content_y
|
||||
local content_w, content_h = base.fit_widget(self, context, w, width, height)
|
||||
|
||||
-- When scrolling down, the content itself moves up -> substract
|
||||
local scrolled_pos = pos - (scroll_position * interval)
|
||||
|
||||
-- Stop processing completely once we're passed the visible portion
|
||||
if scrolled_pos > avail_in_dir then
|
||||
break
|
||||
end
|
||||
|
||||
if is_y then
|
||||
content_x, content_y = widget_x, scrolled_pos
|
||||
pos = pos + content_h + spacing
|
||||
|
||||
if self._private.fill_space then
|
||||
content_w = width
|
||||
end
|
||||
else
|
||||
content_x, content_y = scrolled_pos, widget_y
|
||||
pos = pos + content_w + spacing
|
||||
|
||||
if self._private.fill_space then
|
||||
content_h = height
|
||||
end
|
||||
end
|
||||
|
||||
local is_in_view = is_y
|
||||
and (scrolled_pos + content_h > 0)
|
||||
or (scrolled_pos + content_w > 0)
|
||||
|
||||
if is_in_view then
|
||||
-- Add the spacing widget, but not before the first widget
|
||||
if i > 1 and spacing_widget then
|
||||
table.insert(result, base.place_widget_at(
|
||||
spacing_widget,
|
||||
-- The way how spacing is added for regular widgets
|
||||
-- and the `spacing_widget` is disconnected:
|
||||
-- The offset for regular widgets is added to `pos` one
|
||||
-- iteration _before_ the one where the widget is actually
|
||||
-- placed.
|
||||
-- Because of that, the placement for the spacing widget
|
||||
-- needs to substract that offset to be placed right after
|
||||
-- the previous regular widget.
|
||||
math.floor(is_y and content_x or (content_x - spacing)),
|
||||
math.floor(is_y and (content_y - spacing) or content_y),
|
||||
math.floor(is_y and content_w or spacing),
|
||||
math.floor(is_y and spacing or content_h)
|
||||
))
|
||||
end
|
||||
|
||||
table.insert(result, base.place_widget_at(
|
||||
w,
|
||||
math.floor(content_x),
|
||||
math.floor(content_y),
|
||||
math.floor(content_w),
|
||||
math.floor(content_h)
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function overflow:before_draw_children(_, cr, width, height)
|
||||
-- Clip drawing for children to the space we're allowed to draw in
|
||||
cr:rectangle(0, 0, width, height)
|
||||
cr:clip()
|
||||
end
|
||||
|
||||
|
||||
--- The amount of units to advance per scroll event.
|
||||
-- This affects calls to `scroll` and the default mouse wheel handler.
|
||||
--
|
||||
-- The default is `10`.
|
||||
--
|
||||
-- @property step
|
||||
-- @tparam number step The step size.
|
||||
-- @see set_step
|
||||
|
||||
--- Set the step size.
|
||||
--
|
||||
-- @method overflow:set_step
|
||||
-- @tparam number step The step size.
|
||||
-- @see step
|
||||
function overflow:set_step(step)
|
||||
self._private.step = step
|
||||
-- We don't need to emit enything here, since changing step only really
|
||||
-- takes effect the next time the user scrolls
|
||||
end
|
||||
|
||||
|
||||
--- Scroll the layout's content by `amount * step`.
|
||||
-- A positive amount scroll down/right, a negative amount scrolls up/left.
|
||||
--
|
||||
-- @method overflow:scroll
|
||||
-- @tparam number amount The amount to scroll by.
|
||||
-- @emits property::overflow::position
|
||||
-- @emitstparam property::overflow::position number position The new position.
|
||||
-- @emits widget::layout_changed
|
||||
-- @emits widget::redraw_needed
|
||||
-- @see step
|
||||
function overflow:scroll(amount)
|
||||
if amount == 0 then
|
||||
return
|
||||
end
|
||||
local interval = self._private.used_in_dir
|
||||
local delta = self._private.step / interval
|
||||
|
||||
local pos = self._private.position + (delta * amount)
|
||||
self:set_position(pos)
|
||||
end
|
||||
|
||||
|
||||
--- The scroll position.
|
||||
-- The position is represented as a fraction from `0` to `1`.
|
||||
--
|
||||
-- @property position
|
||||
-- @tparam number position The position.
|
||||
-- @propemits true false
|
||||
-- @see set_position
|
||||
|
||||
--- Set the current scroll position.
|
||||
--
|
||||
-- @method overflow:set_position
|
||||
-- @tparam number position The new position.
|
||||
-- @propemits true false
|
||||
-- @emits widget::layout_changed
|
||||
-- @emits widget::redraw_needed
|
||||
-- @see position
|
||||
function overflow:set_position(pos)
|
||||
local current = self._private.position
|
||||
local interval = self._private.used_in_dir - self._private.avail_in_dir
|
||||
if current == pos
|
||||
-- the content takes less space than what is available, i.e. everything
|
||||
-- is already visible
|
||||
or interval <= 0
|
||||
-- the position is out of range
|
||||
or (current <= 0 and pos < 0)
|
||||
or (current >= 1 and pos > 1) then
|
||||
return
|
||||
end
|
||||
|
||||
self._private.position = math.min(1, math.max(pos, 0))
|
||||
|
||||
self:emit_signal("widget::layout_changed")
|
||||
self:emit_signal("property::position", pos)
|
||||
end
|
||||
|
||||
|
||||
--- Get the current scroll position.
|
||||
--
|
||||
-- @method overflow:get_position
|
||||
-- @treturn number position The current position.
|
||||
-- @see position
|
||||
function overflow:get_position()
|
||||
return self._private.position
|
||||
end
|
||||
|
||||
|
||||
--- The scrollbar width.
|
||||
-- For horizontal scrollbars, this is the scrollbar height
|
||||
--
|
||||
-- The default is `5`.
|
||||
--
|
||||
--@DOC_wibox_layout_overflow_scrollbar_width_EXAMPLE@
|
||||
--
|
||||
-- @property scrollbar_width
|
||||
-- @tparam number scrollbar_width The scrollbar width.
|
||||
-- @propemits true false
|
||||
-- @see set_scrollbar_width
|
||||
|
||||
--- Set the scrollbar width.
|
||||
--
|
||||
-- @method overflow:set_scrollbar_width
|
||||
-- @tparam number scrollbar_width The new scrollbar width.
|
||||
-- @propemits true false
|
||||
-- @emits widget::layout_changed
|
||||
-- @emits widget::redraw_needed
|
||||
-- @see scrollbar_width
|
||||
function overflow:set_scrollbar_width(width)
|
||||
if self._private.scrollbar_width == width then
|
||||
return
|
||||
end
|
||||
|
||||
self._private.scrollbar_width = width
|
||||
|
||||
self:emit_signal("widget::layout_changed")
|
||||
self:emit_signal("property::scrollbar_width", width)
|
||||
end
|
||||
|
||||
|
||||
--- The scrollbar position.
|
||||
--
|
||||
-- For horizontal scrollbars, this can be `"top"` or `"bottom"`,
|
||||
-- for vertical scrollbars this can be `"left"` or `"right"`.
|
||||
-- The default is `"left"`/`"bottom"`.
|
||||
--
|
||||
--@DOC_wibox_layout_overflow_scrollbar_position_EXAMPLE@
|
||||
--
|
||||
-- @property scrollbar_position
|
||||
-- @tparam string scrollbar_position The scrollbar position.
|
||||
-- @propemits true false
|
||||
-- @see set_scrollbar_position
|
||||
|
||||
--- Set the scrollbar position.
|
||||
--
|
||||
-- @method overflow:set_scrollbar_position
|
||||
-- @tparam string scrollbar_position The new scrollbar position.
|
||||
-- @propemits true false
|
||||
-- @emits widget::layout_changed
|
||||
-- @emits widget::redraw_needed
|
||||
-- @see scrollbar_position
|
||||
function overflow:set_scrollbar_position(position)
|
||||
if self._private.scrollbar_position == position then
|
||||
return
|
||||
end
|
||||
|
||||
self._private.scrollbar_position = position
|
||||
|
||||
self:emit_signal("widget::layout_changed")
|
||||
self:emit_signal("property::scrollbar_position", position)
|
||||
end
|
||||
|
||||
|
||||
--- The scrollbar visibility.
|
||||
-- If this is set to `false`, no scrollbar will be rendered, even if the layout's
|
||||
-- content overflows. Mouse wheel scrolling will work regardless.
|
||||
--
|
||||
-- The default is `true`.
|
||||
--
|
||||
-- @property scrollbar_enabled
|
||||
-- @tparam boolean scrollbar_enabled The scrollbar visibility.
|
||||
-- @propemits true false
|
||||
-- @see set_scrollbar_enabled
|
||||
|
||||
--- Enable or disable the scrollbar visibility.
|
||||
--
|
||||
-- @method overflow:set_scrollbar_enabled
|
||||
-- @tparam boolean scrollbar_enabled The new scrollbar visibility.
|
||||
-- @propemits true false
|
||||
-- @emits widget::layout_changed
|
||||
-- @emits widget::redraw_needed
|
||||
-- @see scrollbar_enabled
|
||||
function overflow:set_scrollbar_enabled(enabled)
|
||||
if self._private.scrollbar_enabled == enabled then
|
||||
return
|
||||
end
|
||||
|
||||
self._private.scrollbar_enabled = enabled
|
||||
|
||||
self:emit_signal("widget::layout_changed")
|
||||
self:emit_signal("property::scrollbar_enabled", enabled)
|
||||
end
|
||||
|
||||
-- Wraps a callback function for `mousegrabber` that is capable of
|
||||
-- updating the scroll position.
|
||||
local function build_grabber(container)
|
||||
local is_y = container._private.dir == "y"
|
||||
local bar_interval = container._private.avail_in_dir - container._private.bar_length
|
||||
local start_pos = container._private.position * bar_interval
|
||||
local coords = mouse.coords()
|
||||
local start = is_y and coords.y or coords.x
|
||||
|
||||
return function(mouse)
|
||||
if not mouse.buttons[1] then
|
||||
return false
|
||||
end
|
||||
|
||||
local pos = is_y and mouse.y or mouse.x
|
||||
container:set_position((start_pos + (pos - start)) / bar_interval)
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Applies a mouse button signal using `build_grabber` to a scrollbar widget.
|
||||
local function apply_scrollbar_mouse_signal(container, w)
|
||||
w:connect_signal('button::press', function(_, _, _, button_id)
|
||||
if button_id ~= 1 then
|
||||
return
|
||||
end
|
||||
mousegrabber.run(build_grabber(container), "fleur")
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
--- The scrollbar widget.
|
||||
-- This widget is rendered as the scrollbar element.
|
||||
--
|
||||
-- The default is `awful.widget.separator{ shape = gears.shape.rectangle }`.
|
||||
--
|
||||
--@DOC_wibox_layout_overflow_scrollbar_widget_EXAMPLE@
|
||||
--
|
||||
-- @property scrollbar_widget
|
||||
-- @tparam widget scrollbar_widget The scrollbar widget.
|
||||
-- @propemits true false
|
||||
-- @see set_scrollbar_widget
|
||||
|
||||
--- Set the scrollbar widget.
|
||||
--
|
||||
-- This will also apply the mouse button handler.
|
||||
--
|
||||
-- @method overflow:set_scrollbar_widget
|
||||
-- @tparam widget scrollbar_widget The new scrollbar widget.
|
||||
-- @propemits true false
|
||||
-- @emits widget::layout_changed
|
||||
-- @see scrollbar_widget
|
||||
function overflow:set_scrollbar_widget(widget)
|
||||
local w = base.make_widget_from_value(widget)
|
||||
|
||||
apply_scrollbar_mouse_signal(self, w)
|
||||
|
||||
self._private.scrollbar_widget = w
|
||||
|
||||
self:emit_signal("widget::layout_changed")
|
||||
self:emit_signal("property::scrollbar_widget", widget)
|
||||
end
|
||||
|
||||
local function new(dir, ...)
|
||||
local ret = fixed[dir](...)
|
||||
|
||||
gtable.crush(ret, overflow, true)
|
||||
ret.widget_name = gobject.modulename(2)
|
||||
|
||||
-- Manually set the position here. We don't know the bounding size yet.
|
||||
ret._private.position = 0
|
||||
|
||||
-- Apply defaults. Bypass setters to avoid signals.
|
||||
ret._private.step = 10
|
||||
ret._private.fill_space = true
|
||||
ret._private.scrollbar_width = 5
|
||||
ret._private.scrollbar_enabled = true
|
||||
ret._private.scrollbar_position = dir == "vertical" and "right" or "bottom"
|
||||
|
||||
local scrollbar_widget = separator({ shape = gshape.rectangle })
|
||||
apply_scrollbar_mouse_signal(ret, scrollbar_widget)
|
||||
ret._private.scrollbar_widget = scrollbar_widget
|
||||
|
||||
ret:connect_signal('button::press', function(self, _, _, button)
|
||||
if button == 4 then
|
||||
if self.scroll_speed == nil or self.scroll_speed <= 0 then
|
||||
self:scroll(-1)
|
||||
else
|
||||
self:scroll(-1 * self.scroll_speed)
|
||||
end
|
||||
elseif button == 5 then
|
||||
if self.scroll_speed == nil or self.scroll_speed <= 0 then
|
||||
self:scroll(1)
|
||||
else
|
||||
self:scroll(1 * self.scroll_speed)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
|
||||
--- Returns a new horizontal overflow layout.
|
||||
-- Child widgets are placed similar to `wibox.layout.fixed`, except that
|
||||
-- they may take as much width as they want. If the total width of all child
|
||||
-- widgets exceeds the width available whithin the layout's outer container
|
||||
-- a scrollbar will be added and scrolling behavior enabled.
|
||||
-- @tparam widget ... Widgets that should be added to the layout.
|
||||
-- @constructorfct wibox.layout.overflow.horizontal
|
||||
function overflow.horizontal(...)
|
||||
return new("horizontal", ...)
|
||||
end
|
||||
|
||||
|
||||
--- Returns a new vertical overflow layout.
|
||||
-- Child widgets are placed similar to `wibox.layout.fixed`, except that
|
||||
-- they may take as much height as they want. If the total height of all child
|
||||
-- widgets exceeds the height available whithin the layout's outer container
|
||||
-- a scrollbar will be added and scrolling behavior enabled.
|
||||
-- @tparam widget ... Widgets that should be added to the layout.
|
||||
-- @constructorfct wibox.layout.fixed.horizontal
|
||||
function overflow.vertical(...)
|
||||
return new("vertical", ...)
|
||||
end
|
||||
|
||||
|
||||
--- Add spacing between each layout widgets.
|
||||
--
|
||||
-- This behaves just like in `wibox.layout.fixed`:
|
||||
--
|
||||
--@DOC_wibox_layout_fixed_spacing_EXAMPLE@
|
||||
--
|
||||
-- @property spacing
|
||||
-- @tparam number spacing Spacing between widgets.
|
||||
-- @propemits true false
|
||||
-- @see wibox.layout.fixed
|
||||
|
||||
|
||||
--- The widget used to fill the spacing between the layout elements.
|
||||
-- By default, no widget is used.
|
||||
--
|
||||
-- This behaves just like in `wibox.layout.fixed`:
|
||||
--
|
||||
--@DOC_wibox_layout_fixed_spacing_widget_EXAMPLE@
|
||||
--
|
||||
-- @property spacing_widget
|
||||
-- @tparam widget spacing_widget
|
||||
-- @propemits true false
|
||||
-- @see wibox.layout.fixed
|
||||
|
||||
|
||||
--- Set the layout's fill_space property.
|
||||
--
|
||||
-- If this property is `true`, widgets
|
||||
-- take all space in the non-scrolling directing (e.g. `width` for vertical
|
||||
-- scrolling). If `false`, they will only take as much as they need for their
|
||||
-- content.
|
||||
--
|
||||
-- The default is `true`.
|
||||
--
|
||||
--@DOC_wibox_layout_overflow_fill_space_EXAMPLE@
|
||||
--
|
||||
-- @property fill_space
|
||||
-- @tparam boolean fill_space
|
||||
-- @propemits true false
|
||||
|
||||
|
||||
--@DOC_fixed_COMMON@
|
||||
|
||||
--@DOC_widget_COMMON@
|
||||
|
||||
--@DOC_object_COMMON@
|
||||
|
||||
return setmetatable(overflow, overflow.mt)
|
||||
|
||||
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
|
11
config/awesome/module/rubato/.editorconfig
Normal file
|
@ -0,0 +1,11 @@
|
|||
root = true
|
||||
|
||||
[*.lua]
|
||||
charset = utf-8
|
||||
intent_style = tab
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
max_line_length = 120
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
21
config/awesome/module/rubato/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 andOrlando
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
352
config/awesome/module/rubato/README.md
Normal file
|
@ -0,0 +1,352 @@
|
|||
# rubato
|
||||
|
||||
- [Background and explanation](#background)
|
||||
- [How to actually use it](#usage)
|
||||
- [But why though?](#why)
|
||||
- [Arguments and Methods](#arguments-methods)
|
||||
- [Custom Easing Functions](#easing)
|
||||
- [Installation](#install)
|
||||
- [Why the name?](#name)
|
||||
- [Todo](#todo)
|
||||
|
||||
Basically like [awestore](https://github.com/K4rakara/awestore) but not really.
|
||||
|
||||
Join the cool curve crew
|
||||
|
||||
<img src="https://cdn.discordapp.com/attachments/702548961780826212/879022533314216007/download.jpeg" height=160>
|
||||
|
||||
<h1 id="background">Background and Explanation</h1>
|
||||
|
||||
The general premise of this is that I don't understand how awestore works. That and I really wanted to be able to have an interpolator that didn't have a set time. That being said, I haven't made an interpolator that doesn't have a set time yet, so I just have this instead. It has a similar function to awestore but the method in which you actually go about doing the easing is very different.
|
||||
|
||||
When creating an animation, the goal is to make it as smooth as humanly possible, but I was finding that with conventional methods, should the animation be interrupted with another call for animation, it would look jerky and inconsistent. You can see this jerkiness everywhere in websites made by professionals and it makes me very sad. I didn’t want that for my desktop so I used a bit of a different method.
|
||||
|
||||
This jerkiness is typically caused by discontinuous velocity graphs. One moment it’s slowing down, and the next it’s way too fast. This is caused by just lazily starting the animation anew when already in the process of animating. This kind of velocity graph looks like this:
|
||||
|
||||
<img src="images/disconnected_graph.png" alt="Disconnected Velocity Graph" height=160/>
|
||||
|
||||
Whereas rubato takes into account this initial velocity and restarts animation taking it into account. In the case of one wanting to interpolate from one point to another and then back, it would look like this:
|
||||
|
||||
<img src="images/connected_graph.png" alt="Connected Velocity Graph" height=160/>
|
||||
|
||||
<sub><sup>okay maybe my graph consistancy is trash, what can I do...</sup></sub>
|
||||
|
||||
These are what they would look like with forwards-and-back animations. A forwards-than-forwards animation would look more like this, just for reference:
|
||||
|
||||
<img src="images/forwards_forwards_graph.png" alt="Forwards ForwardsGraph" height=160/>
|
||||
|
||||
To ask one of you to give these graphs as inputs, however, would be really dumb. So instead we define an intro function and its duration, which in the figure above is the `y=x` portion, an outro function and its duration, which is the `y=-x` portion, and the rest is filled with constant velocity. The area under the curve for this must be equal to the position for this to end up at the correct position (antiderivative of velocity is position). If we know the area under the curve for the intro and outro functions, the only component we need to ensure that the antiderivative is equal to the position would be the height of the graph. We find that with this formula:
|
||||
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}m=\frac{d %2B ib(F_i(1)-1)}{i(F_i(1)-1) %2B o(F_o(1)-1) %2B t}" height=50>
|
||||
|
||||
where `m` is the height of the plateau, `i` is intro duration, `F_i` is the antiderivative of the intro easing function, `o` is outro duration, `F_o` is the antiderivative of the outro easing function, `d` is the total distance needed to be traveled, `b` is the initial slope, and `t` is the total duration.
|
||||
|
||||
We then simulate the antiderivative by adding `v(t)` (or the y-value at time `t` on the slope graph) to the current position 30 times per second (by default, but I recommend 60). There is some inaccuracy since it’s not a perfect antiderivative and there’s some weirdness when going from positive slopes to negative slopes that I don’t know how to intelligently fix (I have to simulate the antiderivative beforehand and multiply everything by a coefficient to prevent weird errors), but overall it results in good looking interruptions and I get a dopamine hit whenever I see it in action.
|
||||
|
||||
There are two main small issues that I can’t/don’t know how to fix mathematically:
|
||||
- It’s not perfectly accurate (it is perfectly accurate as `dt` goes to zero) which I don’t think is possible to fix unless I stop simulating the antiderivative and actually calc out the function, which seems time inefficient
|
||||
- When going from a positive m to a negative m, or in other words going backwards after going forwards in the animation, it will always undershoot by some value. I don’t know what that value is, I don’t know where it comes from, I don’t know how to fix it except for lots and lots of time-consuming testing, but it’s there. To compensate for this, whenever there’s a situation in which this will happen, I simulate the animation beforehand and multiply the entire animation by a corrective coefficient to make it do what I want
|
||||
- Awesome is kinda slow at redrawing imaages, so 60 redraws per second is realistically probably not going to happen. If you were to, for example, set the redraws per second to 500 or some arbitrarily large value, if I did nothing to dt, it would take forever to complete an animaiton. So since I can't fix awesome, I just (by default but this is optional) limit the rate based on the time it takes for awesome to render the first frame of the animation (Thanks Kasper for pointing this out and showing me a solution).
|
||||
|
||||
So that’s how it works. I’d love any contributions anyone’s willing to give. I also have plans to create an interpolator without a set duration called `target` as opposed to `timed` when I have the time (or need it for my rice).
|
||||
|
||||
<h1 id="usage">How to actually use it</h1>
|
||||
|
||||
So to actually use it, just create the object, give it a couple parameters, give it some function to
|
||||
execute, and then run it by updating `target`! In practice it'd look like this:
|
||||
|
||||
```lua
|
||||
timed = rubato.timed {
|
||||
intro = 0.1,
|
||||
duration = 0.5,
|
||||
subscribed = function(pos) print(pos) end
|
||||
}
|
||||
|
||||
--you can also achieve the same effect as the `subscribed` parameter with this:
|
||||
--timed:subscribe(function(pos) print(pos) end)
|
||||
|
||||
--target is initially 0 (unless you set pos otherwise)
|
||||
timed.target = 1
|
||||
--here it would print out a bunch of values (15 by default) which
|
||||
--I would normally copy and paste here but my stdout is broken
|
||||
--on awesome rn so just pretend there are a bunch of floats here
|
||||
|
||||
--and this'll send it back from 1 to 0, printing out another 15 #s
|
||||
timed.target = 0
|
||||
```
|
||||
|
||||
If you're familiar with the awestore api and don't wanna use what I've got, you can use those methods
|
||||
instead if you set `awestore_compat = true`. It’s a drop-in replacement, so your old code should work perfectly with it. If it doesn’t, please make an issue and I’ll do my best to fix it. Please include the broken code so I can try it out myself.
|
||||
|
||||
So how do the animations actually look? Let’s check out what I (at one point) use(ed) for my workspaces:
|
||||
|
||||
```lua
|
||||
timed = rubato.timed {
|
||||
intro = 0.1,
|
||||
duration = 0.3
|
||||
}
|
||||
```
|
||||
|
||||
![Normal Easing](./images/trapezoid_easing.gif)
|
||||
|
||||
The above is very subtly eased. A somewhat more pronounced easing would look more like this:
|
||||
|
||||
```lua
|
||||
timed = rubato.timed {
|
||||
intro = 0.5,
|
||||
duration = 1,
|
||||
easing = rubato.quadratic --quadratic slope, not easing
|
||||
}
|
||||
```
|
||||
|
||||
![Quadratic Easing](./images/quadratic_easing.gif)
|
||||
|
||||
The first animation’s velocity graph looks like a trapezoid, while the second looks like the graph shown below. Note the lack of a plateau and longer duration which gives the more pronounced easing:
|
||||
|
||||
![More Quadratic Easing](./images/triangleish.png)
|
||||
|
||||
<h1 id="why">But why though?</h1>
|
||||
|
||||
Why go through all this hassle? Why not just use awestore? That's a good question and to be fair you
|
||||
can use whatever interpolator you so choose. That being said, rubato is solely focused on animation, has mathematically perfect interruptions and I’ve been told it also looks smoother.
|
||||
|
||||
Furthermore, if you use rubato, you get to brag about how annoying it was to set up a monstrous
|
||||
derivative just to write a custom easing function, like the one shown in [Custom Easing
|
||||
Function](#easing)'s example. That's a benefit, not a downside, I promise.
|
||||
|
||||
Also maybe hopefully the code should be almost digestible kinda maybe. I tried my best to comment
|
||||
and documentate, but I actually have no idea how to do lua docs or anything.
|
||||
|
||||
Also it has a cooler name
|
||||
|
||||
<h1 id="arguments-methods">Arguments and Methods</h1>
|
||||
|
||||
**For rubato.timed**:
|
||||
|
||||
Arguments (in the form of a table):
|
||||
- `duration`: the total duration of the animation
|
||||
- `rate`: the number of times per second the timer executes. Higher rates mean
|
||||
smoother animations and less error.
|
||||
- `pos`: the initial position of the animation (def. `0`)
|
||||
- `intro`: the duration of the intro
|
||||
- `outro`: the duration of the outro (def. same as `intro`\*)
|
||||
- `prop_intro`: when `true`, `intro`, `outro` and `inter` represent proportional
|
||||
values; 0.5 would be half the duration. (def. `false`)
|
||||
- `easing`: the easing table (def. `interpolate.linear`)
|
||||
- `easing_outro`: the outro easing table (def. as `easing`)
|
||||
- `easing_inter`: the "intermittent" easing function, which defines which
|
||||
easing to use in the case of animation interruptions (def. same as
|
||||
`easing`)
|
||||
- `subscribed`: a function to subscribe at initialization (def. `nil`)
|
||||
- `override_simulate`: when `true`, will simulate everything instead of just
|
||||
when `dx` and `b` have opposite signs at the cost of having to do a little
|
||||
more work (and making my hard work on finding the formula for `m` worthless
|
||||
:slightly_frowning_face:) (def. `false`)
|
||||
- `override_dt`: will cap rate to the fastest that awesome can possibly handle.
|
||||
This may result in frame-skipping. By setting it to false, it may make
|
||||
animations slower (def. `true`)
|
||||
- `awestore_compat`: make api even *more* similar to awestore's (def. `false`)
|
||||
- `log`: it would print additional logs, but there aren't any logs to print right
|
||||
now so it kinda just sits there (def. `false`)
|
||||
|
||||
All of these values (except awestore_compat and subscribed) are mutable and changing them will
|
||||
change how the animation looks. I do not suggest changing `pos`, however, unless you change the
|
||||
position of what's going to be animated in some other way
|
||||
|
||||
\*with the caviat that if the outro being the same as the intro would result in an error, it would go
|
||||
for the largest allowable outro time. Ex: duration = 1, intro = 0.6, then outro will default to 0.4.
|
||||
|
||||
Useful properties:
|
||||
- `target`: when set, sets the target and starts the animation, otherwise returns the target
|
||||
- `state`: immutable, returns true if an animation is in progress
|
||||
|
||||
Methods are as follows:
|
||||
- `timed:subscribe(func)`: subscribe a function to be ran every refresh of the animation
|
||||
- `timed:unsubscribe(func)`: unsubscribe a function
|
||||
- `timed:fire()`: run all subscribed functions at current position
|
||||
- `timed:abort()`: stop the animation
|
||||
- `timed:restart()`: restart the animaiton from it's approximate initial state (if a value is
|
||||
changed during the animation it will remain changed after calling restart)
|
||||
|
||||
Awestore compatibility functions (`awestore_compat` must be true):
|
||||
- `timed:set(target_new)`: sets the position the animation should go to, effectively the same
|
||||
as setting target
|
||||
- `timed:initial()`: returns the intiial position
|
||||
- `timed:last()`: returns the target position, effectively the same as `timed.target`
|
||||
|
||||
Awestore compatibility properties:
|
||||
- `timed.started`: subscribable table which is called when the animation starts or is interrupted
|
||||
+ `timed.started:subscribe(func)`: subscribes a function
|
||||
+ `timed.started:unsubscribe(func)`: unsubscribes a function
|
||||
+ `timed.started:fire()`: runs all subscribed functions
|
||||
- `timed.ended`: subscribable table which is called when the animation ends
|
||||
+ `timed.ended:subscribe(func)`: subscribes a function
|
||||
+ `timed.ended:unsubscribe(func)`: unsubscribes a function
|
||||
+ `timed.ended:fire()`: runs all subscribed functions
|
||||
|
||||
**builtin easing functions**
|
||||
- `easing.zero`: linear easing, zero slope
|
||||
- `easing.linear`: linear slope, quadratic easing
|
||||
- `easing.quadratic`: quadratic slope, cubic easing
|
||||
- `easing.bouncy`: the bouncy thing as shown in the example
|
||||
|
||||
**functions for setting default values**
|
||||
- `rubato.set_def_rate(rate)`: set default rate for all interpolators, takes an `int`
|
||||
- `rubato.set_override_dt(value))`: set default for override_dt for all interpolators, takes a
|
||||
`bool`
|
||||
|
||||
<h1 id="easing">Custom Easing Functions</h1>
|
||||
|
||||
To make a custom easing function, it's pretty easy. You just need a table with two values:
|
||||
|
||||
- `easing`, which is the function of the slope curve you want. So if you want quadratic easing
|
||||
you'd take the derivative, which would result in linear easing. **Important:** `f(0)=0` and
|
||||
`f(1)=1` must be true for it to look nice.
|
||||
- `F`, which is basically just the value of the antiderivative of the easing function at `x=1`.
|
||||
This is the antiderivative of the scaled function (such that (0, 0) and (1, 1) are in the
|
||||
function), however, so be wary of that.
|
||||
|
||||
In practice, creating your own easing would look like this:
|
||||
|
||||
1. Go to [easings.net](https://easings.net)
|
||||
|
||||
For the sake of this tutorial, we'll do an extremely complex easing, "ease in elastic"
|
||||
|
||||
2. Find the necessary information
|
||||
|
||||
**Important:** You should really use sagemath or Wolfram Mathematica to get as exact of a derivative
|
||||
as you can. Wolfram Alpha doesn't cut it. I personally used sagemath because it's actually free,
|
||||
which is pretty cool. To take that one step further, I'd suggest using jupyter notebook in tandem
|
||||
with sagemath because if you run `%display latex` you get a super good looking output. If you can't
|
||||
use jupyter (or don't want to), `%display ascii_art` is a pretty cool alternative.
|
||||
|
||||
The initial function, given by [easings.net](https://easings.net), is as follows:
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}f(x)=-2^{10 \, x - 10}\times \sin\left(-\frac{43}{6} \, \pi %2B \frac{20}{3} \, \pi x\right))">
|
||||
|
||||
The derivative (via sagemath) is as follows:
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}f^\prime (x)=-\frac{5}{3} \, {\left(2 \, \pi \cos\left(-\frac{43}{6} \, \pi %2B \frac{20}{3} \, \pi x\right) %2B 3 \, \log\left(2\right) \sin\left(-\frac{43}{6} \, \pi %2B \frac{20}{3} \, \pi x\right)\right)}\times 2^{10 \, x - 9}">
|
||||
|
||||
First we double check that `f'(0)=0`, which in this case it is not.
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}f^\prime (0)=\frac{5}{1536} \, \sqrt{3} \pi - \frac{5}{1024} \, \log\left(2\right)">
|
||||
|
||||
so now we subtract `f'(0)` from `f'(x)` and get a pretty messy function, let's say `f_2(x)`.
|
||||
Regrettably, we're about to mess up that function a little more. Next we check that `f_2(1)=1`. In
|
||||
this case, once again, it doesn't. We get
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}f_2(1)=-\frac{5}{3072} \, \sqrt{3} {\left(2 \, \pi - 2049 \, \sqrt{3}\times \log\left(2\right)\right)}">
|
||||
|
||||
So now we divide our `f(x)` by `f(1)`, to get our final function, `f_e(x)` (easing function) (I am
|
||||
so good at naming these kinds of things)
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}f_e(x)=\frac{6 \, \pi %2B \sqrt{3} \pi \times 2^{10 \, x %2B 2}\times \cos\left(-\frac{43}{6} \, \pi %2B \frac{20}{3} \, \pi x\right) %2B 3 \, \sqrt{3}\times 2^{10 \, x %2B 1}\times \log\left(2\right) \sin\left(-\frac{43}{6} \, \pi %2B \frac{20}{3} \, \pi x\right) - 3 \, \sqrt{3}\times \log\left(2\right)}{3 \, {\left(2 \, \pi - 2049 \, \sqrt{3} \times\log\left(2\right)\right)}}">
|
||||
|
||||
Great... This is going to be a treat to write as lua... Anyways, our final step is to find the
|
||||
definite integral from 0 to 1 of our `f(x)`, which is this
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}\int_0^1 f_e(x) \,dx=\frac{20 \, \pi - 10 \, \sqrt{3}\times \log\left(2\right) - 2049 \, \sqrt{3}}{10 \, {\left(2 \, \pi - 2049 \, \sqrt{3}\times \log\left(2\right)\right)}}">
|
||||
|
||||
Now I'm sure that looks pretty daunting. However, these functions are kinda stupidly easy to find
|
||||
with sagemath. You basically only have to run these commands:
|
||||
|
||||
```python
|
||||
from sage.symbolic.integration.integral import definite_integral
|
||||
function('f')
|
||||
f(x)=factor(derivative('''your function goes here''', x))
|
||||
f(x)=factor(f(x)-f(0))
|
||||
f(x)=factor(f(x)/f(1))
|
||||
print(f(x)) # easing
|
||||
print(definite_integral(f(x), x, 0, 1)) # F
|
||||
```
|
||||
|
||||
which will tell you all you need to know.
|
||||
|
||||
It's important to use the `factor(...)` thing because otherwise you may end up with decimals, which
|
||||
really should be avoided if possible. When I didn't do factor, there were 0.499999s which makes it
|
||||
decently less accurate and substantially more complicated.
|
||||
|
||||
4. Now we just have to translate this into an actual lua table. You might want to be careful about
|
||||
not doing more operations than necessary but honestly it probably doesn't much matter.
|
||||
|
||||
```lua
|
||||
--all the constants are calculated only once
|
||||
local cs = {
|
||||
c1 = 6 * math.pi - 3 * math.sqrt(3) * math.log(2),
|
||||
c2 = math.sqrt(3) * math.pi,
|
||||
c3 = 6 * math.sqrt(3) * math.log(2),
|
||||
c4 = 6 * math.pi - 6147 * math.sqrt(3) * math.log(2),
|
||||
c5 = 46 * math.pi / 6
|
||||
}
|
||||
|
||||
bouncy = {
|
||||
F = (20 * math.pi - (10 * math.log(2) - 2049) * math.sqrt(3)) /
|
||||
(20 * math.pi - 20490 * math.sqrt(3) * math.log(2)),
|
||||
easing = function(t)
|
||||
--both of these values are reused
|
||||
local c1 = (20 * t * math.pi) / 3 - cs.c5
|
||||
local c2 = math.pow(2, 10 * t + 1) --in the 2^{10x+2} I factored out the 2 to calculate this once
|
||||
|
||||
return (cs.c1 + cs.c2 * c2 * math.cos(c1) + cs.c3 * c2 * math.sin(c1)) / cs.c4
|
||||
end
|
||||
}
|
||||
|
||||
timed = rubato.timed {
|
||||
intro = 0, --we'll use this as an outro, since it's weird as an intro
|
||||
outro = 0.7,
|
||||
duration = 1,
|
||||
easing = bouncy
|
||||
}
|
||||
```
|
||||
|
||||
We did it! Now to check whether or not it actually works
|
||||
|
||||
![Beautiful](./images/beautiful.gif)
|
||||
|
||||
While you can't see its full glory in 25 fps gif form, it really is pretty cool. Furthermore, if it
|
||||
works with *that* function, it'll probably work with anything. As long as you have the correct
|
||||
antiderivative and it's properly scaled, you can probably use any (real, differentiable) function
|
||||
under the sun.
|
||||
|
||||
Note that if it's not properly scaled, this can be worked around (if you're lazy and don't care
|
||||
about a bit of a performance decrease). You can set `override_simulaton` to true. However, it is
|
||||
possible that it will not perform exactly as you expected if you do this so do your best to just
|
||||
find the derivative and antiderivative of the derivative.
|
||||
|
||||
<h1 id="install">Installation</h1>
|
||||
|
||||
So actually telling people how to install this is important, isn't it
|
||||
|
||||
It supports luarocks, so that'll cut it if you want a really really easy install, but it'll install
|
||||
it in some faraway lua bin where you'll probably leave it forever if you either stop using rubato or
|
||||
stop using awesome. However, it's certainly the easiest way to go about it. I personally don't like
|
||||
doing this much because it adds it globally and I'm only gonna be using this with awesome, but it's
|
||||
a really easy install.
|
||||
|
||||
```
|
||||
luarocks install rubato
|
||||
```
|
||||
|
||||
Otherwise, somewhere in your awesome directory, (I use `~/.config/awesome/lib`) you can run this
|
||||
command:
|
||||
|
||||
```
|
||||
git clone https://github.com/andOrlando/rubato.git
|
||||
```
|
||||
|
||||
Then, whenever you actually want to use rubato, do this at the start of the lua file: `local rubato
|
||||
= require "lib.rubato"`
|
||||
|
||||
<h1 id="name">Why the name?</h1>
|
||||
|
||||
Because I play piano so this kinda links up with other stuff I do, and rubato really well fits the
|
||||
project. In music, it means "push and pull of tempo" basically, which really is what easing is all
|
||||
about in the first place. Plus, it'll be the first of my projects without garbage names
|
||||
("minesweperSweeper," "Latin Learning").
|
||||
|
||||
<h1 id="todo">Todo</h1>
|
||||
|
||||
- [ ] add `target` function, which rather than a set time has a set distance.
|
||||
- [x] improve intro and outro arguments (asserts, default values, proportional intros/outros)
|
||||
- [x] get a better name... (I have a cool name now!)
|
||||
- [x] make readme cooler
|
||||
- [x] have better documentation and add to luarocks
|
||||
- [ ] remove gears dependency
|
||||
- [ ] only apply corrective coefficient to plateau
|
||||
- [ ] Do `prop_intro` more intelligently so it doesn't have to do so many comparisons
|
||||
- [ ] Make things like `abort` more useful
|
48
config/awesome/module/rubato/easing.lua
Normal file
|
@ -0,0 +1,48 @@
|
|||
--- Linear easing (in quotes).
|
||||
local linear = {
|
||||
F = 0.5,
|
||||
easing = function(t) return t end
|
||||
}
|
||||
|
||||
--- Sublinear (?) easing.
|
||||
local zero = {
|
||||
F = 1,
|
||||
easing = function() return 1 end
|
||||
}
|
||||
|
||||
--- Quadratic easing.
|
||||
local quadratic = {
|
||||
F = 1/3,
|
||||
easing = function(t) return t * t end
|
||||
}
|
||||
|
||||
--bouncy constants
|
||||
local b_cs = {
|
||||
c1 = 6 * math.pi - 3 * math.sqrt(3) * math.log(2),
|
||||
c2 = math.sqrt(3) * math.pi,
|
||||
c3 = 6 * math.sqrt(3) * math.log(2),
|
||||
c4 = 6 * math.pi - 6147 * math.sqrt(3) * math.log(2),
|
||||
c5 = 46 * math.pi / 6
|
||||
}
|
||||
|
||||
--the bouncy one as seen in the readme
|
||||
local bouncy = {
|
||||
F = (20 * math.pi - (10 * math.log(2) - 2049) * math.sqrt(3)) /
|
||||
(20 * math.pi - 20490 * math.sqrt(3) * math.log(2)),
|
||||
easing = function(t)
|
||||
--short circuit
|
||||
if t == 0 then return 0 end
|
||||
if t == 1 then return 1 end
|
||||
|
||||
local c1 = (20 * t * math.pi) / 3 - b_cs.c5
|
||||
local c2 = math.pow(2, 10 * t + 1)
|
||||
return (b_cs.c1 + b_cs.c2 * c2 * math.cos(c1) + b_cs.c3 * c2 * math.sin(c1)) / b_cs.c4
|
||||
end
|
||||
}
|
||||
|
||||
return {
|
||||
linear = linear,
|
||||
zero = zero,
|
||||
quadratic = quadratic,
|
||||
bouncy = bouncy
|
||||
}
|
BIN
config/awesome/module/rubato/images/beautiful.gif
Normal file
After Width: | Height: | Size: 809 KiB |
BIN
config/awesome/module/rubato/images/connected_graph.png
Normal file
After Width: | Height: | Size: 7 KiB |
BIN
config/awesome/module/rubato/images/disconnected_graph.png
Normal file
After Width: | Height: | Size: 10 KiB |