intrntbrn / modalisa

hybrid modal keymap framework for awesomewm
MIT License
14 stars 0 forks source link
awesome awesomewm hotkey keybind keygrabber leader modal picker plugin sequence vim whichkey

โŒจ๏ธ modalisa ๐ŸŽจ

modalisa is a hybrid modal keymap framework for AwesomeWM. It combines the best of both traditional and modal input modes while also providing a visually appealing interface.

Traditional input modes in tiling window managers often require complex keymaps that involve pressing multiple modifiers simultaneously due to limited keyspace. Additionally, users have to rely on their memory or unpractical cheatsheets as there are no key suggestions while typing. On the other hand, modal input modes allow for easy entry of key sequences but can become cumbersome when repeating actions. Users either have to repeat the entire sequence or specify a count beforehand, making the process inefficient.

modalisa addresses these shortcomings by providing a flexible hybrid approach. It keeps track of modifier states to provide distinct input modes that can be tailored to the user's needs for each key and at every stage of the sequence individually. It allows for traditional keybinds for common operations that can be repeated by holding down the modifier, while also enabling users to tap the modifier (like a leader key) to enter modal mode followed by a key sequence for less common commands. This flexibility makes it possible to transition into a more modal-esque keymap gradually without having to relearn most of the AwesomeWM controls.

In addition, modalisa comes with a preconfigured default keymap that includes improved default AwesomeWM controls (and a lot more) to provide a starting point and to give an overview about the possibilities using this framework.

Which Key Hints AwesomeWM Controls System Controls
Client Labels Mouse Menu File Picker

โœจ Features

๐Ÿ’ก Example

{
    ["t"] = { desc = "tag", opts = { mode = "hybrid", hints = { enabled = true } } },
    -- opts are inherited from predecessor t
    ["t<Tab>"] = {
        desc = "view last tag",
        fn = function(opts)
            awful.tag.history.restore()
        end,
    },
    ["tD"] = {
        desc = "delete current tag",
        opts = { hints = { placement = "centered" } },
        cond = function()
            return awful.screen.focused().selected_tag.index > 1
        end,
        fn = function(opts)
            local dynamic_menu = {
                y = {
                    desc = function()
                        return "yes, delete tag " .. awful.screen.focused().selected_tag.index
                    end,
                    highlight = { bg = "#FF0000", desc = { bold = true } },
                    function(opts)
                        awful.screen.focused().selected_tag:delete()
                    end,
                },
                n = { desc = "no, cancel delete" },
            }
            return dynamic_menu
        end,
    },
}

โš ๏ธ Warning

modalisa is in early development and breaking changes might occur.

๐Ÿ“‹ Requirements

๐Ÿ“ฆ Installation

  1. Clone the repo:

git clone https://github.com/intrntbrn/modalisa ~/.config/awesome/modalisa

  1. Import and configure the module in rc.lua:
-- NOTE:
-- modalisa is designed to be used in conjunction with a modifier as the leader
-- key (e.g. Super_L), but this prevents all current keybinds utilizing
-- that modifier from functioning.
require("modalisa").setup({
    -- root_keys = { "<M-a>" }, -- "Mod4" + "a"
    root_keys = { "<Super_L>" }, -- or "<Alt_L>"
    back_keys = { "<BackSpace>" },
    stop_keys = { "<Escape>" },
    mode = "hybrid",
    include_default_keys = true,
    -- try the auto-generated theme first
    theme = {
        -- fg = "#eceffc",
        -- bg = "#24283B",
        -- grey = "#959cbc",
        -- border = "#444A73",
        -- accent = "#82AAFF",
    },
})

Create Your Own Keymap

  1. Copy the default keymap keys.lua as modalisa_keys.lua into the home AwesomeWM directory:

cp ~/.config/awesome/modalisa/keys.lua ~/.config/awesome/modalisa_keys.lua

  1. Disable the default keymap by setting include_default_keys = false during setup.

  2. Edit the copy. Restart AwesomeWM for the changes to take effect.

You can retrieve keynames by running the command

awesome-client "awesome.emit_signal('modalisa::showkey')"

from the terminal. Press any key combination to show the respective keyname.

โš™๏ธ Configuration

Most configuration options can be explored interactively by pressing i on the default keymap.

Default Settings ```lua { root_keys = { "" }, back_keys = { "" }, stop_keys = { "" }, toggle_keys = { "." }, include_default_keys = true, mode = "hybrid", -- "modal" | "hold" | "hybrid" | "forever" smart_modifiers = true, -- like smartcase but for all root key modifiers stop_on_unknown_key = false, timeout = 0, -- ms labels = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@\\^_`{|}~", ignore_shift_state_for_special_characters = true, toggle_false = "on", -- "๏€Œ", "๏ˆ…", "toggle", "on" toggle_true = "off", -- "๏™•", -"๏ˆ„", "toggle", "off" theme = { fg = beautiful.fg_focus or "#eceffc", bg = beautiful.bg_focus or "#24283B", grey = beautiful.fg_normal or "#959cbc", border = beautiful.border_color_normal or "#444A73", accent = "#82AAFF", }, hints = { enabled = true, delay = 0, -- ms show_header = false, show_disabled_keys = true, low_priority = true, -- generate hints when idling sort = "group", -- group | id | key | none mouse_button_select = 1, -- left click mouse_button_select_continue = 3, -- right click mouse_button_stop = 2, -- middle click mouse_button_back = 8, -- back click color_border = nil, color_odd_bg = -8, -- color or luminosity delta color_hover_bg = 15, -- color or luminosity delta color_disabled_fg = nil, font_header = "Monospace 12", color_header_fg = nil, color_header_bg = nil, highlight = { bg = nil, key = { font = "Monospace 12", }, desc = { font = "Monospace 12", italic = true, }, separator = { font = "Monospace 12", }, }, menu_highlight = { desc = { bold = true, }, }, group_highlights = { -- ["^awesome"] = { -- desc = { -- underline = true, -- }, -- }, }, separator = " โžœ ", entry_key_width = 5, -- chars min_entry_width = 25, -- chars max_entry_width = 30, -- chars entry_padding = { top = 0, bottom = 0, left = 0, right = 0, }, padding = { top = 0, bottom = 0, left = 0, right = 0, }, margin = { top = 0, bottom = 0, left = 0, right = 0, }, width = 0.75, -- fraction or abs pixel count height = 0.35, -- fraction or abs pixel count stretch_vertical = false, -- use all available height stretch_horizontal = false, -- use all available width flow_horizontal = false, -- fill from left to right expand_horizontal = true, -- use all available columns first placement = function(h) -- function, placement (e.g. "centered") or false (last position) awful.placement.bottom(h, { honor_workarea = true }) end, border_width = beautiful.border_width or dpi(1), opacity = 1, shape = nil, odd_style = "row", -- row | column | checkered | none odd_empty = true, -- continue odd pattern for empty entries key_aliases = { [" "] = "space", Left = "โ†", Right = "โ†’", ["^Up"] = "โ†‘", ["[%-]Up"] = "โ†‘", ["^Down"] = "โ†“", ["[%-]Down"] = "โ†“", XF86MonBrightnessUp = "๓ฐƒ +", XF86MonBrightnessDown = "๓ฐƒ -", XF86AudioRaiseVolume = "๓ฐ", XF86AudioLowerVolume = "๓ฐž", XF86AudioMute = "๓ฐŸ", XF86AudioPlay = "๓ฐŠ", XF86AudioPrev = "๓ฐ’ฎ", XF86AudioNext = "๓ฐ’ญ", XF86AudioStop = "๓ฐ“›", }, }, echo = { enabled = true, show_percentage_as_progressbar = false, -- display 0-1.0 as progressbar placement = "centered", -- or any awful.placement func timeout = 1000, -- ms align_vertical = true, -- key above value vertical_layout = false, -- kvs from top to bottom sort = true, entry_width = 20, -- chars entry_width_strategy = "exact", -- min | max | exact padding = { top = dpi(3), bottom = dpi(3), left = dpi(3), right = dpi(3), }, spacing = 0, border_width = beautiful.border_width or dpi(1), shape = nil, opacity = 1, color_border = nil, highlight = { key = { font = "Monospace 20", bg = nil, fg = nil, italic = true, bold = true, }, value = { font = "Monospace 20", bg = nil, fg = nil, }, }, progressbar = { shape = gears.shape.rounded_rect, bar_shape = gears.shape.rounded_rect, border_width = dpi(2), bar_border_width = dpi(2), color = nil, background_color = nil, border_color = nil, bar_border_color = nil, margin = { left = dpi(8), right = dpi(8), top = dpi(8), bottom = dpi(8), }, padding = { left = 0, right = 0, top = 0, bottom = 0, }, opacity = 1, }, }, prompt = { placement = "centered", -- or any awful.placement func vertical_layout = true, -- from top to bottom width = 20, -- chars width_strategy = "min", -- min | max | exact padding = { top = dpi(5), bottom = dpi(5), left = dpi(5), right = dpi(5), }, spacing = 0, border_width = beautiful.border_width or dpi(1), shape = nil, opacity = 1, color_border = nil, header_highlight = { font = "Monospace 20", fg = nil, bg = nil, bold = true, italic = true, }, font = "Monospace 20", color_bg = nil, color_fg = nil, color_cursor_fg = nil, color_cursor_bg = nil, }, label = { shape = gears.shape.rounded_rect, border_width = beautiful.border_width or dpi(1), color_border = nil, width = dpi(100), height = dpi(100), opacity = 1, highlight = { font = "Monospace 40", bg = nil, fg = nil, bold = true, }, }, awesome = { auto_select_the_only_choice = false, resize_delta = dpi(32), resize_factor = 0.025, wallpaper_dir = os.getenv("HOME") .. "/.config/awesome/", browser = "firefox || chromium || google-chrome-stable || qutebrowser", terminal = terminal or "alacritty || kitty || wezterm || st || urxvt || xterm", app_menu = "rofi -show drun || dmenu_run", }, } ```

