307 lines
7.1 KiB
Lua
307 lines
7.1 KiB
Lua
local awful = require("awful")
|
|
local glib = require("lgi").GLib
|
|
local gears = require("gears")
|
|
local akeygrabber = require("awful.keygrabber")
|
|
local unpack = unpack or table.unpack -- luacheck: globals unpack
|
|
local awesome, keygrabber, client, root = awesome, keygrabber, client, root
|
|
|
|
local M = {}
|
|
|
|
local conversion = nil
|
|
|
|
local function generate_conversion_map()
|
|
if conversion then
|
|
return nil
|
|
end
|
|
|
|
local mods = awesome._modifiers
|
|
assert(mods)
|
|
|
|
conversion = {}
|
|
|
|
for mod, keysyms in pairs(mods) do
|
|
for _, keysym in ipairs(keysyms) do
|
|
assert(keysym.keysym)
|
|
conversion[mod] = conversion[mod] or keysym.keysym
|
|
conversion[keysym.keysym] = mod
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function stop_keygrabber()
|
|
local instance = akeygrabber.current_instance
|
|
if instance then
|
|
instance:stop()
|
|
end
|
|
|
|
akeygrabber.stop()
|
|
end
|
|
|
|
local function run_key_sequence(seq)
|
|
for _, s in ipairs(seq) do
|
|
if s.action == "press" then
|
|
root.fake_input("key_press", s.key)
|
|
elseif s.action == "release" then
|
|
root.fake_input("key_release", s.key)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function run_key_sequence_xdotool(seq)
|
|
local run_fn = function(s)
|
|
if s.action == "press" then
|
|
awful.spawn("xdotool keydown " .. s.key)
|
|
elseif s.action == "release" then
|
|
awful.spawn("xdotool keyup " .. s.key)
|
|
end
|
|
end
|
|
|
|
for _, s in ipairs(seq) do
|
|
run_fn(s)
|
|
end
|
|
end
|
|
|
|
-- get key sequence to transition from current mods to next mods
|
|
local function change_mods(current, next)
|
|
local sequence = {}
|
|
local intersect = {}
|
|
|
|
-- determine mods that needs to be released
|
|
for _, c in ipairs(current) do
|
|
local is_unique = true
|
|
for _, n in ipairs(next) do
|
|
if string.upper(c) == string.upper(n) then
|
|
is_unique = false
|
|
break
|
|
end
|
|
end
|
|
if is_unique then
|
|
table.insert(sequence, { action = "release", key = c })
|
|
else
|
|
intersect[#intersect + 1] = c
|
|
end
|
|
end
|
|
|
|
-- determine mods that needs to be pressed
|
|
for _, n in ipairs(next) do
|
|
local is_unique = true
|
|
for _, i in ipairs(intersect) do
|
|
if string.upper(n) == string.upper(i) then
|
|
is_unique = false
|
|
break
|
|
end
|
|
end
|
|
if is_unique then
|
|
table.insert(sequence, { action = "press", key = n })
|
|
end
|
|
end
|
|
|
|
return sequence
|
|
end
|
|
|
|
local function dump_sequence(seq)
|
|
local str = "{ "
|
|
for _, s in ipairs(seq) do
|
|
str = string.format("%s%s %s, ", str, s.action, s.key)
|
|
end
|
|
str = str .. "}"
|
|
return str
|
|
end
|
|
|
|
local function new(args)
|
|
local cfg = args or {}
|
|
|
|
local mod = cfg.mod or "Mod4"
|
|
local mod_keysym = cfg.mod_keysym
|
|
|
|
local up = cfg.up or { "k", "Up" }
|
|
local down = cfg.down or { "j", "Down" }
|
|
local left = cfg.down or { "h", "Left" }
|
|
local right = cfg.down or { "l", "Right" }
|
|
|
|
local focus = cfg.focus or awful.client.focus.global_bydirection
|
|
local dont_restore_mods = cfg.dont_restore_mods
|
|
local debug = cfg.debug
|
|
|
|
local regexpr_dynamic_vim = cfg.regexpr_dynamic_vim or "%- N?VIM$"
|
|
local regexpr_dynamic_tmux = cfg.regexpr_dynamic_tmux or "%- TMUX$"
|
|
|
|
local mods = mod_keysym and { mod_keysym } or generate_conversion_map()
|
|
|
|
local no_global_keybinds = cfg.no_global_keybinds
|
|
|
|
local wm_keys = {
|
|
mods = mods,
|
|
up = up,
|
|
down = down,
|
|
left = left,
|
|
right = right,
|
|
}
|
|
|
|
local use_xdotool = cfg.use_xdotool
|
|
local use_pstree = cfg.use_pstree or cfg.experimental -- experimental is a legacy parameter
|
|
|
|
local tmux_keys = cfg.tmux
|
|
or {
|
|
mods = { "Control_L" },
|
|
up = "Up",
|
|
down = "Down",
|
|
left = "Left",
|
|
right = "Right",
|
|
}
|
|
|
|
local vim_keys = cfg.vim or {
|
|
mods = { "Control_L" },
|
|
left = "h",
|
|
down = "j",
|
|
up = "k",
|
|
right = "l",
|
|
}
|
|
|
|
local get_key_sequence = function(wm_mods, app_mods, fn, dir)
|
|
if not wm_mods then
|
|
-- detect active wm mods
|
|
local active = awesome._active_modifiers
|
|
wm_mods = {}
|
|
for _, m in ipairs(active) do
|
|
---@diagnostic disable-next-line: need-check-nil
|
|
local modsym = conversion[m]
|
|
if modsym ~= "Num_Lock" or modsym ~= "Caps_Lock" then
|
|
table.insert(wm_mods, modsym)
|
|
if debug then
|
|
debug("mod detection: " .. modsym)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local sequence = {}
|
|
-- release wm mods, press vim/tmux mods
|
|
gears.table.merge(sequence, change_mods(wm_mods, app_mods))
|
|
|
|
-- press navigation direction
|
|
gears.table.merge(sequence, fn(dir))
|
|
|
|
-- release vim/tmux mods, restore wm mods
|
|
local restore_mods = dont_restore_mods and {} or wm_mods
|
|
gears.table.merge(sequence, change_mods(app_mods, restore_mods))
|
|
|
|
return sequence
|
|
end
|
|
|
|
local navigate_tmux = function(dir)
|
|
return {
|
|
{ action = "release", key = tmux_keys[dir] },
|
|
{ action = "press", key = tmux_keys[dir] },
|
|
{ action = "release", key = tmux_keys[dir] },
|
|
}
|
|
end
|
|
|
|
local navigate_vim = function(dir)
|
|
return {
|
|
{ action = "release", key = vim_keys[dir] },
|
|
{ action = "press", key = vim_keys[dir] },
|
|
{ action = "release", key = vim_keys[dir] },
|
|
}
|
|
end
|
|
|
|
local run_fn = function(seq)
|
|
stop_keygrabber()
|
|
if use_xdotool then
|
|
run_key_sequence_xdotool(seq)
|
|
else
|
|
run_key_sequence(seq)
|
|
end
|
|
end
|
|
|
|
-- use dynamic titles to determine type of client (default)
|
|
local navigate = function(dir)
|
|
local c = client.focus
|
|
local client_name = c and c.name or ""
|
|
|
|
if string.find(client_name, regexpr_dynamic_vim) then
|
|
local seq = get_key_sequence(wm_keys.mods, vim_keys.mods, navigate_vim, dir)
|
|
run_fn(seq)
|
|
if debug then
|
|
debug(string.format("VIM(%s): %s", dir, dump_sequence(seq)))
|
|
end
|
|
return
|
|
elseif string.find(client_name, regexpr_dynamic_tmux) then
|
|
local seq = get_key_sequence(wm_keys.mods, tmux_keys.mods, navigate_tmux, dir)
|
|
run_fn(seq)
|
|
if debug then
|
|
debug(string.format("TMUX(%s): %s", dir, dump_sequence(seq)))
|
|
end
|
|
return
|
|
else
|
|
focus(dir)
|
|
if debug then
|
|
debug(string.format("WM(%s)", dir))
|
|
end
|
|
return
|
|
end
|
|
end
|
|
|
|
-- use pstree to determine type of client (experimental)
|
|
if use_pstree then
|
|
navigate = function(dir)
|
|
local c = client.focus
|
|
local pid = c and c.pid or -1
|
|
awful.spawn.easy_async("pstree -A -T " .. pid, function(out)
|
|
if string.find(out, "[^.*\n]%-tmux: client") then
|
|
run_fn(get_key_sequence(wm_keys.mods, tmux_keys.mods, navigate_tmux, dir))
|
|
return
|
|
elseif
|
|
string.find(out, "[^.*\n]%-n?vim$")
|
|
or string.find(out, "[^.*\n]%-n?vim%-")
|
|
or string.find(out, "^neovide%-%-%-nvim")
|
|
or string.find(out, "^gvim$")
|
|
or string.find(out, "^gvim%-")
|
|
then
|
|
run_fn(get_key_sequence(wm_keys.mods, vim_keys.mods, navigate_vim, dir))
|
|
return
|
|
else
|
|
focus(dir)
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
-- register signals
|
|
awesome.connect_signal("navigator::focus", focus)
|
|
awesome.connect_signal("navigator::navigate", navigate)
|
|
|
|
-- add to module
|
|
M.focus = focus
|
|
M.navigate = navigate
|
|
|
|
-- setup keybinds
|
|
if not no_global_keybinds then
|
|
glib.idle_add(glib.PRIORITY_DEFAULT_IDLE, function()
|
|
local aw = {}
|
|
for k, v in pairs(cfg) do
|
|
for _, dir in pairs({ "left", "right", "up", "down" }) do
|
|
if k == dir then
|
|
for _, key_name in ipairs(v) do
|
|
aw[#aw + 1] = awful.key({ mod }, key_name, function()
|
|
navigate(k)
|
|
end, { description = "navigate " .. k, group = "client" })
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
root.keys(awful.util.table.join(root.keys(), unpack(aw)))
|
|
end)
|
|
end
|
|
|
|
return M
|
|
end
|
|
|
|
return setmetatable(M, {
|
|
__call = function(_, ...)
|
|
return new(...)
|
|
end,
|
|
})
|