Skip to content

Latest commit

 

History

History
624 lines (551 loc) · 22.8 KB

README.md

File metadata and controls

624 lines (551 loc) · 22.8 KB

⌨️ 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

  • 🪄 Unique input modes
  • 💥 Which key hints with mouse support
  • 🌳 Keymap trees with property inheritance
  • 💄 UI building blocks (prompt, echo, labels)
  • 🌐 Define options for every key individually
  • 🪟 Preconfigured AwesomeWM and system keymap
  • 🧙 Use vim syntax to define key sequences
  • ♿ Easy and intuitive configuration
  • 📡 Highly extensible and customizable (200+ parameters)

💡 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

  • awesome-git
  • nerd-font (optional)

📦 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
{
	root_keys = { "<M-a>" },
	back_keys = { "<BackSpace>" },
	stop_keys = { "<Escape>" },
	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
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
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