Better aliases for modifiers
al-ce opened this issue · 21 comments
g
for command
(gui) and a
for option
(alt) are ok, but h
for shift
and r
for control
look bad in the configuration. l
for caps_lock
isn't great either. I don't likem
/m
for left/right_command
mainly because in Vim M
is used for Meta
as a modifier, and on Mac that's equivalent to the option keys.
The lower/upper-case system for left/right modifiers seemed like a good solution for keeping modifiers down to a single letter but it can be hard to distinguish them, especially o
and O
, and I've found myself wondering why some mapping wasn't triggering because I didn't notice this.
Needs improvement.
Personally, I think the ideal aliases for me would be to just use the unicode modifier/key symbols directly, ⌘, ⌥, ⌃, ⇧, ⎋, ⇥ and so on. I also personally use ☆ for Hyper. I use the actual symbols all the time in a bunch of different contexts and I have Typinator snippets that make them very quick to insert.
This was requested and discussed in an issue for GokuRakuJoudo and implemented in a fork by eugenesvk. I also like how left and right options were implemented just using the ‹⌘
and ⌘›
syntax. as documented here.
I definitely prefer your implementation of a config using YAML over EDN, I'd likely switch over if you added this type of symbol aliases or I might fork it and work on that myself when I have a little more free time.
Thanks for the link to that PR! I really like that system of modifiers, I should have done more research before I implemented the current one. I lazily defaulted to using vim-like syntax because it was familiar.
- Adding the unicode characters to the alias list into the current system is simple, at least for: ⌘, ⌥, ⌃, ⇧.
/base/:
<⌘⇧-e>: shnotify(==HELLO==)
This translates to
"from": { "key_code": "e", "modifiers": { "mandatory": [ "command", "shift" ] } },
"to": [ { "shell_command": "osascript -e 'display notification \"==HELLO==\"'" } ],
I used the 'side-neutral' modifiers rather than 'left_' or 'right_'.
I already pushed this but I don't expect this to meet the standards for your request. I'd like to consider doing an overhaul of the modifier-syntax if you're willing to answer a few questions to guide me. I'm typing this up real quick before I have to leave the house but I'll do more research in the goku repo and the fork you linked later. Thanks again for the comment!
Yeah, absolutely, however I can help!
I was going to write a whole discourse on my thought process, but no one should be asked to read that. So, here's some off the cuff ideas. I'm pretty sure I could implement this in one day (other than updating guides and README and updating tests) and have all these as valid options. I might work on this in a dev branch just to see if I can get some foresight for some issues that might come up in a bigger overhaul.
/base/:
<cosm-s>: string(git status) # Current syntax
cosm-s: string(git status)
cOSm-s: string(git status ) # Still got the case-sensitivity to indicate sides
rahg-s: string(git status ) # and the bad side-neutral aliases
⌃⌥⇧⌘-s: string(git status)
‹⌃⌥›⇧⌘›-s: string(git status)
⌃ ⌥ ⇧ ⌘ | s: string(git status) # This would help some of the visual clutter
# We would lose the pipe alias '|' but we can add 'pipe'
‹⌃ ⌥› ⇧ ⌘› | s: string(git status)
‹⌃ ⌥› ⇧ ⌘› | s + t + a: string(git status) # For simultaneous 'from' events
I need to do a little more work to figure out how to get aliases for a multi-modifier like hyper as a modifier (I should list this in the list of limitations/bugs, but the current alias hyper
only works one way, e.g. if you want to map escape
to hyper, you can, but you can't use it as an alias like <hyper-s>
)
Also, I eventually want to have some defaults (the four unicode symbols + some predefined 'alphabet soup' to quote @eugenesvk) and then let the user add a dictionary in their YAML config to set their own aliases, unicode or otherwise. Not just for modifiers but for any key.
Something like this, anywhere in the .yaml file:
aliases:
⨁: left_control + right_option
☀︎: brightness_up
# etc.
But I'm not attached to any of this, I'm mainly just thinking out loud and trying to create a roadmap. Would you mind sharing if you have a certain syntax in mind and any criticism you have of the above examples? This is just a playground plan
As of 1c359f7, the syntax I mentioned above should work, but there's a good chance I missed something. Also, ☆ works as a hyper alias - you can use it as a modifier or map other keys to it.
The syntax that I don't love is requiring +
to concatenate keys for simultaneous 'from' events. Maybe I'll tackle that in the future. Also, there's too many options for modifier syntax now, though they each have their advantages.
Didn't lose the pipe alias for |
like I thought I would. The new regex allows it without issue. The lesson learned is that this entire program should have been written as 90% regex functions.
I'm open to radical changes and innovations and reverting this commit if we settle on something cleaner.
TODO: update README, guides, and sample configs, and add some more tests. Priority: add functions to catch user error. e.g. right now, adding an invalid char that's not in the UNICODE_MODS dict in the 'from' part of the mapping raises a KeyError. Sleep first!
As of 28f26af, user-defined aliases are available, but not for modifiers.
So, you can define an alias to remap a key to a modifier combo, e.g.
aliases:
⨁:
- right_option
- [left_control]
super_tab:
- tab
- [left_option, left_control, left_shift]
force_push: shell(git push --force origin master)
But you can only use the alias in the primary-key 'field', not the modifier field.
/base/:
insert: ⨁ # This is valid
⨁ | g: string(git init) + CR # This is not (for now)
With the example above, you could have a mapping like:
/base/:
⨁ + g: string(git init) + CR
But that's strictly a simultaneous from event, so you'd be forced to execute it within the simultaneous threshold.
With the latest commit, if an alias
YAML mapping exists, the program updates the existing dictionary of Alias objects before it starts interpreting layers. An Alias (a named tuple) has a key_code
field (str
) and a modifiers
field (list
). For single character aliases, when we're building the modifiers list for a mapping, we could:
- check if the character is in the aliases YAML map
- make a copy of the Alias objects'
modifiers
attribute (list), append thekey_code
attribute to it, and add that list to the mapping's final modifiers list
I'm not sure about multi-char modifiers aliases, e.g. if you wanted kitty_mod | g
or something, since the modifier parsing functions are fairly strict about interpreting single characters (except for pre-defined cases like ⌘›
.
Okay, sorry for the slow replies, it's been a bit of nightmare week for me.
I really like being able to implement custom aliases but the syntax for multiple modifiers isn't ideal.
aliases:
★:
- fn
- [right_control, right_option, right_shift, right_command]
I wish I could just do ★: [right_control, right_option, right_shift, right_command, fn]
and let Karaml infer that the last keycode is the event string.
In general, I would love to be able to define a list of items and have it infer the final item as the field string if a field string isn't explicitly set (| xyz
, + xyz
) but that's trickier. I also may not be considering all cases, are there places you want to dfine a list without a field string?
That said, the pipe syntax is excellent. I'm finding it really clean to use and it makes it pretty obvious what a line does. It's significantly easier to read than using angle brackets. It might be nice if modifier aliases could be used to the left of a pipe but I don't think it's a big deal.
I'm not sure about multi-char modifiers aliases, e.g. if you wanted kitty_mod | g or something, since the modifier parsing functions are fairly strict about interpreting single characters (except for pre-defined cases like ⌘›.
That's a tough call. If you could get it working it might be useful, not essential.
It is still a bit confusing knowing where you can use aliases and where you can't. It took me a couple of tries before I got this bit working:
# Brackets
⇧› | left_shift: ["(", left_shift]
‹⇧ | right_shift: [")", right_shift]
⇧› ⌘ | left_shift: ["[", ⌘-left_shift]
‹⇧ ⌘ | right_shift: ["]", ⌘-right_shift]
⇧› ⌥ | left_shift: ["{", ⌥-left_shift]
‹⇧ ⌥ | right_shift: ["}", ⌥-right_shift]
⇧› ⌃ | left_shift: ["<", ⌃-left_shift]
‹⇧ ⌃ | right_shift: [">", ⌃-right_shift]
Great work on the documentation, it's excellent!
I am so excited about Karaml, I found Goku to be a bit tedious to work with, though dramatically better than JSON, and I was experimenting with skhd as a possible replacement to Karabiner because it's configs were easier to write. It unfortunately didn't support layering however, just key chords so I switched back. If I'd found Karaml sooner I wouldn't have even looked for alternatives.
Thanks for the detailed reply. As a precursory note, I know I'm probably conflating terminology and being inconsistent, so feel free to point that out if I'm being confusing in my comments. I'll try to be more thoughtful.
I wish I could just do
★: [right_control, right_option, right_shift, right_command, fn]
and let Karaml infer that the last keycode is the event string.
I like that idea, and it would be easy to implement since it's easy to distinguish between the string and the nested list.
In general, I would love to be able to define a list of items and have it infer the final item as the field string if a field string isn't explicitly set (
| xyz
,+ xyz
) but that's trickier. I also may not be considering all cases, are there places you want to dfine a list without a field string?
If I'm understanding correctly, would you want a syntax where we just get rid of an explicit delimiter between modifiers and and just use the final whitespace as the delimiter? Also a great idea, and I don't think it would be an issue so long as +
or some other character is available to indicate simultaneous event concatenation. Or let me know what you'd like to see exactly.
It might be nice if modifier aliases could be used to the left of a pipe but I don't think it's a big deal.
I can work on that, shouldn't take too long, just need a few days to catch up on other things.
It is still a bit confusing knowing where you can use aliases and where you can't. It took me a couple of tries before I got this bit working:
That was sort of an oversight on my part. I could have made the Unicode aliases available for the 'primary' key code (this is one of those terms in my own mental model so lmk if it's wrong) by just inserting them in the ALIAS dict (the dict of Alias tuples for 'primary' keys, separate from the MODIFIERS and UNICODE_MODS dicts of strings). This modifier keys qua modifier vs. modifier keys qua primary key distinction is why I unconsciously didn't add aliases for the modifiers in these recent updates, because I wanted to make explicit when a modifier key code needed to be primary. That may be what was unclear about where and when you could use those modifier aliases. Sorry about that.
Also, when modifier key codes are 'primary', they need to have a left_
or right_
side. So, the issue is consistency. We could say '⌘
is neutral as a modifier alias, but left_sided when it's a primary key alias, and also ‹⌘
and ⌘›
are still available as explicit indicators of which side.
So, I'll update the ALIAS dict and push it. After this commit, you should be able to have this in your config:
# Brackets
⇧› | ‹⇧: ["(", ⇧›]
⇧› ⌥ | ‹⇧: ["{", ⌥ | ‹⇧] # The pipe-as-delimiter works in the rhs as well if you prefer it over the dash
# Or, equivalent:
# Here, the non-explicit ⇧ defaults to the left side when it's an alias for a 'primary' key code
⇧› ⌥ | ⇧: ["{", ⌥ | ⇧]
And btw, since the aliases
map from the YAML config updates the predefined ALIAS dict, anything you define in it should overwrite the default ALIAS dict (say you don't want ⇧ defaulting to the left side, or maybe you want to alias it to left shift + right shift)
Is this what you were expecting when you were trying to use the aliases in different places? Let me know if I interpreted 'final whitespace as delimiter' correctly and I'll work on it. Sorry the code is such a mess.
If I'm understanding correctly, would you want a syntax where we just get rid of an explicit delimiter between modifiers and and just use the final whitespace as the delimiter?
I believe so, basically I want it to behave just like the previous suggestions for [right_control, right_option, right_shift, right_command, fn]
. In most cases I would probably just continue to use the pipe for better readability but it seems like an option that might be useful in some contexts like a stand-alone command.
# Like this
⌥ ⌘› o: Do a simple stand-alone action
# While keeping
⌥ ⌘ | 1: Do
⌃ | 1: some
⇧ ⌃ | 1: complex or related
⇧ | 1: things
The more I think about this the less necessary I think it is but it's still an idea that might be worth considering.
That was sort of an oversight on my part [...]
Sound reasoning, might be better to spell it out explicitly.
So, I'll update the ALIAS dict and push it […]
Love it, thanks!
Also, when modifier key codes are 'primary', they need to have a left_ or right_ side. So, the issue is consistency. We could say '⌘ is neutral as a modifier alias, but left_sided when it's a primary key alias, and also ‹⌘ and ⌘› are still available as explicit indicators of which side.
I like this idea a lot!
So I did find someplace where the aliases shouldn't expanded and should be treated simply as the unicode character:
# # Modifiers Layer
#
# Because text expansion is just too slow.
#
/modifiers/:
c: string(⌃) # Outputs nothing
o: string(\⌥) # Outputs `\`
s: string('⇧') # Outputs `''`
n: string(⌘) # Outputs nothing
h: string(☆) # Outputs nothing
l: string(‹) # Throws a fatal error
r: string(\›) # Throws a fatal error. As does `string('›')`
I really can't express how fantastic Karaml is! Keep up the great work!
Thanks for the kind words! I appreciate your help in making this a cleaner interface.
/modifiers/: c: string(⌃) # Outputs nothing o: string(\⌥) # Outputs `\` s: string('⇧') # Outputs `''` n: string(⌘) # Outputs nothing h: string(☆) # Outputs nothing l: string(‹) # Throws a fatal error r: string(\›) # Throws a fatal error. As does `string('›')`
Nice bug find. This is a mix of issues. Primarily, the string() 'pseudo-function' (another one of my imprecise terms) works by reading the string's arg character by character and adding the corresponding key code to a list of 'to' events. There are no Unicode key_codes, so generally those are the ones throwing those errors. Those aliases that we have added already (⌘ ⌃ etc.) in your examples above are being read as valid key codes, but they're being added to the list of 'to' events as the key they're the alias of. So, string('⇧')
is actually outputting '
, then a left_shift
event, then '
, and so on for the others.
It would make sense to ignore Unicode aliases in the string()
pseudo-func. Also, we need to decide on a system for how to send Unicode characters as an event. I think I tried figuring that out once but set it aside and never got back to it because I couldn't figure out the right AppleScript.
pqrs-org/KE-complex_modifications#697
This issue might provide some insight. I'll do more research. Thanks for finding that bug. I'm going to push this to the top of the todo-list so you can add that /modifiers/ layer asap.
Also, we need to decide on a system for how to send Unicode characters as an event. I think I tried figuring that out once but set it aside and never got back to it because I couldn't figure out the right AppleScript.
Try this one I'm using to insert various symbols with karabiner/goku, the only downside of Applescript is that it's not immediate unlike the more complicated solutions that require editing your layout with Ukelele (or adding some Hex code layout that you'll never use anywhere else) and using a simple key combo, so would be nice to have a less hacky instant solution that is just as convenient as [:echo "⇪"]
Thanks for the suggestion! Yes, AppleScript does seem to be kinda sluggish. I used to use it for some minor mouse movements and it felt like an eternity. I like the idea of using a shell script instead of appending a series of events to the 'to' list, mostly because then you wouldn't end up with a monstrous JSON file full of giant 'to' lists (even though we're trying to abstract from it here). I'll mess around with that.
I wouldn't push this as the top priority on my account, I can pretty easily send a osascript command to a Typinator expander, Keyboard Maestro macro or possibly an Alfred expander or snippet if Alfred exposes those commands in its AppleScript dictionary to achieve this functionality.
That said, if I'm understanding was the issue is correctly, you could potentially just use the clipboard for inserting a string. You could call pbcopy
/pbpaste
shell commands or use AppleScript. I think I'd go with the latter unless it adds a performance lag because I think you could backup the existing clipboard as a saved clipboard, copy the string, paste the string and restore the saved clipboard.
Which is pretty how Goku does it.
I made an attempt at solving the symbols issue with the string()
func. Basically, if the string contains a character that doesn't have a corresponding Karabiner key_code, then print it the entire string with the AppleScript command @eugenesvk linked to. There is a mild delay but I found it tolerable, but please let me know.
Otherwise, if all characters in the string can be handled by Karabiner, then string() works the way it has been, by adding a series of key_code
events to the to:[]
list.
One issue with the delay of the AppleScript. Say you have a karaml map like this:
/base/:
⌥ | m: string(⌘ ⌃ ⌥ ⇧) + ESC + return + string(hello!)
⌥ | n: string(g r a h) + ESC + return + string(hello!)
The first string in the first map is now a shell command. The subsequent events aren't going to wait for the shell command to execute, they'll just go when the shell command leaves the stack (? or however tekezo does it). See #3 for more on this, but this behavior is more or less intentional.
In the second map, everything executes fast enough that it's not an issue.
Anyway.
I'm having some weird issue with my shell, but this feature was working for me for a minute. It stopped working while I was spam testing some of those commands. @ChristinWhite If you want to check it out on the branch at this commit 13f9a60,(EDIT: now on main) the /modifiers/
layer you have should hopefully work. I'm just locked out of testing right now.
/modifiers/:
c: string(⌃)
o: string(⌥)
s: string('⇧')
n: string(⌘)
h: string(☆)
l: string(‹)
# r: string(\›) # non-working case that I'm too lazy to fix rn
# (a backslash followed by a unicode escape sequence)
w: string(hello ⇧⌘☆ \n\tworld ☆⌃) # yaml escape characters (new line, tab etc.) also work
a: string(👍) # and emojis!
Regarding the issues with consistency I mentioned above, after those commands stop working (for whatever reason I haven't figured out yet),the Karbiner-Elements GUI log shows this error when I try to execute it again:
[2023-04-19 20:46:20.361] [error] [console_user_server] shell_command stderr:execution error: Can’t make some data into the expected type. (-1700)
After restarting the console with this command though, they work again, and I can't reproduce my earlier issue
launchctl kickstart -k gui/$(id -u)/org.pqrs.karabiner.karabiner_console_user_server
I merged 13f9a60 for the new string() feature to print any character.
Also, as of 2798bb1, the 'final whitespace as delimiter' syntax is supported, per #2 (comment)
Any amount of whitespace is valid, except that the first character has to be properly indented per YAML syntax.
⌥ ⌘› o: string(I can print ⌘⌥⇧⌃ now!) # either opt + right command + o prints that string
⌘› o: string(bye) # this isn't valid
' ⌘› o': string(bye) # but this is (per standard YAML) if you're shooting for alignment of symbols
Also, with all the progress made, I want to make it a goal to change all the examples in the docs to use the pipe syntax, and maybe even the symbol aliases for the modifiers. But first I want to try to learn more about what caused those console crashes (? or whatever they were) earlier.
I think the only major feature that was mentioned that's left to address (EDIT: I'm happy to address new ones!) is multi-char aliases in the modifier 'field', e.g.
lcmd rcmd | o: app(Obsidian)
kitty_mod | g: # some fancy command that needs the kitty modifier (e.g. ctrl + shift)
I think the only major feature that was mentioned that's left to address (EDIT: I'm happy to address new ones!) is multi-char aliases in the modifier 'field', e.g.
Whoops,also gotta fix up the aliases
map in the YAML config, I'll do that next
All of the strings are now working perfectly!
@ChristinWhite good news / breaking changes 4094671 (plus a fix in 2de7cff, whoops)
I updated the syntax for the aliases
map per your suggestion in #2 (comment)
Also, if an alias is composed entirely of modifiers, it will also be added to the (now renamed) MODIFIER_ALIASES
dict, where the modifier symbols ⌘ ⇧ etc. were being held. Only supporting single character aliases in the MODIFIER_ALIASES
dict at the moment, I'll work on it.
So, as of this commit, this is possible:
aliases:
⁙: o c | s # With a delimiter, this aliases to `left opt` + `left ctrl + `s`
⁙: o c s # With no (explicit) delim, this aliases to `left opt` + `left ctrl` + `left shift`
⁙: ⌥ ⌃ ⇧ # When it's all modifier symbols, syntax doesn't matter.
# Both of the two above will be added to both the general ALIASES and MODIFIER_ALIASES dict
⏎: return_or_enter
screen_saver: shell(open -b com.apple.ScreenSaver.Engine) # Start Screen Saver
/base/:
tab: [tab, ⁙] # tab to left opt, ctrl, and shift when held
j+k: ⁙ | e # simul j+k to left opt, ctrl, shift, and e
⁙ | ⏎: screen_saver # left opt, ctrl, shift, and enter starts screen saver
⁙ e: ⁙ | ⏎ # etc.
Hopefully I didn't miss any bugs (EDIT: lol @ my audacity. See ca6eff5 and 5013c6e for fixes in the user defined aliases map)
This is fantastic!
Since this is somewhat alias related, I'll mention here that I added a user-defined templates feature a while back that imitates Goku's (but Goku's is more flexible b/c I think it's actual Clojure and not just regex parsing)
https://github.com/al-ce/karaml#user-defined-shell-command-templates