Skip to content

Commit

Permalink
Merge pull request #5 from adammillerio/pr5
Browse files Browse the repository at this point in the history
keyslib: add tmux plugin, start README
  • Loading branch information
adammillerio authored Oct 10, 2024
2 parents e75c9d9 + 7b5f592 commit 3ad5092
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 16 deletions.
78 changes: 77 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,78 @@
# keyslib
a python library and cli for working with key bindings

`keyslib` is a Python library for working with key sequences. It has a standard
grammar for expressing them, as well as `pathlib`-like overloads of standard binary
operators for composing key sequences in code:

```python
from keyslib import KeySequence

TMUX_PREFIX = KeySequence("(ctrl)b")
TMUX_CREATE_WINDOW = TMUX_PREFIX + "c"

# (ctrl)b+c
print(TMUX_CREATE_WINDOW)
```

In addition to key parsing, `keyslib` also has formatters for other applications and their key sequence formats, as well as a `keys` CLI:
```sh
# For a tmux send-keys command:
keys format tmux "(ctrl)b+c"
C-b c

# For a Visual Studio Code keybindings.json file:
keys format vscode "(ctrl)b+c"
ctrl+b c

# For a call to the Hammerspoon mac automation framework:
keys format hammerspoon "(ctrl)b+c"
hs.eventtap.keyStroke({"ctrl"}, "b"); hs.eventtap.keyStroke({}, "c");
```

The `keys` CLI can also directly control applications using the `send` command:
```sh
# tmux send-keys 'C-b' 'c'
keys send tmux "(ctrl)b+c"

# echo '\x02c' | wezterm cli send-text --no-paste
keys send wezterm "(ctrl)b+c"

# hs -q -c 'hs.eventtap.keyStroke({"ctrl"}, "b"); hs.eventtap.keyStroke({}, "c");'
keys send hammerspoon "(ctrl)b+c"
```

Collections of key bindings can be stored in the form of "keys files", which are
just [dotenv](https://saurabh-kumar.com/python-dotenv/#file-format) files:

[`example/binds/tmux.env`](example/binds/tmux.env):
```sh
CREATE_WINDOW='(ctrl)b+c #window Create window'
SPLIT_WINDOW_VERTICAL='(ctrl)b+% #pane Create vertical split'
SPLIT_WINDOW_HORIZONTAL='(ctrl)b+<quote> #pane Create horizontal split'
```

These files can be written by hand or generated via Python and the `keys build`
command. See [`example/keys/tmux.py`](example/keys/tmux.py) for an example.

Putting it all together, the [`tools/kcmp.sh`](tools/kcmp.sh) script provides
an interactive key completion menu via the [fzf](https://github.com/junegunn/fzf)
CLI:

`tools/kcmp.sh tmux nvim`:
![keyslib-kcmp.png](https://raw.githubusercontent.com/adammillerio/i/refs/heads/main/keyslib-kcmp.png)

In this example, keys are loaded from `~/.config/keyslib/binds/nvim.env` and are
sent via a `tmux send-keys` command.

Additionally, a tmux plugin is available to bind `kcmp.sh` to a hotkey:
![keyslib-tmux-htop.png](https://raw.githubusercontent.com/adammillerio/i/refs/heads/main/keyslib-tmux-htop.png)


When the complete hotkey (`(ctrl)<space>` by default) is pressed, the plugin
will use `tmux list-panes -F '#{pane_current_command}'` to determine the current
running command. If there is a matching keys file at
`~/.config/keyslib/binds/<cmd>.env`, it will be displayed in a tmux window via
`kcmp.sh`. The selected key sequence is then sent to the active pane via tmux.
This provides a context-specific "command palette" like experience for terminal
applications.

62 changes: 62 additions & 0 deletions example/binds/tmux.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
KEYS_COMPLETE='(ctrl)<space> #keys Show completion palette'
RENAME_SESSION='(ctrl)b+$ #session Rename session'
DETACH_SESSION='(ctrl)b+d #session Detach from session'
SHOW_SESSION='(ctrl)b+s #session Show all sessions'
SHOW_PREVIEW='(ctrl)b+w #session Show session and window preview'
PREVIOUS_SESSION='(ctrl)b+<lparen> #session Previous session'
NEXT_SESSION='(ctrl)b+<rparen> #session Next session'
CREATE_WINDOW='(ctrl)b+c #window Create window'
RENAME_WINDOW='(ctrl)b+, #window Rename current window'
CLOSE_WINDOW='(ctrl)b+& #window Close current window'
LIST_WINDOW='(ctrl)b+w #window List windows'
PREVIOUS_WINDOW='(ctrl)b+p #window Previous window'
NEXT_WINDOW='(ctrl)b+n #window Next window'
TOGGLE_WINDOW='(ctrl)b+l #window Toggle last active window'
CLOSE_PANE='(ctrl)b+x #pane Close current pane'
TOGGLE_PANE='(ctrl)b+; #pane Toggle last active pane'
MOVE_PANE_LEFT='(ctrl)b+{ #pane Move the current pane left'
MOVE_PANE_RIGHT='(ctrl)b+} #pane Move the current pane right'
TOGGLE_PANE_LAYOUT='(ctrl)b+<space> #pane Toggle between pane layouts'
NEXT_PANE='(ctrl)b+o #pane Switch to next pane'
SHOW_PANE_NUMBERS='(ctrl)b+q #pane Show pane numbers'
TOGGLE_PANE_ZOOM='(ctrl)b+z #pane Toggle pane zoom'
CONVERT_PANE_WINDOW='(ctrl)b+1 #pane Convert pane into a window'
SPLIT_WINDOW_VERTICAL='(ctrl)b+% #pane Create vertical split'
SPLIT_WINDOW_HORIZONTAL='(ctrl)b+<quote> #pane Create horizontal split'
SHOW_COMMAND_PROMPT='(ctrl)b+: #misc Enter command mode'
LIST_KEYS='(ctrl)b+? #misc List key bindings in tmux'
ENTER_COPY='(ctrl)b+[ #misc Enter copy mode'
ENTER_CLOCK_MODE='(ctrl)b+t #misc Display a large clock'
SELECT_WINDOW_0='(ctrl)b+0 #window_select Select window 0'
SELECT_WINDOW_1='(ctrl)b+1 #window_select Select window 1'
SELECT_WINDOW_2='(ctrl)b+2 #window_select Select window 2'
SELECT_WINDOW_3='(ctrl)b+3 #window_select Select window 3'
SELECT_WINDOW_4='(ctrl)b+4 #window_select Select window 4'
SELECT_WINDOW_5='(ctrl)b+5 #window_select Select window 5'
SELECT_WINDOW_6='(ctrl)b+6 #window_select Select window 6'
SELECT_WINDOW_7='(ctrl)b+7 #window_select Select window 7'
SELECT_WINDOW_8='(ctrl)b+8 #window_select Select window 8'
SELECT_WINDOW_9='(ctrl)b+9 #window_select Select window 9'
SELECT_PANE_UP='(ctrl)b+<up> #pane_select Select pane above'
SELECT_PANE_DOWN='(ctrl)b+<down> #pane_select Select pane below'
SELECT_PANE_LEFT='(ctrl)b+<left> #pane_select Select pane to the left'
SELECT_PANE_RIGHT='(ctrl)b+<right> #pane_select Select pane to the right'
SELECT_PANE_0='(ctrl)b+q+0 #pane_select Select pane 0'
SELECT_PANE_1='(ctrl)b+q+1 #pane_select Select pane 1'
SELECT_PANE_2='(ctrl)b+q+2 #pane_select Select pane 2'
SELECT_PANE_3='(ctrl)b+q+3 #pane_select Select pane 3'
SELECT_PANE_4='(ctrl)b+q+4 #pane_select Select pane 4'
SELECT_PANE_5='(ctrl)b+q+5 #pane_select Select pane 5'
SELECT_PANE_6='(ctrl)b+q+6 #pane_select Select pane 6'
SELECT_PANE_7='(ctrl)b+q+7 #pane_select Select pane 7'
SELECT_PANE_8='(ctrl)b+q+8 #pane_select Select pane 8'
SELECT_PANE_9='(ctrl)b+q+9 #pane_select Select pane 9'
UPDATE_PLUGINS='(ctrl)b+U #tpm Update plugins'
UNINSTALL_PLUGINS='(ctrl)b+(alt)u #tpm Uninstall plugins not in the plugin list'
RELOAD_PLUGINS='(ctrl)b+I #tpm Install any new plugins and refresh tmux environment'
SAVE_SESSION='(ctrl)b+(ctrl)s #resurrect Save current session'
RESTORE_SESSION='(ctrl)b+(ctrl)r #resurrect Restore saved session'
CLEAR_HISTORY='(ctrl)b+(alt)c #logging Clear the history for the current pane'
TOGGLE_LOGGING='(ctrl)b+P #logging Toggle logging of the current pane to file'
PRINT_SCREEN='(ctrl)b+(alt)p #logging Save visible text of the current pane to file'
SAVE_HISTORY='(ctrl)b+(alt)P #logging Save entire history of the current pane to file'
1 change: 1 addition & 0 deletions example/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
# ruff: noqa: F401, F811

from keys.htop import __name__
from keys.tmux import __name__
153 changes: 153 additions & 0 deletions example/keys/tmux.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
from typing import Dict

from keyslib import KeySequence
from keyslib.builder import bind_multi, bind

APP = "tmux"

PREFIX = KeySequence("(ctrl)b")

bind(APP, "KEYS_COMPLETE", "(ctrl)<space> #keys Show completion palette")

SESSION_BINDS: Dict[str, KeySequence] = {
# Session management
"RENAME_SESSION": PREFIX + "$ #session Rename session",
"DETACH_SESSION": PREFIX + "d #session Detach from session",
"SHOW_SESSION": PREFIX + "s #session Show all sessions",
"SHOW_PREVIEW": PREFIX + "w #session Show session and window preview",
"PREVIOUS_SESSION": PREFIX + "<lparen> #session Previous session",
"NEXT_SESSION": PREFIX + "<rparen> #session Next session",
}
bind_multi(APP, SESSION_BINDS)

WINDOW_BINDS: Dict[str, KeySequence] = {
# https://tmuxcheatsheet.com
# Window management
"CREATE_WINDOW": PREFIX + "c #window Create window",
"RENAME_WINDOW": PREFIX + ", #window Rename current window",
"CLOSE_WINDOW": PREFIX + "& #window Close current window",
"LIST_WINDOW": PREFIX + "w #window List windows",
"PREVIOUS_WINDOW": PREFIX + "p #window Previous window",
"NEXT_WINDOW": PREFIX + "n #window Next window",
"TOGGLE_WINDOW": PREFIX + "l #window Toggle last active window",
# TODO: Add some sort of ability to specify sequence literals ie
# "COPY_CAPTURE_PANE": PREFIX + ":" + "capture-pane",
}
bind_multi(APP, WINDOW_BINDS)

PANE_BINDS: Dict[str, KeySequence] = {
# Pane management
"CLOSE_PANE": PREFIX + "x #pane Close current pane",
"TOGGLE_PANE": PREFIX + "; #pane Toggle last active pane",
"MOVE_PANE_LEFT": PREFIX + "{ #pane Move the current pane left",
"MOVE_PANE_RIGHT": PREFIX + "} #pane Move the current pane right",
"TOGGLE_PANE_LAYOUT": PREFIX + "<space> #pane Toggle between pane layouts",
"NEXT_PANE": PREFIX + "o #pane Switch to next pane",
"SHOW_PANE_NUMBERS": PREFIX + "q #pane Show pane numbers",
"TOGGLE_PANE_ZOOM": PREFIX + "z #pane Toggle pane zoom",
"CONVERT_PANE_WINDOW": PREFIX + "1 #pane Convert pane into a window",
"SPLIT_WINDOW_VERTICAL": PREFIX + "% #pane Create vertical split",
"SPLIT_WINDOW_HORIZONTAL": PREFIX + "<quote> #pane Create horizontal split",
# TODO: Implement handling of arrow keys
# # Resize current pane height up
# "RESIZE_PANE_UP": "(ctrl)<up>",
# # Resize current pane height down
# "RESIZE_PANE_DOWN": "(ctrl)<down>",
# # Resize current pane left
# "RESIZE_PANE_LEFT": "(ctrl)<left>",
# # Resize current pane right
# "RESIZE_PANE_RIGHT": "(ctrl)<right>",
}
bind_multi(APP, PANE_BINDS)

MISC_BINDS: Dict[str, KeySequence] = {
"SHOW_COMMAND_PROMPT": PREFIX + ": #misc Enter command mode",
"LIST_KEYS": PREFIX + "? #misc List key bindings in tmux",
"ENTER_COPY": PREFIX + "[ #misc Enter copy mode",
"ENTER_CLOCK_MODE": PREFIX + "t #misc Display a large clock",
# Enter copy mode and scroll one page up
# TODO: Handle <pageup> unicode escape sequence
# "ENTER_COPY_UP": PREFIX + "<pageup>",
}
bind_multi(APP, MISC_BINDS)


# Select window <n>
WINDOW_SELECT_BINDS: Dict[str, KeySequence] = {
f"SELECT_WINDOW_{n}": PREFIX + f"{n} #window_select Select window {n}"
for n in range(0, 10)
}
bind_multi(APP, WINDOW_SELECT_BINDS)

# Select pane <n>
PANE_SELECT_BINDS: Dict[str, KeySequence] = {
"SELECT_PANE_UP": PREFIX + "<up> #pane_select Select pane above",
"SELECT_PANE_DOWN": PREFIX + "<down> #pane_select Select pane below",
"SELECT_PANE_LEFT": PREFIX + "<left> #pane_select Select pane to the left",
"SELECT_PANE_RIGHT": PREFIX + "<right> #pane_select Select pane to the right",
}
PANE_SELECT_BINDS.update(
{
f"SELECT_PANE_{n}": PREFIX + f"q+{n} #pane_select Select pane {n}"
for n in range(0, 10)
}
)
bind_multi(APP, PANE_SELECT_BINDS)

# tmux plugin manager
# https://github.com/tmux-plugins/tpm
TPM_BINDS: Dict[str, KeySequence] = {
"UPDATE_PLUGINS": PREFIX + "U #tpm Update plugins",
"UNINSTALL_PLUGINS": PREFIX
+ "(alt)u #tpm Uninstall plugins not in the plugin list",
"RELOAD_PLUGINS": PREFIX
+ "I #tpm Install any new plugins and refresh tmux environment",
}
bind_multi("tmux", TPM_BINDS)

# tmux-resurrect plugin binds
# https://github.com/tmux-plugins/tmux-resurrect
RESURRECT_BINDS: Dict[str, KeySequence] = {
"SAVE_SESSION": PREFIX + "(ctrl)s #resurrect Save current session",
"RESTORE_SESSION": PREFIX + "(ctrl)r #resurrect Restore saved session",
}
bind_multi("tmux", RESURRECT_BINDS)

# tmux-logging plugin binds
# https://github.com/tmux-plugins/tmux-logging
LOGGING_BINDS: Dict[str, KeySequence] = {
"CLEAR_HISTORY": PREFIX + "(alt)c #logging Clear the history for the current pane",
"TOGGLE_LOGGING": PREFIX
+ "P #logging Toggle logging of the current pane to file",
"PRINT_SCREEN": PREFIX
+ "(alt)p #logging Save visible text of the current pane to file",
"SAVE_HISTORY": PREFIX
+ "(alt)P #logging Save entire history of the current pane to file",
}
bind_multi("tmux", LOGGING_BINDS)

# TODO: Figure out what I want to do with these, since they are "contextual"
# keybinds
# COPY_MODE_BINDS = {
# # Quit copy mode
# "COPY_EXIT": "q",
# # Copy: Go to top line
# "COPY_GO_TOP": "g",
# # Copy: Go to bottom line
# "COPY_GO_BOTTOM": "G",
# # Copy: Scroll up
# "COPY_SCROLL_UP": "<up>",
# # Copy: Scroll down
# "COPY_SCROLL_DOWN": "<down>",
# # Copy: Move cursor up
# "COPY_MOVE_UP": "k",
# # Copy: Move cursor down
# "COPY_MOVE_DOWN": "j",
# # Copy: Move cursor left
# "COPY_MOVE_LEFT": "h",
# # Copy: Move cursor right
# "COPY_MOVE_RIGHT": "l",
# }
#
# bind_multi("tmux", COPY_MODE_BINDS)
9 changes: 5 additions & 4 deletions keyslib.tmux
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

source "$CURRENT_DIR/tmux/variables.sh"

default_complete_key="Space"
complete_key=$(tmux show-option -gqv "@keys_complete_key")
complete_key=${logging_key:-$default_complete_key}

main() {
tmux bind-key -T prefix "$completion_key" run-shell "$CURRENT_DIR/tmux/keymux.sh"
tmux bind-key -T prefix "$complete_key" run-shell "$CURRENT_DIR/tools/keymux.sh"
# TODO: This should be enabled via a separate option
tmux bind-key -T root "C-$completion_key" run-shell "$CURRENT_DIR/tmux/keymux.sh"
tmux bind-key -T root "C-$complete_key" run-shell "$CURRENT_DIR/tools/keymux.sh"
}

main
6 changes: 0 additions & 6 deletions tmux/variables.sh

This file was deleted.

7 changes: 4 additions & 3 deletions tools/kcmp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
KEYS_SENDER=${1:?'ERROR: KEYS_SENDER not provided'}
KEYS_APP=${2:?'ERROR: KEYS_APP not provided'}

# Command for running keys - defaults to keys cli in PATH
KEYS=${KEYS_CMD:-'keys'}
# Command for running keys - defaults to running keys cli via uvx (uv tool run)
# https://github.com/astral-sh/uv
KEYS=${KEYS_CMD:-'uvx --from keyslib keys'}

# Local state dir for storing fzf query history - defaults to ~/.local/state/keyslib
KEYS_XDG=${XDG_STATE_HOME:-"${HOME}/.local/state"}
Expand Down Expand Up @@ -75,5 +76,5 @@ fi

# Send selection, using special bind parsing for fzf, see the cli help for more
# info
"${KEYS}" send --fzf "${KEYS_SENDER}" "${KEYS_CMD}"
${KEYS} send --fzf "${KEYS_SENDER}" "${KEYS_CMD}"

4 changes: 2 additions & 2 deletions tmux/keymux.sh → tools/keymux.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#!/bin/bash
# keymux.sh
# Simple helper script for routing to keys completion for the current tmux pane
# This is bound to @keys_completion_key in the keyslib tmux plugin
# This is bound to @keys_complete_key in the keyslib tmux plugin

CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# Get the current command of the pane this is running in (ie nvim)
CURRENT_CMD=$(tmux list-panes -F '#{pane_current_command}')

# Attempt to run key completion for current command
# Attempt to run keys complete for current command
"${CURRENT_DIR}/kcmp.sh" tmux "${CURRENT_CMD}"

RETURN_CODE=$?
Expand Down

0 comments on commit 3ad5092

Please sign in to comment.