A terminal multiplexer written in Rust, with a vertical tab sidebar, a client/server design (detach and reattach), and everything configurable.
Everything below the PTY is written from scratch: the VT/ANSI parser, the
screen model, the frame differ and the layout engine. There is no vt100,
vte or portable-pty.
- Windows, splits and sessions, with a sidebar listing them (click, search, reorder).
- Window groups with one-letter tags:
Ctrl-a g 1jumps to window 1 of groupg. - Projects: describe a session's windows, splits and start commands in
YAML, open it with
dtm open, export a running session back to a file. - A dock for session-wide panes (lazygit, a log tail) and a popup shell.
- Maximize, zoom, recently-used window cycling, and a rounded-box "islands" look
(with a
plainalternative) with themes. - Copy mode with search and word motions, and a mouse that selects, resizes and reorders.
- A
:command prompt, a key list and a CLI to script a running server. - Seamless
Ctrl-h/j/k/lmovement between panes and Neovim splits. - Works on Linux and macOS.
cargo build --release # binary: target/release/dtm
cargo install --path . # or install it into ~/.cargo/bin
dtm # attach to the first session (starting the server and a session if needed)
dtm new -s work # new named session
dtm attach -t work # attach to an existing session
dtm ls # list sessions
dtm open [FILE|DIR|NAME] # open a project file (see Projects)
dtm export [FILE|DIR|NAME] # write the running session to a project file
dtm project ls|edit|rm # manage saved projects
dtm reload # re-read the config file
dtm select-pane -R # move pane focus (used by editor plugins, see below)
dtm query '#{pane_at_left}' # print a pane's state (used by editor plugins)
dtm run toggle-maximize # run any action or command (see Commands and scripting)
dtm new-window -c ~/code htop # ... or one of the shorthands, also split-window, send-keys, kill-session
dtm kill-server # end the server and every session
The default prefix is Ctrl-a. The server exits when its last session ends.
<prefix> Q closes the shown session after asking.
On macOS a binary that was copied or rebuilt can be killed on start (Killed: 9)
because its code signature no longer matches; re-sign it with
codesign --force --sign - /path/to/dtm.
| Key | Action | Key | Action |
|---|---|---|---|
c |
new window | | |
split side by side |
n / p |
next / previous window | - |
split stacked |
1-9 |
window 1-9 of the first group | arrows, Ctrl-h/j/k/l |
move focus |
, |
rename window | h j k l / H J K L |
resize pane by 1 / by 5 |
& |
kill window | o |
next pane |
x |
kill pane | z |
zoom pane |
S |
new session | $ |
rename session |
( / ) |
previous / next session | b |
show / hide sidebar |
s |
sidebar: windows ↔ sessions | d |
detach |
w |
focus the sidebar | ; |
previously focused pane |
Ctrl-l |
send a literal Ctrl-l | [ or PageUp |
copy mode |
] |
paste | a |
send a literal Ctrl-a |
r |
reload config | R |
respawn pane (asks) |
Alt-r |
respawn window (asks) | f or / |
search windows |
Tab |
previously opened window (then Tab / Shift-Tab repeat) |
m |
maximize / restore |
C |
new window group (asks) | <tag> <tag> |
window of a group, e.g. g 1 |
Q |
kill the session (asks) | : |
command prompt (see below) |
? |
list the key bindings | ||
P |
popup shell (toggle) | D |
show / hide the dock |
{ / } |
previous / next dock pane | A / X |
add / close a dock pane |
Without the prefix: Ctrl-h Ctrl-j Ctrl-k Ctrl-l move between panes
(see below).
Maximize: <prefix> m makes the focused thing fill the whole terminal: a window pane, a dock pane, the popup, or the sidebar (when it has focus). The pane is resized to the full screen, so lazygit or vim redraw at full size. Press it again to restore. Moving focus to something else (or hiding the popup or dock) restores it too. Unlike z, which zooms inside the pane area, this hides the sidebar and dock as well.
Window groups: windows are organised in groups with one-letter tags, and <prefix> <group tag> <window tag> opens a window, e.g. Ctrl-a g 1. See Window groups.
Recent windows: <prefix> Tab toggles to the previously opened window; for
the repeat time after it (options.repeat_time, 1 second) plain Tab goes
further back, like Alt-Tab, and Shift-Tab goes the other way. Any other key
ends it, and after that Tab is the shell's again. A window counts as
opened when you choose it (Enter/o in the sidebar, a click, <prefix> 1-9,
n/p, search), not while the sidebar selection merely passes over it. (n/p stepping
onto a project window that has not started only shows it as "Not loaded"; see Projects.)
Ctrl-Tab / Ctrl-Shift-Tab do the same without the prefix, but terminals only
send them when asked to, and most use them for their own tabs: make yours send ESC [ 9 ; 5 u (and ESC [ 9 ; 6 u for Ctrl-Shift-Tab), e.g.
Ghostty keybind = ctrl+tab=csi:9;5u, kitty map ctrl+tab send_text all \x1b[9;5u,
WezTerm { key = "Tab", mods = "CTRL", action = act.SendString "\x1b[9;5u" }, or
an iTerm2 key mapping "Send Escape Sequence" [9;5u. xterm's
modifyOtherKeys form (ESC [ 27 ; 5 ; 9 ~) works too.
Switching windows focuses the top-left pane of the window
(options.window_focus = "last" keeps the pane that was focused there).
Size indicator: while you resize (and for the repeat window after it), the middle of the pane shows its size as columns×rows, and the sidebar or dock shows its width, so panes in different windows can be lined up.
Resizing: while the sidebar or the dock has focus, <prefix> h / l move its inner edge to resize it, and then plain h / l keep resizing by one cell for a second, as for panes. Both widths belong to the session (every client and later attach sees them); the dock's is exported, the sidebar's is not (its default is [sidebar] width). Otherwise <prefix> h j k l resizes the focused pane by 1 cell (H J K L by 5), then plain h j k l keep
resizing by 1 cell for a second (options.repeat_time, [keys.resize]), so you
don't have to press the prefix again for every step. Any other key ends it.
Copy mode (<prefix> [, or PageUp): move with k j h l, PageUp/Down, Ctrl-u/d,
g/G for top/bottom, 0/$, the word motions w b e, and H M L for the top,
middle and bottom row of the view. / searches forward and ? backward (it asks for the
text; a term without capitals ignores case), n repeats the search the way it was typed
and N the other way; matches are highlighted and the current one stands out
([theme] match_bg). v selects, V selects whole lines, Ctrl-v a rectangle; y
copies: the selection ends but you stay in copy mode with the cursor where it was (so you can
select and copy again), and q leaves. Copied text is kept in dtm's paste buffer and also sent
to the terminal clipboard (OSC 52). The cursor is a steady block while you are in copy mode,
whatever shape the program in the pane asked for.
Mouse: click a sidebar entry (or a group's title) to switch window or session, click a pane or a dock tab to focus it, scroll the wheel to enter copy mode (or scroll full-screen apps).
- Select text by dragging in a pane: it enters copy mode with the text selected (dragging above or
below the pane scrolls it),
yor Enter copies. A double click selects a word and a triple click a line. Panes whose program asked for mouse reports (vim withmouse=a, htop) get the events instead. - Resize by dragging the border between panes, the edge of the sidebar, or the edge of the dock.
- Reorder by dragging a window (or session) up or down in the sidebar; it moves into the neighbouring group when it passes the edge of its own.
dtm asks the terminal for button-event mouse reports (mode 1002, which includes drags); a terminal
without it still gets clicks and the wheel. Hold Shift to select text with the terminal itself,
or set mouse = false.
<prefix> : opens a prompt that runs any action by name (select-window 3, toggle-maximize,
copy-mode) or one of these commands:
| Command | Does |
|---|---|
new-window [-c DIR] [-n NAME] [COMMAND] |
a window in the shown group; COMMAND is typed into its shell |
split-window [-h] [-v] [-c DIR] [COMMAND] |
split the shown pane (-h side by side, -v stacked, the default) |
new-session [-s NAME] [-c DIR] |
a session, and switch to it |
kill-session NAME |
close a session without asking |
rename-window NAME, rename-session NAME |
rename the shown window or session |
send-keys [-l] [-t PANE] KEY|TEXT... |
type into a pane: Enter, C-c, Up, F5 are keys, anything else is text (-l: everything is text) |
As you type the name, the names that match are listed under the prompt. Tab or Down choose the
next one (the first Tab also completes as far as the choices agree), Shift-Tab or Up the
previous; with nothing typed, Up / Down walk the commands you ran before. Enter runs it, Esc cancels. Quote words with spaces: new-window -n "my app".
The same commands run from a shell, which makes dtm scriptable:
dtm run 'new-window -c ~/code -n api "cargo run"'
dtm new-window -c ~/code -n api cargo run # shorthand, the arguments are the command
dtm split-window -h
dtm send-keys 'cargo test' Enter # into the focused pane
dtm send-keys -t 7 C-c # into pane 7
dtm run select-window 2
dtm --session work kill-session scratch
Inside a pane they target that pane's session; elsewhere --session NAME picks one (a single
running session is picked automatically). Actions that need someone looking at the session
(focus, prompts, maximize) use the most recently active attached client and say "no client attached"
when there is none; new-window, split-window, send-keys and kill-session work detached.
<prefix> ? lists the key bindings, taken from your config so remaps show up (j/k, space and
b scroll, q closes). / searches it: type part of a key or an action name (any case) and only
the matching lines are listed under their headings; Enter keeps the filter, Esc clears it.
Ctrl-h/j/k/l move focus left/down/up/right, continuing into the sidebar at the
edge (and back out again). They are root bindings, so change them in
[keys.root] (navigate-left|right|up|down, navigate-last), or remove one with
"C-h" = "none". Because they replace the shell's own Ctrl-l (clear screen) and
friends, <prefix> Ctrl-l sends a real Ctrl-l; send-key does that for any
binding. (A terminal that sends Ctrl-h for Backspace will lose Backspace; most
send 0x7f.)
If an editor is in the foreground of the active pane, dtm hands the key to it
instead: the editor moves between its own windows and, when it is already at the
edge, asks dtm to move pane focus (dtm select-pane, see below). The editor is recognised by process name: vim, nvim, view, vimdiff, fzf
(including Nix's .nvim-wrapped), plus anything listed in
options.navigator_apps.
This repository is also the Neovim plugin: plugin/ and lua/dtm_navigator/ at the
root make Ctrl-h/j/k/l move between Neovim windows and, at the edge of Neovim, hand
over to dtm (via dtm select-pane). With lazy.nvim:
{ "Firely-Pasha/dtm", name = "dtm-navigator" }Outside dtm (no $DTM_PANE) it only moves between Neovim windows.
Without a plugin manager, clone the repository and set rtp+=/path/to/dtm.
Neovim only (classic Vim is not supported). It uses APIs available since Neovim 0.7
and is tested with 0.12.
Commands: :DtmNavigateLeft, :DtmNavigateDown, :DtmNavigateUp,
:DtmNavigateRight, :DtmNavigatePrevious (mapped to <C-h/j/k/l> and <C-\>).
Options, set before or after loading (no_mappings must be set first):
| Variable | Default | |
|---|---|---|
g:dtm_navigator_no_mappings |
0 | don't define the default mappings (use the commands) |
g:dtm_navigator_save_on_switch |
0 | 1: :update the buffer, 2: :wall, before leaving for another pane |
g:dtm_navigator_disable_when_zoomed |
0 | stay in Neovim when the dtm window is zoomed |
g:dtm_navigator_disable_netrw_workaround |
0 | don't make <C-l> work in netrw |
require("dtm_navigator").setup({ save_on_switch = 1 }) sets them from Lua.
Every pane gets DTM_PANE (its id), DTM_SOCKET (so the dtm CLI reaches the
right server) and DTM_BIN (the dtm executable). Two commands use them:
dtm select-pane [-t PANE] -L|-R|-U|-D|-l # move focus (-l: previously focused pane)
dtm query [-t PANE] '#{window_zoomed_flag}' # also pane_at_left/right/top/bottom, pane_id, pane_active
A project file describes a session: its windows, how each window is split,
where each pane starts and what it runs. See
examples/project.dtm.yaml, which uses every key.
name: app # session name (default: the directory's name)
root: ~/code/app # default: the directory of the file
lazy: true # default: a window starts when you open it (hovering shows "Not loaded")
groups: # optional: window groups, in order (tag: one letter, optional)
- {name: code, tag: g} # Ctrl-a g 1 opens the first window of this group
windows:
- name: edit
group: code # windows without `group` are in `main`
focus: true # shown first
layout:
split: columns # columns = side by side, rows = stacked
panes:
- command: nvim
size: 65% # N% or cells; the rest is shared equally
focus: true
- split: rows
panes:
- command: cargo watch -x check
- path: scripts
- name: server # one pane: path/command straight on the window
command: cargo run
lazy: false- A
pathis relative to the nearestpathabove it, then toroot;~and absolute paths work. A pane'scommandis typed into its shell, so you are left at a prompt when it ends. Unknown keys and mistakes (a split with one pane, a bad size) are reported with their position before anything starts. sidebar(top level) sets this project's sidebar:width(4-200) andvisible. Exporting keeps what you wrote; a width you resized to is not saved (set the default in[sidebar] widthof config.toml).pane_command(top level) is typed into every pane's shell before the pane's own command, e.g.pane_command: source .venv/bin/activate. It also applies to panes you create later in the session (splits, new windows, the popup, dock panes) and to respawned ones.- Lazy start. Only the first window is created when the project opens; the
others get their panes and run their commands the first time you open them
(a
…marks them in the sidebar; just moving the selection over one shows "Not loaded" and starts nothing, and so does stepping onto it with<prefix> n/p; with focus on such a window,oorEnterloads it). Setlazy: falseon the file or on a window to start it at once. - Opening.
dtmin a directory with aproject.dtm.yamlopens it.dtm open [FILE|DIR|NAME]takes a file, a directory holding one, or a saved project. If a session with the project's name is already running, dtm attaches to it and starts nothing again. - Saved projects live in
~/.config/dtm/projects/NAME.yaml(next to the config file):dtm project ls,dtm project edit NAME(opens$VISUAL/$EDITOR, creating the file from a template, and checks it afterwards) anddtm project rm NAME. A saved project has no directory of its own, so give it aroot:. - Export.
dtm export [FILE|DIR|NAME] [-t SESSION]writes a running session as a project: the splits and their sizes, each pane's directory and, when a program other than the shell is running, its command line. With no argument it updates the file the session was opened from, or createsproject.dtm.yamlin the session's directory. Paths are written relative to the file. A file without arootis written without one; a file that has one keeps it, and the paths are then written relative to it. A saved project is updated in place, never exported to its root. When updating, yourname,root,lazy,pane_commandand dock size are kept, but comments are not.ein the focused sidebar does the same after asking (it shows the file it will write); the unboundexport-projectaction writes at once. - Respawn.
<prefix> Rrestarts the focused pane the way it started (same directory and command, a new process) and<prefix> Alt-rrestarts the whole window from its project layout, closing panes you added since. Both ask first. In the sidebar,Rrespawns the selected window.
A second side panel, opposite the sidebar, holds panes that belong to the whole session instead of one window: lazygit, a claude session, a log tail. It shows one pane at a time under a row of tabs.
dock:
width: 35% # N% of the terminal or cells (default 35%)
visible: true # shown when the session opens (default: hidden)
panes:
- name: git # default: the command's first word
command: lazygit
- command: claude
path: .. # like window panes: relative to rootDock panes start the first time they are shown. <prefix> D shows or hides
the dock, { and } cycle its panes, A asks for a command and adds a pane
running it (empty: a shell), X closes the current one (asks first), and the
dock-rename / focus-dock actions are unbound. Ctrl-l from the rightmost
window pane (or any focus move towards the dock, including the Neovim plugin's
at the edge of Neovim) moves into it and the opposite
direction comes back; clicks on tabs and panes work too. <prefix> x closes the
focused dock pane without asking. The dock is exported with the project.
<prefix> P opens a floating shell in the current directory ([popup] width
and height size it) and <prefix> P again hides it; it keeps running while
hidden. While it is shown it has the keyboard, and <prefix> x (or exiting the
shell) closes it.
Windows belong to groups, so a session with many windows stays easy to navigate.
Every session starts with one group, main.
- Tags. A group has a name and a one-letter tag (
a-z,A-Z). A window has no number of its own: its tag is its place in its group,1-9and thena-z(so a group holds up to 35 windows). Adding, moving or closing windows re-tags the ones after them. - Opening a window.
<prefix> <group tag> <window tag>:Ctrl-a g 1is the first window of the group taggedg,Ctrl-a g aits tenth. After the group tag the status line readsgroup NAME: window?; any key that is not a window tag cancels.<prefix> 1-9picks from the first group, as it always did. When the sidebar has focus, its selection follows the window that opens. - New group.
<prefix> Casks for a name (empty:2,3, ...) and starts the group with a new window. Its tag is taken from the letters of the name, skipping ones that are taken.<prefix> cadds a window to the group of the shown one. A group with no windows left disappears. - Rename and re-tag. With the sidebar focused (
<prefix> w),grenames the group of the selected window andtgives it another tag. A tag is refused, with the reason shown, when it is not one letter, when it is already bound after the prefix (C-a c is already bound), or when another group has it. - Moving windows. In the sidebar
J/Kmove a window within its group; past the group's edge it joins the next or previous group. A window that is alone in its group moves the whole group instead, which is how groups are reordered. - In the sidebar. With one group its name is the title under the session name, with its tag at the right edge. With several, each group has such a title row above its windows, with an empty line between groups; clicking a title opens the group's first window.
- Search.
<prefix> ffinds windows across groups. Results are listed group by group under the group's title (best match first within each group), and a group's name matches its windows too. - Projects. A window says
group: NAME; a top-levelgroups:list sets the order and, optionally, the tags (a tag that is already bound is replaced by a free one). Windows withoutgroupare inmain. Exporting writes both back, but only when there is more than one group or the one group is notmain.
groups:
- {name: code, tag: g}
- {name: ops, tag: o}
windows:
- {name: edit, group: code}
- {name: test, group: code} # Ctrl-a g 2
- {name: logs, group: ops} # Ctrl-a o 1Actions (bindable in [keys.prefix] and [keys.sidebar]): new-group,
sidebar-rename-group, sidebar-group-tag.
By default ([options] style = "islands") the sidebar, every pane and the dock are
separate rounded boxes, with a one-cell gap between boxes side by side and none between
stacked ones; the focused pane's box (and a focused sidebar or dock) has a highlighted
border. style = "plain" gives flat panes split by single lines instead.
╭ work ──────────────╮ ╭────────────────────╮ ╭────────╮
│ main g │ │ $ nvim │ │ logs │
│ 1 edit │ ╰────────────────────╯ │ $ │
│ 2 test │ ╭────────────────────╮ │ │
╰────────────────────╯ │ $ │ ╰────────╯
╰────────────────────╯
A box takes one row and column on each side, so a pane (or the sidebar and dock)
has two fewer columns and rows of text than in the plain style, and stty size
in a pane reports that smaller size. Ctrl-a m still makes a pane fill the whole
terminal without a box, and dtm reload switches styles in a running server.
Colours are set in [theme]. Start from a preset with preset = "default" (the
terminal's own palette colours), "mono", "nord" or "gruvbox", and change single
slots; a colour is a name (red, bright-black), a palette number 0-255, or
"#rrggbb":
[theme]
preset = "nord"
border_focus = "#a3be8c" # the border of whatever has the keyboard| Slot | Used for |
|---|---|
border, border_focus |
box borders and separators; the focused one |
muted |
dim text: subheaders, hints, placeholders |
title |
the session name and prompt text (always bold) |
selection_bg, selection_fg |
the sidebar's keyboard selection, and the dock tab while it has focus |
active_bg, active_fg |
the shown window in the sidebar, the dock's current tab |
match_bg, match_current_bg |
copy-mode search matches, and the one the cursor is on |
A row style (selection_*, active_*) and the match colours are drawn inverted while
their background is default; setting a background colour replaces that. A mistake in
the theme is reported with its name (theme.border: unknown colour "mauve") by
dtm reload, and the running theme stays. See examples/config.toml.
<prefix> f, or / in the focused sidebar, filters the windows by typing
(fuzzy: srv finds 2 server; with several groups the results are listed group by
group, and a group's name finds its windows). A project window that has not
started shows "Not loaded" while you move through the results; Enter starts and opens it. Up/Down (or Ctrl-n/Ctrl-p) move the
selection, which shows that window as you go; Enter opens it and Esc goes
back to where you were.
A column on the left or right ([sidebar] position) lists the current
session's windows, under their group's title when there are several
(groups): 1 vim, with ● for activity, ! for a bell, Z when
zoomed. <prefix> s toggles it to a list of sessions. Window names come from
rename-window, else the pane's title (OSC 0/2), else the foreground process.
It hides itself when the terminal is too narrow.
The sidebar can take keyboard focus with <prefix> w, or by moving focus
towards it (<prefix> Left from the leftmost pane, or a root binding such as
"C-h" = "focus-left"; the opposite direction leaves it). While focused:
j/k select (and show) the window or session as the highlight moves
(sidebar.follow, on by default), Enter (or o) opens it and leaves, q leaves, x kills (after a y/n confirmation), r
renames, g/t rename / re-tag the window's group, J/K reorder, n creates a window or session, Tab toggles the
list, q leaves. All of it is configurable in [keys.sidebar].
[sidebar] options: position, width, visible, default_mode, follow, and
hints (the key-hint footer while focused), group_gap (the empty line between
groups), marks (activity, bell, zoom and not-loaded markers).
At the bottom of the sidebar, below a line, are lines of your
choosing, [status] lines: each is a template of text and #{...} variables, e.g.
the default
[status]
lines = [
"#{user}@#{host}#{fill}#{time:%H:%M}",
"#{?git,⎇ #{git_branch}#{git_dirty},}",
"#{date:%a %d %b}",
"load #{load}#{fill}mem #{mem_percent}",
]#{name}is a value,#{name:arg}a value with an argument (#{time:%H:%M:%S}takes anystrftimeformat).#{?name,then,else}showsthenwhen the value is neither empty nor0, elseelse(optional). Branches can hold variables, other conditions and fills; write#,and#}for a comma or brace in a branch.##is a literal#.#{fill}pads with spaces to the box's width, so the text after it sits at the right edge; several fills share the room (#{fill}#{time}#{fill}centres the clock), and#{fill:·}fills with that character.- A line whose variables all come out empty is left out (no battery, no repository); an empty string is a blank line.
- Variables:
user,host,time,date,session,window,group,pane_cwd,pane_command,sessions,windows,clients,load,mem,mem_percent,battery,uptime(the last three on Linux), andgit,git_branch,git_dirty. The git ones describe the repository around the session's directory (where dtm was started, or the project root), found in it or a parent; they do not follow acdin a pane. They come from a backgroundgit statusevery few seconds, so rendering never waits for git.
visible = false hides the box. It also hides itself when fewer than six rows would be left
for the list, and while the sidebar is maximized.
Prompts (rename, confirmations, the dock command) are a box in the middle of the screen: type and press Enter, or Esc to cancel; confirmations take y and cancel on any other key.
~/.config/dtm/config.toml (or $DTM_CONFIG). See
examples/config.toml for every option and binding; it is
checked by the test suite to match the built-in defaults.
options.shellis the program started in new panes (default$SHELL, then/bin/sh). The other[options]:prefix(defaultC-a),history_limit(scrollback lines per pane, 10000),mouse,clipboard(copy also goes to the terminal clipboard),repeat_time(ms, 1000),navigator_apps,window_focus("top-left"or"last") andstyle("islands", the default, or"plain").[sidebar]setsposition(left/right),width,visible,default_mode(windows/sessions) andfollow;[popup]setswidthandheight.[keys.prefix],[keys.root],[keys.copy],[keys.resize]and[keys.sidebar]map keys to actions. Set a binding to"none"to remove a default.- Key notation:
C-a,M-h,S-Left,C-M-x,F5,PageUp,Space, or a single character. - Actions:
new-window,kill-window,next-window,prev-window,select-window N,new-group,sidebar-rename-group,sidebar-group-tag,toggle-maximize,recent-window,recent-window-reverse,rename-window,split-vertical,split-horizontal,focus-left|right|up|down,focus-next,resize-left|right|up|down [N],kill-pane,zoom-pane,new-session,next-session,prev-session,select-session N,rename-session,toggle-sidebar,toggle-sidebar-mode,copy-mode,paste-buffer,detach,send-prefix,reload-config,respawn-pane,respawn-window,export-project,search-windows,toggle-popup,toggle-dock,focus-dock,dock-next|prev|add|close|rename,focus-last,navigate-left|right|up|down,navigate-last,send-key(sends the key that triggered the binding), and in copy modecopy-up|down|left|right,copy-page-up|down,copy-half-page-up|down,copy-top|bottom,copy-line-start|end,copy-select,copy-select-line,copy-yank,copy-cancel,copy-select-rect,copy-search-forward|backward|next|prev,copy-word-next|prev|end,copy-view-top|middle|bottom; for the sidebarfocus-sidebarandsidebar-down|up|open|kill|rename|move-down|move-up|new|respawn|export|search|leave. Alsokill-session,command-promptandhelp.dtm runand the:prompt take the same names. - A config error is reported by
dtm reload/<prefix> rand the running configuration stays active.
DTM_CONFIG overrides the config file path. DTM_SOCKET overrides the socket path (default $XDG_RUNTIME_DIR/dtm/default.sock,
falling back to /tmp/dtm-$UID/). The server logs to
$XDG_STATE_HOME/dtm/server.log.
dtm (client) ──unix socket, length-prefixed bincode──► dtm server
raw mode, stdin → Input one actor task owns all state:
SIGWINCH → Resize sessions → windows → split tree → panes
Output bytes → stdout PTY reader/writer tasks, per-client connection tasks
- The client is a thin relay. The server decodes keys, handles the prefix, renders each client's frame and sends only the escape sequences that differ from what that client last saw.
- The window is sized to the most recently active client; other clients see it cropped or padded.
- Panes are spawned with
openptyplussetsid/TIOCSCTTYin apre_exechook, never a rawforkin the multithreaded process.
src/term/ parser.rs (VT500 state machine), screen.rs (emulator), canvas.rs (frame + differ)
src/server/ mod.rs (event loop, rendering), state.rs, actions.rs, input.rs, mouse.rs, layout.rs,
render.rs, sidebar.rs, copy.rs, groups.rs, search.rs, project.rs, extra.rs (dock, popup),
commands.rs (the `:` prompt and `dtm run`), help.rs, ipc.rs
src/ keys.rs, action.rs, command.rs (the command language), config.rs, theme.rs, project.rs,
protocol.rs, pty.rs, client.rs, procinfo.rs, sysproc.rs (process info on Linux and macOS), fuzzy.rs
plugin/, lua/ Neovim plugin for Ctrl-h/j/k/l navigation
cargo test
Unit tests cover the parser, screen model, differ (diffs are replayed into a
second emulator and compared), key decoding, config, layout, sidebar and copy
mode. tests/e2e.rs runs the real binary on a pty and checks what a terminal
would show: attach/detach/reattach, splits, windows, the sidebar, config
reload, remapped keys, multiple clients, copy mode, mouse, and less/vim
running inside dtm.