Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-install command for new domains.
  • A : command line with Tab completion for commands, domain names, themes, paths and every virsh flag, 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

ToolPackage (Debian/Ubuntu, Fedora, Arch)Used for
virshlibvirt-clients, libvirt-client, libvirteverything (required)
virt-installvirtinst, virt-install, virt-installthe new domain wizard
virt-viewervirt-viewerthe graphical viewer (v)
ipiproute2host 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 the libvirt group: sudo usermod -aG libvirt "$USER", then log out and back in.
  • qemu:///session needs 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

  1. Move with j and k. The right-hand panel follows the selection.
  2. Press s to start a stopped domain. The message line at the bottom shows ✓ Started <name> ── virsh start <name>: the result and the command.
  3. Press S to 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.
  4. Press Enter to open the domain detail. Use H and L to move between Overview, Monitor, Hardware, Snapshots, Console and XML. Backspace or q goes back.
  5. Press / and type part of a name to filter the list. Esc clears the filter.
  6. Press Space and wait a moment. A popup lists everything you can do after the leader key, such as Space n for a new domain or Space s c for a snapshot.
  7. Press : and then Tab. Every command and virsh subcommand is listed. Try :theme followed by Tab to preview the themes.
  8. Press ? for the full key reference, and , for the settings.
  9. :q quits (or q from 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 disconnected and 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. :messages shows 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

ModeHow you get thereWhat keys do
NORMALdefault, Esckeys are commands
INSERTediting a field in the hardware editor or the wizardkeys type text
FILTER/keys type a live filter; Enter keeps it, Esc clears it
COMMAND:keys type a command; Tab completes, Enter runs
VISUALVj/k extend a selection of domains
CONFIRMa destructive actiony 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 n new domain, Space c clone, Space r rename, Space M migrate
  • Space s c / Space s r / Space s d snapshot create, revert, delete
  • Space m i / Space m e insert or eject an ISO
  • Space d a / Space d r attach or resize a disk
  • Space i a / Space i l attach a NIC or toggle its link
  • Space x export the XML, Space b edit 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.

KeyViewWhat it shows
1Domainsevery domain, with live stats for the selected one
2Hostthe hypervisor host: CPU threads, memory, pools, networks
3Networksvirtual networks, DHCP leases and attached interfaces
4Storagestorage pools and volumes, ISO media
5Eventsthe 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

KeyAction
j / kmove down / up
gg / Gfirst / last domain
Ctrl-d / Ctrl-uhalf a page down / up
/filter by name as you type; Enter keeps the filter, Esc clears it
n / Nnext / previous match
f / Fcycle the state chips: all, running, paused, crashed, off
ocycle the sort order: state, name, CPU, memory, uptime
:42jump to row 42

Lifecycle

KeyActionvirsh command
sstartvirsh start
SACPI shutdownvirsh shutdown
Ddestroy (force off), confirmsvirsh destroy
rrebootvirsh reboot
Rreset (hard), confirmsvirsh reset
ppause or resumevirsh suspend / virsh resume
Zsave state to disk and stopvirsh managedsave
atoggle autostartvirsh autostart [--disable]
Xundefine, asks you to type the namevirsh 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

KeyAction
cserial console (virsh console); Ctrl-] returns to virsh-tui
vgraphical viewer (virt-viewer, or remote-viewer if configured)
eopen the XML in your editor, then virsh define --validate it
Space xsave the XML as ./<name>.xml in the current directory (never overwrites)

Copying

KeyCopies
yydomain name
yuUUID
yifirst IP address
ycthe 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:

KeyAction
j / kmove between devices
Tab / Shift-Tabnext / previous field of the form
iedit the focused field (INSERT mode)
aadd a device of the selected kind: prefills :attach-disk, :attach-nic or :attach-hostdev
ddetach the selected device, confirms
J / Kmove the selected device down / up in the boot order
uundo the changes to the focused field
:wapply the pending changes
:q!discard them
gxjump 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.

KeyAction
j / kselect a snapshot
Space s ccreate a snapshot (name, description, flags)
Space s rrevert to the selected snapshot, confirms
Space s ddelete the selected snapshot, confirms
eedit 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.

KeyAction
j / kselect a running domain in the table
gg / Gfirst / last domain
Enteropen 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.

KeyAction
j / kselect a network
lmove the focus to the leases table (and back)
Enter on a leaseopen the domain that owns it
Space lpin the selected lease as a static DHCP host entry
Space Lremove the static host entry
yicopy the lease’s IP address
sstart the network
Ddestroy (stop) the network, confirms
atoggle autostart
eedit the network XML in your editor
ncreate a network from a short form
Xundefine 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

KeyAction
sstart the pool
Ddestroy (stop) the pool, confirms
bbuild the pool (create its directory or format its device)
rrefresh the volume list
aenable autostart

Volumes

KeyAction
ncreate a volume (name, size, format)
Rresize: prefills :vol-resize
Cclone: prefills :vol-clone
uupload a local file into the volume: prefills :vol-upload
Wwipe the volume with zeros, confirms
Xdelete 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.

KeyAction
j / kmove through the log
f / Ffilter by type: all, lifecycle, devices, jobs, network, storage, app
/search the log
Enterjump 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.

StepFields
1. Name & OSname, OS variant (osinfo), title, description
2. Install medialocal ISO, URL install tree with kernel arguments, PXE, import an existing disk, or none
3. CPU & memoryvCPUs and maximum, CPU model, memory and maximum, balloon, hugepages
4. Storagea new disk (pool, size, format, bus) or an existing image
5. Networka virtual network (or none), NIC model, fixed or automatic MAC
6. Review & createfirmware (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

KeyAction
Tab, ↓, Ctrl-jnext field
Shift-Tab, ↑, Ctrl-kprevious field
letters, digitstype into text and number fields
Backspacedelete the last character
Ctrl-wclear the field
h / l, ← / →change a choice, or adjust a number by 1
H / Ladjust a number by 10
Spacetoggle a check box or cycle a choice
Ctrl-n / Ctrl-pnext / previous step
Enternext step; on the last step, create the domain
Ctrl-ycopy the virt-install command
Ctrl-eedit the generated XML in your editor, then define it
Ctrl-rreset the wizard to its defaults
Escclose; 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-install for 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 virsh subcommand, with a one-line summary
  • domain names, with their state, after commands that take a domain
  • network names after net-… commands, pool names after pool-… commands
  • theme names after :theme, with a colour preview
  • connection URIs after :connect, setting names after :set
  • --flags of any virsh subcommand, read from virsh help <command>
  • file paths for words that start with /, ~/ or ./

virsh-tui commands

CommandAction
: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|uptimesort the domain list
:set <key>=<value>change a setting and save it
:42jump to row 42
:messagesshow the message history
:help [topic]open the help
:wapply hardware changes
:qclose the current screen, or quit from the dashboard
:q!discard hardware changes
:qaquit

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.

KeyAction
typefilter the entries
↑ / ↓, Ctrl-k / Ctrl-jmove through the entries, or through the options when the pane has focus
Ctrl-lmove the focus between the entries and the options pane
Tabtoggle the option under the cursor
Ctrl-ycopy the resulting virsh command
Enterrun the entry
Escclose

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

KeyActionName
j / kdown / upmove_down / move_up
gg / Gtop / bottomgo_top / go_bottom
Ctrl-d / Ctrl-uhalf page down / uphalf_down / half_up
Enteropenopen
Backspace, qback (q quits from the top level)back
/filterfilter
n / Nnext / previous matchnext_match / prev_match
H / L, [ / ]previous / next tabprev_tab / next_tab
1 … 5Domains, Host, Networks, Storage, Eventsview_domains … view_events

Domain actions

KeyActionName
sstartstart
SACPI shutdownshutdown
Ddestroy (force off)destroy
rrebootreboot
Rresetreset
ppause / resumepause
Zmanaged save (suspend to disk)managed_save
atoggle autostartautostart
cserial consoleconsole
vgraphical viewerviewer
eedit XMLedit_xml
Xundefineundefine

Copy

KeyCopiesName
yynameyank_name
yuUUIDyank_uuid
yiIP addressyank_ip
yclast commandyank_cmd

Leader (Space)

KeyActionName
Space nnew domainnew_domain
Space ccloneclone
Space rrenamerename
Space Mmigratemigrate
Space s c / s r / s dsnapshot create / revert / deletesnapshot_new / snapshot_revert / snapshot_delete
Space m i / m einsert / eject ISOinsert_media / eject_media
Space d a / d rattach / resize diskdisk_attach / disk_resize
Space i a / i lattach NIC / toggle linknic_attach / nic_link
Space xexport XMLexport_xml
Space bboot orderboot_order
Space l / Space Lpin / unpin a DHCP leaselease_pin / lease_unpin

Lists and monitor

KeyActionName
mmark / unmarkmark
Vvisual selectionvisual
ocycle sortsort_cycle
f / Fcycle filter chipschip_cycle / chip_cycle_back
thistory window 60 s / 5 min / 1 hwindow_cycle
lfocus the leases table (Networks)lease_focus
uundo field (hardware editor)undo_field
gxgo to the XML tabgoto_xml

App

KeyActionName
:command lineex
Ctrl-pcommand palettepalette
?helphelp
,settingssettings
Ctrl-rrefresh nowrefresh
Ctrl-lredraw the screenredraw
.repeat the last change
q, ZZ, Ctrl-cquitquit

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-x for Ctrl, or one of Space, 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-nightthe default, shown throughout this book
tokyo-night-storma lighter navy background
tokyo-night-daylight
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-mochagruvbox-dark
norddracula
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]
OptionDescription
-c, --connect <URI>libvirt connection URI, e.g. qemu:///system. Defaults to general.default_uri
--demouse the in-memory demo backend; nothing is executed
--dry-runshow 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)
--probeprint 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
--frozenfreeze the demo clock and fill the graph history (for tests and screenshots)
-h, --helpprint help
-V, --versionprint 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.

PathContents
~/.config/virsh-tui/config.tomlsettings (Configuration)
~/.config/virsh-tui/keymap.tomlkey overrides (Key bindings)
~/.config/virsh-tui/themes/*.tomlcustom themes (Themes)
~/.local/state/virsh-tui/virsh-tui.loglog of the last session
~/.local/state/virsh-tui/wizard-draft.tomlthe new domain wizard’s draft
$XDG_RUNTIME_DIR/vt-*.xmltemporary 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 manage qemu:///system. Add it to the libvirt group and log in again, or use -c qemu:///session.
  • … (is libvirtd running?): start the daemon, for example sudo systemctl enable --now libvirtd (or the modular virtqemud).
  • The list is empty but virsh list --all shows domains: check the URI in the top bar. Without -c, virsh-tui uses general.default_uri, which may differ from your LIBVIRT_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 let virt-install detect it, or pick a name from virt-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

PathContents
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.pyregenerates 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.toml sets a line width of 110.
  • No unwrap() on data that comes from virsh or 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.