rbreaves / kinto

Mac-style shortcut keys for Linux & Windows.
http://kinto.sh
GNU General Public License v2.0
4.27k stars 214 forks source link

[Linux] Unicode and dead keys character entry support #701

Open RedBearAK opened 2 years ago

RedBearAK commented 2 years ago

@rbreaves @joshgoebel I'm looking at some different ways of implementing a scheme to enter accented characters and special symbols with the Option key, the way Apple does it. There are a couple of different methods to do this on Linux.

Method 1: Compose key macros. This doesn't seem to work correctly unless the DE is set up to use the Menu key as the compose key. At this time I don't know how to make this reliable without making the user change the relevant setting on their system.

Method 2: Hold Ctrl+Shift, press u, then enter the Unicode character code (e.g., 00A1) and release Ctrl+Shift.

This second method seems to work fine without changing any settings, but it won't work with Kinto's branch of xkeysnail in its current state. A macro in xkeysnail doesn't have the ability to hold down some modifier keys and then type an entire sequence of other characters before releasing the modifiers. Unless I'm missing something.

With AutoHotKey on Windows it has ways to say {Shift down}{Ctrl down}u00A1{Shift up}{Ctrl up} to complete a sequence like this as if you were typing the "u00A1" part while holding the modifier keys down, then lifting afterward.

Then again, AHK also has the ability to input Unicode characters with a very simple Send format:

!1::Send, {U+00A1}    ; Inverted Exclamation Mark

This seems to work even in Windows apps like Notepad.exe that refuse to accept Alt Codes.

The Option-key special symbol and accented character scheme for the Windows version of Kinto is nearing completion. But without some solution for a reliable way to enter Unicode characters in the Linux config, I can't work on bringing it over to the Linux side.

Maybe there could be a special function added that recognizes the {U+0000} format and translates it into the proper Shift+Ctrl+u0000 key presses and releases?

K({U+000A1})    # Inverted Exclamation Mark

The other issue that will stop me is that multi-key nested shortcuts, which seems to work in mainline xkeysnail, is broken in Kinto's fork. So it will be impossible to implement the "dead keys" method of entering accented characters until that gets repaired.

joshgoebel commented 2 years ago

This doesn't seem to work correctly unless the DE is set up to use the Menu key as the compose key.

Is this really a DE thing or just an X thing? When I tried playing with it I think I enabled just by running one of the low-level X utilities... xmodmap maybe? I mean "MENU" is a key but I think you have to tell X to map menu to menu or something dumb...

Hold Ctrl+Shift, press u, then enter the Unicode character code (e.g., 00A1) and release Ctrl+Shift.

My branch can't do this either, but it's within the realm of possibility that we could add that functionality.

Maybe there could be a special function added that recognizes the {U+0000} format and translates it into the proper Shift+Ctrl+u0000 key presses and releases?

You could do that with a tiny Python helper, doesn't even need to be built in.

multi-key nested shortcuts ... is broken in Kinto's fork.

Yep, known issue.

RedBearAK commented 2 years ago

Is this really a DE thing or just an X thing? When I tried playing with it I think I enabled just by running one of the low-level X utilities... xmodmap maybe? I mean "MENU" is a key but I think you have to tell X to map menu to menu or something dumb...

Either way, it's something to user would have to do outside of Kinto, is it not? And it's changing a setting in their environment that they may have configured a different way for a good reason. I have no idea if it's safe to mess with the user's compose key settings. Or a method to not need to do that.

You could do that with a tiny Python helper, doesn't even need to be built in.

I figured it was possible. Just a question of who's going to be motivated to implement it.

Yep, known issue.

Nested shortcuts work OK in your branch, don't they? Maybe you could provide a link to the section of Kinto's xkeysnail branch where things are going wrong.

joshgoebel commented 2 years ago

Either way, it's something to user would have to do outside of Kinto, is it not? And it's changing a setting in their environment that they may have configured a different way for a good reason. I have no idea if it's safe to mess with the user's compose key settings.

That sounds like a Kinto decision... it mucks with a lot of stuff already... if it was declared that the menu key was "in scope" then I presume it could also muck with that. I'm also not 100% opposed to mucking with X from inside the keymapper itself IF it was simple enough since it bills itself as a Linux/X11 keymapper...

