From f60c0380501f74ae84957138c811071691dfd0b0 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 29 May 2026 13:54:55 +0200 Subject: [PATCH] Open clipboard entries from history --- bin/omarchy-clipboard-open | 69 +++++++++++++++++++++ shell/plugins/clipboard/Clipboard.qml | 17 ++++- shell/plugins/clipboard/ClipboardHistory.js | 2 +- test/shell.d/clipboard-test.sh | 46 +++++++++++++- 4 files changed, 128 insertions(+), 6 deletions(-) create mode 100755 bin/omarchy-clipboard-open diff --git a/bin/omarchy-clipboard-open b/bin/omarchy-clipboard-open new file mode 100755 index 00000000..04ea3d77 --- /dev/null +++ b/bin/omarchy-clipboard-open @@ -0,0 +1,69 @@ +#!/bin/bash + +# omarchy:summary=Open a clipboard history entry +# omarchy:group=clipboard +# omarchy:args=--history-index + +history_index="" +history_path="$HOME/.local/state/omarchy/clipboard-history.json" + +while [[ $# -gt 0 ]]; do + case "$1" in + --history-index) + history_index="${2:-}" + shift 2 + ;; + *) + echo "Usage: omarchy-clipboard-open --history-index " >&2 + exit 1 + ;; + esac +done + +[[ $history_index =~ ^[0-9]+$ ]] || exit 1 +[[ -r $history_path ]] || exit 1 + +entry_type=$(jq -er --argjson index "$history_index" '.[$index].type' "$history_path") || exit 1 + +open_image() { + local path="$1" + + [[ -r $path ]] || exit 1 + exec satty --filename "$path" --output-filename "$path" +} + +open_text() { + local text="$1" + local url="" + local open_dir="" + local open_file="" + + url=$(grep -Eom1 'https?://[^[:space:]"'\''<>]+' <<<"$text" || true) + if [[ -z $url && $text =~ ^[[:space:]]*([[:alnum:]][[:alnum:].-]+\.[[:alpha:]]{2,})(/[^[:space:]]*)?[[:space:]]*$ ]]; then + url="https://${BASH_REMATCH[1]}${BASH_REMATCH[2]}" + fi + + if [[ -n $url ]]; then + exec omarchy-launch-browser "$url" + fi + + open_dir="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/clipboard-open" + mkdir -p "$open_dir" + open_file=$(mktemp --tmpdir="$open_dir" clipboard.XXXXXX.txt) || exit 1 + printf '%s' "$text" >"$open_file" + exec omarchy-launch-editor "$open_file" +} + +case "$entry_type" in + image) + path=$(jq -er --argjson index "$history_index" '.[$index].path' "$history_path") || exit 1 + open_image "$path" + ;; + text) + text=$(jq -er --argjson index "$history_index" '.[$index].text' "$history_path") || exit 1 + open_text "$text" + ;; + *) + exit 1 + ;; +esac diff --git a/shell/plugins/clipboard/Clipboard.qml b/shell/plugins/clipboard/Clipboard.qml index a472249d..2419b8f6 100644 --- a/shell/plugins/clipboard/Clipboard.qml +++ b/shell/plugins/clipboard/Clipboard.qml @@ -19,7 +19,7 @@ Item { property string historyPath: Quickshell.env("HOME") + "/.local/state/omarchy/clipboard-history.json" property string captureScript: root.omarchyPath + "/shell/plugins/clipboard/capture.sh" - property string watchCommand: "script=$1\ntrap 'kill $(jobs -p) 2>/dev/null' EXIT\nwl-paste --watch \"$script\" &\nOMARCHY_CLIPBOARD_WATCH_MIME=image/png wl-paste --type image/png --watch \"$script\" 2>/dev/null &\nOMARCHY_CLIPBOARD_WATCH_MIME=image/jpeg wl-paste --type image/jpeg --watch \"$script\" 2>/dev/null &\nOMARCHY_CLIPBOARD_WATCH_MIME=image/webp wl-paste --type image/webp --watch \"$script\" 2>/dev/null &\nOMARCHY_CLIPBOARD_WATCH_MIME=image/gif wl-paste --type image/gif --watch \"$script\" 2>/dev/null &\nOMARCHY_CLIPBOARD_WATCH_MIME=image/bmp wl-paste --type image/bmp --watch \"$script\" 2>/dev/null &\nOMARCHY_CLIPBOARD_WATCH_MIME=image/tiff wl-paste --type image/tiff --watch \"$script\" 2>/dev/null &\nwait" + property string watchCommand: "script=$1\ntrap 'kill $(jobs -p) 2>/dev/null' EXIT\nwl-paste --watch \"$script\" &\nOMARCHY_CLIPBOARD_WATCH_MIME=image/png wl-paste --type image/png --watch \"$script\" 2>/dev/null &\nwait" // Shares the [menu] surface tokens — themes that style the menu also // style the clipboard. Selected-row colors composed in the // singleton so consumers drop them straight into Rectangle bindings. @@ -182,6 +182,12 @@ Item { root.copySelected(row) } + function openIndex(index) { + if (index < 0 || index >= displayModel.count) return + var row = displayModel.get(index) + root.openSelected(row) + } + function applySelected(row) { if (!row) return root.opened = false @@ -202,6 +208,12 @@ Item { } } + function openSelected(row) { + if (!row) return + root.opened = false + Quickshell.execDetached([root.omarchyPath + "/bin/omarchy-clipboard-open", "--history-index", String(row.historyIndex)]) + } + Component.onCompleted: initProc.running = true ListModel { id: displayModel } @@ -312,7 +324,8 @@ Item { root.select(6) event.accepted = true } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - if (root.cursorActive && (event.modifiers & Qt.ShiftModifier)) root.copyIndex(root.selectedIndex) + if (root.cursorActive && (event.modifiers & Qt.AltModifier)) root.openIndex(root.selectedIndex) + else if (root.cursorActive && (event.modifiers & Qt.ShiftModifier)) root.copyIndex(root.selectedIndex) else if (root.cursorActive) root.activateIndex(root.selectedIndex) else if (displayModel.count > 0) root.cursorActive = true event.accepted = true diff --git a/shell/plugins/clipboard/ClipboardHistory.js b/shell/plugins/clipboard/ClipboardHistory.js index 5a1865e2..313704b1 100644 --- a/shell/plugins/clipboard/ClipboardHistory.js +++ b/shell/plugins/clipboard/ClipboardHistory.js @@ -100,7 +100,7 @@ function imagePreviewText(entry) { if (!timestamp) return "Image" var label = String(entry && entry.mime || "") === "image/png" ? "Screenshot" : "Image" - return label + " " + timestamp + return label + " from " + timestamp } function previewText(entry) { diff --git a/test/shell.d/clipboard-test.sh b/test/shell.d/clipboard-test.sh index 9bbe4cd5..0d1008d1 100644 --- a/test/shell.d/clipboard-test.sh +++ b/test/shell.d/clipboard-test.sh @@ -73,13 +73,13 @@ assertDeepEqual( assertDeepEqual( clipboard.displayRows([{ type: 'image', path: '/tmp/a.png', mime: 'image/png', capturedAt: 'Friday 14:42' }], '', 50)[0].previewText, - 'Screenshot Friday 14:42', + 'Screenshot from Friday 14:42', 'clipboard labels timestamped png image entries as screenshots' ) assertDeepEqual( clipboard.displayRows([{ type: 'image', path: '/tmp/a.jpg', mime: 'image/jpeg', capturedAt: 'Friday 14:42' }], '', 50)[0].previewText, - 'Image Friday 14:42', + 'Image from Friday 14:42', 'clipboard labels timestamped non-png image entries as images' ) @@ -114,7 +114,23 @@ cat >"$TMPDIR/bin/wtype" <<'SH' printf '%s\n' "$*" >"$WTYPE_OUT" SH -chmod +x "$TMPDIR/bin/wl-copy" "$TMPDIR/bin/wtype" +cat >"$TMPDIR/bin/omarchy-launch-browser" <<'SH' +#!/bin/bash +printf '%s\n' "$*" >"$BROWSER_OUT" +SH + +cat >"$TMPDIR/bin/omarchy-launch-editor" <<'SH' +#!/bin/bash +printf '%s\n' "$1" >"$EDITOR_PATH_OUT" +cat "$1" >"$EDITOR_TEXT_OUT" +SH + +cat >"$TMPDIR/bin/satty" <<'SH' +#!/bin/bash +printf '%s\n' "$*" >"$SATTY_OUT" +SH + +chmod +x "$TMPDIR/bin/wl-copy" "$TMPDIR/bin/wtype" "$TMPDIR/bin/omarchy-launch-browser" "$TMPDIR/bin/omarchy-launch-editor" "$TMPDIR/bin/satty" jq -n --arg text "$(printf 'large block line 1\nlarge block line 2\n')" '[{type:"text", text:"ignored"}, {type:"text", text:$text}]' >"$TMPDIR/home/.local/state/omarchy/clipboard-history.json" @@ -147,3 +163,27 @@ pass "clipboard file paste helper copy-only copies file content" [[ ! -e "$TMPDIR/wtype" ]] || fail "clipboard file paste helper copy-only skips paste keystroke" pass "clipboard file paste helper copy-only skips paste keystroke" + +jq -n --arg url 'https://example.com/docs' --arg text "$(printf 'plain text\nsecond line')" --arg image "$TMPDIR/image.png" \ + '[{type:"text", text:$url}, {type:"text", text:$text}, {type:"image", mime:"image/png", path:$image}]' >"$TMPDIR/home/.local/state/omarchy/clipboard-history.json" + +BROWSER_OUT="$TMPDIR/browser" HOME="$TMPDIR/home" PATH="$TMPDIR/bin:$PATH" \ + "$ROOT/bin/omarchy-clipboard-open" --history-index 0 + +[[ $(<"$TMPDIR/browser") == "https://example.com/docs" ]] || fail "clipboard open helper opens URL entries in browser" +pass "clipboard open helper opens URL entries in browser" + +EDITOR_PATH_OUT="$TMPDIR/editor-path" EDITOR_TEXT_OUT="$TMPDIR/editor-text" HOME="$TMPDIR/home" XDG_STATE_HOME="$TMPDIR/state" PATH="$TMPDIR/bin:$PATH" \ + "$ROOT/bin/omarchy-clipboard-open" --history-index 1 + +[[ $(<"$TMPDIR/editor-text") == "$(printf 'plain text\nsecond line')" ]] || fail "clipboard open helper opens text entries in editor" +pass "clipboard open helper opens text entries in editor" + +[[ $(<"$TMPDIR/editor-path") == "$TMPDIR"/state/omarchy/clipboard-open/clipboard.*.txt ]] || fail "clipboard open helper writes text entries to a temporary file" +pass "clipboard open helper writes text entries to a temporary file" + +SATTY_OUT="$TMPDIR/satty" HOME="$TMPDIR/home" PATH="$TMPDIR/bin:$PATH" \ + "$ROOT/bin/omarchy-clipboard-open" --history-index 2 + +[[ $(<"$TMPDIR/satty") == "--filename $TMPDIR/image.png --output-filename $TMPDIR/image.png" ]] || fail "clipboard open helper opens image entries in satty" +pass "clipboard open helper opens image entries in satty"