Skip to content

Commit 15ebc28

Browse files
committed
- added filter button
- added theming with config
1 parent b782371 commit 15ebc28

7 files changed

Lines changed: 357 additions & 76 deletions

File tree

src/app/mod.rs

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub struct Theme {
5151

5252
impl Theme {
5353
/// Dark default theme.
54+
#[allow(dead_code)]
5455
pub fn dark() -> Self {
5556
Self {
5657
text: Color::Gray,
@@ -65,6 +66,144 @@ impl Theme {
6566
highlight_bg: Color::Reset,
6667
}
6768
}
69+
70+
/// Catppuccin Mocha theme defaults.
71+
pub fn mocha() -> Self {
72+
// Palette reference: https://github.com/catppuccin/catppuccin
73+
Self {
74+
// text & neutrals
75+
text: Color::Rgb(0xcd, 0xd6, 0xf4), // text
76+
_muted: Color::Rgb(0x7f, 0x84, 0x9c), // overlay1
77+
// accents and chrome
78+
title: Color::Rgb(0xcb, 0xa6, 0xf7), // mauve
79+
border: Color::Rgb(0x58, 0x5b, 0x70), // surface2
80+
header_bg: Color::Rgb(0x31, 0x32, 0x44), // surface0
81+
header_fg: Color::Rgb(0xb4, 0xbe, 0xfe), // lavender
82+
status_bg: Color::Rgb(0x45, 0x47, 0x5a), // surface1
83+
status_fg: Color::Rgb(0xcd, 0xd6, 0xf4), // text
84+
highlight_fg: Color::Rgb(0xf9, 0xe2, 0xaf), // yellow
85+
highlight_bg: Color::Rgb(0x45, 0x47, 0x5a), // surface1
86+
}
87+
}
88+
89+
/// Load theme from a simple key=value file. Unknown or missing keys fall back to `mocha`.
90+
pub fn from_file(path: &str) -> Option<Self> {
91+
let contents = std::fs::read_to_string(path).ok()?;
92+
let mut theme = Self::mocha();
93+
94+
for raw_line in contents.lines() {
95+
let line = raw_line.trim();
96+
if line.is_empty() || line.starts_with('#') {
97+
continue;
98+
}
99+
let mut parts = line.splitn(2, '=');
100+
let key = parts.next().map(|s| s.trim()).unwrap_or("");
101+
let val = parts.next().map(|s| s.trim()).unwrap_or("");
102+
if key.is_empty() || val.is_empty() {
103+
continue;
104+
}
105+
if let Some(color) = Self::parse_color(val) {
106+
match key {
107+
"text" => theme.text = color,
108+
"muted" | "_muted" => theme._muted = color,
109+
"title" => theme.title = color,
110+
"border" => theme.border = color,
111+
"header_bg" => theme.header_bg = color,
112+
"header_fg" => theme.header_fg = color,
113+
"status_bg" => theme.status_bg = color,
114+
"status_fg" => theme.status_fg = color,
115+
"highlight_fg" => theme.highlight_fg = color,
116+
"highlight_bg" => theme.highlight_bg = color,
117+
_ => {}
118+
}
119+
}
120+
}
121+
122+
Some(theme)
123+
}
124+
125+
/// Parse a color from hex ("#RRGGBB" or "RRGGBB") or special names: "reset".
126+
fn parse_color(s: &str) -> Option<Color> {
127+
let t = s.trim();
128+
let lower = t.to_ascii_lowercase();
129+
if lower == "reset" {
130+
return Some(Color::Reset);
131+
}
132+
let hex = if let Some(h) = lower.strip_prefix('#') { h } else { lower.as_str() };
133+
if hex.len() == 6 {
134+
if let (Ok(r), Ok(g), Ok(b)) = (
135+
u8::from_str_radix(&hex[0..2], 16),
136+
u8::from_str_radix(&hex[2..4], 16),
137+
u8::from_str_radix(&hex[4..6], 16),
138+
) {
139+
return Some(Color::Rgb(r, g, b));
140+
}
141+
}
142+
None
143+
}
144+
145+
/// Persist the theme to a config file in key=value format.
146+
pub fn write_file(&self, path: &str) -> std::io::Result<()> {
147+
use std::fmt::Write as _;
148+
let mut buf = String::new();
149+
// Minimal header
150+
buf.push_str("# usrgrp-manager theme configuration\n");
151+
buf.push_str("# Colors: hex as #RRGGBB or RRGGBB, or 'reset'\n\n");
152+
153+
fn color_to_str(c: Color) -> String {
154+
match c {
155+
Color::Rgb(r, g, b) => format!("#{:02X}{:02X}{:02X}", r, g, b),
156+
Color::Reset => "reset".to_string(),
157+
// For named colors, emit a best-effort hex approximation
158+
Color::Black => "#000000".to_string(),
159+
Color::Red => "#FF0000".to_string(),
160+
Color::Green => "#00FF00".to_string(),
161+
Color::Yellow => "#FFFF00".to_string(),
162+
Color::Blue => "#0000FF".to_string(),
163+
Color::Magenta => "#FF00FF".to_string(),
164+
Color::Cyan => "#00FFFF".to_string(),
165+
Color::Gray => "#B3B3B3".to_string(),
166+
Color::DarkGray => "#4D4D4D".to_string(),
167+
Color::LightRed => "#FF6666".to_string(),
168+
Color::LightGreen => "#66FF66".to_string(),
169+
Color::LightYellow => "#FFFF66".to_string(),
170+
Color::LightBlue => "#6666FF".to_string(),
171+
Color::LightMagenta => "#FF66FF".to_string(),
172+
Color::LightCyan => "#66FFFF".to_string(),
173+
Color::White => "#FFFFFF".to_string(),
174+
Color::Indexed(i) => format!("index:{}", i),
175+
}
176+
}
177+
178+
let mut kv = |k: &str, v: Color| {
179+
let _ = writeln!(&mut buf, "{} = {}", k, color_to_str(v));
180+
};
181+
182+
kv("text", self.text);
183+
kv("muted", self._muted);
184+
kv("title", self.title);
185+
kv("border", self.border);
186+
kv("header_bg", self.header_bg);
187+
kv("header_fg", self.header_fg);
188+
kv("status_bg", self.status_bg);
189+
kv("status_fg", self.status_fg);
190+
kv("highlight_fg", self.highlight_fg);
191+
kv("highlight_bg", self.highlight_bg);
192+
193+
std::fs::write(path, buf)
194+
}
195+
196+
/// Ensure a config file exists; if missing, write one with the current default theme and return it.
197+
/// If present, load from it; on parse errors, return `mocha`.
198+
pub fn load_or_init(path: &str) -> Self {
199+
let p = std::path::Path::new(path);
200+
if p.exists() {
201+
return Self::from_file(path).unwrap_or_else(Self::mocha);
202+
}
203+
let t = Self::mocha();
204+
let _ = t.write_file(path);
205+
t
206+
}
68207
}
69208

70209
/// Modal dialog states for user and group actions.
@@ -73,6 +212,9 @@ pub enum ModalState {
73212
Actions {
74213
selected: usize,
75214
},
215+
FilterMenu {
216+
selected: usize,
217+
},
76218
ModifyMenu {
77219
selected: usize,
78220
},
@@ -167,6 +309,18 @@ pub enum ModifyField {
167309
Fullname,
168310
}
169311

312+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
313+
pub enum UsersFilter {
314+
OnlyUserIds, // uid >= 1000
315+
OnlySystemIds, // uid < 1000
316+
}
317+
318+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
319+
pub enum GroupsFilter {
320+
OnlyUserGids, // gid >= 1000
321+
OnlySystemGids, // gid < 1000
322+
}
323+
170324
/// Actions that require privileged changes, executed via `sys::SystemAdapter`.
171325
#[derive(Clone, Debug)]
172326
pub enum PendingAction {
@@ -254,6 +408,8 @@ pub struct AppState {
254408
pub modal: Option<ModalState>,
255409
pub users_focus: UsersFocus,
256410
pub sudo_password: Option<String>,
411+
pub users_filter: Option<UsersFilter>,
412+
pub groups_filter: Option<GroupsFilter>,
257413
}
258414

259415
impl AppState {
@@ -277,10 +433,12 @@ impl AppState {
277433
_table_state: TableState::default(),
278434
input_mode: InputMode::Normal,
279435
search_query: String::new(),
280-
theme: Theme::dark(),
436+
theme: Theme::load_or_init("theme.conf"),
281437
modal: None,
282438
users_focus: UsersFocus::UsersList,
283439
sudo_password: None,
440+
users_filter: None,
441+
groups_filter: None,
284442
}
285443
}
286444
}

0 commit comments

Comments
 (0)