//! Turning a config screen into the panes tmux should run. //! //! Only `socktop` produces more than one pane today. The representation is a //! plain `Vec` 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, } #[derive(Debug, Clone)] pub struct Cell { pub coord: Coord, pub kind: MonitorType, /// Human label for the position indicator. pub label: String, pub panes: Vec, 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 { 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 { 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> { cfg.screens .iter() .map(|s| build_cell(s, &cfg.binaries)) .collect() }