Skip to main content

max / mountaineer-sysop

Add \`sysop motd\` — opt-out login tip rotation Four verbs: \`sysop motd\` (status), \`sysop motd on\`, \`sysop motd off\`, \`sysop motd print\` (called by /etc/profile.d/sysop-motd.sh on login). Settings live in /etc/mountaineer/sysop.toml (\$SYSOP_CONFIG env override for host dev). New \`config\` module owns load/save; first piece of sysop state that will grow as more subcommands need persistent settings. Tip rotation is time-bucketed (epoch_seconds / 60) — no persistent counter, no read/write race on shared state, naturally approximates per-login rotation. Six tips, drawn verbatim from AESTHETICS.md. Default enabled = true (opt-out, per AESTHETICS). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-18 01:23 UTC
Commit: 68327d61c1f66c038a9c816903c21f412bfd801f
Parent: 1b08d0a
4 files changed, +118 insertions, -7 deletions
@@ -0,0 +1,47 @@
1 + use anyhow::{Context, Result};
2 + use serde::{Deserialize, Serialize};
3 + use std::fs;
4 + use std::path::PathBuf;
5 +
6 + const DEFAULT_PATH: &str = "/etc/mountaineer/sysop.toml";
7 +
8 + pub fn path() -> PathBuf {
9 + std::env::var_os("SYSOP_CONFIG")
10 + .map(PathBuf::from)
11 + .unwrap_or_else(|| PathBuf::from(DEFAULT_PATH))
12 + }
13 +
14 + #[derive(Debug, Default, Serialize, Deserialize)]
15 + pub struct Config {
16 + #[serde(default)]
17 + pub motd: Motd,
18 + }
19 +
20 + #[derive(Debug, Serialize, Deserialize)]
21 + pub struct Motd {
22 + pub enabled: bool,
23 + }
24 +
25 + impl Default for Motd {
26 + fn default() -> Self {
27 + Self { enabled: true }
28 + }
29 + }
30 +
31 + pub fn load() -> Result<Config> {
32 + let p = path();
33 + if !p.exists() {
34 + return Ok(Config::default());
35 + }
36 + let s = fs::read_to_string(&p).with_context(|| format!("read {}", p.display()))?;
37 + toml::from_str(&s).with_context(|| format!("parse {}", p.display()))
38 + }
39 +
40 + pub fn save(cfg: &Config) -> Result<()> {
41 + let p = path();
42 + if let Some(dir) = p.parent() {
43 + fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
44 + }
45 + let s = toml::to_string_pretty(cfg).context("serialize config")?;
46 + fs::write(&p, s).with_context(|| format!("write {}", p.display()))
47 + }
@@ -1,4 +1,6 @@
1 + mod config;
1 2 mod info;
3 + mod motd;
2 4 mod roles;
3 5
4 6 use clap::{Parser, Subcommand};
@@ -17,12 +19,33 @@ enum Command {
17 19 /// Role name (omit for the full list)
18 20 role: Option<String>,
19 21 },
22 + /// Login tip rotation (on/off/print)
23 + Motd {
24 + #[command(subcommand)]
25 + action: Option<MotdAction>,
26 + },
27 + }
28 +
29 + #[derive(Subcommand)]
30 + enum MotdAction {
31 + /// Enable the login tip
32 + On,
33 + /// Disable the login tip
34 + Off,
35 + /// Emit the current tip (called by /etc/profile.d/sysop-motd.sh on login)
36 + Print,
20 37 }
21 38
22 39 fn main() -> anyhow::Result<()> {
23 40 let cli = Cli::parse();
24 41 let code = match cli.command {
25 42 Command::Info { role } => info::run(role)?,
43 + Command::Motd { action } => match action {
44 + None => motd::status()?,
45 + Some(MotdAction::On) => motd::set(true)?,
46 + Some(MotdAction::Off) => motd::set(false)?,
47 + Some(MotdAction::Print) => motd::print()?,
48 + },
26 49 };
27 50 std::process::exit(code);
28 51 }
@@ -0,0 +1,43 @@
1 + use crate::config;
2 + use anyhow::Result;
3 + use std::time::SystemTime;
4 +
5 + const TIPS: &[&str] = &[
6 + "sysop info <role> — show which tool fills a role and where its config lives",
7 + "sysop services — s6-rc service health",
8 + "sysop wifi — network connections",
9 + "sysop storage — ZFS pool state",
10 + "sysop logs <svc> — structured logs for a service",
11 + "sysop help — list all sysop commands",
12 + ];
13 +
14 + pub fn status() -> Result<i32> {
15 + let cfg = config::load()?;
16 + let state = if cfg.motd.enabled { "on" } else { "off" };
17 + println!("motd: {state}");
18 + println!("config: {}", config::path().display());
19 + Ok(0)
20 + }
21 +
22 + pub fn set(enabled: bool) -> Result<i32> {
23 + let mut cfg = config::load()?;
24 + cfg.motd.enabled = enabled;
25 + config::save(&cfg)?;
26 + println!("motd: {}", if enabled { "on" } else { "off" });
27 + Ok(0)
28 + }
29 +
30 + pub fn print() -> Result<i32> {
31 + let cfg = config::load()?;
32 + if !cfg.motd.enabled {
33 + return Ok(0);
34 + }
35 + let bucket = SystemTime::now()
36 + .duration_since(SystemTime::UNIX_EPOCH)
37 + .map(|d| d.as_secs() / 60)
38 + .unwrap_or(0) as usize;
39 + let idx = bucket % TIPS.len();
40 + println!();
41 + println!(" {}", TIPS[idx]);
42 + Ok(0)
43 + }
M docs/todo.md +5 -7
@@ -1,6 +1,6 @@
1 1 # mountaineer-sysop — todo
2 2
3 - Status: Pre-v0. Active: `sysop motd on/off`. `sysop info` lands end-to-end (list + detail TUI, `cargo run -p sysop -- info` on host). Primitives exercised by `cargo run --example smoketest -p sysop-tui`.
3 + Status: Pre-v0. Active: next subcommand TBD (suggest `sysop services` to unblock the yambar `svc` segment). `sysop info` and `sysop motd` land end-to-end on host. Primitives exercised by `cargo run --example smoketest -p sysop-tui`.
4 4
5 5 ## Phase 1 — sysop-tui primitives (open items)
6 6
@@ -14,13 +14,11 @@ Status: Pre-v0. Active: `sysop motd on/off`. `sysop info` lands end-to-end (list
14 14 - [ ] Runtime override: layer `/etc/mountaineer/roles.toml` on top of the compiled-in defaults (principle 2 — operator can see/edit). Currently only `include_str!` source
15 15 - [ ] Server vs Sysadmin profile filter — graphical-layer roles should be hidden in `--server` mode
16 16
17 - ## Phase 3 — `sysop motd on/off`
17 + ## Phase 3 — `sysop motd` (open items)
18 18
19 - Smallest stateful subcommand. Writes the motd-enable flag (from installer or operator). Exercises config-write path.
20 -
21 - - [ ] Config file location decided (`/etc/mountaineer/sysop.toml`? `~/.config/mountaineer/`? both?)
22 - - [ ] `sysop motd on` / `sysop motd off` / `sysop motd` (status)
23 - - [ ] motd rotation script driven by this flag
19 + - [ ] `/etc/profile.d/sysop-motd.sh` glue script — calls `sysop motd print` on login. Lives in **mountaineer-build apkovl-src**, not this repo
20 + - [ ] Tip set worth revisiting once more `sysop` verbs exist — currently 6, drawn from AESTHETICS
21 + - [ ] Decide whether `sysop motd print` should respect a "first-login-of-the-day" suppression to avoid tip spam in tmux-heavy sessions
24 22
25 23 ## Deferred (post-MVP scaffolding)
26 24