Just a question of who's going to be motivated to implement it.

I'd love a link to read up on this second way but if it's standard and if we added the necessary hold/lift macros then I'd be open to a unicode macro or some such in the core mapper - though it's not a super high priority.

Nested shortcuts work OK in your branch, don't they?

No reason they shouldn't.

Maybe you could provide a link to the section of Kinto's xkeysnail branch

No idea, it's something to do with the changes that were made to keep keys pressed...

RedBearAK commented 2 years ago

@joshgoebel

It looks like there are actually a couple of different ways to do the Shift+Ctrl+u entry method. The second method lets you let go of the modifiers and type the code and hit Enter. This should be working with a macro, but for some reason I can't get it to work. It works when I do it in GNOME Text Editor by hand, but when I try to use it from a macro I just end up with the last three characters of the Unicode sequence.

    K("Alt-Key_1"): [K("Shift-C-u"), K("Key_0"), K("Key_0"), K("a"), K("Key_1"), K("Enter")],

Output from this is "0a1" in the text editor. I can't understand why it just seems to stop the Unicode entry after the first "0" character. Even tried a sleep_and_then to introduce a delay after the shortcut that initiates Unicode entry. Same result.

Hmm, if I introduce a much longer delay I can see it starting the Unicode entry (you see a "u" with underline) then it just reverts to normal character entry after the first character. This is with the Kinto branch.

Your branch... Well, as usual I'm not quite sure what it thinks its doing. I have six tabs open in the text editor and when I try this shortcut it switches me to the first tab. Looks like it's passing the Alt+1 through to the app for some reason, which the app should never see. Alt+number appears to go to specific tabs in that app.

joshgoebel commented 2 years ago

What does the verbose log say it's doing?

RedBearAK commented 2 years ago

@joshgoebel

It appears that your branch is only doing the input shortcut, the Alt+1, and not doing the macro at all. This is from going into the text editor (with the mouse from the dock icon) and trying the macro shortcut, then coming back to the terminal and killing it with the debug key.

I made sure there was a blank line before attempting the macro, and there was nothing inserted into the text file, and nothing inserted into the file in the first tab. It just switches tabs, as if I pressed Alt+1 without this shortcut even being in the config file.

keyszer-output-2022-06-12-003436.txt

joshgoebel commented 2 years ago

And the relevant section of your config?

RedBearAK commented 2 years ago

@joshgoebel

As posted above.

    K("Alt-Key_1"): [K("Shift-C-u"), K("Key_0"), K("Key_0"), K("a"), K("Key_1"), K("Enter")],

It's located in the "General GUI" section, so should be active even for apps with no valid WM_CLASS, except in apps with a conflicting shortcut in a section that comes further up in the config file. The macro activates in this same app with the Kinto branch. It just breaks part way through, as described.

RedBearAK commented 2 years ago

@joshgoebel @rbreaves

I wondered if it had something to with the zero character. So I changed the code, attempting to enter an "Almost Equal To" sign (the wavy equals sign). Once again, the Unicode entry breaks after the first character following the Shift+Ctrl+u shortcut, leaving a Unicode "box" character (assumedly the U+0002 Unicode character, if there was such a thing) followed by "248", the rest of the "2248" Unicode sequence that should have resulted in the desired character. I can type it manually right here: ≈

    K("Alt-Key_1"): [K("Shift-C-u"), K("Key_2"), K("Key_2"), K("Key_4"), K("Key_8"), K("Enter")],
joshgoebel commented 2 years ago

Works for me here right away: ≈

(II) in KEY_1 (press)
(DD) on_key KEY_1 press

