max / mountaineer-sysop
8 files changed,
+736 insertions,
-14 deletions
| @@ -9,6 +9,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 9 | 9 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" | |
| 10 | 10 | ||
| 11 | 11 | [[package]] | |
| 12 | + | name = "anstream" | |
| 13 | + | version = "1.0.0" | |
| 14 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 15 | + | checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" | |
| 16 | + | dependencies = [ | |
| 17 | + | "anstyle", | |
| 18 | + | "anstyle-parse", | |
| 19 | + | "anstyle-query", | |
| 20 | + | "anstyle-wincon", | |
| 21 | + | "colorchoice", | |
| 22 | + | "is_terminal_polyfill", | |
| 23 | + | "utf8parse", | |
| 24 | + | ] | |
| 25 | + | ||
| 26 | + | [[package]] | |
| 27 | + | name = "anstyle" | |
| 28 | + | version = "1.0.14" | |
| 29 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 30 | + | checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" | |
| 31 | + | ||
| 32 | + | [[package]] | |
| 33 | + | name = "anstyle-parse" | |
| 34 | + | version = "1.0.0" | |
| 35 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 36 | + | checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" | |
| 37 | + | dependencies = [ | |
| 38 | + | "utf8parse", | |
| 39 | + | ] | |
| 40 | + | ||
| 41 | + | [[package]] | |
| 42 | + | name = "anstyle-query" | |
| 43 | + | version = "1.1.5" | |
| 44 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 45 | + | checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" | |
| 46 | + | dependencies = [ | |
| 47 | + | "windows-sys 0.61.2", | |
| 48 | + | ] | |
| 49 | + | ||
| 50 | + | [[package]] | |
| 51 | + | name = "anstyle-wincon" | |
| 52 | + | version = "3.0.11" | |
| 53 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 54 | + | checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" | |
| 55 | + | dependencies = [ | |
| 56 | + | "anstyle", | |
| 57 | + | "once_cell_polyfill", | |
| 58 | + | "windows-sys 0.61.2", | |
| 59 | + | ] | |
| 60 | + | ||
| 61 | + | [[package]] | |
| 12 | 62 | name = "anyhow" | |
| 13 | 63 | version = "1.0.102" | |
| 14 | 64 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -42,6 +92,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 42 | 92 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" | |
| 43 | 93 | ||
| 44 | 94 | [[package]] | |
| 95 | + | name = "clap" | |
| 96 | + | version = "4.6.1" | |
| 97 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 98 | + | checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" | |
| 99 | + | dependencies = [ | |
| 100 | + | "clap_builder", | |
| 101 | + | "clap_derive", | |
| 102 | + | ] | |
| 103 | + | ||
| 104 | + | [[package]] | |
| 105 | + | name = "clap_builder" | |
| 106 | + | version = "4.6.0" | |
| 107 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 108 | + | checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" | |
| 109 | + | dependencies = [ | |
| 110 | + | "anstream", | |
| 111 | + | "anstyle", | |
| 112 | + | "clap_lex", | |
| 113 | + | "strsim", | |
| 114 | + | ] | |
| 115 | + | ||
| 116 | + | [[package]] | |
| 117 | + | name = "clap_derive" | |
| 118 | + | version = "4.6.1" | |
| 119 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 120 | + | checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" | |
| 121 | + | dependencies = [ | |
| 122 | + | "heck", | |
| 123 | + | "proc-macro2", | |
| 124 | + | "quote", | |
| 125 | + | "syn", | |
| 126 | + | ] | |
| 127 | + | ||
| 128 | + | [[package]] | |
| 129 | + | name = "clap_lex" | |
| 130 | + | version = "1.1.0" | |
| 131 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 132 | + | checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" | |
| 133 | + | ||
| 134 | + | [[package]] | |
| 135 | + | name = "colorchoice" | |
| 136 | + | version = "1.0.5" | |
| 137 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 138 | + | checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" | |
| 139 | + | ||
| 140 | + | [[package]] | |
| 45 | 141 | name = "compact_str" | |
| 46 | 142 | version = "0.8.1" | |
| 47 | 143 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -161,6 +257,12 @@ dependencies = [ | |||
| 161 | 257 | ] | |
| 162 | 258 | ||
| 163 | 259 | [[package]] | |
| 260 | + | name = "hashbrown" | |
| 261 | + | version = "0.17.1" | |
| 262 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 263 | + | checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" | |
| 264 | + | ||
| 265 | + | [[package]] | |
| 164 | 266 | name = "heck" | |
| 165 | 267 | version = "0.5.0" | |
| 166 | 268 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -173,6 +275,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 173 | 275 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" | |
| 174 | 276 | ||
| 175 | 277 | [[package]] | |
| 278 | + | name = "indexmap" | |
| 279 | + | version = "2.14.0" | |
| 280 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 281 | + | checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" | |
| 282 | + | dependencies = [ | |
| 283 | + | "equivalent", | |
| 284 | + | "hashbrown 0.17.1", | |
| 285 | + | ] | |
| 286 | + | ||
| 287 | + | [[package]] | |
| 176 | 288 | name = "indoc" | |
| 177 | 289 | version = "2.0.7" | |
| 178 | 290 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -195,6 +307,12 @@ dependencies = [ | |||
| 195 | 307 | ] | |
| 196 | 308 | ||
| 197 | 309 | [[package]] | |
| 310 | + | name = "is_terminal_polyfill" | |
| 311 | + | version = "1.70.2" | |
| 312 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 313 | + | checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" | |
| 314 | + | ||
| 315 | + | [[package]] | |
| 198 | 316 | name = "itertools" | |
| 199 | 317 | version = "0.13.0" | |
| 200 | 318 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -242,10 +360,16 @@ version = "0.12.5" | |||
| 242 | 360 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 243 | 361 | checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" | |
| 244 | 362 | dependencies = [ | |
| 245 | - | "hashbrown", | |
| 363 | + | "hashbrown 0.15.5", | |
| 246 | 364 | ] | |
| 247 | 365 | ||
| 248 | 366 | [[package]] | |
| 367 | + | name = "memchr" | |
| 368 | + | version = "2.8.0" | |
| 369 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 370 | + | checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" | |
| 371 | + | ||
| 372 | + | [[package]] | |
| 249 | 373 | name = "mio" | |
| 250 | 374 | version = "1.2.0" | |
| 251 | 375 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -258,6 +382,12 @@ dependencies = [ | |||
| 258 | 382 | ] | |
| 259 | 383 | ||
| 260 | 384 | [[package]] | |
| 385 | + | name = "once_cell_polyfill" | |
| 386 | + | version = "1.70.2" | |
| 387 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 388 | + | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" | |
| 389 | + | ||
| 390 | + | [[package]] | |
| 261 | 391 | name = "parking_lot" | |
| 262 | 392 | version = "0.12.5" | |
| 263 | 393 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -366,6 +496,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 366 | 496 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" | |
| 367 | 497 | ||
| 368 | 498 | [[package]] | |
| 499 | + | name = "serde" | |
| 500 | + | version = "1.0.228" | |
| 501 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 502 | + | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" | |
| 503 | + | dependencies = [ | |
| 504 | + | "serde_core", | |
| 505 | + | "serde_derive", | |
| 506 | + | ] | |
| 507 | + | ||
| 508 | + | [[package]] | |
| 509 | + | name = "serde_core" | |
| 510 | + | version = "1.0.228" | |
| 511 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 512 | + | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" | |
| 513 | + | dependencies = [ | |
| 514 | + | "serde_derive", | |
| 515 | + | ] | |
| 516 | + | ||
| 517 | + | [[package]] | |
| 518 | + | name = "serde_derive" | |
| 519 | + | version = "1.0.228" | |
| 520 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 521 | + | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" | |
| 522 | + | dependencies = [ | |
| 523 | + | "proc-macro2", | |
| 524 | + | "quote", | |
| 525 | + | "syn", | |
| 526 | + | ] | |
| 527 | + | ||
| 528 | + | [[package]] | |
| 529 | + | name = "serde_spanned" | |
| 530 | + | version = "0.6.9" | |
| 531 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 532 | + | checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" | |
| 533 | + | dependencies = [ | |
| 534 | + | "serde", | |
| 535 | + | ] | |
| 536 | + | ||
| 537 | + | [[package]] | |
| 369 | 538 | name = "signal-hook" | |
| 370 | 539 | version = "0.3.18" | |
| 371 | 540 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -452,9 +621,12 @@ name = "sysop" | |||
| 452 | 621 | version = "0.1.0" | |
| 453 | 622 | dependencies = [ | |
| 454 | 623 | "anyhow", | |
| 624 | + | "clap", | |
| 455 | 625 | "crossterm", | |
| 456 | 626 | "ratatui", | |
| 627 | + | "serde", | |
| 457 | 628 | "sysop-tui", | |
| 629 | + | "toml", | |
| 458 | 630 | ] | |
| 459 | 631 | ||
| 460 | 632 | [[package]] | |
| @@ -466,6 +638,47 @@ dependencies = [ | |||
| 466 | 638 | ] | |
| 467 | 639 | ||
| 468 | 640 | [[package]] | |
| 641 | + | name = "toml" | |
| 642 | + | version = "0.8.23" | |
| 643 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 644 | + | checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" | |
| 645 | + | dependencies = [ | |
| 646 | + | "serde", | |
| 647 | + | "serde_spanned", | |
| 648 | + | "toml_datetime", | |
| 649 | + | "toml_edit", | |
| 650 | + | ] | |
| 651 | + | ||
| 652 | + | [[package]] | |
| 653 | + | name = "toml_datetime" | |
| 654 | + | version = "0.6.11" | |
| 655 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 656 | + | checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" | |
| 657 | + | dependencies = [ | |
| 658 | + | "serde", | |
| 659 | + | ] | |
| 660 | + | ||
| 661 | + | [[package]] | |
| 662 | + | name = "toml_edit" | |
| 663 | + | version = "0.22.27" | |
| 664 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 665 | + | checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" | |
| 666 | + | dependencies = [ | |
| 667 | + | "indexmap", | |
| 668 | + | "serde", | |
| 669 | + | "serde_spanned", | |
| 670 | + | "toml_datetime", | |
| 671 | + | "toml_write", | |
| 672 | + | "winnow", | |
| 673 | + | ] | |
| 674 | + | ||
| 675 | + | [[package]] | |
| 676 | + | name = "toml_write" | |
| 677 | + | version = "0.1.2" | |
| 678 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 679 | + | checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" | |
| 680 | + | ||
| 681 | + | [[package]] | |
| 469 | 682 | name = "unicode-ident" | |
| 470 | 683 | version = "1.0.24" | |
| 471 | 684 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -501,6 +714,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 501 | 714 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" | |
| 502 | 715 | ||
| 503 | 716 | [[package]] | |
| 717 | + | name = "utf8parse" | |
| 718 | + | version = "0.2.2" | |
| 719 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 720 | + | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" | |
| 721 | + | ||
| 722 | + | [[package]] | |
| 504 | 723 | name = "wasi" | |
| 505 | 724 | version = "0.11.1+wasi-snapshot-preview1" | |
| 506 | 725 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -615,3 +834,12 @@ name = "windows_x86_64_msvc" | |||
| 615 | 834 | version = "0.52.6" | |
| 616 | 835 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 617 | 836 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | |
| 837 | + | ||
| 838 | + | [[package]] | |
| 839 | + | name = "winnow" | |
| 840 | + | version = "0.7.15" | |
| 841 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 842 | + | checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" | |
| 843 | + | dependencies = [ | |
| 844 | + | "memchr", | |
| 845 | + | ] |
| @@ -12,3 +12,6 @@ sysop-tui = { path = "crates/sysop-tui" } | |||
| 12 | 12 | ratatui = "0.29" | |
| 13 | 13 | crossterm = "0.28" | |
| 14 | 14 | anyhow = "1" | |
| 15 | + | clap = { version = "4", features = ["derive"] } | |
| 16 | + | serde = { version = "1", features = ["derive"] } | |
| 17 | + | toml = "0.8" |
| @@ -14,3 +14,6 @@ sysop-tui.workspace = true | |||
| 14 | 14 | ratatui.workspace = true | |
| 15 | 15 | crossterm.workspace = true | |
| 16 | 16 | anyhow.workspace = true | |
| 17 | + | clap.workspace = true | |
| 18 | + | serde.workspace = true | |
| 19 | + | toml.workspace = true |
| @@ -0,0 +1,254 @@ | |||
| 1 | + | # Mountaineer role → tool registry. | |
| 2 | + | # | |
| 3 | + | # Each role is a stable name for a job the OS needs done. The tool filling | |
| 4 | + | # the role is implementation detail and may change between releases; the | |
| 5 | + | # role name does not. `sysop info <role>` reads this file. | |
| 6 | + | # | |
| 7 | + | # Order matters: roles are presented in the list view in the order they | |
| 8 | + | # appear here. Group by concern (core → network → graphical). | |
| 9 | + | ||
| 10 | + | # --- core --- | |
| 11 | + | ||
| 12 | + | [[role]] | |
| 13 | + | name = "init" | |
| 14 | + | description = "Service supervisor and runlevel manager" | |
| 15 | + | tool = "s6 + s6-rc" | |
| 16 | + | packages = ["s6", "s6-rc"] | |
| 17 | + | config_paths = ["/etc/s6/", "/etc/s6-rc/"] | |
| 18 | + | ||
| 19 | + | [[role]] | |
| 20 | + | name = "shell-login" | |
| 21 | + | description = "POSIX /bin/sh" | |
| 22 | + | tool = "busybox ash" | |
| 23 | + | packages = ["busybox"] | |
| 24 | + | config_paths = ["/etc/profile", "/etc/profile.d/"] | |
| 25 | + | ||
| 26 | + | [[role]] | |
| 27 | + | name = "shell-interactive" | |
| 28 | + | description = "Login interactive shell with structured pipelines" | |
| 29 | + | tool = "nushell" | |
| 30 | + | packages = ["nushell"] | |
| 31 | + | config_paths = ["~/.config/nushell/"] | |
| 32 | + | ||
| 33 | + | [[role]] | |
| 34 | + | name = "editor" | |
| 35 | + | description = "Modal editor with LSP and tree-sitter" | |
| 36 | + | tool = "helix" | |
| 37 | + | packages = ["helix"] | |
| 38 | + | config_paths = ["~/.config/helix/"] | |
| 39 | + | ||
| 40 | + | [[role]] | |
| 41 | + | name = "multiplexer" | |
| 42 | + | description = "Terminal multiplexer with discoverable keybindings" | |
| 43 | + | tool = "zellij" | |
| 44 | + | packages = ["zellij"] | |
| 45 | + | config_paths = ["~/.config/zellij/"] | |
| 46 | + | ||
| 47 | + | [[role]] | |
| 48 | + | name = "container-runtime" | |
| 49 | + | description = "Rootless container runtime" | |
| 50 | + | tool = "podman" | |
| 51 | + | packages = ["podman"] | |
| 52 | + | config_paths = ["/etc/containers/", "~/.config/containers/"] | |
| 53 | + | ||
| 54 | + | [[role]] | |
| 55 | + | name = "filesystem" | |
| 56 | + | description = "Root filesystem and pool/dataset management" | |
| 57 | + | tool = "ZFS" | |
| 58 | + | packages = ["zfs", "zfs-lts"] | |
| 59 | + | config_paths = ["/etc/zfs/"] | |
| 60 | + | ||
| 61 | + | [[role]] | |
| 62 | + | name = "ssh" | |
| 63 | + | description = "SSH server, hardened defaults" | |
| 64 | + | tool = "OpenSSH" | |
| 65 | + | packages = ["openssh"] | |
| 66 | + | config_paths = ["/etc/ssh/sshd_config", "/etc/ssh/sshd_config.d/"] | |
| 67 | + | ||
| 68 | + | # --- network --- | |
| 69 | + | ||
| 70 | + | [[role]] | |
| 71 | + | name = "firewall" | |
| 72 | + | description = "Stateful packet filter, default-deny" | |
| 73 | + | tool = "nftables" | |
| 74 | + | packages = ["nftables"] | |
| 75 | + | config_paths = ["/etc/nftables.nft", "/etc/nftables.d/"] | |
| 76 | + | ||
| 77 | + | [[role]] | |
| 78 | + | name = "resolver" | |
| 79 | + | description = "Local validating recursive DNS resolver" | |
| 80 | + | tool = "unbound" | |
| 81 | + | packages = ["unbound"] | |
| 82 | + | config_paths = ["/etc/unbound/unbound.conf", "/etc/unbound/conf.d/"] | |
| 83 | + | ||
| 84 | + | [[role]] | |
| 85 | + | name = "time" | |
| 86 | + | description = "NTP client and time synchronisation" | |
| 87 | + | tool = "chrony" | |
| 88 | + | packages = ["chrony"] | |
| 89 | + | config_paths = ["/etc/chrony/chrony.conf"] | |
| 90 | + | ||
| 91 | + | [[role]] | |
| 92 | + | name = "network-wired" | |
| 93 | + | description = "Wired interface configuration" | |
| 94 | + | tool = "ifupdown-ng" | |
| 95 | + | packages = ["ifupdown-ng"] | |
| 96 | + | config_paths = ["/etc/network/interfaces", "/etc/network/interfaces.d/"] | |
| 97 | + | ||
| 98 | + | [[role]] | |
| 99 | + | name = "network-wireless" | |
| 100 | + | description = "Wireless interface daemon" | |
| 101 | + | tool = "iwd" | |
| 102 | + | packages = ["iwd"] | |
| 103 | + | config_paths = ["/etc/iwd/", "/var/lib/iwd/"] | |
| 104 | + | ||
| 105 | + | [[role]] | |
| 106 | + | name = "mesh" | |
| 107 | + | description = "Overlay mesh VPN with mutual auth" | |
| 108 | + | tool = "Nebula" | |
| 109 | + | packages = ["nebula"] | |
| 110 | + | config_paths = ["/etc/nebula/"] | |
| 111 | + | ||
| 112 | + | [[role]] | |
| 113 | + | name = "reverse-proxy" | |
| 114 | + | description = "HTTPS-terminating reverse proxy with auto-TLS" | |
| 115 | + | tool = "Caddy" | |
| 116 | + | packages = ["caddy"] | |
| 117 | + | config_paths = ["/etc/caddy/Caddyfile", "/etc/caddy/conf.d/"] | |
| 118 | + | ||
| 119 | + | [[role]] | |
| 120 | + | name = "mail-outbound" | |
| 121 | + | description = "Outbound mail relay (notifications, alerts)" | |
| 122 | + | tool = "msmtp" | |
| 123 | + | packages = ["msmtp"] | |
| 124 | + | config_paths = ["/etc/msmtprc"] | |
| 125 | + | ||
| 126 | + | # --- observability / data --- | |
| 127 | + | ||
| 128 | + | [[role]] | |
| 129 | + | name = "logs-router" | |
| 130 | + | description = "Structured log collection and routing" | |
| 131 | + | tool = "Vector" | |
| 132 | + | packages = ["vector"] | |
| 133 | + | config_paths = ["/etc/vector/vector.toml"] | |
| 134 | + | ||
| 135 | + | [[role]] | |
| 136 | + | name = "logs-viewer" | |
| 137 | + | description = "Interactive log browser" | |
| 138 | + | tool = "lnav" | |
| 139 | + | packages = ["lnav"] | |
| 140 | + | config_paths = ["~/.config/lnav/"] | |
| 141 | + | ||
| 142 | + | [[role]] | |
| 143 | + | name = "secrets" | |
| 144 | + | description = "Age-encrypted secrets with sops integration" | |
| 145 | + | tool = "age + sops" | |
| 146 | + | packages = ["age", "sops"] | |
| 147 | + | config_paths = ["~/.config/sops/", ".sops.yaml"] | |
| 148 | + | ||
| 149 | + | [[role]] | |
| 150 | + | name = "backup" | |
| 151 | + | description = "Encrypted, deduplicated backups" | |
| 152 | + | tool = "restic" | |
| 153 | + | packages = ["restic"] | |
| 154 | + | config_paths = ["~/.config/restic/"] | |
| 155 | + | ||
| 156 | + | # --- graphical layer (Sysadmin build only) --- | |
| 157 | + | ||
| 158 | + | [[role]] | |
| 159 | + | name = "compositor" | |
| 160 | + | description = "Tiling Wayland compositor" | |
| 161 | + | tool = "river" | |
| 162 | + | packages = ["river"] | |
| 163 | + | config_paths = ["~/.config/river/init"] | |
| 164 | + | ||
| 165 | + | [[role]] | |
| 166 | + | name = "terminal" | |
| 167 | + | description = "Wayland terminal emulator" | |
| 168 | + | tool = "foot" | |
| 169 | + | packages = ["foot"] | |
| 170 | + | config_paths = ["~/.config/foot/foot.ini"] | |
| 171 | + | ||
| 172 | + | [[role]] | |
| 173 | + | name = "bar" | |
| 174 | + | description = "Status bar (read-only system surface)" | |
| 175 | + | tool = "yambar" | |
| 176 | + | packages = ["yambar"] | |
| 177 | + | config_paths = ["~/.config/yambar/config.yml"] | |
| 178 | + | ||
| 179 | + | [[role]] | |
| 180 | + | name = "launcher" | |
| 181 | + | description = "Application launcher" | |
| 182 | + | tool = "fuzzel" | |
| 183 | + | packages = ["fuzzel"] | |
| 184 | + | config_paths = ["~/.config/fuzzel/fuzzel.ini"] | |
| 185 | + | ||
| 186 | + | [[role]] | |
| 187 | + | name = "clipboard" | |
| 188 | + | description = "Clipboard history" | |
| 189 | + | tool = "cliphist" | |
| 190 | + | packages = ["cliphist"] | |
| 191 | + | config_paths = [] | |
| 192 | + | ||
| 193 | + | [[role]] | |
| 194 | + | name = "notifications" | |
| 195 | + | description = "Wayland notification daemon" | |
| 196 | + | tool = "fnott" | |
| 197 | + | packages = ["fnott"] | |
| 198 | + | config_paths = ["~/.config/fnott/fnott.ini"] | |
| 199 | + | ||
| 200 | + | [[role]] | |
| 201 | + | name = "screenshots" | |
| 202 | + | description = "Region capture, annotation, pipeline" | |
| 203 | + | tool = "grim + slurp + swappy" | |
| 204 | + | packages = ["grim", "slurp", "swappy"] | |
| 205 | + | config_paths = ["~/.config/swappy/"] | |
| 206 | + | ||
| 207 | + | [[role]] | |
| 208 | + | name = "lock" | |
| 209 | + | description = "Screen locker" | |
| 210 | + | tool = "swaylock" | |
| 211 | + | packages = ["swaylock"] | |
| 212 | + | config_paths = ["~/.config/swaylock/config"] | |
| 213 | + | ||
| 214 | + | [[role]] | |
| 215 | + | name = "idle" | |
| 216 | + | description = "Idle event dispatcher" | |
| 217 | + | tool = "swayidle" | |
| 218 | + | packages = ["swayidle"] | |
| 219 | + | config_paths = ["~/.config/swayidle/config"] | |
| 220 | + | ||
| 221 | + | [[role]] | |
| 222 | + | name = "wallpaper" | |
| 223 | + | description = "Wallpaper / background" | |
| 224 | + | tool = "swaybg" | |
| 225 | + | packages = ["swaybg"] | |
| 226 | + | config_paths = [] | |
| 227 | + | ||
| 228 | + | [[role]] | |
| 229 | + | name = "audio" | |
| 230 | + | description = "Audio server and session manager" | |
| 231 | + | tool = "pipewire + wireplumber" | |
| 232 | + | packages = ["pipewire", "wireplumber"] | |
| 233 | + | config_paths = ["/etc/pipewire/", "~/.config/pipewire/", "~/.config/wireplumber/"] | |
| 234 | + | ||
| 235 | + | [[role]] | |
| 236 | + | name = "seat" | |
| 237 | + | description = "Seat / device management for non-root Wayland" | |
| 238 | + | tool = "seatd" | |
| 239 | + | packages = ["seatd"] | |
| 240 | + | config_paths = [] | |
| 241 | + | ||
| 242 | + | [[role]] | |
| 243 | + | name = "brightness" | |
| 244 | + | description = "Backlight control" | |
| 245 | + | tool = "brightnessctl" | |
| 246 | + | packages = ["brightnessctl"] | |
| 247 | + | config_paths = [] | |
| 248 | + | ||
| 249 | + | [[role]] | |
| 250 | + | name = "power" | |
| 251 | + | description = "Laptop power management profiles" | |
| 252 | + | tool = "tlp" | |
| 253 | + | packages = ["tlp"] | |
| 254 | + | config_paths = ["/etc/tlp.conf", "/etc/tlp.d/"] |
| @@ -0,0 +1,186 @@ | |||
| 1 | + | use crate::roles::{self, Role}; | |
| 2 | + | use anyhow::Result; | |
| 3 | + | use crossterm::event::{self, Event, KeyCode}; | |
| 4 | + | use ratatui::Frame; | |
| 5 | + | use ratatui::layout::{Constraint, Layout, Rect}; | |
| 6 | + | use ratatui::style::Style; | |
| 7 | + | use ratatui::text::{Line, Span}; | |
| 8 | + | use ratatui::widgets::Paragraph; | |
| 9 | + | use sysop_tui::{Footer, GlobalAction, classify, hint, layout, palette, selection, style}; | |
| 10 | + | ||
| 11 | + | enum View { | |
| 12 | + | List { selected: usize }, | |
| 13 | + | Detail { idx: usize }, | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | pub fn run(initial: Option<String>) -> Result<i32> { | |
| 17 | + | let roles = roles::load()?; | |
| 18 | + | ||
| 19 | + | let mut view = match initial.as_deref() { | |
| 20 | + | None => View::List { selected: 0 }, | |
| 21 | + | Some(name) => match roles::find(&roles, name) { | |
| 22 | + | Some(_) => View::Detail { | |
| 23 | + | idx: roles.iter().position(|r| r.name == name).unwrap(), | |
| 24 | + | }, | |
| 25 | + | None => { | |
| 26 | + | eprintln!("sysop: unknown role: {name}"); | |
| 27 | + | eprintln!(" run `sysop info` for the list"); | |
| 28 | + | return Ok(1); | |
| 29 | + | } | |
| 30 | + | }, | |
| 31 | + | }; | |
| 32 | + | ||
| 33 | + | let mut terminal = ratatui::init(); | |
| 34 | + | let result = event_loop(&mut terminal, &roles, &mut view); | |
| 35 | + | ratatui::restore(); | |
| 36 | + | result.map(|_| 0) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | fn event_loop( | |
| 40 | + | terminal: &mut ratatui::DefaultTerminal, | |
| 41 | + | roles: &[Role], | |
| 42 | + | view: &mut View, | |
| 43 | + | ) -> Result<()> { | |
| 44 | + | loop { | |
| 45 | + | terminal.draw(|frame| draw(frame, roles, view))?; | |
| 46 | + | ||
| 47 | + | let Event::Key(key) = event::read()? else { | |
| 48 | + | continue; | |
| 49 | + | }; | |
| 50 | + | let action = classify(key); | |
| 51 | + | ||
| 52 | + | match view { | |
| 53 | + | View::List { selected } => match action { | |
| 54 | + | GlobalAction::Quit => return Ok(()), | |
| 55 | + | GlobalAction::Passthrough => match key.code { | |
| 56 | + | KeyCode::Char('j') | KeyCode::Down if *selected + 1 < roles.len() => { | |
| 57 | + | *selected += 1; | |
| 58 | + | } | |
| 59 | + | KeyCode::Char('k') | KeyCode::Up => { | |
| 60 | + | *selected = selected.saturating_sub(1); | |
| 61 | + | } | |
| 62 | + | KeyCode::Enter => { | |
| 63 | + | *view = View::Detail { idx: *selected }; | |
| 64 | + | } | |
| 65 | + | _ => {} | |
| 66 | + | }, | |
| 67 | + | _ => {} | |
| 68 | + | }, | |
| 69 | + | View::Detail { idx } => match action { | |
| 70 | + | GlobalAction::Quit => return Ok(()), | |
| 71 | + | GlobalAction::Back => { | |
| 72 | + | *view = View::List { selected: *idx }; | |
| 73 | + | } | |
| 74 | + | GlobalAction::Passthrough => match key.code { | |
| 75 | + | KeyCode::Char('j') | KeyCode::Down if *idx + 1 < roles.len() => { | |
| 76 | + | *idx += 1; | |
| 77 | + | } | |
| 78 | + | KeyCode::Char('k') | KeyCode::Up => { | |
| 79 | + | *idx = idx.saturating_sub(1); | |
| 80 | + | } | |
| 81 | + | _ => {} | |
| 82 | + | }, | |
| 83 | + | _ => {} | |
| 84 | + | }, | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | fn draw(frame: &mut Frame, roles: &[Role], view: &View) { | |
| 90 | + | let [body, foot] = layout::split(frame.area()); | |
| 91 | + | match view { | |
| 92 | + | View::List { selected } => { | |
| 93 | + | frame.render_widget(list_widget(roles, *selected), body); | |
| 94 | + | frame.render_widget( | |
| 95 | + | &Footer::new([ | |
| 96 | + | hint("j/k", "move"), | |
| 97 | + | hint("Enter", "details"), | |
| 98 | + | hint("q", "quit"), | |
| 99 | + | ]), | |
| 100 | + | foot, | |
| 101 | + | ); | |
| 102 | + | } | |
| 103 | + | View::Detail { idx } => { | |
| 104 | + | draw_detail(frame, &roles[*idx], body); | |
| 105 | + | frame.render_widget( | |
| 106 | + | &Footer::new([ | |
| 107 | + | hint("j/k", "next/prev role"), | |
| 108 | + | hint("Esc", "back"), | |
| 109 | + | hint("q", "quit"), | |
| 110 | + | ]), | |
| 111 | + | foot, | |
| 112 | + | ); | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | fn list_widget(roles: &[Role], selected: usize) -> Paragraph<'_> { | |
| 118 | + | let name_w = roles.iter().map(|r| r.name.len()).max().unwrap_or(0); | |
| 119 | + | let lines: Vec<Line> = roles | |
| 120 | + | .iter() | |
| 121 | + | .enumerate() | |
| 122 | + | .map(|(i, r)| { | |
| 123 | + | let is_sel = i == selected; | |
| 124 | + | let marker = if is_sel { selection::MARKER } else { " " }; | |
| 125 | + | let line = Line::from(vec![ | |
| 126 | + | Span::raw(format!(" {marker} ")), | |
| 127 | + | Span::raw(format!("{:<name_w$}", r.name)), | |
| 128 | + | Span::raw(" "), | |
| 129 | + | style::dim(r.tool.clone()), | |
| 130 | + | ]); | |
| 131 | + | if is_sel { | |
| 132 | + | line.style(selection::selected_style()) | |
| 133 | + | } else { | |
| 134 | + | line | |
| 135 | + | } | |
| 136 | + | }) | |
| 137 | + | .collect(); | |
| 138 | + | Paragraph::new(lines).style(Style::default().bg(palette::BG).fg(palette::FG)) | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | fn draw_detail(frame: &mut Frame, role: &Role, area: Rect) { | |
| 142 | + | let [_, right] = | |
| 143 | + | Layout::horizontal([Constraint::Length(2), Constraint::Min(0)]).areas(area); | |
| 144 | + | let [_, body] = | |
| 145 | + | Layout::vertical([Constraint::Length(1), Constraint::Min(0)]).areas(right); | |
| 146 | + | ||
| 147 | + | let label_w = 12; | |
| 148 | + | let mut lines: Vec<Line> = Vec::new(); | |
| 149 | + | ||
| 150 | + | lines.push(field_line("role", &role.name, label_w)); | |
| 151 | + | lines.push(field_line("tool", &role.tool, label_w)); | |
| 152 | + | lines.push(field_line( | |
| 153 | + | "packages", | |
| 154 | + | &role.packages.join(", "), | |
| 155 | + | label_w, | |
| 156 | + | )); | |
| 157 | + | ||
| 158 | + | if role.config_paths.is_empty() { | |
| 159 | + | lines.push(field_line("config", "(none)", label_w)); | |
| 160 | + | } else { | |
| 161 | + | lines.push(field_line("config", &role.config_paths[0], label_w)); | |
| 162 | + | for extra in &role.config_paths[1..] { | |
| 163 | + | lines.push(continuation_line(extra, label_w)); | |
| 164 | + | } | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | lines.push(Line::from("")); | |
| 168 | + | lines.push(field_line("description", &role.description, label_w)); | |
| 169 | + | ||
| 170 | + | let para = Paragraph::new(lines).style(Style::default().bg(palette::BG).fg(palette::FG)); | |
| 171 | + | frame.render_widget(para, body); | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | fn field_line(label: &str, value: &str, label_w: usize) -> Line<'static> { | |
| 175 | + | Line::from(vec![ | |
| 176 | + | style::dim(format!("{:<label_w$} ", label)), | |
| 177 | + | style::fg(value.to_string()), | |
| 178 | + | ]) | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | fn continuation_line(value: &str, label_w: usize) -> Line<'static> { | |
| 182 | + | Line::from(vec![ | |
| 183 | + | Span::raw(" ".repeat(label_w + 2)), | |
| 184 | + | style::fg(value.to_string()), | |
| 185 | + | ]) | |
| 186 | + | } |
| @@ -1,3 +1,28 @@ | |||
| 1 | + | mod info; | |
| 2 | + | mod roles; | |
| 3 | + | ||
| 4 | + | use clap::{Parser, Subcommand}; | |
| 5 | + | ||
| 6 | + | #[derive(Parser)] | |
| 7 | + | #[command(name = "sysop", about = "Mountaineer operator interface")] | |
| 8 | + | struct Cli { | |
| 9 | + | #[command(subcommand)] | |
| 10 | + | command: Command, | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | #[derive(Subcommand)] | |
| 14 | + | enum Command { | |
| 15 | + | /// Show which tool fills a role and where its config lives | |
| 16 | + | Info { | |
| 17 | + | /// Role name (omit for the full list) | |
| 18 | + | role: Option<String>, | |
| 19 | + | }, | |
| 20 | + | } | |
| 21 | + | ||
| 1 | 22 | fn main() -> anyhow::Result<()> { | |
| 2 | - | Ok(()) | |
| 23 | + | let cli = Cli::parse(); | |
| 24 | + | let code = match cli.command { | |
| 25 | + | Command::Info { role } => info::run(role)?, | |
| 26 | + | }; | |
| 27 | + | std::process::exit(code); | |
| 3 | 28 | } |
| @@ -0,0 +1,27 @@ | |||
| 1 | + | use anyhow::{Context, Result}; | |
| 2 | + | use serde::Deserialize; | |
| 3 | + | ||
| 4 | + | const EMBEDDED: &str = include_str!("../data/roles.toml"); | |
| 5 | + | ||
| 6 | + | #[derive(Debug, Clone, Deserialize)] | |
| 7 | + | pub struct Role { | |
| 8 | + | pub name: String, | |
| 9 | + | pub description: String, | |
| 10 | + | pub tool: String, | |
| 11 | + | pub packages: Vec<String>, | |
| 12 | + | pub config_paths: Vec<String>, | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | #[derive(Debug, Deserialize)] | |
| 16 | + | struct Registry { | |
| 17 | + | role: Vec<Role>, | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | pub fn load() -> Result<Vec<Role>> { | |
| 21 | + | let reg: Registry = toml::from_str(EMBEDDED).context("parse embedded roles.toml")?; | |
| 22 | + | Ok(reg.role) | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | pub fn find<'a>(roles: &'a [Role], name: &str) -> Option<&'a Role> { | |
| 26 | + | roles.iter().find(|r| r.name == name) | |
| 27 | + | } |
| @@ -1,22 +1,18 @@ | |||
| 1 | 1 | # mountaineer-sysop — todo | |
| 2 | 2 | ||
| 3 | - | Status: Pre-v0. Active: `sysop info <role>` as first subcommand. Next: `sysop motd on/off`. Primitives crate has palette, layout, footer, selection, keys; exercised by `cargo run --example smoketest -p sysop-tui`. | |
| 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`. | |
| 4 | 4 | ||
| 5 | 5 | ## Phase 1 — sysop-tui primitives (open items) | |
| 6 | 6 | ||
| 7 | - | - [ ] Visually verify the smoketest on a flatwhite-friendly terminal (Berkeley Mono / Iosevka, true-color) and tune `SELECTION_BG` if the row contrast is too soft | |
| 8 | - | - [ ] Decide whether `Footer` should right-align when there's a status segment on the left (deferred until first subcommand needs it) | |
| 9 | - | - [ ] Help overlay primitive (`?` action currently just classifies; no overlay renderer yet) | |
| 7 | + | - [ ] Visually verify on a flatwhite-friendly terminal (Iosevka, true-color) and tune `SELECTION_BG` if the row contrast is too soft | |
| 8 | + | - [ ] `Footer` right-align option (deferred until a subcommand needs a left status segment) | |
| 9 | + | - [ ] Help overlay primitive (`?` action classifies; no overlay renderer yet) | |
| 10 | 10 | ||
| 11 | - | ## Phase 2 — first subcommand: `sysop info <role>` | |
| 11 | + | ## Phase 2 — `sysop info` (open items) | |
| 12 | 12 | ||
| 13 | - | The smallest useful subcommand. Resolves the role→tool mapping (principle 15). Read-only, single screen, exercises sysop-tui end-to-end. | |
| 14 | - | ||
| 15 | - | - [ ] Role→tool table source-of-truth (location TBD — `STACK.md`? embedded TOML? generated?) | |
| 16 | - | - [ ] CLI parsing (clap derive, single positional arg) | |
| 17 | - | - [ ] TUI screen: role name, current tool, config path, package name, version | |
| 18 | - | - [ ] `sysop info` (no arg) → list view of all roles, selectable, Enter → detail screen | |
| 19 | - | - [ ] Exit code 0 on found, 1 on unknown role | |
| 13 | + | - [ ] Version field on the detail screen (requires per-tool version queries — shell out to `<tool> --version`, parse, cache; defer until subprocess plumbing is settled) | |
| 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 | + | - [ ] Server vs Sysadmin profile filter — graphical-layer roles should be hidden in `--server` mode | |
| 20 | 16 | ||
| 21 | 17 | ## Phase 3 — `sysop motd on/off` | |
| 22 | 18 |