feat: support user preview configuration and document UI

This commit is contained in:
2026-10-05 19:17:27 +05:30
parent ee6186ed0b
commit de8843471b
10 changed files with 210 additions and 80 deletions

View File

@@ -2,6 +2,12 @@
## Unreleased ## Unreleased
- Preview mode now loads `$XDG_CONFIG_HOME/latchd/config.toml` or
`~/.config/latchd/config.toml` when no `--config` path is set.
- Added `make install-user` to create a user configuration without replacing
an existing file.
- Added Docker PTY screenshots and a short UI guide.
- Fixed Wayland compositors launched through user services, including Niri, by - Fixed Wayland compositors launched through user services, including Niri, by
providing the selected session type, desktop, seat, and VT to PAM before providing the selected session type, desktop, seat, and VT to PAM before
`pam_open_session`. `pam_open_session`.

View File

@@ -9,6 +9,8 @@ PREFIX ?= /usr
DESTDIR ?= DESTDIR ?=
BINDIR := $(DESTDIR)$(PREFIX)/bin BINDIR := $(DESTDIR)$(PREFIX)/bin
CONFDIR := $(DESTDIR)/etc/$(PROJECT) CONFDIR := $(DESTDIR)/etc/$(PROJECT)
USER_CONFIG_HOME ?= $(if $(XDG_CONFIG_HOME),$(XDG_CONFIG_HOME),$(HOME)/.config)
USER_CONFDIR := $(USER_CONFIG_HOME)/$(PROJECT)
SYSTEMD_DIR := $(DESTDIR)/usr/lib/systemd/system SYSTEMD_DIR := $(DESTDIR)/usr/lib/systemd/system
BUILD_DIR ?= build BUILD_DIR ?= build
DIST_DIR ?= dist DIST_DIR ?= dist
@@ -49,7 +51,7 @@ endif
.PHONY: all deps build release-build test check fmt lint doc audit docker-test \ .PHONY: all deps build release-build test check fmt lint doc audit docker-test \
integration-test pam-fixture-test system-e2e system-e2e-strict \ integration-test pam-fixture-test system-e2e system-e2e-strict \
package release release-check version install uninstall clean run-preview \ package release release-check version install install-user uninstall clean run-preview \
legacy-build legacy-check enable update help legacy-build legacy-check enable update help
all: release-build ## Build the optimized release binary all: release-build ## Build the optimized release binary
@@ -147,6 +149,16 @@ install: build ## Install the binary, config, PAM policy, and systemd unit
@install -Dm644 extra/latchd.service '$(SYSTEMD_DIR)/$(PROJECT).service' @install -Dm644 extra/latchd.service '$(SYSTEMD_DIR)/$(PROJECT).service'
@install -d '$(CONFDIR)/wms' '$(CONFDIR)/wayland' @install -d '$(CONFDIR)/wms' '$(CONFDIR)/wayland'
install-user: ## Copy the example config to the current user's config directory
@test -n '$(USER_CONFIG_HOME)' || { echo 'HOME or XDG_CONFIG_HOME is required'; exit 1; }
@install -d -m700 '$(USER_CONFDIR)'
@if [ -e '$(USER_CONFDIR)/config.toml' ]; then \
printf 'configuration at %s was preserved\n' '$(USER_CONFDIR)/config.toml'; \
else \
install -m600 extra/config.toml '$(USER_CONFDIR)/config.toml'; \
printf 'created %s\n' '$(USER_CONFDIR)/config.toml'; \
fi
uninstall: ## Remove files installed by make install uninstall: ## Remove files installed by make install
@rm -f '$(BINDIR)/$(BINARY)' '$(DESTDIR)/etc/pam.d/$(PROJECT)' '$(SYSTEMD_DIR)/$(PROJECT).service' @rm -f '$(BINDIR)/$(BINARY)' '$(DESTDIR)/etc/pam.d/$(PROJECT)' '$(SYSTEMD_DIR)/$(PROJECT).service'
@printf '$(YELLOW)configuration at %s was preserved; remove it manually if no longer needed$(RESET)\n' '$(CONFDIR)' @printf '$(YELLOW)configuration at %s was preserved; remove it manually if no longer needed$(RESET)\n' '$(CONFDIR)'

155
README.md
View File

@@ -1,47 +1,50 @@
# latchd # latchd
Want a TUI-based login manager because you are tired of GDM or Plasma Login? Latchd gives you a small, keyboard-first login screen on a Linux virtual terminal. It uses PAM for authentication and can start a TTY shell, an X11 desktop, or a Wayland desktop. Want a TUI-based login manager because you are tired of GDM or Plasma Login? Latchd puts a small, keyboard-driven login screen on a Linux virtual terminal. It uses PAM for authentication and can start a TTY shell, an X11 desktop, or a Wayland desktop.
Latchd is written in Rust. The old Go prototype is kept in `legacy/go/` for reference and is not built or shipped. Latchd is written in Rust. The old Go prototype remains in `legacy/go/` for reference. It is not part of the release build.
## What it does ![Latchd login screen](docs/images/latchd-preview.png)
- Finds normal login accounts through NSS. The image above is the preview screen. It was captured from the Docker test image. See [`docs/ui.md`](docs/ui.md) for the keyboard reference and the second screenshot.
- Shows password, account-expiry, locked-account, and faillock status without replacing PAM's decision.
- Runs PAM password changes for accounts that require a new password before login.
- Lets you switch users and sessions without leaving the login screen.
- Remembers each account's last successful session choice in a root-owned state file.
- Finds X11 and Wayland desktop entries and can include a TTY shell.
- Starts sessions with a cleared, explicit environment and drops privileges before running the user process.
- Registers the selected desktop, session type, seat, and VT with PAM/logind,
then imports only approved PAM runtime values into the user session.
- Records user sessions with UTMPX when the platform provides it.
- Provides a Ratatui TUI with filtering, keyboard help, refresh, notifications, wallpaper, and reduced-motion support.
- Runs safely in preview mode for UI work and configuration checks.
The real login handoff still needs a Linux system with PAM, a virtual terminal, and the display/session services you want to use. The Docker tests cover builds, unit tests, rendering, configuration, and capability probes; they do not pretend to be a real VT or systemd-logind session. ## Features
## Build - Discovers normal login accounts through NSS.
- Shows account expiry, locked-account, and faillock status before login.
- Lets PAM handle authentication and password changes.
- Switches between users and sessions from the same screen.
- Finds X11 and Wayland desktop entries from configured directories.
- Can offer a TTY shell as a session choice.
- Starts the selected session with a restricted environment and the correct user ID.
- Passes the selected session, seat, and virtual terminal to PAM and logind.
- Imports approved values from PAM into the session environment.
- Writes UTMPX records when the platform supports them.
- Provides filtering, keyboard help, refresh, notifications, wallpaper, and reduced-motion controls.
- Has a preview mode for UI work and configuration checks.
On Debian or Ubuntu, install the build dependencies first: The service needs Linux PAM and a virtual terminal. X11 and Wayland sessions also need their own display server, compositor, and desktop files. Docker covers the parts that do not require those host services. The remaining checks must run on a prepared machine or VM.
## Requirements
Builds use Rust 1.88 and the system PAM development files. On Debian or Ubuntu:
```sh ```sh
sudo apt-get install build-essential pkg-config libpam0g-dev libpam-modules sudo apt-get install build-essential pkg-config libpam0g-dev libpam-modules
``` ```
Then run: The repository includes the required `rustfmt` and `clippy` components in `rust-toolchain.toml`.
## Build and run
Run the local verification gate and build the release binary:
```sh ```sh
make check make check
make docker-test make build
make integration-test
``` ```
`make build` writes the optimized binary to `build/latchd`. `make package` creates a deterministic archive and SHA-256 file in `dist/`. The binary is written to `build/latchd`. To open the non-authenticating preview:
## Try the UI
Preview mode does not call PAM, start a session, or run power actions:
```sh ```sh
make run-preview make run-preview
@@ -49,11 +52,41 @@ make run-preview
./build/latchd --preview --no-log ./build/latchd --preview --no-log
``` ```
Use `LATCHD_ASCII=1` for terminals without reliable Unicode support, `NO_COLOR=1` for monochrome output, and `LATCHD_HIGH_CONTRAST=1` for a stronger focus palette. Preview mode does not contact PAM, start a user session, or run power actions. It is safe to use while changing the layout or configuration.
For terminals with limited character support, set `LATCHD_ASCII=1`. Set `NO_COLOR=1` for plain output. Set `LATCHD_HIGH_CONTRAST=1` for a stronger focus colour.
## Configuration
The example file is [`extra/config.toml`](extra/config.toml). Latchd uses separate paths for a system service and a local preview:
- A root service reads `/etc/latchd/config.toml`.
- Preview mode first checks `$XDG_CONFIG_HOME/latchd/config.toml`.
- If `XDG_CONFIG_HOME` is not set, preview mode checks `~/.config/latchd/config.toml`.
- `--config PATH` selects a file explicitly and takes precedence over the defaults.
Create the user file from the example:
```sh
make install-user
./build/latchd --preview --no-log
```
`make install-user` creates `~/.config/latchd/` with mode `700` and creates `config.toml` with mode `600`. It does not replace an existing file. Set `USER_CONFIG_HOME=/path` or `XDG_CONFIG_HOME=/path` when the file must be stored elsewhere.
The main sections in the example are:
- `[accounts]` for NSS discovery, faillock, and account-age checks.
- `[x11]` and `[wayland]` for desktop-entry and administrator script directories.
- `[environment_switcher]` for session choices and the last-session preference file.
- `[visual]` for wallpaper, animation, notification, and reduced-motion settings.
- `[power_controls]` for confirmed power commands. Latchd starts these programs directly; it does not use a shell.
Keep the system file owned by root and unwritable by group or other users. Do not put passwords or other secrets in command-line arguments.
## Install on a test machine ## Install on a test machine
Do this on a disposable machine first. Keep another login path available while changing a display manager. Use a disposable machine first. Keep another login path available while changing the display manager.
```sh ```sh
sudo make install sudo make install
@@ -67,83 +100,83 @@ This installs:
- `/etc/pam.d/latchd` - `/etc/pam.d/latchd`
- `/usr/lib/systemd/system/latchd.service` - `/usr/lib/systemd/system/latchd.service`
The default service owns `/dev/tty2`. Change the `tty` setting and the unit's `TTYPath` together when using another virtual terminal. The supplied service uses `/dev/tty2`. If you choose another VT, change both the `tty` value in the config and `TTYPath` in the systemd unit. Stop or disable the existing display manager before enabling latchd on the same terminal.
## Configuration ## Keyboard controls
The complete example is in [`extra/config.toml`](extra/config.toml). Important sections are:
- `[accounts]` controls NSS discovery, the faillock helper, faillock policy, and `/etc/shadow` account-age checks.
- `[x11]` and `[wayland]` select desktop-entry and administrator script directories.
- `[environment_switcher]` controls session choices, the optional TTY shell,
and the owner-checked last-session preference file.
- `[visual]` controls wallpaper size, animation, notifications, and reduced motion.
- `[power_controls]` contains absolute program paths and argv arrays. Commands are never passed through a shell.
Configuration files used by a real root service must be root-owned and not writable by group or other users. Use a variables file for deployment-specific values instead of putting secrets in command arguments.
## Controls
| Key | Action | | Key | Action |
| --- | --- | | --- | --- |
| Tab / Shift-Tab | Move focus | | Tab / Shift-Tab | Move focus |
| Up / Down | Move focus or move in a filtered list | | Up / Down | Move focus or move in a filtered list |
| Left / Right | Change the selected session | | Left / Right | Change the selected session |
| `/` | Filter sessions or discovered accounts | | `/` | Filter sessions or accounts |
| Ctrl-R | Refresh account and session discovery | | Ctrl-R | Refresh account and session discovery |
| Ctrl-N / Ctrl-P | Select the next or previous account | | Ctrl-N / Ctrl-P | Select the next or previous account |
| Ctrl-B | Toggle wallpaper and ambient effects | | Ctrl-B | Toggle wallpaper and ambient effects |
| Ctrl-M | Toggle reduced motion | | Ctrl-M | Toggle reduced motion |
| Enter | Authenticate, or submit a PAM password change when requested | | Enter | Authenticate or submit a PAM password change |
| Esc | Clear the password or close a popup | | Esc | Clear the password or close a popup |
| `?` | Show keyboard help | | `?` | Show keyboard help |
| Ctrl-C | Restore the terminal and quit | | Ctrl-C | Restore the terminal and quit |
| F1–F12 | Confirm a configured power action | | F1–F12 | Confirm a configured power action |
The filter is smart-case: lowercase searches ignore case; a search containing uppercase characters is case-sensitive. The account-age and faillock messages are advisory. PAM remains the only authorization decision. Lowercase filters ignore case. A filter with an uppercase letter is case-sensitive. The status line can show account and faillock information before login.
## Testing a deployment ## Testing
The ordinary tests do not need a privileged container: Run the local checks with these commands:
```sh ```sh
make check make check
make audit
make docker-test
make integration-test make integration-test
make pam-fixture-test # runs when root is available; otherwise reports SKIP make docker-test
make system-e2e make system-e2e
``` ```
`make system-e2e` reports real host capabilities as `PASS`, `SKIP`, or `FAIL`. It does not call an account password or claim that a pseudo-terminal is a virtual terminal. `make system-e2e-strict` is for a prepared disposable VM with a test PAM policy, a real VT, systemd-logind, and the X11 or Wayland stack under test. See [`docs/e2e-testing.md`](docs/e2e-testing.md). Run `make audit` after you install `cargo-audit`. Run `make pam-fixture-test` as root. The PAM fixture command creates temporary policies and removes them when it exits.
`make system-e2e` checks the PAM library, faillock, virtual terminal, X11, Wayland, and systemd-logind on the current host. It prints `PASS`, `SKIP`, or `FAIL`. A pseudo-terminal does not provide a virtual terminal. The check does not install a PAM policy or start systemd-logind.
Run the strict check in a disposable VM with a test PAM policy, a real VT, systemd-logind, and the X11 or Wayland stack you want to test:
```sh
make system-e2e-strict
```
See [`docs/e2e-testing.md`](docs/e2e-testing.md) for the test limits and VM setup.
## Security notes ## Security notes
- PAM handles stay on one worker thread. - PAM handles stay on one worker thread.
- Password buffers are zeroized and never placed in argv, logs, or the TUI. - Password buffers are zeroized. Passwords are not written to argv, logs, or the TUI.
- Security audit events contain only the account name and stable outcome/reason - Audit records contain the account and a stable result code. They do not contain PAM's free-form error text or password data.
fields; PAM's free-form error text and password material are not logged.
- Session environments are rebuilt from an allowlist. - Session environments are rebuilt from an allowlist.
- Desktop files, scripts, binaries, homes, logs, and configuration sources are checked before use. - Latchd checks desktop files, scripts, binaries, home directories, logs, and configuration files before use.
- Xauthority files and session logs use private permissions and cleanup guards. - Xauthority files and session logs use private permissions and cleanup guards.
- Power actions require confirmation and are disabled in preview mode. - Power commands need confirmation and are disabled in preview mode.
- Account status and faillock output never bypass PAM or provide an administrator unlock shortcut. - Account and faillock checks do not unlock accounts or bypass PAM.
## Project layout ## Project layout
```text ```text
crates/latchd-core/ configuration, account discovery, sessions, UTMPX crates/latchd-core/ configuration, account discovery, sessions, and UTMPX
crates/latchd-auth/ PAM FFI, password changes, and the authentication worker crates/latchd-auth/ PAM FFI, password changes, and the authentication worker
crates/latchd-tui/ Ratatui state, rendering, filtering, and effects crates/latchd-tui/ Ratatui state, rendering, filtering, and effects
rust/main.rs command-line entry point and service orchestration rust/main.rs command-line entry point and service orchestration
extra/ example configuration, PAM policy, and systemd unit extra/ example configuration, PAM policy, and systemd unit
scripts/ Docker and CLI/system capability tests scripts/ Docker, CLI, PAM, and system capability tests
legacy/go/ non-production Go reference legacy/go/ non-production Go reference
``` ```
## Releases ## Releases
GitLab CI runs formatting, tests, Clippy, rustdoc, RustSec auditing, CLI integration tests, capability probes, and the Docker suite. SemVer tags produce a reproducible Linux archive with checksums. Live VT, PAM-policy, X11, Wayland, and logind validation belongs in the isolated VM gate described above. GitLab CI runs formatting, tests, Clippy, rustdoc, RustSec checks, CLI tests, capability checks, and the Docker suite. A SemVer tag runs the package job and publishes a Linux archive with its SHA-256 file. The strict VT, PAM, X11, Wayland, and logind checks are separate because CI containers do not provide those host services.
Create a local package with:
```sh
make package
```
## License ## License

View File

@@ -1,6 +1,6 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs, env, fs,
os::unix::fs::MetadataExt, os::unix::fs::MetadataExt,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@@ -9,6 +9,19 @@ use serde::Deserialize;
use crate::error::{Error, ResultExt}; use crate::error::{Error, ResultExt};
pub const SYSTEM_CONFIG_PATH: &str = "/etc/latchd/config.toml";
const PROJECT_CONFIG_DIR: &str = "latchd";
const CONFIG_FILE: &str = "config.toml";
/// Returns the current user's configuration path.
pub fn user_config_path() -> Option<PathBuf> {
let base = env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.filter(|path| path.is_absolute())
.or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))?;
Some(base.join(PROJECT_CONFIG_DIR).join(CONFIG_FILE))
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
pub struct Config { pub struct Config {

View File

@@ -31,6 +31,8 @@ make docker-test
make system-e2e make system-e2e
``` ```
The preview interface is documented in [`ui.md`](ui.md). Its screenshots come from a Docker PTY and do not represent a host VT or compositor.
For a VM with a real seat, compositor, and logind: For a VM with a real seat, compositor, and logind:
```sh ```sh

BIN
docs/images/latchd-help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

39
docs/ui.md Normal file
View File

@@ -0,0 +1,39 @@
# Latchd user interface
Latchd starts on a virtual terminal. The interface keeps the session list and the login fields visible at the same time.
![Login screen](images/latchd-preview.png)
The screenshot comes from the Docker test image. It uses preview mode. Preview mode does not contact PAM or start a user session.
## Help panel
Press `?` to show the key reference.
![Keyboard help](images/latchd-help.png)
Use the following keys:
- Press `Tab` or `Shift-Tab` to move focus.
- Press the arrow keys to choose a session.
- Press `/` to filter sessions or accounts.
- Press `Ctrl-N` or `Ctrl-P` to change the account.
- Press `Ctrl-R` to refresh account and session discovery.
- Press `Ctrl-B` to toggle the background.
- Press `Ctrl-M` to toggle reduced motion.
- Press `Enter` to authenticate.
- Press `Esc` to clear the password or close the help panel.
- Press `Ctrl-C` to restore the terminal and quit.
These messages show account state before login. PAM accepts or rejects the login.
## Capture method
The capture uses a Docker PTY and the release binary from `latchd-test:local`:
```sh
tmux new-session -d -x 120 -y 36 \
'docker run --rm -it latchd-test:local /workspace/target/release/latchd --preview --no-log'
```
The image in this directory contains the plain terminal capture. It does not show a host VT, a host compositor, or a real PAM login.

View File

@@ -7,7 +7,7 @@ use std::{
fs::{self, OpenOptions}, fs::{self, OpenOptions},
io, io,
os::unix::fs::{MetadataExt, OpenOptionsExt}, os::unix::fs::{MetadataExt, OpenOptionsExt},
path::PathBuf, path::{Path, PathBuf},
}; };
use clap::Parser; use clap::Parser;
@@ -34,8 +34,8 @@ const BUILD_TIME: &str = env!("LATCHD_BUILD_TIME");
)] )]
struct Cli { struct Cli {
/// Configuration TOML path. /// Configuration TOML path.
#[arg(short, long, default_value = "/etc/latchd/config.toml")] #[arg(short, long)]
config: PathBuf, config: Option<PathBuf>,
/// Optional variable TOML path. /// Optional variable TOML path.
#[arg(short = 'v', long)] #[arg(short = 'v', long)]
variables: Option<PathBuf>, variables: Option<PathBuf>,
@@ -79,17 +79,7 @@ fn run() -> Result<(), Error> {
println!("version={BUILD_VERSION}\ncommit={BUILD_COMMIT}\nepoch={BUILD_TIME}"); println!("version={BUILD_VERSION}\ncommit={BUILD_COMMIT}\nepoch={BUILD_TIME}");
return Ok(()); return Ok(());
} }
let config_path = if cli.config.exists() { let config_path = resolve_config_path(cli.config.as_deref(), cli.preview)?;
Some(cli.config.as_path())
} else {
if cli.config.as_os_str() != "/etc/latchd/config.toml" {
return Err(Error::Config(format!(
"configuration does not exist: {}",
cli.config.display()
)));
}
None
};
if let Some(path) = cli.variables.as_deref() { if let Some(path) = cli.variables.as_deref() {
if !path.exists() { if !path.exists() {
return Err(Error::Config(format!( return Err(Error::Config(format!(
@@ -100,7 +90,7 @@ fn run() -> Result<(), Error> {
} }
let variables_path = cli.variables.as_deref(); let variables_path = cli.variables.as_deref();
if !cli.preview { if !cli.preview {
if let Some(path) = config_path { if let Some(path) = config_path.as_deref() {
config::validate_source(path, "configuration")?; config::validate_source(path, "configuration")?;
} }
if let Some(path) = variables_path { if let Some(path) = variables_path {
@@ -109,7 +99,7 @@ fn run() -> Result<(), Error> {
} }
let mut config = Config::default(); let mut config = Config::default();
if config_path.is_some() || variables_path.is_some() { if config_path.is_some() || variables_path.is_some() {
config = config::load(config_path, variables_path)?; config = config::load(config_path.as_deref(), variables_path)?;
} }
if let Some(tty) = cli.tty { if let Some(tty) = cli.tty {
if tty == 0 { if tty == 0 {
@@ -148,7 +138,7 @@ fn run() -> Result<(), Error> {
"already inside a session; use --preview".into(), "already inside a session; use --preview".into(),
)); ));
} }
if unsafe { libc::geteuid() } != 0 { if !running_as_root() {
return Err(Error::Config("latchd must run as root".into())); return Err(Error::Config("latchd must run as root".into()));
} }
validate_terminal()?; validate_terminal()?;
@@ -212,6 +202,33 @@ fn run() -> Result<(), Error> {
Ok(()) Ok(())
} }
fn resolve_config_path(explicit: Option<&Path>, preview: bool) -> Result<Option<PathBuf>, Error> {
if let Some(path) = explicit {
if path.exists() {
return Ok(Some(path.to_owned()));
}
return Err(Error::Config(format!(
"configuration does not exist: {}",
path.display()
)));
}
let system_path = Path::new(config::SYSTEM_CONFIG_PATH);
if !preview {
return Ok(system_path.exists().then(|| system_path.to_owned()));
}
if let Some(path) = config::user_config_path().filter(|path| path.exists()) {
return Ok(Some(path));
}
Ok(system_path.exists().then(|| system_path.to_owned()))
}
fn running_as_root() -> bool {
// SAFETY: `geteuid` has no pointer arguments or memory preconditions.
unsafe { libc::geteuid() == 0 }
}
fn setup_logging(path: &std::path::Path) -> Result<(), Error> { fn setup_logging(path: &std::path::Path) -> Result<(), Error> {
const MAX_LOG_BYTES: u64 = 64 * 1024 * 1024; const MAX_LOG_BYTES: u64 = 64 * 1024 * 1024;
let parent = path let parent = path

View File

@@ -85,6 +85,14 @@ shown="$workdir/show-config"
assert_contains "tty = 2" "$shown" assert_contains "tty = 2" "$shown"
assert_contains "pam_service" "$shown" assert_contains "pam_service" "$shown"
user_config_home="$workdir/config-home"
mkdir -p "$user_config_home/latchd"
sed 's/^tty = 2$/tty = 7/' "$config" >"$user_config_home/latchd/config.toml"
user_shown="$workdir/user-show-config"
HOME="$workdir/home" XDG_CONFIG_HOME="$user_config_home" \
"$binary" --preview --show-config --no-log >"$user_shown"
assert_contains "tty = 7" "$user_shown"
unknown="$workdir/unknown.toml" unknown="$workdir/unknown.toml"
printf 'unknown = true\n' >"$unknown" printf 'unknown = true\n' >"$unknown"
if "$binary" --preview --no-tui --no-log --config "$unknown" >"$workdir/unknown.out" 2>&1; then if "$binary" --preview --no-tui --no-log --config "$unknown" >"$workdir/unknown.out" 2>&1; then