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:
commit
4a09e95370
15 changed files with 363 additions and 231 deletions
|
@ -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 {
|
||||
|
|
|
@ -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:?}"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"))?;
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue