rice: AwesomeWM Night

This commit is contained in:
rxyhn 2022-02-21 18:10:32 +07:00
parent 231e846756
commit 40a23fe4fc
145 changed files with 17347 additions and 0 deletions

21
bin/awesomefetch Executable file
View 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
View 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

View 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'

View 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

View 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

View 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()

View 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")

View 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 ------------------------------------------------------------------------

View 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)

View 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)

View 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
View 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, "[&<>]",
{["&"] = "&amp;", ["<"] = "&lt;", [">"] = "&gt;"}))
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 ------------------------------------------------------------------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View 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

View 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

View 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)

View file

@ -0,0 +1,2 @@
/module/* @Nooo37
/widget/* @JavaCafe01

View 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.

View 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`.

View 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",
},
}

View 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

View 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

View 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

View 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)

View file

@ -0,0 +1,7 @@
return {
client = require(... .. ".client"),
color = require(... .. ".color"),
filesystem = require(... .. ".filesystem"),
shape = require(... .. ".shape"),
time = require(... .. ".time"),
}

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View 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"),
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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 }

View 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"),
}

View 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)

View 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

View 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

View 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,
}

View 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,
}

View file

@ -0,0 +1,3 @@
return {
playerctl = require(... .. ".playerctl"),
}

View 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
}

View 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)

View 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)

View 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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,656 @@
---------------------------------------------------------------------------
--- Modified Prompt module.
-- @author Julien Danjou &lt;julien@danjou.info&gt;
-- @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)

View 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"),
}

View 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,
}

View 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,
}

View 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,
}

View 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,
}

View file

@ -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

View 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"
),
}

View file

@ -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

View 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}

View 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 }

View 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 }

View 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.

View 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

View 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

View 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,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B

View 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,
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

View 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,

View 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

View 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

View 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

View 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.

View 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 didnt 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 its slowing down, and the next its 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 its not a perfect antiderivative and theres some weirdness when going from positive slopes to negative slopes that I dont 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 cant/dont know how to fix mathematically:
- Its not perfectly accurate (it is perfectly accurate as `dt` goes to zero) which I dont 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 dont know what that value is, I dont know where it comes from, I dont know how to fix it except for lots and lots of time-consuming testing, but its there. To compensate for this, whenever theres 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 thats how it works. Id love any contributions anyones 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`. Its a drop-in replacement, so your old code should work perfectly with it. If it doesnt, please make an issue and Ill 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? Lets 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 animations 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 Ive 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

View 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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Some files were not shown because too many files have changed in this diff Show more