Zsh plugin that lets you edit your command line like a text editor. Select text with Shift + Arrow keys or the mouse, type or paste to replace selections, use standard editing shortcuts (copy, cut, paste, undo, redo, select all), and customize keybindings through an interactive wizard — with full X11 and Wayland clipboard support.
demo.mp4
- Overview
- Features
- Auto Installation
- Manual Installation
- Configuration Wizard
- Terminal Setup
- Wayland Support
- Default Key Bindings Reference
- Troubleshooting
- Platform Compatibility
- Performance-Optimized Architecture
- Contributing
- License
- Acknowledgments
- References
Zsh Edit-Select brings familiar text editor behaviors to your Zsh command line:
- ✅ Shift selection — Select text using Shift + Arrow keys
- ✅ Type-to-replace — Type over selected text to replace it
- ✅ Paste-to-replace — Paste clipboard content over selections
- ✅ Mouse integration — Works with text selected by mouse
- ✅ Clipboard integration — Works with X11 and Wayland
- ✅ Standard shortcuts — Ctrl+A (select all), Ctrl+C (copy), Ctrl+X (cut), Ctrl+V (paste), Ctrl+Z (undo), Ctrl+Shift+Z (redo)
Customization: The plugin works after installation with editor-like defaults. Use the command
edit-select configto customize mouse behavior and keybindings.
Select text using familiar keyboard shortcuts:
| Shortcut | Action |
|---|---|
| Shift + ←/→ | Select character by character |
| Shift + ↑/↓ | Select line by line |
| Shift + Home/End | Select to line start/end |
| Shift + Ctrl + ←/→ | Select word by word |
| Shift + Ctrl + Home/End | Select to buffer start/end |
| Ctrl + A | Select all text (including multi-line commands) |
The plugin intelligently integrates mouse selections:
When Mouse Replacement is Enabled (default):
- ✅ Copy mouse selections with Ctrl+C (or Ctrl+Shift+C if configured)
- ✅ Cut mouse selections with Ctrl+X
- ✅ Type to replace mouse selections
- ✅ Delete mouse selections with Backspace/Delete
- ✅ Paste over mouse selections with Ctrl+V
When Mouse Replacement is Disabled:
- ✅ Copy mouse selections with Ctrl+C (or Ctrl+Shift+C if configured)
- ✅ Replacement/Deletion work with keyboard selections
Note: Configure mouse behavior with the command
edit-select config→ Option 1
Type or paste while text is selected to replace it automatically.
Works with both keyboard and mouse selections (when mouse replacement is enabled).
⚠️ Mouse Replacement Note (Safeguard Prompt)
If you see the message "Duplicate text: place cursor inside the occurrence you want to modify", the plugin has detected multiple identical occurrences of the selected text in your command buffer.
When does this appear? This message only appears when:
- The selection was made with the mouse, AND
- The exact same text occurs more than once in the buffer, AND
- You try to replace the selected text by either typing or pasting
Why does this happen? This is a protective safeguard for the plugin's mouse-selection workaround. Since mouse replacement is not enabled by default, the implemented workaround cannot automatically distinguish between multiple occurrences of identical text. This prompt prevents accidental edits to the wrong occurrence when using mouse-based selection.
What should you do? When prompted, place the cursor inside the specific occurrence you want to edit, then retry the operation (select it and type or paste to replace).
Note: This safeguard is only for mouse selections. Using Shift+Arrow keys doesn't require caret
replacement and works directly without ambiguity or extra prompting.
Standard editing shortcuts:
- Ctrl + C (or Ctrl+Shift+C if configured): Copy selected text
- Ctrl + X: Cut selected text
- Ctrl + V: Paste (replaces selection if any)
Clipboard Managers Compatibility Note: The plugin is fully compatible with clipboard history managers like CopyQ, GPaste, and others. Since it uses standard X11 and Wayland clipboard protocols, all copied text is automatically captured by your clipboard manager.
Navigate through your command line editing history:
- Ctrl + Z: Undo last edit
- Ctrl + Shift + Z: Redo last undone edit
Note: The Ctrl+Z keybinding works seamlessly alongside the traditional suspend process functionality (Ctrl+Z suspends a running foreground process to background). The plugin intelligently handles undo operations for command line editing while preserving the ability to suspend processes when needed.
Note: The Copy and the Redo keybinding (Ctrl+Shift+Z) requires terminal configuration to send the correct escape sequence. See Terminal Setup for manual configuration instructions, or use the Auto Installation script to configure this automatically.
The plugin includes purpose-built clipboard agents that replace external tools entirely:
Clipboard Integration Agents: Small compiled programs built specifically for this plugin handle all clipboard and selection operations:
| Display Server | Agent | Protocol | Performance vs. External Tool |
|---|---|---|---|
| X11 | zes-x11-selection-agent |
XFixes extension + CLIPBOARD atom | 44.6% faster than xclip |
| Wayland | zes-wl-selection-agent |
zwp_primary_selection_unstable_v1 + ext_data_control_v1 / zwlr_data_control_v1 / wl_data_device |
96.4% faster than wl-copy |
| XWayland | zes-xwayland-agent |
X11 XFixes through XWayland | XWayland compatibility layer |
External Tools (Fallback Only):
| Display Server | Tool | When Used |
|---|---|---|
| X11 | xclip |
Only if agent unavailable |
| Wayland | wl-copy / wl-paste |
Only if agent unavailable |
The agents handle copy, paste, and clipboard operations directly through native protocols—no external tools needed. They run as background processes and communicate with the plugin through a fast in-memory cache, giving you instant clipboard response with zero subprocess overhead.
See Performance-Optimized Architecture for benchmarks and implementation details.
Recommendation: If you are comfortable editing dotfiles and prefer full control over your system configuration, Manual Installation is the recommended approach.
Installation consists of three straightforward steps:
- install dependencies
- plugin to your plugin manager
- configure your terminal
Each documented with exact commands and copy-paste configurations.
- Completing all three steps should take no longer than 8 minutes on a first install.
- All instructions are organized in collapsed sections so you can expand only what applies to your specific setup and platform.
The auto-installer is provided as a convenience for users who are less comfortable with terminal configuration or who prefer a fully guided, hands-off setup. It detects your environment (X11, Wayland, or XWayland), installs dependencies, sets up the plugin, and configures your terminal in a single run. It has been tested across multiple distributions using Docker containers and virtual machines, and handles the most common configurations — but not every edge case can be guaranteed. If you encounter an issue, please report it so it can be addressed.
To use the auto-installer, simply run:
curl -fsSL https://raw.githubusercontent.com/Michael-Matta1/zsh-edit-select/main/assets/auto-install.sh -o install.sh && chmod +x install.sh && bash install.shClick to expand
The installer is designed for reliability and system safety:
- Idempotency: The script checks your configuration files before making changes. It can be run multiple times without creating duplicate entries or corrupting files.
- System Safety: Creates timestamped backups of every file before modification. Implements standard signal trapping (INT, TERM, EXIT) to ensure clean rollbacks even if interrupted.
- Universal Compatibility: Supports 11 different package managers (including
apt,dnf,pacman,zypper,apk, andnix) across X11, Wayland, and XWayland environments. - Robust Pre-flight Checks: Validates network connectivity, disk space, and package manager health before starting. Also proactively detects and reports broken repositories (e.g., problematic apt sources) to prevent installation failures.
The script handles the end-to-end setup process:
| Category | Automated Actions |
|---|---|
| Dependencies | - Installs system packages (git, zsh, gcc, make, xclip/wl-clipboard)- Detects your OS (Debian, Fedora, Arch, etc.) and uses the correct package manager ( apt, dnf, pacman) |
| Plugin Manager | - Detects your existing manager (Oh My Zsh, Zinit, Antigen, Sheldon, etc.) - Offers to install Oh My Zsh if you don't have a plugin manager. You can refuse if you prefer manual installation - Note: The installer detects and installs the plugin for other managers such as Zinit or Antigen, but it does not install those managers themselves. If you prefer using them instead of OMZ, make sure they are installed before running the installer. |
| Terminal Setup | - Configures Kitty, Alacritty, WezTerm, Foot, and VS Code to support keybindings - Backs up existing config files before making changes |
| Safeguards | - Checks for conflicting keybindings in your .zshrc and terminal configuration files (Kitty, Alacritty, WezTerm, Foot, VS Code)- Verifies the installation with a self-test suite |
When run without arguments, the installer provides an interactive menu with the following options:
- Full Installation (Recommended): The complete setup process. Required for first-time installations.
- Configure Terminals Only: Only detects and configures your terminal emulators.
- Check for Conflicts Only: Scans your configuration files for conflicting keybindings.
- Update Plugin: Pulls the latest changes from the repository.
- Build Agents Only: Rebuild clipboard agents for your display server.
- Uninstall: Remove the plugin, configuration entries, and agents.
Advanced Usage & Options
You can customize the installation behavior with command-line flags. To use them, download the script first or pass them to bash:
| Option | Description |
|---|---|
--non-interactive |
Run in headless mode without user prompts (accepts all defaults) |
--skip-deps |
Skip installing system dependencies (useful if you manage packages manually) |
--skip-conflicts |
Skip the configuration conflict detection phase |
--skip-verify |
Skip the post-installation verification tests |
--test-mode |
Allow running as root (for testing only) |
--help |
Show the help message and exit |
Example: Non-interactive installation (CI/CD friendly)
bash auto-install.sh --non-interactiveThe script provides detailed, color-coded feedback for every step:
- ✅ Success: Step completed successfully
⚠️ Warning: Non-critical issue (e.g., optional component missing)- ❌ Error: Critical failure that requires attention
At the end, you'll receive a Summary Report listing all installed components and any manual steps
required. A detailed log is also saved to ~/.zsh-edit-select-install.log.
Troubleshooting / Manual Preference: If the automated installation fails or if you prefer to configure everything yourself, you can follow the comprehensive Manual Installation and Terminal Setup guides below.
Manual installation is the recommended approach if you are comfortable with dotfiles and want complete visibility and control over every change made to your system. The process consists of three steps:
- Install build dependencies — A one-line command for your distribution.
- Install the plugin — Clone the repository with your plugin manager and add one line to your
.zshrc. - Configure your terminal — Add a few keybinding entries to your terminal's config file.
All steps are fully documented with exact commands and copy-paste configuration snippets. The instructions are organized in collapsed sections labeled by distribution and terminal — expand only what applies to your setup.
If you prefer an automated setup, the Auto Installation script can handle all of these steps for you. If you run into any difficulty at any step, please open an issue and it will be addressed.
How to check if you're using X11 or Wayland
Run this command in your terminal:
echo $XDG_SESSION_TYPE- If it returns
x11→ You're using X11 - If it returns
wayland→ You're using Wayland
Note: The plugin automatically detects your display server and loads the appropriate implementation.
The plugin automatically compiles native agents on first use. Install the required build tools and libraries for your platform:
Debian/Ubuntu
sudo apt install build-essential libx11-dev libxfixes-dev pkg-config xclipArch Linux
sudo pacman -S --needed base-devel libx11 libxfixes pkgconf xclipFedora
sudo dnf install gcc make libX11-devel libXfixes-devel pkgconfig xclipDebian/Ubuntu
sudo apt install build-essential libx11-dev libxfixes-dev libwayland-dev wayland-protocols pkg-config wl-clipboardArch Linux
sudo pacman -S --needed base-devel libx11 libxfixes wayland wayland-protocols pkgconf wl-clipboardFedora
sudo dnf install gcc make libX11-devel libXfixes-devel wayland-devel wayland-protocols-devel pkgconfig wl-clipboardImportant: Before installing, ensure you have the required Build Dependencies installed.
You may use the Auto Installation script to perform this step automatically, or open an issue if you run into any difficulty.
Oh My Zsh
Expand your plugin manager:
git clone https://github.com/Michael-Matta1/zsh-edit-select.git \
${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-edit-selectAdd to your .zshrc:
plugins=(... zsh-edit-select)zgenom
zgenom load Michael-Matta1/zsh-edit-selectzinit
zinit light Michael-Matta1/zsh-edit-selectzplug
zplug "Michael-Matta1/zsh-edit-select"antigen
antigen bundle Michael-Matta1/zsh-edit-selectantibody (deprecated)
Note: antibody has been archived since May 2022. Consider migrating to antidote, a drop-in replacement.
antibody bundle Michael-Matta1/zsh-edit-selectzgen (unmaintained)
Note: zgen is no longer maintained. Consider migrating to zgenom, its maintained successor.
zgen load Michael-Matta1/zsh-edit-selectsheldon
sheldon add zsh-edit-select --github Michael-Matta1/zsh-edit-selectManual Installation
git clone https://github.com/Michael-Matta1/zsh-edit-select.git \
~/.local/share/zsh/plugins/zsh-edit-select
# Add to ~/.zshrc:
source ~/.local/share/zsh/plugins/zsh-edit-select/zsh-edit-select.plugin.zshSome terminals need configuration to support Shift selection. See Terminal Setup for details.
source ~/.zshrcImportant: You may need to fully close and reopen your terminal (not just source ~/.zshrc) for all features to work correctly, especially in some terminal emulators.
You're ready! Try selecting text with Shift + Arrow keys.
The plugin works immediately with sensible defaults, but you can customize:
- Mouse replacement behavior
- Keybindings (Ctrl+A, Ctrl+V, Ctrl+X, Ctrl+Z, Ctrl+Shift+Z)
Run the interactive configuration wizard:
edit-select configLaunch the interactive configuration wizard:
edit-select configThe wizard provides:
- Mouse Replacement — Enable/disable mouse selection integration
- Key Bindings — Customize Copy, Cut, Paste, Select All, Undo, Redo, and Word Navigation shortcuts
- View Full Configuration — See current settings
- Reset to Defaults — Restore factory settings
- Exit Wizard — Close the wizard
All changes are saved to ~/.config/zsh-edit-select/config and persist across sessions.
Mouse Replacement Modes
Configure how the plugin handles mouse selections:
Enabled (default):
- Full integration: type, paste, cut, and delete work with mouse selections
- Best for users who want seamless mouse+keyboard workflow
Disabled:
- Mouse selections can be copied with Ctrl+C (or Ctrl+Shift+C if configured)
- Typing, pasting, cutting, and deleting only work with keyboard selections
- Best for users who prefer strict keyboard-only editing
Change the mode:
edit-select config # → Option 1: Mouse ReplacementKeybinding Customization
Customize the main editing shortcuts:
edit-select config # → Option 2: Key BindingsDefault bindings:
- Ctrl + A — Select all
- Ctrl + V — Paste
- Ctrl + X — Cut
- Ctrl + Shift + C — Copy
- Ctrl + Z — Undo
- Ctrl + Shift + Z — Redo
- Ctrl + ← — Word left
- Ctrl + → — Word right
Custom Keybinding Notes (Terminal Configuration)
⚠️ Important: When using custom keybindings (especially with Shift modifiers), you may need to configure your terminal emulator to send the correct escape sequences.
To find the escape sequence for any key combination:
- Run
cat(without arguments) in your terminal - Press the key combination
- The terminal will display the escape sequence
- Use this sequence in your configuration
For example, if you want to use Ctrl + Shift + X for cut add the following to your terminal dotfile:
Kitty
Add to kitty.conf:
map ctrl+shift+x send_text all \x1b[88;6u
WezTerm
Add to wezterm.lua:
return {
keys = {
{
key = 'X',
mods = 'CTRL|SHIFT',
action = wezterm.action.SendString '\x1b[88;6u',
},
},
}Alacritty
Add to alacritty.toml:
[[keyboard.bindings]]
key = "X"
mods = "Control|Shift"
chars = "\u001b[88;6u"Legacy YAML format (deprecated since Alacritty v0.13)
key_bindings:
- { key: X, mods: Control|Shift, chars: "\x1b[88;6u" }VS Code Terminal
Add to keybindings.json (usually at ~/.config/Code/User/):
[
{
"key": "ctrl+shift+x",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[88;6u" },
"when": "terminalFocus"
}
]Display Server Override
Note: The plugin automatically uses the correct implementation (X11 or Wayland) based on your system.
# Force a specific implementation (overrides auto-detection)
export ZES_FORCE_IMPL=x11 # Force X11 implementation
export ZES_FORCE_IMPL=wayland # Force Wayland implementationUse case: Force a specific implementation if auto-detection fails or if you want to use a different display server intentionally.
The Auto Installation script can configure supported terminals (Kitty, WezTerm, Alacritty, Foot, VS Code) automatically. If you prefer to configure manually follow the steps below. Open an issue if you need help with a terminal that is not covered.
How to Find Escape Sequences
To find the escape sequence for any key combination:
- Run
cat(without arguments) in your terminal - Press the key combination
- The terminal will display the escape sequence
- Use this sequence in your configuration
⚠️ CRITICAL: Before adding these mappings, you MUST remove or comment out any existingctrl+shift+cmappings in your terminal config (such asmap ctrl+shift+c copy_to_clipboardin Kitty). These will conflict and prevent the plugin from working correctly.
Kitty
To use Ctrl+Shift+C for copying, add the following to kitty.conf:
map ctrl+shift+c send_text all \x1b[67;6u
If you prefer to use Ctrl+C for copying (like in GUI applications) and Ctrl+Shift+C for interrupt:
# Ctrl+C sends the escape sequence for copying
map ctrl+c send_text all \x1b[67;6u
# Ctrl+Shift+C sends interrupt (default behavior)
map ctrl+shift+c send_text all \x03
WezTerm
To use Ctrl+Shift+C for copying, add the following to wezterm.lua:
return {
keys = {
{
key = 'C',
mods = 'CTRL|SHIFT',
action = wezterm.action.SendString '\x1b[67;6u',
},
},
}If you prefer to use Ctrl+C for copying and Ctrl+Shift+C for interrupt:
return {
keys = {
{
-- Ctrl+C sends the escape sequence for copying
key = 'c',
mods = 'CTRL',
action = wezterm.action.SendString '\x1b[67;6u',
},
{
-- Ctrl+Shift+C sends interrupt signal
key = 'C',
mods = 'CTRL|SHIFT',
action = wezterm.action.SendString '\x03',
},
},
}Alacritty
Add to alacritty.toml:
[[keyboard.bindings]]
key = "C"
mods = "Control|Shift"
chars = "\u001b[67;6u"If you prefer to use Ctrl+C for copying and Ctrl+Shift+C for interrupt:
# Ctrl+C sends the escape sequence for copying
[[keyboard.bindings]]
key = "C"
mods = "Control"
chars = "\u001b[67;6u"
# Ctrl+Shift+C sends interrupt signal
[[keyboard.bindings]]
key = "C"
mods = "Control|Shift"
chars = "\u0003"Legacy YAML format (deprecated since Alacritty v0.13)
Default (Ctrl+Shift+C):
key_bindings:
- { key: C, mods: Control|Shift, chars: "\x1b[67;6u" }Reversed (Ctrl+C for copy):
key_bindings:
- { key: C, mods: Control, chars: "\x1b[67;6u" }
- { key: C, mods: Control|Shift, chars: "\x03" }VS Code Terminal
To use Ctrl+Shift+C for copying, add the following to keybindings.json (usually at ~/.config/Code/User/):
[
{
"key": "ctrl+shift+c",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[67;6u" },
"when": "terminalFocus"
}
]If you prefer to use Ctrl+C for copying and Ctrl+Shift+C for interrupt:
[
{
// Ctrl+C sends copy sequence to terminal
"key": "ctrl+c",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[67;6u" },
"when": "terminalFocus"
},
{
// Ctrl+Shift+C sends interrupt signal
"key": "ctrl+shift+c",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u0003" },
"when": "terminalFocus"
}
]Foot
Add the following to foot.ini. The default clipboard-copy binding must be unbound first so the escape
sequence reaches the shell:
[key-bindings]
clipboard-copy=none
[text-bindings]
\x1b[67;6u = Control+Shift+cIf you prefer to use Ctrl+C for copying and Ctrl+Shift+C for interrupt:
[key-bindings]
clipboard-copy=none
[text-bindings]
\x1b[67;6u = Control+c
\x03 = Control+Shift+cNote: Foot uses the
[text-bindings]section to send custom escape sequences to the shell. The defaultclipboard-copy=Control+Shift+cmust be unbound first, otherwise Foot intercepts the key for its own clipboard action and the plugin never receives it. If you follow both Step 1 and Step 2, merge the[key-bindings]and[text-bindings]entries into single sections.
Alternative: Without Terminal Remapping
If your terminal doesn't support key remapping, you can add the following to your ~/.zshrc to use Ctrl +
/ for copying:
x-copy-selection () {
if [[ $MARK -ne $CURSOR ]]; then
local start=$(( MARK < CURSOR ? MARK : CURSOR ))
local length=$(( MARK > CURSOR ? MARK - CURSOR : CURSOR - MARK ))
local selected="${BUFFER:$start:$length}"
print -rn "$selected" | xclip -selection clipboard
fi
}
zle -N x-copy-selection
bindkey '^_' x-copy-selectionYou can change the keybinding to any key you prefer. For example, to use Ctrl + K:
bindkey '^K' x-copy-selectionNote: The
^_sequence represents Ctrl + / (Ctrl + Slash), and^Krepresents Ctrl + K. You can find other key sequences by runningcatin your terminal and pressing the desired key combination.
Bonus Feature: If no text is selected, this manual keybinding will copy the entire current line to the clipboard.
Kitty
Add to kitty.conf:
map ctrl+shift+z send_text all \x1b[90;6u
WezTerm
Add to wezterm.lua:
return {
keys = {
{
key = 'Z',
mods = 'CTRL|SHIFT',
action = wezterm.action.SendString '\x1b[90;6u',
},
},
}Alacritty
Add to alacritty.toml:
[[keyboard.bindings]]
key = "Z"
mods = "Control|Shift"
chars = "\u001b[90;6u"Legacy YAML format (deprecated since Alacritty v0.13)
key_bindings:
- { key: Z, mods: Control|Shift, chars: "\x1b[90;6u" }VS Code Terminal
Add to keybindings.json (usually at ~/.config/Code/User/):
[
{
"key": "ctrl+z",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001a" },
"when": "terminalFocus"
},
{
"key": "ctrl+shift+z",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[90;6u" },
"when": "terminalFocus"
}
]Note: The Ctrl+Z keybinding works seamlessly alongside the traditional suspend process functionality (Ctrl+Z suspends a running foreground process to background). The plugin intelligently handles undo operations for command line editing while preserving the ability to suspend processes when needed.
Foot
Add the following to foot.ini. The default prompt-prev binding must be unbound first because Foot maps
Ctrl+Shift+Z to prompt navigation by default:
[key-bindings]
prompt-prev=none
[text-bindings]
\x1b[90;6u = Control+Shift+zNote: If you already configured Foot for Step 1 (Copy), merge these entries into the existing
[key-bindings]and[text-bindings]sections rather than creating duplicates.
Some terminals intercept Shift key combinations by default. Here's how to configure popular terminals:
Kitty
Add to kitty.conf:
# Pass Shift and Ctrl+Shift keys through to Zsh for selection
# (overrides any default or custom Kitty mappings on these keys)
map shift+left no_op
map shift+right no_op
map shift+up no_op
map shift+down no_op
map shift+home no_op
map shift+end no_op
# Ctrl+Shift+Left/Right default to previous_tab/next_tab in Kitty
map ctrl+shift+left no_op
map ctrl+shift+right no_op
# Ctrl+Shift+Home/End default to scroll_home/scroll_end in Kitty
map ctrl+shift+home no_op
map ctrl+shift+end no_op
WezTerm
Add to wezterm.lua:
return {
keys = {
{ key = 'LeftArrow', mods = 'CTRL|SHIFT', action = wezterm.action.DisableDefaultAssignment },
{ key = 'RightArrow', mods = 'CTRL|SHIFT', action = wezterm.action.DisableDefaultAssignment },
{ key = 'Home', mods = 'CTRL|SHIFT', action = wezterm.action.DisableDefaultAssignment },
{ key = 'End', mods = 'CTRL|SHIFT', action = wezterm.action.DisableDefaultAssignment },
},
}VS Code Terminal
Add to keybindings.json (usually at ~/.config/Code/User/):
[
{
"key": "shift+left",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[1;2D" },
"when": "terminalFocus"
},
{
"key": "shift+right",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[1;2C" },
"when": "terminalFocus"
},
{
"key": "shift+up",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[1;2A" },
"when": "terminalFocus"
},
{
"key": "shift+down",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[1;2B" },
"when": "terminalFocus"
},
{
"key": "shift+home",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[1;2H" },
"when": "terminalFocus"
},
{
"key": "shift+end",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[1;2F" },
"when": "terminalFocus"
},
{
"key": "ctrl+shift+left",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[1;6D" },
"when": "terminalFocus"
},
{
"key": "ctrl+shift+right",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[1;6C" },
"when": "terminalFocus"
},
{
"key": "ctrl+shift+home",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[1;6H" },
"when": "terminalFocus"
},
{
"key": "ctrl+shift+end",
"command": "workbench.action.terminal.sendSequence",
"args": { "text": "\u001b[1;6F" },
"when": "terminalFocus"
}
]Alacritty
Alacritty's defaults intercept Shift+Home (ScrollToTop) and Shift+End (ScrollToBottom). Add to
your Alacritty configuration to pass them through for selection:
TOML (alacritty.toml)
# Pass Shift+Home/End through for selection
# (overrides Alacritty defaults: ScrollToTop / ScrollToBottom)
[[keyboard.bindings]]
key = "Home"
mods = "Shift"
action = "ReceiveChar"
[[keyboard.bindings]]
key = "End"
mods = "Shift"
action = "ReceiveChar"Legacy YAML format (alacritty.yml, deprecated since Alacritty v0.13)
key_bindings:
# Pass Shift+Home/End through for selection
- { key: Home, mods: Shift, action: ReceiveChar }
- { key: End, mods: Shift, action: ReceiveChar }All other Shift / Ctrl+Shift arrow keys pass through to Zsh natively.
Foot
Foot passes Shift+Arrow keys through to the terminal natively — no additional configuration is needed for Shift selection.
Configurations in practice: The dev-dotfiles repository includes working setups for Kitty (
kitty.conf) and VS Code (keybindings.json) that demonstrate that this plugin can be seamlessly integrated alongside other tools and configurations.
The Auto Installation script automatically detects your display server and selects the correct agent. For manual setup, follow the Wayland-specific instructions in Manual Installation.
Wayland is fully supported with native protocol implementation. The plugin automatically detects your Wayland setup and uses the optimal clipboard agent:
Clipboard Agent Priority (automatically selected):
-
zes-wl-selection-agent(Native Wayland) — Clipboard integration with direct Wayland protocol support- Handles PRIMARY selection and CLIPBOARD using native Wayland protocols
- Works on all compositors with protocol support (Sway, Hyprland, KDE Plasma, River, Wayfire)
- Full mouse selection replacement — no external tools needed
- Sub-2.2ms clipboard latency (96.4% faster than
wl-copy)
-
zes-xwayland-agent(XWayland) — XWayland compatibility layer (used whenDISPLAYis available)- Uses X11 XFixes via XWayland for clipboard integration
- Seamless support for mixed X11/Wayland environments
- Complements the native Wayland agent for maximum compatibility
The native Wayland implementation connects directly to Wayland protocols, eliminating reliance on
wl-copy/wl-paste. All clipboard operations happen within the persistent agent process — zero subprocess overhead.Architecture: The clipboard agents (
zes-wl-selection-agent,zes-xwayland-agent,zes-x11-selection-agent) are lightweight background processes that integrate with display server clipboard protocols. Updates are written to a fast in-memory cache (typically onXDG_RUNTIME_DIRor/dev/shm). The shell reads this cache via a singlestat()call per keypress — no forks, no pipes, no latency.
Native Wayland Protocol Support (Fully Implemented)
zes-wl-selection-agent provides complete clipboard and selection support on all Wayland compositors with
protocol implementation:
Supported Compositors:
- ✅ wlroots-based compositors — Sway, Hyprland, River, Wayfire (full PRIMARY support)
- ✅ KDE Plasma Wayland — Full PRIMARY selection via
zwp_primary_selection_unstable_v1 - ✅ GNOME Wayland (Mutter) — Native Wayland implementation provides PRIMARY selection support*
*Note: GNOME's PRIMARY selection support depends on compositor configuration. If unavailable, XWayland bridge provides seamless fallback.
Performance Advantage: Direct Wayland protocol access means:
- No subprocess spawning for clipboard operations
- No
wl-copy/wl-pasteprocess overhead - Native event-driven architecture
- Instant response to selection changes
- Zero typing lag even with frequent selections
XWayland Bridge (Recommended, for Enhanced Compatibility)
zes-xwayland-agent uses XWayland (if available) for an extra X11 compatibility layer for clipboard
integration. XWayland provides:
- Seamless fallback for hybrid X11/Wayland environments
- Support for legacy applications running under XWayland
- Alternative PRIMARY selection detection when native Wayland protocols unavailable
Enabling XWayland (Recommended)
zes-xwayland-agent uses XWayland for clipboard integration — it requires XWayland to be available. XWayland
provides an X11 compatibility layer on top of Wayland, allowing the agent to use X11's XFixes extension —
completely invisible: no windows, no dock entries, no compositor artifacts.
Check if XWayland is already running:
echo $DISPLAYIf this prints something like :0 or :1, XWayland is already available.
If DISPLAY is empty, enable XWayland on your compositor:
GNOME (Mutter)
XWayland is enabled by default on GNOME. If it was disabled:
# Re-enable XWayland (requires logout/login)
gsettings reset org.gnome.mutter experimental-featuresOr add Xwayland to the experimental features if using a custom list. On GNOME 47+, XWayland starts on demand
when any X11 app connects.
KDE Plasma
XWayland is enabled by default. If disabled, re-enable it in:
System Settings → Display and Monitor → Compositor → Allow XWayland applications
Sway
XWayland is enabled by default. If disabled, add to your Sway config:
xwayland enable
Then reload Sway ($mod+Shift+C).
Hyprland
Add to your Hyprland config:
xwayland {
force_zero_scaling = true
}
XWayland is enabled by default in Hyprland.
Without XWayland: The plugin uses zes-wl-selection-agent directly, which uses
zwp_primary_selection_v1. This works on wlroots-based compositors (Sway, Hyprland) and KDE Plasma, but may
show a small surface in the dock/taskbar on GNOME/Mutter.
| Key Combination | Action |
|---|---|
| Ctrl + ← | Move cursor one word left |
| Ctrl + → | Move cursor one word right |
| Key Combination | Action |
|---|---|
| Shift + ← | Select one character left |
| Shift + → | Select one character right |
| Shift + ↑ | Select one line up |
| Shift + ↓ | Select one line down |
| Shift + Home | Select to line start |
| Shift + End | Select to line end |
| Shift + Ctrl + ← | Select to word start |
| Shift + Ctrl + → | Select to word end |
| Shift + Ctrl + Home | Select to buffer start |
| Shift + Ctrl + End | Select to buffer end |
| Ctrl + A | Select all text |
| Key Combination | Action |
|---|---|
| Ctrl + C | Copy selected text |
| Ctrl + X | Cut selected text |
| Ctrl + V | Paste (replaces selection if any) |
| Ctrl + Z | Undo last edit |
| Ctrl + Shift + Z | Redo last undone edit |
| Delete/Backspace | Delete selected text |
| Any character | Replace selected text if any |
Shift selection doesn't work
Solution: Configure your terminal to pass Shift key sequences. See Terminal Setup.
Verify: Run cat and press Shift+Left. You should see an escape sequence like ^[[1;2D.
Clipboard operations don't work
Solution: Install the required clipboard tool:
- Wayland:
wl-clipboard - X11:
xclip
Verify: Run wl-copy <<< "test" or xclip -i <<< "test" to check if the tool works.
Mouse replacement not working
Solution:
- Check if mouse replacement is enabled:
edit-select config→ View Configuration - Ensure your terminal supports mouse selection (most do)
- Try selecting text with your mouse, then typing—it should replace the selection
If this does not work for you, it is often due to platform limitations or compatibility issues with the PRIMARY selection. See Platform Compatibility for more details.
Ctrl+C doesn't copy
Solution: Configure your terminal to remap Ctrl+C. See Step 1: Configure Copy Shortcut at the Terminal Setup section.
Alternative: Use Ctrl+Shift+C for copying, or configure a custom keybinding with edit-select config, or
use the 'Without Terminal Remapping' method if your terminal doesn't support key remapping.
Configuration wizard doesn't launch
Symptoms: Running edit-select config shows "file not found" error
Solution:
- Check the plugin was installed correctly
- Verify the wizard file exists in the plugin directory (
edit-select-wizard-x11.zshon X11, oredit-select-wizard-wayland.zshon Wayland) - Ensure the file has read permissions:
# X11: chmod +r ~/.oh-my-zsh/custom/plugins/zsh-edit-select/impl-x11/edit-select-wizard-x11.zsh # Wayland: chmod +r ~/.oh-my-zsh/custom/plugins/zsh-edit-select/impl-wayland/edit-select-wizard-wayland.zsh
- Try sourcing your
.zshrcagain:source ~/.zshrc - Fully close and reopen your terminal
Delete key not removing mouse-selected text
If the Delete key does not remove mouse-selected text, ensure your ~/.zshrc does not contain a line that
forces the Delete key to the default handler such as:
bindkey '^[[3~' delete-charThat line will override the plugin's binding for the Delete key and prevent zsh-edit-select from handling
mouse selections correctly.
Solution: Remove or comment out that line and reload your shell:
source ~/.zshrcMouse selection replaces text in a different pane (tmux users)
Symptoms: When using tmux with multiple panes, selecting text with the mouse in one pane and then switching to another pane causes typed text to unexpectedly replace the previously selected text from the other pane.
Solution: Enable focus events in tmux. The plugin uses terminal focus reporting (DECSET 1004) to distinguish between selections made in the active pane versus other panes.
Add this line to your ~/.tmux.conf:
set-option -g focus-events onThen reload your tmux configuration:
tmux source-file ~/.tmux.confNote:
focus-events onhas been the default since tmux 3.3a (released April 2023). If you're running an older version of tmux, either upgrade or add the line above to your configuration.
Alternative: If you cannot enable focus events, you can disable mouse replacement entirely with
edit-select config → Option 1 → Disable. This will preserve keyboard selection functionality while
preventing cross-pane mouse selection issues.
Manual Build (Optional)
The plugin compiles agents automatically on first use. To manually build them, first locate your plugin directory — this depends on your plugin manager:
# Common locations (adjust to wherever you installed the plugin):
# Oh My Zsh: ~/.oh-my-zsh/custom/plugins/zsh-edit-select
# Zinit: ~/.local/share/zinit/plugins/Michael-Matta1---zsh-edit-select
# Sheldon: ~/.local/share/sheldon/repos/github.com/Michael-Matta1/zsh-edit-select
# Manual: wherever you ran: git clone https://github.com/Michael-Matta1/zsh-edit-select
PLUGIN_DIR=~/.oh-my-zsh/custom/plugins/zsh-edit-select # ← change this to your pathX11 Agent:
cd "$PLUGIN_DIR/impl-x11/backends/x11"
makeWayland Agent:
cd "$PLUGIN_DIR/impl-wayland/backends/wayland"
makeXWayland Agent:
cd "$PLUGIN_DIR/impl-wayland/backends/xwayland"
makeTo clean/rebuild:
make clean && makeNote: The plugin will automatically detect your display server and compile the appropriate agent on first shell startup. The Makefiles require:
- X11 builds:
libX11andlibXfixesdevelopment headers/libraries- Wayland builds:
wayland-clientdevelopment headers,wayland-scanner, andwayland-protocolsIf compilation fails, the plugin will fall back to using external clipboard tools (
xclipfor X11,wl-clipboardfor Wayland).
Build Optimization:
The default Makefiles compile with aggressive optimization flags for maximum runtime performance:
| Flag | Purpose |
|---|---|
-O3 |
Maximum compiler optimization level |
-march=native |
CPU-specific instruction set (SSE, AVX, etc.) |
-mtune=native |
CPU-specific scheduling optimizations |
-flto |
Link-time optimization across compilation units |
-ffunction-sections -fdata-sections |
Granular dead code elimination |
-Wl,--gc-sections |
Remove unused functions/data at link time |
-Wl,--as-needed |
Skip linking unused shared libraries |
-Wl,-z,now |
Resolve all symbols at load time (security + perf) |
-Wl,-z,relro |
Read-only relocations after startup |
-Wl,-z,noexecstack |
Mark stack non-executable (security hardening) |
-Wl,-O1 |
Linker-level optimization pass |
-Wl,--hash-style=gnu |
Faster symbol lookup with GNU hash tables |
-Wl,--build-id=none |
Omit build-id section for smaller binaries |
-fomit-frame-pointer |
Free up a register for better performance |
-fno-plt |
Eliminate PLT indirection for faster library calls |
-fno-semantic-interposition |
Enable inlining across translation units |
-fno-strict-aliasing |
Permit type-punning casts in X11 agents (X11 only) |
-fno-asynchronous-unwind-tables |
Omit async unwind info not needed by signal handlers |
-fno-unwind-tables |
Omit synchronous unwind tables (no C++ exceptions) |
-fmerge-all-constants |
Deduplicate identical constants across units |
-fipa-pta |
Interprocedural pointer analysis for better inlining |
-fno-ident |
Omit compiler identification string from binary |
-fno-stack-protector |
Remove stack-canary overhead (local-only agents) |
-DNDEBUG |
Disable assertions in release builds |
-funroll-loops |
Unroll small loops for throughput |
-s |
Strip symbols for smaller production binaries |
Important:
-march=nativeproduces binaries optimized for the CPU you're building on. These binaries may not run correctly on different CPU architectures. For distributed builds, replace-march=native -mtune=nativewith a portable baseline like-march=x86-64-v2.
Mouse Selection Replacement Feature
The Mouse Selection Replacement feature (automatically detecting and replacing mouse-selected text) has comprehensive support across platforms via our custom agent implementations:
X11 & XWayland:
- X11 - Complete PRIMARY selection support via XFixes extension
- XWayland bridge - Full compatibility layer for mixed environments
Native Wayland (Direct Protocol Implementation):
- wlroots-based compositors — Sway, Hyprland, River, Wayfire with
zwp_primary_selection_unstable_v1 - KDE Plasma Wayland - Full PRIMARY selection support via native protocols
- GNOME Wayland (Mutter) - Native Wayland implementation provides PRIMARY selection support where available
- Other Wayland compositors - Full support for any compositor implementing PRIMARY selection protocols
The native Wayland agent (zes-wl-selection-agent) provides:
- ✅ Direct protocol access (no
wl-copy/wl-pastesubprocess overhead) - ✅ Zero typing lag with instant selection detection
- ✅ Event-driven architecture
- ✅ Superior responsiveness compared to standard clipboard tools
- Verify native Wayland or XWayland support is available
- Check that your compositor supports PRIMARY selection protocols
- Disable mouse replacement if needed:
edit-select config→ Option 1 - Report issues with your compositor on GitHub
Testing Coverage
This plugin has been thoroughly and heavily tested on Kitty Terminal and briefly on other popular terminals.
If you encounter issues on other terminals or platforms, please open an issue with your terminal name, OS, and display server.
Core Features (Universal)
These features work universally on X11, Wayland, and XWayland:
- ✅ Shift+Arrow keys for text selection
- ✅ Ctrl+A to select all
- ✅ Ctrl+Shift+C to copy (or Ctrl+C in reversed mode)
- ✅ Ctrl+X to cut keyboard selection
- ✅ Ctrl+V to paste
- ✅ Ctrl+Z to undo
- ✅ Ctrl+Shift+Z to redo
- ✅ Delete/Backspace to remove keyboard selection
- ✅ Type or paste to replace keyboard selection
- ✅ Mouse selection replacement (where PRIMARY selection available)
The plugin architecture is built around compiled native C agents that run as persistent background processes.
Each agent tracks selection changes via display server events, writes updates to a RAM-backed cache, and the
shell reads that cache using a single zstat call per keypress — zero process forks during normal typing.
Backend detection, agent startup, and configuration loading occur once at plugin load; all subsequent
operations use the cached results directly.
Core architectural properties:
- Single-pass initialization — Backend detection, agent startup, and configuration loading occur at plugin load time. The results are cached in shell variables and reused for the entire session.
- Event-driven selection tracking — X11 XFixes events and Wayland compositor events drive cache updates;
all agents sleep in
poll()between events, consuming no CPU while idle. - Compiled C agents — Direct system calls compiled with aggressive optimization flags
(
-O3 -march=native -flto -fipa-ptaand link-time dead code elimination); no interpreter overhead. - RAM-backed cache — Cache files reside in
XDG_RUNTIME_DIR(tmpfs on most Linux distributions), withTMPDIRor/tmpas fallback. On standard systemd-based systems, all cache I/O remains in memory. - Graceful fallback — If the compiled agents are unavailable, the plugin falls back to standard clipboard
tools (
xclip,wl-paste/wl-copy) transparently. No functionality is lost.
Startup & Initialization
Backend Detection
- Platform detection runs once at plugin load time by inspecting
ZES_FORCE_IMPL,XDG_SESSION_TYPE,WAYLAND_DISPLAY, andDISPLAYin priority order - The detected backend (
x11orwayland) is stored in read-only shell variables (ZES_ACTIVE_IMPL,ZES_DETECTION_REASON,ZES_IMPL_PATH) and reused for the entire session - A double-load guard (
_ZES_LOADER_LOADED) prevents re-execution when.zshrcis re-sourced mid-session
Lazy Backend Loading
- Only the implementation matching the detected display server (X11 or Wayland) is sourced
- The other implementation is never loaded into memory, reducing both startup time and memory footprint
- The configuration wizard is also lazy-loaded — its file is only sourced when the user explicitly runs
edit-select config
Zsh Bytecode Compilation
- Plugin files and all backend
.zshfiles are compiled to.zwc(Zsh wordcode bytecode) on first load viazcompile - The bytecode is reused on subsequent sessions, bypassing source parsing entirely
- A file-existence guard (
[[ ! -f "${file}.zwc" ]]) prevents redundant recompilation
Agent Auto-Compilation
- If the compiled agent binary is missing but its
Makefileis present, the loader runsmakeautomatically in a subshell - Build errors produce stderr diagnostics naming the required
-devpackages for the user's distribution
Configuration Loading
- The configuration file (
~/.config/zsh-edit-select/config) is read once at startup and its values are stored in shell variables - No configuration file I/O occurs during individual plugin operations
Configuration Wizard
The wizard file is lazy-loaded — sourced only when the user explicitly invokes edit-select config,
adding zero overhead to normal shell sessions. All wizard operations are implemented entirely as Zsh
built-in operations with no subprocess spawning:
- Config reads use
while IFS= read -rloops; config writes useprint -r --with Zsh array filtering (${(@)array:#KEY=*}) — nosedorgrepforks at any point in the config I/O path - Screen redraws use inline ANSI escape sequences (
printf '\033[2J\033[3J\033[H') instead of theclearcommand; this also clears the scrollback buffer in a singlewrite()call rather than a fork - The color gradient used in the wizard UI is computed once and cached in
$_ZESW_GRADIENT_CACHEon first invocation; subsequent calls within the same shell session reuse the cached values directly - Keybinding changes applied through the wizard take effect immediately in the current shell session via
direct
bindkeycalls — no shell restart or.zshrcre-source is required
Agent Startup & Readiness
- Before launching a new agent instance, the backend removes any leftover
seqandprimarycache files from a previous session (rm -f "$_EDIT_SELECT_SEQ_FILE" "$_EDIT_SELECT_PRIMARY_FILE"). This prevents the shell from treating stale data written by the previous daemon as a new selection event immediately after startup. - The agent is launched inside a subshell using the pattern
( agent_binary "$cache_dir" &>/dev/null & ; disown ). The wrapping subshell isolates job control: the agent process does not appear in the shell'sjobslist, does not receiveSIGHUPwhen the terminal closes, and does not trigger Zsh background-job notifications. - The plugin polls for the agent's readiness signal (the
seqcache file appearing) with a maximum wait of 1 second (40 × 25 ms intervals), rather than using a fixed sleep — the poll exits as soon as the file appears, so startup overhead matches actual agent initialization time. - If a running agent is already present (PID file exists and
kill -0succeeds), it is reused without restart. - After the readiness poll completes, the plugin reads the initial
seqfile mtime and sets_EDIT_SELECT_EVENT_FIRED_FOR_MTIME=1. This marks the startup mtime as already-seen, preventing the first observed value from being treated as a new selection event on the first ZLE callback.
Runtime Execution
mtime-Based Selection Detection
The typing hot path is designed around a single stat() syscall per keypress:
- The background agent writes selection content to a
primarycache file, then updates aseqfile - The shell detects changes by reading the
seqfile's modification time viazstat(the Zsh builtin, which performs a directstat()syscall — no process fork) - If the mtime matches the cached value, the function returns immediately with no further work
- If the mtime has changed, the
primaryfile content is read via$(<file)(Zsh builtin read — also zero forks) and stored in a shell variable
Under normal typing conditions with no selection changes, the entire detection path costs one stat() syscall
and an integer comparison per keypress.
Write-Ordering Guarantee
The agent always writes the primary content file before updating the seq file. Since the shell uses the
seq file's mtime as its change signal, this ordering guarantees the shell never reads a half-written
primary file.
In-Memory State Caching
- The last-known selection state is held in shell variables (
_EDIT_SELECT_LAST_PRIMARY,_EDIT_SELECT_LAST_MTIME) _zes_sync_selection_state()returns immediately if the cache file mtime is unchanged- An event-fired gate (
_EDIT_SELECT_EVENT_FIRED_FOR_MTIME) prevents the same mtime from triggering redundant processing across multiple ZLE callbacks within the same redraw cycle - Keyboard selections bypass the mouse-detection path entirely
- State is invalidated only when the agent writes a new cache entry
- Widget handlers call
zle -c(flush pending typeahead) rather thanzle -Rc(flush + force full redraw); this avoids an unnecessary redraw cycle on every keypress that does not modify the display - After each paste or cut operation,
_zes_sync_after_paste()re-reads the currentseqfile mtime andprimaryfile content directly from the daemon cache and updates_EDIT_SELECT_LAST_MTIMEand_EDIT_SELECT_LAST_PRIMARY. This resets the detection baseline to the post-operation state, preventing the mtime written during the operation from being re-detected as a new selection event on the next ZLE callback.
Direct Buffer Manipulation
Paste and replace-selection operations compute the selection bounds and splice BUFFER directly using Zsh
string indexing (${BUFFER:0:$start}${replacement}${BUFFER:$((start+len))}), bypassing zle kill-region.
This prevents these operations from writing to ZLE's kill buffer, which would interfere with subsequent yank
(Ctrl+Y) operations. Mouse-selection deletion widgets use the same direct-splice approach. Cut operations
(Ctrl+X) intentionally retain kill-region so the deleted text remains available for yank.
Cut Operation Ordering
Cut copies the selected text to the clipboard before deleting it from the buffer. By performing the copy first, the clipboard server begins serving the content to other applications immediately while the subsequent buffer deletion completes — a single in-memory string splice with no external I/O.
Agent Health Monitoring
- Agent liveness is checked via
kill(pid, 0)at 30-second intervals (amortized viaEPOCHSECONDScomparison) - If the agent process has exited, it is restarted transparently
- Health checks are not issued on individual keypress operations
Event-Driven Detection
- X11 / XWayland: The agent subscribes to XFixes
XFixesSetSelectionOwnerNotifyMaskevents; it wakes only on selection owner changes. The main loop usespoll()with a 1-second timeout used solely for cleanSIGTERMshutdown — no periodic work is performed on timeout - Wayland: The compositor delivers primary selection events on owner change via
zwp_primary_selection_unstable_v1. A 50 mspoll()timeout provides a secondary detection path for content changes within the same selection owner (e.g., the user extending a terminal text selection without releasing the mouse button — which changes content without changing the selection owner) - All agents sleep in
poll()between events, consuming no CPU during idle periods
C Agent Internals
Compilation & Binary Optimization
Agents are compiled with aggressive optimization flags to minimize binary size and maximize runtime performance:
-O3 -march=native -mtune=native— Full optimization with CPU-specific instruction scheduling-flto(Link-Time Optimization) — Whole-program optimization across all translation units-fipa-pta— Interprocedural pointer analysis for better alias resolution-fomit-frame-pointer— Frees a general-purpose register by omitting the frame pointer-funroll-loops— Unrolls loops to reduce branch overhead in tight event-handling paths-fmerge-all-constants— Merges identical constants across translation units, reducing.rodatasize-ffunction-sections -fdata-sections+-Wl,--gc-sections— Dead code elimination: each function and data object is placed in its own section; the linker discards unreferenced sections-fno-plt -fno-semantic-interposition— Direct function calls without PLT indirection; allows the compiler to inline across translation units without interposition checks-fno-asynchronous-unwind-tables -fno-unwind-tables— Removes.eh_frameexception unwind sections (unnecessary for C agents that do not use C++ exceptions), reducing binary size-DNDEBUG— Disables allassert()checks in release builds, removing debug overhead-Wl,--as-needed— Only links libraries that are actually referenced-Wl,-O1— Linker optimization pass for symbol resolution and relocation processing-Wl,-z,now -Wl,-z,relro— Full RELRO: the Global Offset Table is resolved and marked read-only at load time-Wl,-z,noexecstack— Non-executable stack-Wl,--hash-style=gnu— GNU hash table for faster dynamic symbol lookup-s -Wl,--build-id=none -fno-ident— Strips all symbols, build-id, and compiler version strings from the binary-fno-strict-aliasing(X11 and XWayland agents only) — Permits the type-punning pointer casts required by Xlib's event structures without aliasing-rule violations; not applied to the Wayland agent, which does not cast between unrelated pointer types-fno-stack-protector— Removes stack-canary instrumentation overhead; the agents run locally as unprivileged user daemons with no network-facing attack surface- System libraries (
libwayland-client,libX11,libXfixes) are the only runtime dependencies
Operation Modes
Each agent binary supports five operation modes within a single executable, eliminating the need for separate per-mode binaries:
| Mode | CLI Flag | Behavior |
|---|---|---|
| Daemon | (default) | Persistent PRIMARY selection monitoring with event-driven cache updates |
| Oneshot | --oneshot |
Print current PRIMARY selection to stdout and exit |
| Get clipboard | --get-clipboard |
Print current CLIPBOARD contents to stdout and exit |
| Copy clipboard | --copy-clipboard |
Read stdin, take clipboard ownership, fork a background server |
| Clear primary | --clear-primary |
Clear the PRIMARY selection and exit |
Persistent File Descriptor Architecture
All three agents open the cache file descriptors (fd_primary, fd_seq) once at daemon startup and hold them
open for the entire agent lifetime. Cache writes use pwrite() (atomic positional write — no preceding
lseek()) followed by ftruncate() to trim the file to the exact written length, preventing stale trailing
bytes from longer previous entries. This reduces each cache update to 2 syscalls per file, compared to the
open()/write()/fsync()/close() pattern (4 syscalls per file) used by conventional approaches.
Content Deduplication (Wayland agent)
The Wayland agent's check_and_update_primary() and ps_device_handle_selection() compare incoming selection
content against a cached copy (last_known_content) using memcmp() before writing. When the content is
unchanged — common during static selections or repeated compositor events — the cache write is skipped
entirely, avoiding unnecessary disk I/O. When new content does arrive, buffer ownership is transferred by
nulling the source pointer (sel = NULL) after assigning it to last_known_content, rather than
duplicating the buffer — eliminating one malloc + memcpy per selection event.
The X11 and XWayland agents intentionally skip deduplication: they always increment the sequence counter and write, because a re-selection of identical text (e.g., deselect then re-select the same word) must still fire a new event in the shell for correct mouse-selection tracking.
Descriptor Safety
O_CLOEXEC is applied to every file descriptor: all open(), pipe2(), and memfd_create() calls include
the close-on-exec flag. This prevents file descriptor leaks if the agent forks a clipboard server child
process.
Sequence Counter Design
The sequence counter is seeded from time(NULL) at daemon startup. This provides monotonic ordering across
agent restarts — a newly started agent will always produce sequence values higher than those from the previous
instance, preventing the shell from misinterpreting a restart as "no change." The daemon writes the initial
sequence value to the seq file before the shell begins polling, closing the startup race window.
X11 Atom Handling
- The native X11 agent (
zes-x11-selection-agent) uses private atom names (ZES_SEL,ZES_CLIP) as selection conversion properties. This avoids collisions with properties written by other applications on a shared X server. - The XWayland agent (
zes-xwayland-agent) reuses the standardPRIMARYandCLIPBOARDatoms directly as property names, which is safe because XWayland provides an isolated per-session X server where no other clients compete for property names. - Both agents intern all atom handles once at startup and reuse them for the agent's lifetime — no per-event
XInternAtom()round-trips to the X server.
Clipboard Server Lifecycle
When the shell copies text to the clipboard (--copy-clipboard), the agent forks a background child process
that becomes the clipboard owner and serves paste requests to other applications:
- The parent process exits immediately, returning control to the shell
- The child calls
setsid()to create a new session and ignoresSIGHUPto survive terminal closure. The Wayland agent additionally ignoresSIGPIPEbecause paste requestors may close their pipe mid-transfer - X11 / XWayland: The server advertises
TARGETS,UTF8_STRING, andXA_STRING, and servesSelectionRequestevents in apoll()loop with 100 ms timeout. It exits when another application takes clipboard ownership (SelectionClear) or after approximately 50 seconds of idle time - Wayland: The server creates a
wl_data_sourceoffering multiple MIME types (text/plain;charset=utf-8,text/plain,UTF8_STRING,STRING) and responds tosendcallbacks. It exits when the compositor signals ownership loss via thecancelledcallback
Adaptive Poll Timeouts (selection retrieval)
When reading selection content after a conversion request, the agents use adaptive timeouts to balance responsiveness against syscall frequency:
- X11 / XWayland: 5 ms polls for the first 20 ms (catching common fast responses), then 20 ms polls thereafter to reduce syscall rate during slow responses
- Wayland: 500 ms initial timeout covers the IPC round-trip; subsequent read chunks use a 100 ms timeout to detect EOF quickly
Non-Blocking Clipboard Reads (Wayland agent)
Clipboard read pipes are created with pipe2(O_CLOEXEC) and configured with fcntl(fd, F_SETFL, O_NONBLOCK)
directly — without a preceding F_GETFL read — then read via poll() + read() in a loop with exponential
buffer growth (capped at 1 MB for PRIMARY, 4 MB for CLIPBOARD).
Protocol & Compositor Compatibility
Wayland Protocol Integration
The Wayland agent connects directly to the compositor via wl_display_connect() and negotiates protocol
support through the registry. It handles three distinct compositor architectures:
PRIMARY selection is managed via zwp_primary_selection_unstable_v1, which is the standard unstable protocol
supported by all major compositors.
Clipboard operations use a three-mechanism priority chain, selected based on compositor capabilities:
ext_data_control_v1(preferred) — The standardized successor to the wlroots data-control protocol. Supports clipboard read and write without requiring keyboard focus. The agent prefers this overzwlrwhen both are advertised.zwlr_data_control_unstable_v1— The wlroots-originated data-control protocol, serving as fallback whenext_data_control_v1is not available. Same capabilities.wl_data_device— Core Wayland protocol fallback for compositors without any data-control extension (primarily GNOME/Mutter versions before 47). Requires a valid keyboard focus serial, which the agent obtains by creating a visible surface.
An additional OSC 52 path is available for clipboard writes — a fire-and-forget terminal escape sequence
written in a single write() call to /dev/tty, requiring no Wayland protocol involvement.
Mutter/GNOME Compatibility
Mutter only delivers PRIMARY selection events to Wayland clients that have a mapped surface. The daemon creates
a permanent 1×1 pixel transparent xdg_toplevel surface with an empty input region (so it cannot receive
input focus or interfere with user interaction). The surface pixel is a fully transparent ARGB value, rendered
via a SHM buffer created with memfd_create() (or shm_open() on systems without memfd_create).
For --copy-clipboard on compositors requiring a keyboard focus serial (wl_data_device path), a separate
surface without an empty input region is created to receive wl_keyboard.enter events that carry the serial
needed by wl_data_device.set_selection().
The xdg_wm_base ping/pong handler responds to compositor ping requests — failure to respond causes the
compositor to mark the client as unresponsive and stop delivering events.
X11 XFixes Integration
- The X11 agents use
XFixesSelectSelectionInput()to subscribe toSetSelectionOwnerNotifyMaskon the root window - Events are delivered by the X server on selection owner changes — no polling is required
- The main loop uses
poll()on the X connection file descriptor instead of blockingXNextEvent(), because with glibc'ssignal()(which setsSA_RESTART), a blockingXNextEventcannot be interrupted bySIGTERM. Afterpoll()returns,XPending()is called to drain Xlib's internal buffer — data may have arrived during a previousread()that filled the internal buffer with multiple events.
XWayland Agent Selection
On Wayland sessions where DISPLAY is also set (XWayland available), the plugin selects zes-xwayland-agent
over zes-wl-selection-agent. The XWayland agent reads selection state through X11 atoms via the XWayland
bridge, bypassing the Wayland protocol stack entirely. This provides lower latency, avoids the Mutter surface
requirement, and offers broader compositor compatibility.
Selection Detection Architecture
Shell-Side Detection Path
The _zes_sync_selection_state() function is called by every widget before acting. Its execution path:
zstat -A stat_info +mtime "$SEQ_FILE"— reads the sequence cache file's mtime via a singlestat()syscall (Zsh builtin, zero forks)- If the mtime matches
_EDIT_SELECT_LAST_MTIME, the function returns immediately - If the mtime has changed, the
primaryfile is read via$(<file)(Zsh builtin) and_EDIT_SELECT_NEW_SELECTION_EVENTis set to 1 - The new mtime and an event-fired gate (
_EDIT_SELECT_EVENT_FIRED_FOR_MTIME) are updated to prevent the same mtime from re-triggering across multiple ZLE callbacks
ZLE Pre-Redraw Hook
The edit-select::zle-line-pre-redraw hook is registered via add-zle-hook-widget and runs before every
prompt redraw. It performs:
- Amortized liveness probe: Checks
kill -0 $pidonly ifEPOCHSECONDS > _ZES_LAST_PID_CHECK + 30. If the agent has died, restarts it transparently. - Mtime check: Same
zstatpath as_zes_sync_selection_state()— onestat()syscall per redraw. On mtime change, reads theprimaryfile and sets the event flag.
Cache File Protocol
- The agent writes primary content first, then increments and writes the sequence number — this ordering
guarantee prevents the shell from reading a partially updated
primaryfile - The shell reads only the sequence file's mtime as the change signal
- Full content is read only when a change is confirmed
- The sequence counter starts from
time(NULL), providing monotonic ordering even across agent restarts
Early Return Conditions
- Unchanged mtime → immediate return before any selection comparison
- Mouse replacement disabled →
_zes_detect_mouse_selection()returns immediately - Active keyboard selection → mouse detection path is never entered
- Stale selection state → invalidated on mtime change, not on a timer
Terminal Focus & Multi-Pane Isolation
DECSET 1004 Focus Tracking
Terminal focus tracking is enabled at startup via printf '\e[?1004h' >/dev/tty. The escape sequence is
written to /dev/tty rather than stdout to avoid triggering Powerlevel10k instant-prompt console-output
warnings. Terminals that do not support DECSET 1004 silently ignore the request; the plugin's behavior is
unchanged.
Focus-In Handler
When the terminal pane receives focus (CSI I escape sequence), the _zes_terminal_focus_in handler:
- Records the current
seqfile mtime as already-seen (_EDIT_SELECT_LAST_MTIME) - Sets
_EDIT_SELECT_EVENT_FIRED_FOR_MTIME = 1 - Clears
_EDIT_SELECT_NEW_SELECTION_EVENT,_EDIT_SELECT_ACTIVE_SELECTION, and_EDIT_SELECT_PENDING_SELECTION
This ensures that selection events written by another pane to the shared cache while this pane was unfocused
are not mistakenly treated as new mouse selections. Focus events are bound in all keymaps (emacs,
edit-select, and main).
Independent Selection State
Each terminal pane maintains its own selection state in independent shell variables. PRIMARY selection is cleared after each cut/paste operation to prevent a subsequent pane's detection from reading a stale value.
Resource Behavior
- All detection and configuration reads use in-memory cached values — no file I/O during normal typing
- Selection state changes are detected via one
stat()syscall per keypress; file content is read only when the mtime has changed - Agent liveness verification runs at 30-second intervals; it is not issued on individual keystroke operations
- C agents operate with direct system calls only; no interpreter or scripting runtime is involved at runtime
- Zsh plugin scripts are compiled to
.zwcbytecode on first load; source parsing is skipped on all subsequent sessions - Cache files reside in
XDG_RUNTIME_DIR(tmpfs on most Linux distributions),TMPDIR, or/tmp; on standard systemd-based systems, no disk I/O occurs - Integer state flags (
_EDIT_SELECT_DAEMON_ACTIVE,_EDIT_SELECT_NEW_SELECTION_EVENT, etc.) enable fast arithmetic checks without string comparison EPOCHSECONDSandEPOCHREALTIME(fromzsh/datetime) provide second-resolution and microsecond-resolution timestamps for liveness probes and selection timing respectively — nodateforks- The cache holds only the current selection state; stale entries are not accumulated
Clipboard Operation Responsiveness
The following tables document clipboard operation latency for the custom agent, measured with
clock_gettime(CLOCK_MONOTONIC) across multiple payload sizes and iteration counts. All measurements include
full end-to-end time: from operation initiation through data availability.
X11 Clipboard Latency:
| Test Scenario | xclip Avg | Custom Avg | Improvement |
|---|---|---|---|
| Small text (50 chars, 100 iterations) | 4.025 ms | 2.258 ms | 43.9% faster |
| Medium text (500 chars, 50 iterations) | 4.307 ms | 2.211 ms | 48.7% faster |
| Large text (5KB, 25 iterations) | 3.949 ms | 2.310 ms | 41.5% faster |
| Very large (50KB, 10 iterations) | 4.451 ms | 2.499 ms | 43.9% faster |
| Rapid consecutive (200 iterations) | 4.206 ms | 2.321 ms | 44.8% faster |
| Overall Average | 4.187 ms | 2.320 ms | 44.6% faster |
Wayland Clipboard Latency:
| Test Scenario | wl-copy Avg | Custom Avg | Improvement |
|---|---|---|---|
| Small text (50 chars, 100 iterations) | 57.073 ms | 1.966 ms | 96.6% faster |
| Medium text (500 chars, 50 iterations) | 60.382 ms | 2.441 ms | 96.0% faster |
| Large text (5KB, 25 iterations) | 63.020 ms | 1.809 ms | 97.1% faster |
| Very large (50KB, 10 iterations) | 58.343 ms | 2.907 ms | 95.0% faster |
| Rapid consecutive (200 iterations) | 58.860 ms | 1.546 ms | 97.4% faster |
| Overall Average | 59.535 ms | 2.134 ms | 96.4% faster |
Observed Latency:
- X11: 2.320 ms average; 2.211 ms minimum across all payload sizes
- Wayland: 2.134 ms average; 1.546 ms minimum under rapid consecutive operations
- Latency is consistent across payload sizes from 50 bytes to 50 KB
- Paste operations retrieve data directly from the in-memory agent cache
Why the Wayland improvement is larger than X11:
wl-copy forks a new process for every clipboard operation, adding approximately 60 ms of fork()+exec()
and IPC overhead regardless of payload size. xclip also forks per operation, but its overhead is
approximately 4.2 ms — one order of magnitude lower. The persistent agent eliminates the process spawn cost
on both platforms; the remaining latency is the native protocol IPC round-trip time.
Clipboard Server Behavior:
- The agent maintains clipboard ownership and responds to paste requests internally, without involving the shell process
- On X11/XWayland, the clipboard server exits when another application takes clipboard ownership
(
SelectionClearevent) or after approximately 50 seconds of inactivity (whichever comes first) - On Wayland, the clipboard server exits when the compositor signals ownership loss via the
cancelledcallback - If the compiled agents are unavailable, the plugin falls back to
xclip(X11) orwl-copy/wl-paste(Wayland) — all functionality is preserved
Benchmark Methodology: Tests conducted using purpose-built C benchmarking tools with
clock_gettime(CLOCK_MONOTONIC)for nanosecond accuracy. Each iteration measures the full end-to-end path including process spawn, IPC, and data transfer. The benchmark suite is available inassets/benchmarks/.
Operations complete faster than the human perception threshold.
Contributions, suggestions, and recommendations are welcome. If you encounter a bug or unexpected behavior, please open an issue with a clear description and steps to reproduce. Pull requests are open for any meaningful improvement — bug fixes, new features, or compatibility with additional environments.
If you have ideas for enhancements, feature requests, or recommendations to improve the plugin's functionality or documentation, feel free to share them. Your feedback helps shape the direction of the project and ensures it meets the needs of the community.
A note on development: This plugin is developed and tested privately over an extended period before any public release. After every change — whether a fix, enhancement, or new feature — the plugin is heavily tested to validate stability and catch regressions under real conditions. New features are typically accompanied by new edge cases; each one is identified and resolved before the code is released. The goal is to ship complete, reliable increments rather than incremental works-in-progress. As a result, public commits tend to represent significant, well-tested milestones rather than a continuous stream of small changes.
If something does not work as expected, please report it — every issue report directly improves the plugin's reliability for everyone.
This project is licensed under the MIT License.
-
This project began as a fork (Michael-Matta1/zsh-shift-select) of jirutka/zsh-shift-select
- The fork was started to add the ability to copy selected text, because the jirutka/zsh-shift-select plugin only supported deleting selected text and did not offer copying by default. Since then, the project has evolved with its own new features, enhancements, bug fixes, design improvements, and a fully changed codebase, and it now provides a full editor-like experience.
-
The bundled Wayland protocol XML files and their
wayland-scanner-generated C bindings are covered by their respective copyright and license terms:primary-selection-unstable-v1.xml— Copyright © 2015, 2016 Red Hat (MIT License)wlr-data-control-unstable-v1.xml— Copyright © 2018 Simon Ser, © 2019 Ivan Molodetskikh (MIT-like License)ext-data-control-v1.xml— Copyright © 2018 Simon Ser, © 2019 Ivan Molodetskikh, © 2024 Neal Gompa (MIT-like License)
The
xdg-shellbinding files follow the same pattern, generated from thexdg-shell.xmlspecification in the wayland-protocols repository.
-
Michael-Matta1/dev-dotfiles — Dotfiles showcasing the plugin with Kitty, VS Code, and Zsh.
-
Zsh ZLE shift selection — StackOverflow — Q&A on Shift-based selection in ZLE.
-
Zsh Line Editor Documentation — Official ZLE widgets and keybindings reference.
