local wibox = require("wibox") local gears = require("gears") local awful = require("awful") local theme = require("beautiful") local naughty = require("naughty") local glib = require("lgi").GLib local dpi = theme.xresources.apply_dpi local module = {} local client, screen, mouse, awesome = client, screen, mouse, awesome local instances = {} local function update_on_signal(c, signal, widget) local sig_instances = instances[signal] if sig_instances == nil then sig_instances = setmetatable({}, { __mode = "k" }) instances[signal] = sig_instances client.connect_signal(signal, function(cl) local widgets = sig_instances[cl] if widgets then for _, w in pairs(widgets) do w.update() end end end) end local widgets = sig_instances[c] if widgets == nil then widgets = setmetatable({}, { __mode = "v" }) sig_instances[c] = widgets end table.insert(widgets, widget) end local glib_context = function(fn) return function(args) glib.idle_add(glib.PRIORITY_DEFAULT_IDLE, function() fn(args) end) end end 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 len(T) local count = 0 for _ in pairs(T) do count = count + 1 end return count 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 name = t.name or "" local entry = { t.index .. ": " .. 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(custom_menu, 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, }) if custom_menu and len(custom_menu) > 0 then local function generate_menu_entry(e) if e and type(e) == "table" and e.text then local text = "" if type(e.text) == "string" then text = e.text end if type(e.text) == "function" then text = e.text(c) end return { text, function() if e.func then e.func(c) end end, } end end local class = c.class or "" for regex, entries in pairs(custom_menu) do if string.find(class, regex) then for _, e in ipairs(entries) do local menu_entry = generate_menu_entry(e) if menu_entry then table.insert(list, menu_entry) end end end end 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 add_hot_corner = function(args) args = args or {} local position = args.position or "" local placement = awful.placement[position] if not placement then return end local actions = args.buttons or {} local s = args.screen or awful.screen.focused() local width = args.width local height = args.height local color = args.color local corner = awful.popup({ screen = s, placement = placement, ontop = true, border_width = 0, minimum_height = height, maximum_height = height, minimum_width = width, maximum_width = width, bg = color, widget = wibox.container.background, }) -- this will run for every screen, so we have to make sure to only add one signal handler for every assigned signal local must_connect_signal = (s.index == 1) local function signal_name(pos, action) return "hot_corners::" .. pos .. "::" .. action end local defs = { { name = "left_click", button = 1 }, { name = "middle_click", button = 2 }, { name = "right_click", button = 3 }, { name = "wheel_up", button = 4 }, { name = "wheel_down", button = 5 }, { name = "back_click", button = 8 }, { name = "forward_click", button = 9 }, } local buttons = {} for _, btn in ipairs(defs) do if actions[btn.name] then local signal = signal_name(position, btn.name) table.insert( buttons, awful.button({}, btn.button, function() awesome.emit_signal(signal) end) ) if must_connect_signal then awesome.connect_signal(signal, glib_context(actions[btn.name])) end end end corner:buttons(buttons) for _, action in pairs({ "enter", "leave" }) do if actions[action] then local signal = signal_name(position, action) corner:connect_signal("mouse::" .. action, function() awesome.emit_signal(signal) end) if must_connect_signal then awesome.connect_signal(signal, glib_context(actions[action])) end end end 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 color_floating = cfg.color_floating or nil local color_maximized = cfg.color_maximized 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 snapping = cfg.snapping or false local snapping_center_mouse = cfg.snapping_center_mouse or false local snapping_max_distance = cfg.snapping_max_distance or nil local hot_corners = cfg.hot_corners or {} local hot_corners_color = cfg.hot_corners_color or "#00000000" local hot_corners_width = cfg.hot_corners_width or dpi(1) local hot_corners_height = cfg.hot_corners_height or dpi(1) 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! local custom_menu_entries = cfg.custom_menu_entries or {} 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(custom_menu_entries, 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 = {} local left_click_function = function(c) if doubleclicked(c) then button_double_click(c) else button_left_click(c) end end client.connect_signal("smart_borders::left_click", left_click_function) client.connect_signal("smart_borders::middle_click", button_middle_click) client.connect_signal("smart_borders::right_click", button_right_click) client.connect_signal("smart_borders::wheel_up", button_wheel_up) client.connect_signal("smart_borders::wheel_down", button_wheel_down) client.connect_signal("smart_borders::back_click", button_back) client.connect_signal("smart_borders::forward_click", button_forward) button_funcs[1] = function(c) c:emit_signal("smart_borders::left_click") end button_funcs[2] = function(c) c:emit_signal("smart_borders::middle_click") end button_funcs[3] = function(c) c:emit_signal("smart_borders::right_click") end button_funcs[4] = function(c) c:emit_signal("smart_borders::wheel_up") end button_funcs[5] = function(c) c:emit_signal("smart_borders::wheel_down") end button_funcs[8] = function(c) c:emit_signal("smart_borders::back_click") end button_funcs[9] = function(c) c:emit_signal("smart_borders::forward_click") 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.ontop = not cl.ontop end, } for s in screen do for pos, btns in pairs(hot_corners) do add_hot_corner({ buttons = btns, screen = s, position = pos, color = hot_corners_color, width = hot_corners_width, height = hot_corners_height, }) end end if layout ~= "fixed" and layout ~= "ratio" then layout = "fixed" end if type(button_positions) == "string" then button_positions = { button_positions } end if snapping then if awful and awful.mouse and awful.mouse.append_global_mousebindings then local mouse_closest_client = function() local s = awful.screen.focused() local m_x = mouse.coords().x local m_y = mouse.coords().y local closest_distance, closest_c for _, c in ipairs(s.all_clients) do if c:isvisible() then local x = c.x + (c.width / 2) local y = c.y + (c.height / 2) local dx = math.max(math.abs(m_x - x) - (c.width / 2), 0) local dy = math.max(math.abs(m_y - y) - (c.height / 2), 0) local distance = math.sqrt(dx * dx + dy * dy) if (not snapping_max_distance or (distance <= snapping_max_distance)) and (not closest_distance or distance < closest_distance) then closest_distance = distance closest_c = c end end end if closest_c and closest_c.valid then closest_c:emit_signal("request::activate", "smart_borders::snapping", { raise = true }) end return closest_c end awful.mouse.append_global_mousebindings({ awful.button({}, 1, function() local c = mouse_closest_client() if c then if snapping_center_mouse then mouse.coords({ x = c.x + c.width / 2, y = c.y + c.height / 2 }) end left_click_function(c) end end), awful.button({}, 2, function() local c = mouse_closest_client() if c then button_middle_click(c) end end), awful.button({}, 3, function() local c = mouse_closest_client() if c then button_right_click(c) end end), awful.button({}, 4, function() local c = mouse_closest_client() if c then button_wheel_up(c) end end), awful.button({}, 5, function() local c = mouse_closest_client() if c then button_wheel_down(c) end end), awful.button({}, 8, function() local c = mouse_closest_client() if c then button_back(c) end end), awful.button({}, 9, function() local c = mouse_closest_client() if c then button_forward(c) end end), }) else naughty.notify({ title = "smart_borders", text = "snapping requires awesomewm git version!", timeout = 0 }) end end local smart_border_titlebars = function(c) if c.disable_smart_borders then return end local border_bg = wibox.widget.base.make_widget_declarative({ { widget = wibox.container.margin }, 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 not b then -- custom button b = {} b.name = cfg["button_" .. btn .. "_name"] or btn b.button_size = cfg["button_" .. btn .. "_size"] or button_size b.color_focus = cfg["color_" .. btn .. "_focus"] or "#ff00ff" b.color_normal = cfg["color_" .. btn .. "_normal"] or "#ff00ff" b.color_hover = cfg["color_" .. btn .. "_hover"] or "#ff1aff" b.action = cfg["button_" .. btn .. "_function"] or nil end local button_widget = wibox.widget.base.make_widget_declarative({ { widget = wibox.container.margin }, 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 }, align = "top_left", 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) local update = function() if client.focus == c then button_widget.bg = stealth and color_focus or b.color_focus return end button_widget.bg = stealth and color_normal or b.color_normal end button_widget.update = update update_on_signal(c, "focus", button_widget) update_on_signal(c, "unfocus", button_widget) 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 local update_border = function() if client.focus == c then border_bg.bg = color_focus return end if color_maximized and c.maximized then border_bg.bg = color_maximized return end if color_floating and c.floating then border_bg.bg = color_floating return end border_bg.bg = color_normal end border_bg.update = update_border update_on_signal(c, "focus", border_bg) update_on_signal(c, "unfocus", border_bg) update_on_signal(c, "property::maximized", border_bg) update_on_signal(c, "property::floating", border_bg) end client.connect_signal("request::tag", smart_border_titlebars) client.connect_signal("property::disable_smart_borders", function(c) for _, pos in pairs(positions) do if c.disable_smart_borders then awful.titlebar.hide(c, pos) else awful.titlebar.show(c, pos) end end end) end return setmetatable(module, { __call = function(_, ...) new(...) return module end, })