From c3b2b797f016c1f3f78a350144f3a5d9de7105ca Mon Sep 17 00:00:00 2001 From: XS Date: Thu, 2 Apr 2026 12:48:41 +0800 Subject: [PATCH 1/4] feat(keyman): add SSH and GPG key management plugin with commands and configuration --- plugins/keyman/README.sh | 97 +++++ plugins/keyman/keyman.plugin.zsh | 600 +++++++++++++++++++++++++++++++ 2 files changed, 697 insertions(+) create mode 100644 plugins/keyman/README.sh create mode 100644 plugins/keyman/keyman.plugin.zsh diff --git a/plugins/keyman/README.sh b/plugins/keyman/README.sh new file mode 100644 index 000000000..32b55aab9 --- /dev/null +++ b/plugins/keyman/README.sh @@ -0,0 +1,97 @@ +# keyman plugin + +Provides convenient commands for managing SSH and GPG keys from the terminal. +Works on macOS, Linux (X11/Wayland), and WSL. + +To enable it, add `keyman` to your plugins: + +```zsh +plugins=(... keyman) +``` + +Then type `keyman` to see all available commands. + +## Commands + +### SSH + +| Command | Description | +|---|---| +| `km-ssh-new [comment] [file] [type]` | Create a new SSH key (default: ed25519) | +| `km-ssh-ls` | List all SSH public keys in `~/.ssh` | +| `km-ssh-copy [pubkey_file]` | Copy a public key to clipboard | +| `km-ssh-rm ` | Delete an SSH key pair | + +### GPG + +| Command | Description | +|---|---| +| `km-gpg-new` | Create a GPG key (interactive, via `gpg --full-generate-key`) | +| `km-gpg-quick-new "Name" "Email" [expiry]` | Create a GPG key non-interactively (ed25519, default 2y expiry) | +| `km-gpg-ls [-s\|--secret]` | List public keys, or secret keys with `-s` | +| `km-gpg-pub ` | Export a GPG public key (armored) | +| `km-gpg-priv ` | Export a GPG secret key (armored, with confirmation) | +| `km-gpg-copy ` | Copy a GPG public key to clipboard | +| `km-gpg-fp ` | Show a GPG key fingerprint | +| `km-gpg-rm ` | Delete a GPG key (secret + public) | + +## Settings + +**IMPORTANT: put these settings _before_ the line that sources oh-my-zsh.** + +### `lang` + +Set the UI language. Supported values: `en` (default), `zh`. + +```zsh +zstyle :omz:plugins:keyman lang zh +``` + +### `debug` + +Show a status message when the plugin loads: + +```zsh +zstyle :omz:plugins:keyman debug true +``` + +### `default-ssh-type` + +Set the default SSH key type for `km-ssh-new`. Supported values: +`ed25519` (default), `rsa`, `ecdsa`. + +```zsh +zstyle :omz:plugins:keyman default-ssh-type rsa +``` + +## Examples + +```zsh +# Create a default ed25519 key +km-ssh-new + +# Create an RSA key with a custom comment and path +km-ssh-new "me@work" ~/.ssh/work_key rsa + +# List all SSH keys +km-ssh-ls + +# Copy the default public key to clipboard +km-ssh-copy + +# Create a GPG key quickly +km-gpg-quick-new "John Doe" "john@example.com" 1y + +# Export and copy a GPG public key +km-gpg-copy john@example.com +``` + +## Requirements + +At least one of the following must be available: + +- `ssh-keygen` -- for SSH key commands +- `gpg` -- for GPG key commands + +For clipboard support, one of: `pbcopy` (macOS), `xclip` (Linux X11), +`wl-copy` (Linux Wayland), or `clip.exe` (WSL). diff --git a/plugins/keyman/keyman.plugin.zsh b/plugins/keyman/keyman.plugin.zsh new file mode 100644 index 000000000..d628588c0 --- /dev/null +++ b/plugins/keyman/keyman.plugin.zsh @@ -0,0 +1,600 @@ +#!/usr/bin/env zsh +# keyman.plugin.zsh -- SSH & GPG key management plugin for oh-my-zsh +# +# Usage: add 'keyman' to plugins in ~/.zshrc +# +# Configuration (in .zshrc, before plugins=(...)): +# zstyle ':omz:plugins:keyman' lang en # en (default) | zh +# zstyle ':omz:plugins:keyman' debug true # show load message (default: false) +# zstyle ':omz:plugins:keyman' default-ssh-type ed25519 # ed25519 | rsa | ecdsa + +# Require at least one of ssh-keygen or gpg +(( $+commands[ssh-keygen] + $+commands[gpg] )) || return + +# ===================================================== +# Persistent globals +# ===================================================== +typeset -gA _km_msg + +typeset -g _km_red=$'\033[0;31m' +typeset -g _km_green=$'\033[0;32m' +typeset -g _km_yellow=$'\033[0;33m' +typeset -g _km_blue=$'\033[0;34m' +typeset -g _km_cyan=$'\033[0;36m' +typeset -g _km_reset=$'\033[0m' + +_km_info() { print -r -- "${_km_blue}[keyman]${_km_reset} $*" } +_km_ok() { print -r -- "${_km_green}[keyman] ✅${_km_reset} $*" } +_km_warn() { print -r -- "${_km_yellow}[keyman] ⚠️${_km_reset} $*" } +_km_err() { print -r -- "${_km_red}[keyman] ❌${_km_reset} $*" } + +# Cross-platform clipboard +_km_copy_to_clipboard() { + local content="$1" + if command -v pbcopy &>/dev/null; then + printf '%s' "$content" | pbcopy + elif command -v xclip &>/dev/null; then + printf '%s' "$content" | xclip -selection clipboard + elif command -v wl-copy &>/dev/null; then + printf '%s' "$content" | wl-copy + elif command -v clip.exe &>/dev/null; then + printf '%s' "$content" | clip.exe + else + _km_warn "${_km_msg[clipboard_not_found]}" + return 1 + fi +} + +# ===================================================== +# Initialization (scoped via anonymous function) +# ===================================================== +function { + local lang + zstyle -s ':omz:plugins:keyman' lang lang || lang=en + + case "$lang" in + zh) + _km_msg=( + # -- clipboard -- + clipboard_not_found "未找到剪贴板工具 (pbcopy/xclip/wl-copy/clip.exe)" + # -- general -- + cancelled "已取消" + deleted "已删除" + confirm_delete "确认删除?(y/N) " + about_to_delete "即将删除:" + label_private_key "私钥:" + label_public_key "公钥:" + label_pubkey_content "公钥内容:" + label_file "文件:" + label_type "类型:" + label_fingerprint "指纹:" + label_comment "注释:" + # -- km-ssh-new -- + ssh_dir_created "已创建 ~/.ssh 目录" + file_exists "文件已存在" + confirm_overwrite "是否覆盖?(y/N) " + creating_key "正在创建 %s 密钥..." + unsupported_key_type "不支持的密钥类型: %s (可选: ed25519, rsa, ecdsa)" + key_created "密钥已创建" + added_to_agent "已添加到 ssh-agent" + key_creation_failed "密钥创建失败" + # -- km-ssh-ls -- + ssh_dir_not_found "~/.ssh 目录不存在" + no_ssh_keys_found "未找到任何 SSH 公钥" + # -- km-ssh-copy -- + pubkey_not_found "公钥文件不存在" + available_pubkeys "可用的公钥:" + none "(无)" + pubkey_copied "公钥已复制到剪贴板" + # -- km-ssh-rm -- + usage_ssh_rm "用法: km-ssh-rm " + key_not_found "密钥不存在" + # -- km-gpg-quick-new -- + usage_gpg_quick_new "用法: km-gpg-quick-new \"姓名\" \"邮箱\" [过期时间]" + email_has_gpg_key "该邮箱已有 GPG 密钥:" + confirm_create_new "继续创建新密钥?(y/N) " + creating_gpg_key "正在创建 GPG 密钥..." + label_name " 姓名:" + label_email " 邮箱:" + label_expiry " 过期:" + gpg_key_created "GPG 密钥已创建" + gpg_key_creation_failed "GPG 密钥创建失败" + # -- km-gpg-ls -- + gpg_secret_key_list "GPG 私钥列表:" + gpg_public_key_list "GPG 公钥列表:" + # -- km-gpg-pub -- + usage_gpg_pub "用法: km-gpg-pub <邮箱或KeyID>" + gpg_key_not_found "未找到密钥" + gpg_public_key "GPG 公钥" + # -- km-gpg-priv -- + usage_gpg_priv "用法: km-gpg-priv <邮箱或KeyID>" + gpg_secret_not_found "未找到私钥" + warn_export_secret "即将导出私钥!请确保在安全环境下操作" + confirm_export "确认导出?(y/N) " + # -- km-gpg-copy -- + usage_gpg_copy "用法: km-gpg-copy <邮箱或KeyID>" + gpg_pubkey_copied "GPG 公钥已复制到剪贴板" + # -- km-gpg-fp -- + usage_gpg_fp "用法: km-gpg-fp <邮箱或KeyID>" + # -- km-gpg-rm -- + usage_gpg_rm "用法: km-gpg-rm <邮箱或KeyID>" + about_to_delete_gpg "即将删除 GPG 密钥" + key_info "密钥信息:" + # -- debug -- + loaded "已加载,输入 keyman 查看帮助" + # -- help -- + help_text \ +"╔═══════════════════════════════════════════════════════════════╗ +║ keyman 密钥管理 ║ +╠═══════════════════════════════════════════════════════════════╣ +║ ║ +║ SSH 命令: ║ +║ ────────────────────────────────────────────────────── ║ +║ km-ssh-new [comment] [file] [type] 创建 SSH 密钥 ║ +║ km-ssh-ls 列出所有 SSH 公钥 ║ +║ km-ssh-copy [pubkey_file] 复制公钥到剪贴板 ║ +║ km-ssh-rm 删除 SSH 密钥对 ║ +║ ║ +║ GPG 命令: ║ +║ ────────────────────────────────────────────────────── ║ +║ km-gpg-new 创建密钥(交互式) ║ +║ km-gpg-quick-new \"姓名\" \"邮箱\" [期限] 创建密钥(快速) ║ +║ km-gpg-ls [-s|--secret] 列出密钥 ║ +║ km-gpg-pub 导出公钥 ║ +║ km-gpg-priv 导出私钥 ║ +║ km-gpg-copy 复制公钥到剪贴板 ║ +║ km-gpg-fp 查看指纹 ║ +║ km-gpg-rm 删除密钥 ║ +║ ║ +╚═══════════════════════════════════════════════════════════════╝" + ) + ;; + *) + _km_msg=( + # -- clipboard -- + clipboard_not_found "Clipboard tool not found (pbcopy/xclip/wl-copy/clip.exe)" + # -- general -- + cancelled "Cancelled" + deleted "Deleted" + confirm_delete "Confirm deletion? (y/N) " + about_to_delete "About to delete:" + label_private_key "Private key:" + label_public_key "Public key:" + label_pubkey_content "Public key content:" + label_file "File:" + label_type "Type:" + label_fingerprint "Fingerprint:" + label_comment "Comment:" + # -- km-ssh-new -- + ssh_dir_created "Created ~/.ssh directory" + file_exists "File already exists" + confirm_overwrite "Overwrite? (y/N) " + creating_key "Creating %s key..." + unsupported_key_type "Unsupported key type: %s (supported: ed25519, rsa, ecdsa)" + key_created "Key created" + added_to_agent "Added to ssh-agent" + key_creation_failed "Key creation failed" + # -- km-ssh-ls -- + ssh_dir_not_found "~/.ssh directory does not exist" + no_ssh_keys_found "No SSH public keys found" + # -- km-ssh-copy -- + pubkey_not_found "Public key file not found" + available_pubkeys "Available public keys:" + none "(none)" + pubkey_copied "Public key copied to clipboard" + # -- km-ssh-rm -- + usage_ssh_rm "Usage: km-ssh-rm " + key_not_found "Key not found" + # -- km-gpg-quick-new -- + usage_gpg_quick_new "Usage: km-gpg-quick-new \"Name\" \"Email\" [expiry]" + email_has_gpg_key "This email already has a GPG key:" + confirm_create_new "Continue creating new key? (y/N) " + creating_gpg_key "Creating GPG key..." + label_name " Name:" + label_email " Email:" + label_expiry " Expiry:" + gpg_key_created "GPG key created" + gpg_key_creation_failed "GPG key creation failed" + # -- km-gpg-ls -- + gpg_secret_key_list "GPG secret key list:" + gpg_public_key_list "GPG public key list:" + # -- km-gpg-pub -- + usage_gpg_pub "Usage: km-gpg-pub " + gpg_key_not_found "Key not found" + gpg_public_key "GPG public key" + # -- km-gpg-priv -- + usage_gpg_priv "Usage: km-gpg-priv " + gpg_secret_not_found "Secret key not found" + warn_export_secret "About to export secret key! Make sure you are in a secure environment" + confirm_export "Confirm export? (y/N) " + # -- km-gpg-copy -- + usage_gpg_copy "Usage: km-gpg-copy " + gpg_pubkey_copied "GPG public key copied to clipboard" + # -- km-gpg-fp -- + usage_gpg_fp "Usage: km-gpg-fp " + # -- km-gpg-rm -- + usage_gpg_rm "Usage: km-gpg-rm " + about_to_delete_gpg "About to delete GPG key" + key_info "Key info:" + # -- debug -- + loaded "Loaded. Type 'keyman' for help" + # -- help -- + help_text \ +"╔═══════════════════════════════════════════════════════════════╗ +║ keyman - Key Manager ║ +╠═══════════════════════════════════════════════════════════════╣ +║ ║ +║ SSH Commands: ║ +║ ────────────────────────────────────────────────────── ║ +║ km-ssh-new [comment] [file] [type] Create SSH key ║ +║ km-ssh-ls List SSH public keys ║ +║ km-ssh-copy [pubkey_file] Copy pubkey to clipboard║ +║ km-ssh-rm Delete SSH key pair ║ +║ ║ +║ GPG Commands: ║ +║ ────────────────────────────────────────────────────── ║ +║ km-gpg-new Create key (interactive)║ +║ km-gpg-quick-new \"Name\" \"Email\" [exp] Create key (quick) ║ +║ km-gpg-ls [-s|--secret] List keys ║ +║ km-gpg-pub Export public key ║ +║ km-gpg-priv Export secret key ║ +║ km-gpg-copy Copy pubkey to clipboard║ +║ km-gpg-fp Show fingerprint ║ +║ km-gpg-rm Delete key ║ +║ ║ +╚═══════════════════════════════════════════════════════════════╝" + ) + ;; + esac + + # Debug output + zstyle -t ':omz:plugins:keyman' debug && _km_info "${_km_msg[loaded]}" +} + +# ===================================================== +# SSH Commands +# ===================================================== + +# Create SSH key +# Usage: km-ssh-new [comment] [keyfile] [type] +km-ssh-new() { + local comment="${1:-${USER:-$(whoami)}@${HOST:-$(hostname)}}" + local keyfile="${2:-}" + local keytype="${3:-}" + + if [[ -z "$keytype" ]]; then + zstyle -s ':omz:plugins:keyman' default-ssh-type keytype || keytype=ed25519 + fi + + # Set default path by type + if [[ -z "$keyfile" ]]; then + case "$keytype" in + rsa) keyfile="$HOME/.ssh/id_rsa" ;; + ecdsa) keyfile="$HOME/.ssh/id_ecdsa" ;; + ed25519) keyfile="$HOME/.ssh/id_ed25519" ;; + *) keyfile="$HOME/.ssh/id_${keytype}" ;; + esac + fi + + # Ensure .ssh directory exists + if [[ ! -d "$HOME/.ssh" ]]; then + mkdir -p "$HOME/.ssh" + chmod 700 "$HOME/.ssh" + _km_info "${_km_msg[ssh_dir_created]}" + fi + + # Prevent overwriting existing key + if [[ -f "$keyfile" ]]; then + _km_warn "${_km_msg[file_exists]}: $keyfile" + read -q "REPLY?${_km_msg[confirm_overwrite]}" + echo + if [[ "$REPLY" != "y" ]]; then + _km_info "${_km_msg[cancelled]}" + return 1 + fi + fi + + _km_info "$(printf "${_km_msg[creating_key]}" "$keytype")" + + local _km_rc=0 + case "$keytype" in + rsa) + ssh-keygen -t rsa -b 4096 -C "$comment" -f "$keyfile" || _km_rc=$? + ;; + ecdsa) + ssh-keygen -t ecdsa -b 521 -C "$comment" -f "$keyfile" || _km_rc=$? + ;; + ed25519) + ssh-keygen -t ed25519 -C "$comment" -f "$keyfile" || _km_rc=$? + ;; + *) + _km_err "$(printf "${_km_msg[unsupported_key_type]}" "$keytype")" + return 1 + ;; + esac + + if [[ $_km_rc -eq 0 && -f "${keyfile}.pub" ]]; then + chmod 600 "$keyfile" + chmod 644 "${keyfile}.pub" + + _km_ok "${_km_msg[key_created]}" + print "" + print -r -- "${_km_cyan}${_km_msg[label_private_key]}${_km_reset} $keyfile" + print -r -- "${_km_cyan}${_km_msg[label_public_key]}${_km_reset} ${keyfile}.pub" + print "" + print -r -- "${_km_cyan}${_km_msg[label_pubkey_content]}${_km_reset}" + cat "${keyfile}.pub" + + # Add to ssh-agent (start agent if needed) + if [[ -z "$SSH_AUTH_SOCK" ]]; then + eval "$(ssh-agent -s)" >/dev/null 2>&1 + fi + if ssh-add "$keyfile" 2>/dev/null; then + _km_ok "${_km_msg[added_to_agent]}" + fi + else + _km_err "${_km_msg[key_creation_failed]}" + return 1 + fi +} + +# List all SSH public keys +km-ssh-ls() { + local found=0 + + if [[ ! -d "$HOME/.ssh" ]]; then + _km_warn "${_km_msg[ssh_dir_not_found]}" + return 1 + fi + + for pubkey in "$HOME"/.ssh/*.pub(N); do + found=1 + local info + info=$(ssh-keygen -l -f "$pubkey" 2>/dev/null) || continue + local bits=${info%% *} + local rest=${info#* } + local fingerprint=${rest%% *} + rest=${rest#* } + local keytype=${rest##* } + local comment=${rest% *} + + print -r -- "${_km_cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${_km_reset}" + print -r -- "${_km_green}${_km_msg[label_file]}${_km_reset} $pubkey" + print -r -- "${_km_green}${_km_msg[label_type]}${_km_reset} ${keytype} (${bits} bits)" + print -r -- "${_km_green}${_km_msg[label_fingerprint]}${_km_reset} $fingerprint" + print -r -- "${_km_green}${_km_msg[label_comment]}${_km_reset} $comment" + print -r -- "${_km_green}${_km_msg[label_pubkey_content]}${_km_reset}" + cat "$pubkey" + echo "" + done + + if [[ $found -eq 0 ]]; then + _km_warn "${_km_msg[no_ssh_keys_found]}" + fi +} + +# Copy SSH public key to clipboard +# Usage: km-ssh-copy [keyfile] +km-ssh-copy() { + local pubkey="${1:-$HOME/.ssh/id_ed25519.pub}" + + [[ "$pubkey" != *.pub ]] && pubkey="${pubkey}.pub" + + if [[ ! -f "$pubkey" ]]; then + _km_err "${_km_msg[pubkey_not_found]}: $pubkey" + _km_info "${_km_msg[available_pubkeys]}" + ls "$HOME"/.ssh/*.pub 2>/dev/null || echo " ${_km_msg[none]}" + return 1 + fi + + local content=$(cat "$pubkey") + if _km_copy_to_clipboard "$content"; then + _km_ok "${_km_msg[pubkey_copied]}: $pubkey" + fi +} + +# Delete SSH key pair +# Usage: km-ssh-rm +km-ssh-rm() { + if [[ -z "${1:-}" ]]; then + _km_err "${_km_msg[usage_ssh_rm]}" + return 1 + fi + local keyfile="$1" + + keyfile="${keyfile%.pub}" + + if [[ ! -f "$keyfile" && ! -f "${keyfile}.pub" ]]; then + _km_err "${_km_msg[key_not_found]}: $keyfile" + return 1 + fi + + _km_warn "${_km_msg[about_to_delete]}" + [[ -f "$keyfile" ]] && echo " ${_km_msg[label_private_key]} $keyfile" + [[ -f "${keyfile}.pub" ]] && echo " ${_km_msg[label_public_key]} ${keyfile}.pub" + + read -q "REPLY?${_km_msg[confirm_delete]}" + echo + if [[ "$REPLY" == "y" ]]; then + ssh-add -d "$keyfile" 2>/dev/null + [[ -f "$keyfile" ]] && rm -f "$keyfile" + [[ -f "${keyfile}.pub" ]] && rm -f "${keyfile}.pub" + _km_ok "${_km_msg[deleted]}" + else + _km_info "${_km_msg[cancelled]}" + fi +} + +# ===================================================== +# GPG Commands +# ===================================================== + +# Create GPG key (interactive) +alias km-gpg-new='gpg --full-generate-key' + +# Create GPG key (non-interactive) +# Usage: km-gpg-quick-new "Name" "Email" [expiry] +km-gpg-quick-new() { + if [[ -z "${1:-}" || -z "${2:-}" ]]; then + _km_err "${_km_msg[usage_gpg_quick_new]}" + return 1 + fi + local name="$1" + local email="$2" + local expire="${3:-2y}" + + if gpg --list-keys "$email" &>/dev/null; then + _km_warn "${_km_msg[email_has_gpg_key]}" + gpg --list-keys "$email" + read -q "REPLY?${_km_msg[confirm_create_new]}" + echo + [[ "$REPLY" != "y" ]] && return 1 + fi + + _km_info "${_km_msg[creating_gpg_key]}" + _km_info "${_km_msg[label_name]} $name" + _km_info "${_km_msg[label_email]} $email" + _km_info "${_km_msg[label_expiry]} $expire" + + local _km_rc=0 + gpg --batch --gen-key < +km-gpg-pub() { + if [[ -z "${1:-}" ]]; then + _km_err "${_km_msg[usage_gpg_pub]}" + return 1 + fi + local key_id="$1" + + if ! gpg --list-keys "$key_id" &>/dev/null; then + _km_err "${_km_msg[gpg_key_not_found]}: $key_id" + return 1 + fi + + _km_info "${_km_msg[gpg_public_key]} ($key_id):" + echo "" + gpg --armor --export "$key_id" +} + +# Export GPG secret key +# Usage: km-gpg-priv +km-gpg-priv() { + if [[ -z "${1:-}" ]]; then + _km_err "${_km_msg[usage_gpg_priv]}" + return 1 + fi + local key_id="$1" + + if ! gpg --list-secret-keys "$key_id" &>/dev/null; then + _km_err "${_km_msg[gpg_secret_not_found]}: $key_id" + return 1 + fi + + _km_warn "${_km_msg[warn_export_secret]}" + read -q "REPLY?${_km_msg[confirm_export]}" + echo + + if [[ "$REPLY" == "y" ]]; then + gpg --armor --export-secret-keys "$key_id" + else + _km_info "${_km_msg[cancelled]}" + fi +} + +# Copy GPG public key to clipboard +# Usage: km-gpg-copy +km-gpg-copy() { + if [[ -z "${1:-}" ]]; then + _km_err "${_km_msg[usage_gpg_copy]}" + return 1 + fi + local key_id="$1" + + if ! gpg --list-keys "$key_id" &>/dev/null; then + _km_err "${_km_msg[gpg_key_not_found]}: $key_id" + return 1 + fi + + local content=$(gpg --armor --export "$key_id") + if _km_copy_to_clipboard "$content"; then + _km_ok "${_km_msg[gpg_pubkey_copied]} ($key_id)" + fi +} + +# Show GPG key fingerprint +# Usage: km-gpg-fp +km-gpg-fp() { + if [[ -z "${1:-}" ]]; then + _km_err "${_km_msg[usage_gpg_fp]}" + return 1 + fi + gpg --fingerprint "$1" +} + +# Delete GPG key +# Usage: km-gpg-rm +km-gpg-rm() { + if [[ -z "${1:-}" ]]; then + _km_err "${_km_msg[usage_gpg_rm]}" + return 1 + fi + local key_id="$1" + + _km_warn "${_km_msg[about_to_delete_gpg]}: $key_id" + _km_info "${_km_msg[key_info]}" + gpg --list-keys "$key_id" 2>/dev/null + echo "" + + read -q "REPLY?${_km_msg[confirm_delete]}" + echo + + if [[ "$REPLY" == "y" ]]; then + gpg --delete-secret-and-public-key "$key_id" + _km_ok "${_km_msg[deleted]}" + else + _km_info "${_km_msg[cancelled]}" + fi +} + +# ===================================================== +# Help +# ===================================================== +keyman() { + print -r -- "${_km_msg[help_text]}" +} From 4ae29045f2960871cca5a97d93df5de10dc40094 Mon Sep 17 00:00:00 2001 From: XS Date: Thu, 2 Apr 2026 12:51:29 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(keyman):=20=E6=B7=BB=E5=8A=A0=20SSH=20?= =?UTF-8?q?=E5=92=8C=20GPG=20=E5=AF=86=E9=92=A5=E7=AE=A1=E7=90=86=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E7=9A=84=20README=20=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/keyman/{README.sh => README.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plugins/keyman/{README.sh => README.md} (100%) diff --git a/plugins/keyman/README.sh b/plugins/keyman/README.md similarity index 100% rename from plugins/keyman/README.sh rename to plugins/keyman/README.md From 7cc680d82c5908b9c90a618901882064c3e8a0cf Mon Sep 17 00:00:00 2001 From: XS Date: Thu, 2 Apr 2026 13:10:05 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(keyman):=20=E6=9B=B4=E6=96=B0=20README?= =?UTF-8?q?=20=E5=92=8C=E6=8F=92=E4=BB=B6=E6=96=87=E6=A1=A3=EF=BC=8C?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=91=BD=E4=BB=A4=E6=A0=BC=E5=BC=8F=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=B8=AE=E5=8A=A9=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/keyman/README.md | 122 +++++++--- plugins/keyman/keyman.plugin.zsh | 394 +++++++++++++++++++++---------- 2 files changed, 359 insertions(+), 157 deletions(-) diff --git a/plugins/keyman/README.md b/plugins/keyman/README.md index 32b55aab9..704fc3d77 100644 --- a/plugins/keyman/README.md +++ b/plugins/keyman/README.md @@ -1,39 +1,89 @@ # keyman plugin -Provides convenient commands for managing SSH and GPG keys from the terminal. +Provides a unified `keyman` command for managing SSH and GPG keys from the terminal. Works on macOS, Linux (X11/Wayland), and WSL. +> **Relationship to other plugins:** The [`ssh-agent`](../ssh-agent) and +> [`gpg-agent`](../gpg-agent) plugins manage agent *daemons* (auto-starting, +> identity loading, forwarding). `keyman` focuses on key *lifecycle* — creating, +> listing, copying, and deleting keys. They are complementary and can be used +> together. + To enable it, add `keyman` to your plugins: ```zsh plugins=(... keyman) ``` -Then type `keyman` to see all available commands. +Then type `keyman help` to see all available commands. + +## Requirements + +At least one of the following must be available: + +- `ssh-keygen` — for SSH key commands +- `gpg` — for GPG key commands + +### Clipboard support + +For clipboard commands (`keyman ssh copy`, `keyman gpg copy`), one of the tools +below must be installed: + +| Platform | Tool | Notes | +| --------------- | ---------- | -------------------------- | +| macOS | `pbcopy` | Built-in | +| Linux (X11) | `xclip` | `apt install xclip` | +| Linux (Wayland) | `wl-copy` | `apt install wl-clipboard` | +| WSL | `clip.exe` | Built-in (Windows side) | ## Commands ### SSH -| Command | Description | -|---|---| -| `km-ssh-new [comment] [file] [type]` | Create a new SSH key (default: ed25519) | -| `km-ssh-ls` | List all SSH public keys in `~/.ssh` | -| `km-ssh-copy [pubkey_file]` | Copy a public key to clipboard | -| `km-ssh-rm ` | Delete an SSH key pair | +| Command | Description | +| --------------------------------------------- | --------------------------------------- | +| `keyman ssh new [comment] [file] [type]` | Create a new SSH key (default: ed25519) | +| `keyman ssh ls` | List all SSH public keys in `~/.ssh` | +| `keyman ssh copy [pubkey_file]` | Copy a public key to clipboard | +| `keyman ssh rm ` | Delete an SSH key pair | ### GPG -| Command | Description | -|---|---| -| `km-gpg-new` | Create a GPG key (interactive, via `gpg --full-generate-key`) | -| `km-gpg-quick-new "Name" "Email" [expiry]` | Create a GPG key non-interactively (ed25519, default 2y expiry) | -| `km-gpg-ls [-s\|--secret]` | List public keys, or secret keys with `-s` | -| `km-gpg-pub ` | Export a GPG public key (armored) | -| `km-gpg-priv ` | Export a GPG secret key (armored, with confirmation) | -| `km-gpg-copy ` | Copy a GPG public key to clipboard | -| `km-gpg-fp ` | Show a GPG key fingerprint | -| `km-gpg-rm ` | Delete a GPG key (secret + public) | +| Command | Description | +| -------------------------------------------------- | --------------------------------------------------------------- | +| `keyman gpg new` | Create a GPG key (interactive, via `gpg --full-generate-key`) | +| `keyman gpg quick-new "Name" "Email" [expiry]` | Create a GPG key non-interactively (ed25519, default 2y expiry) | +| `keyman gpg ls [-s\|--secret]` | List public keys, or secret keys with `-s` | +| `keyman gpg pub ` | Export a GPG public key (armored) | +| `keyman gpg priv ` | Export a GPG secret key (armored, with confirmation) | +| `keyman gpg copy ` | Copy a GPG public key to clipboard | +| `keyman gpg fp ` | Show a GPG key fingerprint | +| `keyman gpg rm ` | Delete a GPG key (secret + public) | + +### General + +| Command | Description | +| -------------- | ----------------- | +| `keyman help` | Show help message | + +## Tab Completion + +All commands support multi-level Zsh tab completion: + +``` +keyman → ssh | gpg | help +keyman ssh → new | ls | copy | rm +keyman gpg → new | quick-new | ls | pub | priv | copy | fp | rm +``` + +At the argument level: + +- **`keyman ssh new`** — completes key types (`ed25519`, `rsa`, `ecdsa`) and file paths +- **`keyman ssh copy`** — completes `~/.ssh/*.pub` files +- **`keyman ssh rm`** — completes private key files in `~/.ssh` +- **`keyman gpg ls`** — completes `--secret` / `-s` options +- **`keyman gpg quick-new`** — completes common expiry values (`1y`, `2y`, `3y`, `5y`, `0`) +- **`keyman gpg pub`**, **`priv`**, **`copy`**, **`fp`**, **`rm`** — complete GPG key IDs and emails from your keyring ## Settings @@ -57,7 +107,7 @@ zstyle :omz:plugins:keyman debug true ### `default-ssh-type` -Set the default SSH key type for `km-ssh-new`. Supported values: +Set the default SSH key type for `keyman ssh new`. Supported values: `ed25519` (default), `rsa`, `ecdsa`. ```zsh @@ -68,30 +118,32 @@ zstyle :omz:plugins:keyman default-ssh-type rsa ```zsh # Create a default ed25519 key -km-ssh-new +keyman ssh new # Create an RSA key with a custom comment and path -km-ssh-new "me@work" ~/.ssh/work_key rsa +keyman ssh new "me@work" ~/.ssh/work_key rsa # List all SSH keys -km-ssh-ls +keyman ssh ls # Copy the default public key to clipboard -km-ssh-copy +keyman ssh copy + +# Delete an SSH key +keyman ssh rm ~/.ssh/id_ed25519 + +# Create a GPG key interactively +keyman gpg new # Create a GPG key quickly -km-gpg-quick-new "John Doe" "john@example.com" 1y +keyman gpg quick-new "John Doe" "john@example.com" 1y + +# List GPG secret keys +keyman gpg ls --secret # Export and copy a GPG public key -km-gpg-copy john@example.com +keyman gpg copy john@example.com + +# Show GPG key fingerprint +keyman gpg fp john@example.com ``` - -## Requirements - -At least one of the following must be available: - -- `ssh-keygen` -- for SSH key commands -- `gpg` -- for GPG key commands - -For clipboard support, one of: `pbcopy` (macOS), `xclip` (Linux X11), -`wl-copy` (Linux Wayland), or `clip.exe` (WSL). diff --git a/plugins/keyman/keyman.plugin.zsh b/plugins/keyman/keyman.plugin.zsh index d628588c0..2fbd448c3 100644 --- a/plugins/keyman/keyman.plugin.zsh +++ b/plugins/keyman/keyman.plugin.zsh @@ -1,7 +1,14 @@ #!/usr/bin/env zsh # keyman.plugin.zsh -- SSH & GPG key management plugin for oh-my-zsh # +# Author: keyman contributors +# Version: 0.2.0 +# License: MIT (same as oh-my-zsh) +# # Usage: add 'keyman' to plugins in ~/.zshrc +# keyman ssh [args...] +# keyman gpg [args...] +# keyman help # # Configuration (in .zshrc, before plugins=(...)): # zstyle ':omz:plugins:keyman' lang en # en (default) | zh @@ -21,6 +28,7 @@ typeset -g _km_green=$'\033[0;32m' typeset -g _km_yellow=$'\033[0;33m' typeset -g _km_blue=$'\033[0;34m' typeset -g _km_cyan=$'\033[0;36m' +typeset -g _km_bold=$'\033[1m' typeset -g _km_reset=$'\033[0m' _km_info() { print -r -- "${_km_blue}[keyman]${_km_reset} $*" } @@ -69,7 +77,7 @@ function { label_type "类型:" label_fingerprint "指纹:" label_comment "注释:" - # -- km-ssh-new -- + # -- ssh new -- ssh_dir_created "已创建 ~/.ssh 目录" file_exists "文件已存在" confirm_overwrite "是否覆盖?(y/N) " @@ -78,19 +86,19 @@ function { key_created "密钥已创建" added_to_agent "已添加到 ssh-agent" key_creation_failed "密钥创建失败" - # -- km-ssh-ls -- + # -- ssh ls -- ssh_dir_not_found "~/.ssh 目录不存在" no_ssh_keys_found "未找到任何 SSH 公钥" - # -- km-ssh-copy -- + # -- ssh copy -- pubkey_not_found "公钥文件不存在" available_pubkeys "可用的公钥:" none "(无)" pubkey_copied "公钥已复制到剪贴板" - # -- km-ssh-rm -- - usage_ssh_rm "用法: km-ssh-rm " + # -- ssh rm -- + usage_ssh_rm "用法: keyman ssh rm " key_not_found "密钥不存在" - # -- km-gpg-quick-new -- - usage_gpg_quick_new "用法: km-gpg-quick-new \"姓名\" \"邮箱\" [过期时间]" + # -- gpg quick-new -- + usage_gpg_quick_new "用法: keyman gpg quick-new \"姓名\" \"邮箱\" [过期时间]" email_has_gpg_key "该邮箱已有 GPG 密钥:" confirm_create_new "继续创建新密钥?(y/N) " creating_gpg_key "正在创建 GPG 密钥..." @@ -99,54 +107,56 @@ function { label_expiry " 过期:" gpg_key_created "GPG 密钥已创建" gpg_key_creation_failed "GPG 密钥创建失败" - # -- km-gpg-ls -- + # -- gpg ls -- gpg_secret_key_list "GPG 私钥列表:" gpg_public_key_list "GPG 公钥列表:" - # -- km-gpg-pub -- - usage_gpg_pub "用法: km-gpg-pub <邮箱或KeyID>" + # -- gpg pub -- + usage_gpg_pub "用法: keyman gpg pub <邮箱或KeyID>" gpg_key_not_found "未找到密钥" gpg_public_key "GPG 公钥" - # -- km-gpg-priv -- - usage_gpg_priv "用法: km-gpg-priv <邮箱或KeyID>" + # -- gpg priv -- + usage_gpg_priv "用法: keyman gpg priv <邮箱或KeyID>" gpg_secret_not_found "未找到私钥" warn_export_secret "即将导出私钥!请确保在安全环境下操作" confirm_export "确认导出?(y/N) " - # -- km-gpg-copy -- - usage_gpg_copy "用法: km-gpg-copy <邮箱或KeyID>" + # -- gpg copy -- + usage_gpg_copy "用法: keyman gpg copy <邮箱或KeyID>" gpg_pubkey_copied "GPG 公钥已复制到剪贴板" - # -- km-gpg-fp -- - usage_gpg_fp "用法: km-gpg-fp <邮箱或KeyID>" - # -- km-gpg-rm -- - usage_gpg_rm "用法: km-gpg-rm <邮箱或KeyID>" + # -- gpg fp -- + usage_gpg_fp "用法: keyman gpg fp <邮箱或KeyID>" + # -- gpg rm -- + usage_gpg_rm "用法: keyman gpg rm <邮箱或KeyID>" about_to_delete_gpg "即将删除 GPG 密钥" key_info "密钥信息:" + # -- errors -- + unknown_group "未知命令组: %s (可用: ssh, gpg)" + unknown_ssh_action "未知 SSH 操作: %s (可用: new, ls, copy, rm)" + unknown_gpg_action "未知 GPG 操作: %s (可用: new, quick-new, ls, pub, priv, copy, fp, rm)" # -- debug -- - loaded "已加载,输入 keyman 查看帮助" + loaded "已加载,输入 keyman help 查看帮助" # -- help -- help_text \ -"╔═══════════════════════════════════════════════════════════════╗ -║ keyman 密钥管理 ║ -╠═══════════════════════════════════════════════════════════════╣ -║ ║ -║ SSH 命令: ║ -║ ────────────────────────────────────────────────────── ║ -║ km-ssh-new [comment] [file] [type] 创建 SSH 密钥 ║ -║ km-ssh-ls 列出所有 SSH 公钥 ║ -║ km-ssh-copy [pubkey_file] 复制公钥到剪贴板 ║ -║ km-ssh-rm 删除 SSH 密钥对 ║ -║ ║ -║ GPG 命令: ║ -║ ────────────────────────────────────────────────────── ║ -║ km-gpg-new 创建密钥(交互式) ║ -║ km-gpg-quick-new \"姓名\" \"邮箱\" [期限] 创建密钥(快速) ║ -║ km-gpg-ls [-s|--secret] 列出密钥 ║ -║ km-gpg-pub 导出公钥 ║ -║ km-gpg-priv 导出私钥 ║ -║ km-gpg-copy 复制公钥到剪贴板 ║ -║ km-gpg-fp 查看指纹 ║ -║ km-gpg-rm 删除密钥 ║ -║ ║ -╚═══════════════════════════════════════════════════════════════╝" +"${_km_bold}keyman${_km_reset} — SSH & GPG 密钥管理 + +${_km_cyan}用法:${_km_reset} + keyman <命令组> <操作> [参数...] + keyman help + +${_km_cyan}SSH 命令:${_km_reset} + keyman ssh new [comment] [file] [type] 创建 SSH 密钥 + keyman ssh ls 列出所有 SSH 公钥 + keyman ssh copy [pubkey_file] 复制公钥到剪贴板 + keyman ssh rm 删除 SSH 密钥对 + +${_km_cyan}GPG 命令:${_km_reset} + keyman gpg new 创建密钥(交互式) + keyman gpg quick-new \"姓名\" \"邮箱\" [期限] 创建密钥(快速) + keyman gpg ls [-s|--secret] 列出密钥 + keyman gpg pub 导出公钥 + keyman gpg priv 导出私钥 + keyman gpg copy 复制公钥到剪贴板 + keyman gpg fp 查看指纹 + keyman gpg rm 删除密钥" ) ;; *) @@ -165,7 +175,7 @@ function { label_type "Type:" label_fingerprint "Fingerprint:" label_comment "Comment:" - # -- km-ssh-new -- + # -- ssh new -- ssh_dir_created "Created ~/.ssh directory" file_exists "File already exists" confirm_overwrite "Overwrite? (y/N) " @@ -174,19 +184,19 @@ function { key_created "Key created" added_to_agent "Added to ssh-agent" key_creation_failed "Key creation failed" - # -- km-ssh-ls -- + # -- ssh ls -- ssh_dir_not_found "~/.ssh directory does not exist" no_ssh_keys_found "No SSH public keys found" - # -- km-ssh-copy -- + # -- ssh copy -- pubkey_not_found "Public key file not found" available_pubkeys "Available public keys:" none "(none)" pubkey_copied "Public key copied to clipboard" - # -- km-ssh-rm -- - usage_ssh_rm "Usage: km-ssh-rm " + # -- ssh rm -- + usage_ssh_rm "Usage: keyman ssh rm " key_not_found "Key not found" - # -- km-gpg-quick-new -- - usage_gpg_quick_new "Usage: km-gpg-quick-new \"Name\" \"Email\" [expiry]" + # -- gpg quick-new -- + usage_gpg_quick_new "Usage: keyman gpg quick-new \"Name\" \"Email\" [expiry]" email_has_gpg_key "This email already has a GPG key:" confirm_create_new "Continue creating new key? (y/N) " creating_gpg_key "Creating GPG key..." @@ -195,54 +205,56 @@ function { label_expiry " Expiry:" gpg_key_created "GPG key created" gpg_key_creation_failed "GPG key creation failed" - # -- km-gpg-ls -- + # -- gpg ls -- gpg_secret_key_list "GPG secret key list:" gpg_public_key_list "GPG public key list:" - # -- km-gpg-pub -- - usage_gpg_pub "Usage: km-gpg-pub " + # -- gpg pub -- + usage_gpg_pub "Usage: keyman gpg pub " gpg_key_not_found "Key not found" gpg_public_key "GPG public key" - # -- km-gpg-priv -- - usage_gpg_priv "Usage: km-gpg-priv " + # -- gpg priv -- + usage_gpg_priv "Usage: keyman gpg priv " gpg_secret_not_found "Secret key not found" warn_export_secret "About to export secret key! Make sure you are in a secure environment" confirm_export "Confirm export? (y/N) " - # -- km-gpg-copy -- - usage_gpg_copy "Usage: km-gpg-copy " + # -- gpg copy -- + usage_gpg_copy "Usage: keyman gpg copy " gpg_pubkey_copied "GPG public key copied to clipboard" - # -- km-gpg-fp -- - usage_gpg_fp "Usage: km-gpg-fp " - # -- km-gpg-rm -- - usage_gpg_rm "Usage: km-gpg-rm " + # -- gpg fp -- + usage_gpg_fp "Usage: keyman gpg fp " + # -- gpg rm -- + usage_gpg_rm "Usage: keyman gpg rm " about_to_delete_gpg "About to delete GPG key" key_info "Key info:" + # -- errors -- + unknown_group "Unknown command group: %s (available: ssh, gpg)" + unknown_ssh_action "Unknown SSH action: %s (available: new, ls, copy, rm)" + unknown_gpg_action "Unknown GPG action: %s (available: new, quick-new, ls, pub, priv, copy, fp, rm)" # -- debug -- - loaded "Loaded. Type 'keyman' for help" + loaded "Loaded. Type 'keyman help' for help" # -- help -- help_text \ -"╔═══════════════════════════════════════════════════════════════╗ -║ keyman - Key Manager ║ -╠═══════════════════════════════════════════════════════════════╣ -║ ║ -║ SSH Commands: ║ -║ ────────────────────────────────────────────────────── ║ -║ km-ssh-new [comment] [file] [type] Create SSH key ║ -║ km-ssh-ls List SSH public keys ║ -║ km-ssh-copy [pubkey_file] Copy pubkey to clipboard║ -║ km-ssh-rm Delete SSH key pair ║ -║ ║ -║ GPG Commands: ║ -║ ────────────────────────────────────────────────────── ║ -║ km-gpg-new Create key (interactive)║ -║ km-gpg-quick-new \"Name\" \"Email\" [exp] Create key (quick) ║ -║ km-gpg-ls [-s|--secret] List keys ║ -║ km-gpg-pub Export public key ║ -║ km-gpg-priv Export secret key ║ -║ km-gpg-copy Copy pubkey to clipboard║ -║ km-gpg-fp Show fingerprint ║ -║ km-gpg-rm Delete key ║ -║ ║ -╚═══════════════════════════════════════════════════════════════╝" +"${_km_bold}keyman${_km_reset} — SSH & GPG Key Manager + +${_km_cyan}Usage:${_km_reset} + keyman [args...] + keyman help + +${_km_cyan}SSH Commands:${_km_reset} + keyman ssh new [comment] [file] [type] Create SSH key + keyman ssh ls List SSH public keys + keyman ssh copy [pubkey_file] Copy pubkey to clipboard + keyman ssh rm Delete SSH key pair + +${_km_cyan}GPG Commands:${_km_reset} + keyman gpg new Create key (interactive) + keyman gpg quick-new \"Name\" \"Email\" [exp] Create key (quick) + keyman gpg ls [-s|--secret] List keys + keyman gpg pub Export public key + keyman gpg priv Export secret key + keyman gpg copy Copy pubkey to clipboard + keyman gpg fp Show fingerprint + keyman gpg rm Delete key" ) ;; esac @@ -252,12 +264,11 @@ function { } # ===================================================== -# SSH Commands +# SSH Actions # ===================================================== -# Create SSH key -# Usage: km-ssh-new [comment] [keyfile] [type] -km-ssh-new() { +# keyman ssh new [comment] [keyfile] [type] +_km_ssh_new() { local comment="${1:-${USER:-$(whoami)}@${HOST:-$(hostname)}}" local keyfile="${2:-}" local keytype="${3:-}" @@ -338,8 +349,8 @@ km-ssh-new() { fi } -# List all SSH public keys -km-ssh-ls() { +# keyman ssh ls +_km_ssh_ls() { local found=0 if [[ ! -d "$HOME/.ssh" ]]; then @@ -373,9 +384,8 @@ km-ssh-ls() { fi } -# Copy SSH public key to clipboard -# Usage: km-ssh-copy [keyfile] -km-ssh-copy() { +# keyman ssh copy [keyfile] +_km_ssh_copy() { local pubkey="${1:-$HOME/.ssh/id_ed25519.pub}" [[ "$pubkey" != *.pub ]] && pubkey="${pubkey}.pub" @@ -393,9 +403,8 @@ km-ssh-copy() { fi } -# Delete SSH key pair -# Usage: km-ssh-rm -km-ssh-rm() { +# keyman ssh rm +_km_ssh_rm() { if [[ -z "${1:-}" ]]; then _km_err "${_km_msg[usage_ssh_rm]}" return 1 @@ -426,15 +435,16 @@ km-ssh-rm() { } # ===================================================== -# GPG Commands +# GPG Actions # ===================================================== -# Create GPG key (interactive) -alias km-gpg-new='gpg --full-generate-key' +# keyman gpg new (interactive) +_km_gpg_new() { + gpg --full-generate-key +} -# Create GPG key (non-interactive) -# Usage: km-gpg-quick-new "Name" "Email" [expiry] -km-gpg-quick-new() { +# keyman gpg quick-new "Name" "Email" [expiry] +_km_gpg_quick_new() { if [[ -z "${1:-}" || -z "${2:-}" ]]; then _km_err "${_km_msg[usage_gpg_quick_new]}" return 1 @@ -479,9 +489,8 @@ EOF fi } -# List GPG keys -# Usage: km-gpg-ls [--secret|-s] -km-gpg-ls() { +# keyman gpg ls [--secret|-s] +_km_gpg_ls() { if [[ "$1" == "--secret" || "$1" == "-s" ]]; then _km_info "${_km_msg[gpg_secret_key_list]}" echo "" @@ -493,9 +502,8 @@ km-gpg-ls() { fi } -# Export GPG public key -# Usage: km-gpg-pub -km-gpg-pub() { +# keyman gpg pub +_km_gpg_pub() { if [[ -z "${1:-}" ]]; then _km_err "${_km_msg[usage_gpg_pub]}" return 1 @@ -512,9 +520,8 @@ km-gpg-pub() { gpg --armor --export "$key_id" } -# Export GPG secret key -# Usage: km-gpg-priv -km-gpg-priv() { +# keyman gpg priv +_km_gpg_priv() { if [[ -z "${1:-}" ]]; then _km_err "${_km_msg[usage_gpg_priv]}" return 1 @@ -537,9 +544,8 @@ km-gpg-priv() { fi } -# Copy GPG public key to clipboard -# Usage: km-gpg-copy -km-gpg-copy() { +# keyman gpg copy +_km_gpg_copy() { if [[ -z "${1:-}" ]]; then _km_err "${_km_msg[usage_gpg_copy]}" return 1 @@ -557,9 +563,8 @@ km-gpg-copy() { fi } -# Show GPG key fingerprint -# Usage: km-gpg-fp -km-gpg-fp() { +# keyman gpg fp +_km_gpg_fp() { if [[ -z "${1:-}" ]]; then _km_err "${_km_msg[usage_gpg_fp]}" return 1 @@ -567,9 +572,8 @@ km-gpg-fp() { gpg --fingerprint "$1" } -# Delete GPG key -# Usage: km-gpg-rm -km-gpg-rm() { +# keyman gpg rm +_km_gpg_rm() { if [[ -z "${1:-}" ]]; then _km_err "${_km_msg[usage_gpg_rm]}" return 1 @@ -593,8 +597,154 @@ km-gpg-rm() { } # ===================================================== -# Help +# Main dispatcher # ===================================================== keyman() { - print -r -- "${_km_msg[help_text]}" + local group="${1:-help}" + local action="${2:-}" + shift 2 2>/dev/null + + case "$group" in + help|-h|--help) + print -r -- "${_km_msg[help_text]}" + ;; + ssh) + case "$action" in + new) _km_ssh_new "$@" ;; + ls) _km_ssh_ls "$@" ;; + copy) _km_ssh_copy "$@" ;; + rm) _km_ssh_rm "$@" ;; + *) + _km_err "$(printf "${_km_msg[unknown_ssh_action]}" "$action")" + return 1 + ;; + esac + ;; + gpg) + case "$action" in + new) _km_gpg_new "$@" ;; + quick-new) _km_gpg_quick_new "$@" ;; + ls) _km_gpg_ls "$@" ;; + pub) _km_gpg_pub "$@" ;; + priv) _km_gpg_priv "$@" ;; + copy) _km_gpg_copy "$@" ;; + fp) _km_gpg_fp "$@" ;; + rm) _km_gpg_rm "$@" ;; + *) + _km_err "$(printf "${_km_msg[unknown_gpg_action]}" "$action")" + return 1 + ;; + esac + ;; + *) + _km_err "$(printf "${_km_msg[unknown_group]}" "$group")" + return 1 + ;; + esac } + +# ===================================================== +# Zsh Completions +# ===================================================== +_keyman() { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + '1:command group:->group' \ + '*::args:->args' + + case "$state" in + group) + local -a groups=( + 'ssh:Manage SSH keys' + 'gpg:Manage GPG keys' + 'help:Show help message' + ) + _describe 'command group' groups + ;; + args) + case "${line[1]}" in + ssh) + _arguments -C \ + '1:ssh action:->ssh_action' \ + '*::ssh_args:->ssh_args' + + case "$state" in + ssh_action) + local -a actions=( + 'new:Create SSH key' + 'ls:List SSH public keys' + 'copy:Copy public key to clipboard' + 'rm:Delete SSH key pair' + ) + _describe 'ssh action' actions + ;; + ssh_args) + case "${line[1]}" in + new) + _arguments \ + '1:comment:' \ + '2:keyfile:_files -W "$HOME/.ssh"' \ + '3:key type:(ed25519 rsa ecdsa)' + ;; + copy) + _arguments '1:public key file:_files -W "$HOME/.ssh" -g "*.pub"' + ;; + rm) + local -a keys + if [[ -d "$HOME/.ssh" ]]; then + keys=("$HOME"/.ssh/id_*(N:t)) + keys=(${keys:#*.pub}) + fi + _describe 'SSH key' keys + ;; + esac + ;; + esac + ;; + gpg) + _arguments -C \ + '1:gpg action:->gpg_action' \ + '*::gpg_args:->gpg_args' + + case "$state" in + gpg_action) + local -a actions=( + 'new:Create GPG key (interactive)' + 'quick-new:Create GPG key (quick)' + 'ls:List GPG keys' + 'pub:Export public key' + 'priv:Export secret key' + 'copy:Copy public key to clipboard' + 'fp:Show fingerprint' + 'rm:Delete GPG key' + ) + _describe 'gpg action' actions + ;; + gpg_args) + case "${line[1]}" in + ls) + _arguments '1:option:(--secret -s)' + ;; + quick-new) + _arguments \ + '1:name:' \ + '2:email:' \ + '3:expiry:(1y 2y 3y 5y 0)' + ;; + pub|priv|copy|fp|rm) + local -a key_ids + key_ids=(${(f)"$(gpg --list-keys --with-colons 2>/dev/null \ + | awk -F: '/^uid/{print $10}; /^pub/{print $5}')"}) + _describe 'GPG key ID or email' key_ids + ;; + esac + ;; + esac + ;; + esac + ;; + esac +} +compdef _keyman keyman From ff601692ec871fbbb719b379d9d38e0f0758ab90 Mon Sep 17 00:00:00 2001 From: XS Date: Thu, 2 Apr 2026 13:25:02 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat(keyman):=20=E6=9B=B4=E6=96=B0=20GPG=20?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E7=9A=84=E7=94=A8=E6=B3=95=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E5=AF=86=E9=92=A5=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/keyman/keyman.plugin.zsh | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/plugins/keyman/keyman.plugin.zsh b/plugins/keyman/keyman.plugin.zsh index 2fbd448c3..720826ba1 100644 --- a/plugins/keyman/keyman.plugin.zsh +++ b/plugins/keyman/keyman.plugin.zsh @@ -196,7 +196,7 @@ ${_km_cyan}GPG 命令:${_km_reset} usage_ssh_rm "Usage: keyman ssh rm " key_not_found "Key not found" # -- gpg quick-new -- - usage_gpg_quick_new "Usage: keyman gpg quick-new \"Name\" \"Email\" [expiry]" + usage_gpg_quick_new "Usage: keyman gpg quick-new \"Name\" \"Email\" [exp]" email_has_gpg_key "This email already has a GPG key:" confirm_create_new "Continue creating new key? (y/N) " creating_gpg_key "Creating GPG key..." @@ -735,8 +735,23 @@ _keyman() { ;; pub|priv|copy|fp|rm) local -a key_ids - key_ids=(${(f)"$(gpg --list-keys --with-colons 2>/dev/null \ - | awk -F: '/^uid/{print $10}; /^pub/{print $5}')"}) + key_ids=(${(f)"$(gpg --list-keys --with-colons 2>/dev/null | awk -F: ' + /^pub/ { keyid = $5 } + /^uid/ && keyid != "" { + uid = $10 + if (uid != "") { + print keyid ":" uid + n = index(uid, "<") + if (n > 0) { + email = substr(uid, n + 1) + p = index(email, ">") + if (p > 0) email = substr(email, 1, p - 1) + if (email != "") print email ":" uid + } + keyid = "" + } + } + ')"}) _describe 'GPG key ID or email' key_ids ;; esac