Kitty shortcuts work only with Latin characters - How to fix?
Tags: kitty, python, programming, automation
Published: 2024-01-28 11:20:32

<!-- wp:paragraph -->
<p>If you are reading this probably you have the same issues as I faced before. I love Kitty terminal. It's fast and simple. At the same time, I use in my life following languages: Bulgarian, Ukrainian, and Russian. All of them use the Cyrillic alphabet. And Kitty has an issue: shortcuts work only with Latin characters. That means, if your current layout is Bulgarian for example, you can't use shortcuts such as CMD+C, CMD+V, or their alternative Ctrl+C, Ctrl+V. It could make things hard if you need to use Cyrillic language and shortcuts at the same time. I have tried to fix that. Let me share what I found.</p>
<!-- /wp:paragraph -->

<!-- wp:more -->
<!--more-->
<!-- /wp:more -->

<!-- wp:heading -->
<h2 class="wp-block-heading">First step of the issue solving</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>While researching how to fix the issue I found&nbsp;<a href="https://github.com/kovidgoyal/kitty/issues/606" target="_blank" rel="noreferrer noopener">this GitHub issue</a>&nbsp;with the fun number 606 (almost 666). First, I should say, that there is no easy solution. Shortly you have to specify for each shortcut mapping alternative with your keyboard layout. That means, for example, if your keyboard has Cyrillic "м" instead of Latin "v" then for making work CMD+V you should add also into configuration an additional line with "м".</p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">map cmd+v       paste_from_clipboard # default Latin 
map cmd+ъ       paste_from_clipboard # for default MacOS Bulgarian layout
map cmd+м       paste_from_clipboard # for default MacOS Ukrainian or Russian layout</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>Let't summarize. <strong>To resolve the issue just add for each shortcut additional mapping with your keyboard layout. </strong></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Pretty easy, isn't it? Yeah, but if you want to use all shortcuts you have to do the same for each one. And I don't want to be you if you want to make that for 2 or more different non-Latin layouts. </p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 class="wp-block-heading">Automated solution</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>I decided to create automated solution for that. Why? Because manually mapping each key is tedious. And would be great to share the process with you. If you aren't interested in the automation process then just use the Python script from <a href="https://github.com/Jakeroid/kitty-shortcuts-work-only-with-latin-characters" target="_blank" rel="noreferrer noopener">my GitHub repository</a>. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>For those who are interested in learning more. Let's do it together. The first step is creating a project directory. I usually prefer to use venv (virtual environment) in my Python scripts. </p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">mkdir kitty-shortcuts-work-only-with-latin-characters
cd kitty-shortcuts-work-only-with-latin-characters
python -m venv ./venv
source ./venv/bin/activate # maybe you should use activate.fish or other file depends on your shell type</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>Great! Now we're ready to start coding. Before making any changes, I'd like to create a backup of the kitty.config file. It would be ideal if the script could handle this task. On Unix systems, the default configuration file is located at ~/.config/kitty/kitty.conf. I suspect that Kitty may already store a backup in the file named ~/.config/kitty/kitty.conf.bak. To be safe, we should avoid overwriting the default backup location. Let's use current unix timestamp as an additional name part for the file.  </p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">import time  # we will use it to get unix timestamp
import os  # we will use it to expand user path

# config file paths
current_unix_timestamp = str(time.time())
default_config_file = '~/.config/kitty/kitty.conf'
current_backup_file = f'~/.config/kitty/kitty.conf.{current_unix_timestamp}.bak'
default_config_file_full_path = os.path.expanduser(default_config_file)
current_backup_file_full_path = os.path.expanduser(current_backup_file)</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>Time to make a code for making backup. </p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">import shutil  # we will use it to copy files

# backup config file
shutil.copyfile(default_config_file_full_path, current_backup_file_full_path)</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>Now we need to build a map of our keyboard layouts. In my case, I would create a mapping between the default English and Russian layouts because I use them most frequently for typing in any language. However, if your keyboard differs from the English US layout or if you use other non-Latin layouts, you should create your own map.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>To simplify things, I created a text file with two lines. The first line corresponds to my English layout, and the second line to my Russian layout. I simply typed each key on the keyboard once, following the same sequence. This means, for example, that the character at position number 7 in the first line is on the same key as the character at position number 7 in the second line, but they represent different characters because they correspond to different symbols.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Here is my example. </p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">qwertyuiop[]\asdfghjkl;'zxcvbnm,./
йцукенгшщзхъ\фывапролджэячсмитьбю.</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>Let's write some code to read that file and construct a map.</p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""># load map file
with open('map.txt') as f:
    lines = f.readlines()
line1 = lines[0].strip()
line2 = lines[1].strip()
map_dict = dict(zip(line1, line2))</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>Now we could use map_dict as a source of mapping. Here are a few examples.</p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""># get value for v in map_dict
print('Value of v is ', map_dict['v'])

# get value for c in map_dict
print('Value of c is ', map_dict['c'])

# get value for b in map_dict
print('Value of b is ', map_dict['b'])</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>The next step awaits us. I believe it would be a good and straightforward idea to implement the following algorithm:</p>
<!-- /wp:paragraph -->

<!-- wp:list {"ordered":true} -->
<ol><!-- wp:list-item -->
<li>Open the config file.</li>
<!-- /wp:list-item -->

<!-- wp:list-item -->
<li>Read it line by line.</li>
<!-- /wp:list-item -->

<!-- wp:list-item -->
<li>If a line does not contain a key map, then simply add it to the output as is.</li>
<!-- /wp:list-item -->

<!-- wp:list-item -->
<li>If a line contains a key map, then add the original line as well as an additional line.</li>
<!-- /wp:list-item -->

<!-- wp:list-item -->
<li>Save the output lines in the original file.</li>
<!-- /wp:list-item --></ol>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>There are also two important things. The config file contains lines with comments that we do not wish to alter. These documentation comments begin with "#::". We should skip those lines and leave them unmodified. Additionally, the file has mappings lines that are commented out with the symbol "#". These represent default Kitty shortcuts. For such lines, we should remove the comment character and include two lines in the output: the original one and a new one with the non-Latin mapping. This is necessary because our goal is to keep the shortcuts functional for both keyboard layouts.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>To find the correct mapping of the shortcut key, I use the split method on strings to determine the position of the shortcut key combination and then separate it into individual keys. We do not want to alter characters in keys such as "cmd", "alt", "ctrl", etc. However, we would like to change the mappings for keys like "k", "a", "c", etc.</p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""># read config file all lines
with open(default_config_file_full_path) as f:
    lines = f.readlines()

# for each line search mapping line and add additional line with mapping
new_lines = []
for line in lines:

    # if line doesn't contain map
    if ' map ' not in line:
        new_lines.append(line)
        continue

    # if line contains map but starts with #::
    if line.startswith('#::'):
        new_lines.append(line)
        continue

    # line contains map the first thing is to find mapping parts
    line_parts = line.split(' ')
    map_part_index = line_parts.index('map')
    keys_part_index = map_part_index + 1
    keys_list = line_parts[keys_part_index].split('+')

    # for each key in keys list find mapping
    new_keys_list = []
    for key in keys_list:
        if key not in map_dict:
            new_keys_list.append(key)
            continue
        new_keys_list.append(map_dict[key])

    # create new line with new keys list
    new_keys_part = '+'.join(new_keys_list)
    new_line = ''
    for i, part in enumerate(line_parts):
        if i == keys_part_index:
            new_line += new_keys_part
        else:
            new_line += part
        new_line += ' '

    # remove comment from line and new line if it exists
    if line.startswith('#'):
        line = line[1:].strip() + '\n'
        new_line = new_line[1:].strip() + '\n'

    new_lines.append(line)
    new_lines.append(new_line)

# write new config file
with open(default_config_file_full_path, 'w') as f:
    f.writelines(new_lines)</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>It looks understandable to me; however, if you have any questions, please ask them below. I hope this article helps you. Don't forget to follow me on X (formerly Twitter) or subscribe to the blog newsletter.</p>
<!-- /wp:paragraph -->