Skip to content

Commit 359618d

Browse files
committed
Add cycling filter states and improve help system
- Add failed state to filter cycle (all→active→inactive→failed) - Reorganize help text with common commands first - Add detailed help popup with '?' key - Fix help popup focus issues - Update version to 1.1.0 - Translate all code comments to English
1 parent 4d2740e commit 359618d

File tree

6 files changed

+202
-28
lines changed

6 files changed

+202
-28
lines changed

.SRCINFO

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
pkgbase = systemd-manager-tui
22
pkgdesc = systemd manager tui
3-
pkgver = 1.0.1
3+
pkgver = 1.1.0
44
pkgrel = 1
55
url = https://github.com/matheus-git/systemd-manager-tui
66
arch = x86_64

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "systemd-manager-tui"
3-
version = "1.0.9"
3+
version = "1.1.0"
44
description = "TUI for managing systemd services"
55
authors = ["Matheus-git <[email protected]>"]
66
license = "MIT"

PKGBUILD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Mantainer: matheus-git <[email protected]>
22
pkgname=systemd-manager-tui
3-
pkgver=1.0.1
3+
pkgver=1.1.0
44
pkgrel=1
55
pkgdesc="systemd manager tui"
66
arch=('x86_64')

src/terminal/app.rs

Lines changed: 126 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ pub enum Actions {
4747
Filter(String),
4848
UpdateIgnoreListKeys(bool),
4949
EditCurrentService,
50-
ServiceAction(ServiceAction)
50+
ServiceAction(ServiceAction),
51+
ShowHelp,
5152
}
5253

5354
pub enum AppEvent {
@@ -84,6 +85,7 @@ pub struct App {
8485
event_rx: Receiver<AppEvent>,
8586
event_tx: Sender<AppEvent>,
8687
selected_tab_index: usize,
88+
show_help: bool,
8789
}
8890

8991
impl App {
@@ -108,6 +110,7 @@ impl App {
108110
event_rx,
109111
event_tx,
110112
selected_tab_index: 0,
113+
show_help: false,
111114
}
112115
}
113116

@@ -161,18 +164,36 @@ fn spawn_key_event_listener(&self) {
161164
match self.event_rx.recv()? {
162165
AppEvent::Key(key) => match self.status {
163166
Status::Log => {
164-
self.on_key_event(key);
165-
log.on_key_event(key)
167+
if self.show_help {
168+
self.show_help = false;
169+
} else {
170+
self.on_key_event(key);
171+
log.on_key_event(key);
172+
}
166173
}
167174
Status::List => {
168-
self.on_key_event(key);
169-
self.on_key_horizontal_event(key, filter.input_mode == InputMode::Editing);
170-
table_service.on_key_event(key);
171-
filter.on_key_event(key);
175+
if self.show_help {
176+
self.show_help = false;
177+
// Ensure the table is active and can receive key events
178+
table_service.set_ignore_key_events(false);
179+
// If no item is selected and list is not empty, select first item
180+
if table_service.table_state.selected().is_none() && !table_service.is_filtered_list_empty() {
181+
table_service.set_selected_index(0);
182+
}
183+
} else {
184+
self.on_key_event(key);
185+
self.on_key_horizontal_event(key, filter.input_mode == InputMode::Editing);
186+
table_service.on_key_event(key);
187+
filter.on_key_event(key);
188+
}
172189
}
173190
Status::Details => {
174-
self.on_key_event(key);
175-
details.on_key_event(key);
191+
if self.show_help {
192+
self.show_help = false;
193+
} else {
194+
self.on_key_event(key);
195+
details.on_key_event(key);
196+
}
176197
}
177198
},
178199
AppEvent::Action(Actions::ServiceAction(action)) => {
@@ -229,6 +250,9 @@ fn spawn_key_event_listener(&self) {
229250
AppEvent::Error(error_msg) => {
230251
self.error_popup(&mut terminal, error_msg)?;
231252
}
253+
AppEvent::Action(Actions::ShowHelp) => {
254+
self.show_help = !self.show_help;
255+
}
232256
}
233257
}
234258

@@ -296,6 +320,67 @@ fn spawn_key_event_listener(&self) {
296320
Ok(())
297321
}
298322

323+
fn draw_help_popup(&self, frame: &mut Frame, area: Rect) {
324+
let popup_width = std::cmp::min(80, area.width.saturating_sub(4));
325+
let popup_height = std::cmp::min(25, area.height.saturating_sub(4));
326+
327+
let popup_x = (area.width.saturating_sub(popup_width)) / 2;
328+
let popup_y = (area.height.saturating_sub(popup_height)) / 2;
329+
330+
let popup_area = Rect::new(
331+
area.x + popup_x,
332+
area.y + popup_y,
333+
popup_width,
334+
popup_height,
335+
);
336+
337+
frame.render_widget(Clear, popup_area);
338+
339+
let text = vec![
340+
Line::from(vec![Span::styled(
341+
"SYSTEMD MANAGER TUI - HELP",
342+
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
343+
)]),
344+
Line::from(""),
345+
Line::from(vec![Span::styled("Navigation:", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))]),
346+
Line::from(" ↑/k - Move up ↓/j - Move down"),
347+
Line::from(" ←/h - Previous tab →/l - Next tab"),
348+
Line::from(" PageUp/PageDown - Jump 10 items"),
349+
Line::from(""),
350+
Line::from(vec![Span::styled("Service Control:", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))]),
351+
Line::from(" s - Start service x - Stop service"),
352+
Line::from(" r - Restart service"),
353+
Line::from(" e - Enable service d - Disable service"),
354+
Line::from(" m - Mask/Unmask service"),
355+
Line::from(""),
356+
Line::from(vec![Span::styled("View & Filter:", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))]),
357+
Line::from(" f - Toggle all/services filter"),
358+
Line::from(" a - Cycle filter (all→active→inactive→failed)"),
359+
Line::from(" u - Refresh service list"),
360+
Line::from(""),
361+
Line::from(vec![Span::styled("Information:", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))]),
362+
Line::from(" v - View service logs"),
363+
Line::from(" c - View unit file details"),
364+
Line::from(""),
365+
Line::from(vec![Span::styled(
366+
"Press ? or any key to close",
367+
Style::default().fg(Color::Gray),
368+
)]),
369+
];
370+
371+
let help_block = Paragraph::new(text)
372+
.block(
373+
Block::default()
374+
.borders(Borders::ALL)
375+
.border_style(Style::default().fg(Color::Cyan))
376+
.title("Help"),
377+
)
378+
.alignment(Alignment::Left)
379+
.wrap(ratatui::widgets::Wrap { trim: true });
380+
381+
frame.render_widget(help_block, popup_area);
382+
}
383+
299384
fn error_popup(&self, terminal: &mut DefaultTerminal, error_msg: String) -> Result<()> {
300385
let user_friendly_message = get_user_friendly_error(&error_msg);
301386

@@ -403,14 +488,45 @@ fn spawn_key_event_listener(&self) {
403488
])
404489
.areas(area);
405490

406-
let tabs = Tabs::new(vec!["System units","Session units"])
491+
let filter_state = table.get_active_filter_state();
492+
493+
let system_tab = if self.selected_tab_index == 0 {
494+
Line::from(vec![
495+
Span::raw("System units"),
496+
Span::styled(
497+
format!(" (Filter: {})", filter_state.as_str()),
498+
Style::default().fg(Color::Gray)
499+
)
500+
])
501+
} else {
502+
Line::from("System units")
503+
};
504+
505+
let session_tab = if self.selected_tab_index == 1 {
506+
Line::from(vec![
507+
Span::raw("Session units"),
508+
Span::styled(
509+
format!(" (Filter: {})", filter_state.as_str()),
510+
Style::default().fg(Color::Gray)
511+
)
512+
])
513+
} else {
514+
Line::from("Session units")
515+
};
516+
517+
let tabs = Tabs::new(vec![system_tab, session_tab])
407518
.select(self.selected_tab_index)
408519
.highlight_style(Style::default().fg(Color::Yellow));
409520

410521
frame.render_widget(tabs, tabs_box);
411522
filter.draw(frame, filter_box);
412523
table.render(frame, list_box);
413524
self.draw_shortcuts(frame, help_area_box, table.shortcuts());
525+
526+
// Show help popup if needed
527+
if self.show_help {
528+
self.draw_help_popup(frame, area);
529+
}
414530
})?;
415531

416532
Ok(())

src/terminal/components/list.rs

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,34 @@ fn generate_rows(services: &[Service]) -> Vec<Row<'static>> {
5151
.collect()
5252
}
5353

54+
#[derive(Clone, Copy)]
55+
pub enum ActiveFilterState {
56+
All,
57+
Active,
58+
Inactive,
59+
Failed,
60+
}
61+
62+
impl ActiveFilterState {
63+
pub fn next(self) -> Self {
64+
match self {
65+
ActiveFilterState::All => ActiveFilterState::Active,
66+
ActiveFilterState::Active => ActiveFilterState::Inactive,
67+
ActiveFilterState::Inactive => ActiveFilterState::Failed,
68+
ActiveFilterState::Failed => ActiveFilterState::All,
69+
}
70+
}
71+
72+
pub fn as_str(self) -> &'static str {
73+
match self {
74+
ActiveFilterState::All => "all",
75+
ActiveFilterState::Active => "active",
76+
ActiveFilterState::Inactive => "inactive",
77+
ActiveFilterState::Failed => "failed",
78+
}
79+
}
80+
}
81+
5482
pub enum ServiceAction {
5583
Start,
5684
Stop,
@@ -60,7 +88,6 @@ pub enum ServiceAction {
6088
RefreshAll,
6189
ToggleFilter,
6290
ToggleMask,
63-
ToggleActiveFilter,
6491
}
6592

6693
pub struct TableServices {
@@ -74,7 +101,7 @@ pub struct TableServices {
74101
sender: Sender<AppEvent>,
75102
usecase: Rc<RefCell<ServicesManager>>,
76103
filter_all: bool,
77-
show_active_only: bool,
104+
active_filter_state: ActiveFilterState,
78105
}
79106

80107
impl TableServices {
@@ -133,7 +160,7 @@ impl TableServices {
133160
ignore_key_events: false,
134161
usecase,
135162
filter_all,
136-
show_active_only: false,
163+
active_filter_state: ActiveFilterState::All,
137164
}
138165
}
139166

@@ -192,6 +219,21 @@ impl TableServices {
192219
self.filtered_services = self.filter(filter_text, self.services.clone());
193220
self.rows = generate_rows(&self.filtered_services.clone());
194221
self.table = self.table.clone().rows(self.rows.clone());
222+
223+
// If no item is selected and the list is not empty, select the first item
224+
if self.table_state.selected().is_none() && !self.filtered_services.is_empty() {
225+
self.table_state.select(Some(0));
226+
}
227+
// If the selected index is out of bounds, reset to first item or None
228+
else if let Some(selected) = self.table_state.selected() {
229+
if selected >= self.filtered_services.len() {
230+
if self.filtered_services.is_empty() {
231+
self.table_state.select(None);
232+
} else {
233+
self.table_state.select(Some(0));
234+
}
235+
}
236+
}
195237
}
196238

197239
fn fetch_services(&mut self) {
@@ -214,10 +256,11 @@ impl TableServices {
214256
.filter(|service| {
215257
let name = service.name();
216258
let name_matches = name.to_lowercase().contains(&lower_filter);
217-
let active_matches = if self.show_active_only {
218-
service.state().active() == "active"
219-
} else {
220-
true
259+
let active_matches = match self.active_filter_state {
260+
ActiveFilterState::All => true,
261+
ActiveFilterState::Active => service.state().active() == "active",
262+
ActiveFilterState::Inactive => service.state().active() != "active" && service.state().active() != "failed",
263+
ActiveFilterState::Failed => service.state().active() == "failed",
221264
};
222265
name_matches && active_matches
223266
})
@@ -261,13 +304,25 @@ impl TableServices {
261304
return;
262305
}
263306
KeyCode::Char('a') => {
264-
self.sender.send(AppEvent::Action(Actions::ServiceAction(ServiceAction::ToggleActiveFilter))).unwrap();
307+
self.active_filter_state = self.active_filter_state.next();
308+
self.refresh(self.old_filter_text.clone());
309+
// Select the first element only if the list is not empty
310+
if !self.filtered_services.is_empty() {
311+
self.table_state.select(Some(0));
312+
} else {
313+
self.table_state.select(None);
314+
}
315+
self.set_ignore_key_events(false);
265316
return;
266317
}
267318
KeyCode::Char('m') => {
268319
self.sender.send(AppEvent::Action(Actions::ServiceAction(ServiceAction::ToggleMask))).unwrap();
269320
return;
270321
}
322+
KeyCode::Char('?') => {
323+
self.sender.send(AppEvent::Action(Actions::ShowHelp)).unwrap();
324+
return;
325+
}
271326
_ => {}
272327
}
273328

@@ -373,11 +428,6 @@ impl TableServices {
373428
self.filter_all = !self.filter_all;
374429
self.fetch_and_refresh(self.old_filter_text.clone());
375430
},
376-
ServiceAction::ToggleActiveFilter => {
377-
self.table_state.select(Some(0));
378-
self.show_active_only = !self.show_active_only;
379-
self.refresh(self.old_filter_text.clone());
380-
},
381431
ServiceAction::RefreshAll => {
382432
self.fetch_services();
383433
self.fetch_and_refresh(self.old_filter_text.clone());
@@ -403,6 +453,14 @@ impl TableServices {
403453
}
404454
}
405455

456+
pub fn is_filtered_list_empty(&self) -> bool {
457+
self.filtered_services.is_empty()
458+
}
459+
460+
pub fn get_active_filter_state(&self) -> ActiveFilterState {
461+
self.active_filter_state
462+
}
463+
406464
pub fn shortcuts(&mut self) -> Vec<Line<'_>> {
407465
let mut help_text: Vec<Line<'_>> = Vec::new();
408466
if !self.ignore_key_events {
@@ -414,7 +472,7 @@ impl TableServices {
414472
)));
415473

416474
help_text.push(Line::from(
417-
"Navigate: ↑/↓ | Switch tab: ←/→ | List all: f | Active only: a | Start: s | Stop: x | Restart: r | Enable: e | Disable: d | Mask/Unmask: m | Refresh list: u | Log: v | Unit File: c"
475+
"Navigate: ↑/↓ | Switch tab: ←/→ | Start: s | Stop: x | Restart: r | Enable: e | Disable: d | Help: ? | List all: f | Filter: a | Mask/Unmask: m | Refresh: u | Log: v | Unit File: c"
418476
));
419477
}
420478

0 commit comments

Comments
 (0)