1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-09-18 12:46:58 +02:00

Merge branch 'master' into feat/networkmanager-multi-icon

This commit is contained in:
Reinout Meliesie 2025-08-20 17:40:27 +02:00
commit 4a09e95370
Signed by: zedfrigg
GPG key ID: 3AFCC06481308BC6
15 changed files with 363 additions and 231 deletions

View file

@ -52,16 +52,11 @@ where
}
#[cfg(feature = "schema")]
pub fn schema_layer(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::JsonSchema;
let mut schema: schemars::schema::SchemaObject = <String>::json_schema(generator).into();
schema.enum_values = Some(vec![
"background".into(),
"bottom".into(),
"top".into(),
"overlay".into(),
]);
schema.into()
pub fn schema_layer(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"enum": ["background", "bottom", "top", "overlay"],
})
}
impl BarPosition {

View file

@ -3,9 +3,10 @@ use color_eyre::{Help, Report, Result};
use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::process::Stdio;
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use tokio::sync::Mutex;
use tracing::{debug, error};
use walkdir::{DirEntry, WalkDir};
@ -238,6 +239,7 @@ impl DesktopFiles {
/// Checks file contents for an exact or partial match of the provided input.
async fn find_by_file_contents(&self, app_id: &str) -> Result<Option<DesktopFile>> {
let mut files = self.files.lock().await;
let app_id_lower = app_id.to_lowercase();
// first pass - check name for exact match
for (_, file_ref) in files.iter_mut() {
@ -253,7 +255,7 @@ impl DesktopFiles {
for (_, file_ref) in files.iter_mut() {
let file = file_ref.get().await?;
if let Some(name) = &file.name {
if name.to_lowercase().contains(app_id) {
if name.to_lowercase().contains(&app_id_lower) {
return Ok(Some(file));
}
}
@ -264,19 +266,19 @@ impl DesktopFiles {
let file = file_ref.get().await?;
if let Some(name) = &file.exec {
if name.to_lowercase().contains(app_id) {
if name.to_lowercase().contains(&app_id_lower) {
return Ok(Some(file));
}
}
if let Some(name) = &file.startup_wm_class {
if name.to_lowercase().contains(app_id) {
if name.to_lowercase().contains(&app_id_lower) {
return Ok(Some(file));
}
}
if let Some(name) = &file.icon {
if name.to_lowercase().contains(app_id) {
if name.to_lowercase().contains(&app_id_lower) {
return Ok(Some(file));
}
}
@ -325,21 +327,37 @@ fn files(dir: &Path) -> Vec<PathBuf> {
}
/// Starts a `.desktop` file with the provided formatted command.
pub fn open_program(file_name: &str, str: &str) {
let expanded = str.replace("{app_name}", file_name);
pub async fn open_program(file_name: &str, launch_command: &str) {
let expanded = launch_command.replace("{app_name}", file_name);
let launch_command_parts: Vec<&str> = expanded.split_whitespace().collect();
if let Err(err) = Command::new(&launch_command_parts[0])
debug!("running {launch_command_parts:?}");
let exit_status = match Command::new(launch_command_parts[0])
.args(&launch_command_parts[1..])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.kill_on_drop(true)
.spawn()
{
error!(
"{:?}",
Report::new(err)
.wrap_err("Failed to run launch command.")
.suggestion("Perhaps the applications file is invalid?")
);
Ok(mut child) => Some(child.wait().await),
Err(err) => {
error!(
"{:?}",
Report::new(err)
.wrap_err("Failed to run launch command.")
.suggestion("Perhaps the desktop file is invalid or orphaned?")
);
None
}
};
match exit_status {
Some(Ok(exit_status)) if !exit_status.success() => {
error!("received non-success exit status running {launch_command_parts:?}")
}
Some(Err(err)) => error!("{err:?}"),
_ => {}
}
}

View file

@ -2,7 +2,7 @@ use super::Ipc;
use crate::ipc::{Command, Response};
use color_eyre::Result;
use color_eyre::{Help, Report};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixStream;
impl Ipc {
@ -16,18 +16,20 @@ impl Ipc {
.suggestion("Is Ironbar running?")),
}?;
let write_buffer = serde_json::to_vec(&command)?;
let mut write_buffer = serde_json::to_vec(&command)?;
if debug {
eprintln!("REQUEST JSON: {}", serde_json::to_string(&command)?);
}
write_buffer.push(b'\n');
stream.write_all(&write_buffer).await?;
let mut read_buffer = vec![0; 1024];
let bytes = stream.read(&mut read_buffer).await?;
let mut read_buffer = String::new();
let mut reader = BufReader::new(stream);
let bytes = reader.read_line(&mut read_buffer).await?;
let response = serde_json::from_slice(&read_buffer[..bytes])?;
let response = serde_json::from_str(&read_buffer[..bytes])?;
Ok(response)
}
}

View file

@ -8,10 +8,10 @@ use std::rc::Rc;
use color_eyre::{Report, Result};
use gtk::Application;
use gtk::prelude::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::mpsc::{self, Receiver, Sender};
use tracing::{debug, error, info, warn};
use tracing::{debug, error, info, trace, warn};
use super::Ipc;
use crate::channels::{AsyncSenderExt, MpscReceiverExt};
@ -52,11 +52,13 @@ impl Ipc {
loop {
match listener.accept().await {
Ok((stream, _addr)) => {
debug!("handling incoming connection");
if let Err(err) =
Self::handle_connection(stream, &cmd_tx, &mut res_rx).await
{
error!("{err:?}");
}
debug!("done");
}
Err(err) => {
error!("{err:?}");
@ -80,10 +82,16 @@ impl Ipc {
cmd_tx: &Sender<Command>,
res_rx: &mut Receiver<Response>,
) -> Result<()> {
let (mut stream_read, mut stream_write) = stream.split();
trace!("awaiting readable state");
stream.readable().await?;
let mut read_buffer = vec![0; 1024];
let bytes = stream_read.read(&mut read_buffer).await?;
let mut read_buffer = Vec::with_capacity(1024);
let mut reader = BufReader::new(&mut stream);
trace!("reading bytes");
let bytes = reader.read_until(b'\n', &mut read_buffer).await?;
debug!("read {} bytes", bytes);
// FIXME: Error on invalid command
let command = serde_json::from_slice::<Command>(&read_buffer[..bytes])?;
@ -95,10 +103,18 @@ impl Ipc {
.recv()
.await
.unwrap_or(Response::Err { message: None });
let res = serde_json::to_vec(&res)?;
stream_write.write_all(&res).await?;
stream_write.shutdown().await?;
let mut res = serde_json::to_vec(&res)?;
res.push(b'\n');
trace!("awaiting writable state");
stream.writable().await?;
debug!("writing {} bytes", res.len());
stream.write_all(&res).await?;
trace!("bytes written, shutting down stream");
stream.shutdown().await?;
Ok(())
}

View file

@ -1,8 +1,7 @@
use std::collections::HashMap;
use color_eyre::Result;
use color_eyre::eyre::Report;
use gtk::prelude::*;
use indexmap::IndexMap;
use serde::Deserialize;
use tokio::sync::mpsc;
use tracing::{debug, trace};
@ -129,7 +128,7 @@ struct Icons {
/// }
/// ```
#[serde(default)]
layout_map: HashMap<String, String>,
layout_map: IndexMap<String, String>,
}
impl Default for Icons {
@ -141,7 +140,7 @@ impl Default for Icons {
num_off: String::new(),
scroll_on: default_icon_scroll(),
scroll_off: String::new(),
layout_map: HashMap::new(),
layout_map: IndexMap::new(),
}
}
}
@ -338,7 +337,19 @@ impl Module<gtk::Box> for KeyboardModule {
}
}
KeyboardUpdate::Layout(KeyboardLayoutUpdate(language)) => {
let text = icons.layout_map.get(&language).unwrap_or(&language);
let text = icons
.layout_map
.iter()
.find_map(|(pattern, display_text)| {
let is_match = if pattern.ends_with("*") {
language.starts_with(&pattern[..pattern.len() - 1])
} else {
pattern == &language
};
is_match.then(|| display_text)
})
.unwrap_or(&language);
layout_button.set_label(text);
}
});

View file

@ -244,21 +244,43 @@ impl Module<gtk::Box> for LauncherModule {
let tx2 = context.tx.clone();
let wl = context.client::<wayland::Client>();
let desktop_files = context.ironbar.desktop_files();
spawn(async move {
let items = items2;
let tx = tx2;
// Build app_id mapping once at startup
let mut app_id_map = IndexMap::<String, String>::new();
{
let favorites: Vec<_> = lock!(items).keys().cloned().collect();
for fav in favorites {
if let Ok(Some(file)) = desktop_files.find(&fav).await {
if let Some(wm_class) = file.startup_wm_class {
app_id_map.insert(wm_class, fav);
}
}
}
}
let resolve_app_id = |app_id: &str| {
app_id_map
.get(app_id)
.cloned()
.unwrap_or_else(|| app_id.to_string())
};
let mut wlrx = wl.subscribe_toplevels();
let handles = wl.toplevel_info_all();
for info in handles {
let mut items = lock!(items);
let item = items.get_mut(&info.app_id);
if let Some(item) = item {
let app_id = resolve_app_id(&info.app_id);
if let Some(item) = items.get_mut(&app_id) {
item.merge_toplevel(info.clone());
} else {
let item = Item::from(info.clone());
items.insert(info.app_id.clone(), item);
let mut item = Item::from(info.clone());
item.app_id = app_id.clone();
items.insert(app_id, item);
}
}
@ -284,14 +306,14 @@ impl Module<gtk::Box> for LauncherModule {
match event {
ToplevelEvent::New(info) => {
let app_id = info.app_id.clone();
let app_id = resolve_app_id(&info.app_id);
let new_item = {
let mut items = lock!(items);
let item = items.get_mut(&info.app_id);
match item {
match items.get_mut(&app_id) {
None => {
let item: Item = info.into();
let mut item: Item = info.into();
item.app_id = app_id.clone();
items.insert(app_id.clone(), item.clone());
ItemOrWindow::Item(item)
@ -313,9 +335,10 @@ impl Module<gtk::Box> for LauncherModule {
}?;
}
ToplevelEvent::Update(info) => {
let app_id = resolve_app_id(&info.app_id);
// check if open, as updates can be sent as program closes
// if it's a focused favourite closing, it otherwise incorrectly re-focuses.
let is_open = if let Some(item) = lock!(items).get_mut(&info.app_id) {
let is_open = if let Some(item) = lock!(items).get_mut(&app_id) {
item.set_window_focused(info.id, info.focused);
item.set_window_name(info.id, info.title.clone());
@ -325,27 +348,27 @@ impl Module<gtk::Box> for LauncherModule {
};
send_update(LauncherUpdate::Focus(
info.app_id.clone(),
app_id.clone(),
is_open && info.focused,
))
.await?;
send_update(LauncherUpdate::Title(
info.app_id.clone(),
app_id.clone(),
info.id,
info.title.clone(),
))
.await?;
}
ToplevelEvent::Remove(info) => {
let app_id = resolve_app_id(&info.app_id);
let remove_item = {
let mut items = lock!(items);
let item = items.get_mut(&info.app_id);
match item {
match items.get_mut(&app_id) {
Some(item) => {
item.unmerge_toplevel(&info);
if item.windows.is_empty() {
items.shift_remove(&info.app_id);
items.shift_remove(&app_id);
Some(ItemOrWindowId::Item)
} else {
Some(ItemOrWindowId::Window)
@ -357,15 +380,11 @@ impl Module<gtk::Box> for LauncherModule {
match remove_item {
Some(ItemOrWindowId::Item) => {
send_update(LauncherUpdate::RemoveItem(info.app_id.clone()))
.await?;
send_update(LauncherUpdate::RemoveItem(app_id.clone())).await?;
}
Some(ItemOrWindowId::Window) => {
send_update(LauncherUpdate::RemoveWindow(
info.app_id.clone(),
info.id,
))
.await?;
send_update(LauncherUpdate::RemoveWindow(app_id.clone(), info.id))
.await?;
}
None => {}
}
@ -388,7 +407,7 @@ impl Module<gtk::Box> for LauncherModule {
if let ItemEvent::OpenItem(app_id) = event {
match desktop_files.find(&app_id).await {
Ok(Some(file)) => {
open_program(&file.file_name, &launch_command_str);
open_program(&file.file_name, &launch_command_str).await;
}
Ok(None) => warn!("Could not find applications file for {}", app_id),
Err(err) => error!("Failed to find parse file for {}: {}", app_id, err),

View file

@ -100,7 +100,11 @@ where
let tx = tx.clone();
button.connect_clicked(move |_button| {
open_program(&file_name, &command);
// TODO: this needs refactoring to call open from the controller
let file_name = file_name.clone();
let command = command.clone();
spawn(async move { open_program(&file_name, &command).await });
sub_menu.hide();
tx.send_spawn(ModuleUpdateEvent::ClosePopup);

View file

@ -11,6 +11,7 @@ use std::collections::HashSet;
use std::ffi::CStr;
use std::os::raw::{c_char, c_int};
use std::ptr;
use system_tray::item::IconPixmap;
/// Gets the GTK icon theme search paths by calling the FFI function.
/// Conveniently returns the result as a `HashSet`.
@ -45,10 +46,10 @@ pub fn get_image(
icon_theme: &IconTheme,
) -> Result<Image> {
if !prefer_icons && item.icon_pixmap.is_some() {
get_image_from_pixmap(item, size)
get_image_from_pixmap(item.icon_pixmap.as_deref(), size)
} else {
get_image_from_icon_name(item, size, icon_theme)
.or_else(|_| get_image_from_pixmap(item, size))
.or_else(|_| get_image_from_pixmap(item.icon_pixmap.as_deref(), size))
}
}
@ -81,12 +82,10 @@ fn get_image_from_icon_name(item: &TrayMenu, size: u32, icon_theme: &IconTheme)
/// which has 8 bits per sample and a bit stride of `4*width`.
/// The Pixbuf expects RGBA32 format, so some channel shuffling
/// is required.
fn get_image_from_pixmap(item: &TrayMenu, size: u32) -> Result<Image> {
fn get_image_from_pixmap(item: Option<&[IconPixmap]>, size: u32) -> Result<Image> {
const BITS_PER_SAMPLE: i32 = 8;
let pixmap = item
.icon_pixmap
.as_ref()
.and_then(|pixmap| pixmap.first())
.ok_or_else(|| Report::msg("Failed to get pixmap from tray icon"))?;

View file

@ -181,9 +181,14 @@ fn on_update(
UpdateEvent::AttentionIcon(_icon) => {
warn!("received unimplemented NewAttentionIcon event");
}
UpdateEvent::Icon(icon) => {
if icon.as_ref() != menu_item.icon_name() {
menu_item.set_icon_name(icon);
UpdateEvent::Icon {
icon_name,
icon_pixmap,
} => {
menu_item.icon_pixmap = icon_pixmap;
if icon_name.as_ref() != menu_item.icon_name() {
menu_item.set_icon_name(icon_name);
match icon::get_image(menu_item, icon_size, prefer_icons, icon_theme) {
Ok(image) => menu_item.set_image(&image),
Err(_) => menu_item.show_label(),