clonetty 0.0.1

Spawn a new Alacritty window cloning an existing terminal's working directory and its nested-shell environment stack, so Ctrl-D peels back one shell layer at a time.
clonetty-0.0.1 is not a library.

clonetty

Spawn a new Alacritty window that clones an existing terminal to form a new instance at the same working directory, and by default its full environment reconstructed as a stack of nested shells (so Ctrl-D peels back one nix-shell/subshell layer at a time, just like the original). Optionally, collapse the stack to just the active environment.

This is perhaps of most interest to tiling windows manager users and those who frequently work in the terminal with multipl instances.

clonetty                 # clone this terminal + its nested shells into a new window
clonetty --base          # clone only the clean base login-shell env at the same path
clonetty --pid 12345     # clone the terminal owning shell PID 12345
clonetty --reuse         # reuse the running alacritty process (fast; no env/nesting)
clonetty -- vim notes.md # open the clone running a command instead of a shell
clonetty --dry-run       # print what would run (incl. generated rc files), spawn nothing
clonetty --focused       # clone the focused X11 window's terminal (Xorg; i3/bspwm keybind)

Future Directions

The involved parts include the

  • graphical display system (Xorg or Wayland),
  • desktop environment,
  • terminal, and
  • shell. This proejct focuses on the terminal first, however X11 offers a window environment variable that can be useful to generalize across terminals.

This has been confirmed to work with alacritty using X11's focused window environment variable. That should generalize the aquisition of the focused terminal's PID, and opens up clonetty's use for other terminals in the future.

After alacritty, the following terminals will slowly be included if a general approach is unable to adopted:

  • kitty
  • ghostty
  • gnome-terminal (targeting the downstream one default in Ubuntu. It has an environment variable that upstream removed)
  • Konsole

How it works

clonetty walks the process tree from the source shell up to the hosting alacritty, reading each ancestor shell's environment from /proc (comm, status for PPid:, environ). Adjacent layers with an identical environment are collapsed, leaving one entry per distinct environment layer — outermost (login) shell first, innermost (leaf) shell last.

  • Current terminal (default): walked from this process's parent; cwd is this process's cwd.

  • Targeted terminal (--pid): walked from /proc/<pid>/. Give the innermost shell PID (echo $$ in the target window) to reconstruct nesting to full depth; the alacritty PID only resolves to its outermost child shell.

    Reading another process requires the same UID (fine for your own terminals).

  • Alacritty is one process, many windows. So the alacritty PID can't name a single window; the per-window handle is the shell PID inside it. If you pass the alacritty PID, clonetty descends to its child shell — and if there are several windows, it lists them (with cwd) so you can pick the shell PID.

This walk is read-only: it only reads /proc and never signals or writes to the running shells, so cloning a terminal can never exit, disturb, or alter the source terminal's shell stack.

Reconstruction (default): env-delta nested shells

The new window is launched with the base login-shell environment (level 0, which predates any nix-shell — it has the session vars Alacritty needs, e.g. DISPLAY/WAYLAND_DISPLAY, XAUTHORITY, XDG_RUNTIME_DIR, DBUS_*, HOME, PATH, and none of the nix vars). Alacritty then runs a chain of nested bash shells, one per captured layer, via synthesized rc files:

  • Each rc file applies only that layer's env delta (export the vars it added/changed, unset the ones it removed) — the vars a nix-shell (or any subshell) contributed on top of the layer below.
  • Each rc file then runs (not execs) the next shell as a child. Because control returns to the parent shell when its child exits, Ctrl-D peels one layer at a time to arbitrary depth. The final Ctrl-D at the base shell closes the window.

The rc files are written to a private (0700) temp dir with 0600 perms (they may hold secrets from the copied environment) and are deleted by the new window's base shell on exit. Use --dry-run to print them without writing or spawning.

Window-/session-specific variables (ALACRITTY_*, TERM, PWD, OLDPWD, SHLVL, WINDOWID, TMUX*, STY, ...) are scrubbed from every layer so the new Alacritty and shells set their own correct values.

--base: cull to a clean terminal

--base skips reconstruction and opens a fresh terminal with just the outermost login-shell environment at the same path — no nix-shell/subshell nesting.

--reuse

