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

Configuration Files

Most setup happens in the app: you add TVs with the pairing wizard and edit profiles on the Profiles page, and TisVlay saves the result for you. This page is for the cases where you want the files themselves: a reproducible or headless setup, editing by hand, or looking up the exact schema.

TisVlay is configured with TOML, spread across a few files:

FileOwnerPurpose
/var/lib/tisvlay/daemon.tomlthe daemon (tisvlay user)TV definitions, discovery, and system profiles, the daemon’s own persisted store
~/.config/tisvlay/session.tomleach desktop useruser profiles and per-user TV profile assignments
~/.config/tisvlay/tray.tomleach desktop usertray notification preferences
~/.config/tisvlay/gui.tomleach desktop userthe window’s own preferences: couch mode and its scale

Each service rewrites its own file as state changes, so changes you make in the app, the tray, or the command line are persisted without any file editing. You can also edit a file by hand and reload it. The window’s gui.toml is the exception: it’s read when the window opens and written by the Settings page, and a reload leaves it alone. If it can’t be read when the window opens, it’s moved aside to gui.toml.edited and the window starts with its defaults; a scale outside the Scale control’s range is brought back into it.

To declare TVs declaratively (for a reproducible or headless setup), give the daemon a seed file with tisvlay service daemon --seed <file>; on NixOS, the module’s settings option writes it for you. The seed is re-applied on every start, but only for TVs whose id is absent from daemon.toml, so runtime edits stick, and a still-declared TV that you removed at runtime comes back (drop the seed entry to drop it). Omit an entry’s id and it is derived deterministically from the entry’s content.

TV definitions

Each TV is a [[tv]] entry with a [tv.config] vendor block:

version = 1

[[tv]]
id = "9a3f1b22-c4d8-4e90-b21f-7f1a2c8e5b40"   # assigned at pair time (optional in a seed file)
name = "Office"
vendor = "lg"
primary_input = "HDMI_1"   # the input this PC uses (for input-aware automation)
managed = true             # participate in automation

[tv.profile]
system = "…"               # assigned system profile, by id (see `tisvlay profile system list`)

[tv.config]
ip = "192.168.1.170"
mac = "84:90:0a:63:a9:94"  # Wake-on-LAN; read from the TV at pair time if left out
# LG also stores, set automatically during pairing:
#   client_key = "…"   scheme = "wss"

A CEC TV uses the same shape with CEC keys instead:

[[tv]]
name = "Living Room"
vendor = "cec"

[tv.config]
device = "/dev/cec0"            # the kernel CEC node the TV is reachable on
physical_address = "1.0.0.0"    # the adapter's HDMI port (USB adapters only; ignored on GPU DP-CEC)
emulate_screen_via_power = true # honor screen_off/screen_on by powering the TV (CEC can't blank a live panel)
advertise_osd_name = true       # answer the TV's name poll, so it labels this PC's HDMI input
osd_name = "TisVlay"            # the label the TV shows for the input (14 characters max)

In a seed file the vendor block can also be written inline: config = { ip = "192.168.1.170", mac = "84:90:0a:63:a9:94" }.

With emulate_screen_via_power (default true), the daemon fulfills screen_off/screen_on on a CEC TV by powering it off/on, since CEC can’t blank a panel that stays on; set it false to have the TV ignore screen actions instead.

With advertise_osd_name (default true), tisvlay answers the TV’s name poll on the CEC bus, so the TV labels the PC’s HDMI input osd_name in its input picker, the way a Chromecast names its own input. Set it false to stay silent and keep the TV’s generic input label. If the input reads something else, such as “LG DVD Player”, the TV recorded another identity while TisVlay wasn’t driving the adapter (a Pulse-Eight can answer the TV on its own, for example after its USB cable was unplugged or the PC was switched off), and keeps it until it re-detects the device: unplugging and replugging the HDMI cable at the adapter, with its USB cable left in, clears it.

Discovery

Discovery has no section in daemon.toml. The SSDP source port is a daemon flag, --ssdp-port; Firewall & Discovery explains it and its firewall rule.

Profiles

A profile maps events to actions. There are two kinds: system profiles (in daemon.toml) for machine lifecycle and the login screen, and user profiles (in session.toml) for your desktop session. What each event means, how profiles inherit from one another, and which TVs they act on is explained in Automation; this section is the file schema.

Any field may be omitted from a profile, in which case it inherits (see How profiles resolve). Setting an event replaces its whole value, so re-gating an inherited action means restating the action too (the app does this for you).

Action vocabulary

Every event is one of five actions: none, screen_off, screen_on, power_off, and power_on; the Automation page says what each does. In a file, an event value is either the bare action token, or a table that adds modifiers:

on_session_unlock = { action = "screen_on", restore_input = true, while_streaming = false }
  • restore_input (default false): after an on-ish action completes, switch the TV to its primary_input. Needs a backend that can switch inputs (LG; not CEC).
  • while_streaming (default false): true lets the action fire even while a stream has the TV armed; see Streaming. The stock defaults below set it on the blanks and power-offs, so overriding one of those with a bare action drops it; write the table form to keep it.

System profile

Every field with its stock default (generated from the code, so it’s always current):

# Action on system startup/boot.
on_startup = { action = "power_on", restore_input = true }

# Action when the system powers off or halts.
on_shutdown = { action = "power_off", while_streaming = true }

# Action when the system reboots (including kexec).
on_reboot = { action = "none", while_streaming = true }

# Action on system sleep/suspend.
on_sleep = { action = "power_off", while_streaming = true }

# Action on system wake/resume.
on_wake = { action = "power_on", restore_input = true }

# Action when the matched host display connector blanks (DPMS off).
on_dpms_off = { action = "screen_off", while_streaming = true }

# Action when the matched host display connector wakes (DPMS on).
on_dpms_on = "screen_on"

# Greeter idle timeout in seconds (0 to disable). Drives the
# `tisvlay-greeter` idle client at the login screen.
idle_timeout_seconds = 0

# Action when the greeter goes idle (after `idle_timeout_seconds`).
on_idle_timeout = { action = "screen_off", while_streaming = true }

# Action when greeter activity resumes after idle.
on_idle_resume = "screen_on"

# When to apply automation relative to the TV's current input.
automate_when = "always"

# What the machine does when the TV powers off by itself or from its
# own remote: `suspend` suspends to RAM, `sleep` uses the host's
# configured sleep (which may hibernate). TisVlay's own power-offs
# never count, and nothing happens while a stream arms the TV.
# Default `none`.
on_tv_power_off = "none"

# What the machine does when the TV's remote powers this PC off (on
# HDMI-CEC, the TV's "power off this device"): `suspend` suspends to
# RAM, `sleep` uses the host's configured sleep. For a managed TV,
# whatever its input: the press is addressed to this PC. A toggling
# key counts only while the TV is showing this PC, since a TV
# switching to it may send one to wake it. Nothing happens while a
# stream arms the TV. Default `none`.
on_remote_power = "none"

# Whether the TV remote's keys reach this PC's desktop (on HDMI-CEC,
# as the kernel's remote-control input device). What each key does is
# the desktop's to decide. The packages' keymap keeps the remote's
# power keys away from the desktop; without it (a source build), the
# power key can put the PC to sleep. Default `false`.
remote_keys = false

# What the TV powering on does while this PC sleeps: `wake` lets it
# wake the PC (on HDMI-CEC, through a USB adapter allowed to wake the
# system, armed for every sleep), and `none` keeps the PC asleep (on
# HDMI-CEC, the adapter's USB port is disabled for the sleep, so the
# TV has no CEC device to show for this PC until it resumes). Where
# the TV comes up on another input, `wake` may still wake the PC. An
# unmanaged TV's adapter is left as the host has it. Default `none`.
on_tv_power_on = "none"

automate_when gates automation on the TV’s current input; see Which TVs react.

User profile

# Action on session lock event.
on_session_lock = { action = "screen_off", while_streaming = true }

# Action on session unlock event.
on_session_unlock = { action = "screen_on", restore_input = true }

# Action when user interacts with the lock screen greeter (before unlocking).
on_lock_wake = "screen_on"

# Idle timeout in seconds while the screen is locked (0 to
# disable) — how long a bumped-but-not-unlocked lock screen stays
# awake before `on_idle_timeout` re-blanks it.
locked_idle_timeout_seconds = 0

# Idle timeout in seconds while unlocked (0 to disable) — the
# in-session idle threshold for `on_idle_timeout`/`on_idle_resume`.
unlocked_idle_timeout_seconds = 0

# Action on idle timeout (either state's timer; each timer only
# fires in its own lock state).
on_idle_timeout = { action = "screen_off", while_streaming = true }

# Action when activity resumes after an unlocked idle timeout.
# Activity while locked dispatches `on_lock_wake` instead.
on_idle_resume = "screen_on"

# Action when a stream starts.
on_stream_start = "screen_off"

# Action when the last stream ends.
on_stream_end = "screen_off"

# Action on the first local input after a stream ends —
# wakes the panel that the disconnect blank left dark for the
# returning person.
on_local_return = { action = "screen_on", restore_input = true }

# Whether an unattributed screen-capture signal arms a stream.
# Attributed sources (streaming apps, hooks, logind) always
# arm; this decides only the bare screencast case (OBS, browser
# tab-share). Default off.
screencast_arms = false

# What the desktop does when the TV powers off by itself or from its
# own remote: `lock` locks this session. TisVlay's own power-offs
# never count, and nothing happens while a stream arms the TV.
# Default `none`.
on_tv_power_off = "none"

# What the desktop does when the TV's remote powers this PC off (on
# HDMI-CEC, the TV's "power off this device"): `lock` locks this
# session. For a managed TV, whatever its input: the press is
# addressed to this PC. A toggling key counts only while the TV is
# showing this PC, since a TV switching to it may send one to wake it.
# Nothing happens while a stream arms the TV. Default `none`.
on_remote_power = "none"

The two idle timers set how long to wait in each lock state; the shared on_idle_timeout/on_idle_resume actions decide what happens. A 0 timer disables idle for that lock state.

Hand-editing and reloading

Any of the three service files can be edited by hand while the services run. Then apply the edit with one of:

  • tisvlay reload, which reloads the daemon’s, the session’s and the tray’s files, one line per service (a service that isn’t running is reported, not failed).
  • The app’s Reload button, in the sidebar, which does the same for all three files.
  • systemctl reload tisvlay-daemon, which reloads the daemon’s file only. systemctl reports success as soon as the signal is sent, so the outcome is in the daemon’s journal.

How a reload treats your edit:

  • Only the edit applies. The service compares the file with what it last wrote and applies just the difference, so changes made through the CLI or GUI but not yet written survive. Where both changed the same field, the file wins.
  • A bad edit is refused whole, for example a missing or unparsable file, a repeated id, a changed or unknown vendor, renaming or removing the default profile, or deleting a profile a TV still uses. A parse error points at its file, line and column. A TV in the middle of pairing is left as it is.
  • Comments survive. The services’ own writes keep your comments and layout wherever the values beside them didn’t change.
  • Reload before the service writes again. An edit nobody reloaded is moved aside to <file>.edited (one slot, readable only by its owner) with a warning, never silently overwritten; redo the edit in the current file and reload.