) -> List<'a, Message, Theme, Renderer> {
48 | match self {
49 | List::Row(row) => List::Row(row.spacing(amount)),
50 | List::Column(col) => List::Column(col.spacing(amount)),
51 | }
52 | }
53 |
54 | pub fn padding(self, padding: P) -> List<'a, Message, Theme, Renderer>
55 | where
56 | P: Into,
57 | {
58 | match self {
59 | List::Row(row) => List::Row(row.padding(padding)),
60 | List::Column(col) => List::Column(col.padding(padding)),
61 | }
62 | }
63 | }
64 |
65 | pub fn list<'a, Message, Theme, Renderer>(
66 | anchor: &BarAnchor,
67 | children: impl IntoIterator- >,
68 | ) -> List<'a, Message, Theme, Renderer>
69 | where
70 | Renderer: iced::core::Renderer,
71 | {
72 | match anchor.vertical() {
73 | true => List::Column(column(children)),
74 | false => List::Row(row(children)),
75 | }
76 | }
77 |
78 | impl<'a, Message, Theme, Renderer> From
>
79 | for Element<'a, Message, Theme, Renderer>
80 | where
81 | Message: 'a,
82 | Theme: 'a,
83 | Renderer: iced::core::Renderer + 'a,
84 | {
85 | fn from(list: List<'a, Message, Theme, Renderer>) -> Self {
86 | match list {
87 | List::Row(row) => Self::new(row),
88 | List::Column(col) => Self::new(col),
89 | }
90 | }
91 | }
92 |
93 | macro_rules! list {
94 | ($anchor:expr) => (
95 | $crate::list::List::new($anchor)
96 | );
97 | ($anchor:expr, $($x:expr),+ $(,)?) => (
98 | $crate::list::List::with_children($anchor, [$(iced::core::Element::from($x)),+])
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/src/listeners/hyprland.rs:
--------------------------------------------------------------------------------
1 | use bar_rs_derive::Builder;
2 | use hyprland::{data::Client, event_listener::AsyncEventListener, shared::HyprDataActiveOptional};
3 | use iced::{futures::SinkExt, stream, Subscription};
4 |
5 | use crate::{
6 | config::ConfigEntry,
7 | modules::hyprland::{
8 | window::update_window,
9 | workspaces::{get_workspaces, HyprWorkspaceMod},
10 | },
11 | Message,
12 | };
13 |
14 | use super::Listener;
15 |
16 | #[derive(Debug, Builder)]
17 | pub struct HyprListener;
18 |
19 | impl Listener for HyprListener {
20 | fn config(&self) -> Vec {
21 | vec![]
22 | }
23 | fn subscription(&self) -> Subscription {
24 | Subscription::run(|| {
25 | stream::channel(1, |mut sender| async move {
26 | let workspaces = get_workspaces(None).await;
27 | sender
28 | .send(Message::update(move |reg| {
29 | let ws = reg.get_module_mut::();
30 | ws.active = workspaces.0;
31 | ws.open = workspaces.1;
32 | }))
33 | .await
34 | .unwrap_or_else(|err| {
35 | eprintln!("Trying to send workspaces failed with err: {err}");
36 | });
37 | if let Ok(window) = Client::get_active_async().await {
38 | update_window(&mut sender, window.map(|w| w.title)).await;
39 | }
40 |
41 | let mut listener = AsyncEventListener::new();
42 |
43 | let senderx = sender.clone();
44 | listener.add_active_window_changed_handler(move |data| {
45 | let mut sender = senderx.clone();
46 | Box::pin(async move {
47 | update_window(&mut sender, data.map(|window| window.title)).await;
48 | })
49 | });
50 |
51 | let senderx = sender.clone();
52 | listener.add_workspace_changed_handler(move |data| {
53 | let mut sender = senderx.clone();
54 | Box::pin(async move {
55 | let workspaces = get_workspaces(Some(data.id)).await;
56 | sender
57 | .send(Message::update(move |reg| {
58 | let ws = reg.get_module_mut::();
59 | ws.active = workspaces.0;
60 | ws.open = workspaces.1;
61 | }))
62 | .await
63 | .unwrap_or_else(|err| {
64 | eprintln!("Trying to send workspaces failed with err: {err}");
65 | });
66 | })
67 | });
68 |
69 | listener
70 | .start_listener_async()
71 | .await
72 | .expect("Failed to listen for hyprland events");
73 | })
74 | })
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/listeners/mod.rs:
--------------------------------------------------------------------------------
1 | use std::{any::Any, fmt::Debug};
2 |
3 | use downcast_rs::{impl_downcast, Downcast};
4 | use hyprland::HyprListener;
5 | use iced::Subscription;
6 | use niri::NiriListener;
7 | use reload::ReloadListener;
8 | use wayfire::WayfireListener;
9 |
10 | use crate::{config::ConfigEntry, registry::Registry, Message};
11 |
12 | pub mod hyprland;
13 | pub mod niri;
14 | mod reload;
15 | pub mod wayfire;
16 |
17 | pub trait Listener: Any + Debug + Send + Sync + Downcast {
18 | fn config(&self) -> Vec {
19 | vec![]
20 | }
21 | fn subscription(&self) -> Subscription;
22 | }
23 | impl_downcast!(Listener);
24 |
25 | pub fn register_listeners(registry: &mut Registry) {
26 | registry.register_listener::();
27 | registry.register_listener::();
28 | registry.register_listener::();
29 | registry.register_listener::();
30 | }
31 |
--------------------------------------------------------------------------------
/src/listeners/niri.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::HashMap, env, sync::Arc};
2 |
3 | use bar_rs_derive::Builder;
4 | use iced::{futures::SinkExt, stream, Subscription};
5 | use niri_ipc::{socket::SOCKET_PATH_ENV, Event, Request};
6 | use tokio::{
7 | io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
8 | net::UnixStream,
9 | sync::mpsc,
10 | };
11 |
12 | use crate::{
13 | config::ConfigEntry,
14 | modules::niri::{NiriWindowMod, NiriWorkspaceMod},
15 | registry::Registry,
16 | Message, UpdateFn,
17 | };
18 |
19 | use super::Listener;
20 |
21 | #[derive(Debug, Builder)]
22 | pub struct NiriListener;
23 |
24 | impl Listener for NiriListener {
25 | fn config(&self) -> Vec {
26 | vec![]
27 | }
28 | fn subscription(&self) -> Subscription {
29 | Subscription::run(|| {
30 | stream::channel(1, |mut sender| async move {
31 | let (sx, mut rx) = mpsc::channel(1);
32 | sender
33 | .send(Message::GetReceiver(sx, |reg| {
34 | reg.get_module::().sender.subscribe()
35 | }))
36 | .await
37 | .unwrap();
38 | let mut receiver = rx.recv().await.unwrap();
39 | drop(rx);
40 | let socket_path = env::var(SOCKET_PATH_ENV).expect("No niri socket was found!");
41 | let mut socket = UnixStream::connect(&socket_path).await.unwrap();
42 | let mut buf = serde_json::to_string(&Request::EventStream).unwrap();
43 | socket.write_all(buf.as_bytes()).await.unwrap();
44 | socket.shutdown().await.unwrap();
45 | let mut reader = BufReader::new(socket);
46 | reader
47 | .read_line(&mut buf)
48 | .await
49 | .map_err(|e| {
50 | eprintln!("Failed to build an event stream with niri: {e}");
51 | })
52 | .ok();
53 | buf.clear();
54 | loop {
55 | tokio::select! {
56 | Ok(_) = reader.read_line(&mut buf) => {
57 | let reply = serde_json::from_str::(&buf);
58 | type F = Box;
59 | let msg: Option = match reply {
60 | Ok(event) => match event {
61 | Event::WorkspacesChanged { workspaces } => Some(Box::new(move |reg| {
62 | let active_ws = workspaces
63 | .iter()
64 | .find_map(|ws| ws.is_focused.then_some(ws.id));
65 | let mut workspaces: HashMap> =
66 | workspaces.into_iter().fold(HashMap::new(), |mut acc, ws| {
67 | match acc
68 | .get_mut(ws.output.as_ref().unwrap_or(&String::new()))
69 | {
70 | Some(workspaces) => workspaces.push(ws),
71 | None => {
72 | acc.insert(
73 | ws.output.clone().unwrap_or_default(),
74 | vec![ws],
75 | );
76 | }
77 | }
78 | acc
79 | });
80 | for (_, workspaces) in workspaces.iter_mut() {
81 | workspaces.sort_by(|a, b| a.idx.cmp(&b.idx));
82 | }
83 | let ws_mod = reg.get_module_mut::();
84 | ws_mod.focused = active_ws.unwrap();
85 | ws_mod.workspaces = workspaces
86 | })),
87 | Event::WorkspaceActivated { id, focused } => match focused {
88 | true => Some(Box::new(move |reg| {
89 | reg.get_module_mut::().focused = id
90 | })),
91 | false => None,
92 | },
93 | Event::WindowsChanged { windows } => Some(Box::new(move |reg| {
94 | let window_mod = reg.get_module_mut::();
95 | window_mod.focused =
96 | windows.iter().find(|w| w.is_focused).map(|w| w.id);
97 | window_mod.windows = windows
98 | .into_iter()
99 | .map(|w| (w.id, w))
100 | .collect()
101 | })),
102 | Event::WindowFocusChanged { id } => Some(Box::new(move |reg| {
103 | reg.get_module_mut::().focused = id
104 | })),
105 | Event::WindowOpenedOrChanged { window } => Some(Box::new(move |reg| {
106 | let window_mod = reg.get_module_mut::();
107 | if window.is_focused {
108 | window_mod.focused = Some(window.id);
109 | }
110 | window_mod
111 | .windows
112 | .insert(window.id, window);
113 | })),
114 | Event::WindowClosed { id } => Some(Box::new(move |reg| {
115 | reg.get_module_mut::().windows.remove(&id);
116 | })),
117 | _ => None,
118 | },
119 | Err(err) => {
120 | eprintln!("Failed to decode Niri IPC msg as Event: {err}");
121 | None
122 | }
123 | };
124 | if let Some(msg) = msg {
125 | sender
126 | .send(Message::Update(Arc::new(UpdateFn(msg))))
127 | .await
128 | .unwrap();
129 | }
130 | buf.clear();
131 | }
132 | Ok(action) = receiver.recv() => {
133 | if let Some(id) = action.downcast_ref::() {
134 | let mut socket = UnixStream::connect(&socket_path).await.unwrap();
135 | let buf = serde_json::to_string(&Request::Action(niri_ipc::Action::FocusWorkspace { reference: niri_ipc::WorkspaceReferenceArg::Id(*id) })).unwrap();
136 | socket.write_all(buf.as_bytes()).await.unwrap();
137 | socket.shutdown().await.unwrap();
138 | }
139 | }
140 | }
141 | }
142 | })
143 | })
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/listeners/reload.rs:
--------------------------------------------------------------------------------
1 | use std::{env, path::PathBuf, time::Duration};
2 |
3 | use bar_rs_derive::Builder;
4 | use iced::{
5 | futures::{executor, SinkExt},
6 | stream, Subscription,
7 | };
8 | use notify::{
9 | event::{ModifyKind, RemoveKind},
10 | Config, Error, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
11 | };
12 | use tokio::time::sleep;
13 |
14 | use crate::{
15 | config::{get_config, ConfigEntry},
16 | Message,
17 | };
18 |
19 | use super::Listener;
20 |
21 | #[derive(Debug, Builder)]
22 | pub struct ReloadListener;
23 |
24 | impl Listener for ReloadListener {
25 | fn config(&self) -> Vec {
26 | vec![ConfigEntry::new("general", "hot_reloading", true)]
27 | }
28 |
29 | fn subscription(&self) -> Subscription {
30 | Subscription::run(|| {
31 | stream::channel(1, |mut sender| async move {
32 | let config_path = get_config(&mut sender).await.0;
33 | let config_pathx = config_path.clone();
34 |
35 | let mut watcher = RecommendedWatcher::new(
36 | move |result: Result| {
37 | let event = result.unwrap();
38 |
39 | if event.paths.contains(&config_pathx) && (matches!(event.kind, EventKind::Modify(ModifyKind::Data(_)))
40 | || matches!(event.kind, EventKind::Remove(RemoveKind::File)))
41 | {
42 | executor::block_on(async {
43 | sender.send(Message::ReloadConfig)
44 | .await
45 | .unwrap_or_else(|err| {
46 | eprintln!("Trying to request config reload failed with err: {err}");
47 | });
48 | });
49 | }
50 | },
51 | Config::default()
52 | ).unwrap();
53 |
54 | watcher
55 | .watch(
56 | config_path.parent().unwrap_or(&default_config_path()),
57 | RecursiveMode::Recursive,
58 | )
59 | .unwrap();
60 |
61 | loop {
62 | sleep(Duration::from_secs(1)).await;
63 | }
64 | })
65 | })
66 | }
67 | }
68 |
69 | fn default_config_path() -> PathBuf {
70 | format!(
71 | "{}/.config/bar-rs",
72 | env::var("HOME").expect("Env $HOME is not set?")
73 | )
74 | .into()
75 | }
76 |
--------------------------------------------------------------------------------
/src/listeners/wayfire.rs:
--------------------------------------------------------------------------------
1 | use std::{error::Error, time::Duration};
2 |
3 | use bar_rs_derive::Builder;
4 | use iced::{
5 | futures::{channel::mpsc::Sender, SinkExt},
6 | stream, Subscription,
7 | };
8 | use serde_json::Value;
9 | use tokio::time::sleep;
10 | use wayfire_rs::ipc::WayfireSocket;
11 |
12 | use crate::{
13 | modules::wayfire::{WayfireWindowMod, WayfireWorkspaceMod},
14 | Message,
15 | };
16 |
17 | use super::Listener;
18 |
19 | #[derive(Debug, Builder)]
20 | pub struct WayfireListener;
21 |
22 | async fn send_first_values(
23 | socket: &mut WayfireSocket,
24 | sender: &mut Sender,
25 | ) -> Result<(), Box> {
26 | let title = socket.get_focused_view().await.ok().map(|v| v.title);
27 | let workspace = socket.get_focused_output().await?.workspace;
28 | sender
29 | .send(Message::update(move |reg| {
30 | reg.get_module_mut::().title = title;
31 | reg.get_module_mut::().active = (workspace.x, workspace.y);
32 | }))
33 | .await?;
34 | Ok(())
35 | }
36 |
37 | impl Listener for WayfireListener {
38 | fn subscription(&self) -> iced::Subscription {
39 | Subscription::run(|| {
40 | stream::channel(1, |mut sender| async move {
41 | let Ok(mut socket) = WayfireSocket::connect().await else {
42 | eprintln!("Failed to connect to wayfire socket");
43 | return;
44 | };
45 |
46 | send_first_values(&mut socket, &mut sender)
47 | .await
48 | .unwrap_or_else(|e| {
49 | eprintln!("Failed to send initial wayfire module data: {e}")
50 | });
51 |
52 | socket
53 | .watch(Some(vec![
54 | "wset-workspace-changed".to_string(),
55 | "view-focused".to_string(),
56 | "view-title-changed".to_string(),
57 | "view-unmapped".to_string(),
58 | ]))
59 | .await
60 | .expect("Failed to watch wayfire socket (but we're connected already)!");
61 |
62 | let mut active_window = None;
63 |
64 | while let Ok(Value::Object(msg)) = socket.read_message().await {
65 | match msg.get("event") {
66 | Some(Value::String(val)) if val == "wset-workspace-changed" => {
67 | let Some(Value::Object(obj)) = msg.get("new-workspace") else {
68 | continue;
69 | };
70 |
71 | // serde_json::Value::Object => (i64, i64)
72 | if let Some((x, y)) = obj.get("x").and_then(|x| {
73 | x.as_i64().and_then(|x| {
74 | obj.get("y").and_then(|y| y.as_i64().map(|y| (x, y)))
75 | })
76 | }) {
77 | // With this wayfire will send an additional msg, see the None
78 | // match arm... No idea why tho
79 | sleep(Duration::from_millis(150)).await;
80 | let title = socket.get_focused_view().await.ok().map(|v| v.title);
81 | active_window = title.clone();
82 | sender
83 | .send(Message::update(move |reg| {
84 | reg.get_module_mut::().active = (x, y);
85 | reg.get_module_mut::().title = title
86 | }))
87 | .await
88 | .unwrap();
89 | }
90 | }
91 |
92 | Some(Value::String(val))
93 | if val == "view-focused" || val == "view-title-changed" =>
94 | {
95 | let Some(Value::String(title)) = msg
96 | .get("view")
97 | .and_then(|v| v.as_object())
98 | .and_then(|o| o.get("title").map(|t| t.to_owned()))
99 | else {
100 | continue;
101 | };
102 | match Some(&title) == active_window.as_ref() {
103 | true => continue,
104 | false => active_window = Some(title.clone()),
105 | }
106 | sender
107 | .send(Message::update(move |reg| {
108 | reg.get_module_mut::().title = Some(title)
109 | }))
110 | .await
111 | .unwrap();
112 | }
113 |
114 | // That sure seems useless, but we need the view-unmapped events that
115 | // somehow end up in the None match arm
116 | Some(Value::String(val)) if val == "view-unmapped" => {}
117 |
118 | None => {
119 | if let Some("ok") = msg.get("result").and_then(|r| r.as_str()) {
120 | let Some(title) = msg.get("info").map(|info| {
121 | if info.is_null() {
122 | return None;
123 | }
124 | info.as_object()
125 | .and_then(|obj| obj.get("title"))
126 | .and_then(|t| t.as_str())
127 | .map(|s| s.to_string())
128 | }) else {
129 | continue;
130 | };
131 | match title == active_window {
132 | true => continue,
133 | false => active_window = title.clone(),
134 | }
135 | sender
136 | .send(Message::update(move |reg| {
137 | reg.get_module_mut::().title = title
138 | }))
139 | .await
140 | .unwrap();
141 | };
142 | }
143 |
144 | _ => eprintln!("got unknown event from wayfire ipc: {msg:#?}"),
145 | }
146 | }
147 |
148 | eprintln!("Failed to read messages from the Wayfire socket!");
149 | })
150 | })
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/modules/cpu.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | collections::{BTreeMap, HashMap},
3 | fs::File,
4 | hash::Hash,
5 | io::{self, BufRead, BufReader},
6 | num,
7 | time::Duration,
8 | };
9 |
10 | use bar_rs_derive::Builder;
11 | use handlebars::Handlebars;
12 | use iced::widget::{button::Style, container, scrollable, Container, Text};
13 | use iced::{futures::SinkExt, stream, widget::text, Element, Subscription};
14 | use tokio::time::sleep;
15 |
16 | use crate::{
17 | button::button,
18 | config::{
19 | anchor::BarAnchor,
20 | module_config::{LocalModuleConfig, ModuleConfigOverride},
21 | popup_config::{PopupConfig, PopupConfigOverride},
22 | },
23 | fill::FillExt,
24 | helpers::UnEscapeString,
25 | impl_on_click, impl_wrapper, Message, NERD_FONT,
26 | };
27 |
28 | use super::Module;
29 |
30 | #[derive(Debug, Builder)]
31 | pub struct CpuMod {
32 | avg_usage: CpuStats,
33 | cores: BTreeMap>,
34 | cfg_override: ModuleConfigOverride,
35 | popup_cfg_override: PopupConfigOverride,
36 | icon: Option,
37 | }
38 |
39 | impl Default for CpuMod {
40 | fn default() -> Self {
41 | Self {
42 | avg_usage: Default::default(),
43 | cores: BTreeMap::new(),
44 | cfg_override: Default::default(),
45 | popup_cfg_override: PopupConfigOverride {
46 | width: Some(150),
47 | height: Some(350),
48 | ..Default::default()
49 | },
50 | icon: None,
51 | }
52 | }
53 | }
54 |
55 | impl Module for CpuMod {
56 | fn name(&self) -> String {
57 | "cpu".to_string()
58 | }
59 |
60 | fn view(
61 | &self,
62 | config: &LocalModuleConfig,
63 | popup_config: &PopupConfig,
64 | anchor: &BarAnchor,
65 | _handlebars: &Handlebars,
66 | ) -> Element {
67 | button(
68 | list![
69 | anchor,
70 | container(
71 | text!("{}", self.icon.as_ref().unwrap_or(&"".to_string()))
72 | .fill(anchor)
73 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size))
74 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color))
75 | .font(NERD_FONT)
76 | )
77 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin)),
78 | container(
79 | text!["{}%", self.avg_usage.all]
80 | .fill(anchor)
81 | .size(self.cfg_override.font_size.unwrap_or(config.font_size))
82 | .color(self.cfg_override.text_color.unwrap_or(config.text_color))
83 | )
84 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin)),
85 | ]
86 | .spacing(self.cfg_override.spacing.unwrap_or(config.spacing)),
87 | )
88 | .on_event_with(Message::popup::(
89 | self.popup_cfg_override.width.unwrap_or(popup_config.width),
90 | self.popup_cfg_override
91 | .height
92 | .unwrap_or(popup_config.height),
93 | anchor,
94 | ))
95 | .style(|_, _| Style::default())
96 | .into()
97 | }
98 |
99 | fn popup_view<'a>(
100 | &'a self,
101 | config: &'a PopupConfig,
102 | template: &Handlebars,
103 | ) -> Element<'a, Message> {
104 | let fmt_text = |text: Text<'a>| -> Container<'a, Message> {
105 | container(
106 | text.size(
107 | self.popup_cfg_override
108 | .font_size
109 | .unwrap_or(config.font_size),
110 | )
111 | .color(
112 | self.popup_cfg_override
113 | .text_color
114 | .unwrap_or(config.text_color),
115 | ),
116 | )
117 | .padding(
118 | self.popup_cfg_override
119 | .text_margin
120 | .unwrap_or(config.text_margin),
121 | )
122 | };
123 | let ctx = BTreeMap::from([
124 | ("total", self.avg_usage.all.to_string()),
125 | ("user", self.avg_usage.user.to_string()),
126 | ("system", self.avg_usage.system.to_string()),
127 | ("guest", self.avg_usage.guest.to_string()),
128 | (
129 | "cores",
130 | self.cores
131 | .iter()
132 | .map(|(ty, stats)| {
133 | let core = BTreeMap::from([
134 | ("index", ty.get_core_index().to_string()),
135 | ("total", stats.all.to_string()),
136 | ("user", stats.user.to_string()),
137 | ("system", stats.system.to_string()),
138 | ("guest", stats.guest.to_string()),
139 | ]);
140 | template
141 | .render("cpu_core", &core)
142 | .map_err(|e| eprintln!("Failed to render cpu core stats: {e}"))
143 | .unwrap_or_default()
144 | })
145 | .collect::>()
146 | .join("\n"),
147 | ),
148 | ]);
149 | let format = template
150 | .render("cpu", &ctx)
151 | .map_err(|e| eprintln!("Failed to render cpu stats: {e}"))
152 | .unwrap_or_default();
153 | container(scrollable(fmt_text(text(format))))
154 | .padding(self.popup_cfg_override.padding.unwrap_or(config.padding))
155 | .style(|_| container::Style {
156 | background: Some(
157 | self.popup_cfg_override
158 | .background
159 | .unwrap_or(config.background),
160 | ),
161 | border: self.popup_cfg_override.border.unwrap_or(config.border),
162 | ..Default::default()
163 | })
164 | .fill_maybe(
165 | self.popup_cfg_override
166 | .fill_content_to_size
167 | .unwrap_or(config.fill_content_to_size),
168 | )
169 | .into()
170 | }
171 |
172 | impl_wrapper!();
173 |
174 | fn read_config(
175 | &mut self,
176 | config: &HashMap>,
177 | popup_config: &HashMap>,
178 | templates: &mut Handlebars,
179 | ) {
180 | self.cfg_override = config.into();
181 | self.popup_cfg_override.update(popup_config);
182 | self.icon = config.get("icon").and_then(|v| v.clone());
183 | templates
184 | .register_template_string(
185 | "cpu",
186 | popup_config
187 | .get("format")
188 | .unescape()
189 | .unwrap_or("Total: {{total}}%\nUser: {{user}}%\nSystem: {{system}}%\nGuest: {{guest}}%\n{{cores}}".to_string()),
190 | )
191 | .unwrap_or_else(|e| eprintln!("Failed to parse battery popup format: {e}"));
192 | templates
193 | .register_template_string(
194 | "cpu_core",
195 | popup_config
196 | .get("format_core")
197 | .unescape()
198 | .unwrap_or("Core {{index}}: {{total}}%".to_string()),
199 | )
200 | .unwrap_or_else(|e| eprintln!("Failed to parse battery popup format: {e}"));
201 | }
202 |
203 | impl_on_click!();
204 |
205 | fn subscription(&self) -> Option> {
206 | Some(Subscription::run(|| {
207 | stream::channel(1, |mut sender| async move {
208 | let interval: u64 = 500;
209 | let gap: u64 = 2000;
210 | loop {
211 | let Ok(mut raw_stats1) = read_raw_stats()
212 | .map_err(|e| eprintln!("Failed to read cpu stats from /proc/stat: {e:?}"))
213 | else {
214 | return;
215 | };
216 | sleep(Duration::from_millis(interval)).await;
217 | let Ok(mut raw_stats2) = read_raw_stats() else {
218 | eprintln!("Failed to read cpu stats from /proc/stat");
219 | return;
220 | };
221 |
222 | let avg = (
223 | &raw_stats1.remove(&CpuType::All).unwrap(),
224 | &raw_stats2.remove(&CpuType::All).unwrap(),
225 | )
226 | .into();
227 |
228 | let cores = raw_stats1
229 | .into_iter()
230 | .filter_map(|(ty, stats1)| {
231 | raw_stats2
232 | .get(&ty)
233 | .map(|stats2| (ty, (&stats1, stats2).into()))
234 | })
235 | .collect();
236 |
237 | sender
238 | .send(Message::update(move |reg| {
239 | let m = reg.get_module_mut::();
240 | m.avg_usage = avg;
241 | m.cores = cores
242 | }))
243 | .await
244 | .unwrap_or_else(|err| {
245 | eprintln!("Trying to send cpu_usage failed with err: {err}");
246 | });
247 |
248 | sleep(Duration::from_millis(gap)).await;
249 | }
250 | })
251 | }))
252 | }
253 | }
254 |
255 | #[derive(Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
256 | enum CpuType {
257 | #[default]
258 | All,
259 | Core(u8),
260 | }
261 |
262 | impl CpuType {
263 | fn get_core_index(&self) -> u8 {
264 | match self {
265 | CpuType::All => 255,
266 | CpuType::Core(index) => *index,
267 | }
268 | }
269 | }
270 |
271 | impl From<&str> for CpuType {
272 | fn from(value: &str) -> Self {
273 | value
274 | .strip_prefix("cpu")
275 | .and_then(|v| v.parse().ok().map(Self::Core))
276 | .unwrap_or(Self::All)
277 | }
278 | }
279 |
280 | #[derive(Default, Debug)]
281 | struct CpuStats {
282 | all: T,
283 | user: T,
284 | system: T,
285 | guest: T,
286 | total: T,
287 | }
288 |
289 | impl TryFrom<&str> for CpuStats {
290 | type Error = ReadError;
291 | fn try_from(value: &str) -> Result {
292 | let values: Result, num::ParseIntError> =
293 | value.split_whitespace().map(|p| p.parse()).collect();
294 | // Documentation can be found at
295 | // https://docs.kernel.org/filesystems/proc.html#miscellaneous-kernel-statistics-in-proc-stat
296 | let [user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice] =
297 | values?[..]
298 | else {
299 | return Err(ReadError::ValueListInvalid);
300 | };
301 | let all = user + nice + system + irq + softirq;
302 | Ok(CpuStats {
303 | all,
304 | user: user + nice,
305 | system,
306 | guest: guest + guest_nice,
307 | total: all + idle + iowait + steal,
308 | })
309 | }
310 | }
311 |
312 | impl From<(&CpuStats, &CpuStats)> for CpuStats {
313 | fn from((stats1, stats2): (&CpuStats, &CpuStats)) -> Self {
314 | let delta_all = stats2.all - stats1.all;
315 | let delta_user = stats2.user - stats1.user;
316 | let delta_system = stats2.system - stats1.system;
317 | let delta_guest = stats2.guest - stats1.guest;
318 | let delta_total = stats2.total - stats1.total;
319 | if delta_total == 0 {
320 | return Self::default();
321 | }
322 | Self {
323 | all: ((delta_all as f32 / delta_total as f32) * 100.) as u8,
324 | user: ((delta_user as f32 / delta_total as f32) * 100.) as u8,
325 | system: ((delta_system as f32 / delta_total as f32) * 100.) as u8,
326 | guest: ((delta_guest as f32 / delta_total as f32) * 100.) as u8,
327 | total: 0,
328 | }
329 | }
330 | }
331 |
332 | fn read_raw_stats() -> Result>, ReadError> {
333 | let file = File::open("/proc/stat")?;
334 | let reader = BufReader::new(file);
335 | let lines = reader.lines().filter_map(|l| {
336 | l.ok().and_then(|line| {
337 | let (cpu, data) = line.split_once(' ')?;
338 | Some((cpu.into(), data.try_into().ok()?))
339 | })
340 | });
341 | Ok(lines.collect())
342 | }
343 |
344 | #[allow(dead_code)]
345 | #[derive(Debug)]
346 | enum ReadError {
347 | IoError(io::Error),
348 | ParseError(num::ParseIntError),
349 | ValueListInvalid,
350 | }
351 |
352 | impl From for ReadError {
353 | fn from(value: io::Error) -> Self {
354 | Self::IoError(value)
355 | }
356 | }
357 |
358 | impl From for ReadError {
359 | fn from(value: num::ParseIntError) -> Self {
360 | Self::ParseError(value)
361 | }
362 | }
363 |
--------------------------------------------------------------------------------
/src/modules/date.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use bar_rs_derive::Builder;
4 | use chrono::Local;
5 | use handlebars::Handlebars;
6 | use iced::widget::{container, text};
7 | use iced::Element;
8 |
9 | use crate::config::popup_config::PopupConfig;
10 | use crate::{
11 | config::{
12 | anchor::BarAnchor,
13 | module_config::{LocalModuleConfig, ModuleConfigOverride},
14 | },
15 | fill::FillExt,
16 | Message, NERD_FONT,
17 | };
18 | use crate::{impl_on_click, impl_wrapper};
19 |
20 | use super::Module;
21 |
22 | #[derive(Debug, Builder)]
23 | pub struct DateMod {
24 | cfg_override: ModuleConfigOverride,
25 | icon: String,
26 | fmt: String,
27 | }
28 |
29 | impl Default for DateMod {
30 | fn default() -> Self {
31 | Self {
32 | cfg_override: Default::default(),
33 | icon: "".to_string(),
34 | fmt: "%a, %d. %b".to_string(),
35 | }
36 | }
37 | }
38 |
39 | impl Module for DateMod {
40 | fn name(&self) -> String {
41 | "date".to_string()
42 | }
43 |
44 | fn view(
45 | &self,
46 | config: &LocalModuleConfig,
47 | _popup_config: &PopupConfig,
48 | anchor: &BarAnchor,
49 | _handlebars: &Handlebars,
50 | ) -> Element {
51 | let time = Local::now();
52 | list![
53 | anchor,
54 | container(
55 | text!("{}", self.icon)
56 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size))
57 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color))
58 | .font(NERD_FONT)
59 | .fill(anchor)
60 | )
61 | .fill(anchor)
62 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin)),
63 | container(
64 | text!("{}", time.format(&self.fmt))
65 | .size(self.cfg_override.font_size.unwrap_or(config.font_size))
66 | .color(self.cfg_override.text_color.unwrap_or(config.text_color))
67 | .fill(anchor)
68 | )
69 | .fill(anchor)
70 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin)),
71 | ]
72 | .spacing(self.cfg_override.spacing.unwrap_or(config.spacing))
73 | .into()
74 | }
75 |
76 | impl_wrapper!();
77 |
78 | fn read_config(
79 | &mut self,
80 | config: &HashMap>,
81 | _popup_config: &HashMap>,
82 | _templates: &mut Handlebars,
83 | ) {
84 | let default = Self::default();
85 | self.cfg_override = config.into();
86 | self.icon = config
87 | .get("icon")
88 | .and_then(|v| v.clone())
89 | .unwrap_or(default.icon);
90 | self.fmt = config
91 | .get("format")
92 | .and_then(|v| v.clone())
93 | .unwrap_or(default.fmt);
94 | }
95 |
96 | impl_on_click!();
97 | }
98 |
--------------------------------------------------------------------------------
/src/modules/disk_usage.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | collections::{BTreeMap, HashMap},
3 | ffi::CString,
4 | mem,
5 | };
6 |
7 | use bar_rs_derive::Builder;
8 | use handlebars::Handlebars;
9 | use iced::{
10 | widget::{button::Style, container, scrollable, text, Container, Text},
11 | Element,
12 | };
13 | use libc::{__errno_location, statvfs};
14 |
15 | use crate::{
16 | button::button,
17 | config::{
18 | anchor::BarAnchor,
19 | module_config::{LocalModuleConfig, ModuleConfigOverride},
20 | popup_config::{PopupConfig, PopupConfigOverride},
21 | },
22 | fill::FillExt,
23 | helpers::UnEscapeString,
24 | impl_on_click, impl_wrapper, Message, NERD_FONT,
25 | };
26 |
27 | use super::Module;
28 |
29 | #[derive(Debug, Builder, Default)]
30 | pub struct DiskUsageMod {
31 | icon: Option,
32 | cfg_override: ModuleConfigOverride,
33 | popup_cfg_override: PopupConfigOverride,
34 | path: CString,
35 | }
36 |
37 | #[derive(Debug, Default)]
38 | /// All values are represented in megabytes, except the `_perc` fields
39 | struct FileSystemStats {
40 | total: u64,
41 | free: u64,
42 | used: u64,
43 | /// Free space in percentage points
44 | free_perc: u8,
45 | /// Used space in percentage points
46 | used_perc: u8,
47 | }
48 |
49 | impl From for BTreeMap<&'static str, u64> {
50 | fn from(value: FileSystemStats) -> Self {
51 | BTreeMap::from([
52 | ("total", value.total),
53 | ("total_gb", value.total / 1000),
54 | ("used", value.used),
55 | ("used_gb", value.used / 1000),
56 | ("free", value.free),
57 | ("free_gb", value.free / 1000),
58 | ("used_perc", value.used_perc.into()),
59 | ("free_perc", value.free_perc.into()),
60 | ])
61 | }
62 | }
63 |
64 | impl From for FileSystemStats {
65 | fn from(value: statvfs) -> Self {
66 | let free_perc = (value.f_bavail as f32 / value.f_blocks as f32 * 100.) as u8;
67 | Self {
68 | total: value.f_blocks * value.f_frsize / 1_000_000,
69 | free: value.f_bavail * value.f_frsize / 1_000_000,
70 | used: (value.f_blocks - value.f_bavail) * value.f_frsize / 1_000_000,
71 | free_perc,
72 | used_perc: 100 - free_perc,
73 | }
74 | }
75 | }
76 |
77 | impl Module for DiskUsageMod {
78 | fn name(&self) -> String {
79 | "disk_usage".to_string()
80 | }
81 |
82 | fn view(
83 | &self,
84 | config: &LocalModuleConfig,
85 | popup_config: &PopupConfig,
86 | anchor: &BarAnchor,
87 | handlebars: &Handlebars,
88 | ) -> Element {
89 | let Ok(stats) = get_stats(&self.path) else {
90 | return "Error".into();
91 | };
92 | let ctx: BTreeMap<&'static str, u64> = stats.into();
93 | let format = handlebars
94 | .render("disk_usage", &ctx)
95 | .map_err(|e| eprintln!("Failed to render disk_usage stats: {e}"))
96 | .unwrap_or_default();
97 | button(
98 | list![
99 | anchor,
100 | container(
101 | text!("{}", self.icon.as_ref().unwrap_or(&"".to_string()))
102 | .fill(anchor)
103 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size))
104 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color))
105 | .font(NERD_FONT)
106 | )
107 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin)),
108 | container(
109 | text(format)
110 | .fill(anchor)
111 | .size(self.cfg_override.font_size.unwrap_or(config.font_size))
112 | .color(self.cfg_override.text_color.unwrap_or(config.text_color))
113 | )
114 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin)),
115 | ]
116 | .spacing(self.cfg_override.spacing.unwrap_or(config.spacing)),
117 | )
118 | .on_event_with(Message::popup::(
119 | self.popup_cfg_override.width.unwrap_or(popup_config.width),
120 | self.popup_cfg_override
121 | .height
122 | .unwrap_or(popup_config.height),
123 | anchor,
124 | ))
125 | .style(|_, _| Style::default())
126 | .into()
127 | }
128 |
129 | fn popup_view<'a>(
130 | &'a self,
131 | config: &'a PopupConfig,
132 | template: &Handlebars,
133 | ) -> Element<'a, Message> {
134 | let fmt_text = |text: Text<'a>| -> Container<'a, Message> {
135 | container(
136 | text.size(
137 | self.popup_cfg_override
138 | .font_size
139 | .unwrap_or(config.font_size),
140 | )
141 | .color(
142 | self.popup_cfg_override
143 | .text_color
144 | .unwrap_or(config.text_color),
145 | ),
146 | )
147 | .padding(
148 | self.popup_cfg_override
149 | .text_margin
150 | .unwrap_or(config.text_margin),
151 | )
152 | };
153 | let Ok(stats) = get_stats(&self.path) else {
154 | return "Error".into();
155 | };
156 | let ctx: BTreeMap<&'static str, u64> = stats.into();
157 | let format = template
158 | .render("disk_usage_popup", &ctx)
159 | .map_err(|e| eprintln!("Failed to render disk_usage stats: {e}"))
160 | .unwrap_or_default();
161 | container(scrollable(fmt_text(text(format))))
162 | .padding(self.popup_cfg_override.padding.unwrap_or(config.padding))
163 | .style(|_| container::Style {
164 | background: Some(
165 | self.popup_cfg_override
166 | .background
167 | .unwrap_or(config.background),
168 | ),
169 | border: self.popup_cfg_override.border.unwrap_or(config.border),
170 | ..Default::default()
171 | })
172 | .fill_maybe(
173 | self.popup_cfg_override
174 | .fill_content_to_size
175 | .unwrap_or(config.fill_content_to_size),
176 | )
177 | .into()
178 | }
179 |
180 | impl_wrapper!();
181 |
182 | fn read_config(
183 | &mut self,
184 | config: &HashMap>,
185 | popup_config: &HashMap>,
186 | templates: &mut Handlebars,
187 | ) {
188 | self.cfg_override = config.into();
189 | self.popup_cfg_override.update(popup_config);
190 | self.icon = config.get("icon").and_then(|v| v.clone());
191 | self.path = config
192 | .get("path")
193 | .and_then(|v| v.clone().and_then(|v| CString::new(v).ok()))
194 | .unwrap_or_else(|| CString::new("/").unwrap());
195 | templates
196 | .register_template_string(
197 | "disk_usage",
198 | config
199 | .get("format")
200 | .unescape()
201 | .unwrap_or("{{used_perc}}%".to_string()),
202 | )
203 | .unwrap_or_else(|e| eprintln!("Failed to parse battery popup format: {e}"));
204 | templates
205 | .register_template_string(
206 | "disk_usage_popup",
207 | popup_config
208 | .get("format")
209 | .unescape()
210 | .unwrap_or("Total: {{total_gb}} GB\nUsed: {{used_gb}} GB ({{used_perc}}%)\nFree: {{free_gb}} GB ({{free_perc}}%)".to_string()),
211 | )
212 | .unwrap_or_else(|e| eprintln!("Failed to parse battery popup format: {e}"));
213 | }
214 |
215 | impl_on_click!();
216 | }
217 |
218 | /// Get file system statistics using the statvfs system call, see
219 | /// https://man7.org/linux/man-pages/man3/statvfs.3.html
220 | fn get_stats(path: &CString) -> Result {
221 | let mut raw_stats: statvfs = unsafe { mem::zeroed() };
222 | if unsafe { libc::statvfs(path.as_ptr(), &mut raw_stats) } != 0 {
223 | eprintln!(
224 | "Got an error while executing the statvfs syscall: {}",
225 | unsafe { *__errno_location() }
226 | );
227 | return Err(());
228 | }
229 | Ok(raw_stats.into())
230 | }
231 |
--------------------------------------------------------------------------------
/src/modules/hyprland/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod window;
2 | pub mod workspaces;
3 |
--------------------------------------------------------------------------------
/src/modules/hyprland/window.rs:
--------------------------------------------------------------------------------
1 | use std::{any::TypeId, collections::HashMap};
2 |
3 | use bar_rs_derive::Builder;
4 | use handlebars::Handlebars;
5 | use iced::widget::{container, rich_text, span, text};
6 | use iced::{
7 | futures::{channel::mpsc::Sender, SinkExt},
8 | Element,
9 | };
10 |
11 | use crate::config::popup_config::PopupConfig;
12 | use crate::tooltip::ElementExt;
13 | use crate::{
14 | config::{
15 | anchor::BarAnchor,
16 | module_config::{LocalModuleConfig, ModuleConfigOverride},
17 | },
18 | fill::FillExt,
19 | listeners::hyprland::HyprListener,
20 | modules::{require_listener, Message, Module},
21 | };
22 | use crate::{impl_on_click, impl_wrapper};
23 |
24 | #[derive(Debug, Builder)]
25 | pub struct HyprWindowMod {
26 | title: Option,
27 | max_length: usize,
28 | cfg_override: ModuleConfigOverride,
29 | }
30 |
31 | impl Default for HyprWindowMod {
32 | fn default() -> Self {
33 | Self {
34 | title: None,
35 | max_length: 25,
36 | cfg_override: Default::default(),
37 | }
38 | }
39 | }
40 |
41 | impl HyprWindowMod {
42 | pub fn get_title(&self) -> Option {
43 | self.title
44 | .as_ref()
45 | .map(|title| match title.len() > self.max_length {
46 | true => format!(
47 | "{}...",
48 | title.chars().take(self.max_length - 3).collect::()
49 | ),
50 | false => title.to_string(),
51 | })
52 | }
53 | }
54 |
55 | impl Module for HyprWindowMod {
56 | fn name(&self) -> String {
57 | "hyprland.window".to_string()
58 | }
59 |
60 | fn active(&self) -> bool {
61 | self.title.is_some()
62 | }
63 |
64 | fn view(
65 | &self,
66 | config: &LocalModuleConfig,
67 | _popup_config: &PopupConfig,
68 | anchor: &BarAnchor,
69 | _handlebars: &Handlebars,
70 | ) -> Element {
71 | container(
72 | rich_text([span(self.get_title().unwrap_or_default())
73 | .size(self.cfg_override.font_size.unwrap_or(config.font_size))
74 | .color(self.cfg_override.text_color.unwrap_or(config.text_color))])
75 | .fill(anchor),
76 | )
77 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin))
78 | .tooltip_maybe(
79 | self.get_title()
80 | .and_then(|t| (t.len() > self.max_length).then_some(text(t).size(12))),
81 | )
82 | }
83 |
84 | impl_wrapper!();
85 |
86 | fn requires(&self) -> Vec {
87 | vec![require_listener::()]
88 | }
89 |
90 | fn read_config(
91 | &mut self,
92 | config: &HashMap>,
93 | _popup_config: &HashMap>,
94 | _templates: &mut Handlebars,
95 | ) {
96 | self.cfg_override = config.into();
97 | self.max_length = config
98 | .get("max_length")
99 | .and_then(|v| v.as_ref().and_then(|v| v.parse().ok()))
100 | .unwrap_or(Self::default().max_length);
101 | }
102 |
103 | impl_on_click!();
104 | }
105 |
106 | pub async fn update_window(sender: &mut Sender, title: Option) {
107 | sender
108 | .send(Message::update(move |reg| {
109 | reg.get_module_mut::().title = title
110 | }))
111 | .await
112 | .unwrap_or_else(|err| {
113 | eprintln!("Trying to send workspaces failed with err: {err}");
114 | });
115 | }
116 |
--------------------------------------------------------------------------------
/src/modules/hyprland/workspaces.rs:
--------------------------------------------------------------------------------
1 | use std::{any::TypeId, collections::HashMap, time::Duration};
2 |
3 | use bar_rs_derive::Builder;
4 | use handlebars::Handlebars;
5 | use hyprland::{
6 | data::{Workspace, Workspaces},
7 | shared::{HyprData, HyprDataActive, HyprDataVec},
8 | };
9 | use iced::{
10 | widget::{container, rich_text, span},
11 | Background, Border, Color, Element, Padding,
12 | };
13 | use tokio::time::sleep;
14 |
15 | use crate::{
16 | config::{
17 | anchor::BarAnchor,
18 | module_config::{LocalModuleConfig, ModuleConfigOverride},
19 | parse::StringExt,
20 | popup_config::PopupConfig,
21 | },
22 | fill::FillExt,
23 | impl_on_click, impl_wrapper,
24 | list::list,
25 | listeners::hyprland::HyprListener,
26 | modules::{require_listener, Module},
27 | Message, NERD_FONT,
28 | };
29 |
30 | #[derive(Debug, Builder)]
31 | pub struct HyprWorkspaceMod {
32 | pub active: usize,
33 | // (Name, Fullscreen state)
34 | pub open: Vec<(String, bool)>,
35 | cfg_override: ModuleConfigOverride,
36 | icon_padding: Padding,
37 | icon_background: Option,
38 | icon_border: Border,
39 | active_padding: Option,
40 | active_size: f32,
41 | active_color: Color,
42 | active_background: Option,
43 | active_icon_border: Border,
44 | }
45 |
46 | impl Default for HyprWorkspaceMod {
47 | fn default() -> Self {
48 | Self {
49 | active: 0,
50 | open: vec![],
51 | cfg_override: ModuleConfigOverride::default(),
52 | icon_padding: Padding::default(),
53 | icon_background: None,
54 | icon_border: Border::default(),
55 | active_padding: None,
56 | active_size: 20.,
57 | active_color: Color::WHITE,
58 | active_background: None,
59 | active_icon_border: Border::default().rounded(8),
60 | }
61 | }
62 | }
63 |
64 | impl Module for HyprWorkspaceMod {
65 | fn name(&self) -> String {
66 | "hyprland.workspaces".to_string()
67 | }
68 |
69 | fn view(
70 | &self,
71 | config: &LocalModuleConfig,
72 | _popup_config: &PopupConfig,
73 | anchor: &BarAnchor,
74 | _handlebars: &Handlebars,
75 | ) -> Element {
76 | list(
77 | anchor,
78 | self.open.iter().enumerate().map(|(id, (ws, _))| {
79 | let mut span = span(ws)
80 | .padding(self.icon_padding)
81 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size))
82 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color))
83 | .background_maybe(self.icon_background)
84 | .border(self.icon_border)
85 | .font(NERD_FONT);
86 | if id == self.active {
87 | span = span
88 | .padding(self.active_padding.unwrap_or(self.icon_padding))
89 | .size(self.active_size)
90 | .color(self.active_color)
91 | .background_maybe(self.active_background)
92 | .border(self.active_icon_border);
93 | }
94 | container(rich_text![span].fill(anchor))
95 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin))
96 | .into()
97 | }),
98 | )
99 | .padding(self.cfg_override.padding.unwrap_or(config.padding))
100 | .spacing(self.cfg_override.spacing.unwrap_or(config.spacing))
101 | .into()
102 | }
103 |
104 | impl_wrapper!();
105 |
106 | fn requires(&self) -> Vec {
107 | vec![require_listener::()]
108 | }
109 |
110 | fn read_config(
111 | &mut self,
112 | config: &HashMap>,
113 | _popup_config: &HashMap>,
114 | _templates: &mut Handlebars,
115 | ) {
116 | let default = Self::default();
117 | self.cfg_override = config.into();
118 | self.icon_padding = config
119 | .get("icon_padding")
120 | .and_then(|v| v.into_insets().map(|i| i.into()))
121 | .unwrap_or(default.icon_padding);
122 | self.icon_background = config
123 | .get("icon_background")
124 | .map(|v| v.into_background())
125 | .unwrap_or(default.icon_background);
126 | self.icon_border = {
127 | let color = config.get("icon_border_color").and_then(|s| s.into_color());
128 | let width = config.get("icon_border_width").and_then(|s| s.into_float());
129 | let radius = config
130 | .get("icon_border_radius")
131 | .and_then(|s| s.into_insets().map(|i| i.into()));
132 | if color.is_some() || width.is_some() || radius.is_some() {
133 | Border {
134 | color: color.unwrap_or_default(),
135 | width: width.unwrap_or(1.),
136 | radius: radius.unwrap_or(8_f32.into()),
137 | }
138 | } else {
139 | default.active_icon_border
140 | }
141 | };
142 | self.active_padding = config
143 | .get("active_padding")
144 | .map(|v| v.into_insets().map(|i| i.into()))
145 | .unwrap_or(default.active_padding);
146 | self.active_size = config
147 | .get("active_size")
148 | .and_then(|v| v.into_float())
149 | .unwrap_or(default.active_size);
150 | self.active_color = config
151 | .get("active_color")
152 | .and_then(|v| v.into_color())
153 | .unwrap_or(default.active_color);
154 | self.active_background = config
155 | .get("active_background")
156 | .map(|v| v.into_background())
157 | .unwrap_or(default.active_background);
158 | self.active_icon_border = {
159 | let color = config
160 | .get("active_border_color")
161 | .and_then(|s| s.into_color());
162 | let width = config
163 | .get("active_border_width")
164 | .and_then(|s| s.into_float());
165 | let radius = config
166 | .get("active_border_radius")
167 | .and_then(|s| s.into_insets().map(|i| i.into()));
168 | if color.is_some() || width.is_some() || radius.is_some() {
169 | Border {
170 | color: color.unwrap_or_default(),
171 | width: width.unwrap_or(1.),
172 | radius: radius.unwrap_or(8_f32.into()),
173 | }
174 | } else {
175 | default.active_icon_border
176 | }
177 | };
178 | }
179 |
180 | impl_on_click!();
181 | }
182 |
183 | pub async fn get_workspaces(active: Option) -> (usize, Vec<(String, bool)>) {
184 | // Sleep a bit, to reduce the probability that a nonexisting ws is still reported active
185 | sleep(Duration::from_millis(10)).await;
186 | let Ok(workspaces) = Workspaces::get_async().await else {
187 | eprintln!("[hyprland.workspaces] Failed to get Workspaces!");
188 | return (0, vec![]);
189 | };
190 | let mut open = workspaces.to_vec();
191 | open.sort_by(|a, b| a.id.cmp(&b.id));
192 | (
193 | open.iter()
194 | .position(|ws| {
195 | ws.id
196 | == active
197 | .unwrap_or_else(|| Workspace::get_active().map(|ws| ws.id).unwrap_or(0))
198 | })
199 | .unwrap_or(0),
200 | open.into_iter()
201 | .map(|ws| (ws.name, ws.fullscreen))
202 | .collect(),
203 | )
204 | }
205 |
--------------------------------------------------------------------------------
/src/modules/memory.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::HashMap, process::Command};
2 |
3 | use bar_rs_derive::Builder;
4 | use handlebars::Handlebars;
5 | use iced::widget::container;
6 | use iced::{widget::text, Element};
7 |
8 | use crate::config::popup_config::PopupConfig;
9 | use crate::{
10 | config::{
11 | anchor::BarAnchor,
12 | module_config::{LocalModuleConfig, ModuleConfigOverride},
13 | },
14 | fill::FillExt,
15 | Message, NERD_FONT,
16 | };
17 | use crate::{impl_on_click, impl_wrapper};
18 |
19 | use super::Module;
20 |
21 | #[derive(Debug, Default, Builder)]
22 | pub struct MemoryMod {
23 | cfg_override: ModuleConfigOverride,
24 | icon: Option,
25 | }
26 |
27 | impl Module for MemoryMod {
28 | fn name(&self) -> String {
29 | "memory".to_string()
30 | }
31 |
32 | fn view(
33 | &self,
34 | config: &LocalModuleConfig,
35 | _popup_config: &PopupConfig,
36 | anchor: &BarAnchor,
37 | _handlebars: &Handlebars,
38 | ) -> Element {
39 | let usage = Command::new("sh")
40 | .arg("-c")
41 | .arg("free | grep Mem | awk '{printf \"%.0f\", $3/$2 * 100.0}'")
42 | .output()
43 | .map(|out| String::from_utf8_lossy(&out.stdout).to_string())
44 | .unwrap_or_else(|e| {
45 | eprintln!("Failed to get memory usage. err: {e}");
46 | "0".to_string()
47 | })
48 | .parse()
49 | .unwrap_or_else(|e| {
50 | eprintln!("Failed to parse memory usage (output from free), e: {e}");
51 | 999
52 | });
53 |
54 | list![
55 | anchor,
56 | container(
57 | text!("{}", self.icon.as_ref().unwrap_or(&"".to_string()))
58 | .fill(anchor)
59 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size))
60 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color))
61 | .font(NERD_FONT)
62 | )
63 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin)),
64 | container(
65 | text!["{}%", usage]
66 | .fill(anchor)
67 | .size(self.cfg_override.font_size.unwrap_or(config.font_size))
68 | .color(self.cfg_override.text_color.unwrap_or(config.text_color))
69 | )
70 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin)),
71 | ]
72 | .spacing(self.cfg_override.spacing.unwrap_or(config.spacing))
73 | .into()
74 | }
75 |
76 | impl_wrapper!();
77 |
78 | fn read_config(
79 | &mut self,
80 | config: &HashMap>,
81 | _popup_config: &HashMap>,
82 | _templates: &mut Handlebars,
83 | ) {
84 | self.cfg_override = config.into();
85 | self.icon = config.get("icon").and_then(|v| v.clone());
86 | }
87 |
88 | impl_on_click!();
89 | }
90 |
--------------------------------------------------------------------------------
/src/modules/mod.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | any::{Any, TypeId},
3 | collections::HashMap,
4 | fmt::Debug,
5 | };
6 |
7 | use battery::BatteryMod;
8 | use cpu::CpuMod;
9 | use date::DateMod;
10 | use disk_usage::DiskUsageMod;
11 | use downcast_rs::{impl_downcast, Downcast};
12 | use handlebars::Handlebars;
13 | use hyprland::{window::HyprWindowMod, workspaces::HyprWorkspaceMod};
14 | use iced::{
15 | theme::Palette,
16 | widget::{container, Container},
17 | Alignment, Color, Event, Theme,
18 | };
19 | use iced::{widget::container::Style, Element, Subscription};
20 | use media::MediaMod;
21 | use memory::MemoryMod;
22 | use niri::{NiriWindowMod, NiriWorkspaceMod};
23 | use time::TimeMod;
24 | use volume::VolumeMod;
25 | use wayfire::{WayfireWindowMod, WayfireWorkspaceMod};
26 |
27 | use crate::{
28 | config::{anchor::BarAnchor, module_config::LocalModuleConfig, popup_config::PopupConfig},
29 | fill::FillExt,
30 | listeners::Listener,
31 | registry::Registry,
32 | Message,
33 | };
34 |
35 | pub mod battery;
36 | pub mod cpu;
37 | pub mod date;
38 | pub mod disk_usage;
39 | pub mod hyprland;
40 | pub mod media;
41 | pub mod memory;
42 | pub mod niri;
43 | pub mod sys_tray;
44 | pub mod time;
45 | pub mod volume;
46 | pub mod wayfire;
47 |
48 | pub trait Module: Any + Debug + Send + Sync + Downcast {
49 | /// The name used to enable the Module in the config.
50 | fn name(&self) -> String;
51 | /// Whether the module is currently active and should be shown.
52 | fn active(&self) -> bool {
53 | true
54 | }
55 | /// What the module actually shows.
56 | /// See [widgets-and-elements](https://docs.iced.rs/iced/#widgets-and-elements).
57 | fn view(
58 | &self,
59 | config: &LocalModuleConfig,
60 | popup_config: &PopupConfig,
61 | anchor: &BarAnchor,
62 | template: &Handlebars,
63 | ) -> Element;
64 | /// The wrapper around this module, which defines things like background color or border for
65 | /// this module.
66 | fn wrapper<'a>(
67 | &'a self,
68 | config: &'a LocalModuleConfig,
69 | content: Element<'a, Message>,
70 | anchor: &BarAnchor,
71 | ) -> Element<'a, Message> {
72 | container(
73 | container(content)
74 | .fill(anchor)
75 | .padding(config.padding)
76 | .style(|_| Style {
77 | background: config.background,
78 | border: config.border,
79 | ..Default::default()
80 | }),
81 | )
82 | .fill(anchor)
83 | .padding(config.margin)
84 | .into()
85 | }
86 | /// The module may optionally have a subscription listening for external events.
87 | /// See [passive-subscriptions](https://docs.iced.rs/iced/#passive-subscriptions).
88 | fn subscription(&self) -> Option> {
89 | None
90 | }
91 | /// Modules may require shared subscriptions. Add `require_listener::()`
92 | /// for every [Listener] this module requires.
93 | fn requires(&self) -> Vec {
94 | vec![]
95 | }
96 | #[allow(unused_variables)]
97 | /// Read configuration options from the config section of this module
98 | fn read_config(
99 | &mut self,
100 | config: &HashMap>,
101 | popup_config: &HashMap>,
102 | templates: &mut Handlebars,
103 | ) {
104 | }
105 | #[allow(unused_variables)]
106 | /// The action to perform on a on_click event
107 | fn on_click<'a>(
108 | &'a self,
109 | event: iced::Event,
110 | config: &'a LocalModuleConfig,
111 | ) -> Option<&'a dyn Action> {
112 | None
113 | }
114 | #[allow(unused_variables, dead_code)]
115 | /// Handle an action (likely produced by a user interaction).
116 | fn handle_action(&mut self, action: &dyn Action) {}
117 | #[allow(unused_variables)]
118 | /// The view of a popup
119 | fn popup_view<'a>(
120 | &'a self,
121 | config: &'a PopupConfig,
122 | template: &Handlebars,
123 | ) -> Element<'a, Message> {
124 | "Missing implementation".into()
125 | }
126 | /// The wrapper around a popup
127 | fn popup_wrapper<'a>(
128 | &'a self,
129 | config: &'a PopupConfig,
130 | anchor: &BarAnchor,
131 | template: &Handlebars,
132 | ) -> Element<'a, Message> {
133 | let align = |elem: Container<'a, Message>| -> Container<'a, Message> {
134 | match anchor {
135 | BarAnchor::Top => elem.align_y(Alignment::Start),
136 | BarAnchor::Bottom => elem.align_y(Alignment::End),
137 | BarAnchor::Left => elem.align_x(Alignment::Start),
138 | BarAnchor::Right => elem.align_x(Alignment::End),
139 | }
140 | };
141 | align(container(self.popup_view(config, template)).fill(anchor)).into()
142 | }
143 | /// The theme of a popup
144 | fn popup_theme(&self) -> Theme {
145 | Theme::custom(
146 | "Default popup theme".to_string(),
147 | Palette {
148 | background: Color::TRANSPARENT,
149 | text: Color::WHITE,
150 | primary: Color::WHITE,
151 | success: Color::WHITE,
152 | danger: Color::WHITE,
153 | },
154 | )
155 | }
156 | }
157 | impl_downcast!(Module);
158 |
159 | pub trait Action: Any + Debug + Send + Sync + Downcast {
160 | fn as_message(&self) -> Message;
161 | }
162 | impl_downcast!(Action);
163 |
164 | impl From<&String> for Box {
165 | fn from(value: &String) -> Box {
166 | Box::new(CommandAction(value.clone()))
167 | }
168 | }
169 |
170 | #[derive(Debug)]
171 | pub struct CommandAction(String);
172 |
173 | impl Action for CommandAction {
174 | fn as_message(&self) -> Message {
175 | Message::command_sh(&self.0)
176 | }
177 | }
178 |
179 | #[derive(Debug, Default)]
180 | pub struct OnClickAction {
181 | pub left: Option>,
182 | pub center: Option>,
183 | pub right: Option>,
184 | }
185 |
186 | impl OnClickAction {
187 | pub fn event(&self, event: Event) -> Option<&dyn Action> {
188 | match event {
189 | Event::Mouse(iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left)) => {
190 | self.left.as_deref()
191 | }
192 | Event::Mouse(iced::mouse::Event::ButtonReleased(iced::mouse::Button::Middle)) => {
193 | self.center.as_deref()
194 | }
195 | Event::Mouse(iced::mouse::Event::ButtonReleased(iced::mouse::Button::Right)) => {
196 | self.right.as_deref()
197 | }
198 | _ => None,
199 | }
200 | }
201 | }
202 |
203 | pub fn require_listener() -> TypeId
204 | where
205 | T: Listener,
206 | {
207 | TypeId::of::()
208 | }
209 |
210 | pub fn register_modules(registry: &mut Registry) {
211 | registry.register_module::();
212 | registry.register_module::();
213 | registry.register_module::();
214 | registry.register_module::();
215 | registry.register_module::();
216 | registry.register_module::();
217 | registry.register_module::();
218 | registry.register_module::();
219 | registry.register_module::();
220 | registry.register_module::();
221 | registry.register_module::();
222 | registry.register_module::();
223 | registry.register_module::();
224 | registry.register_module::();
225 | }
226 |
227 | #[macro_export]
228 | macro_rules! impl_wrapper {
229 | () => {
230 | fn wrapper<'a>(
231 | &'a self,
232 | config: &'a LocalModuleConfig,
233 | content: Element<'a, Message>,
234 | anchor: &BarAnchor,
235 | ) -> Element<'a, Message> {
236 | iced::widget::container(
237 | $crate::button::button(content)
238 | .fill(anchor)
239 | .padding(self.cfg_override.padding.unwrap_or(config.padding))
240 | .on_event_try(|evt, _, _, _, _| {
241 | self.on_click(evt, config).map(|evt| evt.as_message())
242 | })
243 | .style(|_, _| iced::widget::button::Style {
244 | background: self.cfg_override.background.unwrap_or(config.background),
245 | border: self.cfg_override.border.unwrap_or(config.border),
246 | ..Default::default()
247 | }),
248 | )
249 | .fill(anchor)
250 | .padding(self.cfg_override.margin.unwrap_or(config.margin))
251 | .into()
252 | }
253 | };
254 | }
255 |
256 | #[macro_export]
257 | macro_rules! impl_on_click {
258 | () => {
259 | fn on_click<'a>(
260 | &'a self,
261 | event: iced::Event,
262 | config: &'a LocalModuleConfig,
263 | ) -> Option<&'a dyn $crate::modules::Action> {
264 | self.cfg_override
265 | .action
266 | .as_ref()
267 | .unwrap_or(&config.action)
268 | .event(event)
269 | }
270 | };
271 | }
272 |
--------------------------------------------------------------------------------
/src/modules/niri/mod.rs:
--------------------------------------------------------------------------------
1 | mod window;
2 | mod workspaces;
3 |
4 | pub use window::NiriWindowMod;
5 | pub use workspaces::NiriWorkspaceMod;
6 |
--------------------------------------------------------------------------------
/src/modules/niri/window.rs:
--------------------------------------------------------------------------------
1 | use std::collections::BTreeMap;
2 | use std::{any::TypeId, collections::HashMap};
3 |
4 | use bar_rs_derive::Builder;
5 | use handlebars::Handlebars;
6 | use iced::widget::button::Style;
7 | use iced::widget::{container, scrollable, text};
8 | use iced::Element;
9 | use niri_ipc::Window;
10 |
11 | use crate::button::button;
12 | use crate::config::popup_config::{PopupConfig, PopupConfigOverride};
13 | use crate::helpers::UnEscapeString;
14 | use crate::{
15 | config::{
16 | anchor::BarAnchor,
17 | module_config::{LocalModuleConfig, ModuleConfigOverride},
18 | parse::StringExt,
19 | },
20 | fill::FillExt,
21 | listeners::niri::NiriListener,
22 | modules::{require_listener, Module},
23 | Message,
24 | };
25 | use crate::{impl_on_click, impl_wrapper};
26 |
27 | #[derive(Debug, Builder)]
28 | pub struct NiriWindowMod {
29 | // (title, app_id)
30 | pub windows: HashMap,
31 | pub focused: Option,
32 | max_length: usize,
33 | show_app_id: bool,
34 | cfg_override: ModuleConfigOverride,
35 | popup_cfg_override: PopupConfigOverride,
36 | }
37 |
38 | impl Default for NiriWindowMod {
39 | fn default() -> Self {
40 | Self {
41 | windows: HashMap::new(),
42 | focused: None,
43 | max_length: 25,
44 | show_app_id: false,
45 | cfg_override: Default::default(),
46 | popup_cfg_override: PopupConfigOverride {
47 | width: Some(400),
48 | height: Some(250),
49 | ..Default::default()
50 | },
51 | }
52 | }
53 | }
54 |
55 | impl NiriWindowMod {
56 | fn get_title(&self) -> Option<&String> {
57 | self.focused.and_then(|id| {
58 | self.windows.get(&id).and_then(|w| match self.show_app_id {
59 | true => w.app_id.as_ref(),
60 | false => w.title.as_ref(),
61 | })
62 | })
63 | }
64 |
65 | fn trimmed_title(&self) -> String {
66 | self.get_title()
67 | .map(|title| match title.len() > self.max_length {
68 | true => format!(
69 | "{}...",
70 | &title.chars().take(self.max_length - 3).collect::()
71 | ),
72 | false => title.to_string(),
73 | })
74 | .unwrap_or_default()
75 | }
76 | }
77 |
78 | impl Module for NiriWindowMod {
79 | fn name(&self) -> String {
80 | "niri.window".to_string()
81 | }
82 |
83 | fn active(&self) -> bool {
84 | self.focused.is_some()
85 | }
86 |
87 | fn view(
88 | &self,
89 | config: &LocalModuleConfig,
90 | popup_config: &PopupConfig,
91 | anchor: &BarAnchor,
92 | _handlebars: &Handlebars,
93 | ) -> Element {
94 | button(
95 | text(self.trimmed_title())
96 | .size(self.cfg_override.font_size.unwrap_or(config.font_size))
97 | .color(self.cfg_override.text_color.unwrap_or(config.text_color))
98 | .fill(anchor),
99 | )
100 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin))
101 | .on_event_with(Message::popup::(
102 | self.popup_cfg_override.width.unwrap_or(popup_config.width),
103 | self.popup_cfg_override
104 | .height
105 | .unwrap_or(popup_config.height),
106 | anchor,
107 | ))
108 | .style(|_, _| Style::default())
109 | .into()
110 | }
111 |
112 | fn popup_view<'a>(
113 | &'a self,
114 | config: &'a PopupConfig,
115 | template: &Handlebars,
116 | ) -> Element<'a, Message> {
117 | container(scrollable(
118 | container(
119 | if let Some(window) = self.focused.and_then(|id| self.windows.get(&id)) {
120 | let unset = String::from("Unset");
121 | let window_id = window.id.to_string();
122 | let workspace_id = window.workspace_id.unwrap_or_default().to_string();
123 | let ctx = BTreeMap::from([
124 | ("title", window.title.as_ref().unwrap_or(&unset)),
125 | ("app_id", window.app_id.as_ref().unwrap_or(&unset)),
126 | ("window_id", &window_id),
127 | ("workspace_id", &workspace_id),
128 | ]);
129 | text(template.render("niri.window", &ctx).unwrap_or_default())
130 | } else {
131 | "No window focused".into()
132 | }
133 | .color(
134 | self.popup_cfg_override
135 | .text_color
136 | .unwrap_or(config.text_color),
137 | )
138 | .size(
139 | self.popup_cfg_override
140 | .font_size
141 | .unwrap_or(config.font_size),
142 | ),
143 | )
144 | .padding(
145 | self.popup_cfg_override
146 | .text_margin
147 | .unwrap_or(config.text_margin),
148 | ),
149 | ))
150 | .padding(self.popup_cfg_override.padding.unwrap_or(config.padding))
151 | .style(|_| container::Style {
152 | background: Some(
153 | self.popup_cfg_override
154 | .background
155 | .unwrap_or(config.background),
156 | ),
157 | border: self.popup_cfg_override.border.unwrap_or(config.border),
158 | ..Default::default()
159 | })
160 | .fill_maybe(
161 | self.popup_cfg_override
162 | .fill_content_to_size
163 | .unwrap_or(config.fill_content_to_size),
164 | )
165 | .into()
166 | }
167 |
168 | impl_wrapper!();
169 |
170 | fn requires(&self) -> Vec {
171 | vec![require_listener::()]
172 | }
173 |
174 | fn read_config(
175 | &mut self,
176 | config: &HashMap>,
177 | popup_config: &HashMap>,
178 | templates: &mut Handlebars,
179 | ) {
180 | let default = Self::default();
181 | self.cfg_override = config.into();
182 | self.popup_cfg_override.update(popup_config);
183 | self.max_length = config
184 | .get("max_length")
185 | .and_then(|v| v.as_ref().and_then(|v| v.parse().ok()))
186 | .unwrap_or(default.max_length);
187 | self.show_app_id = config
188 | .get("show_app_id")
189 | .and_then(|v| v.into_bool())
190 | .unwrap_or(default.show_app_id);
191 | templates
192 | .register_template_string(
193 | "niri.window",
194 | popup_config
195 | .get("format")
196 | .unescape()
197 | .unwrap_or("Title: {{title}}\nApplication ID: {{app_id}}\nWindow ID: {{window_id}}\nWorkspace ID: {{workspace_id}}".to_string()),
198 | )
199 | .unwrap_or_else(|e| eprintln!("Failed to parse battery popup format: {e}"));
200 | }
201 |
202 | impl_on_click!();
203 | }
204 |
--------------------------------------------------------------------------------
/src/modules/niri/workspaces.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | any::{Any, TypeId},
3 | collections::HashMap,
4 | sync::Arc,
5 | };
6 |
7 | use bar_rs_derive::Builder;
8 | use handlebars::Handlebars;
9 | use iced::{
10 | widget::{button, container, text},
11 | Background, Border, Color, Element, Padding,
12 | };
13 | use niri_ipc::Workspace;
14 | use tokio::sync::broadcast;
15 |
16 | use crate::{
17 | config::{
18 | anchor::BarAnchor,
19 | module_config::{LocalModuleConfig, ModuleConfigOverride},
20 | parse::StringExt,
21 | popup_config::PopupConfig,
22 | },
23 | fill::FillExt,
24 | impl_on_click, impl_wrapper, list,
25 | listeners::niri::NiriListener,
26 | modules::{require_listener, Module},
27 | Message, NERD_FONT,
28 | };
29 |
30 | #[derive(Debug, Builder)]
31 | pub struct NiriWorkspaceMod {
32 | pub workspaces: HashMap>,
33 | pub focused: u64,
34 | pub sender: broadcast::Sender>,
35 | cfg_override: ModuleConfigOverride,
36 | icon_padding: Padding,
37 | icon_background: Option,
38 | icon_border: Border,
39 | active_padding: Option,
40 | active_size: f32,
41 | active_color: Color,
42 | active_background: Option,
43 | active_icon_border: Border,
44 | // Output, (idx, icon)
45 | icons: HashMap>,
46 | fallback_icon: String,
47 | active_fallback_icon: String,
48 | output_order: Vec,
49 | }
50 |
51 | impl Default for NiriWorkspaceMod {
52 | fn default() -> Self {
53 | Self {
54 | workspaces: HashMap::new(),
55 | focused: 0,
56 | sender: broadcast::channel(1).0,
57 | cfg_override: Default::default(),
58 | icon_padding: Padding::default(),
59 | icon_background: None,
60 | icon_border: Border::default(),
61 | active_padding: None,
62 | active_size: 20.,
63 | active_color: Color::WHITE,
64 | active_background: None,
65 | active_icon_border: Border::default().rounded(8),
66 | icons: HashMap::new(),
67 | fallback_icon: String::from(""),
68 | active_fallback_icon: String::from(""),
69 | output_order: vec![],
70 | }
71 | }
72 | }
73 |
74 | impl NiriWorkspaceMod {
75 | fn sort_by_outputs<'a, F, I>(&'a self, f: F) -> Vec>
76 | where
77 | F: Fn((&'a String, &'a Vec)) -> I,
78 | I: Iterator- >,
79 | {
80 | match self.output_order.is_empty() {
81 | true => self
82 | .workspaces
83 | .iter()
84 | .flat_map(f)
85 | .collect::>>(),
86 | false => self
87 | .output_order
88 | .iter()
89 | .filter_map(|o| self.workspaces.get_key_value(o))
90 | .flat_map(f)
91 | .collect::