--reuse (alacritty msg create-window) opens the window via the running Alacritty's IPC socket. It's faster but the window inherits that process's environment, so neither a custom environment nor nested-shell reconstruction is possible in this mode.

Caveats

  • Reconstruction assumes bash for the shell layers (matches nix-shell).
  • Each layer re-sources ~/.bashrc for interactive niceties (prompt, aliases) and then applies its env delta authoritatively; a ~/.bashrc that mutates env non-idempotently (e.g. unconditionally prepends to PATH) may duplicate such entries. Env vars that changed between layers are always reconstructed exactly.
  • Exported shell functions (BASH_FUNC_*%%) and other non-identifier keys are skipped rather than replayed as broken assignments.

Alacritty Keybinding

Add to alacritty.toml to clone the focused window with a keybinding:

[keyboard]
bindings = [
  { key = "N", mods = "Control|Shift", command = { program = "clonetty" } },
]

Keybindings on Wayland: let your compositor supply --pid

--focused is Xorg-only: it reads _NET_ACTIVE_WINDOW_NET_WM_PID off the X root window. Wayland has no portable equivalent — client isolation forbids querying another client's window, and PID is deliberately absent from the wlr-foreign-toplevel-management and ext-foreign-toplevel-list protocols (open request: wlr-protocols#76).

But the compositor knows the focused window, and your keybinding already lives there. So resolve the focused window's PID with the compositor's own IPC and pass it to clonetty --pid. This keeps clonetty windowing-agnostic (--focused is just a bundled X11 convenience backend); each environment plugs in its native resolver. Every snippet below needs jq and the compositor CLI on PATH.

Sway (sway IPC) — ~/.config/sway/config:

bindsym $mod+Shift+Return exec clonetty --pid "$(swaymsg -t get_tree | jq '.. | select(.focused? == true).pid')"

Hyprlandhyprland.conf (bind syntax):

bind = $mainMod SHIFT, Return, exec, clonetty --pid $(hyprctl activewindow -j | jq .pid)

niriconfig.kdl (niri's spawn is shell-less, so wrap in sh -c):

binds {
    Mod+Shift+Return { spawn "sh" "-c" "clonetty --pid $(niri msg --json focused-window | jq .pid)"; }
}

KDE Plasma / KWin — KWin exposes no xdotool-style PID query, but its scripting API has workspace.activeWindow.pid. Drive it with kdotool (it loads a KWin script under the hood) or a focus-tracking script like FocusNotifier, then bind a Custom Shortcut (System Settings → Shortcuts) that reads the PID and calls clonetty --pid.

GNOME Shell (Wayland) — GNOME exposes no window API to clients; install a D-Bus extension that publishes the focused window's PID, e.g. focused-window-dbus or window-calls-extended (adds a FocusPID/List method). Bind a Custom Shortcut (Settings → Keyboard) to a small sh -c that gdbus calls the extension's method and passes the PID to clonetty --pid (exact object path/method are in the extension's README).

COSMIC (System76) — no focused-PID resolver exists on current COSMIC (Epoch 1.4): the cosmic-toplevel-info-v1 protocol (v3) and the community cosmic-ext-window-helper expose app_id/title/state but not PID; native window management is slated for COSMIC Epoch 2. Two options today:

  • XWayland bridge (test first): run the terminal under XWayland (WINIT_UNIX_BACKEND=x11 alacritty) and use clonetty --focused, which reads XWayland's _NET_ACTIVE_WINDOW/_NET_WM_PID. Confirm cosmic-comp maintains them — xprop -root _NET_ACTIVE_WINDOW and xdotool getactivewindow getwindowpid should resolve to the focused terminal. Only covers XWayland (X11) windows.
  • No bridge: run clonetty in-terminal (no --pid; full fidelity) or clonetty --pid "$$" from within the target shell.

OthersWayfire exposes a focused-view query via its ipc plugin; river has no window-query API (it is tag-based), so there you'd run a wlr-foreign-toplevel client and match by app-id/title — PID isn't in that protocol (#76), a genuine gap until it is added.

Graceful degradation: where no focused-PID resolver exists yet (COSMIC, river), the in-terminal clonetty (no flag) and --pid "$$" paths always work and are the highest fidelity — the keybinding is only a convenience for capturing another window without focusing a shell prompt first.