Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions default_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
ascii = "PACMAN_DEFAULT"
ascii_color = "yellow"

# Stats to display. Use "title.{name}" to reference titles defined below.
# Available stats: installed, upgradable, last_update, download_size, installed_size,
# net_upgrade_size, orphaned_packages, cache_size, disk, mirror_url, mirror_health
stats = [
Expand All @@ -24,6 +23,26 @@ stats = [

[display.glyph]
glyph = ": "
color = "none"

################### COLORS ####################
# Global stat colors. All support color names, hex (#RRGGBB), or "none".
[display.colors]
label = "bright_yellow"
stat = "none"

# Per-stat color overrides (takes precedence over global)
# [display.colors.installed]
# label = "bright_green"
# stat = "white"

################### LABELS ####################
# Override label text for individual stats.
# Keys: installed, upgradable, last_update, download_size, installed_size,
# net_upgrade_size, orphaned_packages, cache_size, disk, mirror_url, mirror_health
# [display.labels]
# installed = "Pkgs"
# cache_size = "Cache"

################### TITLES ####################
# Define named titles under [display.titles.{name}]
Expand All @@ -49,25 +68,20 @@ style = "stacked"
width = "title"
line = "-"

# Example: divider with no text
# [display.titles.divider]
# text = ""
# style = "embedded"
# width = "content"
# line = "─"
# left_cap = "├"
# right_cap = "┤"
# padding = 2

# Example: footer
# [display.titles.footer]
# example embedded title
# [display.titles.header]
# text = "default"
# text_color = "bright_yellow"
# line_color = "none"
# style = "embedded"
# width = "content"
# line = "─"
# left_cap = ""
# right_cap = ""
# left_cap = ""
# right_cap = ""
# padding = 2


################### DISK ####################
[disk]
path = "/"
Expand Down
40 changes: 40 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,44 @@ impl Default for DiskConfig {
pub struct GlyphConfig {
#[serde(default = "default_glyph")]
pub glyph: String,
#[serde(default)]
pub color: String,
}

fn default_glyph() -> String {
": ".to_string()
}

#[derive(Deserialize, Default, Clone)]
pub struct StatColorOverride {
pub label: Option<String>,
pub stat: Option<String>,
}

#[derive(Deserialize, Clone)]
pub struct ColorsConfig {
#[serde(default = "default_label_color")]
pub label: String,
#[serde(default)]
pub stat: String,
#[serde(flatten)]
pub overrides: HashMap<String, StatColorOverride>,
}

fn default_label_color() -> String {
"bright_yellow".to_string()
}

impl Default for ColorsConfig {
fn default() -> Self {
ColorsConfig {
label: default_label_color(),
stat: String::new(),
overrides: HashMap::new(),
}
}
}

#[derive(Debug, Clone, Deserialize)]
pub struct TitleConfig {
#[serde(default = "default_title_text")]
Expand Down Expand Up @@ -170,6 +202,12 @@ pub struct DisplayConfig {
#[serde(default)]
pub glyph: GlyphConfig,

#[serde(default)]
pub colors: ColorsConfig,

#[serde(default)]
pub labels: HashMap<String, String>,

#[serde(default)]
pub title: TitleConfig,

Expand Down Expand Up @@ -209,6 +247,8 @@ impl Default for DisplayConfig {
ascii: default_ascii(),
ascii_color: default_ascii_color(),
glyph: GlyphConfig::default(),
colors: ColorsConfig::default(),
labels: HashMap::new(),
title: TitleConfig::default(),
titles: HashMap::new(),
}
Expand Down
17 changes: 17 additions & 0 deletions src/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ impl StatId {
}
}

pub fn config_key(&self) -> &'static str {
match self {
StatId::Title => "title",
StatId::Installed => "installed",
StatId::Upgradable => "upgradable",
StatId::LastUpdate => "last_update",
StatId::DownloadSize => "download_size",
StatId::InstalledSize => "installed_size",
StatId::NetUpgradeSize => "net_upgrade_size",
StatId::OrphanedPackages => "orphaned_packages",
StatId::CacheSize => "cache_size",
StatId::MirrorUrl => "mirror_url",
StatId::MirrorHealth => "mirror_health",
StatId::Disk => "disk",
}
}

pub fn label(&self) -> &'static str {
match self {
StatId::Title => "",
Expand Down
140 changes: 84 additions & 56 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,21 @@ pub fn resolve_title_text(title_config: &TitleConfig, pacman_version: &Option<St
}
}

fn resolve_label(stat_id: &StatId, config: &Config) -> String {
config
.display
.labels
.get(stat_id.config_key())
.cloned()
.unwrap_or_else(|| {
if *stat_id == StatId::Disk {
format!("Disk ({})", config.disk.path)
} else {
stat_id.label().to_string()
}
})
}

pub fn display_stats(stats: &PacmanStats, config: &Config) {
let glyph = &config.display.glyph.glyph;
let parsed_stats = config.display.parsed_stats();
Expand Down Expand Up @@ -213,11 +228,7 @@ pub fn display_stats(stats: &PacmanStats, config: &Config) {
}
StatIdOrTitle::Stat(stat_id) => {
if let Some(value) = stat_id.format_value(stats) {
let label = if *stat_id == StatId::Disk {
format!("Disk ({})", config.disk.path)
} else {
stat_id.label().to_string()
};
let label = resolve_label(stat_id, config);
println!("{}{}{}", label, glyph, value);
}
}
Expand Down Expand Up @@ -274,11 +285,7 @@ pub fn display_stats_with_graphics(stats: &PacmanStats, config: &Config) -> io::
let value = stat_id
.format_value(stats)
.unwrap_or_else(|| "-".to_string());
let label = if *stat_id == StatId::Disk {
format!("Disk ({})", config.disk.path)
} else {
stat_id.label().to_string()
};
let label = resolve_label(stat_id, config);
let line = format!("{}{}{}", label, glyph, value);
content_max_width = content_max_width.max(line.chars().count());
stat_lines_raw.push((*stat_id, line));
Expand Down Expand Up @@ -400,38 +407,57 @@ fn format_stat_with_colors(
config: &Config,
glyph: &str,
) -> String {
let label = resolve_label(&stat_id, config);
let colors = &config.display.colors;
let key = stat_id.config_key();

// Resolve label color: per-stat override > global
let label_color_str = colors
.overrides
.get(key)
.and_then(|o| o.label.as_deref())
.unwrap_or(&colors.label);
let label_color = parse_color(label_color_str);
let colored_label = match label_color {
Some(c) => format!("{}", label.bold().with(c)),
None => format!("{}", label.bold()),
};

// glyph color
let glyph_color = parse_color(&config.display.glyph.color);
let colored_glyph = match glyph_color {
Some(c) => format!("{}", glyph.with(c)),
None => glyph.to_string(),
};

if stat_id == StatId::MirrorHealth {
match (&stats.mirror_url, stats.mirror_sync_age_hours) {
(Some(_), Some(age)) => {
let label = StatId::MirrorHealth.label();
format!(
"{}{}{} (last sync {:.1} hours)",
label.bold().with(Yellow),
glyph,
"OK".green(),
age
)
}
(Some(_), None) => {
let label = StatId::MirrorHealth.label();
format!(
"{}{}{} - could not check sync status",
label.bold().with(Yellow),
glyph,
"Err".red()
)
}
(None, _) => {
let label = StatId::MirrorHealth.label();
format!(
"{}{}{} - no mirror found",
label.bold().with(Yellow),
glyph,
"Err".red()
)
}
}
let val_override = colors
.overrides
.get(key)
.and_then(|o| o.stat.as_deref())
.and_then(parse_color);
let value_str = match (&stats.mirror_url, stats.mirror_sync_age_hours) {
(Some(_), Some(age)) => match val_override {
Some(c) => format!("{} (last sync {:.1} hours)", "OK".with(c), age),
None => format!("{} (last sync {:.1} hours)", "OK".green(), age),
},
(Some(_), None) => match val_override {
Some(c) => format!("{} - could not check sync status", "Err".with(c)),
None => format!("{} - could not check sync status", "Err".red()),
},
(None, _) => match val_override {
Some(c) => format!("{} - no mirror found", "Err".with(c)),
None => format!("{} - no mirror found", "Err".red()),
},
};
format!("{}{}{}", colored_label, colored_glyph, value_str)
} else if stat_id == StatId::Disk {
// Per-stat value override replaces semantic percentage colors
let val_override = colors
.overrides
.get(key)
.and_then(|o| o.stat.as_deref())
.and_then(parse_color);
if let (Some(used), Some(total)) = (stats.disk_used_bytes, stats.disk_total_bytes) {
let used_gib = used as f64 / 1073741824.0;
let total_gib = total as f64 / 1073741824.0;
Expand All @@ -441,31 +467,33 @@ fn format_stat_with_colors(
0.0
};
let pct_str = format!("({:.0}%)", pct);
let colored_pct = if pct > 90.0 {
format!("{}", pct_str.red())
} else if pct >= 70.0 {
format!("{}", pct_str.yellow())
} else {
format!("{}", pct_str.green())
let colored_pct = match val_override {
Some(c) => format!("{}", pct_str.with(c)),
None if pct > 90.0 => format!("{}", pct_str.red()),
None if pct >= 70.0 => format!("{}", pct_str.yellow()),
None => format!("{}", pct_str.green()),
};
let label = format!("Disk ({})", config.disk.path);
format!(
"{}{}{:.2} GiB / {:.2} GiB {}",
label.bold().with(Yellow),
glyph,
used_gib,
total_gib,
colored_pct
colored_label, colored_glyph, used_gib, total_gib, colored_pct
)
} else {
let label = format!("Disk ({})", config.disk.path);
format!("{}{}-", label.bold().with(Yellow), glyph)
format!("{}{}-", colored_label, colored_glyph)
}
} else {
let label = stat_id.label();
let value = stat_id
.format_value(stats)
.unwrap_or_else(|| "-".to_string());
format!("{}{}{}", label.bold().with(Yellow), glyph, value)
let value_color_str = colors
.overrides
.get(key)
.and_then(|o| o.stat.as_deref())
.unwrap_or(&colors.stat);
let value_color = parse_color(value_color_str);
let colored_value = match value_color {
Some(c) => format!("{}", value.with(c)),
None => value,
};
format!("{}{}{}", colored_label, colored_glyph, colored_value)
}
}