Skip to content

Paste Clipboard Images into Markdown (Zed + Hyprland)

Zed editor on Wayland/Hyprland does not natively support pasting images into Markdown files (zed-industries/zed#18112). This workaround uses a shell script and a Hyprland keybinding to:

  1. Save the clipboard image to an assets/ directory next to the current Markdown file
  2. Insert the Markdown image reference at the cursor position

Prerequisites

PackagePurpose
wl-clipboardRead clipboard contents (wl-paste, wl-copy)
wtypeSimulate keyboard input on Wayland
hyprctlQuery active window info from Hyprland
python3Parse JSON output
notify-sendDesktop notifications for feedback

Install

On Arch Linux (Omarchy):

bash
sudo pacman -S wl-clipboard wtype libnotify

Setup

1. Create the script

Save to ~/.local/bin/md-paste-image:

bash
#!/bin/bash
# md-paste-image: Save clipboard image to assets/ and type Markdown reference
# Designed for use with Zed editor on Hyprland/Wayland

set -euo pipefail

# Check if clipboard has an image
MIME=$(wl-paste --list-types 2>/dev/null | grep -E '^image/' | head -1)
if [[ -z "$MIME" ]]; then
  notify-send -u normal -t 3000 "Paste Image" "No image in clipboard"
  exit 1
fi

# Determine file extension from mime type
case "$MIME" in
  image/png)  EXT="png" ;;
  image/jpeg) EXT="jpg" ;;
  image/webp) EXT="webp" ;;
  image/gif)  EXT="gif" ;;
  image/bmp)  EXT="bmp" ;;
  *)          EXT="png" ;;
esac

# Get active window info from Hyprland
WINDOW_JSON=$(hyprctl activewindow -j 2>/dev/null)
WINDOW_CLASS=$(echo "$WINDOW_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('class',''))" 2>/dev/null)
WINDOW_TITLE=$(echo "$WINDOW_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('title',''))" 2>/dev/null)

if [[ "$WINDOW_CLASS" != *"zed"* && "$WINDOW_CLASS" != *"Zed"* ]]; then
  notify-send -u normal -t 3000 "Paste Image" "Not in Zed editor (window: $WINDOW_CLASS)"
  exit 1
fi

# Parse Zed title: "project — file.md"
PROJECT_NAME=$(echo "$WINDOW_TITLE" | sed 's/ — .*//')
FILE_PART=$(echo "$WINDOW_TITLE" | sed 's/^[^—]*— //')

if [[ -z "$PROJECT_NAME" || -z "$FILE_PART" ]]; then
  notify-send -u normal -t 3000 "Paste Image" "Could not parse Zed window title"
  exit 1
fi

# Find the project directory
PROJECT_DIR=""
for base in "$HOME/Development" "$HOME/Projects" "$HOME/Code" "$HOME/Documents" "$HOME"; do
  if [[ -d "$base" ]]; then
    FOUND=$(find "$base" -maxdepth 5 -type d -name "$PROJECT_NAME" 2>/dev/null | head -1)
    if [[ -n "$FOUND" ]]; then
      PROJECT_DIR="$FOUND"
      break
    fi
  fi
done

if [[ -z "$PROJECT_DIR" ]]; then
  notify-send -u normal -t 3000 "Paste Image" "Could not find project: $PROJECT_NAME"
  exit 1
fi

# Resolve the markdown file path - prefer shortest path
MD_FILE="$PROJECT_DIR/$FILE_PART"
if [[ ! -f "$MD_FILE" ]]; then
  MD_FILE=$(find "$PROJECT_DIR" -type f -name "$(basename "$FILE_PART")" 2>/dev/null | awk '{ print length, $0 }' | sort -n | head -1 | cut -d' ' -f2-)
fi

if [[ -z "$MD_FILE" || ! -f "$MD_FILE" ]]; then
  notify-send -u normal -t 3000 "Paste Image" "Could not find file: $FILE_PART"
  exit 1
fi

# Create assets directory next to the markdown file
MD_DIR=$(dirname "$MD_FILE")
ASSETS_DIR="$MD_DIR/assets"
mkdir -p "$ASSETS_DIR"

# Generate filename with timestamp
TIMESTAMP=$(date +%Y-%m-%d_%H%M%S)
IMG_NAME="${TIMESTAMP}.${EXT}"
IMG_PATH="$ASSETS_DIR/$IMG_NAME"

# Save clipboard image to file
wl-paste --type "$MIME" > "$IMG_PATH"

if [[ ! -s "$IMG_PATH" ]]; then
  rm -f "$IMG_PATH"
  notify-send -u normal -t 3000 "Paste Image" "Failed to save image"
  exit 1
fi

# Put markdown reference into clipboard, then simulate paste
MD_REF="![](./assets/${IMG_NAME})"
echo -n "$MD_REF" | wl-copy

# Wait for modifier keys to be released
sleep 0.5
wtype -M ctrl -k v -m ctrl

notify-send -u low -t 2000 "Paste Image" "Saved: assets/${IMG_NAME}"

Make it executable:

bash
chmod +x ~/.local/bin/md-paste-image

2. Add the Hyprland keybinding

Add to ~/.config/hypr/bindings.conf:

# Paste clipboard image into Markdown (saves to assets/ and types reference)
bindd = SHIFT ALT, V, Paste image to Markdown, exec, /home/<user>/.local/bin/md-paste-image

Full path required

Hyprland's exec does not include ~/.local/bin in its PATH. You must use the full absolute path to the script.

Hyprland auto-reloads on config save -- no restart needed.

Usage

  1. Copy an image to clipboard (screenshot, browser image, etc.)
  2. Open a Markdown file in Zed and place your cursor where you want the image
  3. Press Shift + Alt + V

The script will:

  • Create an assets/ directory next to the Markdown file (if it doesn't exist)
  • Save the image as assets/YYYY-MM-DD_HHMMSS.png
  • Insert ![](./assets/YYYY-MM-DD_HHMMSS.png) at the cursor position
  • Show a desktop notification confirming the save

How it works

Shift+Alt+V pressed
  -> Hyprland exec runs md-paste-image
    -> wl-paste --list-types checks for image in clipboard
    -> hyprctl activewindow -j gets Zed window title
    -> Title parsed: "project-name — filename.md"
    -> find locates the project directory and file
    -> mkdir -p assets/ next to the markdown file
    -> wl-paste --type image/png saves the image
    -> wl-copy puts the markdown reference in clipboard
    -> sleep 0.5 (wait for Shift+Alt to release)
    -> wtype simulates Ctrl+V to paste the reference
    -> notify-send shows confirmation

Supported image formats

The script detects the clipboard MIME type and saves with the correct extension:

MIME TypeExtension
image/png.png
image/jpeg.jpg
image/webp.webp
image/gif.gif
image/bmp.bmp

Project directory search paths

The script searches for the project directory (from the Zed window title) under these locations in order:

  1. ~/Development
  2. ~/Projects
  3. ~/Code
  4. ~/Documents
  5. ~/ (home directory)

If your projects live elsewhere, add the path to the for base in ... loop in the script.

Troubleshooting

ProblemCauseFix
No notification at allKeybinding not firingUse full path in binding, check hyprctl binds
"No image in clipboard"Clipboard has text, not an imageCopy an actual image first
"Not in Zed editor"Focus was on another windowMake sure Zed is focused when pressing the shortcut
"Could not find project"Project dir not under search pathsAdd your path to the script's search loop
"Could not find file"Duplicate filenames resolved wrongThe script picks the shortest path; rename if needed
Image saved but no text pastedwtype failed or keys still heldIncrease the sleep value (try 0.8 or 1.0)

Limitations

  • Relies on Zed's window title format (project — file.md) to locate the file. If Zed changes this format, the script will need updating.
  • When multiple files share the same name, the script picks the one with the shortest path. This is usually correct but not guaranteed.
  • The sleep 0.5 delay before wtype is needed to wait for modifier key release. If paste doesn't work, increase this value.

References

Released under the MIT License.