2022-08-14 14:30:13 +01:00
|
|
|
mod item;
|
2022-08-25 23:42:57 +01:00
|
|
|
mod open_state;
|
2022-08-14 14:30:13 +01:00
|
|
|
mod popup;
|
|
|
|
|
|
|
|
use crate::collection::Collection;
|
2022-08-25 23:42:57 +01:00
|
|
|
use crate::modules::launcher::item::{ButtonConfig, LauncherItem, LauncherWindow};
|
|
|
|
use crate::modules::launcher::open_state::OpenState;
|
2022-08-14 14:30:13 +01:00
|
|
|
use crate::modules::launcher::popup::Popup;
|
|
|
|
use crate::modules::{Module, ModuleInfo};
|
2022-08-25 21:53:42 +01:00
|
|
|
use crate::sway::{get_client, SwayNode};
|
2022-08-21 23:36:07 +01:00
|
|
|
use color_eyre::{Report, Result};
|
2022-08-14 14:30:13 +01:00
|
|
|
use gtk::prelude::*;
|
|
|
|
use gtk::{IconTheme, Orientation};
|
|
|
|
use serde::Deserialize;
|
|
|
|
use std::rc::Rc;
|
|
|
|
use tokio::spawn;
|
|
|
|
use tokio::sync::mpsc;
|
|
|
|
use tokio::task::spawn_blocking;
|
2022-08-25 23:42:57 +01:00
|
|
|
use tracing::debug;
|
2022-08-14 14:30:13 +01:00
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
|
|
pub struct LauncherModule {
|
2022-08-28 16:57:41 +01:00
|
|
|
/// List of app IDs (or classes) to always show regardles of open state,
|
|
|
|
/// in the order specified.
|
2022-08-14 14:30:13 +01:00
|
|
|
favorites: Option<Vec<String>>,
|
2022-08-28 16:57:41 +01:00
|
|
|
/// Whether to show application names on the bar.
|
2022-08-14 20:40:11 +01:00
|
|
|
#[serde(default = "crate::config::default_false")]
|
2022-08-14 14:30:13 +01:00
|
|
|
show_names: bool,
|
2022-08-28 16:57:41 +01:00
|
|
|
/// Whether to show application icons on the bar.
|
2022-08-14 20:40:11 +01:00
|
|
|
#[serde(default = "crate::config::default_true")]
|
2022-08-14 14:30:13 +01:00
|
|
|
show_icons: bool,
|
|
|
|
|
2022-08-28 16:57:41 +01:00
|
|
|
/// Name of the GTK icon theme to use.
|
2022-08-14 14:30:13 +01:00
|
|
|
icon_theme: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum FocusEvent {
|
|
|
|
AppId(String),
|
|
|
|
Class(String),
|
|
|
|
ConId(i32),
|
|
|
|
}
|
|
|
|
|
|
|
|
type AppId = String;
|
|
|
|
|
|
|
|
struct Launcher {
|
|
|
|
items: Collection<AppId, LauncherItem>,
|
|
|
|
container: gtk::Box,
|
|
|
|
button_config: ButtonConfig,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Launcher {
|
|
|
|
fn new(favorites: Vec<String>, container: gtk::Box, button_config: ButtonConfig) -> Self {
|
|
|
|
let items = favorites
|
|
|
|
.into_iter()
|
|
|
|
.map(|app_id| {
|
|
|
|
(
|
|
|
|
app_id.clone(),
|
|
|
|
LauncherItem::new(app_id, true, &button_config),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.collect::<Collection<_, _>>();
|
|
|
|
|
|
|
|
for item in &items {
|
|
|
|
container.add(&item.button);
|
|
|
|
}
|
|
|
|
|
|
|
|
Self {
|
|
|
|
items,
|
|
|
|
container,
|
|
|
|
button_config,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Adds a new window to the launcher.
|
|
|
|
/// This gets added to an existing group
|
|
|
|
/// if an instance of the program is already open.
|
2022-08-25 23:42:57 +01:00
|
|
|
fn add_window(&mut self, node: SwayNode) {
|
|
|
|
let id = node.get_id().to_string();
|
|
|
|
|
|
|
|
debug!("Adding window with ID {}", id);
|
2022-08-14 14:30:13 +01:00
|
|
|
|
|
|
|
if let Some(item) = self.items.get_mut(&id) {
|
2022-08-21 23:36:07 +01:00
|
|
|
let mut state = item
|
|
|
|
.state
|
|
|
|
.write()
|
|
|
|
.expect("Failed to get write lock on state");
|
2022-08-25 23:42:57 +01:00
|
|
|
let new_open_state = OpenState::from_node(&node);
|
|
|
|
state.open_state = OpenState::merge_states(vec![&state.open_state, &new_open_state]);
|
|
|
|
state.is_xwayland = node.is_xwayland();
|
2022-08-14 14:30:13 +01:00
|
|
|
|
|
|
|
item.update_button_classes(&state);
|
|
|
|
|
2022-08-25 23:42:57 +01:00
|
|
|
let mut windows = item
|
|
|
|
.windows
|
|
|
|
.write()
|
|
|
|
.expect("Failed to get write lock on windows");
|
2022-08-14 14:30:13 +01:00
|
|
|
|
|
|
|
windows.insert(
|
2022-08-25 23:42:57 +01:00
|
|
|
node.id,
|
2022-08-14 14:30:13 +01:00
|
|
|
LauncherWindow {
|
2022-08-25 23:42:57 +01:00
|
|
|
con_id: node.id,
|
|
|
|
name: node.name,
|
|
|
|
open_state: new_open_state,
|
2022-08-14 14:30:13 +01:00
|
|
|
},
|
|
|
|
);
|
|
|
|
} else {
|
2022-08-25 23:42:57 +01:00
|
|
|
let item = LauncherItem::from_node(&node, &self.button_config);
|
2022-08-14 14:30:13 +01:00
|
|
|
|
|
|
|
self.container.add(&item.button);
|
|
|
|
self.items.insert(id, item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Removes a window from the launcher.
|
|
|
|
/// This removes it from the group if multiple instances were open.
|
|
|
|
/// The button will remain on the launcher if it is favourited.
|
|
|
|
fn remove_window(&mut self, window: &SwayNode) {
|
|
|
|
let id = window.get_id().to_string();
|
|
|
|
|
2022-08-25 23:42:57 +01:00
|
|
|
debug!("Removing window with ID {}", id);
|
|
|
|
|
2022-08-14 14:30:13 +01:00
|
|
|
let item = self.items.get_mut(&id);
|
|
|
|
|
|
|
|
let remove = if let Some(item) = item {
|
|
|
|
let windows = Rc::clone(&item.windows);
|
2022-08-25 23:42:57 +01:00
|
|
|
let mut windows = windows
|
|
|
|
.write()
|
|
|
|
.expect("Failed to get write lock on windows");
|
2022-08-14 14:30:13 +01:00
|
|
|
|
|
|
|
windows.remove(&window.id);
|
|
|
|
|
|
|
|
if windows.is_empty() {
|
2022-08-21 23:36:07 +01:00
|
|
|
let mut state = item.state.write().expect("Failed to get lock on windows");
|
|
|
|
state.open_state = OpenState::Closed;
|
2022-08-14 14:30:13 +01:00
|
|
|
item.update_button_classes(&state);
|
|
|
|
|
|
|
|
if item.favorite {
|
|
|
|
false
|
|
|
|
} else {
|
|
|
|
self.container.remove(&item.button);
|
|
|
|
true
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
};
|
|
|
|
|
|
|
|
if remove {
|
|
|
|
self.items.remove(&id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-25 23:42:57 +01:00
|
|
|
/// Unfocuses the currently focused window
|
|
|
|
/// and focuses the newly focused one.
|
|
|
|
fn set_window_focused(&mut self, node: &SwayNode) {
|
|
|
|
let id = node.get_id().to_string();
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-08-25 23:42:57 +01:00
|
|
|
debug!("Setting window with ID {} focused", id);
|
|
|
|
|
|
|
|
let prev_focused = self.items.iter_mut().find(|item| {
|
2022-08-21 23:36:07 +01:00
|
|
|
item.state
|
|
|
|
.read()
|
|
|
|
.expect("Failed to get read lock on state")
|
|
|
|
.open_state
|
2022-08-25 23:42:57 +01:00
|
|
|
.is_focused()
|
2022-08-21 23:36:07 +01:00
|
|
|
});
|
|
|
|
|
2022-08-25 23:42:57 +01:00
|
|
|
if let Some(prev_focused) = prev_focused {
|
|
|
|
let mut state = prev_focused
|
2022-08-21 23:36:07 +01:00
|
|
|
.state
|
|
|
|
.write()
|
|
|
|
.expect("Failed to get write lock on state");
|
2022-08-25 23:42:57 +01:00
|
|
|
|
|
|
|
// if a window from the same item took focus,
|
|
|
|
// we don't need to unfocus the item.
|
|
|
|
if prev_focused.app_id != id {
|
|
|
|
prev_focused.set_open_state(OpenState::open(), &mut state);
|
|
|
|
prev_focused.update_button_classes(&state);
|
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
let item = self.items.get_mut(&id);
|
|
|
|
if let Some(item) = item {
|
2022-08-21 23:36:07 +01:00
|
|
|
let mut state = item
|
|
|
|
.state
|
|
|
|
.write()
|
|
|
|
.expect("Failed to get write lock on state");
|
2022-08-25 23:42:57 +01:00
|
|
|
item.set_window_open_state(node.id, OpenState::focused(), &mut state);
|
2022-08-14 14:30:13 +01:00
|
|
|
item.update_button_classes(&state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-25 23:42:57 +01:00
|
|
|
/// Updates the window title for the given node.
|
2022-08-14 14:30:13 +01:00
|
|
|
fn set_window_title(&mut self, window: SwayNode) {
|
|
|
|
let id = window.get_id().to_string();
|
|
|
|
let item = self.items.get_mut(&id);
|
|
|
|
|
2022-08-25 23:42:57 +01:00
|
|
|
debug!("Updating title for window with ID {}", id);
|
|
|
|
|
2022-08-14 14:30:13 +01:00
|
|
|
if let (Some(item), Some(name)) = (item, window.name) {
|
2022-08-25 23:42:57 +01:00
|
|
|
let mut windows = item
|
|
|
|
.windows
|
|
|
|
.write()
|
|
|
|
.expect("Failed to get write lock on windows");
|
2022-08-14 14:47:28 +01:00
|
|
|
if windows.len() == 1 {
|
|
|
|
item.set_title(&name, &self.button_config);
|
2022-08-21 23:36:07 +01:00
|
|
|
} else if let Some(window) = windows.get_mut(&window.id) {
|
|
|
|
window.name = Some(name);
|
2022-08-14 14:47:28 +01:00
|
|
|
} else {
|
2022-08-21 23:36:07 +01:00
|
|
|
// This should never happen
|
|
|
|
// But makes more sense to wipe title than keep old one in case of error
|
|
|
|
item.set_title("", &self.button_config);
|
2022-08-14 14:47:28 +01:00
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-25 23:42:57 +01:00
|
|
|
/// Updates the window urgency based on the given node.
|
|
|
|
fn set_window_urgent(&mut self, node: &SwayNode) {
|
|
|
|
let id = node.get_id().to_string();
|
2022-08-14 14:30:13 +01:00
|
|
|
let item = self.items.get_mut(&id);
|
|
|
|
|
2022-08-25 23:42:57 +01:00
|
|
|
debug!(
|
|
|
|
"Setting urgency to {} for window with ID {}",
|
|
|
|
node.urgent, id
|
|
|
|
);
|
|
|
|
|
2022-08-14 14:30:13 +01:00
|
|
|
if let Some(item) = item {
|
2022-08-21 23:36:07 +01:00
|
|
|
let mut state = item
|
|
|
|
.state
|
|
|
|
.write()
|
|
|
|
.expect("Failed to get write lock on state");
|
2022-08-25 23:42:57 +01:00
|
|
|
|
|
|
|
item.set_window_open_state(node.id, OpenState::urgent(node.urgent), &mut state);
|
2022-08-14 14:30:13 +01:00
|
|
|
item.update_button_classes(&state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Module<gtk::Box> for LauncherModule {
|
2022-08-21 23:36:07 +01:00
|
|
|
fn into_widget(self, info: &ModuleInfo) -> Result<gtk::Box> {
|
2022-08-14 14:30:13 +01:00
|
|
|
let icon_theme = IconTheme::new();
|
|
|
|
|
|
|
|
if let Some(theme) = self.icon_theme {
|
|
|
|
icon_theme.set_custom_theme(Some(&theme));
|
|
|
|
}
|
|
|
|
|
2022-08-14 20:40:11 +01:00
|
|
|
let popup = Popup::new(
|
|
|
|
"popup-launcher",
|
|
|
|
info.app,
|
2022-08-15 21:11:00 +01:00
|
|
|
info.monitor,
|
2022-08-14 20:40:11 +01:00
|
|
|
Orientation::Vertical,
|
|
|
|
info.bar_position,
|
|
|
|
);
|
2022-08-14 14:30:13 +01:00
|
|
|
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
|
|
|
|
|
|
|
let (ui_tx, mut ui_rx) = mpsc::channel(32);
|
|
|
|
|
|
|
|
let button_config = ButtonConfig {
|
|
|
|
icon_theme,
|
|
|
|
show_names: self.show_names,
|
|
|
|
show_icons: self.show_icons,
|
|
|
|
popup,
|
|
|
|
tx: ui_tx,
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut launcher = Launcher::new(
|
|
|
|
self.favorites.unwrap_or_default(),
|
|
|
|
container.clone(),
|
|
|
|
button_config,
|
|
|
|
);
|
|
|
|
|
2022-08-25 21:53:42 +01:00
|
|
|
let open_windows = {
|
|
|
|
let sway = get_client();
|
|
|
|
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
|
|
|
sway.get_open_windows()
|
|
|
|
}?;
|
2022-08-14 14:30:13 +01:00
|
|
|
|
|
|
|
for window in open_windows {
|
|
|
|
launcher.add_window(window);
|
|
|
|
}
|
|
|
|
|
|
|
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
|
|
|
|
2022-08-25 21:53:42 +01:00
|
|
|
spawn_blocking(move || {
|
|
|
|
let srx = {
|
|
|
|
let sway = get_client();
|
|
|
|
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
|
|
|
sway.subscribe_window()
|
|
|
|
};
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-08-25 21:53:42 +01:00
|
|
|
while let Ok(payload) = srx.recv() {
|
|
|
|
tx.send(payload)
|
|
|
|
.expect("Failed to send window event payload");
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
{
|
|
|
|
rx.attach(None, move |event| {
|
|
|
|
match event.change.as_str() {
|
|
|
|
"new" => launcher.add_window(event.container),
|
|
|
|
"close" => launcher.remove_window(&event.container),
|
|
|
|
"focus" => launcher.set_window_focused(&event.container),
|
|
|
|
"title" => launcher.set_window_title(event.container),
|
|
|
|
"urgent" => launcher.set_window_urgent(&event.container),
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
|
|
|
|
Continue(true)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
spawn(async move {
|
2022-08-25 21:53:42 +01:00
|
|
|
let sway = get_client();
|
|
|
|
|
2022-08-14 14:30:13 +01:00
|
|
|
while let Some(event) = ui_rx.recv().await {
|
|
|
|
let selector = match event {
|
|
|
|
FocusEvent::AppId(app_id) => format!("[app_id={}]", app_id),
|
|
|
|
FocusEvent::Class(class) => format!("[class={}]", class),
|
|
|
|
FocusEvent::ConId(id) => format!("[con_id={}]", id),
|
|
|
|
};
|
2022-08-25 21:53:42 +01:00
|
|
|
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
2022-08-21 23:36:07 +01:00
|
|
|
sway.run(format!("{} focus", selector))?;
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
2022-08-21 23:36:07 +01:00
|
|
|
|
|
|
|
Ok::<(), Report>(())
|
2022-08-14 14:30:13 +01:00
|
|
|
});
|
|
|
|
|
2022-08-21 23:36:07 +01:00
|
|
|
Ok(container)
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
|
|
|
}
|