This document describes the current LG Buddy architecture.
It is not a product roadmap. It is a map of what exists today and how the main pieces fit together.
For the top-level system, desktop, and service event paths that enter the runtime, see Runtime event handler map.
For the current application-owned presentation contract and GTK renderer boundary, see Frontend architecture.
The repository has one application/runtime crate, one GUI frontend crate, and one setup surface:
- Rust runtime workspace
Cargo.tomlcrates/lg-buddy/crates/lg-buddy-gui/
- shell-based setup surface
configure.shinstall.shuninstall.shbin/LG_Buddy_Commonsystemd/
The Rust application owns operational behavior and toolkit-neutral presentation state. The GUI crate uses a libadwaita application/window shell and GTK widgets to render that state. The remaining shell layer exists for configuration, installation, and removal.
The Rust crate is organized as a small core with explicit boundaries:
main.rs
-> lib.rs
-> parse CLI arguments
-> dispatch command
-> commands.rs
-> load config/state/dependencies
-> sources/
-> linux/logind.rs
-> linux/network_manager.rs
-> desktop/gnome.rs
-> desktop/swayidle.rs
-> events.rs
-> screen.rs
-> lifecycle.rs
-> policy.rs
-> runtime_phase.rs
-> tv.rs / wol.rs / state.rs
LG Buddy is organized as a semantic abstraction ladder. Each rung translates implementation-specific observations and outcomes into a smaller, stable semantic contract for the rung above. For example:
- G923 HID reports become gamepad control observations, then
UserActivity, then inactivity decisions and screen actions. - webOS messages and power states become TV operation outcomes, which screen and lifecycle policy use without knowing the underlying protocol.
Each rung owns the interpretation, validation, postcondition verification, and recovery that are fully scoped to its abstraction. It exports stable semantics, not its internal representation. Decisions that require broader product context remain with the higher-level policy layer.
This confines complexity rather than bubbling it upward. If every low-level detail reaches the top, policy must understand every device, provider, transport, and platform quirk, making the core progressively harder to reason about and change. Allowing each layer to operate at its own altitude limits how much of the system any one component must understand, localizes changes and tests, and lets new implementations satisfy existing contracts without teaching policy their mechanics.
This is the project-wide application of information hiding, Design by Contract, and separation of policy from mechanism. The session rule to unify providers semantically rather than mechanically is one instance of this principle.
The current runtime can be visualized as several consumer paths into the Rust runtime, and then one control path from policy code into the TV transport boundary.
The main runtime consumers are:
- system lifecycle and service integrations, including systemd, NetworkManager, and logind
- desktop environment and session integrations, including GNOME, native
Wayland,
swayidle, and Linux input activity sources - TTY users invoking the CLI directly
- the installed
lg-buddylauncher with no arguments, which opens normal Overview through the matching GTK executable - the
lg-buddy brightnesslauncher, which opens the matching GTK executable focused on brightness and uses Zenity only when that executable is absent - the
lg-buddy-guiGTK window, which renders Overview, TVs, pairing, and Settings from typed application state and sends semantic user intents through the in-process Rust API
flowchart LR
subgraph Desktop["Desktop Session / External Tools"]
GNOME["GNOME session bus<br/>ScreenSaver / Mutter signals"]
WAYLAND["Wayland compositor<br/>ext_idle_notifier_v1"]
SWAY["swayidle<br/>idle hooks"]
INPUT["Linux input devices<br/>gamepads / wheels / device events"]
FDO_NOTIFY["desktop notification service<br/>org.freedesktop.Notifications"]
end
subgraph SystemLifecycle["System Lifecycle"]
LOGIND["logind system bus<br/>PrepareForSleep / session LockedHint"]
NM["NetworkManager dispatcher<br/>pre-down"]
UPDATE_TIMER["systemd user timer<br/>background update checks"]
end
subgraph TTY["TTY / CLI"]
TERMINAL["terminal commands<br/>settings / brightness / volume / updates / manual actions"]
end
subgraph Frontend["Frontend"]
ZENITY["zenity brightness dialog<br/>interactive prompt"]
GTK["lg-buddy-gui<br/>Overview / TVs / Settings / dialogs"]
end
subgraph Rust["Rust Runtime"]
MAIN["main.rs / lib.rs<br/>CLI + command dispatch"]
COMMANDS["commands.rs<br/>CLI/API entrypoints"]
ACTIONS["session::actions<br/>action dependencies + TV client owner"]
EVENTS["events.rs<br/>canonical runtime events"]
POLICY["policy.rs<br/>action / no-action / state trail"]
NOTIFICATIONS["notifications.rs<br/>native desktop notifications"]
SESSIONNOTIFY["session_notifications.rs<br/>session D-Bus surface / update notifications"]
SCREEN["screen.rs<br/>session screen policy"]
LIFECYCLE["lifecycle.rs<br/>machine lifecycle policy"]
PHASE["runtime_phase.rs<br/>machine sleep phase provider"]
CONFIG["config.rs<br/>config.env parsing"]
STATE["state.rs<br/>runtime markers"]
APPLICATION["application.rs<br/>cross-view coordination"]
VIEWS["overview.rs / tvs.rs / settings_view.rs<br/>view state + typed operations"]
PRESENTATION["presentation/*<br/>typed content / controls / feedback"]
BRIGHTNESS["brightness.rs / audio.rs<br/>TV control operations"]
PAIRING["pairing.rs / pairing_store.rs<br/>native pairing + credential persistence"]
SETTINGS["settings/*<br/>registry / validation / persistence / apply"]
subgraph SessionSubsystem["Session Integration Subsystem"]
BACKEND["backend.rs<br/>backend selection"]
SESSIONMODEL["session.rs<br/>shared session model"]
RUNNER["session::runner<br/>monitor + lifecycle commands"]
GAMEPAD["session::gamepad<br/>gamepad activity source"]
BUS["session_bus.rs<br/>generic D-Bus transport"]
subgraph Sources["Source Adapters"]
LOGINDADAPTER["sources/linux/logind.rs<br/>lifecycle mapping + lock observer"]
NMGATE["sources/linux/network_manager.rs<br/>pre-down event source"]
GADAPTER["sources/desktop/gnome.rs<br/>GNOME bus + observation source"]
WADAPTER["sources/desktop/wayland.rs<br/>Wayland registry + observation source"]
SADAPTER["sources/desktop/swayidle.rs<br/>process fact source"]
end
end
subgraph ExternalInterfaces["External Interfaces"]
TV["tv.rs<br/>TvDevice / TvClient"]
WOL["wol.rs<br/>Wake-on-LAN"]
end
end
subgraph TVBoundary["TV Control Boundary"]
BSCPY["bscpylgtvcommand"]
WEBOS["native webOS session"]
LGTV["LG TV"]
end
MAIN --> BACKEND
MAIN --> RUNNER
RUNNER --> BACKEND
BACKEND --> GADAPTER
BACKEND --> WADAPTER
BACKEND --> SADAPTER
RUNNER -->|"starts"| GADAPTER
RUNNER -->|"starts"| WADAPTER
RUNNER -->|"starts"| LOGINDADAPTER
GNOME --> GADAPTER
GADAPTER --> BUS
GADAPTER -->|"SessionObservation"| RUNNER
WAYLAND --> WADAPTER
WADAPTER -->|"SessionObservation"| RUNNER
LOGIND --> LOGINDADAPTER
LOGINDADAPTER --> BUS
LOGINDADAPTER -->|"lifecycle RuntimeEvent"| EVENTS
LOGINDADAPTER -->|"lock SessionObservation"| RUNNER
NM --> MAIN
TERMINAL --> MAIN
MAIN -->|"normal / brightness launcher"| GTK
MAIN -.->|"brightness only; GUI absent"| ZENITY
ZENITY --> MAIN
GTK -->|"semantic intents / worker completions"| APPLICATION
APPLICATION --> VIEWS
VIEWS --> PRESENTATION
PRESENTATION -->|"render"| GTK
VIEWS --> BRIGHTNESS
VIEWS --> PAIRING
VIEWS --> SETTINGS
VIEWS --> CONFIG
VIEWS --> TV
SETTINGS --> CONFIG
PAIRING --> CONFIG
PAIRING --> WEBOS
MAIN --> COMMANDS
COMMANDS --> SETTINGS
COMMANDS --> EVENTS
COMMANDS --> NMGATE
COMMANDS --> NOTIFICATIONS
COMMANDS --> SESSIONNOTIFY
COMMANDS -->|"screen / sleep"| ACTIONS
RUNNER -->|"retains"| ACTIONS
ACTIONS -->|"screen events"| SCREEN
ACTIONS -->|"sleep / resume"| LIFECYCLE
ACTIONS --> TV
COMMANDS --> LIFECYCLE
SCREEN --> POLICY
LIFECYCLE --> POLICY
SCREEN --> PHASE
NMGATE --> LIFECYCLE
RUNNER -->|"starts"| SADAPTER
SADAPTER --> SWAY
SWAY -->|"timeout / resume facts"| SADAPTER
SADAPTER -->|"SessionObservation"| RUNNER
INPUT --> GAMEPAD
GAMEPAD -->|"UserActivity"| RUNNER
NOTIFICATIONS --> FDO_NOTIFY
SESSIONNOTIFY --> FDO_NOTIFY
SESSIONNOTIFY --> BUS
RUNNER --> SESSIONNOTIFY
SESSIONMODEL --> RUNNER
COMMANDS --> CONFIG
COMMANDS --> STATE
BRIGHTNESS --> CONFIG
BRIGHTNESS --> NOTIFICATIONS
BRIGHTNESS --> TV
SCREEN --> STATE
LIFECYCLE --> STATE
SCREEN --> TV
LIFECYCLE --> TV
SCREEN --> WOL
LIFECYCLE --> WOL
TV -->|"tv.platform=bscpylgtv"| BSCPY --> LGTV
TV -->|"tv.platform=lg_webos"| WEBOS --> LGTV
WOL -->|"magic packet"| LGTV
The current split is:
lib.rs- public entry surface for the binary
- command parsing
- shared error types
commands.rs- CLI/API command entrypoints
- delegates screen and sleep actions to a short-lived runtime action owner
- config, state, and dependency loading for other command execution
- command output handoff
application.rs- toolkit-neutral coordination between Overview, TVs, and Settings
- cross-view refresh and operation availability after pairing, unpairing, or settings changes
overview.rs,tvs.rs, andsettings_view.rs- view state, semantic intents, typed operations, and completion handling
- retry decisions, progress, cancellation boundaries, and stale-completion rejection
brightness.rsandaudio.rs- brightness and audio read/write operations over shared TV/config adapters
- the brightness application contract and operation identities reused by Overview
pairing.rsandpairing_store.rs- native webOS pairing and profile/credential persistence with rollback
settings/*- the canonical settings registry, descriptions, validation, persistence, and runtime apply behavior shared by CLI and GUI
presentation/*- typed screen content, values, availability, progress, and user-facing errors
- brightness declarations reused inside the combined Overview presentation
crates/lg-buddy-gui/src/*- persistent native widgets, navigation, focus, accessibility, and dialogs
- workers that execute application operations and return typed completions
- no separate validation, persistence, retry policy, or TV workflow
events.rs- canonical runtime event envelope and source classification
policy.rs- explicit policy outcomes: selected actions, no-action decisions, diagnostics, and state-transition trail
notifications.rs- native desktop notification dispatch through
org.freedesktop.Notifications - passive notification delivery for brightness
- native desktop notification dispatch through
session_notifications.rs- LG Buddy-owned user-session D-Bus surface for update notification handoff
- session-owned update notification dispatch through
org.freedesktop.Notifications - update notification action handling for
View Releaseand automatic update-check opt-out - hosted by the user-session
monitorprocess
screen.rs- pure session screen blank and restore policy decisions over already-read observations
- edge glue that reads runtime phase and TV state, applies marker transitions, renders output, and dispatches TV/Wake-on-LAN effects
- session marker ownership rules
- screen restore policy and retry behavior for screen actions
lifecycle.rs- pure startup, shutdown, system sleep pre-action, NetworkManager sleep-gate, and system resume decisions over already-read observations
- edge glue that reads reboot state, TV state, and marker state, applies marker transitions, renders output, dispatches TV/Wake-on-LAN effects, and performs retry/backoff
- locked, idempotent pre-sleep attempt handling
- system marker ownership rules
runtime_phase.rs- source-agnostic machine sleep phase read used by screen policy
- Linux implementation reads logind
PreparingForSleep
config.rs- config path resolution
- parsing of the existing
config.envformat - typed values for HDMI input, backend, MAC address, and idle timeout
state.rs- runtime directory resolution
- system/session state separation
- ownership marker management
upgrade_preflight.rs- observes whether the current release-bundle installation can be replaced safely
- returns structured, actionable refusals without downloading or mutating anything
- provides separate installed-runtime and verified-candidate entrypoints
tv.rs- TV transport abstraction
- profile-bound
bscpylgtvcommandadapter - configured selection between the compatibility and native adapters
- adapter-neutral errors and selected-client construction
- typed facade for input, screen, power, brightness, and audio operations
web_os/adapter.rs- profile-bound native webOS adapter
- lazy authenticated session ownership, serialization, reuse, and invalidation
web_os/audio.rs- typed native SSAP volume and mute requests and responses
wol.rs- native Wake-on-LAN packet generation and UDP send
backend.rs- backend selection and detection
auto,gnome, nativewayland, and deprecatedswayidlecompatibility
session.rs- backend-neutral session event model
- normalized source-observation boundary
- top-level event consumption is mapped separately in runtime-event-handler-map.md
session/inactivity.rs- owns the configured inactivity deadline and fixed five-minute post-blank power-off deadline
- resets the deadline from normalized activity observations and blanks when it expires
- arms power-off only after confirmed blank success and cancels it on activity
- keeps blank and restore decisions edge-triggered instead of poll-triggered
session/gamepad/- discovers readable Linux gamepad-like input devices
- refreshes discovery from Linux input-device add, remove, and change events
- periodically reconciles the watched device set in case an event is missed
- maps raw controller events into activity observations
- hosts device-specific adapters for supplemental activity surfaces
- includes a Logitech G923 adapter for raw HID wheel and pedal reports that may not appear through evdev
- detailed in gamepad-subsystem.md
session_bus.rs- generic blocking D-Bus transport seam
- consumed by the GNOME and logind source adapters
session/runner.rs- backend-neutral monitor and lifecycle runners
- starts the user-session notification surface before screen backend work
- keeps the user-session process alive when idle blanking is disabled or a screen backend is temporarily unavailable
- combines backend observations with the inactivity engine
- dispatches semantic session events into screen and lifecycle policy
- starts source workers and multiplexes their normalized observations
session/actions.rs- shared dependency assembly for screen and sleep actions
- retains the native TV client across compatible events in each monitor
- reloads configuration and replaces clients when their target or operation policy changes
sources/linux/logind.rs- Linux system lifecycle and current-session lock-state adapter
- maps
org.freedesktop.login1resume signals into canonical lifecycle events - reads the
PreparingForSleepproperty used by the NetworkManager pre-down gate - owns the optional lock observer: system-bus connection, graphical-session
resolution, subscriptions, owner rebinding, and
LockedHinttranslation
sources/linux/network_manager.rs- NetworkManager
pre-downdispatcher source - emits
NetworkTeardownImminentwith the logind sleep-phase reading
- NetworkManager
sources/desktop/gnome.rs- owns GNOME session-bus setup, subscriptions, sender validation, Mutter polling, and translation into normalized observations
sources/desktop/wayland.rs- native Wayland capability probing and dynamic registry/seat ownership
- maps zero-timeout resumed notifications into desktop activity facts
sources/desktop/swayidle.rs- owns the production
swayidleprocess and translates timeout/resume callbacks into idle/activity facts
- owns the production
The session-facing pieces should be read as one subsystem:
backend.rs- selects the active session backend
session.rs- defines canonical session events and normalized source observations
session/inactivity.rs- owns session-phase synthesis and the configured inactivity deadline
session/gamepad/- supplies auxiliary user-activity observations for controller input
- owns gamepad device discovery, event-triggered refresh, and reconciliation
- see gamepad-subsystem.md for adapter and lifecycle details
session/runner.rs- owns shared session orchestration, including source selection, worker lifetime, multiplexing, and gamepad activity
- converts provider and auxiliary input into activity observations, resets the inactivity deadline, and dispatches source-classified runtime policy
- treats
screen_idle_blank=disabledas a passive user-session mode that preserves update notification handoff without TV idle blank/restore actions - consumes
swayidletimeout/resume facts through the same inactivity policy - owns the
lifecycleevent loop for system sleep/wake handling
sources/linux/logind.rs- adapts Linux system lifecycle signals and owns observation of an eligible
graphical session's
LockedHint
- adapts Linux system lifecycle signals and owns observation of an eligible
graphical session's
sources/desktop/gnome.rs,sources/desktop/wayland.rs, andsources/desktop/swayidle.rs- own their provider-specific connection or process mechanics and expose normalized observations to the runner
The intended public user-action surface is:
power onpower offbrightnessbrightness getbrightness set <0-100>volumevolume <0-100>volume upvolume downvolume mute [on|off]screen offscreen onsettings listsettings describe [KEY]settings get <KEY>settings set <KEY> <VALUE>settings unset <KEY>updates check [--notify]updates install
The installed application entrypoint is lg-buddy with no arguments. It
locates lg-buddy-gui beside the runtime and launches it with no arguments for
normal Overview. lg-buddy-gui with no arguments has the same normal Overview
behavior. lg-buddy brightness remains a brightness-focused deep link;
lg-buddy --help and lg-buddy help remain CLI help. These launch routes do
not change the operational CLI, service, or update paths listed above.
The binary also retains package-owned and compatibility entrypoints during the public-surface migration:
startup [auto|boot|wake]shutdownsleep-presleepnm-pre-downscreen-offscreen-onmonitorlifecycledetect-backendupdates background-check
lib.rs parses the command line into a typed command enum and dispatches into
the runtime command handlers in commands.rs and session/runner.rs.
Screen and sleep actions share dependency assembly in session/actions.rs:
monitors retain one action owner, while one-shot commands create a fresh owner.
The owner delegates decisions to the screen and lifecycle domain modules;
platform ingestion belongs to sources/. The on-demand
updates check command reads the saved updates.channel policy and consumes
the GitHub Releases API without entering the screen, lifecycle, or scheduling
paths. updates install adds the user-confirmed upgrade orchestration: initial
host preflight, fresh settings-driven discovery, target identity resolution,
explicit terminal confirmation, verified bundle acquisition, candidate
preflight, direct install.sh --upgrade execution, and installed identity
verification. The verified bundle and acquisition lock remain owned until the
installer and final verification finish. updates background-check is the
timer-owned wrapper: it exits before GitHub/cache work when
updates.auto_check is disabled and otherwise delegates to the same
settings-driven check path with notification intent enabled. When notification
is requested and an update is available, the one-shot CLI process hands the
resolved update facts to the LG Buddy-owned user-session D-Bus surface. The
running session process then owns desktop notification dispatch, notification
ids, the View Release action, and the notification opt-out action. The
opt-out action persists updates.auto_check=disabled through the settings API,
which also disables/stops the installed update-check timer. The update command
owns an operational cache under the user cache directory for GitHub ETag,
latest release metadata, and last-notified release state used by the observable
update notification policy; that cache is not user configuration and is not
part of the settings API.
The no-argument lg-buddy command locates lg-buddy-gui beside the running CLI
and launches its no-argument entrypoint for normal Overview. A missing GUI is an
error on this path. The brightness command locates the same executable and
launches its brightness entrypoint, which selects the brightness control even
when another view is already open. Only an absent GUI on this focused path
selects the temporary Zenity compatibility flow; an invalid installation or
failed GUI process is returned directly without a second prompt. The
brightness get and brightness set commands never enter either launcher and
use the TV picture abstraction in tv.rs for typed OLED brightness validation
and live TV read/write operations. The interactive Zenity brightness dialog
delegates its TV operations back through those direct CLI commands. The GTK
entrypoint opens one Overview alongside the primary TV summary, volume, and
mute. Two icon-and-slider rows submit changes as the sliders move; the sound
icon toggles mute. The core Overview application owns its declarations and semantic
intents; GTK renders them without adding TV or configuration policy. Workers
keep blocking operations off the GTK main loop. Capability state is independent,
and opaque operation identity prevents late results from replacing newer or
closed presentation state. Successful changes keep Overview open; brightness
writes retain the existing success notification. Passive native operations use
stored credentials without opening pairing prompts.
The TVs tab reads the existing primary profile and local credential state
through application-owned operations, then enriches the display name with a
separate optional model read from the TV. The application owns navigation and TV
selection; GTK supplies the native view switcher and adaptive layout. Zero TVs
produces a blank state, one TV opens directly to details, and only multi-profile
renderer fixtures expose the TV-selection sidebar. Production storage remains
limited to one primary profile. Tab changes retain pending Overview operations
and do not initiate TV writes or pairing.
The TVs application also declares immediate managed-input changes and confirmed
local unpairing. Input edits reuse the settings registry, persistence, and apply
strategy. Unpairing shares the pairing store’s lock and atomic config publication,
removes only primary-profile keys and the local native token, and restores that
token if config publication fails. Compatibility credential storage and unrelated
settings are retained. A started change disables profile controls and suspends
Overview operations; completion reloads Overview with new operation identities.
Active Overview writes must finish before profile changes can begin. GTK only
renders the input selection, confirmation, progress availability, and errors.
The zero-TV blank state offers first-TV pairing through a separate foreground
application workflow. GTK forwards the native webOS form and cancellation
intents, and worker progress describes connecting, TV confirmation, verification,
and saving. Validation, protocol authentication, capability checks, and credential
persistence remain in the core. Pairing is refused as root. The access token
stays in memory until verification succeeds; the primary profile is published
last, with credential rollback on a failed save. Accepted cancellation prevents
publication. Once saving begins, it finishes even if the window closes.
The toolkit-independent application coordinator opens the new TV details and
refreshes Overview with fresh operation identities after success. The application
backend selects the capability checks; the webOS client supplies authentication
and cancellable reads. GTK forwards unexpected worker termination to the core
as an internal failure. This does not install or activate services.
Settings reads the seven behavior settings from the shared registry, including
their descriptions, value choices, and validation. Toggle and choice changes
submit immediately; the numeric timeout commits on Enter or focus loss.
SettingsApplication serializes mutations through the same persistence and
runtime apply path as the CLI. Successful changes are silent. Validation and
persistence failures restore the prior value; an apply failure retains the
saved value and offers a retry of the runtime step. Missing or inactive services
are reported after the apply attempt, rather than monitored continuously.
Persistent native rows keep focus and layout stable across refreshes. About is
a native informational dialog reached from the main menu.
The volume family uses the TV audio abstraction for typed volume and mute
operations. Setting or stepping volume explicitly unmutes after the volume
operation; mute toggle reads the current state before writing its inverse.
This keeps CLI parsing separate from operational behavior.
release_bundle.rs turns a selected GitHub release into an owned, verified
candidate without invoking its installer. Acquisition refreshes the selected
tag directly from the fixed LG Buddy repository instead of trusting cached
asset metadata, resolves the tag to a bounded immutable commit, and requires
exactly one Linux-musl archive and one checksum asset.
Asset downloads use fixed GitHub API URLs, bounded bodies and deadlines, and an
explicit one-hop HTTPS release-asset redirect policy. Both assets must match
GitHub's SHA-256 digest and declared size; the archive digest must also match
the single corresponding entry in sha256sums.txt.
The process holds a nonblocking filesystem lock while staging under a private user-cache directory. It scans the complete archive before extraction, rejecting path aliases, traversal, links, special files, duplicate entries, unsafe modes, excessive sizes, and unexpected layout. The manifest must agree with the release, target, and resolved commit. The extracted ELF is never run: its build-generated, linker-retained identity record is parsed as data and must independently agree on version, channel, target, tag, and commit. The returned guard owns the verified candidate and removes its staging tree when dropped; no executable, installer, sudo, or configuration action runs in this boundary.
upgrade_preflight.rs checks observable host and installation state. It does
not infer upgrade support from the distribution name, a build flag, an install
receipt, or where the binary originally came from.
The initial preflight expects the running binary to be the mutable
/usr/bin/lg-buddy installation. It checks the conventional release-bundle
filesystem topology, ordinary file and directory types, ownership, writable
mounts, config-pointer discovery, readable configuration state, user and system
integrations, systemd manager availability, and the absence of legacy layouts
that would require migration. Each path is tied to the upgrade
operation that consumes it: file replacement, executable replacement,
directory mutation, read-only input, or exact drop-in replacement. Those
policies carry their ownership, permission, link, mount, and containment
invariants. Symlinks, mounted or multiply linked replacement targets,
untrusted writable system paths, unexpected systemd drop-ins, read-only
mutation targets, and special files in owned config state are refused.
After a bundle has been verified, its candidate binary can run the second preflight. That pass rechecks the installed state, proves it is executing the candidate from the supplied bundle root, and checks the candidate manifest, installer, runtime, GUI executable, desktop entry, and systemd assets before any privileged mutation. The first GUI upgrade permits the installed GUI to be absent; once present, it must satisfy the same safe executable-replacement policy as the runtime. Candidate inputs must be owner-usable and not writable by another user. The installer also runs both candidates' non-graphical version paths and requires exact release identity agreement before mutation. The external ancestor chain must remain root- or user-owned and cannot be shared-writable unless sticky-directory semantics protect its trusted child. Configuration and pairing scripts are deliberately excluded because the non-interactive upgrade mode preserves existing configuration and credentials without invoking them.
The installer then reads the existing platform choice and checks the legacy Python environment without mutating either. Native installations and healthy compatibility environments preserve that directory unchanged. Only an unhealthy compatibility environment triggers a second candidate preflight for recursive repair; that conditional pass also refuses unsafe virtualenv roots and nested mounts before the directory is cleared.
These checks are a conservative, evolving safety boundary, not an exhaustive host-support declaration or a promise that no later privileged operation can fail. New observable checks can be added as real installations expose unsafe conditions; callers only consume the structured compatibility result.
screen off is an idle policy action.
Flow:
- Load config.
- Resolve the session state marker path.
- For session-originated events, read the runtime sleep phase through
runtime_phase.rs. - If machine sleep is pending and lifecycle automation is enabled, record a no-action decision and do not touch the TV.
- Query the TV's current input.
- If the configured HDMI input is active:
- try to blank the screen
- if blanking fails, fall back to
power_off - create the ownership marker on success
- If another input is active:
- clear the marker
- do nothing to the TV
After a successful automatic blank, the inactivity engine starts a fixed
five-minute grace period. Activity cancels it before restore. At expiry,
screen.rs rechecks that automatic blanking is enabled, the session marker is
present, the configured input is still active, and machine lifecycle allows a
session action. It then attempts power_off once and preserves the marker for
later activity restore. Input mismatch clears ownership; input-query failure
skips without a blind power-off fallback. A monitor restart with an existing
marker starts a fresh grace period.
screen on is a resume policy action.
Flow:
- Load config.
- Resolve the session marker.
- For session-originated events, read the runtime sleep phase through
runtime_phase.rs. - If machine sleep is pending and lifecycle automation is enabled, record a no-action decision and do not touch the TV.
- Apply
screen_restore_policy:conservative: skip if the marker is missingaggressive: continue even without the marker
- Try the adapter-neutral screen-unblank operation.
- On failure, fall back to Wake-on-LAN plus repeated input-restore attempts.
- If input restore reports that the screen is not visible, unblank it and retry the input so the complete restore is verified.
- Clear the marker on success.
- Leave the marker in place if wake recovery fails.
startup handles both cold-boot and wake restoration behavior.
Flow:
- Load config.
- Resolve the system-scope marker.
- Decide behavior from
StartupModeandscreen_restore_policy:boot: always restorewake: restore only when policy allows itauto: treat marker presence as wake, otherwise boot
- Clear the marker before attempting restore.
- Send Wake-on-LAN.
- Retry
set_inputuntil the TV is reachable on the configured HDMI input or attempts are exhausted.
shutdown is a guard-rail policy action.
Flow:
- Load config.
- Ask
systemctl list-jobswhether a reboot is pending. - If reboot is pending, skip TV power-off.
- Otherwise query current input.
- If the configured HDMI input is active, issue
power_off. - If input query fails, still attempt
power_off. - Power-off failures are logged but do not abort shutdown handling.
lifecycle is the system sleep/wake event loop. Linux pre-sleep TV power-off is
owned by one cooperative suspend rail that accepts both logind
PrepareForSleep(true) and NetworkManager pre-down opportunities.
Flow:
- Load config and suppress lifecycle TV actions while
system_sleep_wake_policy=disabled. - Open the system bus.
- Subscribe to logind
PrepareForSleepsignals. - On
PrepareForSleep(true):- enter the central suspend rail under the logind delay inhibitor
- run one bounded pre-sleep TV decision unless another source already owns or completed the cycle
- On
PrepareForSleep(false):- run wake restore policy from the canonical logind resume event
- clear sleep-cycle coordination state
- If config is changed to disable lifecycle handling while the service is running, stop the lifecycle monitor cleanly.
The NetworkManager pre-down gate runs lg-buddy nm-pre-down. That command reads
logind PreparingForSleep; false or read failure returns quickly, true runs an
idempotent pre-sleep rail before NetworkManager tears down the interface. If
logind already owns the cycle, NetworkManager waits for a terminal rail outcome
or bounded timeout before releasing teardown.
Hidden detect-backend compatibility entrypoint
detect-backend resolves the desktop backend to use for existing package
callers. It is hidden from public help while those callers migrate to the
shared settings/backend presentation.
Selection order:
LG_BUDDY_SCREEN_BACKENDoverride if presentscreen_backendfrom config- default to
auto
Detection behavior:
autoprefers GNOME when the current session satisfies the full GNOME contract and the session bus is reachable- native
waylandvalidatesext_idle_notifier_v1version 2 or newer plus at least one advertised seat; explicit selection does not fall back autoprefers complete GNOME, then compatible native Wayland, then the deprecatedswayidlecompatibility backend when installed- other forced backends validate their required services or commands
The TV layer is intentionally split into two levels:
- low-level transport trait:
TvClient - higher-level domain facade:
TvDevice
TvClient models adapter-neutral operations for one configured TV profile. The
target address and implementation-specific credential context are bound when a
client is constructed; policy cannot redirect a client by passing an address to
an operation.
The current contract covers:
current_inputset_inputoled_brightnessset_oled_brightnessaudio_statusset_volumevolume_upvolume_downset_mutedpower_offblank_screenunblank_screen
TvDevice provides a more readable surface to policy code:
tv.input().current()tv.input().set(...)tv.screen().blank()tv.screen().unblank()tv.power().off()tv.power().wake(...)tv.picture().oled_brightness()tv.picture().set_oled_brightness(...)tv.audio().status()tv.audio().set_volume(...)tv.audio().volume_up()/tv.audio().volume_down()tv.audio().set_muted(...)
Successful effectful operations return no transport-specific output. Failures
are normalized into typed TV errors. Policy may react to adapter-neutral
outcomes such as the screen not being visible, but transport and platform state
remain inside the adapter. Wake-on-LAN keeps the configured network identity at
TvDevice; adapter operations do not accept targeting data.
tv.platform selects the production TV implementation. Fresh profiles select
the native Rust lg_webos implementation and verify pairing before the profile
is saved. Existing profiles retain their explicit choice; a missing platform
value continues to resolve to bscpylgtv and is materialized as that
compatibility choice when configuration is rewritten. bscpylgtvcommand
remains available as an explicit fallback.
The Rust runtime talks to it through BscpylgtvCommandClient, which:
- belongs to one configured TV address
- shells out to the configured command path
- keeps subprocess output and exit status inside the legacy adapter
- maps reads and failures into the shared domain contract
- privately verifies screen visibility after input restore and screen unblank
SelectedTvClient is the internal delegation point for the configured legacy
or native implementation. WebOsTvClient owns one lazily authenticated
websocket session behind a mutex, reuses it while healthy, and discards it after
transport or framing failure. Native effectful operations verify their own
postconditions before reporting success. After an ambiguous failure the adapter
may reconnect for safe read-only verification, but it never replays the
effectful operation. The legacy adapter performs its equivalent power-state
readback through bscpylgtvcommand; neither implementation exposes webOS power
states to policy code.
Each monitor's RuntimeActionExecutor retains this adapter across compatible
events. Client construction and connection remain lazy: starting a monitor
does not contact the TV. The owner reloads configuration for every action and
replaces its client when the profile path, TV address, MAC, platform, or client
options change. Unrelated settings changes preserve the connection. Client
options keep foreground pairing and timeouts separate from unattended suspend
and resume operations. Legacy clients are rebuilt per action, and one-shot
commands drop their owner on completion. A consumed or invalidated native
session reconnects on demand; there is no background connection maintenance.
If an input query invalidates a reused session, the adapter retries that read once on a fresh connection before returning a failure to policy. This prevents a socket closed between events from triggering the screen-off fallback without checking the TV's current input. Fresh-session failures return normally, and effectful operations are never replayed by this recovery path.
Native picture settings have two known service-invocation paths. A direct SSAP
write sends ssap://settings/setSystemSettings on the websocket. The Luna path
uses that same websocket to create and close a temporary notification alert;
the alert callback invokes
luna://com.webos.settingsservice/setSystemSettings inside the TV. Luna is
therefore not a second network transport.
LG Buddy uses only the alert-backed Luna path for brightness writes. It does not try direct SSAP first, select a path from detected firmware, or fall back between the two. Direct SSAP is rejected on affected firmware, while the Luna path is the one supported by both evidence-backed firmware profiles. The direct path remains represented in tests only so the mock can preserve the observed firmware difference.
Keeping native TV control within the Rust runtime removes the Python client from that selected operation path. This is useful groundwork for declarative or immutable distributions such as NixOS, but it is not yet first-class NixOS installation support. The shell installer still provisions the compatibility fallback and writes conventional mutable system locations; alternative install layouts are tracked in issue #24.
State is intentionally small.
The runtime currently uses two ownership markers:
screen_off_by_usin session scopescreen_off_by_usin system scope
The ownership markers answer one question:
- did LG Buddy blank or power off the TV as part of its own policy?
It does not answer whether restore should always be blocked.
In aggressive mode, restore may proceed even when the marker is absent.
When the system-scope marker exists after a sleep pre-action, session screen
actions defer to the lifecycle resume path while system_sleep_wake_policy is
enabled. The lifecycle path keeps that marker present while it waits for network
readiness and attempts input restore, then clears it after success or exhausted
restore attempts.
There are two scopes:
System- default path under
/run/lg_buddy
- default path under
Session- default path under
$XDG_RUNTIME_DIR/lg_buddy - fallback under
/run/user/<uid>/lg_buddy
- default path under
This is a direct replacement for the earlier ad hoc script coordination pattern.
The cooperative suspend rail uses system-scope lock and cycle state files to prevent concurrent pre-sleep handlers from racing each other. Repeated hooks are expected to be safe through idempotent TV policy and persisted terminal cycle outcomes.
Desktop backends are treated as adapters, not owners of policy.
The runtime core owns:
- config
- state
- TV control
- Wake-on-LAN
- retries and recovery behavior
- lifecycle decisions
Desktop source modules should only answer questions like:
- how is the selected provider connected or started?
- which native signals or activity facts are valid?
- how should those facts map into normalized observations?
session.rs defines the backend-neutral semantic contract:
- canonical session events
IdleActiveWakeRequestedUserActivityBeforeSleepAfterResumeLockUnlock
- a normalized observation carrying its source and observation time
The detailed session model is documented in docs/session-backend-model.md.
sources/desktop/gnome.rs is the native GNOME adapter. It currently provides:
- the GNOME session-bus connection and subscriptions
- ScreenSaver sender ownership validation and signal mapping
- Mutter user-active watches when honoring inhibitors, legacy idletime polling otherwise, and normalized activity observations
sources/desktop/wayland.rs is the native non-GNOME adapter. It owns the
Wayland connection, registry, every advertised seat, and zero-timeout idle
notifications. Resumed notifications become desktop activity observations in
the shared inactivity runtime; compositor idle does not directly blank the TV.
sources/desktop/swayidle.rs is the compatibility process adapter. Its timeout
callback publishes Idle; its resume callback publishes independent desktop
activity. The adapter does not invoke TV-facing commands or own screen policy.
The session subsystem is intentionally asymmetric where the providers are asymmetric:
- the current GNOME provider treats ScreenSaver active/wake and recent Mutter input as activity that resets LG Buddy's inactivity deadline; ScreenSaver idle is not a blanking authority
- the shared session runtime consumes gamepad activity directly from
Linux input devices as
AuxiliaryInput, independently of desktop providers; every enabled monitor backend uses this runtime - the same runtime opportunistically observes
LockedHinton the current graphical logind session; lock requests the normal session blank policy, unlock is informational, fresh independent activity can restore while locked after the fixed one-second post-lock grace, and logind owner changes trigger session rebinding and reconciliation; failure or lack of support does not affect the selected desktop backend - the gamepad source refreshes its device set from Linux device add, remove, and change events, with periodic reconciliation for missed events
swayidletimeout and resume callbacks feed the shared inactivity engine- system lifecycle is handled by the NetworkManager pre-down gate plus logind lifecycle service, while lock state is optional in the shared session runtime
swayidle remains an explicit and automatic compatibility fallback during the
1.x migration window, but emits a deprecation notice and is not offered by
fresh interactive configuration. Removal is planned for 2.0.0 after native
Wayland remains field-validated across supported compositors and unsupported
sessions have precise diagnostics.
The runtime is designed to be testable and relocatable.
Important environment overrides:
LG_BUDDY_CONFIG- explicit config file path
LG_BUDDY_SCREEN_BACKEND- force backend selection
LG_BUDDY_BSCPYLGTV_COMMAND- override TV command path
LG_BUDDY_GUI- override the matching
lg-buddy-guipath for relocation and tests
- override the matching
LG_BUDDY_SYSTEM_RUNTIME_DIR- override system state directory
LG_BUDDY_SESSION_RUNTIME_DIR- override session state directory
LG_BUDDY_SYSTEMCTL- override the
systemctlcommand path used by shutdown logic
- override the
These exist mainly so the runtime can be tested without mutating real system paths or depending on globally installed commands.
The test strategy has three layers:
- unit tests for parsing, state, backend selection, and policy
- subprocess-backed integration tests for TV behavior
- manual hardware probes when exact external behavior is unclear
TV-facing tests exercise the production protocol boundaries instead of relying only on in-memory fakes. The compatibility adapter uses a stateful subprocess mock, while the native adapter uses a centralized stateful webOS test server.
Relevant test assets:
tools/mock_bscpylgtvcommand.pycrates/lg-buddy/tests/support/mod.rscrates/lg-buddy/tests/mock_bscpylgtvcommand.rscrates/lg-buddy/src/web_os/test_support/test_server.rscrates/lg-buddy/src/web_os/observed_behavior.rscrates/lg-buddy/tests/features/webos.feature
The legacy mock preserves the command and response shapes observed from the installed client. Native behavior claimed as real is linked to hardware evidence and modeled by the centralized server; defensive protocol faults are identified separately. See Native webOS testing.
The Rust runtime currently owns:
- config loading
- state handling
- TV abstraction
- Wake-on-LAN
- backend detection
- startup
- shutdown
- system lifecycle handling through the cooperative logind/NetworkManager suspend rail plus logind resume monitor
- screen off
- screen on
- brightness control
- volume and mute control
- application coordination for Overview, TVs, native pairing, and Settings
- TV profile and credential persistence, including confirmed unpairing
- the shared settings registry, validation, persistence, and runtime apply path
monitorcommand with GNOME, native Wayland, andswayidlepaths
The shell layer still owns:
- interactive configuration
- installation
- uninstallation
What is still not implemented:
- additional desktop backends
- an immutable-distribution install layout that avoids conventional
/usrwrites
The no-argument launcher opens the installed application, while the shell layer remains the explicit setup, installation, and uninstallation surface for headless use. v1.7.0 extends the GUI with the complete first-run, service activation, update, and troubleshooting journey under issue #129. The application owns typed runtime/service and update state, including on-demand diagnostics; the GUI renders those states without inferring policy. A resolved screen backend display is not a GUI requirement, and the existing CLI paths remain available. The current architecture is a Rust-owned runtime and application with a thin GTK renderer and shell setup surface. See Frontend architecture for the current view and renderer boundaries.