(DD) WM_CLS 'Google-chrome' | DEV 'Apple, Inc Apple Keyboard' | KMAPS = [global default, screen paging (not term), Chrome Browsers, General Web Browsers, Mac OS Cmd+dot, General GUI, Wordwise - not vscode]
(DD)   COMBO: LAlt-KEY_1 => [Shift-Ctrl-U, KEY_2, KEY_2, KEY_4, KEY_8, ENTER]   [General GUI]
(DD) spent modifiers [<Key.LEFT_ALT: 56>]
(DD) resuspending keys
(DD) suspending keys [LAlt<Key.LEFT_ALT>]
(OO) press LEFT_SHIFT
(OO) press LEFT_CTRL
(OO) press U
(OO) release U
(OO) release LEFT_CTRL
(OO) release LEFT_SHIFT
(OO) press KEY_2
(OO) release KEY_2
(OO) press KEY_2
(OO) release KEY_2
(OO) press KEY_4
(OO) release KEY_4
(OO) press KEY_8
(OO) release KEY_8
(OO) press ENTER
(OO) release ENTER

(II) in KEY_1 (release)
(DD) on_key KEY_1 release

I'm at revision eb8cafcfab64054d89fa24980ec40fdb0ce7881e

joshgoebel commented 2 years ago

That's pretty awesome too, btw. :)

RedBearAK commented 2 years ago

@joshgoebel

Git does not pull anything new, says "Already up to date". Still doesn't work for me.

I can try to use the DIAG key, but where exactly does the diagnostic file end up?

joshgoebel commented 2 years ago

The diag is just in the log... and my modmap config for reference:

define_conditional_modmap(lambda wm_class: wm_class.casefold() not in terminals,{

    Key.CAPSLOCK: LHyper,

    # - Mac Only
    Key.LEFT_META: Key.RIGHT_CTRL,  # Mac
    Key.LEFT_CTRL: Key.LEFT_META,   # Mac
    Key.RIGHT_META: Key.RIGHT_CTRL, # Mac - Multi-language (Remove)
    # Key.RIGHT_CTRL: Key.RIGHT_META, # Mac - Multi-language (Remove)
})

# [Conditional modmap] Change modifier keys in certain applications
define_conditional_modmap(re.compile(termStr, re.IGNORECASE), {
    Key.CAPSLOCK: LHyper,

    Key.LEFT_META: Key.RIGHT_CTRL,  # Mac
    # Left Ctrl Stays Left Ctrl
    Key.RIGHT_META: Key.RIGHT_CTRL, # Mac - Multi-language (Remove)
    # Key.RIGHT_CTRL: Key.LEFT_CTRL,  # Mac - Multi-language (Remove)
})

The hyper shouldn't matter but if you wanted to see which keys i'm modmapping at the top-level... incase your are different. So for me alt is a naked key... no modmapping... it just gets suspended then triggered when the 1 is hit, outputting the combo.

joshgoebel commented 2 years ago

I don't think DIAG will help yu here, it just tells you the state... good for understanding stuck keys and such things and weird state. But who knows.

RedBearAK commented 2 years ago

I don't mess with the modmaps that Kinto sets up, and of course I'm on a PC laptop so it will have different lines activated. Trimming out the inactive lines leaves:

# [Global modemap] Change modifier keys as in xmodmap
define_conditional_modmap(lambda wm_class: wm_class.casefold() not in terminals,{
...
    # -- Default Win
    Key.LEFT_ALT: Key.RIGHT_CTRL,   # WinMac
    Key.LEFT_META: Key.LEFT_ALT,    # WinMac
    Key.LEFT_CTRL: Key.LEFT_META,   # WinMac
    Key.RIGHT_ALT: Key.RIGHT_CTRL,  # WinMac - Multi-language (Remove)
    Key.RIGHT_META: Key.RIGHT_ALT,  # WinMac - Multi-language (Remove)
    Key.RIGHT_CTRL: Key.RIGHT_META, # WinMac - Multi-language (Remove)
...
})

# [Conditional modmap] Change modifier keys in certain applications
define_conditional_modmap(re.compile(termStr, re.IGNORECASE), {
...
    # - Default Mac/Win
    # -- Default Win
    Key.LEFT_ALT: Key.RIGHT_CTRL,   # WinMac
    Key.LEFT_META: Key.LEFT_ALT,    # WinMac
    Key.LEFT_CTRL: Key.LEFT_CTRL,   # WinMac
    Key.RIGHT_ALT: Key.RIGHT_CTRL,  # WinMac - Multi-language (Remove)
    Key.RIGHT_META: Key.RIGHT_ALT,  # WinMac - Multi-language (Remove)
    Key.RIGHT_CTRL: Key.LEFT_CTRL,  # WinMac - Multi-language (Remove)
...
})

