Repository navigation
Description
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
ghostty-vouch commentedon Mar 5, 2026
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.
jcollie commentedon Mar 5, 2026
!vouch
Update VOUCHED list
Update VOUCHED list (#11176)
douglas commentedon Mar 5, 2026
Update: Root cause traced to GTK4's Wayland backend
After further investigation with
WAYLAND_DEBUG=1protocol traces, the root cause is in GTK4'sgdk/wayland/gdkdrop-wayland.c, not in Hyprland or Ghostty.The bug in GTK4
In
gdk_wayland_drop_update_actions():Hyprland sends
source_actions(7)(copy|move|ask) andaction(2)(move) before the enter event. Sinceaction=2is non-zero and doesn't include ASK, GTK takes theelsebranch and narrowsgdk_drop_get_actions()to justmove. Thengtkdroptarget.c:gtk_drop_target_accept()checksmove & copy == 0and rejects the drop.Per the Wayland spec,
source_actionsrepresents what is theoretically possible andactionis a preliminary compositor preference that is not final. GTK should not use the preliminaryactionto 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 theifbranch and use the fullsource_actions. This masks the bug under GNOME but exposes it under Hyprland.Recommendation
The
actions: copy | movefix 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.douglas commentedon Mar 5, 2026
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):
Hyprland (broken):
Root cause
Hyprland sends
action(2)(move) before theenterevent, so GTK sees a non-zeroactionat enter time and narrowsgdk_drop_get_actions()to justmove. Mutter doesn't sendaction()until after the target has calledset_actions(), so GTK seesaction=0at enter time and uses the fullsource_actionsset.This is a dual issue:
action()before the target can negotiateactionto restrict the accept check — it should usesource_actionsEither fix would solve the problem, but the safest workaround for Ghostty is still
actions: copy | movein the Blueprint.douglas commentedon Mar 5, 2026
Filed the Hyprland protocol bug: hyprwm/Hyprland#13578
TL;DR: Hyprland's
CWLDataOfferResource::sendData()sends a hardcodedaction(MOVE)beforeenter(), and never handles the target'swl_data_offer.set_actions()response. Mutter handles both correctly, which is why this works on Fedora/GNOME.The
actions: copy | movefix in Ghostty is still the right workaround regardless — a terminal paste-on-drop shouldn't care about the drag action semantics.mitchellh commentedon Mar 5, 2026
If there is a workaround here in Ghostty that would work and is minimal, it sounds fine to me.
GTK: add 'move' to the drop target actions
GTK: add 'move' to the drop target actions (#11182)
Update VOUCHED list (ghostty-org#11176)
GTK: add 'move' to the drop target actions
Update VOUCHED list (ghostty-org#11176)
GTK: add 'move' to the drop target actions
Merge upstream-main into main (#34)
Update VOUCHED list (#11176)