# clonetty
Spawn a new [Alacritty](https://alacritty.org) 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.
```sh
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 `exec`s)** 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:
```toml
[keyboard]
bindings = [
```
## 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`](https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1)
and [`ext-foreign-toplevel-list`](https://wayland.app/protocols/ext-foreign-toplevel-list-v1)
protocols ([open request: wlr-protocols#76](https://github.com/swaywm/wlr-protocols/issues/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](https://man.archlinux.org/man/sway-ipc.7)** (sway IPC) — `~/.config/sway/config`:
```
bindsym $mod+Shift+Return exec clonetty --pid "$(swaymsg -t get_tree | jq '.. | select(.focused? == true).pid')"
```
**[Hyprland](https://wiki.hypr.land/Configuring/Using-hyprctl/)** — `hyprland.conf` ([bind syntax](https://wiki.hypr.land/Configuring/Basics/Binds/)):
```
**[niri](https://github.com/YaLTeR/niri/wiki/IPC)** — `config.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](https://develop.kde.org/docs/plasma/kwin/)** — KWin exposes no
xdotool-style PID query, but its scripting API has `workspace.activeWindow.pid`.
Drive it with [`kdotool`](https://github.com/jinliu/kdotool) (it loads a KWin script
under the hood) or a focus-tracking script like
[FocusNotifier](https://github.com/c-massie/FocusNotifier), then bind a Custom
Shortcut (System Settings → Shortcuts) that reads the PID and calls
`clonetty --pid`.
**[GNOME Shell](https://gjs.guide/extensions/) (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](https://github.com/flexagoon/focused-window-dbus) or
[window-calls-extended](https://github.com/hseliger/window-calls-extended) (adds a
`FocusPID`/`List` method). Bind a Custom Shortcut (Settings → Keyboard) to a small
`sh -c` that `gdbus call`s the extension's method and passes the PID to
`clonetty --pid` (exact object path/method are in the extension's README).
**[COSMIC](https://github.com/pop-os/cosmic-comp) (System76)** — no focused-PID
resolver exists on current COSMIC (Epoch 1.4): the
[`cosmic-toplevel-info-v1`](https://wayland.app/protocols/cosmic-toplevel-info-unstable-v1)
protocol (v3) and the community
[`cosmic-ext-window-helper`](https://github.com/lapause/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.
**Others** — [Wayfire](https://github.com/WayfireWM/wayfire/wiki) exposes a
focused-view query via its `ipc` plugin; [river](https://codeberg.org/river/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.