From 93de73a77aa32a01086ee66fe7d85b0f06e26cc2 Mon Sep 17 00:00:00 2001 From: Tuna Cuma Date: Tue, 23 Dec 2025 13:02:54 +0300 Subject: [PATCH] feat(plugins): add zsh-vi-man plugin for smart man page lookup in vi mode --- plugins/zsh-vi-man/README.md | 67 ++++++++++++++ plugins/zsh-vi-man/zsh-vi-man.plugin.zsh | 7 ++ plugins/zsh-vi-man/zsh-vi-man.zsh | 108 +++++++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 plugins/zsh-vi-man/README.md create mode 100644 plugins/zsh-vi-man/zsh-vi-man.plugin.zsh create mode 100644 plugins/zsh-vi-man/zsh-vi-man.zsh diff --git a/plugins/zsh-vi-man/README.md b/plugins/zsh-vi-man/README.md new file mode 100644 index 000000000..65cdf79a2 --- /dev/null +++ b/plugins/zsh-vi-man/README.md @@ -0,0 +1,67 @@ +# zsh-vi-man plugin + +Smart man page lookup for zsh vi mode. Press `K` (Shift-K) on any command or option to instantly open its man page. + +To use it, add `zsh-vi-man` to the plugins array in your zshrc file: + +```zsh +plugins=(... zsh-vi-man) +``` + +## Features + +- **Smart Detection**: Automatically finds the right man page for subcommands (e.g., `git commit` → `man git-commit`) +- **Option Jumping**: Opens man page directly at the option definition (e.g., `grep -r` → jumps to `-r` entry) +- **Combined Options**: Works with combined short options (e.g., `rm -rf` → finds both `-r` and `-f`) +- **Value Extraction**: Handles options with values (e.g., `--color=always` → searches `--color`) +- **Pipe Support**: Detects correct command in pipelines (e.g., `cat file | grep -i` → opens `man grep`) +- **Multiple Formats**: Supports various man page styles (GNU, jq, find, etc.) + +## Usage + +1. Type a command (e.g., `ls -la` or `git commit --amend`) +2. Press `Escape` to enter vi normal mode +3. Move cursor to any word +4. Press **`K`** to open the man page + +### Examples + +| Command | Cursor On | Result | +| :--------------------- | :------------- | :----------------------------------- | +| `ls -la` | `ls` | Opens `man ls` | +| `ls -la` | `-la` | Opens `man ls`, jumps to `-l` | +| `git commit --amend` | `commit` | Opens `man git-commit` | +| `grep --color=auto` | `--color=auto` | Opens `man grep`, jumps to `--color` | +| `cat file \| sort -r` | `-r` | Opens `man sort`, jumps to `-r` | +| `find . -name "*.txt"` | `-name` | Opens `man find`, jumps to `-name` | + +## Configuration + +Set these variables **before** sourcing oh-my-zsh: + +```zsh +# Change the trigger key (default: K) +ZVM_MAN_KEY='?' + +# Use a different pager (default: less) +ZVM_MAN_PAGER='bat' +``` + +## Integration with zsh-vi-mode + +This plugin works seamlessly with [zsh-vi-mode](https://github.com/jeffreytse/zsh-vi-mode). It automatically detects zsh-vi-mode and hooks into its lazy keybindings system. + +For best results, ensure zsh-vi-mode is loaded before this plugin: + +```zsh +plugins=(... zsh-vi-mode zsh-vi-man) +``` + +## Requirements + +- zsh with vi mode enabled (built-in or via `vi-mode` plugin) +- `man` command available + +## License + +MIT License - see [LICENSE](https://github.com/TunaCuma/zsh-vi-man/blob/main/LICENSE) for details. diff --git a/plugins/zsh-vi-man/zsh-vi-man.plugin.zsh b/plugins/zsh-vi-man/zsh-vi-man.plugin.zsh new file mode 100644 index 000000000..0076fd797 --- /dev/null +++ b/plugins/zsh-vi-man/zsh-vi-man.plugin.zsh @@ -0,0 +1,7 @@ +# According to the standard: +# https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html +0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}" +0="${${(M)0:#/*}:-$PWD/$0}" + +source "${0:h}/zsh-vi-man.zsh" + diff --git a/plugins/zsh-vi-man/zsh-vi-man.zsh b/plugins/zsh-vi-man/zsh-vi-man.zsh new file mode 100644 index 000000000..1923428a9 --- /dev/null +++ b/plugins/zsh-vi-man/zsh-vi-man.zsh @@ -0,0 +1,108 @@ +# zsh-vi-man.zsh -- Smart man page viewer for zsh vi mode +# https://github.com/TunaCuma/zsh-vi-man +# +# Press K in vi normal mode to open the man page for the current command. +# If your cursor is on an option (like -r or --recursive), it will jump +# directly to that option in the man page. +# +# MIT License - Copyright (c) 2025 Tuna Cuma + +# Configuration variables (can be set before sourcing) +# ZVM_MAN_KEY: the key to trigger man page lookup (default: K) +# ZVM_MAN_PAGER: the pager to use (default: less) + +: ${ZVM_MAN_KEY:=K} +: ${ZVM_MAN_PAGER:=less} + +function zvm-man() { + # Get the word at cursor position + local left="${LBUFFER##*[[:space:]]}" + local right="${RBUFFER%%[[:space:]]*}" + local word="${left}${right}" + + # Find the current command segment (handles pipes: tree | grep -A) + # Get everything after the last pipe before cursor + local current_segment="${LBUFFER##*|}" + # Trim leading whitespace + current_segment="${current_segment#"${current_segment%%[![:space:]]*}"}" + # Extract the command (first word of segment) + local cmd="${current_segment%%[[:space:]]*}" + + if [[ -z "$cmd" ]]; then + zle -M "No command found" + return 1 + fi + + # Determine the man page to open + local man_page="$cmd" + local rest="${current_segment#*[[:space:]]}" + local potential_subcommand="${rest%%[[:space:]]*}" + + # Check for subcommand man pages (e.g., git-commit, docker-run) + if [[ -n "$potential_subcommand" && ! "$potential_subcommand" =~ ^- ]]; then + if man -w "${cmd}-${potential_subcommand}" &>/dev/null; then + man_page="${cmd}-${potential_subcommand}" + fi + fi + + # Build the search pattern for the current word + # Patterns match option definitions: lines starting with whitespace then dash + # Supports comma-separated (GNU style) and slash-separated (jq style) options + local pattern="" + if [[ -n "$word" ]]; then + # Long option with value: --color=always -> search for --color + if [[ "$word" =~ ^--[^=]+= ]]; then + local opt="${word%%=*}" + pattern="^[[:space:]]*${opt}([,/=:[[:space:]]|$)|^[[:space:]]*-.*[,/][[:space:]]+${opt}([,/=:[[:space:]]|$)" + # Combined short options: -rf -> search for -[rf] to find individual options + # Also includes fallback for single-dash long options like find's -name, -type + elif [[ "$word" =~ ^-[a-zA-Z]{2,}$ ]]; then + local chars="${word:1}" + # Pattern 1: individual chars (e.g., -r or -f from -rf) + # Pattern 2: the full word as-is (e.g., -name for find) + pattern="^[[:space:]]*-[${chars}][,/:[:space:]]|^[[:space:]]*-.*[,/][[:space:]]+-[${chars}][,/:[:space:]]|^[[:space:]]*${word}([,/:[:space:]]|$)|^[[:space:]]*-.*[,/][[:space:]]+${word}([,/:[:space:]]|$)" + # Single short option: -r -> match at start of option definition line + elif [[ "$word" =~ ^-[a-zA-Z]$ ]]; then + pattern="^[[:space:]]*${word}[,/:[:space:]]|^[[:space:]]*-.*[,/][[:space:]]+${word}([,/:[:space:]]|$)" + # Long option without value: --recursive + elif [[ "$word" =~ ^-- ]]; then + pattern="^[[:space:]]*${word}([,/=:[[:space:]]|$)|^[[:space:]]*-.*[,/][[:space:]]+${word}([,/=:[[:space:]]|$)" + fi + fi + + # Clear screen and open man page + zle -I + + if [[ -n "$pattern" ]]; then + man "$man_page" 2>/dev/null | ${ZVM_MAN_PAGER} -p "${pattern}" 2>/dev/null || \ + man "$man_page" 2>/dev/null | ${ZVM_MAN_PAGER} + else + man "$man_page" 2>/dev/null || zle -M "No manual entry for ${man_page}" + fi + + zle reset-prompt +} + +# Register the widget and bind the key +zle -N zvm-man + +function _zvm_man_bind_key() { + bindkey -M vicmd "${ZVM_MAN_KEY}" zvm-man +} + +# Support both immediate binding and lazy loading with zsh-vi-mode +if (( ${+functions[zvm_after_lazy_keybindings]} )); then + # zsh-vi-mode is loaded with lazy keybindings, hook into it + if [[ -z "${ZVM_LAZY_KEYBINDINGS}" ]] || [[ "${ZVM_LAZY_KEYBINDINGS}" == true ]]; then + zvm_after_lazy_keybindings_commands+=(_zvm_man_bind_key) + else + _zvm_man_bind_key + fi +elif (( ${+functions[zvm_after_init]} )); then + # zsh-vi-mode without lazy keybindings + zvm_after_init_commands+=(_zvm_man_bind_key) +else + # Standalone or other vi-mode setups + _zvm_man_bind_key +fi +