atagen

monolith

public
Star0
Watch0

Commit 5b7cddd2e177

tarball

atagen <boss@atagen.co>2026-10-16 10:44 UTC

5 files changed+112-33
monolith: parse the command line with pound

Any argument that was not a client subcommand used to be taken as a program to start, so `monolith --help` launched a second compositor spawning `--help`. Starting a program now goes through an explicit `run` subcommand, and bare `monolith` still runs the compositor.
Modified file: Cargo.lock+40 additions
@@ -400,6 +400,7 @@ dependencies = [
400400 "monolith-layout",
401401 "monolith-protocols",
402402 "monolith-render-vk",
403+ "pound",
403404 "serde_json",
404405 "slotmap",
405406 "smithay",
@@ -535,6 +536,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
535536 checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
536537
537538 [[package]]
539+name = "pound"
540+version = "0.5.2"
541+source = "registry+https://github.com/rust-lang/crates.io-index"
542+checksum = "88a1548289dfa12bd53954303a19ef93cfc3d834f9211e3e182bf12aef41f003"
543+dependencies = [
544+ "pound-derive",
545+]
546+
547+[[package]]
548+name = "pound-derive"
549+version = "0.5.2"
550+source = "registry+https://github.com/rust-lang/crates.io-index"
551+checksum = "0b0f793b0c8b1db299b1a3bd960b8bd7b5aadc58b2291b647c8c0faa91b14af4"
552+dependencies = [
553+ "pound-derive-impl",
554+]
555+
556+[[package]]
557+name = "pound-derive-impl"
558+version = "0.5.2"
559+source = "registry+https://github.com/rust-lang/crates.io-index"
560+checksum = "8a6ddc2609d7787602494304b1b676676cddcc2f775db9aae178d4f47c6b7a45"
561+dependencies = [
562+ "proc-macro2",
563+ "quote",
564+ "venial",
565+]
566+
567+[[package]]
538568 name = "presser"
539569 version = "0.3.1"
540570 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1088,6 +1118,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
10881118 checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
10891119
10901120 [[package]]
1121+name = "venial"
1122+version = "0.6.1"
1123+source = "registry+https://github.com/rust-lang/crates.io-index"
1124+checksum = "9a42528baceab6c7784446df2a10f4185078c39bf73dc614f154353f1a6b1229"
1125+dependencies = [
1126+ "proc-macro2",
1127+ "quote",
1128+]
1129+
1130+[[package]]
10911131 name = "version_check"
10921132 version = "0.9.5"
10931133 source = "registry+https://github.com/rust-lang/crates.io-index"
Modified file: crates/monolith/Cargo.toml+1 additions
@@ -18,6 +18,7 @@ monolith-ipc.workspace = true
1818 monolith-layout.workspace = true
1919 monolith-protocols.workspace = true
2020 monolith-render-vk.workspace = true
21+pound = "0.5"
2122 serde_json.workspace = true
2223 slotmap.workspace = true
2324 smithay = { workspace = true, features = ["desktop"] }
Modified file: crates/monolith/src/ipc.rs+5 additions−19 deletions
@@ -2,7 +2,7 @@
22 //! subcommands that talk to it.
33
44 use crate::state::{State, identity};
5-use monolith_ipc::{Answer, Command, Envelope, Query, Reply, Request, VERSION};
5+use monolith_ipc::{Answer, Envelope, Query, Reply, Request, VERSION};
66 use smithay::reexports::calloop::generic::Generic;
77 use smithay::reexports::calloop::{EventLoop, Interest, LoopHandle, Mode, PostAction};
88 use std::io::{BufRead, BufReader, ErrorKind, Read, Write};
@@ -208,26 +208,12 @@ fn snapshot(state: &State) -> monolith_ipc::State {
208208 }
209209 }
210210
211-/// Runs a client subcommand, if `args` names one.
212-pub fn client(args: &[String]) -> Option<ExitCode> {
213- let (sub, rest) = args.split_first()?;
214- let request = match sub.as_str() {
215- "msg" => match Command::parse(rest) {
216- Ok(cmd) => Request::Run(cmd),
217- Err(e) => return Some(fail(&e.to_string())),
218- },
219- "get" => match rest {
220- [q] if q == "state" => Request::Get(Query::State),
221- [q] if q == "version" => Request::Get(Query::Version),
222- _ => return Some(fail("get takes state or version")),
223- },
224- "subscribe" => Request::Subscribe {},
225- _ => return None,
226- };
227- Some(talk(request))
211+/// Sends `request` to the running compositor and prints the reply.
212+pub fn client(request: Request) -> ExitCode {
213+ talk(request)
228214 }
229215
230-fn fail(msg: &str) -> ExitCode {
216+pub fn fail(msg: &str) -> ExitCode {
231217 eprintln!("monolith: {msg}");
232218 ExitCode::FAILURE
233219 }
Modified file: crates/monolith/src/main.rs+64 additions−13 deletions
@@ -6,23 +6,75 @@ mod launch;
66 mod nested;
77 mod state;
88
9+use monolith_ipc::{Command, Query, Request};
10+use pound::{Parse, ValueEnum};
911 use smithay::reexports::calloop::EventLoop;
1012 use smithay::reexports::wayland_server::Display;
1113 use state::State;
14+use std::path::PathBuf;
1215 use std::process::ExitCode;
1316 use tracing_subscriber::EnvFilter;
1417
18+/// a tiling Wayland compositor
19+#[derive(Parse)]
20+#[pound(name = "monolith")]
21+struct Cli {
22+ #[pound(subcommand)]
23+ cmd: Option<Cmd>,
24+}
25+
26+#[derive(Parse)]
27+enum Cmd {
28+ /// run the compositor, the default
29+ Run {
30+ /// read this config instead of ~/.config/monolith/config.toml
31+ #[pound(long)]
32+ config: Option<String>,
33+ /// a program to start inside, with its arguments
34+ #[pound(trailing)]
35+ program: Vec<String>,
36+ },
37+ /// send a command to the running compositor, e.g. `msg focus left`
38+ Msg {
39+ #[pound(trailing)]
40+ command: Vec<String>,
41+ },
42+ /// print the running compositor's state or version
43+ Get { what: What },
44+ /// print the state as a JSON line after every change
45+ Subscribe,
46+}
47+
48+#[derive(ValueEnum)]
49+enum What {
50+ State,
51+ Version,
52+}
53+
1554 fn main() -> ExitCode {
16- let args: Vec<String> = std::env::args().skip(1).collect();
17- if let Some(code) = ipc::client(&args) {
18- return code;
19- }
55+ let (config, program) = match Cli::parse().cmd {
56+ None => (None, Vec::new()),
57+ Some(Cmd::Run { config, program }) => (config.map(PathBuf::from), program),
58+ Some(Cmd::Msg { command }) => {
59+ return match Command::parse(&command) {
60+ Ok(cmd) => ipc::client(Request::Run(cmd)),
61+ Err(e) => ipc::fail(&e.to_string()),
62+ };
63+ }
64+ Some(Cmd::Get { what }) => {
65+ return ipc::client(Request::Get(match what {
66+ What::State => Query::State,
67+ What::Version => Query::Version,
68+ }));
69+ }
70+ Some(Cmd::Subscribe) => return ipc::client(Request::Subscribe {}),
71+ };
2072 tracing_subscriber::fmt()
2173 .with_env_filter(
2274 EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
2375 )
2476 .init();
25- match run(&args) {
77+ match run(config, &program) {
2678 Ok(()) => ExitCode::SUCCESS,
2779 Err(e) => {
2880 tracing::error!("{e}");
@@ -31,20 +83,19 @@ fn main() -> ExitCode {
3183 }
3284 }
3385
34-/// Runs the compositor. Any arguments are a program to start inside it.
35-fn run(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
86+fn run(config: Option<PathBuf>, program: &[String]) -> Result<(), Box<dyn std::error::Error>> {
3687 let mut event_loop: EventLoop<'static, State> = EventLoop::try_new()?;
3788 let display: Display<State> = Display::new()?;
38- let mut state = State::new(&mut event_loop, display)?;
89+ let mut state = State::new(&mut event_loop, display, config)?;
3990 nested::init(&mut event_loop, &mut state)?;
4091 ipc::init(&event_loop, &mut state)?;
4192 tracing::info!(socket = ?state.socket_name, device = state.renderer.device_name(), "monolith running nested");
4293
43- let startup = state.config.startup.clone();
44- for argv in startup
45- .iter()
46- .chain((!args.is_empty()).then_some(&args.to_vec()))
47- {
94+ let mut startup = state.config.startup.clone();
95+ if !program.is_empty() {
96+ startup.push(program.to_vec());
97+ }
98+ for argv in &startup {
4899 if let Err(e) = state.spawn(argv) {
49100 tracing::warn!("startup: {e}");
50101 }
Modified file: crates/monolith/src/state.rs+2 additions−1 deletions
@@ -71,9 +71,10 @@ impl State {
7171 pub fn new(
7272 event_loop: &mut EventLoop<State>,
7373 display: Display<State>,
74+ config_path: Option<PathBuf>,
7475 ) -> Result<Self, Box<dyn std::error::Error>> {
7576 let dh = display.handle();
76- let config_path = Config::path();
77+ let config_path = config_path.or_else(Config::path);
7778 let config = Config::load(config_path.as_deref()).unwrap_or_else(|e| {
7879 tracing::error!("config: {e}, using the built-in default");
7980 Config::parse(monolith_config::DEFAULT).expect("the default config parses")