Skip to content

Michael-Matta1/zsh-edit-select

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Zsh Edit-Select

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
If the demo video is unavailable, click here to view the GIF.

Demo


Table of Contents


Overview

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 config to customize mouse behavior and keybindings.


Features

Keyboard Selection

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)

Mouse Selection Integration

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-to-Replace and Paste-to-Replace

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.

Copy, Cut, and Paste

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.

Undo and Redo

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.

Clipboard Integration

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.


Auto Installation

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:

  1. install dependencies
  2. plugin to your plugin manager
  3. 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.sh

Key Features

Click 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, and nix) 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.

Automated Capabilities

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

Interactive Menu

When run without arguments, the installer provides an interactive menu with the following options:

  1. Full Installation (Recommended): The complete setup process. Required for first-time installations.
  2. Configure Terminals Only: Only detects and configures your terminal emulators.
  3. Check for Conflicts Only: Scans your configuration files for conflicting keybindings.
  4. Update Plugin: Pulls the latest changes from the repository.
  5. Build Agents Only: Rebuild clipboard agents for your display server.
  6. 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-interactive

Installation Output

The 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

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:

  1. Install build dependencies — A one-line command for your distribution.
  2. Install the plugin — Clone the repository with your plugin manager and add one line to your .zshrc.
  3. 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.

1. Prerequisites (Build Dependencies)

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:

For X11 Users

Debian/Ubuntu
sudo apt install build-essential libx11-dev libxfixes-dev pkg-config xclip
Arch Linux
sudo pacman -S --needed base-devel libx11 libxfixes pkgconf xclip
Fedora
sudo dnf install gcc make libX11-devel libXfixes-devel pkgconfig xclip

For Wayland Users

Debian/Ubuntu
sudo apt install build-essential libx11-dev libxfixes-dev libwayland-dev wayland-protocols pkg-config wl-clipboard
Arch Linux
sudo pacman -S --needed base-devel libx11 libxfixes wayland wayland-protocols pkgconf wl-clipboard
Fedora
sudo dnf install gcc make libX11-devel libXfixes-devel wayland-devel wayland-protocols-devel pkgconfig wl-clipboard

2. Install the Plugin

Important: 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-select

Add to your .zshrc:

plugins=(... zsh-edit-select)
zgenom
zgenom load Michael-Matta1/zsh-edit-select
zinit
zinit light Michael-Matta1/zsh-edit-select
zplug
zplug "Michael-Matta1/zsh-edit-select"
antigen
antigen bundle Michael-Matta1/zsh-edit-select
antibody (deprecated)

Note: antibody has been archived since May 2022. Consider migrating to antidote, a drop-in replacement.

antibody bundle Michael-Matta1/zsh-edit-select
zgen (unmaintained)

Note: zgen is no longer maintained. Consider migrating to zgenom, its maintained successor.

zgen load Michael-Matta1/zsh-edit-select
sheldon
sheldon add zsh-edit-select --github Michael-Matta1/zsh-edit-select
Manual 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.zsh

3. Configure Your Terminal

Some terminals need configuration to support Shift selection. See Terminal Setup for details.

4. Restart Your Shell

source ~/.zshrc

Important: 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.

5. (Optional) Customize Settings

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 config

Configuration Wizard

Launch the interactive configuration wizard:

edit-select config

The wizard provides:

  1. Mouse Replacement — Enable/disable mouse selection integration
  2. Key Bindings — Customize Copy, Cut, Paste, Select All, Undo, Redo, and Word Navigation shortcuts
  3. View Full Configuration — See current settings
  4. Reset to Defaults — Restore factory settings
  5. 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 Replacement
Keybinding Customization

Customize the main editing shortcuts:

edit-select config  # → Option 2: Key Bindings

Default 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:

  1. Run cat (without arguments) in your terminal
  2. Press the key combination
  3. The terminal will display the escape sequence
  4. 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.

Environment Variables

# Force a specific implementation (overrides auto-detection)
export ZES_FORCE_IMPL=x11    # Force X11 implementation
export ZES_FORCE_IMPL=wayland # Force Wayland implementation

Use case: Force a specific implementation if auto-detection fails or if you want to use a different display server intentionally.


Terminal Setup

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:

  1. Run cat (without arguments) in your terminal
  2. Press the key combination
  3. The terminal will display the escape sequence
  4. Use this sequence in your configuration

Step 1: Configure Copy Shortcut

⚠️ CRITICAL: Before adding these mappings, you MUST remove or comment out any existing ctrl+shift+c mappings in your terminal config (such as map ctrl+shift+c copy_to_clipboard in Kitty). These will conflict and prevent the plugin from working correctly.

Kitty

Using Ctrl+Shift+C (Default)

To use Ctrl+Shift+C for copying, add the following to kitty.conf:

map ctrl+shift+c send_text all \x1b[67;6u

Using Ctrl+C for Copying (Reversed)

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

Using Ctrl+Shift+C (Default)

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',
    },
  },
}

Using Ctrl+C for Copying (Reversed)

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

Using Ctrl+Shift+C (Default)

Add to alacritty.toml:

[[keyboard.bindings]]
key = "C"
mods = "Control|Shift"
chars = "\u001b[67;6u"

Using Ctrl+C for Copying (Reversed)

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

Using Ctrl+Shift+C (Default)

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"
  }
]

Using Ctrl+C for Copying (Reversed)

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

Using Ctrl+Shift+C (Default)

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+c

Using Ctrl+C for Copying (Reversed)

If 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+c

Note: Foot uses the [text-bindings] section to send custom escape sequences to the shell. The default clipboard-copy=Control+Shift+c must 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-selection

You can change the keybinding to any key you prefer. For example, to use Ctrl + K:

bindkey '^K' x-copy-selection

Note: The ^_ sequence represents Ctrl + / (Ctrl + Slash), and ^K represents Ctrl + K. You can find other key sequences by running cat in 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.


Step 2: Configure Undo and Redo Shortcut

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+z

Note: 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.


Step 3: Enable Shift Selection Keys

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.


Wayland Support

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):

  1. 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)
  2. zes-xwayland-agent (XWayland) — XWayland compatibility layer (used when DISPLAY is 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 on XDG_RUNTIME_DIR or /dev/shm). The shell reads this cache via a single stat() 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-paste process 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 $DISPLAY

If 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-features

Or 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.


Default Key Bindings Reference

Navigation Keys

Key Combination Action
Ctrl + ← Move cursor one word left
Ctrl + → Move cursor one word right

Selection Keys

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

Editing Keys

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

Troubleshooting

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:

  1. Check if mouse replacement is enabled: edit-select config → View Configuration
  2. Ensure your terminal supports mouse selection (most do)
  3. 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:

  1. Check the plugin was installed correctly
  2. Verify the wizard file exists in the plugin directory (edit-select-wizard-x11.zsh on X11, or edit-select-wizard-wayland.zsh on Wayland)
  3. 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
  4. Try sourcing your .zshrc again: source ~/.zshrc
  5. 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-char

That 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 ~/.zshrc
Mouse 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 on

Then reload your tmux configuration:

tmux source-file ~/.tmux.conf

Note: focus-events on has 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 path

X11 Agent:

cd "$PLUGIN_DIR/impl-x11/backends/x11"
make

Wayland Agent:

cd "$PLUGIN_DIR/impl-wayland/backends/wayland"
make

XWayland Agent:

cd "$PLUGIN_DIR/impl-wayland/backends/xwayland"
make

To clean/rebuild:

make clean && make

Note: The plugin will automatically detect your display server and compile the appropriate agent on first shell startup. The Makefiles require:

  • X11 builds: libX11 and libXfixes development headers/libraries
  • Wayland builds: wayland-client development headers, wayland-scanner, and wayland-protocols

If compilation fails, the plugin will fall back to using external clipboard tools (xclip for X11, wl-clipboard for 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=native produces 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=native with a portable baseline like -march=x86-64-v2.


Platform Compatibility

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:

✅ Fully Supported

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

Performance on Wayland

The native Wayland agent (zes-wl-selection-agent) provides:

  • ✅ Direct protocol access (no wl-copy/wl-paste subprocess overhead)
  • ✅ Zero typing lag with instant selection detection
  • ✅ Event-driven architecture
  • ✅ Superior responsiveness compared to standard clipboard tools

If Selection Replacement Doesn't Work

  1. Verify native Wayland or XWayland support is available
  2. Check that your compositor supports PRIMARY selection protocols
  3. Disable mouse replacement if needed: edit-select config → Option 1
  4. 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)

Performance-Optimized Architecture

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-pta and link-time dead code elimination); no interpreter overhead.
  • RAM-backed cache — Cache files reside in XDG_RUNTIME_DIR (tmpfs on most Linux distributions), with TMPDIR or /tmp as 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.

Optimization Techniques

Startup & Initialization

Backend Detection

  • Platform detection runs once at plugin load time by inspecting ZES_FORCE_IMPL, XDG_SESSION_TYPE, WAYLAND_DISPLAY, and DISPLAY in priority order
  • The detected backend (x11 or wayland) 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 .zshrc is 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 .zsh files are compiled to .zwc (Zsh wordcode bytecode) on first load via zcompile
  • 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 Makefile is present, the loader runs make automatically in a subshell
  • Build errors produce stderr diagnostics naming the required -dev packages 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 -r loops; config writes use print -r -- with Zsh array filtering (${(@)array:#KEY=*}) — no sed or grep forks 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 the clear command; this also clears the scrollback buffer in a single write() call rather than a fork
  • The color gradient used in the wizard UI is computed once and cached in $_ZESW_GRADIENT_CACHE on 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 bindkey calls — no shell restart or .zshrc re-source is required

Agent Startup & Readiness

  • Before launching a new agent instance, the backend removes any leftover seq and primary cache 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's jobs list, does not receive SIGHUP when the terminal closes, and does not trigger Zsh background-job notifications.
  • The plugin polls for the agent's readiness signal (the seq cache 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 -0 succeeds), it is reused without restart.
  • After the readiness poll completes, the plugin reads the initial seq file 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:

  1. The background agent writes selection content to a primary cache file, then updates a seq file
  2. The shell detects changes by reading the seq file's modification time via zstat (the Zsh builtin, which performs a direct stat() syscall — no process fork)
  3. If the mtime matches the cached value, the function returns immediately with no further work
  4. If the mtime has changed, the primary file 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 than zle -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 current seq file mtime and primary file content directly from the daemon cache and updates _EDIT_SELECT_LAST_MTIME and _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 via EPOCHSECONDS comparison)
  • 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 XFixesSetSelectionOwnerNotifyMask events; it wakes only on selection owner changes. The main loop uses poll() with a 1-second timeout used solely for clean SIGTERM shutdown — 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 ms poll() 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 .rodata size
  • -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_frame exception unwind sections (unnecessary for C agents that do not use C++ exceptions), reducing binary size
  • -DNDEBUG — Disables all assert() 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 standard PRIMARY and CLIPBOARD atoms 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 ignores SIGHUP to survive terminal closure. The Wayland agent additionally ignores SIGPIPE because paste requestors may close their pipe mid-transfer
  • X11 / XWayland: The server advertises TARGETS, UTF8_STRING, and XA_STRING, and serves SelectionRequest events in a poll() 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_source offering multiple MIME types (text/plain;charset=utf-8, text/plain, UTF8_STRING, STRING) and responds to send callbacks. It exits when the compositor signals ownership loss via the cancelled callback

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:

  1. 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 over zwlr when both are advertised.
  2. zwlr_data_control_unstable_v1 — The wlroots-originated data-control protocol, serving as fallback when ext_data_control_v1 is not available. Same capabilities.
  3. 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 to SetSelectionOwnerNotifyMask on 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 blocking XNextEvent(), because with glibc's signal() (which sets SA_RESTART), a blocking XNextEvent cannot be interrupted by SIGTERM. After poll() returns, XPending() is called to drain Xlib's internal buffer — data may have arrived during a previous read() 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:

  1. zstat -A stat_info +mtime "$SEQ_FILE" — reads the sequence cache file's mtime via a single stat() syscall (Zsh builtin, zero forks)
  2. If the mtime matches _EDIT_SELECT_LAST_MTIME, the function returns immediately
  3. If the mtime has changed, the primary file is read via $(<file) (Zsh builtin) and _EDIT_SELECT_NEW_SELECTION_EVENT is set to 1
  4. 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:

  1. Amortized liveness probe: Checks kill -0 $pid only if EPOCHSECONDS > _ZES_LAST_PID_CHECK + 30. If the agent has died, restarts it transparently.
  2. Mtime check: Same zstat path as _zes_sync_selection_state() — one stat() syscall per redraw. On mtime change, reads the primary file 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 primary file
  • 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:

  1. Records the current seq file mtime as already-seen (_EDIT_SELECT_LAST_MTIME)
  2. Sets _EDIT_SELECT_EVENT_FIRED_FOR_MTIME = 1
  3. 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 .zwc bytecode 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
  • EPOCHSECONDS and EPOCHREALTIME (from zsh/datetime) provide second-resolution and microsecond-resolution timestamps for liveness probes and selection timing respectively — no date forks
  • 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 (SelectionClear event) or after approximately 50 seconds of inactivity (whichever comes first)
  • On Wayland, the clipboard server exits when the compositor signals ownership loss via the cancelled callback
  • If the compiled agents are unavailable, the plugin falls back to xclip (X11) or wl-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 in assets/benchmarks/.

Operations complete faster than the human perception threshold.


Contributing

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.


License

This project is licensed under the MIT License.


Acknowledgments

    • 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.
  • Wayland Protocol Specifications

    The bundled Wayland protocol XML files and their wayland-scanner-generated C bindings are covered by their respective copyright and license terms:

    The xdg-shell binding files follow the same pattern, generated from the xdg-shell.xml specification in the wayland-protocols repository.


References

About

Edit your Zsh command line like a text editor, with Shift/Shift+Ctrl selection, mouse support, type-to-replace editing, clipboard-aware copy/cut/paste, and fully customizable keybindings.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors