feat(bar): auto select/hide widget based on value

This commit adds new settings to some widgets that allows to auto
select/hide them based on their current values.

The cpu/memory/network/storage widgets get a setting that auto selects
the widget if the current value/percentage is over a value.

The battery widget gets a setting that auto selects the widget if the
current percentage is under a value.

The storage widget gets a setting that auto hides the disk widget if the
percentage is under a value.

Also added 2 new settings (auto_select_fill and auto_select_text) to the
theme, in order to select the fill and text colors of an auto selected
widget.

(Easter egg: the network icons change if the value is over the limit)

PR: #1353
This commit is contained in:
Csaba
2025-03-16 21:54:30 +01:00
committed by Jeezy
parent 2934d011fd
commit f4bbee0a2e
11 changed files with 915 additions and 242 deletions

View File

@@ -14,6 +14,8 @@ use crate::widgets::komorebi::KomorebiNotificationState;
use crate::widgets::widget::BarWidget; use crate::widgets::widget::BarWidget;
use crate::widgets::widget::WidgetConfig; use crate::widgets::widget::WidgetConfig;
use crate::KomorebiEvent; use crate::KomorebiEvent;
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use crate::BAR_HEIGHT; use crate::BAR_HEIGHT;
use crate::DEFAULT_PADDING; use crate::DEFAULT_PADDING;
use crate::MAX_LABEL_WIDTH; use crate::MAX_LABEL_WIDTH;
@@ -43,6 +45,7 @@ use eframe::egui::Vec2;
use eframe::egui::Visuals; use eframe::egui::Visuals;
use font_loader::system_fonts; use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder; use font_loader::system_fonts::FontPropertyBuilder;
use komorebi_client::Colour;
use komorebi_client::KomorebiTheme; use komorebi_client::KomorebiTheme;
use komorebi_client::MonitorNotification; use komorebi_client::MonitorNotification;
use komorebi_client::NotificationEvent; use komorebi_client::NotificationEvent;
@@ -88,71 +91,82 @@ pub fn apply_theme(
grouping: Option<Grouping>, grouping: Option<Grouping>,
render_config: Rc<RefCell<RenderConfig>>, render_config: Rc<RefCell<RenderConfig>>,
) { ) {
match theme { let (auto_select_fill, auto_select_text) = match theme {
KomobarTheme::Catppuccin { KomobarTheme::Catppuccin {
name: catppuccin, name: catppuccin,
accent: catppuccin_value, accent: catppuccin_value,
} => match catppuccin { auto_select_fill: catppuccin_auto_select_fill,
Catppuccin::Frappe => { auto_select_text: catppuccin_auto_select_text,
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE); } => {
let catppuccin_value = catppuccin_value.unwrap_or_default(); match catppuccin {
let accent = catppuccin_value.color32(catppuccin.as_theme()); Catppuccin::Frappe => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| { ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent; style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent; style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent; style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None; style.visuals.override_text_color = None;
}); });
bg_color.replace(catppuccin_egui::FRAPPE.base); bg_color.replace(catppuccin_egui::FRAPPE.base);
}
Catppuccin::Latte => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::LATTE.base);
}
Catppuccin::Macchiato => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MACCHIATO.base);
}
Catppuccin::Mocha => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MOCHA.base);
}
} }
Catppuccin::Latte => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| { (
style.visuals.selection.stroke.color = accent; catppuccin_auto_select_fill.map(|c| c.color32(catppuccin.as_theme())),
style.visuals.widgets.hovered.fg_stroke.color = accent; catppuccin_auto_select_text.map(|c| c.color32(catppuccin.as_theme())),
style.visuals.widgets.active.fg_stroke.color = accent; )
style.visuals.override_text_color = None; }
});
bg_color.replace(catppuccin_egui::LATTE.base);
}
Catppuccin::Macchiato => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MACCHIATO.base);
}
Catppuccin::Mocha => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MOCHA.base);
}
},
KomobarTheme::Base16 { KomobarTheme::Base16 {
name: base16, name: base16,
accent: base16_value, accent: base16_value,
auto_select_fill: base16_auto_select_fill,
auto_select_text: base16_auto_select_text,
} => { } => {
ctx.set_style(base16.style()); ctx.set_style(base16.style());
let base16_value = base16_value.unwrap_or_default(); let base16_value = base16_value.unwrap_or_default();
@@ -165,15 +179,22 @@ pub fn apply_theme(
}); });
bg_color.replace(base16.background()); bg_color.replace(base16.background());
(
base16_auto_select_fill.map(|c| c.color32(Base16Wrapper::Base16(base16))),
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Base16(base16))),
)
} }
KomobarTheme::Custom { KomobarTheme::Custom {
colours, colours,
accent: base16_value, accent: base16_value,
auto_select_fill: base16_auto_select_fill,
auto_select_text: base16_auto_select_text,
} => { } => {
let background = colours.background(); let background = colours.background();
ctx.set_style(colours.style()); ctx.set_style(colours.style());
let base16_value = base16_value.unwrap_or_default(); let base16_value = base16_value.unwrap_or_default();
let accent = base16_value.color32(Base16Wrapper::Custom(colours)); let accent = base16_value.color32(Base16Wrapper::Custom(colours.clone()));
ctx.style_mut(|style| { ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent; style.visuals.selection.stroke.color = accent;
@@ -182,8 +203,22 @@ pub fn apply_theme(
}); });
bg_color.replace(background); bg_color.replace(background);
(
base16_auto_select_fill.map(|c| c.color32(Base16Wrapper::Custom(colours.clone()))),
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Custom(colours.clone()))),
)
} }
} };
AUTO_SELECT_FILL_COLOUR.store(
auto_select_fill.map_or(0, |c| Colour::from(c).into()),
Ordering::SeqCst,
);
AUTO_SELECT_TEXT_COLOUR.store(
auto_select_text.map_or(0, |c| Colour::from(c).into()),
Ordering::SeqCst,
);
// Apply transparency_alpha // Apply transparency_alpha
let theme_color = *bg_color.borrow(); let theme_color = *bg_color.borrow();

View File

@@ -382,18 +382,24 @@ pub enum KomobarTheme {
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin) /// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
name: komorebi_themes::Catppuccin, name: komorebi_themes::Catppuccin,
accent: Option<komorebi_themes::CatppuccinValue>, accent: Option<komorebi_themes::CatppuccinValue>,
auto_select_fill: Option<komorebi_themes::CatppuccinValue>,
auto_select_text: Option<komorebi_themes::CatppuccinValue>,
}, },
/// A theme from base16-egui-themes /// A theme from base16-egui-themes
Base16 { Base16 {
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/) /// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
name: komorebi_themes::Base16, name: komorebi_themes::Base16,
accent: Option<komorebi_themes::Base16Value>, accent: Option<komorebi_themes::Base16Value>,
auto_select_fill: Option<komorebi_themes::Base16Value>,
auto_select_text: Option<komorebi_themes::Base16Value>,
}, },
/// A custom Base16 theme /// A custom Base16 theme
Custom { Custom {
/// Colours of the custom Base16 theme palette /// Colours of the custom Base16 theme palette
colours: Box<komorebi_themes::Base16ColourPalette>, colours: Box<komorebi_themes::Base16ColourPalette>,
accent: Option<komorebi_themes::Base16Value>, accent: Option<komorebi_themes::Base16Value>,
auto_select_fill: Option<komorebi_themes::Base16Value>,
auto_select_text: Option<komorebi_themes::Base16Value>,
}, },
} }
@@ -405,12 +411,16 @@ impl From<KomorebiTheme> for KomobarTheme {
} => Self::Catppuccin { } => Self::Catppuccin {
name, name,
accent: bar_accent, accent: bar_accent,
auto_select_fill: None,
auto_select_text: None,
}, },
KomorebiTheme::Base16 { KomorebiTheme::Base16 {
name, bar_accent, .. name, bar_accent, ..
} => Self::Base16 { } => Self::Base16 {
name, name,
accent: bar_accent, accent: bar_accent,
auto_select_fill: None,
auto_select_text: None,
}, },
KomorebiTheme::Custom { KomorebiTheme::Custom {
colours, colours,
@@ -419,6 +429,8 @@ impl From<KomorebiTheme> for KomobarTheme {
} => Self::Custom { } => Self::Custom {
colours, colours,
accent: bar_accent, accent: bar_accent,
auto_select_fill: None,
auto_select_text: None,
}, },
} }
} }

View File

@@ -23,6 +23,7 @@ use std::io::BufReader;
use std::io::Read; use std::io::Read;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::atomic::AtomicI32; use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::LazyLock; use std::sync::LazyLock;
@@ -47,6 +48,9 @@ pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
pub static BAR_HEIGHT: f32 = 50.0; pub static BAR_HEIGHT: f32 = 50.0;
pub static DEFAULT_PADDING: f32 = 10.0; pub static DEFAULT_PADDING: f32 = 10.0;
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
pub static ICON_CACHE: LazyLock<Mutex<HashMap<isize, RgbaImage>>> = pub static ICON_CACHE: LazyLock<Mutex<HashMap<isize, RgbaImage>>> =
LazyLock::new(|| Mutex::new(HashMap::new())); LazyLock::new(|| Mutex::new(HashMap::new()));

View File

@@ -1,6 +1,8 @@
use crate::bar::Alignment; use crate::bar::Alignment;
use crate::config::KomobarConfig; use crate::config::KomobarConfig;
use crate::config::MonitorConfigOrIndex; use crate::config::MonitorConfigOrIndex;
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use eframe::egui::Color32; use eframe::egui::Color32;
use eframe::egui::Context; use eframe::egui::Context;
use eframe::egui::CornerRadius; use eframe::egui::CornerRadius;
@@ -11,8 +13,11 @@ use eframe::egui::Margin;
use eframe::egui::Shadow; use eframe::egui::Shadow;
use eframe::egui::TextStyle; use eframe::egui::TextStyle;
use eframe::egui::Ui; use eframe::egui::Ui;
use komorebi_client::Colour;
use komorebi_client::Rgb;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::num::NonZeroU32;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
@@ -55,6 +60,10 @@ pub struct RenderConfig {
pub icon_font_id: FontId, pub icon_font_id: FontId,
/// Show all icons on the workspace section of the Komorebi widget /// Show all icons on the workspace section of the Komorebi widget
pub show_all_icons: bool, pub show_all_icons: bool,
/// Background color of the selected frame
pub auto_select_fill: Option<Color32>,
/// Text color of the selected frame
pub auto_select_text: Option<Color32>,
} }
pub trait RenderExt { pub trait RenderExt {
@@ -108,6 +117,10 @@ impl RenderExt for &KomobarConfig {
text_font_id, text_font_id,
icon_font_id, icon_font_id,
show_all_icons, show_all_icons,
auto_select_fill: NonZeroU32::new(AUTO_SELECT_FILL_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
auto_select_text: NonZeroU32::new(AUTO_SELECT_TEXT_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
} }
} }
} }
@@ -133,6 +146,8 @@ impl RenderConfig {
text_font_id: FontId::default(), text_font_id: FontId::default(),
icon_font_id: FontId::default(), icon_font_id: FontId::default(),
show_all_icons: false, show_all_icons: false,
auto_select_fill: None,
auto_select_text: None,
} }
} }

View File

@@ -10,15 +10,29 @@ use eframe::egui::Ui;
/// Same as SelectableLabel, but supports all content /// Same as SelectableLabel, but supports all content
pub struct SelectableFrame { pub struct SelectableFrame {
selected: bool, selected: bool,
selected_fill: Option<Color32>,
} }
impl SelectableFrame { impl SelectableFrame {
pub fn new(selected: bool) -> Self { pub fn new(selected: bool) -> Self {
Self { selected } Self {
selected,
selected_fill: None,
}
}
pub fn new_auto(selected: bool, selected_fill: Option<Color32>) -> Self {
Self {
selected,
selected_fill,
}
} }
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response { pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
let Self { selected } = self; let Self {
selected,
selected_fill,
} = self;
Frame::NONE Frame::NONE
.show(ui, |ui| { .show(ui, |ui| {
@@ -32,7 +46,16 @@ impl SelectableFrame {
); );
// since the stroke is drawn inside the frame, we always reserve space for it // since the stroke is drawn inside the frame, we always reserve space for it
if response.hovered() || response.highlighted() || response.has_focus() { if selected && response.hovered() {
let visuals = ui.style().interact_selectable(&response, selected);
Frame::NONE
.stroke(Stroke::new(1.0, visuals.bg_stroke.color))
.corner_radius(visuals.corner_radius)
.fill(selected_fill.unwrap_or(visuals.bg_fill))
.inner_margin(inner_margin)
.show(ui, add_contents);
} else if response.hovered() || response.highlighted() || response.has_focus() {
let visuals = ui.style().interact_selectable(&response, selected); let visuals = ui.style().interact_selectable(&response, selected);
Frame::NONE Frame::NONE
@@ -47,7 +70,7 @@ impl SelectableFrame {
Frame::NONE Frame::NONE
.stroke(Stroke::new(1.0, visuals.bg_fill)) .stroke(Stroke::new(1.0, visuals.bg_fill))
.corner_radius(visuals.corner_radius) .corner_radius(visuals.corner_radius)
.fill(visuals.bg_fill) .fill(selected_fill.unwrap_or(visuals.bg_fill))
.inner_margin(inner_margin) .inner_margin(inner_margin)
.show(ui, add_contents); .show(ui, add_contents);
} else { } else {

View File

@@ -28,6 +28,8 @@ pub struct BatteryConfig {
pub data_refresh_interval: Option<u64>, pub data_refresh_interval: Option<u64>,
/// Display label prefix /// Display label prefix
pub label_prefix: Option<LabelPrefix>, pub label_prefix: Option<LabelPrefix>,
/// Select when the current percentage is under this value [[1-100]]
pub auto_select_under: Option<u8>,
} }
impl From<BatteryConfig> for Battery { impl From<BatteryConfig> for Battery {
@@ -38,9 +40,10 @@ impl From<BatteryConfig> for Battery {
enable: value.enable, enable: value.enable,
hide_on_full_charge: value.hide_on_full_charge.unwrap_or(false), hide_on_full_charge: value.hide_on_full_charge.unwrap_or(false),
manager: Manager::new().unwrap(), manager: Manager::new().unwrap(),
last_state: String::new(), last_state: None,
data_refresh_interval, data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon), label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
auto_select_under: value.auto_select_under.map(|u| u.clamp(1, 100)),
state: BatteryState::Discharging, state: BatteryState::Discharging,
last_updated: Instant::now() last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval)) .checked_sub(Duration::from_secs(data_refresh_interval))
@@ -54,6 +57,12 @@ pub enum BatteryState {
Discharging, Discharging,
} }
#[derive(Clone, Debug)]
struct BatteryOutput {
label: String,
selected: bool,
}
pub struct Battery { pub struct Battery {
pub enable: bool, pub enable: bool,
hide_on_full_charge: bool, hide_on_full_charge: bool,
@@ -61,24 +70,25 @@ pub struct Battery {
pub state: BatteryState, pub state: BatteryState,
data_refresh_interval: u64, data_refresh_interval: u64,
label_prefix: LabelPrefix, label_prefix: LabelPrefix,
last_state: String, auto_select_under: Option<u8>,
last_state: Option<BatteryOutput>,
last_updated: Instant, last_updated: Instant,
} }
impl Battery { impl Battery {
fn output(&mut self) -> String { fn output(&mut self) -> Option<BatteryOutput> {
let mut output = self.last_state.clone(); let mut output = self.last_state.clone();
let now = Instant::now(); let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) { if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
output.clear(); output = None;
if let Ok(mut batteries) = self.manager.batteries() { if let Ok(mut batteries) = self.manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) { if let Some(Ok(first)) = batteries.nth(0) {
let percentage = first.state_of_charge().get::<percent>(); let percentage = first.state_of_charge().get::<percent>() as u8;
if percentage == 100.0 && self.hide_on_full_charge { if percentage == 100 && self.hide_on_full_charge {
output = String::new() output = None
} else { } else {
match first.state() { match first.state() {
State::Charging => self.state = BatteryState::Charging, State::Charging => self.state = BatteryState::Charging,
@@ -86,12 +96,19 @@ impl Battery {
_ => {} _ => {}
} }
output = match self.label_prefix { let selected = self.auto_select_under.is_some_and(|u| percentage <= u);
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%") output = Some(BatteryOutput {
} label: match self.label_prefix {
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"), LabelPrefix::Text | LabelPrefix::IconAndText => {
} format!("BAT: {percentage}%")
}
LabelPrefix::None | LabelPrefix::Icon => {
format!("{percentage}%")
}
},
selected,
})
} }
} }
} }
@@ -108,35 +125,39 @@ impl BarWidget for Battery {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable { if self.enable {
let output = self.output(); let output = self.output();
if !output.is_empty() { if let Some(output) = output {
let emoji = match self.state { let emoji = match self.state {
BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING, BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING,
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL, BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
}; };
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple( let mut layout_job = LayoutJob::simple(
match self.label_prefix { match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(), LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
LabelPrefix::None | LabelPrefix::Text => String::new(), LabelPrefix::None | LabelPrefix::Text => String::new(),
}, },
config.icon_font_id.clone(), config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color, auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0, 100.0,
); );
layout_job.append( layout_job.append(
&output, &output.label,
10.0, 10.0,
TextFormat { TextFormat {
font_id: config.text_font_id.clone(), font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(), color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center, valign: Align::Center,
..Default::default() ..Default::default()
}, },
); );
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| { config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false) if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false))) .show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked() .clicked()
{ {

View File

@@ -25,6 +25,8 @@ pub struct CpuConfig {
pub data_refresh_interval: Option<u64>, pub data_refresh_interval: Option<u64>,
/// Display label prefix /// Display label prefix
pub label_prefix: Option<LabelPrefix>, pub label_prefix: Option<LabelPrefix>,
/// Select when the current percentage is over this value [[1-100]]
pub auto_select_over: Option<u8>,
} }
impl From<CpuConfig> for Cpu { impl From<CpuConfig> for Cpu {
@@ -38,6 +40,7 @@ impl From<CpuConfig> for Cpu {
), ),
data_refresh_interval, data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText), label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
last_updated: Instant::now() last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval)) .checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(), .unwrap(),
@@ -45,26 +48,38 @@ impl From<CpuConfig> for Cpu {
} }
} }
#[derive(Clone, Debug)]
struct CpuOutput {
label: String,
selected: bool,
}
pub struct Cpu { pub struct Cpu {
pub enable: bool, pub enable: bool,
system: System, system: System,
data_refresh_interval: u64, data_refresh_interval: u64,
label_prefix: LabelPrefix, label_prefix: LabelPrefix,
auto_select_over: Option<u8>,
last_updated: Instant, last_updated: Instant,
} }
impl Cpu { impl Cpu {
fn output(&mut self) -> String { fn output(&mut self) -> CpuOutput {
let now = Instant::now(); let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) { if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_cpu_usage(); self.system.refresh_cpu_usage();
self.last_updated = now; self.last_updated = now;
} }
let used = self.system.global_cpu_usage(); let used = self.system.global_cpu_usage() as u8;
match self.label_prefix { let selected = self.auto_select_over.is_some_and(|o| used >= o);
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {:.0}%", used),
LabelPrefix::None | LabelPrefix::Icon => format!("{:.0}%", used), CpuOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {}%", used),
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", used),
},
selected,
} }
} }
} }
@@ -73,7 +88,9 @@ impl BarWidget for Cpu {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable { if self.enable {
let output = self.output(); let output = self.output();
if !output.is_empty() { if !output.label.is_empty() {
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple( let mut layout_job = LayoutJob::simple(
match self.label_prefix { match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => { LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -82,23 +99,25 @@ impl BarWidget for Cpu {
LabelPrefix::None | LabelPrefix::Text => String::new(), LabelPrefix::None | LabelPrefix::Text => String::new(),
}, },
config.icon_font_id.clone(), config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color, auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0, 100.0,
); );
layout_job.append( layout_job.append(
&output, &output.label,
10.0, 10.0,
TextFormat { TextFormat {
font_id: config.text_font_id.clone(), font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(), color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center, valign: Align::Center,
..Default::default() ..Default::default()
}, },
); );
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| { config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false) if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false))) .show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked() .clicked()
{ {

View File

@@ -25,6 +25,8 @@ pub struct MemoryConfig {
pub data_refresh_interval: Option<u64>, pub data_refresh_interval: Option<u64>,
/// Display label prefix /// Display label prefix
pub label_prefix: Option<LabelPrefix>, pub label_prefix: Option<LabelPrefix>,
/// Select when the current percentage is over this value [[1-100]]
pub auto_select_over: Option<u8>,
} }
impl From<MemoryConfig> for Memory { impl From<MemoryConfig> for Memory {
@@ -38,6 +40,7 @@ impl From<MemoryConfig> for Memory {
), ),
data_refresh_interval, data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText), label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
last_updated: Instant::now() last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval)) .checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(), .unwrap(),
@@ -45,16 +48,23 @@ impl From<MemoryConfig> for Memory {
} }
} }
#[derive(Clone, Debug)]
struct MemoryOutput {
label: String,
selected: bool,
}
pub struct Memory { pub struct Memory {
pub enable: bool, pub enable: bool,
system: System, system: System,
data_refresh_interval: u64, data_refresh_interval: u64,
label_prefix: LabelPrefix, label_prefix: LabelPrefix,
auto_select_over: Option<u8>,
last_updated: Instant, last_updated: Instant,
} }
impl Memory { impl Memory {
fn output(&mut self) -> String { fn output(&mut self) -> MemoryOutput {
let now = Instant::now(); let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) { if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_memory(); self.system.refresh_memory();
@@ -63,11 +73,17 @@ impl Memory {
let used = self.system.used_memory(); let used = self.system.used_memory();
let total = self.system.total_memory(); let total = self.system.total_memory();
match self.label_prefix { let usage = ((used * 100) / total) as u8;
LabelPrefix::Text | LabelPrefix::IconAndText => { let selected = self.auto_select_over.is_some_and(|o| usage >= o);
format!("RAM: {}%", (used * 100) / total)
} MemoryOutput {
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total), label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("RAM: {}%", usage)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", usage),
},
selected,
} }
} }
} }
@@ -76,7 +92,9 @@ impl BarWidget for Memory {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable { if self.enable {
let output = self.output(); let output = self.output();
if !output.is_empty() { if !output.label.is_empty() {
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple( let mut layout_job = LayoutJob::simple(
match self.label_prefix { match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => { LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -85,23 +103,25 @@ impl BarWidget for Memory {
LabelPrefix::None | LabelPrefix::Text => String::new(), LabelPrefix::None | LabelPrefix::Text => String::new(),
}, },
config.icon_font_id.clone(), config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color, auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0, 100.0,
); );
layout_job.append( layout_job.append(
&output, &output.label,
10.0, 10.0,
TextFormat { TextFormat {
font_id: config.text_font_id.clone(), font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(), color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center, valign: Align::Center,
..Default::default() ..Default::default()
}, },
); );
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| { config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false) if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false))) .show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked() .clicked()
{ {

View File

@@ -1,9 +1,11 @@
use crate::bar::Alignment;
use crate::config::LabelPrefix; use crate::config::LabelPrefix;
use crate::render::RenderConfig; use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame; use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget; use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob; use eframe::egui::text::LayoutJob;
use eframe::egui::Align; use eframe::egui::Align;
use eframe::egui::Color32;
use eframe::egui::Context; use eframe::egui::Context;
use eframe::egui::Label; use eframe::egui::Label;
use eframe::egui::TextFormat; use eframe::egui::TextFormat;
@@ -22,18 +24,36 @@ use sysinfo::Networks;
pub struct NetworkConfig { pub struct NetworkConfig {
/// Enable the Network widget /// Enable the Network widget
pub enable: bool, pub enable: bool,
/// Show total data transmitted /// Show total received and transmitted activity
pub show_total_data_transmitted: bool, #[serde(alias = "show_total_data_transmitted")]
/// Show network activity pub show_total_activity: bool,
pub show_network_activity: bool, /// Show received and transmitted activity
#[serde(alias = "show_network_activity")]
pub show_activity: bool,
/// Show default interface /// Show default interface
pub show_default_interface: Option<bool>, pub show_default_interface: Option<bool>,
/// Characters to reserve for network activity data /// Characters to reserve for received and transmitted activity
pub network_activity_fill_characters: Option<usize>, #[serde(alias = "network_activity_fill_characters")]
pub activity_left_padding: Option<usize>,
/// Data refresh interval (default: 10 seconds) /// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>, pub data_refresh_interval: Option<u64>,
/// Display label prefix /// Display label prefix
pub label_prefix: Option<LabelPrefix>, pub label_prefix: Option<LabelPrefix>,
/// Select when the value is over a limit (1MiB is 1048576 bytes (1024*1024))
pub auto_select: Option<NetworkSelectConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct NetworkSelectConfig {
/// Select the total received data when it's over this value
pub total_received_over: Option<u64>,
/// Select the total transmitted data when it's over this value
pub total_transmitted_over: Option<u64>,
/// Select the received data when it's over this value
pub received_over: Option<u64>,
/// Select the transmitted data when it's over this value
pub transmitted_over: Option<u64>,
} }
impl From<NetworkConfig> for Network { impl From<NetworkConfig> for Network {
@@ -42,16 +62,15 @@ impl From<NetworkConfig> for Network {
Self { Self {
enable: value.enable, enable: value.enable,
show_total_activity: value.show_total_data_transmitted, show_total_activity: value.show_total_activity,
show_activity: value.show_network_activity, show_activity: value.show_activity,
show_default_interface: value.show_default_interface.unwrap_or(true), show_default_interface: value.show_default_interface.unwrap_or(true),
networks_network_activity: Networks::new_with_refreshed_list(), networks_network_activity: Networks::new_with_refreshed_list(),
default_interface: String::new(), default_interface: String::new(),
data_refresh_interval, data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon), label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
network_activity_fill_characters: value auto_select: value.auto_select,
.network_activity_fill_characters activity_left_padding: value.activity_left_padding.unwrap_or_default(),
.unwrap_or_default(),
last_state_total_activity: vec![], last_state_total_activity: vec![],
last_state_activity: vec![], last_state_activity: vec![],
last_updated_network_activity: Instant::now() last_updated_network_activity: Instant::now()
@@ -69,11 +88,12 @@ pub struct Network {
networks_network_activity: Networks, networks_network_activity: Networks,
data_refresh_interval: u64, data_refresh_interval: u64,
label_prefix: LabelPrefix, label_prefix: LabelPrefix,
auto_select: Option<NetworkSelectConfig>,
default_interface: String, default_interface: String,
last_state_total_activity: Vec<NetworkReading>, last_state_total_activity: Vec<NetworkReading>,
last_state_activity: Vec<NetworkReading>, last_state_activity: Vec<NetworkReading>,
last_updated_network_activity: Instant, last_updated_network_activity: Instant,
network_activity_fill_characters: usize, activity_left_padding: usize,
} }
impl Network { impl Network {
@@ -105,24 +125,32 @@ impl Network {
for (interface_name, data) in &self.networks_network_activity { for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) { if friendly_name.eq(interface_name) {
if self.show_activity { if self.show_activity {
let received = Self::to_pretty_bytes(
data.received(),
self.data_refresh_interval,
);
let transmitted = Self::to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval,
);
activity.push(NetworkReading::new( activity.push(NetworkReading::new(
NetworkReadingFormat::Speed, NetworkReadingFormat::Speed,
Self::to_pretty_bytes( ReadingValue::from(received),
data.received(), ReadingValue::from(transmitted),
self.data_refresh_interval,
),
Self::to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval,
),
)); ));
} }
if self.show_total_activity { if self.show_total_activity {
let total_received =
Self::to_pretty_bytes(data.total_received(), 1);
let total_transmitted =
Self::to_pretty_bytes(data.total_transmitted(), 1);
total_activity.push(NetworkReading::new( total_activity.push(NetworkReading::new(
NetworkReadingFormat::Total, NetworkReadingFormat::Total,
Self::to_pretty_bytes(data.total_received(), 1), ReadingValue::from(total_received),
Self::to_pretty_bytes(data.total_transmitted(), 1), ReadingValue::from(total_transmitted),
)) ))
} }
} }
@@ -138,105 +166,121 @@ impl Network {
(activity, total_activity) (activity, total_activity)
} }
fn reading_to_label( fn reading_to_labels(
&self, &self,
select_received: bool,
select_transmitted: bool,
ctx: &Context, ctx: &Context,
reading: NetworkReading, reading: &NetworkReading,
config: RenderConfig, config: RenderConfig,
) -> Label { ) -> (Label, Label) {
let (text_down, text_up) = match self.label_prefix { let (text_down, text_up) = match self.label_prefix {
LabelPrefix::None | LabelPrefix::Icon => match reading.format { LabelPrefix::None | LabelPrefix::Icon => match reading.format {
NetworkReadingFormat::Speed => ( NetworkReadingFormat::Speed => (
format!( format!(
"{: >width$}/s ", "{: >width$}/s ",
reading.received_text, reading.received.pretty,
width = self.network_activity_fill_characters width = self.activity_left_padding
), ),
format!( format!(
"{: >width$}/s", "{: >width$}/s",
reading.transmitted_text, reading.transmitted.pretty,
width = self.network_activity_fill_characters width = self.activity_left_padding
), ),
), ),
NetworkReadingFormat::Total => ( NetworkReadingFormat::Total => (
format!("{} ", reading.received_text), format!("{} ", reading.received.pretty),
reading.transmitted_text, reading.transmitted.pretty.clone(),
), ),
}, },
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format { LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
NetworkReadingFormat::Speed => ( NetworkReadingFormat::Speed => (
format!( format!(
"DOWN: {: >width$}/s ", "DOWN: {: >width$}/s ",
reading.received_text, reading.received.pretty,
width = self.network_activity_fill_characters width = self.activity_left_padding
), ),
format!( format!(
"UP: {: >width$}/s", "UP: {: >width$}/s",
reading.transmitted_text, reading.transmitted.pretty,
width = self.network_activity_fill_characters width = self.activity_left_padding
), ),
), ),
NetworkReadingFormat::Total => ( NetworkReadingFormat::Total => (
format!("\u{2211}DOWN: {}/s ", reading.received_text), format!("\u{2211}DOWN: {}/s ", reading.received.pretty),
format!("\u{2211}UP: {}/s", reading.transmitted_text), format!("\u{2211}UP: {}/s", reading.transmitted.pretty),
), ),
}, },
}; };
let icon_format = TextFormat::simple( let auto_text_color_received = config.auto_select_text.filter(|_| select_received);
config.icon_font_id.clone(), let auto_text_color_transmitted = config.auto_select_text.filter(|_| select_transmitted);
ctx.style().visuals.selection.stroke.color,
);
let text_format = TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
};
// icon // icon
let mut layout_job = LayoutJob::simple( let mut layout_job_down = LayoutJob::simple(
match self.label_prefix { match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => { LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ARROW_FAT_DOWN.to_string() if select_received {
egui_phosphor::regular::ARROW_FAT_LINES_DOWN.to_string()
} else {
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
}
} }
LabelPrefix::None | LabelPrefix::Text => String::new(), LabelPrefix::None | LabelPrefix::Text => String::new(),
}, },
icon_format.font_id.clone(), config.icon_font_id.clone(),
icon_format.color, auto_text_color_received.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0, 100.0,
); );
// text // text
layout_job.append( layout_job_down.append(
&text_down, &text_down,
ctx.style().spacing.item_spacing.x, ctx.style().spacing.item_spacing.x,
text_format.clone(), TextFormat {
font_id: config.text_font_id.clone(),
color: auto_text_color_received.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
); );
// icon // icon
layout_job.append( let mut layout_job_up = LayoutJob::simple(
&match self.label_prefix { match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => { LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ARROW_FAT_UP.to_string() if select_transmitted {
egui_phosphor::regular::ARROW_FAT_LINES_UP.to_string()
} else {
egui_phosphor::regular::ARROW_FAT_UP.to_string()
}
} }
LabelPrefix::None | LabelPrefix::Text => String::new(), LabelPrefix::None | LabelPrefix::Text => String::new(),
}, },
0.0, config.icon_font_id.clone(),
icon_format.clone(), auto_text_color_transmitted.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
); );
// text // text
layout_job.append( layout_job_up.append(
&text_up, &text_up,
ctx.style().spacing.item_spacing.x, ctx.style().spacing.item_spacing.x,
text_format.clone(), TextFormat {
font_id: config.text_font_id.clone(),
color: auto_text_color_transmitted.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
); );
Label::new(layout_job).selectable(false) (
Label::new(layout_job_down).selectable(false),
Label::new(layout_job_up).selectable(false),
)
} }
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String { fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> (u64, String) {
let input = input_in_bytes as f32 / timespan_in_s as f32; let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32; let mut magnitude = input.log(1024f32) as u32;
@@ -248,10 +292,30 @@ impl Network {
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude); let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32; let result = input / ((1u64) << (magnitude * 10)) as f32;
match base { (
Some(DataUnit::B) => format!("{result:.1} B"), input as u64,
Some(unit) => format!("{result:.1} {unit}iB"), match base {
None => String::from("Unknown data unit"), Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
},
)
}
fn show_frame<R>(
&self,
selected: bool,
auto_focus_fill: Option<Color32>,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) {
if SelectableFrame::new_auto(selected, auto_focus_fill)
.show(ui, add_contents)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
eprintln!("{}", error);
}
} }
} }
} }
@@ -259,6 +323,8 @@ impl Network {
impl BarWidget for Network { impl BarWidget for Network {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable { if self.enable {
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
// widget spacing: make sure to use the same config to call the apply_on_widget function // widget spacing: make sure to use the same config to call the apply_on_widget function
let mut render_config = config.clone(); let mut render_config = config.clone();
@@ -266,17 +332,102 @@ impl BarWidget for Network {
let (activity, total_activity) = self.network_activity(); let (activity, total_activity) = self.network_activity();
if self.show_total_activity { if self.show_total_activity {
for reading in total_activity { for reading in &total_activity {
render_config.apply_on_widget(true, ui, |ui| { render_config.apply_on_widget(false, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading, config.clone())); let select_received = self.auto_select.is_some_and(|f| {
f.total_received_over
.is_some_and(|o| reading.received.value > o)
});
let select_transmitted = self.auto_select.is_some_and(|f| {
f.total_transmitted_over
.is_some_and(|o| reading.transmitted.value > o)
});
let labels = self.reading_to_labels(
select_received,
select_transmitted,
ctx,
reading,
config.clone(),
);
if is_reversed {
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
} else {
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
}
}); });
} }
} }
if self.show_activity { if self.show_activity {
for reading in activity { for reading in &activity {
render_config.apply_on_widget(true, ui, |ui| { render_config.apply_on_widget(false, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading, config.clone())); let select_received = self.auto_select.is_some_and(|f| {
f.received_over.is_some_and(|o| reading.received.value > o)
});
let select_transmitted = self.auto_select.is_some_and(|f| {
f.transmitted_over
.is_some_and(|o| reading.transmitted.value > o)
});
let labels = self.reading_to_labels(
select_received,
select_transmitted,
ctx,
reading,
config.clone(),
);
if is_reversed {
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
} else {
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
}
}); });
} }
} }
@@ -314,15 +465,9 @@ impl BarWidget for Network {
); );
render_config.apply_on_widget(false, ui, |ui| { render_config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false) self.show_frame(false, None, ui, |ui| {
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false))) ui.add(Label::new(layout_job).selectable(false))
.clicked() });
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
{
eprintln!("{}", error)
}
}
}); });
} }
} }
@@ -339,19 +484,38 @@ enum NetworkReadingFormat {
Total = 1, Total = 1,
} }
#[derive(Clone)]
struct ReadingValue {
value: u64,
pretty: String,
}
impl From<(u64, String)> for ReadingValue {
fn from(value: (u64, String)) -> Self {
Self {
value: value.0,
pretty: value.1,
}
}
}
#[derive(Clone)] #[derive(Clone)]
struct NetworkReading { struct NetworkReading {
pub format: NetworkReadingFormat, format: NetworkReadingFormat,
pub received_text: String, received: ReadingValue,
pub transmitted_text: String, transmitted: ReadingValue,
} }
impl NetworkReading { impl NetworkReading {
pub fn new(format: NetworkReadingFormat, received: String, transmitted: String) -> Self { fn new(
NetworkReading { format: NetworkReadingFormat,
received: ReadingValue,
transmitted: ReadingValue,
) -> Self {
Self {
format, format,
received_text: received, received,
transmitted_text: transmitted, transmitted,
} }
} }
} }

View File

@@ -1,3 +1,4 @@
use crate::bar::Alignment;
use crate::config::LabelPrefix; use crate::config::LabelPrefix;
use crate::render::RenderConfig; use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame; use crate::selected_frame::SelectableFrame;
@@ -24,6 +25,10 @@ pub struct StorageConfig {
pub data_refresh_interval: Option<u64>, pub data_refresh_interval: Option<u64>,
/// Display label prefix /// Display label prefix
pub label_prefix: Option<LabelPrefix>, pub label_prefix: Option<LabelPrefix>,
/// Select when the current percentage is over this value [[1-100]]
pub auto_select_over: Option<u8>,
/// Hide when the current percentage is under this value [[1-100]]
pub auto_hide_under: Option<u8>,
} }
impl From<StorageConfig> for Storage { impl From<StorageConfig> for Storage {
@@ -33,21 +38,30 @@ impl From<StorageConfig> for Storage {
disks: Disks::new_with_refreshed_list(), disks: Disks::new_with_refreshed_list(),
data_refresh_interval: value.data_refresh_interval.unwrap_or(10), data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText), label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
last_updated: Instant::now(), last_updated: Instant::now(),
} }
} }
} }
struct StorageDisk {
label: String,
selected: bool,
}
pub struct Storage { pub struct Storage {
pub enable: bool, pub enable: bool,
disks: Disks, disks: Disks,
data_refresh_interval: u64, data_refresh_interval: u64,
label_prefix: LabelPrefix, label_prefix: LabelPrefix,
auto_select_over: Option<u8>,
auto_hide_under: Option<u8>,
last_updated: Instant, last_updated: Instant,
} }
impl Storage { impl Storage {
fn output(&mut self) -> Vec<String> { fn output(&mut self) -> Vec<StorageDisk> {
let now = Instant::now(); let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) { if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.disks.refresh(true); self.disks.refresh(true);
@@ -61,17 +75,26 @@ impl Storage {
let total = disk.total_space(); let total = disk.total_space();
let available = disk.available_space(); let available = disk.available_space();
let used = total - available; let used = total - available;
let percentage = ((used * 100) / total) as u8;
disks.push(match self.label_prefix { let hide = self.auto_hide_under.is_some_and(|u| percentage <= u);
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), (used * 100) / total) if !hide {
} let selected = self.auto_select_over.is_some_and(|o| percentage >= o);
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
}) disks.push(StorageDisk {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), percentage)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", percentage),
},
selected,
})
}
} }
disks.sort(); disks.sort_by(|a, b| a.label.cmp(&b.label));
disks.reverse();
disks disks
} }
@@ -80,7 +103,16 @@ impl Storage {
impl BarWidget for Storage { impl BarWidget for Storage {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable { if self.enable {
for output in self.output() { let mut output = self.output();
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
if is_reversed {
output.reverse();
}
for output in output {
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple( let mut layout_job = LayoutJob::simple(
match self.label_prefix { match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => { LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -89,23 +121,25 @@ impl BarWidget for Storage {
LabelPrefix::None | LabelPrefix::Text => String::new(), LabelPrefix::None | LabelPrefix::Text => String::new(),
}, },
config.icon_font_id.clone(), config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color, auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0, 100.0,
); );
layout_job.append( layout_job.append(
&output, &output.label,
10.0, 10.0,
TextFormat { TextFormat {
font_id: config.text_font_id.clone(), font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(), color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center, valign: Align::Center,
..Default::default() ..Default::default()
}, },
); );
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| { config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false) if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false))) .show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked() .clicked()
{ {
@@ -113,7 +147,7 @@ impl BarWidget for Storage {
.args([ .args([
"/C", "/C",
"explorer.exe", "explorer.exe",
output.split(' ').collect::<Vec<&str>>()[0], output.label.split(' ').collect::<Vec<&str>>()[0],
]) ])
.spawn() .spawn()
{ {

View File

@@ -26,6 +26,12 @@
"enable" "enable"
], ],
"properties": { "properties": {
"auto_select_under": {
"description": "Select when the current percentage is under this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -90,6 +96,12 @@
"enable" "enable"
], ],
"properties": { "properties": {
"auto_select_over": {
"description": "Select when the current percentage is over this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -752,6 +764,12 @@
"enable" "enable"
], ],
"properties": { "properties": {
"auto_select_over": {
"description": "Select when the current percentage is over this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -810,10 +828,46 @@
"type": "object", "type": "object",
"required": [ "required": [
"enable", "enable",
"show_network_activity", "show_activity",
"show_total_data_transmitted" "show_total_activity"
], ],
"properties": { "properties": {
"activity_left_padding": {
"description": "Characters to reserve for received and transmitted activity",
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"auto_select": {
"description": "Select when the value is over a limit (1MiB is 1048576 bytes (1024*1024))",
"type": "object",
"properties": {
"received_over": {
"description": "Select the received data when it's over this value",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"total_received_over": {
"description": "Select the total received data when it's over this value",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"total_transmitted_over": {
"description": "Select the total transmitted data when it's over this value",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"transmitted_over": {
"description": "Select the transmitted data when it's over this value",
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
}
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -857,22 +911,16 @@
} }
] ]
}, },
"network_activity_fill_characters": { "show_activity": {
"description": "Characters to reserve for network activity data", "description": "Show received and transmitted activity",
"type": "integer", "type": "boolean"
"format": "uint",
"minimum": 0.0
}, },
"show_default_interface": { "show_default_interface": {
"description": "Show default interface", "description": "Show default interface",
"type": "boolean" "type": "boolean"
}, },
"show_network_activity": { "show_total_activity": {
"description": "Show network activity", "description": "Show total received and transmitted activity",
"type": "boolean"
},
"show_total_data_transmitted": {
"description": "Show total data transmitted",
"type": "boolean" "type": "boolean"
} }
} }
@@ -892,6 +940,18 @@
"enable" "enable"
], ],
"properties": { "properties": {
"auto_hide_under": {
"description": "Hide when the current percentage is under this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"auto_select_over": {
"description": "Select when the current percentage is over this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -1493,6 +1553,12 @@
"enable" "enable"
], ],
"properties": { "properties": {
"auto_select_under": {
"description": "Select when the current percentage is under this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -1557,6 +1623,12 @@
"enable" "enable"
], ],
"properties": { "properties": {
"auto_select_over": {
"description": "Select when the current percentage is over this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -2219,6 +2291,12 @@
"enable" "enable"
], ],
"properties": { "properties": {
"auto_select_over": {
"description": "Select when the current percentage is over this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -2277,10 +2355,46 @@
"type": "object", "type": "object",
"required": [ "required": [
"enable", "enable",
"show_network_activity", "show_activity",
"show_total_data_transmitted" "show_total_activity"
], ],
"properties": { "properties": {
"activity_left_padding": {
"description": "Characters to reserve for received and transmitted activity",
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"auto_select": {
"description": "Select when the value is over a limit (1MiB is 1048576 bytes (1024*1024))",
"type": "object",
"properties": {
"received_over": {
"description": "Select the received data when it's over this value",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"total_received_over": {
"description": "Select the total received data when it's over this value",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"total_transmitted_over": {
"description": "Select the total transmitted data when it's over this value",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"transmitted_over": {
"description": "Select the transmitted data when it's over this value",
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
}
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -2324,22 +2438,16 @@
} }
] ]
}, },
"network_activity_fill_characters": { "show_activity": {
"description": "Characters to reserve for network activity data", "description": "Show received and transmitted activity",
"type": "integer", "type": "boolean"
"format": "uint",
"minimum": 0.0
}, },
"show_default_interface": { "show_default_interface": {
"description": "Show default interface", "description": "Show default interface",
"type": "boolean" "type": "boolean"
}, },
"show_network_activity": { "show_total_activity": {
"description": "Show network activity", "description": "Show total received and transmitted activity",
"type": "boolean"
},
"show_total_data_transmitted": {
"description": "Show total data transmitted",
"type": "boolean" "type": "boolean"
} }
} }
@@ -2359,6 +2467,18 @@
"enable" "enable"
], ],
"properties": { "properties": {
"auto_hide_under": {
"description": "Hide when the current percentage is under this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"auto_select_over": {
"description": "Select when the current percentage is over this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -2893,6 +3013,12 @@
"enable" "enable"
], ],
"properties": { "properties": {
"auto_select_under": {
"description": "Select when the current percentage is under this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -2957,6 +3083,12 @@
"enable" "enable"
], ],
"properties": { "properties": {
"auto_select_over": {
"description": "Select when the current percentage is over this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -3619,6 +3751,12 @@
"enable" "enable"
], ],
"properties": { "properties": {
"auto_select_over": {
"description": "Select when the current percentage is over this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -3677,10 +3815,46 @@
"type": "object", "type": "object",
"required": [ "required": [
"enable", "enable",
"show_network_activity", "show_activity",
"show_total_data_transmitted" "show_total_activity"
], ],
"properties": { "properties": {
"activity_left_padding": {
"description": "Characters to reserve for received and transmitted activity",
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"auto_select": {
"description": "Select when the value is over a limit (1MiB is 1048576 bytes (1024*1024))",
"type": "object",
"properties": {
"received_over": {
"description": "Select the received data when it's over this value",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"total_received_over": {
"description": "Select the total received data when it's over this value",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"total_transmitted_over": {
"description": "Select the total transmitted data when it's over this value",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"transmitted_over": {
"description": "Select the transmitted data when it's over this value",
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
}
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -3724,22 +3898,16 @@
} }
] ]
}, },
"network_activity_fill_characters": { "show_activity": {
"description": "Characters to reserve for network activity data", "description": "Show received and transmitted activity",
"type": "integer", "type": "boolean"
"format": "uint",
"minimum": 0.0
}, },
"show_default_interface": { "show_default_interface": {
"description": "Show default interface", "description": "Show default interface",
"type": "boolean" "type": "boolean"
}, },
"show_network_activity": { "show_total_activity": {
"description": "Show network activity", "description": "Show total received and transmitted activity",
"type": "boolean"
},
"show_total_data_transmitted": {
"description": "Show total data transmitted",
"type": "boolean" "type": "boolean"
} }
} }
@@ -3759,6 +3927,18 @@
"enable" "enable"
], ],
"properties": { "properties": {
"auto_hide_under": {
"description": "Hide when the current percentage is under this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"auto_select_over": {
"description": "Select when the current percentage is over this value [[1-100]]",
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"data_refresh_interval": { "data_refresh_interval": {
"description": "Data refresh interval (default: 10 seconds)", "description": "Data refresh interval (default: 10 seconds)",
"type": "integer", "type": "integer",
@@ -4035,6 +4215,68 @@
"Crust" "Crust"
] ]
}, },
"auto_select_fill": {
"type": "string",
"enum": [
"Rosewater",
"Flamingo",
"Pink",
"Mauve",
"Red",
"Maroon",
"Peach",
"Yellow",
"Green",
"Teal",
"Sky",
"Sapphire",
"Blue",
"Lavender",
"Text",
"Subtext1",
"Subtext0",
"Overlay2",
"Overlay1",
"Overlay0",
"Surface2",
"Surface1",
"Surface0",
"Base",
"Mantle",
"Crust"
]
},
"auto_select_text": {
"type": "string",
"enum": [
"Rosewater",
"Flamingo",
"Pink",
"Mauve",
"Red",
"Maroon",
"Peach",
"Yellow",
"Green",
"Teal",
"Sky",
"Sapphire",
"Blue",
"Lavender",
"Text",
"Subtext1",
"Subtext0",
"Overlay2",
"Overlay1",
"Overlay0",
"Surface2",
"Surface1",
"Surface0",
"Base",
"Mantle",
"Crust"
]
},
"name": { "name": {
"description": "Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)", "description": "Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)",
"type": "string", "type": "string",
@@ -4082,6 +4324,48 @@
"Base0F" "Base0F"
] ]
}, },
"auto_select_fill": {
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"auto_select_text": {
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"name": { "name": {
"description": "Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)", "description": "Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)",
"type": "string", "type": "string",
@@ -4394,6 +4678,48 @@
"Base0F" "Base0F"
] ]
}, },
"auto_select_fill": {
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"auto_select_text": {
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"colours": { "colours": {
"description": "Colours of the custom Base16 theme palette", "description": "Colours of the custom Base16 theme palette",
"type": "object", "type": "object",