ce6acec299
Installer rewritten around a preflight: distro, package manager, display manager, window manager, terminal, tmux, cargo, git, screen locker, touch device, device permissions and free disk are all checked BEFORE anything is installed, and the total cost is printed once for a single confirmation. Prompts read /dev/tty so they still work when the script is piped from curl, and fall back to defaults with a notice when there is no terminal at all. Several "[ test ] && action" statements were set -e landmines: under set -e an AND-OR list that ends up false aborts the script, so a box with no lightdm, no i3 or nothing to install would have exited silently partway through detection -- which is exactly the fresh-Debian case the installer exists for. Rewritten as if-statements and verified against a stripped PATH with no tmux, cargo, git or package manager present. Also fixed cargo detection reporting blank instead of NOT INSTALLED: the status of `cargo --version | cut` is cut's, and cut succeeds on empty input, so the fallback never fired. Device access now defaults to a udev rule matching touchscreens only, rather than the input group, which grants access to every input device including the keyboard and needs a full logout. README rewritten for someone who has not seen the project: what the photo shows, the hardware, install, then a config built up step by step, each step with the YAML and the resulting map. Every example is verified verbatim against the binary, and every relative link resolves. The mechanism and the reasoning move to notes/: DESIGN.md, HARDWARE-NOTES.md, V1-BASH.md, TODO.md. cad/README.md was a verbatim copy of the one inside geeekpi_rack_adapter_release_v1/, so every path in it -- including the screenshot -- was broken from where it sits. Corrected to its own level, and it now states once that the 9-inch screen, the 10-inch mini-rack mount and the 19-inch rack are three different measurements. The v1 shell implementation is removed; it stays recoverable at tag v1.2 and notes/V1-BASH.md carries the setting-by-setting migration table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
182 lines
5.4 KiB
Rust
182 lines
5.4 KiB
Rust
//! Turning a config screen into the panes tmux should run.
|
|
//!
|
|
//! Only `socktop` produces more than one pane today. The representation is a
|
|
//! plain `Vec<Pane>` per cell, so a future type wanting the same treatment is a
|
|
//! data change rather than a redesign.
|
|
|
|
use anyhow::{bail, Context, Result};
|
|
|
|
use crate::config::{expand_tilde, Binaries, Config, Coord, Layout, MonitorType, Screen};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Pane {
|
|
/// Shown in the pane border.
|
|
pub title: String,
|
|
/// argv, already split. Never passed through a shell.
|
|
pub command: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Cell {
|
|
pub coord: Coord,
|
|
pub kind: MonitorType,
|
|
/// Human label for the position indicator.
|
|
pub label: String,
|
|
pub panes: Vec<Pane>,
|
|
pub layout: Layout,
|
|
/// Which sub-screen of this cell is current. Persists while you are
|
|
/// elsewhere, so a vertical return lands where you left.
|
|
pub cursor: usize,
|
|
}
|
|
|
|
impl Cell {
|
|
/// A multi-pane cell shows a tiled overview first, then each pane zoomed.
|
|
/// A single-pane cell has nothing to zoom into, so it is one sub-screen.
|
|
pub fn has_overview(&self) -> bool {
|
|
self.panes.len() > 1
|
|
}
|
|
|
|
pub fn screens(&self) -> usize {
|
|
if self.has_overview() {
|
|
self.panes.len() + 1
|
|
} else {
|
|
1
|
|
}
|
|
}
|
|
|
|
pub fn last_screen(&self) -> usize {
|
|
self.screens() - 1
|
|
}
|
|
|
|
/// `None` for the overview, otherwise the pane index to zoom.
|
|
pub fn zoomed_pane(&self) -> Option<usize> {
|
|
match (self.has_overview(), self.cursor) {
|
|
(true, 0) => None,
|
|
(true, i) => Some(i - 1),
|
|
(false, _) => Some(0),
|
|
}
|
|
}
|
|
|
|
/// What the indicator shows for the current sub-screen.
|
|
pub fn screen_label(&self) -> String {
|
|
match self.zoomed_pane() {
|
|
None => format!("{} (all)", self.label),
|
|
Some(i) if self.has_overview() => self.panes[i].title.clone(),
|
|
Some(_) => self.label.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn build_cell(screen: &Screen, bins: &Binaries) -> Result<Cell> {
|
|
let at = screen.at;
|
|
let extra = screen.args.clone().unwrap_or_default();
|
|
|
|
// An explicit `command:` replaces the generated one for every type.
|
|
if let Some(cmd) = &screen.command {
|
|
let mut argv = shell_words::split(cmd)
|
|
.with_context(|| format!("screen {at}: cannot parse command: {cmd}"))?;
|
|
if argv.is_empty() {
|
|
bail!("screen {at}: command is empty");
|
|
}
|
|
argv[0] = expand_tilde(&argv[0]);
|
|
argv.extend(extra);
|
|
let title = screen
|
|
.title
|
|
.clone()
|
|
.unwrap_or_else(|| screen.kind.to_string());
|
|
return Ok(Cell {
|
|
coord: at,
|
|
kind: screen.kind,
|
|
label: title.clone(),
|
|
panes: vec![Pane {
|
|
title,
|
|
command: argv,
|
|
}],
|
|
layout: screen.layout.unwrap_or_default(),
|
|
cursor: 0,
|
|
});
|
|
}
|
|
|
|
let bin = bins.get(screen.kind);
|
|
|
|
let (label, panes) = match screen.kind {
|
|
MonitorType::Socktop => {
|
|
let hosts = screen
|
|
.socktop_group
|
|
.as_ref()
|
|
.expect("validated: socktop needs socktop_group");
|
|
let panes = hosts
|
|
.0
|
|
.iter()
|
|
.map(|h| Pane {
|
|
title: h.clone(),
|
|
command: {
|
|
let mut c = vec![bin.clone(), "-P".into(), h.clone()];
|
|
c.extend(extra.clone());
|
|
c
|
|
},
|
|
})
|
|
.collect();
|
|
let label = if hosts.0.len() == 1 {
|
|
hosts.0[0].clone()
|
|
} else {
|
|
format!("{} hosts", hosts.0.len())
|
|
};
|
|
(label, panes)
|
|
}
|
|
|
|
MonitorType::UptimeKumaStatus => {
|
|
let url = screen.url.as_ref().expect("validated: kuma needs url");
|
|
let mut c = vec![bin, url.clone()];
|
|
c.extend(extra);
|
|
(
|
|
"uptime kuma".to_string(),
|
|
vec![Pane {
|
|
title: "uptime kuma".into(),
|
|
command: c,
|
|
}],
|
|
)
|
|
}
|
|
|
|
MonitorType::Unifly => {
|
|
let mut c = vec![bin, "tui".into()];
|
|
// Carved out for when the fork grows the flags; see notes/PLAN-v2.md.
|
|
if let Some(site) = &screen.site {
|
|
c.push("--site".into());
|
|
c.push(site.clone());
|
|
}
|
|
if let Some(controller) = &screen.controller {
|
|
c.push("--controller".into());
|
|
c.push(controller.clone());
|
|
}
|
|
c.extend(extra);
|
|
(
|
|
"unifly".to_string(),
|
|
vec![Pane {
|
|
title: "unifly".into(),
|
|
command: c,
|
|
}],
|
|
)
|
|
}
|
|
|
|
MonitorType::Generic => unreachable!("validated: generic always has a command"),
|
|
};
|
|
|
|
let label = screen.title.clone().unwrap_or(label);
|
|
Ok(Cell {
|
|
coord: at,
|
|
kind: screen.kind,
|
|
label,
|
|
panes,
|
|
layout: screen.layout.unwrap_or_default(),
|
|
cursor: 0,
|
|
})
|
|
}
|
|
|
|
pub fn build_cells(cfg: &Config) -> Result<Vec<Cell>> {
|
|
cfg.screens
|
|
.iter()
|
|
.map(|s| build_cell(s, &cfg.binaries))
|
|
.collect()
|
|
}
|