Alan North
Posted on July 26, 2023
I've been recently giving neovim a serious try (this is my fourth/fifth attempt). And I think this time it's going to work out, but I really miss Modifier+Number shortcuts.
Usually you cannot map to modifies + number keys in in neovim (e.g. Ctrl+1). You can try to map the raw key sequences but this is a pain and does not work for all numbers.
The best way to get around this is to map to an obscure keybinding then make the terminal remap Mod+Number to those odd keybindings. I never got around to actually doing this usually because most terminal's configs are static it's also often a pain to remap keys (e.g. kitty, I like you, but...)
Also the ideal way to do this is to map them to a key chain like {ObscureCombo} {Mods}+{Remap}
so as to not take up useful keys. But that's even more painful.
Recently though I tried and switched to wezterm for my terminal emulator, and it's configured through lua which makes this super easy to do for all sequences.
For example, I picked Ctrl+Alt+Shift+F12
for the obscure key combo and letters for the remap part. So Ctrl+1
would map to Ctrl+Alt+Shift+F12 Ctrl+q
.
The letter mapping is to avoid breaking anything. If we mapped them to, for example, the F keys again, we might accidentally shoot ourselves in the foot when pressing Alt+4
.
Note that we also can't map the numbers without the modifiers, since wezterm has no way to know if we're in normal mode or not, and we would be left unable to type numbers if we did so.
Here's my wezterm function for remapping with all mods.
-- Allows using Mods+Number in the receiving program if it does not support it.
-- In the receiving program, the keymap needs to be `<C-A-S-F12> <Mods-F{q-p}>`
local remapModNumber = function(keyConfig, modsToRemap)
-- it's not <Mods-F{Number}> for obvious reasons (i.e. A-F4)
local numMap = {
["1"] = "q",
["2"] = "w",
["3"] = "e",
["4"] = "r",
["5"] = "t",
["6"] = "y",
["7"] = "u",
["8"] = "i",
["9"] = "o",
["0"] = "p",
}
local createModNumberRemap = function(key, mods)
local res = {
key = key,
mods = mods,
action = act.Multiple({
--obscure key to use as base
act.SendKey({ key = "F12", mods = "CTRL|SHIFT|ALT" }),
act.SendKey({ key = numMap[key], mods = mods }),
}),
}
return res
end
print(keyConfig)
for i = 0, 9 do
for _, mod in ipairs(modsToRemap) do
-- cant insert normally???
keyConfig[#keyConfig + 1] = createModNumberRemap(tostring(i), mod)
end
end
end
config.keys = {
-- ...your other keybindings
}
remapModNumber(config.keys, { "CTRL", "SHIFT", "ALT", "CTRL|ALT", "ALT|SHIFT", "CTRL|SHIFT", "ALT|CTRL", "CTRL|SHIFT|ALT" })
Then in vim, if we wanted to use <C-1>
we would map to <C-S-A-F12><C-q>
.
I've also added a small helper function in my local utils module to help with this. I might extract this later into a plugin.
local M = {}
local config = {
baseSequence = "<C-S-A-F12>",
}
M.setup = function(opts)
opts = opts or {}
vim.tbl_extend("force", config, opts)
end
local replacements = {
["1"] = "q",
["2"] = "w",
["3"] = "e",
["4"] = "r",
["5"] = "t",
["6"] = "y",
["7"] = "u",
["8"] = "i",
["9"] = "o",
["0"] = "p",
}
M.mapNum = function(keysString)
local str = ""
for i = 1, #keysString do
local prevPrevChar = i > 2 and keysString:sub(i - 2, i - 2) or nil
local prevChar = i > 1 and keysString:sub(i - 1, i - 1) or nil
local char = keysString:sub(i, i)
local isNumber = tonumber(char) ~= nil
local prevIsNumber = prevChar ~= nil and tonumber(prevChar) ~= nil
local prevIsAllowed = prevChar == nil or prevChar ~= "F" or prevIsNumber
local prevPrevIsAllowed = prevPrevChar == nil or prevPrevChar ~= "F"
if isNumber and prevIsAllowed and prevPrevIsAllowed then
str = str .. replacements[char]
else
str = str .. char
end
end
return config.baseSequence .. str
end
return M
It can then be used like so:
local mn = require("utils").mapNum
vim.keymap.set("n", mn("<C-1>"), ...)
-- will not remap F1
vim.keymap.set("n", mn("<C-F1>"), ...)
How to get a local module to actually load using lazy.nvim is out of scope for this post, but I might write one on that later because it was not very straightforward.
Posted on July 26, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.