git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

src/cce_ctl.rs (10.9K)

  1 // cce-ctl — IPC client for cce
  2  
  3 use std::env;
  4 use std::io::{Read, Write};
  5 use std::os::unix::net::UnixStream;
  6 use std::process;
  7  
  8 fn get_socket_path() -> String {
  9     match env::var("WAYLAND_DISPLAY") {
 10         Ok(display) => format!("/tmp/cce-{}.sock", display),
 11         Err(_) => "/tmp/cce.sock".to_string(),
 12     }
 13 }
 14  
 15 #[allow(dead_code)]
 16 fn get_windows_path() -> String {
 17     match env::var("WAYLAND_DISPLAY") {
 18         Ok(display) => format!("/tmp/cce-windows-{}", display),
 19         Err(_) => "/tmp/cce-windows".to_string(),
 20     }
 21 }
 22  
 23 fn usage(name: &str, to_stderr: bool) {
 24     let print = |s: &str| {
 25         if to_stderr {
 26             eprintln!("{}", s);
 27         } else {
 28             println!("{}", s);
 29         }
 30     };
 31     print(&format!("usage: {} <command> [args...]", name));
 32     print("");
 33     print("commands:");
 34     print("  layout <gap|gap_top|gap_left|gap_right|gap_bottom|offset|grid_gap|bar_height> <value>");
 35     print("  close");
 36     print("  minimize");
 37     print("  focus-next");
 38     print("  focus-prev");
 39     print("  focus-up | focus-down | focus-left | focus-right");
 40     print("  window-switcher            # open the alt-tab window switcher (cce-cloud overlay)");
 41     print("  fullscreen");
 42     print("  mode-next");
 43     print("  mode-next-shared");
 44     print("  set-mode <floating|tiled|fullscreen> [app_id|id]  # set a window's mode (focused if omitted)");
 45     print("  zoom-in");
 46     print("  zoom-out");
 47     print("  zoom-reset");
 48     print("  pan-left");
 49     print("  pan-right");
 50     print("  pan-up");
 51     print("  pan-down");
 52     print("  overlay-left");
 53     print("  overlay-right");
 54     print("  focus-window [--no-wait] <app_id|id>  # focus and raise; replies once the window");
 55     print("                              # stops moving (a pan into view), 3s at most");
 56     print("  close-window <app_id|id> [title-substring]  # close a specific window");
 57     print("  center-window [<app_id>]   # pan focused/named window on-screen; replies x= y= w= h=");
 58     print("  move-window <square> [<app_id>] # put focused/named window on a desktop square (e.g. C-9)");
 59     print("  resize-window <w> <h> [<app_id|id>] # set focused/named window's size (logical px),");
 60     print("                              # top-left fixed, clamped to its min/max hints");
 61     print("  min-size [list]            # X11 minimum sizes learned from refusals (X11 px), numbered");
 62     print("  min-size forget <app_id|id> # drop an open X11 window's learned minimum (relearned on refusal)");
 63     print("  min-size forget-entry <n>  # drop stored entry n from `min-size list`");
 64     print("  move-window-left|-right|-up|-down    # step the focused window one grid cell;");
 65     print("                              # tiled swaps with the tiled window already there,");
 66     print("                              # floating just moves");
 67     print("  place-next <app_id> <x> <y> # one-shot: next map of app_id lands near this layout pos");
 68     print("  place-next-cell <app_id> <x> <y> # one-shot: next map covers the grid square at this pos,");
 69     print("                              # keeping its size and growing away from its neighbours");
 70     print("  overview                   # toggle; the exit lands on the focused window, not the pointer");
 71     print("  windows [--json]           # list windows; --json emits one JSON object per line");
 72     print("  outputs [--json]           # list outputs: mode, scale, logical size, physical mm, px/mm, and where the mm came from");
 73     print("  idle [status]              # idle timeouts in force, idle time, inhibited and by whom (app ids), displays_off/sleeping, and the power plan's override of each (plan_*)");
 74     print("  idle wake|sleep|display on|display off   # act now: wake darkened outputs, lock, then run the sleep command; darken/wake outputs");
 75     print("  idle timeouts <display_off_s> <sleep_s>  # set the config timeouts live (0 = off); config.kdl `idle { }` on reload; a Power-plan file under /run/cce overrides either while it exists");
 76     print("  idle inhibit <token> <ttl_s> <who>  # hold the idle timers off for a holder with no surface (cce-desktop-portal: portal Inhibit, org.freedesktop.ScreenSaver); lapses after ttl_s (1-600) unless renewed");
 77     print("  idle uninhibit <token> | idle inhibit-clear  # end one lease now / drop every lease");
 78     print("  lock                       # lock the session now (cce-lock takes the prompt); replies `ok locked` once every output shows the locked scene");
 79     print("  status-hide-mode [true|false]");
 80     print("  adjust-position-mode [true|false|query]");
 81     print("  exit [force]               # log out; waits for windows to close, cancels if one stays (a save prompt); force skips the wait");
 82     print("  restart");
 83     print("  restart-compositor        # exit cleanly; cce-display-manager relaunches the session");
 84     print("  reload");
 85     print("  repeat [<rate> <delay>]    # key repeat for hardware keyboards: repeats/s (0 = off), ms before the first; until the next config load. No args prints it");
 86     print("  input <device_name|*> scroll-factor <value>");
 87     print("  config-done");
 88     print("  spawn <command>");
 89     print("  screenshot                          # capture the screen to ~/Pictures/screenshots");
 90     print("  screenshot region <x> <y> <w> <h>   # capture an on-screen region (logical px)");
 91     print("  screenshot window [app_id|id]       # capture a window (focused if omitted; works off-screen)");
 92     print("  debug-buffers [app_id|id]           # dump a window's scene buffers (pos/dest/natural/surface)");
 93     print("  notify <title> [body]");
 94     print("  bind <mods> <keysym> <action> [args...]");
 95     print("  pbind <mods> <button> <action>");
 96     print("  retile");
 97     print("  mode <floating|tiled|fullscreen|popup|overlay> <app_id> [title]");
 98     print("  pointer-location");
 99     print("  camera                             (pan, zoom, and the pan target being eased to)");
100     print("  selection                          (overview drag-selection: window ids, image ids, band)");
101     print("  pointer-move-to <x> <y>            (layout pixels)");
102     print("  pointer-move-by <dx> <dy>");
103     print("  pointer-scroll <dy> [dx] [finger] [natural]");
104     print("                                     (positive dy scrolls down; 15 = one notch; finger = a");
105     print("                                      two-finger swipe, ended by finger-stop; natural = from");
106     print("                                      a natural-scrolling touchpad, deltas already flipped)");
107     print("  pointer-scroll finger-stop");
108     print("  pointer-swipe <fingers> <dx> <dy> [steps] | begin <fingers> | update <dx> <dy> | end");
109     print("                                     (a 3/4-finger touchpad swipe; drives input.kdl gesture");
110     print("                                      chords such as focus_left \"swipe3_left\"; the staged");
111     print("                                      form holds a swipe short of its threshold)");
112     print("  pointer-pinch <scale> [rotation] [steps] | begin | update <scale> [rotation] | end");
113     print("  touchpad-view-regions <x11:ID|id|app_id> clear | <x,y,w,h> ...");
114     print("                                     (limit a touchpad_view_apps drag to these window-local");
115     print("                                      rects; a swipe elsewhere scrolls the app normally)");
116     print("  pointer-click [button]             (left|right|middle|back|forward or evdev code)");
117     print("  pointer-press [button]             (held until pointer-release — drives drags)");
118     print("  pointer-release [button]");
119     print("  touch down <id> <x> <y> | motion <id> <x> <y> | up <id> | cancel <id> | tap <x> <y>");
120     print("                                     (a touchscreen finger, layout pixels; a client that");
121     print("                                      bound wl_touch gets touch, anything else the pointer)");
122     print("  migrate-input                      (local: move config.kdl keybindings to input.kdl)");
123     print("  keypress <keycode>                 (evdev code; press+release to the focused client)");
124     print("  key-down <keycode>                 (modifier codes — ctrl 29/97, shift 42/54,");
125     print("  key-up <keycode>                    alt 56/100, super 125/126 — update client");
126     print("                                      xkb state, so e.g. 29+36 lands as ctrl+j)");
127     print("  shortcut bind <session> <id> <trigger>   (portal GlobalShortcuts backend: CTRL+SHIFT+space)");
128     print("  shortcut unbind <session> [<id>] | clear | list");
129 }
130  
131 /// The line sent for `args` (the command and its arguments). `focus-window`
132 /// waits for the window to settle unless told `--no-wait`: focusing can pan
133 /// the window into view, and a script's next pointer command would
134 /// otherwise land where the window was mid-flight. Apps reach the
135 /// compositor directly and keep the immediate reply.
136 fn command_line(args: &[String]) -> String {
137     let mut args: Vec<&str> = args.iter().map(String::as_str).collect();
138     if args.first() == Some(&"focus-window") {
139         match args.get(1) {
140             Some(&"--no-wait") => {
141                 args.remove(1);
142             }
143             Some(&"--wait") => {}
144             _ => args.insert(1, "--wait"),
145         }
146     }
147     args.join(" ") + "\n"
148 }
149 
150 pub fn run_cce_ctl(args: Vec<String>) {
151     if args.len() < 2 {
152         usage(&args[0], true);
153         process::exit(1);
154     }
155  
156     if args[1] == "--help" || args[1] == "-h" || args[1] == "help" {
157         usage(&args[0], false);
158         return;
159     }
160 
161     // Local file operation — no compositor needed.
162     if args[1] == "migrate-input" {
163         crate::migrate_input::run();
164         return;
165     }
166  
167  
168     // Connect to IPC socket
169     let stream = match UnixStream::connect(get_socket_path()) {
170         Ok(s) => s,
171         Err(e) => {
172             eprintln!("connect: {}", e);
173             process::exit(1);
174         }
175     };
176  
177     let mut stream = stream;
178     // Build command string from args
179     let cmd = command_line(&args[1..]);
180     if let Err(e) = stream.write_all(cmd.as_bytes()) {
181         eprintln!("write: {}", e);
182         process::exit(1);
183     }
184  
185     // Read response
186     let mut buf = [0u8; 4096];
187     loop {
188         match stream.read(&mut buf) {
189             Ok(0) => break,
190             Ok(n) => {
191                 let s = String::from_utf8_lossy(&buf[..n]);
192                 print!("{}", s);
193             }
194             Err(e) => {
195                 eprintln!("read: {}", e);
196                 break;
197             }
198         }
199     }
200 }
201 
202 #[cfg(test)]
203 mod tests {
204     use super::command_line;
205 
206     fn line(args: &[&str]) -> String {
207         command_line(&args.iter().map(|s| s.to_string()).collect::<Vec<_>>())
208     }
209 
210     #[test]
211     fn focus_window_waits_unless_told_not_to() {
212         assert_eq!(line(&["focus-window", "cce-fonts"]), "focus-window --wait cce-fonts\n");
213         assert_eq!(line(&["focus-window", "--wait", "12"]), "focus-window --wait 12\n");
214         assert_eq!(line(&["focus-window", "--no-wait", "cce-fonts"]), "focus-window cce-fonts\n");
215         assert_eq!(line(&["windows", "--json"]), "windows --json\n");
216     }
217 }