Drag and drop
Moving panes by dragging their header, and reordering tabs and spaces, within what herdr allows
What herdr allows
Ovvi offers only moves that herdr can make, and builds the rest from herdr calls (D69). What herdr 0.9.5 allows:
| herdr call | Allowed | Refused |
|---|---|---|
pane.swap {source_pane_id, target_pane_id} (or {pane_id, direction}) | two panes of the same tab | panes of different tabs (changed:false, reason:"cross_tab"); a pane with itself (same_pane) |
pane.move {destination:{type:"tab", tab_id, split, target_pane_id?, ratio?}} | into another tab of any space of the session, split right or down beside target_pane_id (or, when it is left out, the tab's focused pane), with an optional ratio (the first side's share) | into its own tab (reason:"same_tab"); into or out of a tab a herdr TUI zoomed (zoomed_tab, Zoom) |
pane.move {destination:{type:"new_tab", workspace_id?, label?}} | a new tab in any space of the session, last there | — |
pane.move {destination:{type:"new_workspace", label?, tab_label?}} | a new space | — |
tab.move {tab_id, insert_index} | reorder inside its space | — (another space's workspace_id is ignored: the tab moves inside its own) |
workspace.move {workspace_id, insert_index}, workspace.move_block | reorder spaces | — |
- A pane moved to another space gets the next free pane id there
(
w1:p4→w2:p2). Its terminal id stays, so its card stays. Inside its space a pane keeps its id. new_workspacegets no label from the app, so herdr names the space after the pane's folder.- Moving the last pane of a tab closes that tab, and of a space's last tab that space.
layout.apply {tab_id, root}with the tab's own pane ids does not rearrange them: it makes a new tab with new terminals, so the programs in them are gone. A tab moves pane by pane instead (below).- Nothing moves between sessions or machines: each session is its own herdr server.
- herdr splits only right or down. A drop on a left or top edge is a move (or a split) and then a swap.
Panes
Drag a card by its header (the title area, not its buttons) by 4 pt or more.
- Preview. The drag image is a lifted card of one fixed shape, 240 × 150 pt: the card's top (header and terminal) scaled to cover it, its 10 pt corners rounded, at 0.85 with a soft shadow, the pointer over the header where it grabbed it. Where the terminal's Metal layer does not draw into the snapshot, the card's fill shows. The card in place dims to 0.4.
- Indicator. A rounded rect (radius 10) filled with the accent color at 0.2, with a 2 pt accent stroke. It moves between targets over 0.12 s.
| Drop on | Indicator | Result | herdr calls |
|---|---|---|---|
| the center of another card in the same tab (all but the edge bands) | the whole card | swap the two panes | pane.swap |
| an edge band of another card in the same tab: the outer quarter on each side is that edge | that half of the card | put the pane beside that card | pane.move to a new tab, then pane.move back beside the target (right or down), then pane.swap for left or top |
| the pane's own tab capsule | none | not a target (herdr refuses same_tab) | — |
| another tab's capsule in the title row | the capsule highlighted | move into that tab, split right of its last card | pane.move {type:"tab", tab_id, split:"right", target_pane_id: the last card} |
| an edge band of a card in another tab (after spring-loading) | that half of the card | split beside that card | pane.move {type:"tab", tab_id, split, target_pane_id}, then pane.swap for left or top |
the empty end of the tab track, or + | a ghost capsule at the end (and the + highlighted) | a new tab in the selected space | pane.move {type:"new_tab", workspace_id} |
| a space row in the sidebar (its agent rows are part of it) | the row highlighted | a new tab in that space | pane.move {type:"new_tab", workspace_id} |
empty area at the bottom of the sidebar page, or the page header's + (a full page has no empty area) | a ghost row "New Space", or the + highlighted | a new space | pane.move {type:"new_workspace"} |
| Archived Sessions, the page dots | none; not a target | — | — |
- Spring-loading. Hold over a tab capsule for 0.65 s: that tab's page shows and the capsule pulses once (to 1.04 over 0.2 s). Then drop on a card there. Hold over a space row for 0.65 s: the space is selected, so its tabs show and the drop can go on a capsule or a card. The pane's own tab's capsule does not spring-load.
- Equal splits. A pane moved into a tab (an edge or a capsule) then gets an equal share of the run of same-direction splits that holds it, as after a split.
- A step fails. If a later step of a drop fails, the pane stays where it got to, and the notice says so ("Moved the pane to a new tab: herdr refused the next step").
- Every
pane.movepassesfocus:false. Each step uses the pane id from herdr's reply to the step before; the app reads the snapshot after the drop. If herdr refuses or changes nothing, the notice says why ("The tab is zoomed in herdr" when a herdr TUI zoomed the tab, else herdr's reason). - After the drop, the page shows the pane's tab and the pane has the keyboard.
- Esc cancels a drag. A failed write leaves the layout as herdr has it.
- The card's context menu has the same moves: Move Pane to Tab ▸ (New Tab, New Space, then every other tab of the session under its space's name). Its New Tab puts the tab right after the pane's tab, not last (New tabs).
Files and images
Drop files from Finder on a terminal: their paths are pasted into that pane,
each with a backslash before every space and shell character, joined by
spaces, in one bracketed paste. That is what Ghostty, herdr's usual terminal,
sends herdr for a drop. An image with no file (from a browser or a screenshot
thumbnail) is saved as a PNG in a new folder in the user's temporary folder
first, or, when the source only promises a file (as browsers may), received
there; then that path is pasted. A file in a TemporaryItems folder (a
screenshot's floating thumbnail drops one) is copied into such a new folder
first, with its name, and the copy's path is pasted: macOS lets the app that
drops it read it, but not the programs in the pane. Text and links dropped on
a terminal are pasted as they are, also in one bracketed paste. A pane drag
over a terminal still shows its drop zones.
A browser or plugin card's page takes no part in the app's own drags: a pane dragged over it shows the card's drop zones, as over a terminal. Files, text and links dragged from other apps or other pages go to the page, as in a browser.
On a remote machine's card, each file is copied to that machine first, and
the paths of the copies are pasted, escaped the same way, once all are
copied. herdr's own remote client does the same with a dropped image. Each
copy goes over the machine's one SSH connection (its relay) as base64 lines,
into a new folder of its own in ovvi-drops-‹user id› in the machine's
temporary folder ($TMPDIR, else /tmp), so it keeps its name. Only the
user can read that folder, and each copy deletes the folders in it that are
more than a day old. A folder, or a file over 16 MB (herdr's limit too), is not copied: a notice
says why, and nothing is pasted. A pane that runs ssh on This Mac still
gets This Mac's paths, as in any terminal.
Tabs and spaces
| Drag | Result | herdr call |
|---|---|---|
| a tab capsule along the tab track | reorder the space's tabs; the others slide apart over 0.22 s; held near either end of a bar that scrolls, the bar scrolls on (a dragged pane too) | tab.move |
| a tab capsule onto another space's row (its own row is not a target), or onto a space menu row (below) | the row highlighted; no spring-loading (that space's tab row would not take the tab); the tab moves there whole, last among its tabs: its terminals, split tree, ratios and label (herdr's number gives way to the new one). The page follows it, and its keyboard card keeps the keyboard | pane.move {type:"new_tab", workspace_id, label} for its first pane, then for each other pane, top down, pane.move {type:"tab", tab_id, split, target_pane_id, ratio} beside the first pane of the split's other side, with herdr's new pane ids |
a tab capsule onto the sidebar's empty area under its rows, the page header's +, or the space menu's New Space row | a ghost row "New Space", or the + or the menu row highlighted; the tab moves whole to a new space (herdr names it after the first pane's folder, as for a pane), as above | pane.move {type:"new_workspace", tab_label} for its first pane, then the others as above |
| a space row up or down its page | reorder the session's spaces; the rows slide apart over 0.22 s; at the drop the row moves from the drag image's place to its own and fades from 0.4 to full, as the other rows move for the new order, in one 0.2 s ease-out (a tab capsule lands the same way, over 0.22 s); the new order stays until herdr's matches; a repository's linked worktrees move with it; a worktree moves only among its repository's worktrees | workspace.move_block {workspace_ids, before_workspace_id}, as Move Up and Move Down |
- A dragged capsule or row dims to 0.4 in place once the system drag image (the capsule or the row, cut to its round shape) starts to follow the pointer. A dragged tab's place is the one under its drag image, not under the pointer, and the selected tab's pill moves with its capsule.
- In the frame of the drop, the drag image goes and the capsule shows at full opacity where that image was, then glides into its place over 0.22 s on the slide's curve (at once with Reduce Motion), and the tab under the pointer shows its ×. The bar keeps the new order until herdr's order matches, so the tabs never slide back first.
- Space menu. While the sidebar is collapsed, a pane or a tab held over the space button for 0.65 s opens the space menu, the button highlighted as a target meanwhile. The rows of the page in view are then targets, highlighted the same way: a pane goes to a new tab in that space, a tab moves there as on a row. On New Space, either goes to a new space. Only rows in view as the drag starts take a drop: on macOS 27 a drop target made after a drag starts never gets the drop. New Space sits under the scrolled list, so it always does. A drop on the button itself does nothing; the menu closes when the drag ends.
- A tab move fails part way. The panes moved so far stay in the new tab, the rest in the old one, and the notice says "Moved part of the tab: herdr refused the next step".
- A space drag also pins and unpins, and in Recent or under the sidebar's filter it reorders less (Sidebar: Sort Spaces and The pin region).
How it works: Writes are herdr calls.