mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-07-03 19:51:03 +02:00
chore: initial commit
This commit is contained in:
commit
e37d8f2b14
36 changed files with 4948 additions and 0 deletions
142
src/modules/launcher/icon.rs
Normal file
142
src/modules/launcher/icon.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconLookupFlags, IconTheme};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
use std::path::PathBuf;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Gets directories that should contain `.desktop` files
|
||||
/// and exist on the filesystem.
|
||||
fn find_application_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = vec![PathBuf::from("/usr/share/applications")];
|
||||
let user_dir = dirs::data_local_dir();
|
||||
|
||||
if let Some(mut user_dir) = user_dir {
|
||||
user_dir.push("applications");
|
||||
dirs.push(user_dir);
|
||||
}
|
||||
|
||||
dirs.into_iter().filter(|dir| dir.exists()).collect()
|
||||
}
|
||||
|
||||
/// Attempts to locate a `.desktop` file for an app id
|
||||
/// (or app class).
|
||||
///
|
||||
/// A simple case-insensitive check is performed on filename == `app_id`.
|
||||
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
|
||||
let dirs = find_application_dirs();
|
||||
|
||||
for dir in dirs {
|
||||
let mut walker = WalkDir::new(dir).max_depth(5).into_iter();
|
||||
|
||||
let entry = walker.find(|entry| match entry {
|
||||
Ok(entry) => {
|
||||
let file_name = entry.file_name().to_string_lossy().to_lowercase();
|
||||
let test_name = format!("{}.desktop", app_id.to_lowercase());
|
||||
file_name == test_name
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
if let Some(Ok(entry)) = entry {
|
||||
let path = entry.path().to_owned();
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Parses a desktop file into a flat hashmap of keys/values.
|
||||
fn parse_desktop_file(path: PathBuf) -> io::Result<HashMap<String, String>> {
|
||||
let file = File::open(path)?;
|
||||
let lines = io::BufReader::new(file).lines();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for line in lines.flatten() {
|
||||
let is_pair = line.contains('=');
|
||||
if is_pair {
|
||||
let (key, value) = line.split_once('=').unwrap();
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// Attempts to get the icon name from the app's `.desktop` file.
|
||||
fn get_desktop_icon_name(app_id: &str) -> Option<String> {
|
||||
match find_desktop_file(app_id) {
|
||||
Some(file) => {
|
||||
let map = parse_desktop_file(file);
|
||||
|
||||
match map {
|
||||
Ok(map) => map.get("Icon").map(std::string::ToString::to_string),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
enum IconLocation {
|
||||
Theme(String),
|
||||
File(PathBuf),
|
||||
}
|
||||
|
||||
fn get_icon_location(theme: &IconTheme, app_id: &str, size: i32) -> Option<IconLocation> {
|
||||
let has_icon = theme
|
||||
.lookup_icon(app_id, size, IconLookupFlags::empty())
|
||||
.is_some();
|
||||
|
||||
if has_icon {
|
||||
return Some(IconLocation::Theme(app_id.to_string()));
|
||||
}
|
||||
|
||||
let is_steam_game = app_id.starts_with("steam_app_");
|
||||
if is_steam_game {
|
||||
let steam_id: String = app_id.chars().skip("steam_app_".len()).collect();
|
||||
let home_dir = dirs::data_dir().unwrap();
|
||||
let path = home_dir.join(format!(
|
||||
"icons/hicolor/32x32/apps/steam_icon_{}.png",
|
||||
steam_id
|
||||
));
|
||||
|
||||
return Some(IconLocation::File(path));
|
||||
}
|
||||
|
||||
let icon_name = get_desktop_icon_name(app_id);
|
||||
if let Some(icon_name) = icon_name {
|
||||
let is_path = PathBuf::from(&icon_name).exists();
|
||||
|
||||
return if is_path {
|
||||
Some(IconLocation::File(PathBuf::from(icon_name)))
|
||||
} else {
|
||||
return Some(IconLocation::Theme(icon_name));
|
||||
};
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Gets the icon associated with an app.
|
||||
pub fn get_icon(theme: &IconTheme, app_id: &str, size: i32) -> Option<Pixbuf> {
|
||||
let icon_location = get_icon_location(theme, app_id, size);
|
||||
|
||||
match icon_location {
|
||||
Some(IconLocation::Theme(icon_name)) => {
|
||||
let icon = theme.load_icon(&icon_name, size, IconLookupFlags::empty());
|
||||
|
||||
match icon {
|
||||
Ok(icon) => icon,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
Some(IconLocation::File(path)) => Pixbuf::from_file_at_scale(path, size, size, true).ok(),
|
||||
None => None,
|
||||
}
|
||||
}
|
256
src/modules/launcher/item.rs
Normal file
256
src/modules/launcher/item.rs
Normal file
|
@ -0,0 +1,256 @@
|
|||
use crate::collection::Collection;
|
||||
use crate::modules::launcher::icon::{find_desktop_file, get_icon};
|
||||
use crate::modules::launcher::node::SwayNode;
|
||||
use crate::modules::launcher::popup::Popup;
|
||||
use crate::modules::launcher::FocusEvent;
|
||||
use crate::popup::PopupAlignment;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Image};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LauncherItem {
|
||||
pub app_id: String,
|
||||
pub favorite: bool,
|
||||
pub windows: Rc<Mutex<Collection<i32, LauncherWindow>>>,
|
||||
pub state: Arc<RwLock<State>>,
|
||||
pub button: Button,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LauncherWindow {
|
||||
pub con_id: i32,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
pub is_xwayland: bool,
|
||||
pub open: bool,
|
||||
pub focused: bool,
|
||||
pub urgent: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ButtonConfig {
|
||||
pub icon_theme: IconTheme,
|
||||
pub show_names: bool,
|
||||
pub show_icons: bool,
|
||||
pub popup: Popup,
|
||||
pub tx: mpsc::Sender<FocusEvent>,
|
||||
}
|
||||
|
||||
impl LauncherItem {
|
||||
pub fn new(app_id: String, favorite: bool, config: &ButtonConfig) -> Self {
|
||||
let button = Button::new();
|
||||
button.style_context().add_class("item");
|
||||
|
||||
let state = State {
|
||||
open: false,
|
||||
focused: false,
|
||||
urgent: false,
|
||||
is_xwayland: false,
|
||||
};
|
||||
|
||||
let item = Self {
|
||||
app_id,
|
||||
favorite,
|
||||
windows: Rc::new(Mutex::new(Collection::new())),
|
||||
state: Arc::new(RwLock::new(state)),
|
||||
button,
|
||||
};
|
||||
|
||||
item.configure_button(config);
|
||||
item
|
||||
}
|
||||
|
||||
pub fn from_node(node: &SwayNode, config: &ButtonConfig) -> Self {
|
||||
let button = Button::new();
|
||||
button.style_context().add_class("item");
|
||||
|
||||
let windows = Collection::from((
|
||||
node.id,
|
||||
LauncherWindow {
|
||||
con_id: node.id,
|
||||
name: node.name.clone(),
|
||||
},
|
||||
));
|
||||
|
||||
let state = State {
|
||||
open: true,
|
||||
focused: node.focused,
|
||||
urgent: node.urgent,
|
||||
is_xwayland: node.is_xwayland(),
|
||||
};
|
||||
|
||||
let item = Self {
|
||||
app_id: node.get_id().to_string(),
|
||||
favorite: false,
|
||||
windows: Rc::new(Mutex::new(windows)),
|
||||
state: Arc::new(RwLock::new(state)),
|
||||
button,
|
||||
};
|
||||
|
||||
item.configure_button(config);
|
||||
item
|
||||
}
|
||||
|
||||
fn configure_button(&self, config: &ButtonConfig) {
|
||||
let button = &self.button;
|
||||
|
||||
let windows = self.windows.lock().unwrap();
|
||||
|
||||
let name = if windows.len() == 1 {
|
||||
windows.first().unwrap().name.as_ref()
|
||||
} else {
|
||||
Some(&self.app_id)
|
||||
};
|
||||
|
||||
if let Some(name) = name {
|
||||
self.set_title(name, config);
|
||||
}
|
||||
|
||||
if config.show_icons {
|
||||
let icon = get_icon(&config.icon_theme, &self.app_id, 32);
|
||||
if icon.is_some() {
|
||||
let image = Image::from_pixbuf(icon.as_ref());
|
||||
button.set_image(Some(&image));
|
||||
button.set_always_show_image(true);
|
||||
}
|
||||
}
|
||||
|
||||
let app_id = self.app_id.clone();
|
||||
let state = Arc::clone(&self.state);
|
||||
let tx_click = config.tx.clone();
|
||||
|
||||
let (focus_tx, mut focus_rx) = mpsc::channel(32);
|
||||
|
||||
button.connect_clicked(move |_| {
|
||||
let state = state.read().unwrap();
|
||||
if state.open {
|
||||
focus_tx.try_send(()).unwrap();
|
||||
} else {
|
||||
// attempt to find desktop file and launch
|
||||
match find_desktop_file(&app_id) {
|
||||
Some(file) => {
|
||||
Command::new("gtk-launch")
|
||||
.arg(file.file_name().unwrap())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let app_id = self.app_id.clone();
|
||||
let state = Arc::clone(&self.state);
|
||||
|
||||
spawn(async move {
|
||||
while focus_rx.recv().await == Some(()) {
|
||||
let state = state.read().unwrap();
|
||||
if state.is_xwayland {
|
||||
tx_click
|
||||
.try_send(FocusEvent::Class(app_id.clone()))
|
||||
.unwrap();
|
||||
} else {
|
||||
tx_click
|
||||
.try_send(FocusEvent::AppId(app_id.clone()))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let popup = config.popup.clone();
|
||||
let popup2 = config.popup.clone();
|
||||
let windows = Rc::clone(&self.windows);
|
||||
let tx_hover = config.tx.clone();
|
||||
|
||||
button.connect_enter_notify_event(move |button, _| {
|
||||
let windows = windows.lock().unwrap();
|
||||
if windows.len() > 1 {
|
||||
let button_w = button.allocation().width();
|
||||
|
||||
let (button_x, _) = button
|
||||
.translate_coordinates(&button.toplevel().unwrap(), 0, 0)
|
||||
.unwrap();
|
||||
|
||||
let button_center = f64::from(button_x) + f64::from(button_w) / 2.0;
|
||||
|
||||
popup.set_windows(windows.as_slice(), &tx_hover);
|
||||
popup.show();
|
||||
|
||||
// TODO: Pass through module location
|
||||
popup.set_pos(button_center, PopupAlignment::Center);
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
{}
|
||||
|
||||
button.connect_leave_notify_event(move |_, e| {
|
||||
let (_, y) = e.position();
|
||||
// hover boundary
|
||||
if y > 2.0 {
|
||||
popup2.hide();
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
let style = button.style_context();
|
||||
|
||||
style.add_class("launcher-item");
|
||||
self.update_button_classes(&self.state.read().unwrap());
|
||||
|
||||
button.show_all();
|
||||
}
|
||||
|
||||
pub fn set_title(&self, title: &str, config: &ButtonConfig) {
|
||||
if config.show_names {
|
||||
self.button.set_label(title);
|
||||
} else {
|
||||
self.button.set_tooltip_text(Some(title));
|
||||
};
|
||||
}
|
||||
|
||||
/// Updates the classnames on the GTK button
|
||||
/// based on its current state.
|
||||
///
|
||||
/// State must be passed as an arg here rather than
|
||||
/// using `self.state` to avoid a weird `RwLock` issue.
|
||||
pub fn update_button_classes(&self, state: &State) {
|
||||
let style = self.button.style_context();
|
||||
|
||||
if self.favorite {
|
||||
style.add_class("favorite");
|
||||
} else {
|
||||
style.remove_class("favorite");
|
||||
}
|
||||
|
||||
if state.open {
|
||||
style.add_class("open");
|
||||
} else {
|
||||
style.remove_class("open");
|
||||
}
|
||||
|
||||
if state.focused {
|
||||
style.add_class("focused");
|
||||
} else {
|
||||
style.remove_class("focused");
|
||||
}
|
||||
|
||||
if state.urgent {
|
||||
style.add_class("urgent");
|
||||
} else {
|
||||
style.remove_class("urgent");
|
||||
}
|
||||
}
|
||||
}
|
271
src/modules/launcher/mod.rs
Normal file
271
src/modules/launcher/mod.rs
Normal file
|
@ -0,0 +1,271 @@
|
|||
mod icon;
|
||||
mod item;
|
||||
mod node;
|
||||
mod popup;
|
||||
|
||||
use crate::collection::Collection;
|
||||
use crate::modules::launcher::item::{ButtonConfig, LauncherItem, LauncherWindow};
|
||||
use crate::modules::launcher::node::{get_open_windows, SwayNode};
|
||||
use crate::modules::launcher::popup::Popup;
|
||||
use crate::modules::{Module, ModuleInfo};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconTheme, Orientation};
|
||||
use ksway::{Client, IpcEvent};
|
||||
use serde::Deserialize;
|
||||
use std::rc::Rc;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct LauncherModule {
|
||||
favorites: Option<Vec<String>>,
|
||||
#[serde(default = "default_false")]
|
||||
show_names: bool,
|
||||
#[serde(default = "default_true")]
|
||||
show_icons: bool,
|
||||
|
||||
icon_theme: Option<String>,
|
||||
}
|
||||
|
||||
const fn default_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WindowEvent {
|
||||
change: String,
|
||||
container: SwayNode,
|
||||
}
|
||||
|
||||
#[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.
|
||||
fn add_window(&mut self, window: SwayNode) {
|
||||
let id = window.get_id().to_string();
|
||||
|
||||
if let Some(item) = self.items.get_mut(&id) {
|
||||
let mut state = item.state.write().unwrap();
|
||||
state.open = true;
|
||||
state.focused = window.focused || state.focused;
|
||||
state.urgent = window.urgent || state.urgent;
|
||||
state.is_xwayland = window.is_xwayland();
|
||||
|
||||
item.update_button_classes(&state);
|
||||
|
||||
let mut windows = item.windows.lock().unwrap();
|
||||
|
||||
windows.insert(
|
||||
window.id,
|
||||
LauncherWindow {
|
||||
con_id: window.id,
|
||||
name: window.name,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
let item = LauncherItem::from_node(&window, &self.button_config);
|
||||
|
||||
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();
|
||||
|
||||
let item = self.items.get_mut(&id);
|
||||
|
||||
let remove = if let Some(item) = item {
|
||||
let windows = Rc::clone(&item.windows);
|
||||
let mut windows = windows.lock().unwrap();
|
||||
|
||||
windows.remove(&window.id);
|
||||
|
||||
if windows.is_empty() {
|
||||
let mut state = item.state.write().unwrap();
|
||||
state.open = false;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_window_focused(&mut self, window: &SwayNode) {
|
||||
let id = window.get_id().to_string();
|
||||
|
||||
let currently_focused = self
|
||||
.items
|
||||
.iter_mut()
|
||||
.find(|item| item.state.read().unwrap().focused);
|
||||
if let Some(currently_focused) = currently_focused {
|
||||
let mut state = currently_focused.state.write().unwrap();
|
||||
state.focused = false;
|
||||
currently_focused.update_button_classes(&state);
|
||||
}
|
||||
|
||||
let item = self.items.get_mut(&id);
|
||||
if let Some(item) = item {
|
||||
let mut state = item.state.write().unwrap();
|
||||
state.focused = true;
|
||||
item.update_button_classes(&state);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_window_title(&mut self, window: SwayNode) {
|
||||
let id = window.get_id().to_string();
|
||||
let item = self.items.get_mut(&id);
|
||||
|
||||
if let (Some(item), Some(name)) = (item, window.name) {
|
||||
item.set_title(&name, &self.button_config);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_window_urgent(&mut self, window: &SwayNode) {
|
||||
let id = window.get_id().to_string();
|
||||
let item = self.items.get_mut(&id);
|
||||
|
||||
if let Some(item) = item {
|
||||
let mut state = item.state.write().unwrap();
|
||||
state.urgent = window.urgent;
|
||||
item.update_button_classes(&state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for LauncherModule {
|
||||
fn into_widget(self, info: &ModuleInfo) -> gtk::Box {
|
||||
let icon_theme = IconTheme::new();
|
||||
|
||||
if let Some(theme) = self.icon_theme {
|
||||
icon_theme.set_custom_theme(Some(&theme));
|
||||
}
|
||||
|
||||
let mut sway = Client::connect().unwrap();
|
||||
|
||||
let popup = Popup::new("popup-launcher", info.app, Orientation::Vertical);
|
||||
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,
|
||||
);
|
||||
|
||||
let open_windows = get_open_windows(&mut sway);
|
||||
|
||||
for window in open_windows {
|
||||
launcher.add_window(window);
|
||||
}
|
||||
|
||||
let srx = sway.subscribe(vec![IpcEvent::Window]).unwrap();
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
spawn_blocking(move || loop {
|
||||
while let Ok((_, payload)) = srx.try_recv() {
|
||||
let payload: WindowEvent = serde_json::from_slice(&payload).unwrap();
|
||||
|
||||
tx.send(payload).unwrap();
|
||||
}
|
||||
sway.poll().unwrap();
|
||||
});
|
||||
|
||||
{
|
||||
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 {
|
||||
let mut sway = Client::connect().unwrap();
|
||||
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),
|
||||
};
|
||||
|
||||
sway.run(format!("{} focus", selector)).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
container
|
||||
}
|
||||
}
|
65
src/modules/launcher/node.rs
Normal file
65
src/modules/launcher/node.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use ksway::{Client, IpcCommand};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SwayNode {
|
||||
#[serde(rename = "type")]
|
||||
pub node_type: String,
|
||||
pub id: i32,
|
||||
pub name: Option<String>,
|
||||
pub app_id: Option<String>,
|
||||
pub focused: bool,
|
||||
pub urgent: bool,
|
||||
pub nodes: Vec<SwayNode>,
|
||||
pub floating_nodes: Vec<SwayNode>,
|
||||
pub shell: Option<String>,
|
||||
pub window_properties: Option<WindowProperties>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WindowProperties {
|
||||
pub class: String,
|
||||
}
|
||||
|
||||
impl SwayNode {
|
||||
pub fn get_id(&self) -> &str {
|
||||
self.app_id.as_ref().map_or_else(
|
||||
|| {
|
||||
&self
|
||||
.window_properties
|
||||
.as_ref()
|
||||
.expect("cannot find node name")
|
||||
.class
|
||||
},
|
||||
|app_id| app_id,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_xwayland(&self) -> bool {
|
||||
self.shell == Some(String::from("xwayland"))
|
||||
}
|
||||
}
|
||||
|
||||
fn check_node(node: SwayNode, window_nodes: &mut Vec<SwayNode>) {
|
||||
if node.name.is_some() && (node.node_type == "con" || node.node_type == "floating_con") {
|
||||
window_nodes.push(node);
|
||||
} else {
|
||||
node.nodes.into_iter().for_each(|node| {
|
||||
check_node(node, window_nodes);
|
||||
});
|
||||
|
||||
node.floating_nodes.into_iter().for_each(|node| {
|
||||
check_node(node, window_nodes);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_open_windows(sway: &mut Client) -> Vec<SwayNode> {
|
||||
let raw = sway.ipc(IpcCommand::GetTree).unwrap();
|
||||
let root_node = serde_json::from_slice::<SwayNode>(&raw).unwrap();
|
||||
|
||||
let mut window_nodes = vec![];
|
||||
check_node(root_node, &mut window_nodes);
|
||||
|
||||
window_nodes
|
||||
}
|
35
src/modules/launcher/popup.rs
Normal file
35
src/modules/launcher/popup.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use crate::modules::launcher::item::LauncherWindow;
|
||||
use crate::modules::launcher::FocusEvent;
|
||||
pub use crate::popup::Popup;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Button;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
impl Popup {
|
||||
pub fn set_windows(&self, windows: &[LauncherWindow], tx: &mpsc::Sender<FocusEvent>) {
|
||||
// clear
|
||||
for child in self.container.children() {
|
||||
self.container.remove(&child);
|
||||
}
|
||||
|
||||
for window in windows {
|
||||
let mut button_builder = Button::builder().height_request(40);
|
||||
|
||||
if let Some(name) = &window.name {
|
||||
button_builder = button_builder.label(name);
|
||||
}
|
||||
|
||||
let button = button_builder.build();
|
||||
|
||||
let con_id = window.con_id;
|
||||
let window = self.window.clone();
|
||||
let tx = tx.clone();
|
||||
button.connect_clicked(move |_| {
|
||||
tx.try_send(FocusEvent::ConId(con_id)).unwrap();
|
||||
window.hide();
|
||||
});
|
||||
|
||||
self.container.add(&button);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue