Rust · 4493 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!(" promote_stack Move the focused stack window to the top");
43 eprintln!(" workspace <1-10> Switch workspace");
44 eprintln!(" move-to-workspace <1-10> Move window to workspace");
45 eprintln!(" toggle-floating Toggle floating state");
46 eprintln!(" reload Hot reload config");
47 eprintln!(" get-workspaces Get workspace info");
48 eprintln!(" get-focused Get focused window info");
49 eprintln!(" get-windows Get all window info");
50 eprintln!(" get-monitors Get monitor info");
51 eprintln!(" get-tree [workspace] Get layout tree for workspace");
52 eprintln!(
53 " subscribe [event_types...] Stream events (workspace_changed, window_focused, etc.)"
54 );
55 eprintln!(" exec <command> Execute shell command");
56 std::process::exit(1);
57 }
58
59 let command = args[0].clone();
60 let cmd_args = args[1..].to_vec();
61
62 let request = Request {
63 command,
64 args: cmd_args,
65 };
66
67 let path = socket_path();
68 let stream = match UnixStream::connect(&path) {
69 Ok(s) => s,
70 Err(e) => {
71 eprintln!("error: could not connect to tarmac at {:?}: {}", path, e);
72 eprintln!("is tarmac running?");
73 std::process::exit(1);
74 }
75 };
76
77 let is_subscribe = request.command == "subscribe";
78
79 let mut writer = &stream;
80 let json = serde_json::to_string(&request).expect("serialize request");
81 writer.write_all(json.as_bytes()).expect("write request");
82 writer.write_all(b"\n").expect("write newline");
83 writer.flush().expect("flush");
84
85 // For subscribe, keep connection open for streaming.
86 // For regular commands, shut down write side.
87 if !is_subscribe {
88 stream.shutdown(std::net::Shutdown::Write).ok();
89 }
90
91 let reader = BufReader::new(&stream);
92 for line in reader.lines() {
93 let line = match line {
94 Ok(l) => l,
95 Err(_) => break,
96 };
97
98 if line.trim().is_empty() {
99 continue;
100 }
101
102 if is_subscribe {
103 // In subscribe mode, print each event line directly.
104 // The first line is the ack response, then streaming events.
105 println!("{}", line);
106 continue;
107 }
108
109 let response: Response = match serde_json::from_str(&line) {
110 Ok(r) => r,
111 Err(e) => {
112 eprintln!("error: invalid response: {}", e);
113 std::process::exit(1);
114 }
115 };
116
117 if response.success {
118 if let Some(data) = response.data {
119 println!("{}", serde_json::to_string_pretty(&data).unwrap());
120 }
121 } else {
122 eprintln!(
123 "error: {}",
124 response.error.unwrap_or_else(|| "unknown".to_string())
125 );
126 std::process::exit(1);
127 }
128 }
129 }
130