mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-04-19 11:24:24 +02:00
feat(workspaces): support for using images in name_map
This commit is contained in:
parent
3cf9be89fd
commit
b054c17d14
13 changed files with 132 additions and 64 deletions
15
docs/Images.md
Normal file
15
docs/Images.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
Ironbar is capable of loading images from multiple sources.
|
||||
In any situation where an option takes text or an icon,
|
||||
you can use a string in any of the following formats, and it will automatically be detected as an image:
|
||||
|
||||
| Source | Example |
|
||||
|-------------------------------|---------------------------------|
|
||||
| GTK icon theme | `icon:firefox` |
|
||||
| Local file | `file:///path/to/file.jpg` |
|
||||
| Remote file (over HTTP/HTTPS) | `https://example.com/image.jpg` |
|
||||
|
||||
Remote images are loaded asynchronously to avoid blocking the UI thread.
|
||||
Be aware this can cause elements to change size upon load if the image is large enough.
|
||||
|
||||
Note that mixing text and images is not supported.
|
||||
Your best option here is to use Nerd Font icons instead.
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
- [Configuration guide](configuration-guide)
|
||||
- [Scripts](scripts)
|
||||
- [Images](images)
|
||||
- [Styling guide](styling-guide)
|
||||
|
||||
# Examples
|
||||
|
|
|
@ -25,7 +25,7 @@ It is well worth looking at the examples.
|
|||
| `class` | `string` | `null` | Widget class name. |
|
||||
| `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. |
|
||||
| `on_click` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). |
|
||||
| `src` | `string` | `null` | [`image`] Image source. More on this [below](#images). |
|
||||
| `src` | `image` | `null` | [`image`] Image source. See [here](images) for information on images. |
|
||||
| `size` | `integer` | `null` | [`image`] Width/height of the image. Aspect ratio is preserved. |
|
||||
| `orientation` | `horizontal` or `vertical` | `horizontal` | [`box`] Whether child widgets should be horizontally or vertically added. |
|
||||
| `widgets` | `Widget[]` | `[]` | [`box`] List of widgets to add to this box. |
|
||||
|
@ -58,17 +58,6 @@ The following bar commands are supported:
|
|||
- `popup:open`
|
||||
- `popup:close`
|
||||
|
||||
### Images
|
||||
|
||||
Ironbar is capable of loading images from multiple sources:
|
||||
|
||||
- GTK icons: `icon:firefox`
|
||||
- Local files: `file:///path/to/file.jpg`
|
||||
- Remote files (over HTTP/HTTPS): `https://example.com/image.jpg`
|
||||
|
||||
Remote images are loaded asynchronously to avoid blocking the UI thread.
|
||||
Be aware this can cause elements to change size upon load if the image is large enough.
|
||||
|
||||
---
|
||||
|
||||
XML is arguably better-suited and easier to read for this sort of markup,
|
||||
|
|
|
@ -8,11 +8,11 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
|||
|
||||
> Type: `workspaces`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|----------------|---------------------------|----------------|----------------------------------------------------------------------------------------------------------------------|
|
||||
| `name_map` | `Map<string, string>` | `{}` | A map of actual workspace names to their display labels. Workspaces use their actual name if not present in the map. |
|
||||
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
||||
| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
||||
| Name | Type | Default | Description |
|
||||
|----------------|-----------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `name_map` | `Map<string, string/image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
|
||||
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
||||
| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
@ -72,15 +72,15 @@ end:
|
|||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "workspaces",
|
||||
name_map.1 = ""
|
||||
name_map.2 = ""
|
||||
name_map.3 = ""
|
||||
all_monitors = false
|
||||
}
|
||||
]
|
||||
end = [
|
||||
{
|
||||
type = "workspaces",
|
||||
name_map.1 = ""
|
||||
name_map.2 = ""
|
||||
name_map.3 = ""
|
||||
all_monitors = false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
48
src/image/gtk.rs
Normal file
48
src/image/gtk.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use super::ImageProvider;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
||||
use tracing::error;
|
||||
|
||||
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
|
||||
let button = Button::new();
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
let image = Image::new();
|
||||
match ImageProvider::parse(input, icon_theme, size)
|
||||
.and_then(|provider| provider.load_into_image(image.clone()))
|
||||
{
|
||||
Ok(_) => {
|
||||
button.set_image(Some(&image));
|
||||
button.set_always_show_image(true);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{err:?}");
|
||||
button.set_label(input);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
button.set_label(input);
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
|
||||
pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Box {
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
let image = Image::new();
|
||||
container.add(&image);
|
||||
|
||||
if let Err(err) = ImageProvider::parse(input, icon_theme, size)
|
||||
.and_then(|provider| provider.load_into_image(image))
|
||||
{
|
||||
error!("{err:?}");
|
||||
}
|
||||
} else {
|
||||
let label = Label::new(Some(input));
|
||||
container.add(&label);
|
||||
}
|
||||
|
||||
container
|
||||
}
|
5
src/image/mod.rs
Normal file
5
src/image/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod gtk;
|
||||
mod provider;
|
||||
|
||||
pub use self::gtk::*;
|
||||
pub use provider::ImageProvider;
|
|
@ -30,7 +30,7 @@ impl<'a> ImageProvider<'a> {
|
|||
///
|
||||
/// Note this checks that icons exist in theme, or files exist on disk
|
||||
/// but no other check is performed.
|
||||
pub fn parse(input: String, theme: &'a IconTheme, size: i32) -> Result<Self> {
|
||||
pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Result<Self> {
|
||||
let location = Self::get_location(input, theme, size)?;
|
||||
Ok(Self { location, size })
|
||||
}
|
||||
|
@ -45,11 +45,10 @@ impl<'a> ImageProvider<'a> {
|
|||
|| input.starts_with("https://")
|
||||
}
|
||||
|
||||
fn get_location(input: String, theme: &'a IconTheme, size: i32) -> Result<ImageLocation> {
|
||||
fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Result<ImageLocation<'a>> {
|
||||
let (input_type, input_name) = input
|
||||
.split_once(':')
|
||||
.map(|(t, n)| (Some(t), n))
|
||||
.unwrap_or((None, &input));
|
||||
.map_or((None, input), |(t, n)| (Some(t), n));
|
||||
|
||||
match input_type {
|
||||
Some(input_type) if input_type == "icon" => Ok(ImageLocation::Icon {
|
||||
|
@ -66,7 +65,7 @@ impl<'a> ImageProvider<'a> {
|
|||
input_name.chars().skip("steam_app_".len()).collect(),
|
||||
)),
|
||||
None if theme
|
||||
.lookup_icon(&input, size, IconLookupFlags::empty())
|
||||
.lookup_icon(input, size, IconLookupFlags::empty())
|
||||
.is_some() =>
|
||||
{
|
||||
Ok(ImageLocation::Icon {
|
||||
|
@ -78,10 +77,10 @@ impl<'a> ImageProvider<'a> {
|
|||
None if PathBuf::from(input_name).exists() => {
|
||||
Ok(ImageLocation::Local(PathBuf::from(input_name)))
|
||||
}
|
||||
None => match get_desktop_icon_name(input_name) {
|
||||
Some(input) => Self::get_location(input, theme, size),
|
||||
None => Err(Report::msg("Unknown image type")),
|
||||
},
|
||||
None => get_desktop_icon_name(input_name).map_or_else(
|
||||
|| Err(Report::msg("Unknown image type")),
|
||||
|input| Self::get_location(&input, theme, size),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -120,8 +119,6 @@ impl<'a> ImageProvider<'a> {
|
|||
Continue(false)
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
let pixbuf = match &self.location {
|
||||
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme),
|
||||
|
@ -131,8 +128,9 @@ impl<'a> ImageProvider<'a> {
|
|||
}?;
|
||||
|
||||
image.set_pixbuf(Some(&pixbuf));
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from the GTK icon theme.
|
||||
|
@ -142,10 +140,10 @@ impl<'a> ImageProvider<'a> {
|
|||
None => Ok(None),
|
||||
}?;
|
||||
|
||||
match pixbuf {
|
||||
Some(pixbuf) => Ok(pixbuf),
|
||||
None => Err(Report::msg("Icon theme does not contain icon '{name}'")),
|
||||
}
|
||||
pixbuf.map_or_else(
|
||||
|| Err(Report::msg("Icon theme does not contain icon '{name}'")),
|
||||
Ok,
|
||||
)
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from a local file.
|
||||
|
@ -158,12 +156,14 @@ impl<'a> ImageProvider<'a> {
|
|||
/// using the Steam game ID to look it up.
|
||||
fn get_from_steam_id(&self, steam_id: &str) -> Result<Pixbuf> {
|
||||
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
|
||||
let path = match dirs::data_dir() {
|
||||
Some(dir) => Ok(dir.join(format!(
|
||||
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
|
||||
))),
|
||||
None => Err(Report::msg("Missing XDG data dir")),
|
||||
}?;
|
||||
let path = dirs::data_dir().map_or_else(
|
||||
|| Err(Report::msg("Missing XDG data dir")),
|
||||
|dir| {
|
||||
Ok(dir.join(format!(
|
||||
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
|
||||
)))
|
||||
},
|
||||
)?;
|
||||
|
||||
self.get_from_file(&path)
|
||||
}
|
|
@ -120,6 +120,8 @@ impl Module<Button> for ClockModule {
|
|||
});
|
||||
}
|
||||
|
||||
container.show_all();
|
||||
|
||||
Some(container)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,9 +103,9 @@ impl Widget {
|
|||
}
|
||||
|
||||
if let Some(widgets) = self.widgets {
|
||||
widgets.into_iter().for_each(|widget| {
|
||||
widget.add_to(&container, tx.clone(), bar_orientation, icon_theme)
|
||||
});
|
||||
for widget in widgets {
|
||||
widget.add_to(&container, tx.clone(), bar_orientation, icon_theme);
|
||||
}
|
||||
}
|
||||
|
||||
container
|
||||
|
@ -185,7 +185,7 @@ impl Widget {
|
|||
|
||||
if let Some(src) = self.src {
|
||||
let size = self.size.unwrap_or(32);
|
||||
if let Err(err) = ImageProvider::parse(src, icon_theme, size)
|
||||
if let Err(err) = ImageProvider::parse(&src, icon_theme, size)
|
||||
.and_then(|image| image.load_into_image(gtk_image.clone()))
|
||||
{
|
||||
error!("{err:?}");
|
||||
|
@ -292,16 +292,18 @@ impl Module<gtk::Box> for CustomModule {
|
|||
}
|
||||
|
||||
if let Some(popup) = self.popup {
|
||||
popup.into_iter().for_each(|widget| {
|
||||
for widget in popup {
|
||||
widget.add_to(
|
||||
&container,
|
||||
tx.clone(),
|
||||
Orientation::Horizontal,
|
||||
info.icon_theme,
|
||||
)
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
container.show_all();
|
||||
|
||||
Some(container)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,7 +111,7 @@ impl Module<gtk::Box> for FocusedModule {
|
|||
let icon_theme = icon_theme.clone();
|
||||
context.widget_rx.attach(None, move |(name, id)| {
|
||||
if self.show_icon {
|
||||
if let Err(err) = ImageProvider::parse(id, &icon_theme, self.icon_size)
|
||||
if let Err(err) = ImageProvider::parse(&id, &icon_theme, self.icon_size)
|
||||
.and_then(|image| image.load_into_image(icon.clone()))
|
||||
{
|
||||
error!("{err:?}");
|
||||
|
|
|
@ -156,7 +156,7 @@ impl ItemButton {
|
|||
|
||||
if show_icons {
|
||||
let gtk_image = gtk::Image::new();
|
||||
let image = ImageProvider::parse(item.app_id.clone(), icon_theme, 32);
|
||||
let image = ImageProvider::parse(&item.app_id.clone(), icon_theme, 32);
|
||||
match image {
|
||||
Ok(image) => {
|
||||
button.set_image(Some(>k_image));
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use crate::clients::compositor::{Compositor, WorkspaceUpdate};
|
||||
use crate::config::CommonConfig;
|
||||
use crate::image::new_icon_button;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::{send_async, try_send};
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::Button;
|
||||
use gtk::{Button, IconTheme};
|
||||
use serde::Deserialize;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
|
@ -49,12 +50,13 @@ fn create_button(
|
|||
name: &str,
|
||||
focused: bool,
|
||||
name_map: &HashMap<String, String>,
|
||||
icon_theme: &IconTheme,
|
||||
tx: &Sender<String>,
|
||||
) -> Button {
|
||||
let button = Button::builder()
|
||||
.label(name_map.get(name).map_or(name, String::as_str))
|
||||
.name(name)
|
||||
.build();
|
||||
let label = name_map.get(name).map_or(name, String::as_str);
|
||||
|
||||
let button = new_icon_button(label, icon_theme, 32);
|
||||
button.set_widget_name(name);
|
||||
|
||||
let style_context = button.style_context();
|
||||
style_context.add_class("item");
|
||||
|
@ -154,6 +156,7 @@ impl Module<gtk::Box> for WorkspacesModule {
|
|||
{
|
||||
let container = container.clone();
|
||||
let output_name = info.output_name.to_string();
|
||||
let icon_theme = info.icon_theme.clone();
|
||||
|
||||
// keep track of whether init event has fired previously
|
||||
// since it fires for every workspace subscriber
|
||||
|
@ -170,6 +173,7 @@ impl Module<gtk::Box> for WorkspacesModule {
|
|||
&workspace.name,
|
||||
workspace.focused,
|
||||
&name_map,
|
||||
&icon_theme,
|
||||
&context.controller_tx,
|
||||
);
|
||||
container.add(&item);
|
||||
|
@ -204,6 +208,7 @@ impl Module<gtk::Box> for WorkspacesModule {
|
|||
&name,
|
||||
workspace.focused,
|
||||
&name_map,
|
||||
&icon_theme,
|
||||
&context.controller_tx,
|
||||
);
|
||||
|
||||
|
@ -227,6 +232,7 @@ impl Module<gtk::Box> for WorkspacesModule {
|
|||
&name,
|
||||
workspace.focused,
|
||||
&name_map,
|
||||
&icon_theme,
|
||||
&context.controller_tx,
|
||||
);
|
||||
|
||||
|
|
|
@ -133,7 +133,7 @@ impl Popup {
|
|||
|
||||
/// Shows the popup
|
||||
pub fn show(&self, geometry: ButtonGeometry) {
|
||||
self.window.show_all();
|
||||
self.window.show();
|
||||
self.set_pos(geometry);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue