1use std::{cell::RefCell, collections::HashMap, path::PathBuf, rc::Rc, sync::mpsc};
2
3use gtk::{
4 gdk::{self, prelude::*},
5 gio, glib,
6 prelude::{GtkApplicationExt, GtkWindowExt, WidgetExt},
7};
8use gtk4_layer_shell::LayerShell;
9
10use crate::{
11 cli,
12 config::{self, Config},
13 dbus, error, log, monitor,
14 socket::{self, r#async::UnixStreamWrapper},
15};
16
17use super::cli::{Arguments, Command};
18
19pub struct AzaleaApp<WM>
21where
22 WM: AzaleaAppExt,
23 Self: 'static + Sized,
24{
25 config: config::Config<WM::ConfigWrapper>,
26 dbus: Option<dbus::DBusWrapper>,
27 windows: HashMap<String, (config::window::Id, WM::WindowWrapper)>,
28
29 dynamic_css_provider: gtk::CssProvider,
30}
31
32impl<WM> AzaleaApp<WM>
33where
34 WM: AzaleaAppExt,
35{
36 pub fn new(config: config::Config<WM::ConfigWrapper>) -> Self {
37 Self {
38 config,
39 dbus: dbus::DBusWrapper::new().ok(),
40 windows: Default::default(),
41
42 dynamic_css_provider: gtk::CssProvider::new(),
43 }
44 }
45
46 fn load_config(path: &PathBuf) -> Result<Config<WM::ConfigWrapper>, error::ConfigError> {
47 let file = std::fs::File::open(path)?;
48 let reader = std::io::BufReader::new(file);
49 let ext = path
50 .extension()
51 .ok_or(error::ConfigError::MissingExtension)?;
52
53 Ok(if ext == "json" {
54 serde_json::from_reader(reader)
55 .map_err(|e| error::ConfigError::ParsingError(e.to_string()))?
56 } else {
57 ron::de::from_reader(reader)
58 .map_err(|e| error::ConfigError::ParsingError(e.to_string()))?
59 })
60 }
61
62 pub fn run(self) {
63 let args = {
64 let arg_style = clap::builder::styling::Style::new().bold().underline();
65
66 Arguments::parse(format!(
67 "{}Window IDs:{} {}",
68 arg_style.render(),
69 arg_style.render_reset(),
70 self.config
71 .windows
72 .keys()
73 .fold(format!(""), |acc, v| format!("{acc}\n {v}"))
74 ))
75 };
76
77 let socket_path = glib::user_runtime_dir().join(WM::SOCKET_NAME);
78
79 if let Some(dbus) = &self.dbus {
80 if dbus.name_has_owner(WM::APP_ID).unwrap_or(false) {
81 self.remote(args, socket_path, Some(std::time::Duration::from_secs(1)));
82 } else if args.wait_for_daemon {
83 log::message!("Waiting for daemon to start");
84 drop(dbus.wait_for_name_owner(WM::APP_ID));
85 std::thread::sleep(std::time::Duration::from_secs(1));
86 self.remote(args, socket_path, Some(std::time::Duration::from_secs(1)));
87 } else {
88 self.daemon(args, socket_path);
89 }
90 }
91 }
92
93 fn daemon(mut self, args: Arguments, socket_path: PathBuf) {
94 match args.command {
95 Command::Daemon(cli::daemon::Command::Start {
96 config: config_path,
97 }) => {
98 let config_path = config_path
99 .map(|p| PathBuf::from(&p))
100 .unwrap_or(gtk::glib::user_config_dir().join(WM::CONFIG_PATH));
101
102 match Self::load_config(&config_path) {
103 Ok(config) => {
104 log::message!("Config loaded from {:?}", config_path);
105 self.config = config;
106 }
107 Err(err) => match err {
108 error::ConfigError::Io(_) => {
109 log::message!(
110 "Config not found at {:?}, using default config",
111 config_path
112 )
113 }
114 error => log::warning!(
115 "Config could not be loaded from {:?}, using default config.\n{:?}",
116 config_path,
117 error
118 ),
119 },
120 }
121 }
122 Command::Monitors => {
123 println!("{}", monitor::monitors_to_string());
124 return;
125 }
126 Command::Config(cli::config::Command::View { json }) => {
127 println!("{}", self.config_to_string(json));
128 return;
129 }
130 _ => {
131 log::error!("Daemon isn't running, invalid command: {:?}", args.command);
132 }
133 }
134
135 let app = gtk::Application::builder()
136 .application_id(WM::APP_ID)
137 .build();
138
139 if let Err(error) = app.register(gio::Cancellable::NONE) {
140 log::error!("Failed to register gtk application {error:?}");
141 }
142
143 let (ping_tx, ping_rx) = mpsc::channel();
144 let (pong_tx, pong_rx) = mpsc::channel();
145
146 ping_tx.send(app.hold()).expect("Daemon could not ping!");
147
148 let state = Rc::new(RefCell::new(self));
149
150 app.connect_activate(move |app| {
151 let Ok(app_guard) = ping_rx.try_recv() else {
153 return;
154 };
155
156 log::message!("Daemon started");
157
158 pong_tx.send(app_guard).expect("Daemon could not pong!");
159
160 state.borrow_mut().create_all_windows(app);
161
162 Self::load_style(>k::CssProvider::new(), None);
163
164 match socket::r#async::UnixListenerWrapper::bind(&socket_path) {
165 Ok(listener) => {
166 glib::spawn_future_local(glib::clone!(
167 #[weak]
168 app,
169 #[weak]
170 state,
171 async move {
172 listener
173 .loop_accept(async |mut stream: UnixStreamWrapper| {
174 match stream.read().await {
175 Ok(cmd) => {
176 let answer =
177 state.borrow_mut().handle_command(cmd, &app);
178 drop(stream.write(answer).await);
179 return true;
180 }
181 Err(e) => {
182 let answer = cli::Response::Error(format!("{e:?}"));
183 drop(stream.write(answer).await);
184 return false;
185 }
186 };
187 })
188 .await;
189 }
190 ));
191 }
192 Err(e) => log::error!("Failed to bind unix socket {e:?}"),
193 }
194 });
195
196 {
197 let mut gtk_args = vec![std::env::args().next().unwrap()];
198 gtk_args.extend(args.gtk_options.clone());
199 app.run_with_args(>k_args);
200 }
201
202 drop(pong_rx.try_recv());
203 }
204
205 fn remote(self, args: Arguments, socket_path: PathBuf, retry: Option<std::time::Duration>) {
206 loop {
207 match socket::sync::UnixStreamWrapper::connect(&socket_path) {
208 Ok(mut stream) => {
209 if let Err(e) = stream.write(&args.command) {
210 log::warning!("failed to write {e:?}");
211 } else {
212 match stream.read::<cli::Response>() {
213 Ok(response) => match response {
214 cli::Response::Success(ans) => println!("{ans}"),
215 cli::Response::Error(e) => log::warning!("{e:?}"),
216 },
217 Err(e) => log::warning!("Failed to receive response: {e:?}"),
218 }
219 }
220 return;
221 }
222 Err(e) => {
223 if let Some(duration) = retry {
224 std::thread::sleep(duration);
225 } else {
226 log::warning!("failed to connect {e:?}");
227 return;
228 }
229 }
230 }
231 }
232 }
233
234 fn handle_command(&mut self, cmd: Command, app: >k::Application) -> cli::Response {
235 match cmd {
236 Command::Daemon(cli::daemon::Command::Start { config: _ }) => {
237 return cli::Response::Error(format!("There's already an instance running."));
238 }
239 Command::Daemon(cli::daemon::Command::Stop) => app.quit(),
240 Command::Window(window_cmd) => match window_cmd {
241 cli::window::Command::Create(arg) => self.create_window(&arg.id, None, app),
242 cli::window::Command::Toggle(arg) => {
243 let Some((_, wrapper)) = self.windows.get(&arg.uuid) else {
244 return cli::Response::Error(format!(
245 "Window with id {} not found",
246 arg.uuid
247 ));
248 };
249 let window = WM::unwrap_window(wrapper);
250 window.set_visible(!window.get_visible());
251 }
252 cli::window::Command::Uuid => {
253 let uuids: Vec<HashMap<&str, String>> = self
254 .windows
255 .iter()
256 .map(|(k, (template_id, window))| {
257 let window = WM::unwrap_window(window);
258 HashMap::from([
259 ("uuid", k.clone()),
260 (
261 "monitor",
262 window
263 .monitor()
264 .and_then(|m| m.model())
265 .unwrap_or_default()
266 .to_string(),
267 ),
268 (
269 "namespace",
270 window
271 .namespace()
272 .map(|m| m.to_string())
273 .unwrap_or(String::new()),
274 ),
275 ("template", template_id.clone()),
276 ])
277 })
278 .collect();
279 return cli::Response::Success(format!(
280 "{}",
281 serde_json::to_string_pretty(&uuids).unwrap_or(format!("[]"))
282 ));
283 }
284 },
285 Command::Layer(cli::layer_shell::Command::Toggle(arg)) => self
286 .windows
287 .values()
288 .map(|(_, win)| WM::unwrap_window(win))
289 .filter(|win| arg.cmp(win))
290 .for_each(|win| win.set_visible(!win.get_visible())),
291 Command::Config(cli::config::Command::View { json }) => {
292 return cli::Response::Success(self.config_to_string(json));
293 }
294 Command::Monitors => {
295 return cli::Response::Success(monitor::monitors_to_string());
296 }
297 Command::Style(command) => match command {
298 cli::style::Command::Reload { file } => {
299 let file = file.unwrap_or(glib::user_config_dir().join(WM::STYLE_PATH));
300
301 let Ok(scss) = std::fs::read_to_string(&file) else {
302 return cli::Response::Error(format!(
303 "failed to find style file: {file:?}"
304 ));
305 };
306
307 Self::load_style(&self.dynamic_css_provider, Some(&scss))
308 }
309 cli::style::Command::Default => Self::load_style(&self.dynamic_css_provider, None),
310 },
311 }
312 cli::Response::Success(format!("Ok"))
313 }
314
315 fn load_style(provider: >k::CssProvider, scss: Option<&str>) {
316 let css = match grass::from_string(
317 scss.unwrap_or(include_str!("./style.scss")),
318 &grass::Options::default(),
319 ) {
320 Ok(css) => css,
321 Err(e) => {
322 log::warning!("Failed to compile sass style: {e}");
323 return;
324 }
325 };
326
327 provider.load_from_string(&css);
328
329 if let Some(display) = gtk::gdk::Display::default() {
330 #[allow(deprecated)] gtk::StyleContext::add_provider_for_display(
332 &display,
333 provider,
334 gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
335 );
336 }
337 }
338
339 fn create_all_windows(&mut self, app: >k::Application) {
340 for (id, cfg) in self.config.windows.clone().iter() {
342 if cfg.lazy {
343 continue;
344 }
345
346 match &cfg.monitor {
347 monitor::Monitor::Dynamic => {
348 self.create_window(&id, None, app);
349 }
350 monitor::Monitor::Single(monitor_match) => {
351 for monitor in monitor_match.find_matches() {
352 self.create_window(&id, Some(monitor), app);
353 }
354 }
355 monitor::Monitor::Multi(monitor_matches) => {
356 for monitor_match in monitor_matches {
357 for monitor in monitor_match.find_matches() {
358 self.create_window(&id, Some(monitor), app);
359 }
360 }
361 }
362 monitor::Monitor::All => {
363 for monitor in monitor::monitors() {
364 self.create_window(&id, Some(monitor), app);
365 }
366 }
367 }
368 }
369 }
370
371 fn create_window(
372 &mut self,
373 id: &config::window::Id,
374 monitor: Option<gdk::Monitor>,
375 app: >k::Application,
376 ) {
377 let Some(window_cfg) = self.config.windows.get(id) else {
378 log::warning!("Window configuration not found for id {}", id);
379 return;
380 };
381
382 let wrapped_window = WM::create_window(&window_cfg.config);
383 let window = WM::unwrap_window(&wrapped_window);
384
385 window.set_title(Some(&id));
386
387 if let Some(layer_shell) = &window_cfg.layer_shell {
388 window.init_layer_shell();
389 window.set_namespace(Some(&layer_shell.namespace));
390 window.set_keyboard_mode(gtk4_layer_shell::KeyboardMode::OnDemand);
391 window.set_layer((&layer_shell.layer).into());
392 window.add_css_class(&layer_shell.namespace);
393 for anchor in &layer_shell.anchors {
394 window.set_anchor(anchor.into(), true);
395 }
396 match layer_shell.exclusive_zone {
397 config::layer_shell::ExclusiveZone::Auto => {
398 window.auto_exclusive_zone_enable();
399 }
400 config::layer_shell::ExclusiveZone::Ignore => {
401 window.set_exclusive_zone(-1);
402 }
403 config::layer_shell::ExclusiveZone::Normal => {
404 window.set_exclusive_zone(0);
405 }
406 }
407 }
408
409 window.set_monitor(monitor.as_ref());
410
411 app.add_window(window);
412 window.present();
413
414 self.windows.insert(
415 uuid::Uuid::new_v4().to_string(),
416 (id.clone(), wrapped_window),
417 );
418 }
419
420 fn config_to_string(&self, json: bool) -> String {
421 if json {
422 serde_json::to_string_pretty(&self.config).unwrap()
423 } else {
424 use ron::extensions::Extensions;
425 ron::ser::to_string_pretty(
426 &self.config,
427 ron::ser::PrettyConfig::default()
428 .extensions(Extensions::IMPLICIT_SOME | Extensions::UNWRAP_VARIANT_NEWTYPES),
429 )
430 .unwrap()
431 }
432 }
433}
434
435pub trait AzaleaAppExt
436where
437 Self: 'static + Sized,
438{
439 type ConfigWrapper: serde::Serialize
440 + serde::de::DeserializeOwned
441 + std::fmt::Debug
442 + Clone
443 + 'static;
444 type WindowWrapper;
445
446 const CONFIG_PATH: &str = "azalea/config.ron";
447 const STYLE_PATH: &str = "azalea/style.scss";
448 const SOCKET_NAME: &str = "azalea.sock";
449 const APP_ID: &str = "br.usp.ime.Azalea";
450
451 fn create_window(config: &Self::ConfigWrapper) -> Self::WindowWrapper;
452 fn unwrap_window(window: &Self::WindowWrapper) -> >k::Window;
453}