Wouldn’t it be nice if you could navigate through your application’s menus using vi-like bindings? You can! Simply something like this in your ~/.gtkrc-2.0 file:

binding "gtk-binding-menu" {
    bind "j" { "move-current" (next) }
    bind "k" { "move-current" (prev) }
    bind "h" { "move-current" (parent) }
    bind "l" { "move-current" (child) }
}
class "GtkMenuShell" binding "gtk-binding-menu"

Keybindings for other GTK widgets can be defined in a similar way. What about moving focus between widgets in the direction they appear?

binding "gtk-binding-widget" {
    bind "j" { "move-focus" (up) }
    bind "k" { "move-focus" (down) }
    bind "h" { "move-focus" (left) }
    bind "l" { "move-focus" (right) }
    bind "m" { "popup-menu" () }
}
class "GtkWidget" binding "gtk-binding-widget"

The m binding lets you pop up the context menu in any widget that not steal the modifier-less keystrokes for text input. For such widgets, prepend a modifier:

    bind "<mod4>m" { "popup-menu" () }

Likewise, the move-focus bindings must also be prepended with a modifier if you want to navigate through text fields.

More examples

Of course you can’t use modal editing for text fields, but the following makes it possible to make small corrections without moving your hands to the arrow keys:

binding "gtk-binding-text" {
    bind "<mod3>j"         { "move-cursor" (display-lines, 1, 0) }
    bind "<mod3><shift>j"  { "move-cursor" (display-lines, 1, 1) }
    bind "<mod3>k"         { "move-cursor" (display-lines, -1, 0) }
    bind "<mod3><shift>k"  { "move-cursor" (display-lines, -1, 1) }
    bind "<mod3>l"         { "move-cursor" (logical-positions, 1, 0) }
    bind "<mod3><shift>i"  { "move-cursor" (logical-positions, 1, 1) }
    bind "<mod3>h"         { "move-cursor" (logical-positions, -1, 0) }
    bind "<mod3><shift>h"  { "move-cursor" (logical-positions, -1, 1) }
    bind "<mod3>g"         { "move-cursor" (buffer-ends, -1, 0) }
    bind "<mod3><shift>g"  { "move-cursor" (buffer-ends, -1, 1) }
    bind "<mod3>y"         { "move-cursor" (buffer-ends, 1, 0) }
    bind "<mod3><shift>y"  { "move-cursor" (buffer-ends, 1, 1) }
    bind "<mod3>u"         { "move-cursor" (words, 1, 0) }
    bind "<mod3><shift>u"  { "move-cursor" (words, 1, 1) }
    bind "<mod3>o"         { "move-cursor" (words, -1, 0) }
    bind "<mod3><shift>o"  { "move-cursor" (words, -1, 1) }

    # Xahlee'ish deletion
    bind "<mod3>d"         { "delete-from-cursor" (chars, -1) }
    bind "<mod3>f"         { "delete-from-cursor" (chars, 1) }
    bind "<mod3>e"         { "delete-from-cursor" (word-ends, -1) }
    bind "<mod3>r"         { "delete-from-cursor" (word-ends, 1) }
    bind "<mod3>s"         { "delete-from-cursor" (paragraph-ends, -1) }
    bind "<mod3>g"         { "delete-from-cursor" (paragraph-ends, 1) }

    # Emacs'ish deletion
    bind "<alt>BackSpace"  { "delete-from-cursor" (word-ends, -1) }
}
class "GtkEntry" binding "gtk-binding-text"
class "GtkTextView" binding "gtk-binding-text"

This one is my favorite and works  neatly in nautilus if you set the default view to list view:

