aboutsummaryrefslogtreecommitdiff
path: root/docs/design/2026-07-02-file-manager-swallow-spec.org
blob: 4c61be1f26421f212ba8b28d38dc826423c3d17d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#+TITLE: File-Manager Swallow Pattern
#+AUTHOR: Craig Jennings
#+DATE: 2026-07-02
#+TODO: TODO | DONE
#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED

* CANCELLED Status
:PROPERTIES:
:ID:       d92e0074-f594-4e83-81a0-faf282e15ed0
:END:
- [2026-07-02 Thu] CANCELLED — targeted the wrong file manager. Craig's ask
  is about the dirvish popup (Super+F, an Emacs frame), not nautilus (the
  Super+Shift+F bind that misled the grounding). For dirvish the right
  design is elisp-side and strictly better: Emacs is the launcher, so it
  can spawn the handler directly (=start-process=), hide the popup frame,
  and restore it from a process sentinel — exact exit tracking plus a
  failure notify, no window-event heuristics. Reassigned to .emacs.d via
  its inbox (2026-07-02-2231-from-archsetup-dirvish-popup-swallow-handoff).
  The gio double-fork finding below stands for any gio-launching file
  manager; the daemon design is kept for reference only.
- [2026-07-02 Thu] DRAFT — initial spec from Craig's roam capture ("when the
  file manager launches another app, it should hide and return when that
  process ends"). Feasibility ground truth sampled live on velox same
  evening: Hyprland's native swallow cannot work here (see Problem), so the
  design is an event-listener daemon.

* Metadata

| Field  | Value                                              |
|--------+----------------------------------------------------|
| Status | cancelled                                          |
|--------+----------------------------------------------------|
| Owner  | Craig Jennings                                     |
|--------+----------------------------------------------------|
| Repo   | dotfiles (daemon + config); archsetup (none)       |
|--------+----------------------------------------------------|
| Kin    | touchpad-auto (socket-listener donor),             |
|        | hypr-refocus-scratchpad (event-daemon sibling)     |
|--------+----------------------------------------------------|

* Problem

Opening a file from nautilus (Super+Shift+F, tiled, class
=org.gnome.Nautilus=) spawns a viewer window while nautilus stays in the
layout. The wanted behavior is the swallow pattern: the file manager hides
while the app it launched runs, and returns when that app exits. Today
there's no signal connecting the two windows — the viewer lands wherever
the layout puts it, nautilus lingers, and quitting is manual.

*Hyprland's native swallow is ruled out — measured, not assumed.*
=misc:enable_swallow= + =swallow_regex= would be exactly this feature in two
config lines, but it matches by walking the new window's PID ancestry to
the swallow candidate's PID. Nautilus launches handlers through GLib
(=g_app_info_launch_default_for_uri=), and that path orphans the child:
reproduced live on velox 2026-07-02 with a python-gi launcher — feh came up
with PPID 1 (reparented to init) while the launcher was still alive. The
ancestry walk hits init before it hits nautilus, every time, for every
handler. Any design that depends on PID parentage is dead on arrival; the
signal has to come from window events instead.

Ground truth on handlers (velox, 2026-07-02): pdf → zathura, image → feh,
video → mpv, text/code → emacsclient (window belongs to the emacs daemon).
Side-note, out of scope here: feh is X11 — an XWayland viewer on a
no-XWayland-by-preference setup; a default-handler review is its own task.

* Goals

- Double-click a file in nautilus → the viewer takes its place; nautilus is
  gone (special workspace, not killed — state and tabs survive).
- Quit the viewer → nautilus returns and has focus.
- Nothing else changes: terminals, scratchpads, and every other window keep
  their current behavior.
- Config-driven, testable logic, one small daemon — the touchpad-auto shape.

* Design sketch

A =hypr-swallow= daemon (dotfiles, =hyprland/.local/bin/=) listening on the
Hyprland IPC event socket (socket2), same as =touchpad-auto=:

- Track the active window (=activewindow>>= events carry class + title;
  =activewindowv2>>= carries the address).
- On =openwindow>>= (address, workspace, class, title) while the active
  window's class is a configured *parent* (nautilus): dispatch
  =movetoworkspacesilent special:swallow,address:0x<parent>=, record
  child-address → {parent-address, origin workspace}.
- On =closewindow>>= of a recorded child: bring the parent back
  (=movetoworkspace=) and focus it; drop the record.
- On =closewindow>>= of a hidden parent (nautilus quit while hidden): drop
  the record, nothing to restore.
- Exception classes (fuzzel, dunst, scratchpad classes, the panels) never
  trigger a swallow even when they open over nautilus.
- Pure event-machine core (parse lines → state transitions → dispatch list),
  unit-tested against recorded event streams; a thin socket loop around it.

Known edge, handled: Super+Shift+F while nautilus is hidden re-runs
=nautilus=, which activates the existing (hidden) instance instead of
opening a window. The daemon (or the bind) must restore-and-untrack in that
case so the bind never appears dead.

Known limitation, accepted: the emacsclient case never swallows — the
window belongs to the long-running emacs daemon and =closewindow= for it
means a frame closed, not "the file is done." The parent-class trigger plus
exception list naturally leaves it alone only if we exclude it explicitly —
see decision 2.

* Decisions (Craig)

** TODO Trigger breadth: any new window while nautilus is active, or an allowlist of viewer classes?
"Any window" is simple and catches every handler, but a false positive
exists: an app you launched seconds earlier from elsewhere finishes starting
while you're focused on nautilus → nautilus gets swallowed by an unrelated
window. An allowlist (zathura, mpv, imv, feh, …) can't be surprised but
needs maintaining. Recommendation: any-window + exception list — the false
positive is rare and self-healing (close the window or refocus).

** TODO The emacs frame case: swallow or exempt?
Opening a text file from nautilus raises/creates an emacs frame. Swallowing
nautilus under it "works" going in, but the restore fires when *any* frame
closes, which may be much later or never. Recommendation: exempt =emacs= —
text files just open, nautilus stays.

** TODO Restore destination: the workspace nautilus came from, or the one you're on when the viewer closes?
If you move the viewer to another workspace and quit it there, "origin"
teleports you back; "current" brings nautilus to you. Recommendation:
current workspace — the restore should land where your attention is.

** TODO Multiple children: refcount or single-slot?
You can only launch a second file after restoring nautilus manually, so
overlap is rare — but a fast double-launch can record two children.
Recommendation: refcount — restore when the last tracked child closes.

* Implementation phases

1. =hypr-swallow= core: pure event-machine (TDD over recorded event
   streams; fake hyprctl for dispatch assertions), config block at the top
   (parent classes, exception classes), unittest suite in =tests/=.
2. Socket loop + wiring: exec-once in hyprland.conf, the Super+Shift+F
   restore-if-hidden interplay, daemon single-instance guard.
3. Live verification on velox (zathura + mpv round-trips, the emacs case,
   the false-positive probe) + manual-testing entries; ratio rides the
   dotfiles pull.