From 9d46acd6b0ce1fb788ba048d5924f32cc0339d44 Mon Sep 17 00:00:00 2001 From: Aire-One Date: Tue, 22 Oct 2024 12:07:18 +0200 Subject: [PATCH] init --- .busted | 13 + .cspell.json | 64 +++++ .editorconfig | 21 ++ .luacheckrc | 39 +++ .luamonrc | 2 + .stylua.toml | 6 + .vscode/extensions.json | 10 + .vscode/launch.json | 17 ++ .vscode/settings.json | 16 ++ Makefile | 5 + README.md | 20 ++ awesomerc-dev-1.rockspec | 17 ++ scripts/run.sh | 40 +++ spec/default_spec.lua | 5 + src/awesomerc/awesomerc.lua | 522 ++++++++++++++++++++++++++++++++++++ 15 files changed, 797 insertions(+) create mode 100644 .busted create mode 100644 .cspell.json create mode 100644 .editorconfig create mode 100644 .luacheckrc create mode 100644 .luamonrc create mode 100644 .stylua.toml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 Makefile create mode 100644 README.md create mode 100644 awesomerc-dev-1.rockspec create mode 100755 scripts/run.sh create mode 100644 spec/default_spec.lua create mode 100644 src/awesomerc/awesomerc.lua diff --git a/.busted b/.busted new file mode 100644 index 0000000..e8e9cfb --- /dev/null +++ b/.busted @@ -0,0 +1,13 @@ +if os.getenv "LOCAL_LUA_DEBUGGER_VSCODE" == "1" then + require("lldebugger").start() +end + +local function lua_module_paths(module_base_path) + return (module_base_path .. "/?.lua;") .. (module_base_path .. "/?/init.lua;") +end + +return { + _all = { + lpath = lua_module_paths "src", + }, +} diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 0000000..9a4ab3a --- /dev/null +++ b/.cspell.json @@ -0,0 +1,64 @@ +{ + "words": [ + "autofocus", + "awesomerc", + "byidx", + "closebutton", + "confdir", + "conffile", + "currenttags", + "dbus", + "drawin", + "floatingbutton", + "fullscreen", + "getmaster", + "halign", + "iconwidget", + "imagebox", + "incmwfact", + "incncol", + "incnmaster", + "jumpto", + "keyboardlayout", + "keygrabber", + "keygroup", + "layoutbox", + "lldebugger", + "lpath", + "luacheck", + "luacheckrc", + "luamon", + "luamonrc", + "luarocks", + "maximizedbutton", + "modkey", + "mousebindings", + "mousegrabber", + "noreset", + "numpad", + "numrow", + "ontop", + "ontopbutton", + "pkill", + "rcfile", + "rockspec", + "stickybutton", + "Stylua", + "systray", + "taglist", + "tasklist", + "textbox", + "textclock", + "titlebar", + "titlebars", + "titlewidget", + "unminimize", + "valign", + "viewnext", + "viewprev", + "viewtoggle", + "wibar", + "wibox", + "xephyr" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..21b11e1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 3 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.json] +indent_size = 2 + +[*.sh] +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..93dc725 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,39 @@ +std = "lua54" + +files[".luacheckrc"].std = "+luacheckrc" + +files[".luamonrc"].std.globals = { + "ext", + "lang", +} + +include_files = { + ".busted", + ".luacheckrc", + ".luamonrc", + "*.rockspec", + "src/", +} + +read_globals = { + "awesome", + "button", + "dbus", + "drawable", + "drawin", + "key", + "keygrabber", + "mousegrabber", + "selection", + "tag", + "window", + "table.unpack", + "math.atan2", +} + +globals = { + "screen", + "mouse", + "root", + "client", +} diff --git a/.luamonrc b/.luamonrc new file mode 100644 index 0000000..6275058 --- /dev/null +++ b/.luamonrc @@ -0,0 +1,2 @@ +ext = { "lua" } +lang = "./scripts/run.sh runAwesome" diff --git a/.stylua.toml b/.stylua.toml new file mode 100644 index 0000000..8440490 --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,6 @@ +indent_type = "Spaces" +indent_width = 3 +call_parentheses = "None" + +[sort_requires] +enabled = true diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..6a07a7e --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "streetsidesoftware.code-spell-checker", + "editorconfig.editorconfig", + "tomblind.local-lua-debugger-vscode", + "johnnymorganz.stylua", + "dwenegar.vscode-luacheck", + "sumneko.lua" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0d21972 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lua-local", + "request": "launch", + "name": "Debug with Xephyr", + "program": { + "command": "${workspaceFolder}/scripts/run.sh" + }, + "args": ["debug"] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e6f3907 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "Lua.runtime.path": [ + "/usr/share/awesome/lib/?.lua", + "/usr/share/awesome/lib/?/init.lua" + ], + "[lua]": { + "editor.defaultFormatter": "JohnnyMorganz.stylua" + }, + "stylua.targetReleaseVersion": "latest", + "files.associations": { + ".busted": "lua", + ".luacheckrc": "lua", + ".luamonrc": "lua", + "*.rockspec": "lua" + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c5eeb11 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +dev: + scripts/run.sh start + +test: + luarocks test diff --git a/README.md b/README.md new file mode 100644 index 0000000..2770318 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# awesomerc.lua boilerplate + +Boilerplate to help getting started with Awesome. This repository contains the default `awesomerc.lua` file with pre-configured tools. + +## Features + +- Luacheck: Statically check the Lua code +- Stylua: Lua code formatter +- cSpell: Fix typo in code +- Live run with Xephyr and restart on change with Luamon (run `make dev` command) +- VSCode settings and recommended extensions +- VSCode debugger with local-lua-debugger-vscode +- Busted: Unit test Lua + +## TODOs + +- Install the config to ~/.config/awesome +- Pipeline (GitHub Actions?) +- Automated integration tests (with Xephyr/headless?) +- Migrate to Teal? diff --git a/awesomerc-dev-1.rockspec b/awesomerc-dev-1.rockspec new file mode 100644 index 0000000..e7a1af0 --- /dev/null +++ b/awesomerc-dev-1.rockspec @@ -0,0 +1,17 @@ +rockspec_format = "3.0" +package = "awesomerc" +version = "dev-1" +source = { + url = "*** please add URL for source tarball, zip or repository here ***", +} +description = { + homepage = "*** please enter a project homepage ***", + license = "*** please specify a license ***", +} +build = { + type = "builtin", + modules = {}, +} +test = { + type = "busted", +} diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100755 index 0000000..dc78b0b --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env sh + +xephyr=/usr/bin/Xephyr +awesome=/usr/bin/awesome + +# TODO: configurable +confdir=./src/awesomerc +rcfile=$confdir/awesomerc.lua +screen=1600x900 + +display=:1.0 + +startXephyr() { + $xephyr $display -ac -br -noreset -screen $screen >/dev/null 2>&1 & +} + +runAwesome() { + DISPLAY=$display $awesome \ + --config $rcfile \ + --search $confdir +} + +case $1 in + start) + startXephyr + luamon . + pkill Xephyr + ;; + runAwesome) + runAwesome + ;; + debug) + startXephyr + sleep 1 # wait for Xephyr to be ready + runAwesome + ;; + *) + echo "Need command" + ;; +esac diff --git a/spec/default_spec.lua b/spec/default_spec.lua new file mode 100644 index 0000000..dcf5eb0 --- /dev/null +++ b/spec/default_spec.lua @@ -0,0 +1,5 @@ +describe("default", function() + it("should work", function() + assert.are.equal(1, 1) + end) +end) diff --git a/src/awesomerc/awesomerc.lua b/src/awesomerc/awesomerc.lua new file mode 100644 index 0000000..7a49e5f --- /dev/null +++ b/src/awesomerc/awesomerc.lua @@ -0,0 +1,522 @@ +-- awesome_mode: api-level=4:screen=on + +if os.getenv "LOCAL_LUA_DEBUGGER_VSCODE" == "1" then + require("lldebugger").start() +end + +local awful = require "awful" +local beautiful = require "beautiful" +local gears = require "gears" +local hotkeys_popup = require "awful.hotkeys_popup" +local menubar = require "menubar" +local naughty = require "naughty" +local ruled = require "ruled" +local wibox = require "wibox" +require "awful.autofocus" +require "awful.hotkeys_popup.keys" +require "luarocks.loader" + +naughty.connect_signal("request::display_error", function(message, startup) + naughty.notification { + urgency = "critical", + title = "Oops, an error happened" .. (startup and " during startup!" or "!"), + message = message, + } +end) + +beautiful.init(gears.filesystem.get_themes_dir() .. "default/theme.lua") + +local terminal = "xterm" +local editor = os.getenv "EDITOR" or "nano" +local editor_cmd = terminal .. " -e " .. editor +local modkey = "Mod4" + +local my_awesome_menu = { + { + "hotkeys", + function() + hotkeys_popup.show_help(nil, awful.screen.focused()) + end, + }, + { "manual", terminal .. " -e man awesome" }, + { "edit config", editor_cmd .. " " .. awesome.conffile }, + { "restart", awesome.restart }, + { + "quit", + function() + awesome.quit() + end, + }, +} +local my_main_menu = awful.menu { + items = { + { "awesome", my_awesome_menu, beautiful.awesome_icon }, + { "open terminal", terminal }, + }, +} +local my_launcher = awful.widget.launcher { image = beautiful.awesome_icon, menu = my_main_menu } +menubar.utils.terminal = terminal -- Set the terminal for applications that require it + +tag.connect_signal("request::default_layouts", function() + awful.layout.append_default_layouts { + awful.layout.suit.floating, + awful.layout.suit.tile, + awful.layout.suit.tile.left, + awful.layout.suit.tile.bottom, + awful.layout.suit.tile.top, + awful.layout.suit.fair, + awful.layout.suit.fair.horizontal, + awful.layout.suit.spiral, + awful.layout.suit.spiral.dwindle, + awful.layout.suit.max, + awful.layout.suit.max.fullscreen, + awful.layout.suit.magnifier, + awful.layout.suit.corner.nw, + } +end) + +screen.connect_signal("request::wallpaper", function(s) + awful.wallpaper { + screen = s, + widget = { + { + image = beautiful.wallpaper, + upscale = true, + downscale = true, + widget = wibox.widget.imagebox, + }, + valign = "center", + halign = "center", + tiled = false, + widget = wibox.container.tile, + }, + } +end) + +local my_keyboardlayout = awful.widget.keyboardlayout() +local my_textclock = wibox.widget.textclock() + +screen.connect_signal("request::desktop_decoration", function(s) + awful.tag({ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + }, s, awful.layout.layouts[1]) + + s.my_prompt_box = awful.widget.prompt() + s.my_layoutbox = awful.widget.layoutbox { + screen = s, + buttons = { + awful.button({}, 1, function() + awful.layout.inc(1) + end), + awful.button({}, 3, function() + awful.layout.inc(-1) + end), + awful.button({}, 4, function() + awful.layout.inc(-1) + end), + awful.button({}, 5, function() + awful.layout.inc(1) + end), + }, + } + s.my_taglist = awful.widget.taglist { + screen = s, + filter = awful.widget.taglist.filter.all, + buttons = { + awful.button({}, 1, function(t) + t:view_only() + end), + awful.button({ modkey }, 1, function(t) + if client.focus then + client.focus:move_to_tag(t) + end + end), + awful.button({}, 3, awful.tag.viewtoggle), + awful.button({ modkey }, 3, function(t) + if client.focus then + client.focus:toggle_tag(t) + end + end), + awful.button({}, 4, function(t) + awful.tag.viewprev(t.screen) + end), + awful.button({}, 5, function(t) + awful.tag.viewnext(t.screen) + end), + }, + } + s.my_tasklist = awful.widget.tasklist { + screen = s, + filter = awful.widget.tasklist.filter.currenttags, + buttons = { + awful.button({}, 1, function(c) + c:activate { context = "tasklist", action = "toggle_minimization" } + end), + awful.button({}, 3, function() + awful.menu.client_list { theme = { width = 250 } } + end), + awful.button({}, 4, function() + awful.client.focus.byidx(-1) + end), + awful.button({}, 5, function() + awful.client.focus.byidx(1) + end), + }, + } + s.my_wibox = awful.wibar { + position = "top", + screen = s, + widget = { + layout = wibox.layout.align.horizontal, + { + layout = wibox.layout.fixed.horizontal, + my_launcher, + s.my_taglist, + s.my_prompt_box, + }, + s.my_tasklist, + { + layout = wibox.layout.fixed.horizontal, + my_keyboardlayout, + wibox.widget.systray(), + my_textclock, + s.my_layoutbox, + }, + }, + } +end) + +awful.mouse.append_global_mousebindings { + awful.button({}, 3, function() + my_main_menu:toggle() + end), + awful.button({}, 4, awful.tag.viewprev), + awful.button({}, 5, awful.tag.viewnext), +} + +awful.keyboard.append_global_keybindings { + awful.key({ modkey }, "s", hotkeys_popup.show_help, { description = "show help", group = "awesome" }), + awful.key({ modkey }, "w", function() + my_main_menu:show() + end, { description = "show main menu", 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" }), + awful.key({ modkey }, "x", function() + awful.prompt.run { + prompt = "Run Lua code: ", + textbox = awful.screen.focused().my_prompt_box.widget, + exe_callback = awful.util.eval, + history_path = awful.util.get_cache_dir() .. "/history_eval", + } + end, { description = "lua execute prompt", group = "awesome" }), + awful.key({ modkey }, "Return", function() + awful.spawn(terminal) + end, { description = "open a terminal", group = "launcher" }), + awful.key({ modkey }, "r", function() + awful.screen.focused().my_prompt_box:run() + end, { description = "run prompt", group = "launcher" }), + awful.key({ modkey }, "p", function() + menubar.show() + end, { description = "show the menubar", group = "launcher" }), +} + +awful.keyboard.append_global_keybindings { + awful.key({ modkey }, "Left", awful.tag.viewprev, { description = "view previous", group = "tag" }), + awful.key({ modkey }, "Right", awful.tag.viewnext, { description = "view next", group = "tag" }), + awful.key({ modkey }, "Escape", awful.tag.history.restore, { description = "go back", group = "tag" }), +} + +awful.keyboard.append_global_keybindings { + 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 }, "Tab", function() + awful.client.focus.history.previous() + if client.focus then + client.focus:raise() + end + end, { description = "go back", group = "client" }), + 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, "Control" }, "n", function() + local c = awful.client.restore() + -- Focus restored client + if c then + c:activate { raise = true, context = "key.unminimize" } + end + end, { description = "restore minimized", group = "client" }), +} + +awful.keyboard.append_global_keybindings { + 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({ 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" }), +} + +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) + +client.connect_signal("request::default_keybindings", function() + awful.keyboard.append_client_keybindings { + awful.key({ modkey }, "f", function(c) + c.fullscreen = not c.fullscreen + c:raise() + end, { description = "toggle fullscreen", group = "client" }), + awful.key({ modkey, "Shift" }, "c", 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 }, "t", function(c) + c.ontop = not c.ontop + end, { description = "toggle keep on top", group = "client" }), + awful.key({ modkey }, "n", function(c) + 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" }), + } +end) + +ruled.client.connect_signal("request::rules", function() + ruled.client.append_rule { + id = "global", + rule = {}, + properties = { + focus = awful.client.focus.filter, + raise = true, + screen = awful.screen.preferred, + placement = awful.placement.no_overlap + awful.placement.no_offscreen, + }, + } + ruled.client.append_rule { + id = "floating", + rule_any = { + -- cSpell:disable + instance = { "copyq", "pinentry" }, + class = { + "Arandr", + "Blueman-manager", + "Gpick", + "Kruler", + "Sxiv", + "Tor Browser", + "Wpa_gui", + "veromix", + "xtightvncviewer", + }, + name = { + "Event Tester", -- xev. + }, + role = { + "AlarmWindow", -- Thunderbird's calendar. + "ConfigManager", -- Thunderbird's about:config. + "pop-up", -- e.g. Google Chrome's (detached) Developer Tools. + }, + -- cSpell:enable + }, + properties = { floating = true }, + } + ruled.client.append_rule { + id = "titlebars", + rule_any = { type = { "normal", "dialog" } }, + properties = { titlebars_enabled = true }, + } +end) + +client.connect_signal("request::titlebars", function(c) + local buttons = { + awful.button({}, 1, function() + c:activate { context = "titlebar", action = "mouse_move" } + end), + awful.button({}, 3, function() + c:activate { context = "titlebar", action = "mouse_resize" } + end), + } + + awful.titlebar(c).widget = { + { + awful.titlebar.widget.iconwidget(c), + buttons = buttons, + layout = wibox.layout.fixed.horizontal, + }, + { + { + halign = "center", + widget = awful.titlebar.widget.titlewidget(c), + }, + buttons = buttons, + layout = wibox.layout.flex.horizontal, + }, + { + awful.titlebar.widget.floatingbutton(c), + awful.titlebar.widget.maximizedbutton(c), + awful.titlebar.widget.stickybutton(c), + awful.titlebar.widget.ontopbutton(c), + awful.titlebar.widget.closebutton(c), + layout = wibox.layout.fixed.horizontal(), + }, + layout = wibox.layout.align.horizontal, + } +end) + +ruled.notification.connect_signal("request::rules", function() + ruled.notification.append_rule { + rule = {}, + properties = { + screen = awful.screen.preferred, + implicit_timeout = 5, + }, + } +end) + +naughty.connect_signal("request::display", function(n) + naughty.layout.box { notification = n } +end) + +client.connect_signal("mouse::enter", function(c) + c:activate { context = "mouse_enter", raise = false } +end)