Rust · 5773 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 /// Refresh window layout (re-apply without changing ratios)
53 RefreshLayout,
54 /// Reload configuration
55 Reload,
56 /// Exit gar
57 Exit,
58 /// Get workspace information
59 GetWorkspaces,
60 /// Get focused window information
61 GetFocused,
62 /// Get window tree
63 GetTree,
64 /// Focus monitor (next, prev, or name)
65 FocusMonitor {
66 /// Target: next, prev, left, right, or monitor name
67 target: String,
68 },
69 /// Move focused window to monitor
70 MoveToMonitor {
71 /// Target: next, prev, left, right, or monitor name
72 target: String,
73 },
74 /// Get monitor information
75 GetMonitors,
76 }
77
78 fn get_socket_path() -> PathBuf {
79 std::env::var("XDG_RUNTIME_DIR")
80 .map(|dir| PathBuf::from(dir).join("gar.sock"))
81 .unwrap_or_else(|_| PathBuf::from("/tmp/gar.sock"))
82 }
83
84 fn send_command(request: Value) -> Result<Value, String> {
85 let socket_path = get_socket_path();
86
87 let mut stream = UnixStream::connect(&socket_path)
88 .map_err(|e| format!("Failed to connect to gar (is it running?): {}", e))?;
89
90 // Send request
91 let json = serde_json::to_string(&request)
92 .map_err(|e| format!("Failed to serialize request: {}", e))?;
93 writeln!(stream, "{}", json)
94 .map_err(|e| format!("Failed to send request: {}", e))?;
95
96 // Read response
97 let mut reader = BufReader::new(stream);
98 let mut response = String::new();
99 reader.read_line(&mut response)
100 .map_err(|e| format!("Failed to read response: {}", e))?;
101
102 serde_json::from_str(&response)
103 .map_err(|e| format!("Failed to parse response: {}", e))
104 }
105
106 fn main() {
107 let cli = Cli::parse();
108
109 let request = match cli.command {
110 Command::Focus { direction } => {
111 json!({ "command": "focus", "args": { "direction": direction } })
112 }
113 Command::Swap { direction } => {
114 json!({ "command": "swap", "args": { "direction": direction } })
115 }
116 Command::Resize { direction, amount } => {
117 json!({ "command": "resize", "args": { "direction": direction, "amount": amount } })
118 }
119 Command::Close => {
120 json!({ "command": "close", "args": {} })
121 }
122 Command::Workspace { number } => {
123 json!({ "command": "workspace", "args": { "number": number } })
124 }
125 Command::MoveToWorkspace { number } => {
126 json!({ "command": "move_to_workspace", "args": { "number": number } })
127 }
128 Command::ToggleFloating => {
129 json!({ "command": "toggle_floating", "args": {} })
130 }
131 Command::Equalize => {
132 json!({ "command": "equalize", "args": {} })
133 }
134 Command::RefreshLayout => {
135 json!({ "command": "refresh_layout", "args": {} })
136 }
137 Command::Reload => {
138 json!({ "command": "reload", "args": {} })
139 }
140 Command::Exit => {
141 json!({ "command": "exit", "args": {} })
142 }
143 Command::GetWorkspaces => {
144 json!({ "command": "get_workspaces", "args": {} })
145 }
146 Command::GetFocused => {
147 json!({ "command": "get_focused", "args": {} })
148 }
149 Command::GetTree => {
150 json!({ "command": "get_tree", "args": {} })
151 }
152 Command::FocusMonitor { target } => {
153 json!({ "command": "focus_monitor", "args": { "target": target } })
154 }
155 Command::MoveToMonitor { target } => {
156 json!({ "command": "move_to_monitor", "args": { "target": target } })
157 }
158 Command::GetMonitors => {
159 json!({ "command": "get_monitors", "args": {} })
160 }
161 };
162
163 match send_command(request) {
164 Ok(response) => {
165 let success = response.get("success").and_then(|v| v.as_bool()).unwrap_or(false);
166
167 if success {
168 if let Some(data) = response.get("data") {
169 if !data.is_null() {
170 // Pretty print data
171 println!("{}", serde_json::to_string_pretty(data).unwrap_or_default());
172 }
173 }
174 } else {
175 let error = response.get("error")
176 .and_then(|v| v.as_str())
177 .unwrap_or("Unknown error");
178 eprintln!("Error: {}", error);
179 std::process::exit(1);
180 }
181 }
182 Err(e) => {
183 eprintln!("{}", e);
184 std::process::exit(1);
185 }
186 }
187 }
188