๐Ÿ“– Documentation

๐ŸŒฒTree

Keymaps are organized and represented using hierarchical tree structures. Each key is stored as a node in the tree. When a character is input, the tree is traversed to find the corresponding key. If a node has been found and there are no possible successors (leaf), the key's function is executed. Users can also use a specific key (by default, BackSpace) to go back a level in the tree.

The tree structure implements property inheritance, which means that options (opts) defined at a higher level in the tree will be inherited by the successor keys, unless overridden. This provides a convenient way to define common options for groups of keys without repeating them for each individual key.

By default, the keymap is stored in the root tree, which is accessed by pressing the keybind defined in root_keys during setup. However, it is possible to create additional trees if needed.

โœจ Modes

Mode Description
hold Run until the (last remaining) modifier has been released.
modal Run until a command has been executed (by entering a valid key sequence).
hybrid Run until a command has been executed and the (last remaining) modifier has been released.
forever Run indefinitely until explicitly stopped (by pressing a stop key).

Please note that the exact behaviour is also dependent on other config parameters. When stop_on_unknown_key is active and an unknown key has been input, operation will stop regardless of the current mode. Operation will also always stop when a timeout (no valid input within a timeframe) occurs. The keys itself can also override the behaviour by explicitly setting a continue flag.

๐Ÿ”‘ Key

Keys can be configured by using the following properties:

Property Type Description
(seq) string The sequence of keys to be pressed.
(fn) function(opts, tree) The function to be executed when the key sequence has been entered. If the key has successors (is a menu) it will only be executed if a timeout occurs (similar to vim). Successor keys can get dynamically created by returning a table containing the key definitions.
desc string or function Provides a brief description of the key.
opts table Specifies custom options for the key that will be merged with options from predecessors.
cond function(opts) A condition that determines whether the key is active (nil means it is always active)
group string Assigns the key to a group, used for sorting purposes.
global string or boolean Creates a global keybinding in AwesomeWM to run the key's function (e.g. "\<M-a>"). If set to true, the key sequence will be used as the global keybinding.
continue boolean Forces continuation after executing the key's function regardless of the current input mode.
hidden boolean Hides the key in hints.
highlight table Custom attributes to display the key in hints (font, fg, bg, bold, italic, underline, strikethrough, etc.)
is_menu boolean Marks the key explicitly as a menu, even if it has no successsors (purely cosmetic, used for dynamic menus).
temp boolean Marks the key as temporary, indicating that it is dynamically created and only available for a single use.
pre function(opts, tree) The function to be executed before the main function fn.
post function(opts, tree) The function to be executed after the main function fn.
on_enter function(opts, tree) The function to be executed when entering a menu (non-leaf node).
on_leave function(opts, tree) The function to be executed when leaving a menu (non-leaf node).
result table Specifies the results or notifications to be shown using echo after executing the key's function (e.g. { volume = 0.5 }).