binding "gtk-binding-tree-view" {
    bind "j"        { "move-cursor" (display-lines, 1) }
    bind "<shift>j" { "move-cursor" (display-lines, 1) }
    bind "k"        { "move-cursor" (display-lines, -1) }
    bind "<shift>k" { "move-cursor" (display-lines, -1) }
    bind "h"        { "move-cursor" (logical-positions, -1) }
    bind "<shift>h" { "move-cursor" (logical-positions, -1) }
    bind "l"        { "move-cursor" (logical-positions, 1) }
    bind "<shift>l" { "move-cursor" (logical-positions, 1) }
    bind "o"        { "move-cursor" (pages, 1) }
    bind "<shift>o" { "move-cursor" (pages, 1) }
    bind "u"        { "move-cursor" (pages, -1) }
    bind "<shift>u" { "move-cursor" (pages, -1) }
    bind "g"        { "move-cursor" (buffer-ends, -1) }
    bind "<shift>g" { "move-cursor" (buffer-ends, -1) }
    bind "y"        { "move-cursor" (buffer-ends, 1) }
    bind "<shift>y" { "move-cursor" (buffer-ends, 1) }
    bind "p"        { "select-cursor-parent" () }
    bind "i"        { "expand-collapse-cursor-row" (0,0,0) }
    bind "semicolon"        { "expand-collapse-cursor-row" (0,1,0) }
    bind "<shift>semicolon" { "expand-collapse-cursor-row" (0,1,1) }
    bind "slash"    { "start-interactive-search" () }
}
class "GtkTreeView" binding "gtk-binding-tree-view"

Note that you can’t use the keys you redefine to autostart interactive search any more — you have to start it manually, in this case by using slash (/). However, I find it faster to navigate this way. Redefine the rest of the alphanummeric keys to some dummy action if you get disturbed by the interactive search when you accidently hit one of the keys not redefined.

You could also experiment with GtkPanedGtkNotebook, GtkScrolledWindow and GtkIconView:

binding "gtk-binding-paned" {
    bind "<mod4>s" { "cycle-child-focus" (0) }
    bind "<mod4><shift>s" { "cycle-child-focus" (1) }
}
class "GtkPaned" binding "gtk-binding-paned"
binding "gtk-binding-notebook" {
    bind "<mod4>d" { "change-current-page" (-1) }
    bind "<mod4>f" { "change-current-page" (1) }
}
class "GtkNotebook" binding "gtk-binding-notebook"
binding "gtk-binding-scrolled-window" {
    bind "<mod4>j" { "scroll-child" (step-down, 0) }
    bind "<mod4>k" { "scroll-child" (step-up, 0) }
    bind "<mod4>h" { "scroll-child" (step-left, 0) }
    bind "<mod4>l" { "scroll-child" (step-right, 0) }
    bind "<mod4>o" { "scroll-child" (page-down, 0) }
    bind "<mod4>u" { "scroll-child" (page-up, 0) }
    bind "<mod4>g" { "scroll-child" (start, 0) }
    bind "<mod4>y" { "scroll-child" (end, 0) }
}
class "GtkScrolledWindow" binding "gtk-binding-scrolled-window"
binding "gtk-binding-icon-view" {
    bind "j"        { "move-cursor" (display-lines, 1) }
    bind "<shift>j" { "move-cursor" (display-lines, 1) }
    bind "k"        { "move-cursor" (display-lines, -1) }
    bind "<shift>k" { "move-cursor" (display-lines, -1) }
    bind "h"        { "move-cursor" (logical-positions, -1) }
    bind "<shift>h" { "move-cursor" (logical-positions, -1) }
    bind "l"        { "move-cursor" (logical-positions, 1) }
    bind "<shift>l" { "move-cursor" (logical-positions, 1) }
    bind "o"        { "move-cursor" (pages, 1) }
    bind "<shift>o" { "move-cursor" (pages, 1) }
    bind "u"        { "move-cursor" (pages, -1) }
    bind "<shift>u" { "move-cursor" (pages, -1) }
    bind "g"        { "move-cursor" (buffer-ends, -1) }
    bind "<shift>g" { "move-cursor" (buffer-ends, -1) }
    bind "y"        { "move-cursor" (buffer-ends, 1) }
    bind "<shift>y" { "move-cursor" (buffer-ends, 1) }
}
class "GtkIconView" binding "gtk-binding-icon-view"

Note that the bindings does not work everywhere. For instance, nautilus does not use GtkIconView for the icon view, nor GtkNotebook for tabs, so you must use the list view to use bindings to navigate between files, as well as use nautilus’ own tab bindings (which can be configured with gtkcanchangeaccels).

Further information

The best place to get further information on customizing GTK application is the API. It describes the syntax for .gtkrc, and the actions available for each widget. The actions available are the signals with the label “action”. Also see GtkSettings for miscellaneous settings that affects application behavior.

Finally it should be noted that the layout of bindings in this post serve as example only. It is not the layout that I actually use, as I have lots other bindings competing for the available key combinations 🙂