I don't see what this would have to do with the macro not activating.

The diag is just in the log

Oh, of course.

So for me alt is a naked key... no modmapping... it just gets suspended then triggered when the 1 is hit, outputting the combo.

Uhhh... But how would that affect me, with a different modmap? I tried a different shortcut (RC-Alt-Key_1) and still nothing happened with your branch. This makes no sense.

joshgoebel commented 2 years ago

But how would that affect me, with a different modmap?

It changes what "Alt" even means in your combo - it means alt AFTER remappings. For example it looks like you have to hit LEFT_META in order to output Alt. So you'd hit LeftMeta-1 on the physical keyboard... to trigger your "alt-1" macro.

(or right meta)

joshgoebel commented 2 years ago

This is why I dislike keymaps, they are impossible to read. And they are meaningless if you don't have the users modmap in your head. For example have to hit Meta-1, while I hit Alt-1... impossible to know from just reading the keymap.

RedBearAK commented 2 years ago

@joshgoebel

I've been using Kinto for a couple of years and written entire sections of the current default Kinto config file (the Finder Mods for Linux file browsers). I got used to the modifier remapping quite a long time ago. Putting Alt-Key_1 in a shortcut means I have to press the physical "Windows" key, aka the "Super" key, aka (if we are in agreement) the "Meta" key. Which is exactly what I'm doing.

I'm trying to mimic Option+1 on an Apple keyboard, and the Windows key is in the same physical location and has the same logical function under Kinto's modmap as Apple's Option key (Alt).

But all of this is sidestepping the issue, which is why is the shortcut triggering a macro like it should with Kinto's branch, and why is your branch just sending the input shortcut to the app? It's doing the same thing with multiple apps that I've tested. It's acting like there is no macro. Kinto must be accidentally sending an Enter or some kind of press/release that's breaking the Unicode entry, and your branch is just ignoring the macro entirely.

joshgoebel commented 2 years ago

I got used to the modifier remapping quite a long time ago.

Well, you know what they say about assumptions. :-)

RedBearAK commented 2 years ago

@joshgoebel

Well, you know what they say about assumptions. :-)

"When you make an assumption, you make an ass out of You, and Umption." (Long Kiss Goodnight, 1996)

joshgoebel commented 2 years ago

and why is your branch just sending the input shortcut to the app?

You need to add more logging in tranform_key, see what it's doing... have it print out the combo it builds, is that what you expect?

Have it print as it loops over the keymaps trying to find one that matches, does it indeed find the General GUI map?

You just add logging one line at a time until the result isn't what you'd expect.

joshgoebel commented 2 years ago

You could lift the contextual debugging (line 468) outside the for loop to get access to it constantly (on every keypress) and see if that's what you expect...

RedBearAK commented 2 years ago

@joshgoebel

I think this is the relevant bit, but I don't see anything yet about anything besides Alt and Key_1. "General GUI" is definitely in there.


(II) in LEFT_META (press)
(DD) modmap: LEFT_META => LEFT_ALT [define_conditional_modmap (old API)]
(DD) on_key LEFT_ALT press
(DD) suspending keys [LAlt<Key.LEFT_ALT>]

(II) in KEY_1 (press)
(DD) on_key KEY_1 press
(DD) WM_CLS 'Gnome-terminal' | DEV 'AT Translated Set 2 keyboard' | KMAPS = [Magic shortcut to kill Kinto, Special overrides for terminals, General GUI, Wordwise - not vscode, terminals]
(DD) resuming keys: [<Key.LEFT_ALT: 56>]
(OO) press LEFT_ALT
(OO) press KEY_1

(II) in KEY_1 (release)
(DD) on_key KEY_1 release
(OO) release KEY_1

(II) in LEFT_META (release)
(DD) on_key LEFT_ALT release
(DD) resume because of mod release
(OO) release LEFT_ALT

*** TRANSFORM  ***
are we suspended: False
_suspend_timer:
None
_last_key:
KEY_1
_states:
{}
_sticky:
{}
*** OUTPUT ***
_pressed_modifier_keys:
set()
_pressed_keys
set()
joshgoebel commented 2 years ago

Well if you moved the contextual logging above line 461... the fact that "General GUI" is there is a godo sign... that just leaves you to log the hell out of lines 463-465... why is the combo not being found? And did you ever log combo itself to make sure it looks right?

RedBearAK commented 2 years ago

@rbreaves

I think the excessive Ctrl key press/release events being issued by the Kinto branch xkeysnail are killing the ability to enter Unicode with a macro. If I type Shift+Ctrl+u and then type a character and hit the Ctrl key, the Unicode entry sequence gets canceled. I believe that's why I'm finding it impossible to do Unicode character entry macros. They will always break after the first character of the Unicode sequence, when they encounter a Ctrl key event.

joshgoebel commented 2 years ago

I think the excessive Ctrl key press/release events being issued by the Kinto branch xkeysnail are killing the ability to enter Unicode with a macro.

YES - because it doesn't have any intelligence to treat the sequence differently than a single key... so it presses/lifts the held keys between every combo - so that will break anything you're trying to do that's more than a single combo.

RedBearAK commented 2 years ago

@joshgoebel

YES - because it doesn't have any intelligence to treat the sequence differently than a single key... so it presses/lifts the held keys between every combo - so that will break anything you're trying to do that's more than a single combo.

And it's never appeared to be a problem until I did something like Unicode entry that actually cares that you're smashing the Ctrl key in between other things, where typical macros simply don't give a hoot.

RedBearAK commented 2 years ago

@joshgoebel

By "logging" do you just mean putting things like print(combo) all over the place?

joshgoebel commented 2 years ago

Yep. I can already feel it's going to be something really dumb, but I just can't see it from looking at the code... some failure in how it's comparing combos or something...

RedBearAK commented 2 years ago

@joshgoebel

Not seeing anything particularly useful popping up yet. Specific suggestions on what exactly to "print" out might be helpful.

(II) in LEFT_ALT (release)
(DD) on_key RIGHT_CTRL release
(DD) lift of sticky RIGHT_CTRL => LEFT_ALT
(OO) release LEFT_ALT
(DD) resuspending keys
(DD) suspending keys []

(II) in LEFT_META (press)
(DD) modmap: LEFT_META => LEFT_ALT [define_conditional_modmap (old API)]
(DD) on_key LEFT_ALT press
(DD) suspending keys [LAlt<Key.LEFT_ALT>]

(II) in KEY_1 (press)
(DD) on_key KEY_1 press
^[[18~Printing combo...
LAlt-KEY_1
(DD) WM_CLS 'Gnome-terminal' | DEV 'AT Translated Set 2 keyboard' | KMAPS = [Magic shortcut to kill Kinto, Special overrides for terminals, General GUI, Wordwise - not vscode, terminals]
Printing from the "for mappings in _mode_maps" loop...
<keyszer.lib.keymap.Keymap object at 0x7fe15d3c0520>
Printing from the "for mappings in _mode_maps" loop...
<keyszer.lib.keymap.Keymap object at 0x7fe15d3c1540>
Printing from the "for mappings in _mode_maps" loop...
<keyszer.lib.keymap.Keymap object at 0x7fe15d3af220>
Printing from the "for mappings in _mode_maps" loop...
<keyszer.lib.keymap.Keymap object at 0x7fe15d3ae020>
Printing from the "for mappings in _mode_maps" loop...
<keyszer.lib.keymap.Keymap object at 0x7fe15d3e85b0>
(DD) resuming keys: [<Key.LEFT_ALT: 56>]
(OO) press LEFT_ALT
(OO) press KEY_1

(II) in KEY_1 (release)
(DD) on_key KEY_1 release
(OO) release KEY_1

(II) in LEFT_META (release)
(DD) on_key LEFT_ALT release
(DD) resume because of mod release
(OO) release LEFT_ALT

*** TRANSFORM  ***
are we suspended: False
_suspend_timer:
None
_last_key:
KEY_1
_states:
{}
_sticky:
{}
*** OUTPUT ***
_pressed_modifier_keys:
set()
_pressed_keys
set()
joshgoebel commented 2 years ago

I think you're going to need to start rewriting the loop...

Instead of just checking if combo is in mappings... loop over every mapping and print them out ... find out why they aren't comparing the same...

    for mappings in _mode_maps:
       for mapping in mappings:
           print(mapping)

Is the Alt-1 combo there somewhere?

joshgoebel commented 2 years ago

I finally got annoyed and just copied your WIndows config into my file (and almost didn't recover, lol)... and it ALSO works fine on my end (when I hit the correct keys)... I'm thinking something is just weird with your setup... and that's going to make it hard to pin down.

joshgoebel commented 2 years ago

What is your python version? Pretty sure I asked you before.

joshgoebel commented 2 years ago

actually you might need for mapping in mappings.items() or something... I'm still learning Python so these snippets I type live often require intepreteation.

joshgoebel commented 2 years ago

Start by asking what you'd want to know at each step, for example:

    for mappings in _mode_maps:
        print(f"checking {mappings.name}")
        if combo not in mappings:
            print(f"could not find {combo} in {mappings.name}")
            continue
joshgoebel commented 2 years ago

It'd sure be interesting if that told you that Alt-1 wasn't in General GUI....

RedBearAK commented 2 years ago

Ubuntu 22.04 is on Python 3.10.4.

Printing upon entering transform_key...
KEY_1
press
<keyszer.lib.key_context.KeyContext object at 0x7f2a4f02c340>
Printing combo...
LAlt-KEY_1
(DD) WM_CLS 'gnome-text-editor' | DEV 'AT Translated Set 2 keyboard' | KMAPS = [GNOME Text Editor, Anonymous keymap, General GUI, Wordwise - not vscode]
Printing from the "for mappings in _mode_maps" loop...
checking GNOME Text Editor
could not find LAlt-KEY_1 in GNOME Text Editor
Printing from the "for mappings in _mode_maps" loop...
checking Anonymous keymap
could not find LAlt-KEY_1 in Anonymous keymap
Printing from the "for mappings in _mode_maps" loop...
checking General GUI
could not find LAlt-KEY_1 in General GUI
Printing from the "for mappings in _mode_maps" loop...
checking Wordwise - not vscode
could not find LAlt-KEY_1 in Wordwise - not vscode
(DD) resuming keys: [<Key.LEFT_ALT: 56>]
(OO) press LEFT_ALT
(OO) press KEY_1

I tried changing the shortcut to actually say LAlt, but it still says it can't find it. But it is most definitely in the "General GUI" block.

joshgoebel commented 2 years ago

See, now that's when you add in for combo in mappings.items() and print out every single mapping and then try to find it yourself in the output... maybe it's not what you think it is...

RedBearAK commented 2 years ago

@joshgoebel

I copied the shortcut line and added it to the block dedicated to "gnome-text-editor". It still doesn't get found. But the "Now is the time" macro that already exists in that block will still work.

joshgoebel commented 2 years ago

Nice idea, but I still think you should just keep logging until you hit bottom... obviously something is wrong with JUST that combo.

RedBearAK commented 2 years ago

@joshgoebel

The long macro is losing MANY keystrokes most of the time. More than with the Kinto branch without the sleep delay in output.

joshgoebel commented 2 years ago

Stay focused one thing at a time.... you're trying to find out why the combo isn't triggering...

RedBearAK commented 2 years ago

for combo in mappings.items()

And print what?

joshgoebel commented 2 years ago

Start with the combo itself and see if there are any surprises...

joshgoebel commented 2 years ago

Eventually you're going to have to find that the combo isn't what you think it is or equality checking or broken or something really weird. Running out of things it could be at this point.

RedBearAK commented 2 years ago

AttributeError: 'Keymap' object has no attribute 'items'

Don't know exactly where to put this for loop.

joshgoebel commented 2 years ago

ah it's a keymap... mappings.mappings.items()

RedBearAK commented 2 years ago

@joshgoebel

ah it's a keymap

Ah yes, of course. Then he looked thoughtfully away, pretending to understand

That's a lot. I'll have to look through it for a while.

joshgoebel commented 2 years ago

At this point you really should start learning more Python. :-)

Look thru it? Come on I'm on the edge of my seat over here... :-p

joshgoebel commented 2 years ago

All interesting possibilties.