Skip to content

GTK: Drag-and-drop from Nautilus fails because DropTarget only accepts copy, not move #11175

Closed

Description

@douglas

Summary

Dragging files from Nautilus (GNOME Files) into Ghostty on Wayland/Hyprland does nothing — the file path is never pasted into the terminal. The root cause is that Ghostty's DropTarget only accepts copy actions, but Nautilus offers move as its default drag action. GTK's DnD negotiation rejects the drop before it ever reaches Ghostty's dtDrop handler.

Environment

  • Ghostty: tip (0.0.0-20260304, commit 619e33a) — also likely affects stable 1.2.3 since the Blueprint has not changed
  • GTK: 4.20.3
  • libadwaita: 1.8.4
  • Compositor: Hyprland 0.54.0 (Wayland)
  • File manager: Nautilus 49.4
  • OS: Arch Linux

How we diagnosed it

Step 1: Confirmed Wayland DnD works

Created a minimal GTK4 app with a DropTarget (accepting copy | move) on a Label. Dragging files from Nautilus worked — paths were received in the drop signal.

Step 2: Narrowed to Ghostty

  • Kitty (custom Wayland client): drag-and-drop works
  • Ghostty (GTK4): drag-and-drop fails silently (no crash, just nothing happens)
  • Alacritty (winit): also fails (same class of issue — copy-only acceptance)

Step 3: Isolated the DnD action mismatch

Created a GTK4 test app with three side-by-side DropTarget zones, each accepting different actions:

DropTarget actions Result
copy only LEAVE immediately — drop rejected
move only ENTER → DROP (works, with gdk_drop_status warning)
copy | move ENTER → DROP (works cleanly)

This confirmed that Nautilus offers move as the drag action, and a DropTarget that only accepts copy will be rejected by GTK's DnD protocol negotiation. The enter signal never fires, so Ghostty's dtDrop handler is never called.

Step 4: Wayland protocol trace confirms GTK4 is the negotiation bottleneck

Using WAYLAND_DEBUG=1, we captured the exact Wayland protocol messages during the DnD. Action bitmask values: 1 = copy, 2 = move, 3 = copy|move, 7 = copy|move|ask.

What Hyprland sends to the target app (from Nautilus):

source_actions(7)    → Nautilus offers: copy | move | ask
action(2)            → Hyprland preferred action: move

Nautilus's drag source offers all actions (7 = copy|move|ask) via source_actions. However, Hyprland pre-selects move (2) as the compositor's preferred action before GTK even negotiates.

How GTK responds per zone:

COPY|MOVE zone (works):

→ set_actions(3, 1)   target accepts: copy|move, prefers: copy
→ set_actions(3, 2)   target accepts: copy|move, prefers: move
→ accept("text/plain;charset=utf-8")
✓ Negotiation succeeds → DROP

MOVE zone (works):

→ set_actions(2, 2)   target accepts: move, prefers: move
→ accept("text/plain;charset=utf-8")
✓ Negotiation succeeds → DROP

COPY zone (fails):

→ set_actions(0, 0)   target accepts: NOTHING
→ accept(nil)          reject
✗ GTK gives up entirely

The bug: When the DropTarget accepts only copy (1) but the compositor's preferred action is move (2), GTK4 sends set_actions(0, 0) — rejecting the entire drop. It never attempts to negotiate copy with the source, even though source_actions(7) includes copy. The correct behavior would be to respond with set_actions(1, 1) and let the compositor re-negotiate, since the source does support copy.

This is arguably a GTK4 bug in its wl_data_offer negotiation logic, but Ghostty can (and should) work around it by accepting move alongside copy.

Step 5: Found the source

In src/apprt/gtk/ui/1.2/surface.blp line 224:

DropTarget drop_target {
    drop => $drop();
    actions: copy;
}

The actions: copy means Ghostty will only accept drags that offer copy. When Hyprland pre-selects move as the preferred action, GTK4 rejects the drop entirely.

Diagnostic script

