azalea_core/
app.rs

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
19/// Main application state
20pub 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            // Make sure only the first one goes past this
152            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(&gtk::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(&gtk_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: &gtk::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: &gtk::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)] // it's not really deprecated
331            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: &gtk::Application) {
340        // TODO: Check if window already exists (match monitor and window id)
341        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: &gtk::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) -> &gtk::Window;
453}