Fixing ^w in tremc

Python Transmission tremc — Published on .

I like collecting GNU+Linux ISOs. I’d like to think I have a pretty serious collection of these things. I have a pretty good and stable Internet connection, so I collect them in my torrent client, and let them seed pretty much forever, so other people can enjoy them too.

For this hobby, I’m using Transmission, with tremc as the frontend. I’ve been using it for a while, but there’s always been a small thing bothering me. Whenever you get a prompt to specify the name of the torrent, or the directory to which its contents should be downloaded, ^w immediately kills the entire application.

By regular shell standards, I’m used to ^w not killing the application, but just removing from the cursor to the start of the previous word. I don’t often change the names of the torrents, but when I do, I often use ^w to quickly remove one or two words in the path. With tremc, this sadly means me killing the application, being upset for a little while as I restart it, and hold down the backspace for a while to get the intended effect.

Until last night. I set out to read the documentation to fix this issue once and for all. I dug into the manual, which specifies that a configuration file at ~/.config/tremc/settings.cfg is read at startup. However, the manual doesn’t specify anything about the format of this file. There’s also no other manual pages included in the package.

The Gentoo package specifies a homepage for this project on Github, so I open up my least-hated browser and see if there’s anything there. Good news, the repository contains a sample settings.cfg, so I can read that and get an idea on how to do keybinds. And the examples do show how to set keybinds. But it doesn’t seem to be able to remove keybinds. This is kind of a bummer. Setting a keybind to an empty value didn’t seem to do the trick either. This leaves only one option, patching the defaults.

This was actually pretty straightforward, just look for the list of default keybinds, and remove a single line.

@@ -222,7 +222,6 @@ class GConfig:
             # First in list: 0=all 1=list 2=details 3=files 4=tracker 16=movement
             # +256 for RPC>=14, +512 for RPC>=16
             'list_key_bindings': [0, ['F1', '?'], 'List key bindings'],
-            'quit_now': [0, ['^w'], 'Quit immediately'],
             'quit': [1, ['q'], 'Quit'],
             'leave_details': [2, ['BACKSPACE', 'q'], 'Back to torrent list'],
             'go_back_or_unfocus': [2, ['ESC', 'BREAK'], 'Unfocus or back to torrent list'],

Since I’m just testing, and this program is a single Python file, I just edit the file in-place, and delay making a proper patch out of for later. Restarting tremc, going to the new torrent, pressing m (for “move”), and hitting ^w to try and remove a single word, hoping for a quick and easy fix, I was met with tremc just quitting again. This was not what I wanted.

So opening up the file again with everyone’s favourite editor, I search around for the quit_now function. Surely that’ll bring me closer? The quit_now string doesn’t seem to be used anywhere else, apart from the function that defines what the keybind action should do, action_quit_now. This seems to simply defer to exit_now, which leads me to a bit of code with a special case to exit_now if a certain character is detected. This character appears to be a ^w, which is exactly what I’m trying to stop. So, let’s patch that out too.

@@ -3760,10 +3759,7 @@ class Interface:
                 self.update_torrent_list([win])

     def wingetch(self, win):
-        c = win.getch()
-        if c == K.W_:
-            self.exit_now = True
-        return c
+        return win.getch()

     def win_message(self, win, height, width, message, first=0):
         ypos = 1

Restart tremc and test again. Still exiting immediately upon getting a ^w. This bit of code gives me some new insights, it appears K.W_ is related to the keycode of a ^w. So I continue the search, this time looking for K.W_. This appears to be used later on to create a list of characters which should act as esc in certain contexts. Removing the K.W_ from this is simple enough.

@@ -5039,7 +5047,7 @@ def parse_config_key(interface, config, gconfig, common_keys, details_keys, list
     else:
         gconfig.esc_keys = (K.ESC, K.q, curses.KEY_BREAK)
     gconfig.esc_keys_no_ascii = tuple(x for x in gconfig.esc_keys if x not in range(32, 127))
-    gconfig.esc_keys_w = gconfig.esc_keys + (K.W_,)
+    gconfig.esc_keys_w = gconfig.esc_keys
     gconfig.esc_keys_w_enter = gconfig.esc_keys_w + (K.LF, K.CR, curses.KEY_ENTER)
     gconfig.esc_keys_w_no_ascii = tuple(x for x in gconfig.esc_keys_w if x not in range(32, 127))

Restart, test, and… Nothing. This is an improvement, the program isn’t exiting immediately, it just does nothing. I know that ^u works as I expect, so perhaps it needs some love to have ^w work properly as well. I search for K.U_, and indeed, there is code dedicated to this keypress, in a long elif construct. So I add some code to get ^w working as well. After a bit of fiddling, and realizing I’ve spent way too much time on adding a torrent, I’ve settled on this little bit of love.

@@ -3957,6 +3953,18 @@ class Interface:
                 # Delete from cursor until beginning of line
                 text = text[index:]
                 index = 0
+            elif c == K.W_:
+                # Delete from cursor to beginning of previous word... mostly
+                text_match = re.search("[\W\s]+", text[::-1])
+
+                if text_match.span()[0] == 0:
+                    # This means the match was found immediately, I can't be
+                    # bothered to make this any nicer.
+                    text = text[:-1]
+                    index -= 1
+                else:
+                    text = text[:-text_match.span()[0]]
+                    index -= text_match.span()[0]
             elif c in (curses.KEY_HOME, K.A_):
                 index = 0
             elif c in (curses.KEY_END, K.E_):

It looks for the first non-word character or a space, starting from the end of the string ([::-1] reverses a string in Python). The resulting Match object can tell me how many characters I need to delete in the .span()[0] value. A small exception is created if that value is 0, otherwise the logic below doesn’t work well.

It’s not perfect, but it gets the job done well enough, and I don’t like Python enough to spend more time on it than I’ve already done. I am open for better solutions that work better, though!