Rust · 5580 bytes Raw Blame History
1 use std::io::{BufRead, BufReader, Write};
2 use std::os::unix::net::UnixStream;
3 use std::path::PathBuf;
4
5 use clap::{Parser, Subcommand};
6 use serde_json::{json, Value};
7
8 #[derive(Parser)]
9 #[command(name = "garctl")]
10 #[command(about = "Control the gar window manager")]
11 struct Cli {
12 #[command(subcommand)]
13 command: Command,
14 }
15
16 #[derive(Subcommand)]
17 enum Command {
18 /// Focus window in direction
19 Focus {
20 /// Direction: left, right, up, down
21 direction: String,
22 },
23 /// Swap with window in direction
24 Swap {
25 /// Direction: left, right, up, down
26 direction: String,
27 },
28 /// Resize split in direction
29 Resize {
30 /// Direction: left, right, up, down
31 direction: String,
32 /// Resize amount (default: 0.05)
33 #[arg(default_value = "0.05")]
34 amount: f32,
35 },
36 /// Close focused window
37 Close,
38 /// Switch to workspace
39 Workspace {
40 /// Workspace number (1-10)
41 number: usize,
42 },
43 /// Move focused window to workspace
44 MoveToWorkspace {
45 /// Workspace number (1-10)
46 number: usize,
47 },
48 /// Toggle floating state of focused window
49 ToggleFloating,
50 /// Equalize split ratios
51 Equalize,
52 /// Reload configuration
53 Reload,
54 /// Exit gar
55 Exit,
56 /// Get workspace information
57 GetWorkspaces,
58 /// Get focused window information
59 GetFocused,
60 /// Get window tree
61 GetTree,
62 /// Focus monitor (next, prev, or name)
63 FocusMonitor {
64 /// Target: next, prev, left, right, or monitor name
65 target: String,
66 },
67 /// Move focused window to monitor
68 MoveToMonitor {
69 /// Target: next, prev, left, right, or monitor name
70 target: String,
71 },
72 /// Get monitor information
73 GetMonitors,
74 }
75
76 fn get_socket_path() -> PathBuf {
77 std::env::var("XDG_RUNTIME_DIR")
78 .map(|dir| PathBuf::from(dir).join("gar.sock"))
79 .unwrap_or_else(|_| PathBuf::from("/tmp/gar.sock"))
80 }
81
82 fn send_command(request: Value) -> Result<Value, String> {
83 let socket_path = get_socket_path();
84
85 let mut stream = UnixStream::connect(&socket_path)
86 .map_err(|e| format!("Failed to connect to gar (is it running?): {}", e))?;
87
88 // Send request
89 let json = serde_json::to_string(&request)
90 .map_err(|e| format!("Failed to serialize request: {}", e))?;
91 writeln!(stream, "{}", json)
92 .map_err(|e| format!("Failed to send request: {}", e))?;
93
94 // Read response
95 let mut reader = BufReader::new(stream);
96 let mut response = String::new();
97 reader.read_line(&mut response)
98 .map_err(|e| format!("Failed to read response: {}", e))?;
99
100 serde_json::from_str(&response)
101 .map_err(|e| format!("Failed to parse response: {}", e))
102 }
103
104 fn main() {
105 let cli = Cli::parse();
106
107 let request = match cli.command {
108 Command::Focus { direction } => {
109 json!({ "command": "focus", "args": { "direction": direction } })
110 }
111 Command::Swap { direction } => {
112 json!({ "command": "swap", "args": { "direction": direction } })
113 }
114 Command::Resize { direction, amount } => {
115 json!({ "command": "resize", "args": { "direction": direction, "amount": amount } })
116 }
117 Command::Close => {
118 json!({ "command": "close", "args": {} })
119 }
120 Command::Workspace { number } => {
121 json!({ "command": "workspace", "args": { "number": number } })
122 }
123 Command::MoveToWorkspace { number } => {
124 json!({ "command": "move_to_workspace", "args": { "number": number } })
125 }
126 Command::ToggleFloating => {
127 json!({ "command": "toggle_floating", "args": {} })
128 }
129 Command::Equalize => {
130 json!({ "command": "equalize", "args": {} })
131 }
132 Command::Reload => {
133 json!({ "command": "reload", "args": {} })
134 }
135 Command::Exit => {
136 json!({ "command": "exit", "args": {} })
137 }
138 Command::GetWorkspaces => {
139 json!({ "command": "get_workspaces", "args": {} })
140 }
141 Command::GetFocused => {
142 json!({ "command": "get_focused", "args": {} })
143 }
144 Command::GetTree => {
145 json!({ "command": "get_tree", "args": {} })
146 }
147 Command::FocusMonitor { target } => {
148 json!({ "command": "focus_monitor", "args": { "target": target } })
149 }
150 Command::MoveToMonitor { target } => {
151 json!({ "command": "move_to_monitor", "args": { "target": target } })
152 }
153 Command::GetMonitors => {
154 json!({ "command": "get_monitors", "args": {} })
155 }
156 };
157
158 match send_command(request) {
159 Ok(response) => {
160 let success = response.get("success").and_then(|v| v.as_bool()).unwrap_or(false);
161
162 if success {
163 if let Some(data) = response.get("data") {
164 if !data.is_null() {
165 // Pretty print data
166 println!("{}", serde_json::to_string_pretty(data).unwrap_or_default());
167 }
168 }
169 } else {
170 let error = response.get("error")
171 .and_then(|v| v.as_str())
172 .unwrap_or("Unknown error");
173 eprintln!("Error: {}", error);
174 std::process::exit(1);
175 }
176 }
177 Err(e) => {
178 eprintln!("{}", e);
179 std::process::exit(1);
180 }
181 }
182 }
183