Rust · 4011 bytes Raw Blame History
1 //! Border management via ers subprocess.
2 //! ers is a standalone border renderer that handles its own window events,
3 //! focus detection, and rendering. Tarmac just spawns and manages the process.
4
5 use std::process::{Child, Command};
6
7 /// RGBA color for border configuration.
8 #[derive(Debug, Clone, Copy)]
9 pub struct BorderColor {
10 pub r: f64,
11 pub g: f64,
12 pub b: f64,
13 pub a: f64,
14 }
15
16 impl BorderColor {
17 /// Parse a hex color string (#RRGGBB or #RRGGBBAA).
18 pub fn from_hex(hex: &str) -> Self {
19 let hex = hex.trim_start_matches('#');
20 let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f64 / 255.0;
21 let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f64 / 255.0;
22 let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f64 / 255.0;
23 let a = if hex.len() >= 8 {
24 u8::from_str_radix(&hex[6..8], 16).unwrap_or(255) as f64 / 255.0
25 } else {
26 1.0
27 };
28 Self { r, g, b, a }
29 }
30
31 fn to_hex(&self) -> String {
32 let r = (self.r * 255.0) as u8;
33 let g = (self.g * 255.0) as u8;
34 let b = (self.b * 255.0) as u8;
35 let a = (self.a * 255.0) as u8;
36 if a == 255 {
37 format!("#{r:02x}{g:02x}{b:02x}")
38 } else {
39 format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
40 }
41 }
42 }
43
44 /// Manages the ers border renderer subprocess.
45 pub struct BorderManager {
46 pub border_width: f64,
47 pub focused_color: BorderColor,
48 pub unfocused_color: BorderColor,
49 pub radius: f64,
50 child: Option<Child>,
51 }
52
53 impl BorderManager {
54 #[allow(clippy::new_without_default)]
55 pub fn new() -> Self {
56 Self {
57 border_width: 0.0,
58 focused_color: BorderColor::from_hex("#5294e2"),
59 unfocused_color: BorderColor::from_hex("#2d2d2d"),
60 radius: 10.0,
61 child: None,
62 }
63 }
64
65 pub fn is_enabled(&self) -> bool {
66 self.border_width > 0.0
67 }
68
69 /// Spawn ers with current settings. Kills any existing instance first.
70 /// Looks for ers next to the tarmac binary first, then falls back to PATH.
71 pub fn spawn(&mut self) {
72 self.kill();
73 if !self.is_enabled() {
74 return;
75 }
76
77 let ers_bin = std::env::current_exe()
78 .ok()
79 .and_then(|p| p.parent().map(|d| d.join("ers")))
80 .filter(|p| p.exists())
81 .map(|p| p.to_string_lossy().to_string())
82 .unwrap_or_else(|| "ers".to_string());
83
84 let cmd = format!(
85 "{} --active-only --width {} --radius {} --color '{}' --inactive '{}'",
86 ers_bin,
87 self.border_width,
88 self.radius,
89 self.focused_color.to_hex(),
90 self.unfocused_color.to_hex(),
91 );
92 tracing::debug!(cmd, "spawning ers");
93 match Command::new("/bin/sh").args(["-c", &cmd]).spawn() {
94 Ok(child) => {
95 self.child = Some(child);
96 }
97 Err(e) => {
98 tracing::warn!(err = %e, "failed to spawn ers");
99 }
100 }
101 }
102
103 /// Kill the managed ers process if running.
104 pub fn kill(&mut self) {
105 if let Some(ref mut child) = self.child {
106 let _ = child.kill();
107 let _ = child.wait();
108 }
109 self.child = None;
110 }
111
112 /// Restart ers with current settings (used on config reload).
113 pub fn restart(&mut self) {
114 self.spawn();
115 }
116
117 // Stub methods for compatibility with existing state.rs calls.
118 // ers handles all of these independently.
119 pub fn update_border(&mut self, _wid: u32, _rect: crate::core::tree::Rect, _focused: bool) {}
120 pub fn remove_border(&mut self, _wid: u32) {}
121 pub fn update_focus(
122 &mut self,
123 _old: Option<u32>,
124 _new: Option<u32>,
125 _get_rect: impl Fn(u32) -> Option<crate::core::tree::Rect>,
126 ) {
127 }
128 }
129
130 impl Drop for BorderManager {
131 fn drop(&mut self) {
132 self.kill();
133 }
134 }
135