smart_borders/init.lua

604 lines
17 KiB
Lua

local wibox = require("wibox")
local gears = require("gears")
local awful = require("awful")
local theme = require("beautiful")
local dpi = theme.xresources.apply_dpi
local module = {}
local client = client
local screen = screen
local function ori(pos)
if pos == "left" or pos == "right" then
return "v"
end
return "h"
end
local function list2map(list)
local set = {}
for _, l in ipairs(list) do
set[l] = true
end
return set
end
local function doubleclicked(obj)
if obj.doubleclick_timer then
obj.doubleclick_timer:stop()
obj.doubleclick_timer = nil
return true
end
obj.doubleclick_timer = gears.timer.start_new(0.3, function()
obj.doubleclick_timer = nil
end)
return false
end
local menu_selection_symbol
local menu_marker = function(condition)
if condition then
return menu_selection_symbol
end
return ""
end
local menu_move2tag = function(c, scr)
local list = {}
local s = scr or awful.screen.focused()
local count = 0
for _, t in pairs(s.tags) do
if t ~= awful.screen.focused().selected_tag then
count = count + 1
local entry = { t.index .. ": " .. t.name .. menu_marker(t.selected) .. " ", function()
c:move_to_tag(t)
end }
table.insert(list, entry)
end
end
if count > 0 then
return list
end
return nil
end
local menu_move2screen = function(c)
local list = {}
local count = 0
for s in screen do
local desc = next(s.outputs) or ""
if s.index ~= awful.screen.focused().index then
count = count + 1
local entry = { s.index .. ": " .. desc .. " ", menu_move2tag(c, s) }
table.insert(list, entry)
end
end
if count > 1 then
return list
end
return nil
end
function module.menu_client(c)
local list = {}
local list_tags = menu_move2tag(c)
if list_tags then
table.insert(list, { "move to tag", list_tags })
end
local list_screens = menu_move2screen(c)
if list_screens then
table.insert(list, { "move to screen", list_screens })
end
table.insert(list, { "fullscreen" .. menu_marker(c.fullscreen), function()
c.fullscreen = not c.fullscreen
c:raise()
end })
table.insert(list, { "maximize" .. menu_marker(c.maximized), function()
c.maximized = not c.maximized
c:raise()
end })
table.insert(list, { "master" .. menu_marker(c == awful.client.getmaster()), function()
c:swap(awful.client.getmaster())
end })
table.insert(list, { "sticky" .. menu_marker(c.sticky), function()
c.sticky = not c.sticky
end })
table.insert(list, { "top" .. menu_marker(c.ontop), function()
c.ontop = not c.ontop
end })
table.insert(list, { "minimize" .. menu_marker(c.minimized), function()
if c.minimized then
c.minimized = false
c:raise()
else
c.minimized = true
end
end })
table.insert(list, { "floating" .. menu_marker(c.floating), function()
c.floating = not c.floating
end })
table.insert(list, { menu_marker(nil) .. "close", function()
c:kill()
end })
return list
end
local rounded_corner_shape = function(radius, position)
if position == "bottom" then
return function(cr, width, height)
gears.shape.partially_rounded_rect(cr, width, height, false, false, true, true, radius)
end
elseif position == "top" then
return function(cr, width, height)
gears.shape.partially_rounded_rect(cr, width, height, true, true, false, false, radius)
end
end
return nil
end
local function new(config)
local cfg = config or {}
local positions = cfg.positions or { "left", "right", "top", "bottom" }
local button_positions = cfg.button_positions or { "top" }
local border_width = cfg.border_width or dpi(6)
local rounded_corner = cfg.rounded_corner or nil
local color_normal = cfg.color_normal or "#56666f"
local color_focus = cfg.color_focus or "#a1bfcf"
local color_hover = cfg.color_hover or nil
local button_size = cfg.button_size or dpi(40)
local spacing_widget = cfg.spacing_widget or nil
local button_maximize_size = cfg.button_maximize_size or button_size
local button_minimize_size = cfg.button_minimize_size or button_size
local button_floating_size = cfg.button_floating_size or button_size
local button_top_size = cfg.button_top_size or button_size
local button_sticky_size = cfg.button_sticky_size or button_size
local button_close_size = cfg.button_close_size or button_size
local color_maximize_normal = cfg.color_maximize_normal or "#a9dd9d"
local color_maximize_focus = cfg.color_maximize_focus or "#a9dd9d"
local color_maximize_hover = cfg.color_maximize_hover or "#c3f7b7"
local color_minimize_normal = cfg.color_minimize_normal or "#f0eaaa"
local color_minimize_focus = cfg.color_minimize_focus or "#f0eaaa"
local color_minimize_hover = cfg.color_minimize_hover or "#f6ffea"
local color_close_normal = cfg.color_close_normal or "#fd8489"
local color_close_focus = cfg.color_close_focus or "#fd8489"
local color_close_hover = cfg.color_close_hover or "#ff9ea3"
local color_floating_normal = cfg.color_floating_normal or "#ddace7"
local color_floating_focus = cfg.color_floating_focus or "#ddace7"
local color_floating_hover = cfg.color_floating_hover or "#f7c6ff"
local color_sticky_normal = cfg.color_sticky_normal or "#fb8965"
local color_sticky_focus = cfg.color_sticky_focus or "#fb8965"
local color_sticky_hover = cfg.color_sticky_hover or "#ffa37f"
local color_top_normal = cfg.color_top_normal or "#7fc1ca"
local color_top_focus = cfg.color_top_focus or "#7fc1ca"
local color_top_hover = cfg.color_top_hover or "#99dbe4"
local stealth = cfg.stealth or false
local show_button_tooltips = cfg.show_button_tooltips or false -- tooltip might intercept mouseclicks; not recommended!
local show_title_tooltip = cfg.show_title_tooltip or false -- might fuck up sloppy mouse focus; not recommended!
menu_selection_symbol = cfg.menu_selection_symbol or ""
local layout = cfg.layout or "fixed" -- "fixed" | "ratio"
local button_ratio = cfg.button_ratio or 0.2
local align_horizontal = cfg.align_horizontal or "right" -- "left" | "center" | "right"
local align_vertical = cfg.align_vertical or "center" -- "top" | "center" | "bottom"
local buttons = cfg.buttons or { "floating", "minimize", "maximize", "close" }
local button_left_click = cfg.button_left_click or function(c)
if c.maximized then
c.maximized = false
end
c:emit_signal("request::activate", "mouse_click", {raise = true})
awful.mouse.client.move(c)
end
local button_double_click = cfg.button_double_click or function(c)
c.maximized = not c.maximized
end
local button_middle_click = cfg.middle_click or function(c)
c:emit_signal("request::activate", "mouse_click", {raise = true})
awful.mouse.client.resize(c)
end
local button_right_click = cfg.right_click or function(c)
if c.client_menu then
c.client_menu:hide()
end
c.client_menu = awful.menu(module.menu_client(c))
c.client_menu:toggle()
end
local resize_factor = cfg.resize_factor or 0.01
local button_wheel_up = cfg.button_wheel_up or function(_)
awful.client.incwfact(resize_factor)
end
local button_wheel_down = cfg.button_wheel_down or function(_)
awful.client.incwfact(-1 * resize_factor)
end
local button_back = cfg.button_back or function(_)
awful.client.swap.byidx(-1)
end
local button_forward = cfg.button_forward or function(_)
awful.client.swap.byidx(1)
end
local button_funcs = {}
button_funcs[1] = function(c)
if doubleclicked(c) then
button_double_click(c)
else
button_left_click(c)
end
end
button_funcs[2] = function(c)
button_middle_click(c)
end
button_funcs[3] = function(c)
button_right_click(c)
end
button_funcs[4] = function(c)
button_wheel_up(c)
end
button_funcs[5] = function(c)
button_wheel_down(c)
end
button_funcs[8] = function(c)
button_back(c)
end
button_funcs[9] = function(c)
button_forward(c)
end
local function handle_button_press(c, button)
local func = button_funcs[button]
if func then
func(c)
end
end
local button_definitions = {}
button_definitions["maximize"] = {
name = "maximize",
color_normal = color_maximize_normal,
color_focus = color_maximize_focus,
color_hover = color_maximize_hover,
button_size = button_maximize_size,
action = function(cl)
cl.maximized = not cl.maximized
end
}
button_definitions["minimize"] = {
name = "minimize",
color_normal = color_minimize_normal,
color_focus = color_minimize_focus,
color_hover = color_minimize_hover,
button_size = button_minimize_size,
action = function(cl)
-- for whatever reason setting minimized does not work without wrapping it.
awful.spawn.easy_async_with_shell("sleep 0", function()
cl.minimized = true
end)
end
}
button_definitions["floating"] = {
name = "floating",
color_normal = color_floating_normal,
color_focus = color_floating_focus,
color_hover = color_floating_hover,
button_size = button_floating_size,
action = function(cl)
cl.floating = not cl.floating
end
}
button_definitions["close"] = {
name = "close",
color_normal = color_close_normal,
color_focus = color_close_focus,
color_hover = color_close_hover,
button_size = button_close_size,
action = function(cl)
cl:kill()
end
}
button_definitions["sticky"] = {
name = "sticky",
color_normal = color_sticky_normal,
color_focus = color_sticky_focus,
color_hover = color_sticky_hover,
button_size = button_sticky_size,
action = function(cl)
cl.sticky = not cl.sticky
end
}
button_definitions["top"] = {
name = "top",
color_normal = color_top_normal,
color_focus = color_top_focus,
color_hover = color_top_hover,
button_size = button_top_size,
action = function(cl)
cl.top = not cl.top
end
}
if layout ~= "fixed" and layout ~= "ratio" then
layout = "fixed"
end
if type(button_positions) == "string" then
button_positions = { button_positions }
end
local smart_border_titlebars = function(c)
local button_widgets = {}
local border_bg = wibox.widget.base.make_widget_declarative({
id = "border_bg",
bg = color_normal,
widget = wibox.container.background
})
border_bg:connect_signal("button::press", function(_, _, _, button)
handle_button_press(c, button)
end)
if color_hover then
border_bg:connect_signal("mouse::enter", function()
border_bg.bg = color_hover
end)
border_bg:connect_signal("mouse::leave", function()
if client.focus == c then
border_bg.bg = color_focus
else
border_bg.bg = color_normal
end
end)
end
local border_expander, border_expander_center
if layout == "fixed" then
border_expander_center = wibox.widget.base.make_widget_declarative({
fill_vertical = true,
fill_horizontal = true,
content_fill_vertical = true,
content_fill_horizontal = true,
border_bg,
widget = wibox.container.place
})
border_expander = wibox.widget.base.make_widget_declarative({
{ layout = wibox.layout.fixed.horizontal },
border_bg,
{ layout = wibox.layout.fixed.horizontal },
widget = wibox.layout.align.horizontal
})
end
local _button_positions = list2map(button_positions)
for _, pos in pairs(positions) do
local tb = awful.titlebar(c, {
size = border_width,
position = pos,
bg = "#00000000"
})
local btn_layout
if layout == "fixed" then
btn_layout = ori(pos) == "v" and wibox.layout.fixed.vertical or wibox.layout.fixed.horizontal
end
if layout == "ratio" then
btn_layout = ori(pos) == "v" and wibox.layout.ratio.vertical or wibox.layout.ratio.horizontal
end
if _button_positions[pos] then
-- border with buttons
local button_layout = wibox.widget.base.make_widget_declarative({
id = "button_layout",
spacing_widget = spacing_widget,
layout = btn_layout
})
local titlebar_widget
if layout == "fixed" then
if ori(pos) == "v" then
local expander = align_vertical == "center" and border_expander_center or border_expander
titlebar_widget = wibox.widget.base.make_widget_declarative({
align_vertical == "top" and button_layout or expander,
align_vertical == "center" and button_layout or expander,
align_vertical == "bottom" and button_layout or expander,
expand = align_vertical == "center" and "none" or "inside",
layout = wibox.layout.align.vertical
})
else
local expander = align_horizontal == "center" and border_expander_center or border_expander
titlebar_widget = wibox.widget.base.make_widget_declarative({
align_horizontal == "left" and button_layout or expander,
align_horizontal == "center" and button_layout or expander,
align_horizontal == "right" and button_layout or expander,
expand = align_horizontal == "center" and "none" or "inside",
layout = wibox.layout.align.horizontal
})
end
end
if layout == "ratio" then
titlebar_widget = wibox.widget.base.make_widget_declarative({
button_layout,
id = "titlebar_widget",
bg = color_normal,
widget = wibox.container.background
})
end
tb:setup{
titlebar_widget,
bg = "#00000000",
shape = rounded_corner and rounded_corner_shape(rounded_corner, pos) or nil,
widget = wibox.container.background()
}
local ratio_button_layout = wibox.widget.base.make_widget_declarative({
homogeneous = layout == "ratio" and true or false,
expand = true,
layout = ori(pos) == "h" and wibox.layout.grid.horizontal or wibox.layout.grid.vertical
})
local list_of_buttons = {}
for _, btn in pairs(buttons) do
local b = button_definitions[btn]
if b then
local button_widget = wibox.widget.base.make_widget_declarative({
id = b.name,
forced_width = ori(pos) == "h" and b.button_size or nil,
forced_height = ori(pos) == "v" and b.button_size or nil,
bg = b.color_normal,
widget = wibox.container.background
})
if show_button_tooltips then
awful.tooltip{
objects = { button_widget },
timer_function = function()
return b.name
end
}
end
button_widget:connect_signal("mouse::enter", function()
button_widget.bg = b.color_hover
end)
button_widget:connect_signal("mouse::leave", function()
if stealth then
if c == client.focus then
button_widget.bg = color_focus
else
button_widget.bg = color_normal
end
else
if c == client.focus then
button_widget.bg = b.color_focus
else
button_widget.bg = b.color_normal
end
end
end)
button_widget:connect_signal("button::press", function(_, _, _, button)
if button == 1 then
if b.action then
b.action(c)
end
else
handle_button_press(c, button)
end
end)
table.insert(list_of_buttons, button_widget)
button_widgets[b.name] = button_widget
end
end
if layout == "ratio" then
ratio_button_layout:set_children(list_of_buttons)
local ratio_children = {}
table.insert(ratio_children, border_bg)
table.insert(ratio_children, ratio_button_layout)
table.insert(ratio_children, border_bg)
button_layout:set_children(ratio_children)
if (ori(pos) == "h" and align_horizontal == "left") or (ori(pos) == "v" and align_vertical == "top") then
button_layout:ajust_ratio(2, 0, button_ratio, 1.0 - button_ratio)
end
if (ori(pos) == "h" and align_horizontal == "right") or (ori( pos) == "v" and align_vertical == "bottom") then
button_layout:ajust_ratio(2, 1.0 - button_ratio, button_ratio, 0)
end
if (ori(pos) == "h" and align_horizontal == "center") or (ori( pos) == "v" and align_vertical == "center") then
local side_ratio = (1.0 - button_ratio) / 2
button_layout:ajust_ratio(2, side_ratio, button_ratio, side_ratio)
end
end
if layout == "fixed" then
button_layout:set_children(list_of_buttons)
end
else
tb:setup{
border_bg,
bg = "#00000000",
shape = rounded_corner and rounded_corner_shape(rounded_corner, pos) or nil,
widget = wibox.container.background
}
end
end
-- show client title tooltip on border hover
if show_title_tooltip then
awful.tooltip{
objects = { border_bg },
timer_function = function()
return c.name
end
}
end
-- focus
c:connect_signal("focus", function(_)
for k, btn in pairs(button_widgets) do
local b = button_definitions[k]
if b then
btn.bg = stealth and color_focus or b.color_focus
end
end
border_bg.bg = color_focus
end)
-- unfocus
c:connect_signal("unfocus", function(_)
for k, btn in pairs(button_widgets) do
local b = button_definitions[k]
if b then
btn.bg = stealth and color_normal or b.color_normal
end
end
border_bg.bg = color_normal
end)
end
client.connect_signal("request::titlebars", smart_border_titlebars)
end
return setmetatable(module, { __call = function(_, ...)
new(...)
return module
end })