Skip to content
NSPC911 edited this page Jun 29, 2026 · 12 revisions

DnD: kitty drag-and-drop

textual_drivers.dnd provides DNDApp, a DrivenApp subclass that implements the full kitty drag-and-drop protocol for both directions:

  • Drag-in: files dragged FROM the desktop INTO the terminal
  • Drag-out: files dragged FROM the terminal TO the desktop

Import

from textual_drivers.dnd import DNDApp, Drop, DropData, DragOutFinished

Messages

Drag-in

class Drop:
    pos: Offset
    # position of the drop operation in cells, namedtuple with x and y attributes
    op: Literal["copy", "move"]
    # operation type
    mimes: list[str]
    # list of MIME types of drop
class DropData:
    drop_event: Drop
    # the original Drop event that triggered this data arrival
    data: list[str] | btes
    # list[str] if it is a text/uri-list, bytes otherwise
    mime: str
    # mime type of this data chunk
class DragOutFinished:
    cancelled: bool
    # True if the drag was cancelled, False if it completed successfully

Internal (do not handle directly)

DNDDragIn, DNDDragOut and DNDDropData are used internally by DNDApp and should not be handled in subclasses.

Reactive attributes

Attribute Type Description
is_dragging_out bool True while a drag-out is in progress (between the gesture and DragOutFinished).
is_dragging_in bool True while an accepted drag-in is hovering over the window.
is_drag_in_rej bool True while a drag-in is hovering but was rejected by dnd_drag_in_operation.

All three are Textual vars, so subclasses can watch them:

def watch_is_dragging_out(self, active: bool) -> None:
    self.query_one("#status", Label).update("Dragging…" if active else "Idle")

They can also be styled with TCSS via their toggle classes:

DNDApp.drag-in-active {
  background: green;
}
DNDApp.drag-in-rejected {
  background: red;
}
DNDApp.drag-out-active {
  background: blue;
}

Override methods

class DNDApp(DrivenApp):
    async def dnd_drag_out_operation(
        self, pos: Offset
    ) -> DNDDragOutOperation | None:
        """Return DNDDragOutOperation to start a drag-out, or None to cancel."""
        ...

    async def dnd_drag_in_operation(self, event: DNDDragIn) -> DNDDragInOperation | bool:
        """Return DNDDragInOperation to customize the drag-in, or bool for simple accept/reject."""
        ...

Requesting data

When you receive the Drop event (from on_drop, or @on(Drop)), the actual data is not yet available. You must request it. DropData is posted once all chunks have arrived and been assembled. For text/uri-list, comment lines and blank lines are stripped and each URI is an element of data. Assembly (base64 decode) runs in a background thread, so large binary MIME types like image/png do not block the UI.

If no data arrives within 30 seconds, DropData is posted with data=b"" as a timeout sentinel — check for this before processing.

Single MIME (auto-close)

If you only need one data, just call it directly in on_drop and the session will close automatically once the data arrives:

async def on_drop(self, event: Drop) -> None:
    idx = event.mimes.index("text/uri-list")
    self.request_data(event, idx)

Multiple MIMEs (explicit close)

If you need multiple data formats, you must include close=False in request_data to keep the session open across multiple requests, and call close_dnd() when you're truly done:

@work
async def on_drop(self, event: Drop) -> None:
    self._requested: list[str] = []
    self.request_data(event, 0, close=False)   # fetch first MIME, leave session open

@work
async def on_drop_data(self, event: DropData) -> None:
    self._requested.append(event.mime)
    remaining = [m for m in event.drop_event.mimes if m not in self._requested]
    if not remaining:
        self.close_dnd()
        return
    # optionally ask the user which to fetch next, then:
    idx = event.drop_event.mimes.index(remaining[0])
    self.request_data(event.drop_event, idx, close=False)

Running the bundled demos

# test drag in
uv run python -m textual_drivers.demo.drag_in

# test drag out
uv run python -m textual_drivers.demo.drag_out

Clone this wiki locally