๐Ÿ–Œ๏ธ Custom UI

One of the design principles is to fully isolate core functionality from UI. Every UI building block (echo, prompt, label) can be disabled individually allowing users to build custom versions by solely implementing signal handlers.

To implement custom UI please refer to the sourcecode.

๐Ÿ“ก API

Todo.

๐Ÿ’ก More Examples

Client Menu ```lua local function client_menu(c) if not c then return end local pc = require("modalisa.presets.client") local opts = { stop_on_unknown_key = true, hints = { enabled = true, sort = "id", show_header = true, delay = 0, fill_remaining_space = false, placement = function(x) awful.placement.under_mouse(x) awful.placement.no_offscreen(x) end, flow_horizontal = false, stretch_vertical = false, stretch_horizontal = false, expand_horizontal = false, width = 0.3, height = 0.3, show_disabled_keys = false, min_entry_width = 11, max_entry_width = 11, entry_key_width = 0, entry_padding = { left = dpi(10), }, separator = "", }, } local function chrome_tabbar(cl) return { "x", desc = "tabbar toggle", cond = function() return string.find(cl.class, ".*chrome.*") end, fn = function() keygrabber.stop() root.fake_input("key_press", "F11") awful.spawn.easy_async_with_shell("sleep 0.15", function() cl.fullscreen = false end) root.fake_input("key_release", "F11") end, } end local list = { pc.kill(c) + { "k", desc = "kill", highlight = { bg = "#F7768E" } }, pc.minimize(c) + { "n" }, pc.toggle_property("fullscreen", c, true) + { "f" }, pc.toggle_property("maximized", c, true) + { "m" }, pc.toggle_property("floating", c) + { "o", desc = "floating" }, pc.move_to_tag_menu(c) + { "t", desc = "send to tag โžœ", opts = { hints = { placement = "no_offscreen" } } }, pc.unminimize_menu(false) + { "u", desc = "unminimize โžœ", opts = { hints = { placement = "no_offscreen" } } }, chrome_tabbar(c), } require("modalisa").fake_input("stop") require("modalisa").run_tree(list, opts, c.name or "") end ```
Tag Menu ```lua local function tag_menu(t) local mt = require("modalisa.presets.tag") local opts = { stop_on_unknown_key = true, hints = { enabled = true, sort = "id", show_header = true, delay = 0, fill_remaining_space = false, placement = function(x) awful.placement.under_mouse(x) awful.placement.no_offscreen(x) end, flow_horizontal = false, stretch_vertical = false, stretch_horizontal = false, expand_horizontal = false, width = 0.3, height = 0.3, show_disabled_keys = false, min_entry_width = 15, -- chars max_entry_width = 15, -- chars entry_key_width = 0, -- chars entry_padding = { left = dpi(10), }, separator = "", }, } local list = { mt.new_tag_copy("") + { "n", desc = "new tag", highlight = { desc = { fg = "#94CF95" } } }, mt.layout_select_menu(t) + { " ", desc = "layout โžœ", opts = { hints = { placement = "no_offscreen" } } }, mt.rename(t) + { "r", desc = "rename", opts = { hints = { placement = "no_offscreen" } } }, mt.set_gap(t) + { "g", desc = "gap" }, mt.master_width_increase(t) + { "w", desc = "master_width +", continue = false }, mt.master_width_decrease(t) + { "W", desc = "master_width -", continue = false }, mt.master_count_increase(t) + { "m", desc = "master_count +", continue = false }, mt.master_count_decrease(t) + { "M", desc = "master_count -", continue = false }, mt.move_all_clients_to_tag_menu(t) + { "a", desc = "move all to โžœ", opts = { hints = { placement = "no_offscreen" } } }, mt.move_tag_to_screen_menu(t) + { "s", desc = "move to screen โžœ", opts = { hints = { placement = "no_offscreen" } } }, mt.delete(t) + { "D", desc = "delete tag", highlight = { desc = { fg = "#F7768E" } } }, } require("modalisa").fake_input("stop") require("modalisa").run_tree(list, opts, t.name or t.index or "") end ```