virsh-tui
virsh-tui is a terminal user interface for libvirt. It
lists your virtual machines with live CPU, memory, disk and network graphs, and
lets you start, stop, snapshot, reconfigure and create them without leaving the
keyboard. The key bindings follow vim: j/k to move, : for commands, /
to filter, a leader key for less common actions.
Under the hood every action is a virsh (or virt-install) command. The
interface shows the exact command before and after it runs, so you can copy it
into a script, check what a button does, or learn the virsh syntax as you go.
Nothing talks to libvirt through a private API: if virsh can do it on your
machine, virsh-tui can do it, with the same permissions.
What it covers
- Domains: a sortable, filterable list with per-domain CPU bars, lifecycle actions (start, ACPI shutdown, destroy, reboot, reset, pause, managed save), bulk actions on marked domains, serial console and graphical viewer.
- Domain detail: overview, live monitor with 60 s / 5 min / 1 h history, a hardware editor that shows the XML diff and the commands it will run, snapshots, console and XML.
- Host: CPU per thread, memory, hugepages, KSM, swap, storage pools and networks at a glance.
- Networks: definitions, DHCP leases, static host entries and attached interfaces.
- Storage: pools, volumes with backing chains and owners, ISO media insertion and ejection.
- Events: the libvirt event stream for domains, networks and pools.
- A six-step wizard that builds a
virt-installcommand for new domains. - A
:command line with Tab completion for commands, domain names, themes, paths and everyvirshflag, and a fuzzy command palette. - Nine built-in themes plus custom themes, remappable keys, and a settings screen that writes back to your config file.
Who it is for
People who already use virsh and want a faster way to see and drive many
domains at once. It runs anywhere virsh runs, including over
qemu+ssh:// connections to remote hosts.
Trying it without libvirt
virsh-tui --demo starts with an in-memory backend full of example domains,
networks and pools. Nothing is executed, so it is a safe way to look around.
All screenshots in this book come from demo mode.
Installation
virsh-tui runs on Linux. It needs the libvirt client tools at run time and a Rust toolchain to build.
Requirements
| Tool | Package (Debian/Ubuntu, Fedora, Arch) | Used for |
|---|---|---|
virsh | libvirt-clients, libvirt-client, libvirt | everything (required) |
virt-install | virtinst, virt-install, virt-install | the new domain wizard |
virt-viewer | virt-viewer | the graphical viewer (v) |
ip | iproute2 | host interfaces in the Networks view |
A terminal with truecolor support gives the best result. On terminals without it, virsh-tui falls back to the xterm 256-colour palette. The layout needs at least 100 columns by 30 rows; 153 columns or more gives the side-by-side layout shown in the screenshots.
A font with box-drawing and braille characters is needed for the borders and graphs. Most modern monospace fonts have them (Fira Code, JetBrains Mono, DejaVu Sans Mono, Iosevka).
Build from source
You need Rust 1.88 or newer. Install it with rustup if your distribution ships an older version.
cargo install --locked --git https://github.com/SyscallBrain/virsh-tui
This puts the virsh-tui binary in ~/.cargo/bin. To build from a clone
instead:
git clone https://github.com/SyscallBrain/virsh-tui
cd virsh-tui
cargo build --release
./target/release/virsh-tui --demo
Permissions
virsh-tui runs virsh as your user, so it sees exactly what virsh sees.
- For
qemu:///system, your user must be allowed to manage the system daemon. On most distributions that means being in thelibvirtgroup:sudo usermod -aG libvirt "$USER", then log out and back in. qemu:///sessionneeds no extra rights; it manages the per-user daemon.- For remote hosts (
qemu+ssh://user@host/system), set up key-based SSH login first. virsh-tui cannot answer password prompts.
Check that virsh -c qemu:///system list --all works in a shell before
starting virsh-tui. If it does not, see Troubleshooting.
Getting started
First run
virsh-tui # default URI from the config (qemu:///system)
virsh-tui -c qemu:///session # the per-user daemon
virsh-tui -c qemu+ssh://me@nas/system
virsh-tui --demo # example data, nothing is executed
The first screen is the domains dashboard. The list is on the left; the panel
on the right shows the selected domain and its live graphs. The bar at the top
shows the views (1 to 5), the connection URI, the host name, the refresh
interval and the clock.
A five-minute tour
- Move with
jandk. The right-hand panel follows the selection. - Press
sto start a stopped domain. The message line at the bottom shows✓ Started <name> ── virsh start <name>: the result and the command. - Press
Sto send an ACPI shutdown. virsh-tui keeps watching the domain and tells you if the guest ignores the request, which happens when the guest has no ACPI support or is still booting. - Press
Enterto open the domain detail. UseHandLto move between Overview, Monitor, Hardware, Snapshots, Console and XML.Backspaceorqgoes back. - Press
/and type part of a name to filter the list.Escclears the filter. - Press
Spaceand wait a moment. A popup lists everything you can do after the leader key, such asSpace nfor a new domain orSpace s cfor a snapshot. - Press
:and thenTab. Every command andvirshsubcommand is listed. Try:themefollowed byTabto preview the themes. - Press
?for the full key reference, and,for the settings. :qquits (orqfrom the dashboard).
When you are not sure what a key does
Run with --dry-run. virsh-tui then shows the command each action would run
and executes nothing:
virsh-tui --dry-run
Read-only queries (the domain list, stats, XML) still run, so the screens show real data.
How the interface works
Screen layout
◆ virsh-tui 1 Domains 2 Host 3 Networks 4 Storage 5 Events ⌁ qemu:///system │ host │ ⟳ 1s │ 14:32
╭ panels … ╮
│ │
╰──────────╯
NORMAL domains › arch-dev j/k move ⏎ open s start … ● 9 ‖ 1 ✗ 1 ○ 4
✓ Started k8s-worker-02 ── virsh start k8s-worker-02
- The top bar lists the five views with their number keys, then the
connection URI, the host name, the refresh interval and the time. When the
connection is lost, the URI is replaced by a red
disconnectedand virsh-tui keeps retrying with backoff. - The status bar shows the current mode, where you are (a breadcrumb), the most useful keys for that screen, and domain counts by state.
- The message line shows the result of the last action. When the action ran a
command, the command follows the
──separator.:messagesshows the full history of messages.
At 153 columns or more the dashboard shows the domain list and the metrics side by side. Between 100 and 152 it stacks the panels and the metrics move to tabs (Overview, CPU, Mem, Disk, Net) under the list. Below 100 × 30 it shows a “terminal too small” notice instead of a broken layout.
Modes
| Mode | How you get there | What keys do |
|---|---|---|
NORMAL | default, Esc | keys are commands |
INSERT | editing a field in the hardware editor or the wizard | keys type text |
FILTER | / | keys type a live filter; Enter keeps it, Esc clears it |
COMMAND | : | keys type a command; Tab completes, Enter runs |
VISUAL | V | j/k extend a selection of domains |
CONFIRM | a destructive action | y confirms, n or Esc cancels |
Counts and repeat
A number before a motion repeats it: 5j moves five rows down. The single
digits 1 to 5 on their own switch views, after a short pause to see if
another digit follows. . repeats the last action that changed something.
The leader key
Space is the leader key. Less common actions live behind it so that single
letters stay free for the frequent ones. After you press Space, a popup in
the bottom-right corner lists what can follow:
Space nnew domain,Space cclone,Space rrename,Space MmigrateSpace s c/Space s r/Space s dsnapshot create, revert, deleteSpace m i/Space m einsert or eject an ISOSpace d a/Space d rattach or resize a diskSpace i a/Space i lattach a NIC or toggle its linkSpace xexport the XML,Space bedit the boot order
The same popup appears for other prefixes such as g and y.
Every action shows its command
virsh-tui builds each mutation as a plan of one or more virsh (or
virt-install) invocations and runs exactly that argument list, without a
shell. Before a destructive action, the confirmation dialog shows the
command. After any action, the message line shows it, and yc copies the last
one to the clipboard.
Some actions prefill the : line with the full virsh command instead of
running it, so you can adjust flags first. Disk and NIC attachment
(Space d a, Space i a) and migration (Space M) work this way.
Confirmations
D (destroy), R (reset), X (undefine), snapshot revert, volume delete and
volume wipe ask for confirmation. Undefine also asks you to type the domain
name. Each confirmation can be turned off in the settings (, then
Confirmations) or in the [confirmations] table of the
config file.
Marks and bulk actions
m marks the selected domain and V starts a visual selection. With marks in
place, lifecycle keys such as s, S or p apply to every marked domain,
and the confirmation lists them all. Esc leaves visual mode.
Dry run
Start with --dry-run to see what each action would run without executing
it. Messages are prefixed with [dry-run]. Queries still run, so the screens
show real data. --demo implies dry run and uses example data.
Views
virsh-tui has five top-level views. Press the number key to switch, from anywhere outside a text field.
| Key | View | What it shows |
|---|---|---|
1 | Domains | every domain, with live stats for the selected one |
2 | Host | the hypervisor host: CPU threads, memory, pools, networks |
3 | Networks | virtual networks, DHCP leases and attached interfaces |
4 | Storage | storage pools and volumes, ISO media |
5 | Events | the libvirt event stream |
Enter on a domain opens the domain detail, which has its own
tabs.
Host, Networks and Storage load in the background when virsh-tui starts and refresh each time you enter them, so the first screen appears straight away even on hosts with many domains and volumes.
Domains
The domains view is the start screen. The list on the left shows every domain
on the connection with its state, vCPUs, memory, a CPU bar, uptime and
autostart flag. The right-hand side follows the selection: an overview with
the OS, machine type, firmware, IP address, display and guest agent status,
then CPU, memory, disk and network graphs. On narrower terminals the graphs
move under the list, with tabs (Overview, CPU, Mem, Disk, Net) that [ and
] switch.
Below the list are the most recent libvirt events and a summary of the host: CPU, memory, swap, hugepages and the two largest storage pools.
Finding domains
| Key | Action |
|---|---|
j / k | move down / up |
gg / G | first / last domain |
Ctrl-d / Ctrl-u | half a page down / up |
/ | filter by name as you type; Enter keeps the filter, Esc clears it |
n / N | next / previous match |
f / F | cycle the state chips: all, running, paused, crashed, off |
o | cycle the sort order: state, name, CPU, memory, uptime |
:42 | jump to row 42 |
Lifecycle
| Key | Action | virsh command |
|---|---|---|
s | start | virsh start |
S | ACPI shutdown | virsh shutdown |
D | destroy (force off), confirms | virsh destroy |
r | reboot | virsh reboot |
R | reset (hard), confirms | virsh reset |
p | pause or resume | virsh suspend / virsh resume |
Z | save state to disk and stop | virsh managedsave |
a | toggle autostart | virsh autostart [--disable] |
X | undefine, asks you to type the name | virsh undefine |
After S, virsh-tui watches the domain. If it is still running after the
grace period, the message line says the guest ignored the request, and you
can retry or use D.
Console, viewer and XML
| Key | Action |
|---|---|
c | serial console (virsh console); Ctrl-] returns to virsh-tui |
v | graphical viewer (virt-viewer, or remote-viewer if configured) |
e | open the XML in your editor, then virsh define --validate it |
Space x | save the XML as ./<name>.xml in the current directory (never overwrites) |
Copying
| Key | Copies |
|---|---|
yy | domain name |
yu | UUID |
yi | first IP address |
yc | the last command virsh-tui ran |
Several domains at once
Mark domains with m, or press V and extend the selection with j/k.
Lifecycle keys then act on every marked domain. The confirmation dialog lists
them before anything runs.
Domain detail
Enter on a domain opens its detail view. H and L (or [ and ]) move
between the tabs; Backspace or q returns to the list.
Overview
The domain’s configuration at a glance: machine type, firmware, vCPUs, memory, disks, network interfaces, graphics, autostart, persistence and UUID.
Monitor
Live graphs for the running domain: total CPU and one meter per vCPU, memory
with balloon and RSS, read and write rates per disk, and receive and transmit
rates per interface. t cycles the history window between 60 seconds,
5 minutes and 1 hour. Samples are kept for an hour while virsh-tui runs, so
switching windows does not lose data.
The numbers come from virsh domstats, one call per refresh for all domains.
Balloon statistics need a guest driver and a stats period. virsh-tui leaves
the period alone unless you enable monitoring.balloon_on, because setting it
changes the running guest.
Hardware
The hardware editor lists the devices on the left (CPUs, memory, boot
options, disks, CD-ROMs, NICs, display, video, sound, input, TPM, USB
redirection) and the selected device’s form in the middle. The right column
shows the XML diff of your pending changes and, below it, the commands that
:w will run.
In NORMAL mode:
| Key | Action |
|---|---|
j / k | move between devices |
Tab / Shift-Tab | next / previous field of the form |
i | edit the focused field (INSERT mode) |
a | add a device of the selected kind: prefills :attach-disk, :attach-nic or :attach-hostdev |
d | detach the selected device, confirms |
J / K | move the selected device down / up in the boot order |
u | undo the changes to the focused field |
:w | apply the pending changes |
:q! | discard them |
gx | jump to the XML tab |
In INSERT mode, typing edits the field and Backspace deletes. On number and
size fields, Ctrl-a / Ctrl-x or l / h add or subtract 1, and L /
H add or subtract 10. Space toggles check boxes and radio options.
Enter or Tab moves to the next field, Esc goes back to NORMAL mode.
Changes are applied to the persistent configuration (--config), to the
running domain (--live), or both, depending on the “Apply to” boxes at the
bottom of each form. Fields that only take effect after a reboot are marked
with ⚠ needs reboot. The editor writes a new XML and runs
virsh define --validate, so libvirt rejects invalid combinations before
anything changes.
Snapshots
The snapshot tree, with the current snapshot marked.
| Key | Action |
|---|---|
j / k | select a snapshot |
Space s c | create a snapshot (name, description, flags) |
Space s r | revert to the selected snapshot, confirms |
Space s d | delete the selected snapshot, confirms |
e | edit the snapshot XML (virsh snapshot-edit) |
Console
Shortcuts for the serial console (c, which runs virsh console) and the
graphical viewer (v). Inside the console, Ctrl-] returns to virsh-tui.
The escape key can be changed with console.escape.
XML
The live XML with syntax highlighting. j/k, Ctrl-d/Ctrl-u and
gg/G scroll. e opens it in your editor; when you save and quit,
virsh-tui runs virsh define --validate and reopens the editor with the
error if libvirt rejects the file, as virsh edit does. Closing the editor
without changes defines nothing.
Host
The host view describes the machine libvirt runs on: CPU model and load, a meter per hardware thread, memory used and free, the resident memory of each domain, hugepages, KSM, swap, storage pools, networks and the virtualization features the CPU exposes.
The running domains are sorted by CPU use, busiest first.
| Key | Action |
|---|---|
j / k | select a running domain in the table |
gg / G | first / last domain |
Enter | open that domain’s detail |
Per-thread CPU meters, temperatures and memory details are read from /proc
and /sys, so they are only shown for local connections. For a remote URI
the view shows what libvirt itself reports (virsh nodeinfo, pools,
networks). Per-thread sampling can be turned off with
monitoring.thread_sampling = false.
Networks
The list of virtual networks with their state, autostart, bridge, forward mode and address range. The selected network’s DHCP leases appear next to it, joined with the static host entries and with the domain that owns each MAC address, followed by the interfaces attached to the network.
| Key | Action |
|---|---|
j / k | select a network |
l | move the focus to the leases table (and back) |
Enter on a lease | open the domain that owns it |
Space l | pin the selected lease as a static DHCP host entry |
Space L | remove the static host entry |
yi | copy the lease’s IP address |
s | start the network |
D | destroy (stop) the network, confirms |
a | toggle autostart |
e | edit the network XML in your editor |
n | create a network from a short form |
X | undefine the network, confirms |
Pinning a lease runs virsh net-update … add ip-dhcp-host on both the live
network and its persistent definition, so the guest keeps its address after a
restart.
Storage and media
Storage pools on the left, the selected pool’s volumes on the right. Each
volume shows its format, capacity, allocation, backing chain and the domains
that use it. h and l move the focus between the two lists.
Pools
| Key | Action |
|---|---|
s | start the pool |
D | destroy (stop) the pool, confirms |
b | build the pool (create its directory or format its device) |
r | refresh the volume list |
a | enable autostart |
Volumes
| Key | Action |
|---|---|
n | create a volume (name, size, format) |
R | resize: prefills :vol-resize |
C | clone: prefills :vol-clone |
u | upload a local file into the volume: prefills :vol-upload |
W | wipe the volume with zeros, confirms |
X | delete the volume, confirms |
Inserting and ejecting ISO media
Space m i opens a picker with the ISO images found in your pools. Choosing
one inserts it into the domain’s CD-ROM drive with
virsh change-media … --insert, on the running domain and its configuration.
If the domain has no CD-ROM drive, virsh-tui offers to attach one.
Space m e ejects the current medium. From the : line,
:attach-iso <domain> <path> does the same with a path you type, with Tab
completion for the file name.
Events
The events view subscribes to virsh event, virsh net-event and
virsh pool-event and shows what happens on the connection as it happens:
domains starting, stopping, crashing, being defined or undefined, guest agent
connections, balloon changes, media changes, network and pool state changes.
| Key | Action |
|---|---|
j / k | move through the log |
f / F | filter by type: all, lifecycle, devices, jobs, network, storage, app |
/ | search the log |
Enter | jump to the object of the event |
The stream reconnects on its own if the daemon restarts. The last few events also appear on the domains dashboard.
Creating a domain
Space n opens the new domain wizard. It collects what virt-install needs
in six steps and shows the resulting command at the bottom, highlighting the
part that belongs to the current step. The command shown is the one that
runs.
| Step | Fields |
|---|---|
| 1. Name & OS | name, OS variant (osinfo), title, description |
| 2. Install media | local ISO, URL install tree with kernel arguments, PXE, import an existing disk, or none |
| 3. CPU & memory | vCPUs and maximum, CPU model, memory and maximum, balloon, hugepages |
| 4. Storage | a new disk (pool, size, format, bus) or an existing image |
| 5. Network | a virtual network (or none), NIC model, fixed or automatic MAC |
| 6. Review & create | firmware (UEFI or BIOS), secure boot, TPM 2.0, graphics, start after creation, open viewer, autostart |
The left column lists the steps with a one-line summary each and a checklist of what still blocks creation, such as a missing name or ISO path.
Keys
| Key | Action |
|---|---|
Tab, ↓, Ctrl-j | next field |
Shift-Tab, ↑, Ctrl-k | previous field |
| letters, digits | type into text and number fields |
Backspace | delete the last character |
Ctrl-w | clear the field |
h / l, ← / → | change a choice, or adjust a number by 1 |
H / L | adjust a number by 10 |
Space | toggle a check box or cycle a choice |
Ctrl-n / Ctrl-p | next / previous step |
Enter | next step; on the last step, create the domain |
Ctrl-y | copy the virt-install command |
Ctrl-e | edit the generated XML in your editor, then define it |
Ctrl-r | reset the wizard to its defaults |
Esc | close; the draft is kept |
In text fields every printable key types, including h, l and y.
Defaults and the OS variant
A new wizard starts with 2 vCPUs (maximum 4), 4 GiB of memory (maximum 8 GiB), a 20 GiB qcow2 virtio disk in the first active pool, a virtio NIC on the first active network, UEFI firmware and SPICE graphics.
Leave the OS field empty and virsh-tui guesses it from the ISO file name
(Fedora, Arch Linux, Ubuntu and Debian are recognised). Otherwise it passes
--osinfo detect=on,require=off and lets virt-install detect it from the
media. virt-install --osinfo list shows the accepted names.
Creating
Enter on the last step checks the form. If something is missing, the
wizard jumps to the step that needs it and says what is wrong. Otherwise it
runs virt-install … --noautoconsole against the current connection.
- With “start after creation” off, virsh-tui asks
virt-installfor the XML (--print-xml) and defines the domain without booting it. - With “open viewer” on, the graphical viewer opens once the domain starts.
Errors from virt-install are shown in the wizard, so you can fix the field
and try again.
Drafts
The wizard saves a draft on every change and when you close it. The next
Space n restores it. A successful creation deletes the draft; Ctrl-r
starts over. The draft lives in ~/.local/state/virsh-tui/wizard-draft.toml.
The command line
: opens the command line at the bottom of the screen. It accepts
virsh-tui’s own commands and any virsh subcommand.
Completion
Tab completes the word under the cursor and opens a list of candidates;
Tab and Shift-Tab move through it, Enter runs the line. Completion
knows about:
- virsh-tui commands and every
virshsubcommand, with a one-line summary - domain names, with their state, after commands that take a domain
- network names after
net-…commands, pool names afterpool-…commands - theme names after
:theme, with a colour preview - connection URIs after
:connect, setting names after:set --flagsof anyvirshsubcommand, read fromvirsh help <command>- file paths for words that start with
/,~/or./
virsh-tui commands
| Command | Action |
|---|---|
:start <domain> | start a domain |
:shutdown <domain> | ACPI shutdown |
:destroy <domain> | force off (confirms) |
:rename <domain> <new-name> | rename (the domain must be shut off) |
:desc <domain> <text> | set the description |
:attach-disk <domain> <source> <target> | attach a disk |
:detach-disk <domain> <target> | detach a disk |
:attach-nic <domain> <source> <model> | attach a NIC to a network |
:block-resize <domain> <target> <size> | grow a disk |
:attach-iso <domain> [target] <iso> | insert an ISO (alias :media) |
:attach-hostdev <domain> <nodedev> | pass a host device through |
:connect <uri> | switch connection |
:theme <name> | switch theme (saved to the config) |
:sort name|state|cpu|mem|uptime | sort the domain list |
:set <key>=<value> | change a setting and save it |
:42 | jump to row 42 |
:messages | show the message history |
:help [topic] | open the help |
:w | apply hardware changes |
:q | close the current screen, or quit from the dashboard |
:q! | discard hardware changes |
:qa | quit |
Running virsh directly
Anything that is not a virsh-tui command is passed to virsh as is, against
the current connection:
:dominfo arch-dev
:setmem arch-dev 8G --live
:snapshot-create-as arch-dev pre-upgrade --atomic
:!virsh net-dhcp-leases default
The line is split like a shell would split it (quotes work), but no shell
runs, so pipes, redirections and variables have no effect. The connection is
set with :connect, not with -c. Destructive subcommands such as
destroy, undefine or vol-delete ask for confirmation first. The first
line of the output appears in the message line and the rest in
:messages.
When you type a command with its own arguments, such as
:attach-disk vm /path/disk.qcow2 vdb --driver qemu --config, virsh-tui
passes it to virsh unchanged instead of using its shorter built-in form.
The command palette
Ctrl-p opens a fuzzy finder over every action virsh-tui has, with its key
binding and the domain it applies to. Type a few letters (snap, iso,
setmem) and press Enter to run the highlighted entry.
For entries backed by a virsh subcommand, the right-hand pane shows its
synopsis and its options as check boxes, so you can add flags such as
--live or --atomic without remembering their names.
| Key | Action |
|---|---|
| type | filter the entries |
↑ / ↓, Ctrl-k / Ctrl-j | move through the entries, or through the options when the pane has focus |
Ctrl-l | move the focus between the entries and the options pane |
Tab | toggle the option under the cursor |
Ctrl-y | copy the resulting virsh command |
Enter | run the entry |
Esc | close |
Key bindings
? shows the bindings inside virsh-tui. The overlay is built from the active
key map, so it reflects your keymap.toml. Type to filter it.
Default bindings
Navigation
| Key | Action | Name |
|---|---|---|
j / k | down / up | move_down / move_up |
gg / G | top / bottom | go_top / go_bottom |
Ctrl-d / Ctrl-u | half page down / up | half_down / half_up |
Enter | open | open |
Backspace, q | back (q quits from the top level) | back |
/ | filter | filter |
n / N | next / previous match | next_match / prev_match |
H / L, [ / ] | previous / next tab | prev_tab / next_tab |
1 … 5 | Domains, Host, Networks, Storage, Events | view_domains … view_events |
Domain actions
| Key | Action | Name |
|---|---|---|
s | start | start |
S | ACPI shutdown | shutdown |
D | destroy (force off) | destroy |
r | reboot | reboot |
R | reset | reset |
p | pause / resume | pause |
Z | managed save (suspend to disk) | managed_save |
a | toggle autostart | autostart |
c | serial console | console |
v | graphical viewer | viewer |
e | edit XML | edit_xml |
X | undefine | undefine |
Copy
| Key | Copies | Name |
|---|---|---|
yy | name | yank_name |
yu | UUID | yank_uuid |
yi | IP address | yank_ip |
yc | last command | yank_cmd |
Leader (Space)
| Key | Action | Name |
|---|---|---|
Space n | new domain | new_domain |
Space c | clone | clone |
Space r | rename | rename |
Space M | migrate | migrate |
Space s c / s r / s d | snapshot create / revert / delete | snapshot_new / snapshot_revert / snapshot_delete |
Space m i / m e | insert / eject ISO | insert_media / eject_media |
Space d a / d r | attach / resize disk | disk_attach / disk_resize |
Space i a / i l | attach NIC / toggle link | nic_attach / nic_link |
Space x | export XML | export_xml |
Space b | boot order | boot_order |
Space l / Space L | pin / unpin a DHCP lease | lease_pin / lease_unpin |
Lists and monitor
| Key | Action | Name |
|---|---|---|
m | mark / unmark | mark |
V | visual selection | visual |
o | cycle sort | sort_cycle |
f / F | cycle filter chips | chip_cycle / chip_cycle_back |
t | history window 60 s / 5 min / 1 h | window_cycle |
l | focus the leases table (Networks) | lease_focus |
u | undo field (hardware editor) | undo_field |
gx | go to the XML tab | goto_xml |
App
| Key | Action | Name |
|---|---|---|
: | command line | ex |
Ctrl-p | command palette | palette |
? | help | help |
, | settings | settings |
Ctrl-r | refresh now | refresh |
Ctrl-l | redraw the screen | redraw |
. | repeat the last change | |
q, ZZ, Ctrl-c | quit | quit |
Some screens add their own keys, listed in the status bar: the hardware editor, the wizard, Networks and Storage.
Remapping keys
Create ~/.config/virsh-tui/keymap.toml:
[normal]
shutdown = ["S", "Space q"] # several bindings for one action
snapshot_new = ["Space s n"] # tokens separated by spaces
console = ["C-t"]
- Each entry maps an action name (the “Name” column above) to a list of key sequences.
- A sequence is a list of tokens separated by spaces. A token is a single
character (
a,A,:),C-xfor Ctrl, or one ofSpace,Enter,Esc,Tab,S-Tab,Backspace. - Remapping an action removes its default keys. List them again if you want to keep them.
- Sub-tables such as
[normal.domains]are accepted to group entries, but their bindings apply in every view.
Unknown action names and syntax errors are reported in the message line when virsh-tui starts; the rest of the file still applies. The help overlay and the which-key popup show your bindings.
All action names: move_up, move_down, go_top, go_bottom, half_down,
half_up, open, back, filter, next_match, prev_match, start,
shutdown, destroy, reboot, reset, pause, managed_save,
managed_save_remove, autostart, console, viewer, edit_xml,
undefine, rename, clone, migrate, send_key, export_xml,
new_domain, yank_name, yank_uuid, yank_ip, yank_cmd, mark,
visual, sort_cycle, chip_cycle, chip_cycle_back, window_cycle,
lease_focus, lease_pin, lease_unpin, ex, palette, help,
settings, refresh, redraw, quit, quit_all, snapshot_new,
snapshot_revert, snapshot_delete, insert_media, eject_media,
disk_attach, disk_resize, nic_attach, nic_link, boot_order,
prev_tab, next_tab, undo_field, goto_xml, view_domains,
view_host, view_networks, view_storage, view_events.
Configuration
virsh-tui reads ~/.config/virsh-tui/config.toml (or $XDG_CONFIG_HOME).
The file is optional; every key has a default. The settings screen (,) and
:set key=value write changes back to the same file, so you rarely need to
edit it by hand. --config <path> uses another file, and changes made in
the app are saved there.
Full example with the defaults
[general]
default_uri = "qemu:///system" # used when -c/--connect is not given
refresh_interval_secs = 1 # how often stats are sampled
editor = "vi" # used when $VISUAL and $EDITOR are unset
start_view = "domains" # domains, host, networks, storage, events
confirm_style = "yn" # reserved, not used yet
[appearance]
theme = "tokyo-night" # built-in name or a file in themes/
transparent = false # use the terminal background
borders = "rounded" # rounded, plain, double, thick
icons = "unicode" # unicode, nerd, ascii
graphs = "braille" # braille, block, tty
gradient = true # colour bars by level (green, yellow, red)
dim_modals = true # dim the screen behind dialogs
[connections]
uris = [ # offered by :connect completion and Settings
"qemu:///system",
"qemu:///session",
"qemu+ssh://user@host/system",
]
default = "qemu:///system"
[monitoring]
balloon_on = false # set a balloon stats period on running guests
balloon_period_secs = 2
thread_sampling = true # per-thread CPU meters in the Host view
[confirmations]
destroy = true
reset = true
undefine = true
revert = true # snapshot revert
delete_volume = true
wipe = true
type_name_for_undefine = true # type the domain name to undefine
[console]
viewer = "virt-viewer" # or "remote-viewer"
escape = "C-]" # key that leaves the serial console
Notes on some settings
monitoring.balloon_on is off by default because it runs
virsh dommemstat --period N --live on running domains, which changes their
configuration. Without it, the memory graphs show what libvirt reports
without a stats period: the balloon size and RSS, but not the guest’s own
view of free memory.
console.viewer = "remote-viewer" makes v ask libvirt for the display URI
(virsh domdisplay) and open it with remote-viewer. Any other value uses
virt-viewer -c <uri> <domain>.
console.escape accepts C-x or ^x notation and is passed to
virsh console as -e ^x.
The editor is chosen in this order: $VISUAL, $EDITOR,
general.editor, then vi. Values with arguments work, for example
code --wait.
Settings screen
, opens the settings. The categories on the left are General, Appearance,
Keybindings, Connections, Monitoring, Confirmations, and Console & viewer.
j and k move through the options; in the theme list they also preview the
theme live. Enter applies and saves, h and l switch panes, Esc
reverts unsaved previews and closes.
:set
:set changes one setting and saves the file:
:set theme=nord
:set graphs=block
:set refresh_interval_secs=2
:set default_uri=qemu:///session
Supported keys: theme, transparent, borders, icons, graphs,
gradient, dim_modals, default_uri, editor, start_view, viewer,
escape, refresh_interval_secs. Tab completes them.
Themes
virsh-tui ships nine themes. The default is tokyo-night.
| Theme | |
|---|---|
tokyo-night | the default, shown throughout this book |
tokyo-night-storm | a lighter navy background |
tokyo-night-day | light |
catppuccin-mocha | |
gruvbox-dark | |
nord | |
dracula | |
kanagawa | |
everforest |
Switch with :theme <name> (Tab completes the names and previews the
colours), in the settings screen (,, Appearance), with --theme <name> for
one session, or with appearance.theme in the config.
catppuccin-mocha | gruvbox-dark |
nord | dracula |
tokyo-night-day |
Custom themes
A theme is a TOML file in ~/.config/virsh-tui/themes/. The file name,
without .toml, is the theme name. It needs all sixteen colour tokens:
# ~/.config/virsh-tui/themes/mine.toml
[tokens]
bg = "#1a1b26" # main background
bg2 = "#16161e" # bars and panel headers
hl = "#292e42" # highlighted rows, chips, empty bar segments
sel = "#283457" # selected row
gutter = "#3b4261" # borders
comment = "#565f89" # dim text
fg = "#c0caf5" # text
fg2 = "#a9b1d6" # secondary text
blue = "#7aa2f7"
cyan = "#7dcfff"
magenta = "#bb9af7"
green = "#9ece6a"
yellow = "#e0af68"
orange = "#ff9e64"
red = "#f7768e"
teal = "#1abc9c"
Then :theme mine. A missing token is reported by name in the message line.
Border style, icon set, graph style, gradients and transparency are separate
settings in [appearance], so they work with any theme.
Terminals without truecolor
virsh-tui checks COLORTERM. Without truecolor or 24bit, it maps the
theme to the nearest xterm 256-colour palette entries.
Command-line options
virsh-tui [OPTIONS]
| Option | Description |
|---|---|
-c, --connect <URI> | libvirt connection URI, e.g. qemu:///system. Defaults to general.default_uri |
--demo | use the in-memory demo backend; nothing is executed |
--dry-run | show the commands that actions would run, without running them |
--theme <NAME> | theme for this session (built-in or custom), overriding the config |
--config <PATH> | use this config file instead of ~/.config/virsh-tui/config.toml |
--screen <NAME> | open on a given screen (see below) |
--probe | print the parsed domains, host, networks and pools as JSON and exit (read-only) |
--dump <FILE> | render one frame to an ANSI file and exit |
--size <WxH> | frame size for --dump, default 174x43 |
--frozen | freeze the demo clock and fill the graph history (for tests and screenshots) |
-h, --help | print help |
-V, --version | print the version |
--screen accepts dashboard, host, networks, storage, events,
detail, monitor, hardware, snapshots, wizard, palette, help
and settings. Without --dump it only changes the start view for the five
top-level views.
Examples
# Manage the per-user daemon
virsh-tui -c qemu:///session
# A remote host over SSH
virsh-tui -c qemu+ssh://admin@hv01/system
# See what the keys would do, without changing anything
virsh-tui --dry-run
# Check what virsh-tui parses from your host
virsh-tui --probe | jq '.domains[].name'
# Render a screenshot-ready frame of the demo
virsh-tui --demo --frozen --screen host --dump host.ans --size 160x45
Files and paths
virsh-tui follows the XDG base directory layout.
| Path | Contents |
|---|---|
~/.config/virsh-tui/config.toml | settings (Configuration) |
~/.config/virsh-tui/keymap.toml | key overrides (Key bindings) |
~/.config/virsh-tui/themes/*.toml | custom themes (Themes) |
~/.local/state/virsh-tui/virsh-tui.log | log of the last session |
~/.local/state/virsh-tui/wizard-draft.toml | the new domain wizard’s draft |
$XDG_RUNTIME_DIR/vt-*.xml | temporary XML files, created with mode 0600 and removed after use |
$XDG_CONFIG_HOME and $XDG_STATE_HOME change the first two locations.
Temporary files fall back to the system temp directory when
$XDG_RUNTIME_DIR is not set.
The log is recreated at each start. RUST_LOG=virsh_tui=debug virsh-tui makes it
verbose; include it when you report a bug.
Troubleshooting
Most problems come from virsh itself. Run the same thing in a shell first:
virsh -c <uri> list --all. If that fails, virsh-tui fails the same way.
“virsh list failed” or an empty list
virsh not found in PATH: install the libvirt client package (see Installation).… (not in the libvirt group?): your user may not manageqemu:///system. Add it to thelibvirtgroup and log in again, or use-c qemu:///session.… (is libvirtd running?): start the daemon, for examplesudo systemctl enable --now libvirtd(or the modularvirtqemud).- The list is empty but
virsh list --allshows domains: check the URI in the top bar. Without-c, virsh-tui usesgeneral.default_uri, which may differ from yourLIBVIRT_DEFAULT_URI.
The top bar says “disconnected”
The connection dropped (daemon restart, SSH timeout). virsh-tui retries with
increasing delays and the bar recovers on its own. Ctrl-r forces an
immediate reload of the domain list.
Shutdown does nothing
S sends an ACPI request; the guest decides what to do with it. Guests
without ACPI support, guests still booting, and some installers ignore it.
virsh-tui reports when the domain is still running after the grace period.
Use D to force it off.
No IP address
Addresses come from the network’s DHCP leases first and from the guest agent
second. Domains on a bridge outside libvirt’s DHCP, or with a static address,
only show an IP when qemu-guest-agent runs in the guest.
Memory graphs show the balloon size only
The guest’s own view of used memory needs a balloon stats period. See
monitoring.balloon_on in Configuration.
The editor or console leaves the screen garbled
virsh-tui redraws the whole screen when an external program returns. If
something still looks wrong, Ctrl-l redraws.
The wizard fails to create a domain
The error from virt-install is shown under the form. Common causes:
--osinfo: the OS name is not in the osinfo database. Leave the field empty to letvirt-installdetect it, or pick a name fromvirt-install --osinfo list.- Permission denied on the pool directory: the user running libvirt cannot write to the pool. Check the pool’s path and permissions in the Storage view.
- UEFI firmware not found: install OVMF (
ovmf,edk2-ovmf) or choose BIOS in the last step.
Strange characters instead of lines and graphs
Your font lacks box-drawing or braille characters. Use a font that has them,
or set graphs = "block" (or "tty") and icons = "ascii" in
[appearance].
Colours look wrong
Set COLORTERM=truecolor if your terminal supports 24-bit colour but does not
advertise it. Inside tmux, also enable set -ga terminal-overrides ",*:Tc".
Reporting a bug
Include the version (virsh-tui -V), virsh --version, the URI type
(system, session, remote), what you pressed, and the log from
~/.local/state/virsh-tui/virsh-tui.log. Start with
RUST_LOG=virsh_tui=debug for a detailed log.
Safety model
virsh-tui manages virtual machines, so a wrong key can stop a server or delete a disk. These are the rules it follows.
Commands, not a private API
Every change is a plan of virsh or virt-install invocations. virsh-tui
runs exactly the argument list it shows you, with execve and no shell, so a
domain name such as a; rm -rf ~ stays a single argument. The same plan is
what --dry-run prints.
Confirmation for destructive actions
Destroy, reset, undefine, snapshot revert and delete, network and pool
destroy and undefine, and volume delete and wipe ask for confirmation. The
dialog shows the command. Undefine also asks you to type the domain name.
Bulk actions list every affected domain. The same applies to destructive
subcommands typed on the : line.
Your files and guests stay as they are
- XML edits go through
virsh define --validate, so libvirt checks them before anything changes. A rejected file reopens in your editor. - Exporting XML never overwrites an existing file.
- Settings that change running guests, such as the balloon stats period, are off until you turn them on.
Temporary files
XML handed to virsh define or attach-device is written to
$XDG_RUNTIME_DIR with a random name, created exclusively (O_EXCL) with
mode 0600, and removed afterwards.
Untrusted text
Domain names, descriptions, event details and command output come from libvirt and from guests. virsh-tui strips terminal control sequences from them before display, and escapes values it writes into XML.
Timeouts
Each virsh call has a time limit suited to the command (long for
migrations, block jobs and volume uploads, short for queries). A command that
runs over its limit is killed instead of left running in the background.
What virsh-tui does not do
It never uses sudo and never asks for passwords. It runs with your user’s
libvirt permissions, nothing more.
Contributing
Bug reports, ideas and pull requests are welcome on GitHub. For anything larger than a small fix, open an issue first so we can agree on the approach.
Building and testing
git clone https://github.com/SyscallBrain/virsh-tui
cd virsh-tui
cargo run -- --demo
Before sending a pull request, run the same checks as CI:
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test
The default tests need neither libvirt nor root. They parse recorded virsh
output from tests/fixtures/ and compare rendered screens with
insta snapshots in tests/snapshots/. When you change
the UI on purpose, review and accept the new snapshots with
cargo insta review.
Integration tests that talk to a real libvirt daemon are behind the it
feature. They only create, change and delete objects whose name starts with
vt-test-, on qemu:///session:
cargo test --features it
Project layout
| Path | Contents |
|---|---|
src/backend/ | virsh execution, output parsers, the event stream, the demo backend |
src/command/ | command plans, builders for each action, execution, temporary XML files |
src/app/ | the event loop, key dispatch per view, background loading |
src/ui/ | views, overlays (help, palette, wizard, confirmations) and widgets |
src/input/ | key engine, key map, the : parser, Tab completion |
src/theme/ | built-in themes, custom theme loading, the 256-colour fallback |
book/ | this documentation (mdBook) |
scripts/screenshots.py | regenerates the screenshots from demo mode |
docs/ | the original design notes and mockups |
Screenshots
The images in the README and this book are rendered from demo mode, so they never show real hosts:
cargo build --release
python3 scripts/screenshots.py
The script needs headless Chrome or Chromium and ImageMagick.
Documentation
The book lives in book/src/. Preview it with:
mdbook serve book --open
Code style
- Rust 2024 edition; the minimum supported Rust version is 1.88.
rustfmt.tomlsets a line width of 110.- No
unwrap()on data that comes fromvirshor from the user: parse errors become messages, not panics. - Every mutation goes through a
CommandPlan, so it can be shown, confirmed, dry-run and copied.