cx is a terminal pager with vim-style navigation that can execute commands against selected lines. Pipe any command's output into it, navigate with j/k, press a configured key on a line, and a shell command runs — with the matched text interpolated in.
ps -ax | cx
Press k on a process line → kill <pid>. Press K → kill -9 <pid>.
The key-to-command mappings live in ~/.cx.yaml. One config file handles all your use cases: process management, git log navigation, Docker container control, log inspection, and anything else that fits the pattern of "pick a line, do something with it."
pipx install cxOr from source:
git clone <repo>
cd cx
pipx install .cx requires Python 3.10+ and PyYAML. pipx handles the dependency automatically.
To install without pipx:
python3 -m venv ~/.venv/cx
~/.venv/cx/bin/pip install .
ln -s ~/.venv/cx/bin/cx ~/.local/bin/cxCopy the example config to your home directory:
cp cx.yaml.example ~/.cx.yamlThen pipe something into cx:
ps -ax | cxNavigate with j/k, press k to send SIGTERM to the selected process, K for SIGKILL. Press q to quit.
cxreads all of stdin into memory.- It opens an interactive curses screen listing every line.
- You navigate to a line and press a key.
cxscans~/.cx.yamlfor rules whosepatternmatches the selected line and whosekeymatches the key you pressed.- If a rule matches, it interpolates the command template with the regex capture groups and executes it in your shell.
- Command output is shown directly in the terminal. Press any key to return to the list.
If more than one rule matches the key on the current line, a small selection menu appears so you can choose which action to run.
| Key | Action |
|---|---|
j / ↓ |
Move down one line |
k / ↑ |
Move up one line |
g |
Jump to first line |
G |
Jump to last line |
Ctrl-F / Page Down |
Page down |
Ctrl-B / Page Up |
Page up |
q / Esc |
Quit |
Any other key triggers rule matching against the current line.
cx reads ~/.cx.yaml on startup. If the file does not exist, cx runs in read-only navigation mode (no actions).
rules:
- pattern: '<regex>'
key: '<single character>'
command: '<shell command template>'
description: '<shown in menus>' # optional- pattern — A Python regular expression.
cxusesre.search, so the pattern does not need to match the whole line. Use capture groups(...)or named groups(?P<name>...)to extract the parts you need. - key — Exactly one character. Case-sensitive.
kandKare distinct. - command — A shell command. Passed to
/bin/sh -c. May use pipes, redirects, and any shell syntax. - description — Optional. Displayed in the multi-rule selection menu and in the status bar. Falls back to the
commandstring if omitted. - exit — Optional boolean, default
false. Whentrue,cxexits immediately after the command finishes instead of returning to the list. The "press any key" prompt is also skipped, which is useful for commands likevimthat manage the terminal themselves.
Multiple rules may share the same key. When you press that key, all rules whose pattern matches the current line are collected. If there is exactly one match, the command runs immediately. If there are multiple matches, a menu lets you choose.
Within the command string, the following placeholders are replaced before the command is executed:
| Placeholder | Value |
|---|---|
{line} |
The full text of the selected line |
{0} |
The substring matched by pattern (the full regex match) |
{1}, {2}, ... |
Capture group 1, 2, ... from pattern |
{name} |
Named capture group (?P<name>...) |
Note: Values are substituted as plain strings with no shell escaping. If a captured value might contain spaces or special characters, quote the placeholder in the command:
command: 'grep -r "{1}" /var/log'rules:
- pattern: '^\s*(\d+)'
key: k
command: 'kill {1}'
description: 'Send SIGTERM'
- pattern: '^\s*(\d+)'
key: K
command: 'kill -9 {1}'
description: 'Send SIGKILL'
- pattern: '^\s*(\d+)'
key: i
command: 'ps -p {1} -o pid,ppid,user,%cpu,%mem,vsz,rss,stat,start,time,command | less'
description: 'Inspect process'rules:
- pattern: '^([0-9a-f]{7,40})'
key: d
command: 'git show {1} | less'
description: 'Show diff'
- pattern: '^([0-9a-f]{7,40})'
key: c
command: 'git checkout {1}'
description: 'Checkout'
- pattern: '^([0-9a-f]{7,40})'
key: r
command: 'git rebase -i {1}~1'
description: 'Interactive rebase from here'
- pattern: '^([0-9a-f]{7,40})'
key: p
command: 'git cherry-pick {1}'
description: 'Cherry-pick'rules:
- pattern: '^([0-9a-f]{12})'
key: s
command: 'docker stop {1}'
description: 'Stop container'
- pattern: '^([0-9a-f]{12})'
key: x
command: 'docker rm -f {1}'
description: 'Force remove container'
- pattern: '^([0-9a-f]{12})'
key: l
command: 'docker logs --tail 100 -f {1}'
description: 'Follow logs'
- pattern: '^([0-9a-f]{12})'
key: e
command: 'docker exec -it {1} /bin/sh'
description: 'Open shell'
exit: true # interactive shell; exit cx when donerules:
- pattern: ':(\d+)\s'
key: o
command: 'curl -s http://localhost:{1} | head -20'
description: 'curl localhost port'rules:
- pattern: '(\S+)$'
key: e
command: '$EDITOR {1}'
description: 'Open in editor'
exit: true # editor takes over the terminal; no need to return to cx
- pattern: '(\S+)$'
key: l
command: 'less {1}'
description: 'Page file'
- pattern: '(\S+)$'
key: d
command: 'rm {1}'
description: 'Delete file'rules:
- pattern: '\b([a-zA-Z0-9_-]+)\[(\d+)\]'
key: f
command: 'grep "{1}" /var/log/syslog | less'
description: 'Filter log by process name'rules:
- pattern: '([\w@.-]+\.service)'
key: r
command: 'sudo systemctl restart {1}'
description: 'Restart service'
- pattern: '([\w@.-]+\.service)'
key: j
command: 'journalctl -u {1} -n 50 | less'
description: 'View journal'
- pattern: '([\w@.-]+\.service)'
key: s
command: 'sudo systemctl stop {1}'
description: 'Stop service'Named groups make commands more readable when patterns are complex:
- pattern: '(?P<host>[\w.-]+)\s+(?P<port>\d+)'
key: c
command: 'ssh {host} -p {port}'
description: 'SSH to host'Short aliases make cx feel like a first-class command:
# ~/.bashrc or ~/.zshrc
alias psk='ps -ax | cx'
alias gll='git log --oneline | cx'
alias dps='docker ps | cx'
alias ssn='ss -tulpn | cx'Long lines: cx truncates lines at the terminal width for display but uses the full original line for pattern matching and {line} substitution.
Interactive commands: Commands like vim, less, docker exec -it, and ssh require a real terminal. They work correctly because cx suspends the curses display, hands the terminal back to the shell, and resumes only after the command exits.
Chaining: Because cx is a passive recipient of stdin, it composes freely with any command pipeline:
ps -ax | grep python | cx
find /var/log -name '*.log' -newer /tmp/marker | cx
kubectl get pods -A | grep CrashLoop | cxMultiple configs: The config path is always ~/.cx.yaml. To switch between different rule sets, use symlinks or a shell function that temporarily copies a file.
Escaping braces: To include a literal { or } in a command, the current interpolation engine treats {unknown_key} as a pass-through (the placeholder is left unchanged). Name your capture groups to avoid collisions with shell syntax.
Full annotated example (cx.yaml.example in the source tree):
rules:
- pattern: '<python regex>' # required; re.search is used
key: '<char>' # required; one character, case-sensitive
command: '<shell command>' # required; passed to sh -c
description: '<label>' # optional; shown in menus and status bar
exit: false # optional; true exits cx after the command runsErrors in the config (invalid regex, multi-character key) are reported on startup before the TUI opens, so you see them immediately.
MIT