mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-07-01 02:31:04 +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)
|
- [Configuration guide](configuration-guide)
|
||||||
- [Scripts](scripts)
|
- [Scripts](scripts)
|
||||||
|
- [Images](images)
|
||||||
- [Styling guide](styling-guide)
|
- [Styling guide](styling-guide)
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
|
|
|
@ -25,7 +25,7 @@ It is well worth looking at the examples.
|
||||||
| `class` | `string` | `null` | Widget class name. |
|
| `class` | `string` | `null` | Widget class name. |
|
||||||
| `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. |
|
| `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). |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `widgets` | `Widget[]` | `[]` | [`box`] List of widgets to add to this box. |
|
||||||
|
@ -58,17 +58,6 @@ The following bar commands are supported:
|
||||||
- `popup:open`
|
- `popup:open`
|
||||||
- `popup:close`
|
- `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,
|
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`
|
> Type: `workspaces`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| 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. |
|
| `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. |
|
| `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. |
|
| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
|
@ -72,15 +72,15 @@ end:
|
||||||
|
|
||||||
```corn
|
```corn
|
||||||
{
|
{
|
||||||
end = [
|
end = [
|
||||||
{
|
{
|
||||||
type = "workspaces",
|
type = "workspaces",
|
||||||
name_map.1 = ""
|
name_map.1 = ""
|
||||||
name_map.2 = ""
|
name_map.2 = ""
|
||||||
name_map.3 = ""
|
name_map.3 = ""
|
||||||
all_monitors = false
|
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
|
/// Note this checks that icons exist in theme, or files exist on disk
|
||||||
/// but no other check is performed.
|
/// 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)?;
|
let location = Self::get_location(input, theme, size)?;
|
||||||
Ok(Self { location, size })
|
Ok(Self { location, size })
|
||||||
}
|
}
|
||||||
|
@ -45,11 +45,10 @@ impl<'a> ImageProvider<'a> {
|
||||||
|| input.starts_with("https://")
|
|| 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
|
let (input_type, input_name) = input
|
||||||
.split_once(':')
|
.split_once(':')
|
||||||
.map(|(t, n)| (Some(t), n))
|
.map_or((None, input), |(t, n)| (Some(t), n));
|
||||||
.unwrap_or((None, &input));
|
|
||||||
|
|
||||||
match input_type {
|
match input_type {
|
||||||
Some(input_type) if input_type == "icon" => Ok(ImageLocation::Icon {
|
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(),
|
input_name.chars().skip("steam_app_".len()).collect(),
|
||||||
)),
|
)),
|
||||||
None if theme
|
None if theme
|
||||||
.lookup_icon(&input, size, IconLookupFlags::empty())
|
.lookup_icon(input, size, IconLookupFlags::empty())
|
||||||
.is_some() =>
|
.is_some() =>
|
||||||
{
|
{
|
||||||
Ok(ImageLocation::Icon {
|
Ok(ImageLocation::Icon {
|
||||||
|
@ -78,10 +77,10 @@ impl<'a> ImageProvider<'a> {
|
||||||
None if PathBuf::from(input_name).exists() => {
|
None if PathBuf::from(input_name).exists() => {
|
||||||
Ok(ImageLocation::Local(PathBuf::from(input_name)))
|
Ok(ImageLocation::Local(PathBuf::from(input_name)))
|
||||||
}
|
}
|
||||||
None => match get_desktop_icon_name(input_name) {
|
None => get_desktop_icon_name(input_name).map_or_else(
|
||||||
Some(input) => Self::get_location(input, theme, size),
|
|| Err(Report::msg("Unknown image type")),
|
||||||
None => Err(Report::msg("Unknown image type")),
|
|input| Self::get_location(&input, theme, size),
|
||||||
},
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,8 +119,6 @@ impl<'a> ImageProvider<'a> {
|
||||||
Continue(false)
|
Continue(false)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
} else {
|
||||||
let pixbuf = match &self.location {
|
let pixbuf = match &self.location {
|
||||||
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme),
|
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme),
|
||||||
|
@ -131,8 +128,9 @@ impl<'a> ImageProvider<'a> {
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
image.set_pixbuf(Some(&pixbuf));
|
image.set_pixbuf(Some(&pixbuf));
|
||||||
Ok(())
|
};
|
||||||
}
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to get a `Pixbuf` from the GTK icon theme.
|
/// Attempts to get a `Pixbuf` from the GTK icon theme.
|
||||||
|
@ -142,10 +140,10 @@ impl<'a> ImageProvider<'a> {
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
match pixbuf {
|
pixbuf.map_or_else(
|
||||||
Some(pixbuf) => Ok(pixbuf),
|
|| Err(Report::msg("Icon theme does not contain icon '{name}'")),
|
||||||
None => Err(Report::msg("Icon theme does not contain icon '{name}'")),
|
Ok,
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to get a `Pixbuf` from a local file.
|
/// 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.
|
/// using the Steam game ID to look it up.
|
||||||
fn get_from_steam_id(&self, steam_id: &str) -> Result<Pixbuf> {
|
fn get_from_steam_id(&self, steam_id: &str) -> Result<Pixbuf> {
|
||||||
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
|
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
|
||||||
let path = match dirs::data_dir() {
|
let path = dirs::data_dir().map_or_else(
|
||||||
Some(dir) => Ok(dir.join(format!(
|
|| Err(Report::msg("Missing XDG data dir")),
|
||||||
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
|
|dir| {
|
||||||
))),
|
Ok(dir.join(format!(
|
||||||
None => Err(Report::msg("Missing XDG data dir")),
|
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
|
||||||
}?;
|
)))
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
self.get_from_file(&path)
|
self.get_from_file(&path)
|
||||||
}
|
}
|
|
@ -120,6 +120,8 @@ impl Module<Button> for ClockModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container.show_all();
|
||||||
|
|
||||||
Some(container)
|
Some(container)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,9 +103,9 @@ impl Widget {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(widgets) = self.widgets {
|
if let Some(widgets) = self.widgets {
|
||||||
widgets.into_iter().for_each(|widget| {
|
for widget in widgets {
|
||||||
widget.add_to(&container, tx.clone(), bar_orientation, icon_theme)
|
widget.add_to(&container, tx.clone(), bar_orientation, icon_theme);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
container
|
container
|
||||||
|
@ -185,7 +185,7 @@ impl Widget {
|
||||||
|
|
||||||
if let Some(src) = self.src {
|
if let Some(src) = self.src {
|
||||||
let size = self.size.unwrap_or(32);
|
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()))
|
.and_then(|image| image.load_into_image(gtk_image.clone()))
|
||||||
{
|
{
|
||||||
error!("{err:?}");
|
error!("{err:?}");
|
||||||
|
@ -292,16 +292,18 @@ impl Module<gtk::Box> for CustomModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(popup) = self.popup {
|
if let Some(popup) = self.popup {
|
||||||
popup.into_iter().for_each(|widget| {
|
for widget in popup {
|
||||||
widget.add_to(
|
widget.add_to(
|
||||||
&container,
|
&container,
|
||||||
tx.clone(),
|
tx.clone(),
|
||||||
Orientation::Horizontal,
|
Orientation::Horizontal,
|
||||||
info.icon_theme,
|
info.icon_theme,
|
||||||
)
|
);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container.show_all();
|
||||||
|
|
||||||
Some(container)
|
Some(container)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,7 +111,7 @@ impl Module<gtk::Box> for FocusedModule {
|
||||||
let icon_theme = icon_theme.clone();
|
let icon_theme = icon_theme.clone();
|
||||||
context.widget_rx.attach(None, move |(name, id)| {
|
context.widget_rx.attach(None, move |(name, id)| {
|
||||||
if self.show_icon {
|
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()))
|
.and_then(|image| image.load_into_image(icon.clone()))
|
||||||
{
|
{
|
||||||
error!("{err:?}");
|
error!("{err:?}");
|
||||||
|
|
|
@ -156,7 +156,7 @@ impl ItemButton {
|
||||||
|
|
||||||
if show_icons {
|
if show_icons {
|
||||||
let gtk_image = gtk::Image::new();
|
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 {
|
match image {
|
||||||
Ok(image) => {
|
Ok(image) => {
|
||||||
button.set_image(Some(>k_image));
|
button.set_image(Some(>k_image));
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use crate::clients::compositor::{Compositor, WorkspaceUpdate};
|
use crate::clients::compositor::{Compositor, WorkspaceUpdate};
|
||||||
use crate::config::CommonConfig;
|
use crate::config::CommonConfig;
|
||||||
|
use crate::image::new_icon_button;
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use crate::{send_async, try_send};
|
use crate::{send_async, try_send};
|
||||||
use color_eyre::{Report, Result};
|
use color_eyre::{Report, Result};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Button;
|
use gtk::{Button, IconTheme};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -49,12 +50,13 @@ fn create_button(
|
||||||
name: &str,
|
name: &str,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
name_map: &HashMap<String, String>,
|
name_map: &HashMap<String, String>,
|
||||||
|
icon_theme: &IconTheme,
|
||||||
tx: &Sender<String>,
|
tx: &Sender<String>,
|
||||||
) -> Button {
|
) -> Button {
|
||||||
let button = Button::builder()
|
let label = name_map.get(name).map_or(name, String::as_str);
|
||||||
.label(name_map.get(name).map_or(name, String::as_str))
|
|
||||||
.name(name)
|
let button = new_icon_button(label, icon_theme, 32);
|
||||||
.build();
|
button.set_widget_name(name);
|
||||||
|
|
||||||
let style_context = button.style_context();
|
let style_context = button.style_context();
|
||||||
style_context.add_class("item");
|
style_context.add_class("item");
|
||||||
|
@ -154,6 +156,7 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
{
|
{
|
||||||
let container = container.clone();
|
let container = container.clone();
|
||||||
let output_name = info.output_name.to_string();
|
let output_name = info.output_name.to_string();
|
||||||
|
let icon_theme = info.icon_theme.clone();
|
||||||
|
|
||||||
// keep track of whether init event has fired previously
|
// keep track of whether init event has fired previously
|
||||||
// since it fires for every workspace subscriber
|
// since it fires for every workspace subscriber
|
||||||
|
@ -170,6 +173,7 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
&workspace.name,
|
&workspace.name,
|
||||||
workspace.focused,
|
workspace.focused,
|
||||||
&name_map,
|
&name_map,
|
||||||
|
&icon_theme,
|
||||||
&context.controller_tx,
|
&context.controller_tx,
|
||||||
);
|
);
|
||||||
container.add(&item);
|
container.add(&item);
|
||||||
|
@ -204,6 +208,7 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
&name,
|
&name,
|
||||||
workspace.focused,
|
workspace.focused,
|
||||||
&name_map,
|
&name_map,
|
||||||
|
&icon_theme,
|
||||||
&context.controller_tx,
|
&context.controller_tx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -227,6 +232,7 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
&name,
|
&name,
|
||||||
workspace.focused,
|
workspace.focused,
|
||||||
&name_map,
|
&name_map,
|
||||||
|
&icon_theme,
|
||||||
&context.controller_tx,
|
&context.controller_tx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -133,7 +133,7 @@ impl Popup {
|
||||||
|
|
||||||
/// Shows the popup
|
/// Shows the popup
|
||||||
pub fn show(&self, geometry: ButtonGeometry) {
|
pub fn show(&self, geometry: ButtonGeometry) {
|
||||||
self.window.show_all();
|
self.window.show();
|
||||||
self.set_pos(geometry);
|
self.set_pos(geometry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue