Skip to content

fix(desktop): make Edit menu Cut/Copy work off macOS - #1336

Open
rezaffikri wants to merge 3 commits into
skyhook-io:mainfrom
rezaffikri:fix/desktop-edit-menu-cut-copy-windows
Open

fix(desktop): make Edit menu Cut/Copy work off macOS#1336
rezaffikri wants to merge 3 commits into
skyhook-io:mainfrom
rezaffikri:fix/desktop-edit-menu-cut-copy-windows

Conversation

@rezaffikri

@rezaffikri rezaffikri commented Aug 5, 2026

Copy link
Copy Markdown

Description

On Windows and Linux, Edit → Cut and Edit → Copy do nothing when clicked.

Both items are registered with a nil callback:

editMenu.AddText("Cut", keys.CmdOrCtrl("x"), nil)
editMenu.AddText("Copy", keys.CmdOrCtrl("c"), nil)

That is correct on macOS, where nil delegates to the native responder chain. Off macOS there is nothing to delegate to — Wails only binds a handler when Click != nil (windows/menu.go), and firing an unbound event is a no-op (winc/eventmanager.go), so the entries are wired to nothing.

Ctrl+C / Ctrl+X were never broken: web/src/main.tsx has its own keydown listener that checks e.ctrlKey, so the keyboard path already worked. Only the menu items were dead.

Fix: off macOS, give the two items a callback that calls document.execCommand('cut'|'copy'). main.tsx already monkey-patches execCommand and routes those to handleCopyOrCut, so Monaco's virtual selection is handled the same way as the existing right-click path. macOS keeps its nil delegation untouched.

The Ctrl+X/Ctrl+C accelerators are kept, so the menu still shows the hints. Adding a callback does mean the accelerator now fires it as well, so the keyboard path reaches handleCopyOrCut twice. That is safe: JS is single-threaded, so the two runs are sequential rather than interleaved, and each one re-reads the selection. Copy writes the same text twice. The first Cut collapses the selection, so the second returns at if (!text) return before deleting anything — there is no state in which a second delete has something to remove.

This is why the accelerator is kept here but dropped for Paste. Paste doesn't read the document, it inserts the clipboard, so a second run always has something to do and the duplicate is visible.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

How has this been tested?

Environment: Windows 11 (26200), Go 1.26.1, Node 24.13.1, Wails v2.12.0.

  • Tested locally with minikube/kind

  • Tested against a remote cluster

  • Added/updated unit tests

  • Added TestClipboardDelegateSkipsOnlyMac and TestCreateMenuCutCopyAreClickableOffMac. The wiring test fails against the unfixed code with Cut has no callback on windows — the menu entry would be inert (and the same for Copy), and passes with the fix. It skips on darwin, where nil is correct.

  • go build ./... — clean.

  • go test ./cmd/desktop/ — pass.

  • go test ./...internal/server and internal/timeline fail identically on unmodified main, so they are pre-existing and unrelated.

  • cd web && npm run tsc — pass (no frontend files touched).

  • gofmt / go vet ./cmd/desktop/ — clean.

  • Manual check on a wails build -platform windows/amd64 binary from this branch: Edit → Copy and Edit → Cut now work (they did nothing before), and Ctrl+C / Ctrl+X still behave correctly.

Not tested on macOS or Linux — I don't have access to either. The change is scoped to non-darwin, so macOS behavior is unchanged by construction.

Checklist

  • My code follows the project's coding standards
  • I have performed a self-review of my code
  • I have added comments where necessary
  • My changes generate no new warnings
  • Any dependent changes have been merged

Related issues

Fixes #1334

Stacked on #1335 (the Paste double-insert fix) — both touch the Edit menu clipboard items, so this branch includes that commit. Please merge that one first, or say the word and I'll rebase this onto main as a standalone change.


Note

Low Risk
Desktop menu wiring only; behavior is scoped by OS with tests and no auth or data-path changes.

Overview
Edit → Cut/Copy on Windows and Linux now run document.execCommand('cut'|'copy') via a new clipboardDelegate helper; macOS still uses nil callbacks for the native responder chain.

The same change set wires Paste through pasteAccelerator, which omits the Ctrl+V menu accelerator on Windows only so Wails does not paste twice (menu callback + webview). Comments in menu.go document the platform split.

Unit tests lock in pasteAccelerator, clipboardDelegate, and non-mac Cut/Copy menu callbacks.

Reviewed by Cursor Bugbot for commit 17da660. Bugbot is set up for automated code reviews on this repo. Configure here.

winc fires a menu accelerator from WM_KEYDOWN without consuming the key, so
Paste's explicit callback ran on top of the webview's own native paste and the
clipboard landed twice. macOS still needs the accelerator, where it does
consume the event and is the only path that reaches Monaco.

Fixes skyhook-io#1276

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1c62dc5. Configure here.

Comment thread cmd/desktop/menu.go
editMenu.AddText("Copy", keys.CmdOrCtrl("c"), nil)
editMenu.AddText("Paste", keys.CmdOrCtrl("v"), func(_ *menu.CallbackData) {
editMenu.AddText("Cut", keys.CmdOrCtrl("x"), clipboardDelegate(goruntime.GOOS, desktopApp, "cut"))
editMenu.AddText("Copy", keys.CmdOrCtrl("c"), clipboardDelegate(goruntime.GOOS, desktopApp, "copy"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cut/Copy steal Linux Ctrl+C

High Severity

Giving Cut/Copy non-nil callbacks while keeping the Ctrl+C/Ctrl+X accelerators newly binds those chords in the native menu. On Linux, GTK accelerators consume the key before it reaches the webview, so Ctrl+C in a pod or local terminal no longer delivers SIGINT. The same hijack risk already drove the Reload accelerator change, and terminalClipboard intentionally leaves Ctrl+C alone for that reason.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1c62dc5. Configure here.

@rezaffikri rezaffikri Aug 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this one is right. The chord is not newly bound by this change.

In wails/v2@v2.12.0/internal/frontend/desktop/linux/menu.go, processMenuItem sets the callback and the accelerator in two separate blocks:

if menuItem.Click != nil {
	handler := C.connectClick(result)
	...
}

if menuItem.Accelerator != nil {
	key, mods := acceleratorToGTK(menuItem.Accelerator)
	C.addAccelerator(result, group, key, mods)
}

Cut/Copy already passed keys.CmdOrCtrl("x") and ("c") on main, so the GTK accel entries were already there before this PR. This change only adds the Click handler. It does not touch Accelerator.

The real question is whether connecting an "activate" handler changes key consumption. That would make your point work. It does not. In GTK 3.24 the closure behind gtk_widget_add_accelerator is:

static void
closure_accel_activate (GClosure *closure, GValue *return_value, ...)
{
  gboolean can_activate = gtk_widget_can_activate_accel (closure->data, aclosure->signal_id);
  if (can_activate)
    g_signal_emit (closure->data, aclosure->signal_id, 0);
  g_value_set_boolean (return_value, can_activate);
}

The "handled" result comes from gtk_widget_can_activate_accel, so sensitivity and visibility, never from whether a handler is connected. Emitting "activate" with no handler still counts as handled, and there is no fall through. gtk_widget_add_accelerator also rejects signals that have a return type. On top of that, gtkwindow.c runs gtk_window_activate_key before gtk_window_propagate_key_event, so an accel entry beats the focused webview anyway.

So on Linux main, Ctrl+C and Ctrl+X were already swallowed by the menubar and never reached the webview. They just did nothing, because no handler was connected. After this change the same keypress actually copies or cuts. That is a fix, not a new hijack.

About the terminalClipboard reference: the "Ctrl+C stays SIGINT" comment sits inside the macOS only branch. That attachCustomKeyEventHandler is gated on mac and matches metaKey && !ctrlKey. It is about JS on macOS and says nothing about Linux.

Comment thread cmd/desktop/menu.go
@roylibman

Copy link
Copy Markdown
Contributor

Thanks, and thank you for filing #1334 with the diagnosis rather than just the symptom. Same reviewer, queued with @hisco. I will let CI finish before we look.

rezaffikri added 2 commits August 5, 2026 11:43
The double insert is specific to winc on Windows, which fires the menu action
from WM_KEYDOWN and still forwards the key to the webview. AppKit and GTK
consume the accelerator, so there it is the only paste path — dropping it would
hand Monaco to native webview behavior that hasn't been verified on either.
A nil menu callback only delegates to a responder chain on macOS; on Windows and
Linux Wails binds no handler at all, so clicking Edit -> Cut or Copy did nothing.
Route those two through execCommand, which main.tsx already intercepts for
Monaco's virtual selection.
@rezaffikri
rezaffikri force-pushed the fix/desktop-edit-menu-cut-copy-windows branch from 1c62dc5 to 17da660 Compare August 5, 2026 04:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Edit → Cut and Edit → Copy do nothing on Windows

2 participants