monitors flake works

This commit is contained in:
2025-08-11 11:52:50 -06:00
parent 47f72bc819
commit e0be16231e
3 changed files with 683 additions and 163 deletions

View File

@@ -1,68 +1,328 @@
use anyhow::Result;
use console::style;
use dialoguer::{theme::ColorfulTheme, Input, MultiSelect, Select};
use std::io;
use crossterm::{
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Clear, List, ListItem, Padding, Paragraph, Wrap},
Terminal,
};
use std::io::{self, stdout};
const APP_NAME: &str = "GNOME Monitor TUI";
fn theme() -> ColorfulTheme {
let mut t = ColorfulTheme::default();
// Slightly more vivid selection and success colors for a modern feel
t.values_style = t.values_style.bold();
t.active_item_style = t.active_item_style.bold();
// Customize prefixes for a cleaner, modern aesthetic
t.prompt_prefix = style("".to_string());
t.success_prefix = style("".to_string());
t.error_prefix = style("".to_string());
t.active_item_prefix = style("".to_string());
t.inactive_item_prefix = style(" ".to_string());
t.checked_item_prefix = style("".to_string());
t.unchecked_item_prefix = style("".to_string());
t.picked_item_prefix = style("".to_string());
t.unpicked_item_prefix = style(" ".to_string());
t
fn with_terminal<F, T>(mut f: F) -> Result<T>
where
F: FnMut(&mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<T>,
{
enable_raw_mode()?;
let mut out = stdout();
execute!(out, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(out);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
let res = f(&mut terminal);
disable_raw_mode()?;
// It's okay if leaving alternate screen fails after rendering; try best-effort
let _ = execute!(terminal.backend_mut(), LeaveAlternateScreen);
let _ = terminal.show_cursor();
res
}
fn center_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(area);
let vertical = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1]);
vertical[1]
}
pub fn msgbox(text: &str) {
let bar = "".repeat(64);
println!("\n{}\n{:<62}\n{}\n", bar, APP_NAME, bar);
println!("{}\n", text);
let _ = prompt_enter();
let _ = with_terminal(|t| {
loop {
t.draw(|f| {
let area = center_rect(80, 50, f.size());
let block = Block::default()
.title(APP_NAME)
.borders(Borders::ALL)
.padding(Padding::uniform(1));
let content = Paragraph::new(Text::from(text.to_string()))
.block(block)
.wrap(Wrap { trim: true })
.alignment(Alignment::Left);
let hint = Paragraph::new(Line::from(vec![
Span::raw("Press "),
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to continue"),
]))
.alignment(Alignment::Center);
f.render_widget(Clear, area);
f.render_widget(content, area);
let hint_area = Rect {
x: area.x,
y: area.y + area.height.saturating_sub(2),
width: area.width,
height: 1,
};
f.render_widget(hint, hint_area);
})?;
if let Event::Key(KeyEvent { code, .. }) = event::read()? {
match code {
KeyCode::Enter | KeyCode::Esc => break,
_ => {}
}
}
}
Ok(())
});
}
pub fn inputbox(prompt: &str, default: &str) -> Result<String> {
Ok(Input::with_theme(&theme())
.with_prompt(prompt)
.default(default.to_string())
.interact_text()?)
let mut value = default.to_string();
with_terminal(|t| {
loop {
t.draw(|f| {
let area = center_rect(80, 40, f.size());
let block = Block::default()
.title(APP_NAME)
.borders(Borders::ALL)
.padding(Padding::uniform(1));
let mut lines = Vec::new();
lines.push(Line::from(Span::styled(
prompt,
Style::default().add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(""));
lines.push(Line::from(value.clone()));
lines.push(Line::from(""));
lines.push(Line::from("Enter to accept, Esc to cancel"));
let content = Paragraph::new(Text::from(lines))
.block(block)
.wrap(Wrap { trim: false })
.alignment(Alignment::Left);
f.render_widget(Clear, area);
f.render_widget(content, area);
// place cursor at input line end
let cursor_x = area.x + value.len() as u16;
let cursor_y = area.y + 2; // third line
f.set_cursor(
cursor_x.min(area.x + area.width.saturating_sub(2)),
cursor_y,
);
})?;
match event::read()? {
Event::Key(KeyEvent {
code, modifiers, ..
}) => match code {
KeyCode::Enter => break,
KeyCode::Esc => {
value = default.to_string();
break;
}
KeyCode::Backspace => {
value.pop();
}
KeyCode::Char('u') if modifiers.contains(KeyModifiers::CONTROL) => {
value.clear();
}
KeyCode::Char(c) => value.push(c),
_ => {}
},
_ => {}
}
}
Ok(value.clone())
})
}
pub fn menu(prompt: &str, options: &[(String, String)]) -> Result<Option<String>> {
let items: Vec<String> = options.iter().map(|(_, d)| d.clone()).collect();
let idx = Select::with_theme(&theme())
.with_prompt(prompt)
.items(&items)
.default(0)
.interact_opt()?;
Ok(idx.map(|i| options[i].0.clone()))
if options.is_empty() {
return Ok(None);
}
let mut selected: usize = 0;
with_terminal(|t| {
loop {
t.draw(|f| {
let area = center_rect(80, 70, f.size());
let block = Block::default()
.title(APP_NAME)
.borders(Borders::ALL)
.padding(Padding::uniform(1));
let mut text = Vec::new();
text.push(Line::from(Span::styled(
prompt,
Style::default().add_modifier(Modifier::BOLD),
)));
text.push(Line::from(""));
let items: Vec<ListItem> = options
.iter()
.enumerate()
.map(|(i, (_, d))| {
let prefix = if i == selected { "" } else { " " };
ListItem::new(format!("{}{}", prefix, d))
})
.collect();
let list = List::new(items)
.block(block)
.highlight_symbol("")
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
f.render_widget(Clear, area);
// Render prompt at top area
let top = Rect {
x: area.x,
y: area.y,
width: area.width,
height: 2,
};
let prompt_para = Paragraph::new(Text::from(text)).alignment(Alignment::Left);
f.render_widget(prompt_para, top);
// Render list below
let list_area = Rect {
x: area.x,
y: area.y + 2,
width: area.width,
height: area.height.saturating_sub(2),
};
f.render_widget(list, list_area);
})?;
if let Event::Key(KeyEvent { code, .. }) = event::read()? {
match code {
KeyCode::Up => {
if selected > 0 {
selected -= 1;
}
}
KeyCode::Down => {
if selected + 1 < options.len() {
selected += 1;
}
}
KeyCode::Enter => return Ok(Some(options[selected].0.clone())),
KeyCode::Esc => return Ok(None),
_ => {}
}
}
}
})
}
pub fn checklist(prompt: &str, items: &[(String, String, bool)]) -> Result<Vec<String>> {
let labels: Vec<String> = items
.iter()
.map(|(k, d, _)| format!("{} {}", k, d))
.collect();
let defaults: Vec<bool> = items.iter().map(|(_, _, on)| *on).collect();
let chosen = MultiSelect::with_theme(&theme())
.with_prompt(prompt)
.items(&labels)
.defaults(&defaults)
.interact()?;
Ok(chosen.into_iter().map(|i| items[i].0.clone()).collect())
}
if items.is_empty() {
return Ok(vec![]);
}
let mut active: usize = 0;
let mut checked: Vec<bool> = items.iter().map(|(_, _, on)| *on).collect();
pub fn prompt_enter() -> Result<()> {
let mut s = String::new();
io::stdin().read_line(&mut s)?;
Ok(())
with_terminal(|t| loop {
t.draw(|f| {
let area = center_rect(80, 80, f.size());
let block = Block::default()
.title(APP_NAME)
.borders(Borders::ALL)
.padding(Padding::uniform(1));
let prompt_para = Paragraph::new(Text::from(vec![
Line::from(Span::styled(
prompt,
Style::default().add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from("Use ↑/↓ to move, Space to toggle, Enter to confirm, Esc to cancel"),
]))
.alignment(Alignment::Left);
let list_items: Vec<ListItem> = items
.iter()
.enumerate()
.map(|(i, (k, d, _))| {
let mark = if checked[i] { "[x]" } else { "[ ]" };
let cursor = if i == active { "" } else { " " };
ListItem::new(format!("{} {} {} {}", cursor, mark, k, d))
})
.collect();
let list = List::new(list_items)
.block(block)
.highlight_symbol("")
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
f.render_widget(Clear, area);
let top = Rect {
x: area.x,
y: area.y,
width: area.width,
height: 3,
};
f.render_widget(prompt_para, top);
let list_area = Rect {
x: area.x,
y: area.y + 3,
width: area.width,
height: area.height.saturating_sub(3),
};
f.render_widget(list, list_area);
})?;
if let Event::Key(KeyEvent { code, .. }) = event::read()? {
match code {
KeyCode::Up => {
if active > 0 {
active -= 1;
}
}
KeyCode::Down => {
if active + 1 < items.len() {
active += 1;
}
}
KeyCode::Char(' ') => {
checked[active] = !checked[active];
}
KeyCode::Enter => {
let out: Vec<String> = items
.iter()
.enumerate()
.filter_map(
|(i, (k, _, _))| if checked[i] { Some(k.clone()) } else { None },
)
.collect();
return Ok(out);
}
KeyCode::Esc => return Ok(vec![]),
_ => {}
}
}
})
}