Rust · 4398 bytes Raw Blame History
1 use std::io::{BufRead, BufReader, Write};
2 use std::os::unix::net::UnixStream;
3
4 use serde::{Deserialize, Serialize};
5
6 #[derive(Serialize)]
7 struct Request {
8 command: String,
9 args: Vec<String>,
10 }
11
12 #[derive(Deserialize)]
13 struct Response {
14 success: bool,
15 data: Option<serde_json::Value>,
16 error: Option<String>,
17 }
18
19 fn socket_path() -> std::path::PathBuf {
20 if let Ok(path) = std::env::var("TARMAC_SOCKET") {
21 return std::path::PathBuf::from(path);
22 }
23 let user = std::env::var("USER").unwrap_or_else(|_| "unknown".to_string());
24 std::path::PathBuf::from(format!("/tmp/tarmac-{}.sock", user))
25 }
26
27 fn main() {
28 let args: Vec<String> = std::env::args().skip(1).collect();
29
30 if args.is_empty() {
31 eprintln!("tarmacctl — control tarmac window manager");
32 eprintln!();
33 eprintln!("Usage: tarmacctl <command> [args...]");
34 eprintln!();
35 eprintln!("Commands:");
36 eprintln!(" focus <left|right|up|down> Focus window in direction");
37 eprintln!(" swap <left|right|up|down> Swap with window in direction");
38 eprintln!(" resize <left|right|up|down> Resize split in direction");
39 eprintln!(" close Close focused window");
40 eprintln!(" equalize Equalize all splits");
41 eprintln!(" unstack Restore the focused stacked subtree");
42 eprintln!(" workspace <1-10> Switch workspace");
43 eprintln!(" move-to-workspace <1-10> Move window to workspace");
44 eprintln!(" toggle-floating Toggle floating state");
45 eprintln!(" reload Hot reload config");
46 eprintln!(" get-workspaces Get workspace info");
47 eprintln!(" get-focused Get focused window info");
48 eprintln!(" get-windows Get all window info");
49 eprintln!(" get-monitors Get monitor info");
50 eprintln!(" get-tree [workspace] Get layout tree for workspace");
51 eprintln!(
52 " subscribe [event_types...] Stream events (workspace_changed, window_focused, etc.)"
53 );
54 eprintln!(" exec <command> Execute shell command");
55 std::process::exit(1);
56 }
57
58 let command = args[0].clone();
59 let cmd_args = args[1..].to_vec();
60
61 let request = Request {
62 command,
63 args: cmd_args,
64 };
65
66 let path = socket_path();
67 let stream = match UnixStream::connect(&path) {
68 Ok(s) => s,
69 Err(e) => {
70 eprintln!("error: could not connect to tarmac at {:?}: {}", path, e);
71 eprintln!("is tarmac running?");
72 std::process::exit(1);
73 }
74 };
75
76 let is_subscribe = request.command == "subscribe";
77
78 let mut writer = &stream;
79 let json = serde_json::to_string(&request).expect("serialize request");
80 writer.write_all(json.as_bytes()).expect("write request");
81 writer.write_all(b"\n").expect("write newline");
82 writer.flush().expect("flush");
83
84 // For subscribe, keep connection open for streaming.
85 // For regular commands, shut down write side.
86 if !is_subscribe {
87 stream.shutdown(std::net::Shutdown::Write).ok();
88 }
89
90 let reader = BufReader::new(&stream);
91 for line in reader.lines() {
92 let line = match line {
93 Ok(l) => l,
94 Err(_) => break,
95 };
96
97 if line.trim().is_empty() {
98 continue;
99 }
100
101 if is_subscribe {
102 // In subscribe mode, print each event line directly.
103 // The first line is the ack response, then streaming events.
104 println!("{}", line);
105 continue;
106 }
107
108 let response: Response = match serde_json::from_str(&line) {
109 Ok(r) => r,
110 Err(e) => {
111 eprintln!("error: invalid response: {}", e);
112 std::process::exit(1);
113 }
114 };
115
116 if response.success {
117 if let Some(data) = response.data {
118 println!("{}", serde_json::to_string_pretty(&data).unwrap());
119 }
120 } else {
121 eprintln!(
122 "error: {}",
123 response.error.unwrap_or_else(|| "unknown".to_string())
124 );
125 std::process::exit(1);
126 }
127 }
128 }
129