use crate::roles::{self, Role}; use anyhow::Result; use crossterm::event::{self, Event, KeyCode}; use ratatui::Frame; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::style::Style; use ratatui::text::{Line, Span}; use ratatui::widgets::Paragraph; use sysop_tui::{Footer, GlobalAction, classify, hint, layout, palette, selection, style}; enum View { List { selected: usize }, Detail { idx: usize }, } pub fn run(initial: Option) -> Result { let roles = roles::load()?; let mut view = match initial.as_deref() { None => View::List { selected: 0 }, Some(name) => match roles::find(&roles, name) { Some(_) => View::Detail { idx: roles.iter().position(|r| r.name == name).unwrap(), }, None => { eprintln!("sysop: unknown role: {name}"); eprintln!(" run `sysop info` for the list"); return Ok(1); } }, }; let mut terminal = ratatui::init(); let result = event_loop(&mut terminal, &roles, &mut view); ratatui::restore(); result.map(|_| 0) } fn event_loop( terminal: &mut ratatui::DefaultTerminal, roles: &[Role], view: &mut View, ) -> Result<()> { loop { terminal.draw(|frame| draw(frame, roles, view))?; let Event::Key(key) = event::read()? else { continue; }; let action = classify(key); match view { View::List { selected } => match action { GlobalAction::Quit => return Ok(()), GlobalAction::Passthrough => match key.code { KeyCode::Char('j') | KeyCode::Down if *selected + 1 < roles.len() => { *selected += 1; } KeyCode::Char('k') | KeyCode::Up => { *selected = selected.saturating_sub(1); } KeyCode::Enter => { *view = View::Detail { idx: *selected }; } _ => {} }, _ => {} }, View::Detail { idx } => match action { GlobalAction::Quit => return Ok(()), GlobalAction::Back => { *view = View::List { selected: *idx }; } GlobalAction::Passthrough => match key.code { KeyCode::Char('j') | KeyCode::Down if *idx + 1 < roles.len() => { *idx += 1; } KeyCode::Char('k') | KeyCode::Up => { *idx = idx.saturating_sub(1); } _ => {} }, _ => {} }, } } } fn draw(frame: &mut Frame, roles: &[Role], view: &View) { let [body, foot] = layout::split(frame.area()); match view { View::List { selected } => { frame.render_widget(list_widget(roles, *selected), body); frame.render_widget( &Footer::new([ hint("j/k", "move"), hint("Enter", "details"), hint("q", "quit"), ]), foot, ); } View::Detail { idx } => { draw_detail(frame, &roles[*idx], body); frame.render_widget( &Footer::new([ hint("j/k", "next/prev role"), hint("Esc", "back"), hint("q", "quit"), ]), foot, ); } } } fn list_widget(roles: &[Role], selected: usize) -> Paragraph<'_> { let name_w = roles.iter().map(|r| r.name.len()).max().unwrap_or(0); let lines: Vec = roles .iter() .enumerate() .map(|(i, r)| { let is_sel = i == selected; let marker = if is_sel { selection::MARKER } else { " " }; let line = Line::from(vec![ Span::raw(format!(" {marker} ")), Span::raw(format!("{: = Vec::new(); lines.push(field_line("role", &role.name, label_w)); lines.push(field_line("tool", &role.tool, label_w)); lines.push(field_line( "packages", &role.packages.join(", "), label_w, )); if role.config_paths.is_empty() { lines.push(field_line("config", "(none)", label_w)); } else { lines.push(field_line("config", &role.config_paths[0], label_w)); for extra in &role.config_paths[1..] { lines.push(continuation_line(extra, label_w)); } } lines.push(Line::from("")); lines.push(field_line("description", &role.description, label_w)); let para = Paragraph::new(lines).style(Style::default().bg(palette::BG).fg(palette::FG)); frame.render_widget(para, body); } fn field_line(label: &str, value: &str, label_w: usize) -> Line<'static> { Line::from(vec![ style::dim(format!("{: Line<'static> { Line::from(vec![ Span::raw(" ".repeat(label_w + 2)), style::fg(value.to_string()), ]) }