The script below reproduces the issue outside of Ghostty. Run it, then drag a file from Nautilus onto each of the three labeled zones:

#\!/usr/bin/env python3
"""Test: COPY vs COPY|MOVE actions."""
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, Gdk, Gio

def make_dt(name, actions):
    dt = Gtk.DropTarget.new(Gdk.FileList, actions)
    dt.set_gtypes([Gdk.FileList.__gtype__, Gio.File.__gtype__, str])
    dt.connect("enter", lambda t,x,y,n=name: (print(f"[{n}] ENTER"), Gdk.DragAction.COPY)[1])
    dt.connect("leave", lambda t,n=name: print(f"[{n}] LEAVE"))
    dt.connect("drop", lambda t,v,x,y,n=name: (print(f"[{n}] DROP: {v}"), True)[1])
    return dt

class TestWindow(Gtk.ApplicationWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_title("DnD action test")
        self.set_default_size(600, 400)

        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        hbox.set_margin_top(20)
        hbox.set_margin_bottom(20)
        hbox.set_margin_start(20)
        hbox.set_margin_end(20)
        self.set_child(hbox)

        # COPY only
        l1 = Gtk.Label(label="COPY only")
        l1.set_hexpand(True)
        l1.set_vexpand(True)
        l1.add_controller(make_dt("COPY", Gdk.DragAction.COPY))
        hbox.append(l1)

        # MOVE only
        l2 = Gtk.Label(label="MOVE only")
        l2.set_hexpand(True)
        l2.set_vexpand(True)
        l2.add_controller(make_dt("MOVE", Gdk.DragAction.MOVE))
        hbox.append(l2)

        # COPY | MOVE
        l3 = Gtk.Label(label="COPY|MOVE")
        l3.set_hexpand(True)
        l3.set_vexpand(True)
        l3.add_controller(make_dt("COPY|MOVE", Gdk.DragAction.COPY | Gdk.DragAction.MOVE))
        hbox.append(l3)

        print("Ready. Drop on each section...")

def on_activate(app):
    win = TestWindow(application=app)
    win.present()

app = Gtk.Application(application_id="test.dnd.actions")
app.connect("activate", on_activate)
app.run(None)

Expected output when dropping a file on each zone:

[COPY] LEAVE                          # rejected — Nautilus offers move, not copy
[MOVE] ENTER → DROP                   # accepted
[COPY|MOVE] ENTER → DROP              # accepted

Suggested fix

Change the DropTarget actions to also accept move:

actions: copy | move;

The existing dtDrop handler in surface.zig already treats all drops the same way (extracts the file path and pastes it), so no Zig code changes should be needed — the handler does not move/delete the source file regardless of the negotiated action.

This would bring Ghostty in line with how Kitty handles file drops (Kitty accepts the drop regardless of the offered action and simply pastes the path).

Activity

  1. ghostty-vouch commented on Mar 5, 2026

    @ghostty-vouch
    Contributor

    Hi @douglas, thanks for your interest!

    This project requires that issue reporters are vouched, and you are not in the list of vouched users.

    This issue will be closed automatically. See https://github.com/ghostty-org/ghostty/blob/main/CONTRIBUTING.md for more details.

  2. jcollie commented on Mar 5, 2026

    @jcollie
    Member

    !vouch

  3. reopened this on Mar 5, 2026
  4. douglas commented on Mar 5, 2026

    @douglas
    Author

    Update: Root cause traced to GTK4's Wayland backend

    After further investigation with WAYLAND_DEBUG=1 protocol traces, the root cause is in GTK4's gdk/wayland/gdkdrop-wayland.c, not in Hyprland or Ghostty.

    The bug in GTK4

    In gdk_wayland_drop_update_actions():

    if (drop->action == 0 ||
        drop->action & WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK)
        wl_actions = drop->source_actions;   // uses full set
    else
        wl_actions = drop->action;           // narrows to compositor preferred only

    Hyprland sends source_actions(7) (copy|move|ask) and action(2) (move) before the enter event. Since action=2 is non-zero and doesn't include ASK, GTK takes the else branch and narrows gdk_drop_get_actions() to just move. Then gtkdroptarget.c:gtk_drop_target_accept() checks move & copy == 0 and rejects the drop.

    Per the Wayland spec, source_actions represents what is theoretically possible and action is a preliminary compositor preference that is not final. GTK should not use the preliminary action to restrict the accept check.

    Why this likely works on GNOME/Mutter

    Mutter probably sends action(0) or includes the ASK bit before enter, causing GTK to take the if branch and use the full source_actions. This masks the bug under GNOME but exposes it under Hyprland.

    Recommendation

    The actions: copy | move fix in the Blueprint is still the right approach for Ghostty — it's the correct behavior regardless of the GTK bug. A terminal accepting a file drop should not care whether it was a "copy" or "move" drag; it just pastes the path.

  5. douglas commented on Mar 5, 2026

    @douglas
    Author

    Confirmed: Mutter vs Hyprland protocol difference

    Ran the same test on Fedora 43 (GNOME/Mutter, GTK 4.20.3). COPY-only DropTarget works on Mutter.

    Wayland protocol trace comparison

    Mutter (works):

    source_actions(7)         ← offers copy|move|ask
                              ← NO action() before enter
    enter(...)                ← GTK sees action=0, uses source_actions (full set)
    → set_actions(0, 0)      ← initial reset
    → set_actions(2, 2)      ← target responds with its preference
    action(2)                 ← Mutter sends action AFTER target responds
    ...
    → set_actions(1, 1)      ← target moves to COPY zone
    action(1)                 ← Mutter re-negotiates to copy
    ✓ DROP succeeds
    

    Hyprland (broken):

    source_actions(7)         ← offers copy|move|ask
    action(2)                 ← Hyprland sends action BEFORE enter
    enter(...)                ← GTK sees action=2, narrows to move-only
    → set_actions(0, 0)      ← COPY zone rejected immediately
    ✗ Drop never happens
    

    Root cause

    Hyprland sends action(2) (move) before the enter event, so GTK sees a non-zero action at enter time and narrows gdk_drop_get_actions() to just move. Mutter doesn't send action() until after the target has called set_actions(), so GTK sees action=0 at enter time and uses the full source_actions set.

    This is a dual issue:

    1. Hyprland sends a premature action() before the target can negotiate
    2. GTK4 shouldn't use a preliminary action to restrict the accept check — it should use source_actions

    Either fix would solve the problem, but the safest workaround for Ghostty is still actions: copy | move in the Blueprint.

  6. douglas commented on Mar 5, 2026

    @douglas
    Author

    Filed the Hyprland protocol bug: hyprwm/Hyprland#13578

    TL;DR: Hyprland's CWLDataOfferResource::sendData() sends a hardcoded action(MOVE) before enter(), and never handles the target's wl_data_offer.set_actions() response. Mutter handles both correctly, which is why this works on Fedora/GNOME.

    The actions: copy | move fix in Ghostty is still the right workaround regardless — a terminal paste-on-drop shouldn't care about the drag action semantics.

  7. added
    gtkIssues exclusive to GTK which are not OS-specific
    upstreamIssue is in an upstream project
    on Mar 5, 2026
  8. mitchellh commented on Mar 5, 2026

    @mitchellh
    Contributor

    If there is a workaround here in Ghostty that would work and is minimal, it sounds fine to me.

  9. added a commit that references this issue on Mar 5, 2026
    c920a88
  10. self-assigned this
    on Mar 5, 2026
  11. added this to the 1.3.0 milestone on Mar 5, 2026
  12. added a commit that references this issue on Mar 5, 2026
    dd575c7
  13. added a commit that references this issue on Mar 10, 2026
  14. added a commit that references this issue on Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

gtkIssues exclusive to GTK which are not OS-specificupstreamIssue is in an upstream project

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions