diff --git a/src/tui/app.rs b/src/tui/app.rs index 087d17a..3739bbf 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -466,6 +466,26 @@ impl App { self.reset_list_selection(); } + /// Cycle to the next view (Tab key) + pub fn next_view(&mut self) { + let views = View::all(); + let current_idx = views.iter().position(|v| *v == self.view).unwrap_or(0); + let next_idx = (current_idx + 1) % views.len(); + self.switch_view(views[next_idx]); + } + + /// Cycle to the previous view (Shift+Tab) + pub fn prev_view(&mut self) { + let views = View::all(); + let current_idx = views.iter().position(|v| *v == self.view).unwrap_or(0); + let prev_idx = if current_idx == 0 { + views.len() - 1 + } else { + current_idx - 1 + }; + self.switch_view(views[prev_idx]); + } + /// Reset list selection to first item pub fn reset_list_selection(&mut self) { let count = self.current_list_len(); diff --git a/src/tui/event.rs b/src/tui/event.rs index 5d3e76b..e48be3f 100644 --- a/src/tui/event.rs +++ b/src/tui/event.rs @@ -91,11 +91,11 @@ pub fn is_down(key: &KeyEvent) -> bool { } pub fn is_page_up(key: &KeyEvent) -> bool { - key.code == KeyCode::PageUp || key.is_ctrl('u') + key.code == KeyCode::PageUp } pub fn is_page_down(key: &KeyEvent) -> bool { - key.code == KeyCode::PageDown || key.is_ctrl('d') + key.code == KeyCode::PageDown } pub fn is_home(key: &KeyEvent) -> bool { @@ -117,3 +117,7 @@ pub fn is_back(key: &KeyEvent) -> bool { pub fn is_tab(key: &KeyEvent) -> bool { key.code == KeyCode::Tab } + +pub fn is_backtab(key: &KeyEvent) -> bool { + key.code == KeyCode::BackTab +} diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 0de6dfb..c7a61cc 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -20,6 +20,7 @@ use crossterm::{ use ratatui::{ backend::CrosstermBackend, layout::{Constraint, Direction, Layout, Rect}, + style::Modifier, text::{Line, Span}, widgets::Paragraph, Frame, Terminal, @@ -30,7 +31,7 @@ use crate::error::Result; use crate::paths::Paths; use app::{App, Mode, Popup, PopupAction, TaskKind, View}; -use event::{is_back, is_down, is_end, is_home, is_page_down, is_page_up, is_select, is_tab, is_up, Event, EventHandler, KeyEventExt}; +use event::{is_back, is_backtab, is_down, is_end, is_home, is_page_down, is_page_up, is_select, is_tab, is_up, Event, EventHandler, KeyEventExt}; use widgets::{render_confirm, render_help, render_input, render_message, render_script_selector}; /// Run the TUI application @@ -97,19 +98,28 @@ pub fn run() -> Result<()> { fn render(f: &mut Frame, app: &mut App) { let size = f.area(); - // Main layout: header, content, footer + // Fill entire screen with base background color + let bg = ratatui::widgets::Block::default() + .style(ratatui::style::Style::default().bg(theme::BASE)); + f.render_widget(bg, size); + + // Main layout: header, spacer, content, status, footer let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(1), // Header + Constraint::Length(1), // Spacer Constraint::Min(0), // Content + Constraint::Length(1), // Status line Constraint::Length(1), // Footer ]) .split(size); render_header(f, chunks[0], app); - render_content(f, chunks[1], app); - render_footer(f, chunks[2], app); + // chunks[1] is empty spacer + render_content(f, chunks[2], app); + render_status_line(f, chunks[3], app); + render_footer(f, chunks[4], app); // Render popup if any if let Some(popup) = &app.popup { @@ -117,35 +127,79 @@ fn render(f: &mut Frame, app: &mut App) { } } -/// Render the header bar +/// Render the header bar (Posting-style) fn render_header(f: &mut Frame, area: Rect, app: &App) { - let mut spans = vec![ - Span::styled(" empeve ", theme::title()), - Span::styled("│ ", theme::border()), + // Left side: app name + let left = vec![ + Span::styled(" empeve ", theme::title()), ]; - // View tabs - format as [D]ashboard [R]epos etc. + // Center: view tabs + let mut center = Vec::new(); for view in View::all() { let is_active = *view == app.view; let label = view.label(); - // Skip the first character of the label (it's the key) - let label_rest = &label[1..]; if is_active { - spans.push(Span::styled("[", theme::highlight())); - spans.push(Span::styled(view.key().to_string(), theme::highlight())); - spans.push(Span::styled("]", theme::highlight())); - spans.push(Span::styled(format!("{} ", label_rest), theme::highlight())); + center.push(Span::styled( + format!(" {} ", label), + ratatui::style::Style::default() + .fg(theme::BASE) + .bg(theme::MAGENTA) + .add_modifier(Modifier::BOLD), + )); } else { - spans.push(Span::styled("[", theme::text_secondary())); - spans.push(Span::styled(view.key().to_string(), theme::keybind())); - spans.push(Span::styled("]", theme::text_secondary())); - spans.push(Span::styled(format!("{} ", label_rest), theme::text_secondary())); + center.push(Span::styled( + format!(" {} ", label), + theme::text_muted(), + )); } } + // Right side: status info + let right = if let Some(ref task) = app.running_task { + vec![ + Span::styled(format!("{} ", app.spinner()), theme::accent()), + Span::styled(task.as_str(), theme::text_muted()), + Span::styled(" ", theme::text()), + ] + } else { + vec![ + Span::styled(format!("{} repos ", app.config.repos.len()), theme::text_muted()), + Span::styled("│ ", theme::border()), + Span::styled(format!("{} targets ", app.config.targets.len()), theme::text_muted()), + ] + }; + + // Calculate positions + let left_width: usize = left.iter().map(|s| s.width()).sum(); + let center_width: usize = center.iter().map(|s| s.width()).sum(); + let right_width: usize = right.iter().map(|s| s.width()).sum(); + let total_width = area.width as usize; + + // Build the header line with proper spacing + let mut spans = left; + + // Add spacing before center + let center_start = (total_width.saturating_sub(center_width)) / 2; + let left_padding = center_start.saturating_sub(left_width); + if left_padding > 0 { + spans.push(Span::raw(" ".repeat(left_padding))); + } + + spans.extend(center); + + // Add spacing before right + let current_width: usize = spans.iter().map(|s| s.width()).sum(); + let right_padding = total_width.saturating_sub(current_width).saturating_sub(right_width); + if right_padding > 0 { + spans.push(Span::raw(" ".repeat(right_padding))); + } + + spans.extend(right); + let header = Paragraph::new(Line::from(spans)) - .style(ratatui::style::Style::default().bg(theme::SURFACE0)); + .style(ratatui::style::Style::default().bg(theme::MANTLE)); f.render_widget(header, area); } @@ -161,45 +215,100 @@ fn render_content(f: &mut Frame, area: Rect, app: &mut App) { } } -/// Render the footer bar with keybindings hint -fn render_footer(f: &mut Frame, area: Rect, app: &App) { - let mode_hint = match app.mode { - Mode::Normal => "", - Mode::Filter => "FILTER: ", - Mode::Popup => "", - }; - - // Status message with spinner support +/// Render the status line above footer +fn render_status_line(f: &mut Frame, area: Rect, app: &App) { let status = if let Some((msg, is_error)) = &app.status_message { - if *is_error { - Span::styled(msg.as_str(), theme::error()) + let style = if *is_error { + theme::error() } else if app.is_busy() { - Span::styled(msg.as_str(), theme::accent()) + theme::accent() } else { - Span::styled(msg.as_str(), theme::success()) - } + theme::success() + }; + Span::styled(format!(" {}", msg), style) } else { - Span::styled("Ready", theme::text_muted()) + Span::styled(" Ready", theme::text_muted()) }; - let hints = Line::from(vec![ - Span::styled(" ", theme::text()), - Span::styled(mode_hint, theme::warning()), - Span::styled("j/k", theme::keybind()), - Span::styled(":move ", theme::text_muted()), - Span::styled("/", theme::keybind()), - Span::styled(":filter ", theme::text_muted()), - Span::styled("?", theme::keybind()), - Span::styled(":help ", theme::text_muted()), - Span::styled("q", theme::keybind()), - Span::styled(":quit ", theme::text_muted()), - Span::styled("│ ", theme::border()), - status, - ]); - - let footer = Paragraph::new(hints) + let line = Paragraph::new(Line::from(status)) .style(ratatui::style::Style::default().bg(theme::SURFACE0)); + f.render_widget(line, area); +} + +/// Render the footer bar with keybindings hint (Posting-style) +fn render_footer(f: &mut Frame, area: Rect, app: &App) { + // Helper for Ctrl+key combinations (shown as ^key) + let ctrl_key = |key: &str, desc: &str| -> Vec> { + vec![ + Span::styled(format!("^{}", key), theme::keybind()), + Span::styled(format!(" {} ", desc), theme::text_muted()), + ] + }; + + // Helper for regular key presses + let key = |key: &str, desc: &str| -> Vec> { + vec![ + Span::styled(key.to_string(), theme::keybind()), + Span::styled(format!(" {} ", desc), theme::text_muted()), + ] + }; + + // Build left side with context-sensitive shortcuts + let mut left_spans: Vec = vec![Span::raw(" ")]; + + // Mode-specific hints + match app.mode { + Mode::Filter => { + left_spans.extend(key("Esc", "Cancel")); + left_spans.extend(key("Enter", "Apply")); + } + Mode::Popup => { + // Popup has its own hints + } + Mode::Normal => { + // Global navigation + left_spans.extend(key("j/k", "Move")); + left_spans.extend(key("/", "Filter")); + left_spans.extend(key("Tab", "Views")); + + // View-specific hints (^key for Ctrl, plain for regular) + match app.view { + View::Dashboard => { + left_spans.extend(ctrl_key("i", "Install")); + left_spans.extend(ctrl_key("u", "Update")); + left_spans.extend(ctrl_key("h", "Doctor")); + } + View::Repos => { + left_spans.extend(key("a", "Add")); + left_spans.extend(key("i", "Install")); + left_spans.extend(key("u", "Update")); + left_spans.extend(key("s", "Scripts")); + } + View::Scripts => { + left_spans.extend(key("e", "Toggle")); + left_spans.extend(key("t", "Target")); + left_spans.extend(key("r", "Remove")); + } + View::Catalog => { + left_spans.extend(key("a", "Add")); + left_spans.extend(key("Enter", "Install")); + } + View::Targets => { + left_spans.extend(key("a", "Add")); + left_spans.extend(key("c", "Create")); + left_spans.extend(key("e", "Toggle")); + } + } + + left_spans.extend(key("?", "Help")); + left_spans.extend(key("q", "Quit")); + } + } + + let footer = Paragraph::new(Line::from(left_spans)) + .style(ratatui::style::Style::default().bg(theme::MANTLE)); + f.render_widget(footer, area); } @@ -262,7 +371,12 @@ fn handle_input(app: &mut App, key: crossterm::event::KeyEvent) { } if is_tab(&key) { - app.toggle_focus(); + app.next_view(); + return; + } + + if is_backtab(&key) { + app.prev_view(); return; } @@ -417,19 +531,25 @@ fn handle_filter_input(app: &mut App, key: crossterm::event::KeyEvent) { } } +/// Check if key is Ctrl+char +fn is_ctrl(key: &crossterm::event::KeyEvent, c: char) -> bool { + use crossterm::event::KeyModifiers; + key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char(c) +} + /// Handle dashboard-specific input fn handle_dashboard_input(app: &mut App, key: crossterm::event::KeyEvent) { - // Quick actions from dashboard - if key.is_char('I') { + // Quick actions from dashboard (Ctrl+key combinations) + if is_ctrl(&key, 'i') { // Install all (non-blocking) app.spawn_task(TaskKind::InstallAll); - } else if key.is_char('U') { + } else if is_ctrl(&key, 'u') { // Update all (non-blocking) app.spawn_task(TaskKind::UpdateAll); - } else if key.is_char('L') { + } else if is_ctrl(&key, 'l') { // Lock versions app.spawn_task(TaskKind::Lock); - } else if key.is_char('X') { + } else if is_ctrl(&key, 'x') { // Clean orphaned - show confirmation with count let scan = ops::scan_orphaned(&app.config, &app.paths); if let Ok(result) = scan { @@ -447,7 +567,7 @@ fn handle_dashboard_input(app: &mut App, key: crossterm::event::KeyEvent) { } else { app.set_status("Failed to scan for orphaned items", true); } - } else if key.is_char('M') { + } else if is_ctrl(&key, 'm') { // Import scripts - show scan results let scan = ops::scan_importable(&app.config, &app.paths); if scan.scripts.is_empty() { @@ -467,14 +587,14 @@ fn handle_dashboard_input(app: &mut App, key: crossterm::event::KeyEvent) { ); } } - } else if key.is_char('H') { + } else if is_ctrl(&key, 'h') { // Run doctor let result = ops::doctor(&app.config, &app.paths); let msg = format!("{} passed, {} warnings, {} errors", result.ok_count, result.warning_count, result.error_count); let is_error = result.error_count > 0; app.set_status(format!("Doctor: {}", msg), is_error); - } else if key.is_char('r') { + } else if is_ctrl(&key, 'r') { // Refresh - reload config if app.reload_config().is_ok() { app.set_status("Config reloaded", false); @@ -557,12 +677,6 @@ fn handle_repos_input(app: &mut App, key: crossterm::event::KeyEvent) { let repo_id = repo.repo.clone(); app.show_script_selector(&repo_id); } - } else if key.is_char('I') { - // Install all (non-blocking) - app.spawn_task(TaskKind::InstallAll); - } else if key.is_char('U') { - // Update all (non-blocking) - app.spawn_task(TaskKind::UpdateAll); } } diff --git a/src/tui/theme.rs b/src/tui/theme.rs index bc88678..12031de 100644 --- a/src/tui/theme.rs +++ b/src/tui/theme.rs @@ -1,28 +1,32 @@ //! Terminal color scheme for the TUI //! -//! Uses the terminal's 16-color palette so colors respect user's terminal theme. +//! Matches Posting's design - dark purple background with magenta accents. use ratatui::style::{Color, Modifier, Style}; -// Base terminal colors (will use terminal's configured colors) -pub const BASE: Color = Color::Reset; // Default background -pub const SURFACE0: Color = Color::DarkGray; // Elevated surface -pub const SURFACE1: Color = Color::Gray; // Border/divider (brighter) -pub const TEXT: Color = Color::Reset; // Main text (default fg) -pub const SUBTEXT0: Color = Color::Gray; // Secondary text -pub const OVERLAY0: Color = Color::Gray; // Muted text (brighter for visibility) +// Posting-style dark purple palette +pub const BASE: Color = Color::Rgb(18, 18, 28); // #12121c - very dark purple bg +pub const MANTLE: Color = Color::Rgb(14, 14, 22); // #0e0e16 - darker bg (footer/header) +pub const SURFACE0: Color = Color::Rgb(30, 30, 46); // #1e1e2e - elevated surface +pub const SURFACE1: Color = Color::Rgb(40, 40, 60); // #28283c - subtle borders +pub const SURFACE2: Color = Color::Rgb(55, 55, 75); // #37374b - brighter border +pub const TEXT: Color = Color::Rgb(205, 214, 244); // #cdd6f4 - main text +pub const SUBTEXT0: Color = Color::Rgb(150, 150, 170); // #9696aa - secondary text +pub const OVERLAY0: Color = Color::Rgb(100, 100, 120); // #646478 - muted text -// Accent colors (terminal palette) -pub const BLUE: Color = Color::Blue; // Primary accent -pub const GREEN: Color = Color::Green; // Success -pub const YELLOW: Color = Color::Yellow; // Warning -pub const RED: Color = Color::Red; // Error -pub const MAUVE: Color = Color::Magenta; // Purple accent -pub const TEAL: Color = Color::Cyan; // Teal accent +// Accent colors (Posting-style) +pub const MAGENTA: Color = Color::Rgb(232, 121, 249); // #e879f9 - vibrant magenta (primary) +pub const PINK: Color = Color::Rgb(244, 114, 182); // #f472b6 - pink +pub const CYAN: Color = Color::Rgb(34, 211, 238); // #22d3ee - cyan/teal +pub const GREEN: Color = Color::Rgb(74, 222, 128); // #4ade80 - success green +pub const YELLOW: Color = Color::Rgb(250, 204, 21); // #facc15 - warning yellow +pub const RED: Color = Color::Rgb(248, 113, 113); // #f87171 - error red +pub const ORANGE: Color = Color::Rgb(251, 146, 60); // #fb923c - orange +pub const BLUE: Color = Color::Rgb(96, 165, 250); // #60a5fa - blue // Pre-built styles pub fn text() -> Style { - Style::default() + Style::default().fg(TEXT) } pub fn text_muted() -> Style { @@ -34,7 +38,7 @@ pub fn text_secondary() -> Style { } pub fn accent() -> Style { - Style::default().fg(BLUE) + Style::default().fg(MAGENTA) } pub fn success() -> Style { @@ -52,12 +56,12 @@ pub fn error() -> Style { pub fn selected() -> Style { Style::default() .bg(SURFACE0) - .fg(BLUE) + .fg(MAGENTA) .add_modifier(Modifier::BOLD) } pub fn highlight() -> Style { - Style::default().fg(BLUE).add_modifier(Modifier::BOLD) + Style::default().fg(MAGENTA).add_modifier(Modifier::BOLD) } pub fn border() -> Style { @@ -65,21 +69,43 @@ pub fn border() -> Style { } pub fn border_focused() -> Style { - Style::default().fg(BLUE) + Style::default().fg(MAGENTA) } pub fn title() -> Style { - Style::default().add_modifier(Modifier::BOLD) + Style::default().fg(MAGENTA).add_modifier(Modifier::BOLD) } pub fn header() -> Style { - Style::default().fg(MAUVE).add_modifier(Modifier::BOLD) + Style::default().fg(PINK).add_modifier(Modifier::BOLD) } pub fn keybind() -> Style { - Style::default().fg(TEAL) + Style::default().fg(MAGENTA) } pub fn keybind_desc() -> Style { Style::default().fg(SUBTEXT0) } + +// Status badge styles (like Posting's "201 Created") +pub fn badge_success() -> Style { + Style::default().fg(BASE).bg(GREEN).add_modifier(Modifier::BOLD) +} + +pub fn badge_warning() -> Style { + Style::default().fg(BASE).bg(YELLOW).add_modifier(Modifier::BOLD) +} + +pub fn badge_error() -> Style { + Style::default().fg(BASE).bg(RED).add_modifier(Modifier::BOLD) +} + +pub fn badge_info() -> Style { + Style::default().fg(BASE).bg(BLUE).add_modifier(Modifier::BOLD) +} + +// Cyan style for special elements (like GET badges) +pub fn cyan() -> Style { + Style::default().fg(CYAN) +} diff --git a/src/tui/views/catalog.rs b/src/tui/views/catalog.rs index 0e8bd05..147f1ac 100644 --- a/src/tui/views/catalog.rs +++ b/src/tui/views/catalog.rs @@ -2,8 +2,9 @@ use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, + style::Style, text::{Line, Span}, - widgets::{Block, Borders, Paragraph, Wrap}, + widgets::{Block, BorderType, Borders, Paragraph, Wrap}, Frame, }; @@ -112,7 +113,9 @@ fn render_catalog_details(f: &mut Frame, area: Rect, app: &App) { .title(" Details ") .title_style(theme::title()) .borders(Borders::ALL) - .border_style(border_style); + .border_type(BorderType::Rounded) + .border_style(border_style) + .style(Style::default().bg(theme::BASE)); let inner = block.inner(area); f.render_widget(block, area); @@ -157,21 +160,21 @@ fn render_catalog_details(f: &mut Frame, area: Rect, app: &App) { if is_added { lines.push(Line::from(vec![ - Span::styled("[r] ", theme::keybind()), + Span::styled("r ", theme::keybind()), Span::styled("Remove from config", theme::keybind_desc()), ])); lines.push(Line::from(vec![ - Span::styled("[i] ", theme::keybind()), + Span::styled("i ", theme::keybind()), Span::styled("Install", theme::keybind_desc()), ])); } else { lines.push(Line::from(vec![ - Span::styled("[a] ", theme::keybind()), + Span::styled("a ", theme::keybind()), Span::styled("Add to config", theme::keybind_desc()), ])); lines.push(Line::from(vec![ - Span::styled("[Enter]", theme::keybind()), - Span::styled(" Add and install", theme::keybind_desc()), + Span::styled("Enter ", theme::keybind()), + Span::styled("Add and install", theme::keybind_desc()), ])); } diff --git a/src/tui/views/dashboard.rs b/src/tui/views/dashboard.rs index d2b39a9..eddc14a 100644 --- a/src/tui/views/dashboard.rs +++ b/src/tui/views/dashboard.rs @@ -2,8 +2,9 @@ use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, + style::Style, text::{Line, Span}, - widgets::{Block, Borders, Paragraph}, + widgets::{Block, BorderType, Borders, Paragraph}, Frame, }; @@ -32,7 +33,9 @@ fn render_stats(f: &mut Frame, area: Rect, app: &App) { .title(" Overview ") .title_style(theme::title()) .borders(Borders::ALL) - .border_style(theme::border()); + .border_type(BorderType::Rounded) + .border_style(theme::border()) + .style(Style::default().bg(theme::BASE)); let inner = block.inner(area); f.render_widget(block, area); @@ -70,7 +73,9 @@ fn render_targets_summary(f: &mut Frame, area: Rect, app: &App) { .title(" Targets ") .title_style(theme::title()) .borders(Borders::ALL) - .border_style(theme::border()); + .border_type(BorderType::Rounded) + .border_style(theme::border()) + .style(Style::default().bg(theme::BASE)); let inner = block.inner(area); f.render_widget(block, area); @@ -109,7 +114,9 @@ fn render_recent_activity(f: &mut Frame, area: Rect, _app: &App) { .title(" Quick Actions ") .title_style(theme::title()) .borders(Borders::ALL) - .border_style(theme::border()); + .border_type(BorderType::Rounded) + .border_style(theme::border()) + .style(Style::default().bg(theme::BASE)); let inner = block.inner(area); f.render_widget(block, area); @@ -123,19 +130,19 @@ fn render_recent_activity(f: &mut Frame, area: Rect, _app: &App) { let left_lines = vec![ Line::from(""), Line::from(vec![ - Span::styled(" [I] ", theme::keybind()), + Span::styled(" ^i ", theme::keybind()), Span::styled("Install all repos", theme::keybind_desc()), ]), Line::from(vec![ - Span::styled(" [U] ", theme::keybind()), + Span::styled(" ^u ", theme::keybind()), Span::styled("Update all repos", theme::keybind_desc()), ]), Line::from(vec![ - Span::styled(" [L] ", theme::keybind()), + Span::styled(" ^l ", theme::keybind()), Span::styled("Lock versions", theme::keybind_desc()), ]), Line::from(vec![ - Span::styled(" [X] ", theme::keybind()), + Span::styled(" ^x ", theme::keybind()), Span::styled("Clean orphaned", theme::keybind_desc()), ]), ]; @@ -143,19 +150,19 @@ fn render_recent_activity(f: &mut Frame, area: Rect, _app: &App) { let right_lines = vec![ Line::from(""), Line::from(vec![ - Span::styled(" [M] ", theme::keybind()), + Span::styled(" ^m ", theme::keybind()), Span::styled("Import scripts", theme::keybind_desc()), ]), Line::from(vec![ - Span::styled(" [H] ", theme::keybind()), + Span::styled(" ^h ", theme::keybind()), Span::styled("Run doctor", theme::keybind_desc()), ]), Line::from(vec![ - Span::styled(" [r] ", theme::keybind()), + Span::styled(" ^r ", theme::keybind()), Span::styled("Reload config", theme::keybind_desc()), ]), Line::from(vec![ - Span::styled(" [?] ", theme::keybind()), + Span::styled(" ? ", theme::keybind()), Span::styled("Show help", theme::keybind_desc()), ]), ]; diff --git a/src/tui/views/repos.rs b/src/tui/views/repos.rs index 9ac9ca6..778a81c 100644 --- a/src/tui/views/repos.rs +++ b/src/tui/views/repos.rs @@ -2,8 +2,9 @@ use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, + style::Style, text::{Line, Span}, - widgets::{Block, Borders, Paragraph, Wrap}, + widgets::{Block, BorderType, Borders, Paragraph, Wrap}, Frame, }; @@ -122,7 +123,9 @@ fn render_repo_details(f: &mut Frame, area: Rect, app: &App) { .title(" Details ") .title_style(theme::title()) .borders(Borders::ALL) - .border_style(border_style); + .border_type(BorderType::Rounded) + .border_style(border_style) + .style(Style::default().bg(theme::BASE)); let inner = block.inner(area); f.render_widget(block, area); @@ -214,26 +217,26 @@ fn render_repo_details(f: &mut Frame, area: Rect, app: &App) { let actions = if repo.disabled { vec![ - ("[e]", "Enable"), - ("[r]", "Remove"), + ("e", "Enable"), + ("r", "Remove"), ] } else { match status { ItemStatus::Pending => vec![ - ("[i]", "Install"), - ("[e]", "Disable"), - ("[r]", "Remove"), + ("i", "Install"), + ("e", "Disable"), + ("r", "Remove"), ], ItemStatus::Installed => vec![ - ("[u]", "Update"), - ("[s]", "Select scripts"), - ("[p]", "Pin version"), - ("[e]", "Disable"), - ("[r]", "Remove"), + ("u", "Update"), + ("s", "Select scripts"), + ("p", "Pin version"), + ("e", "Disable"), + ("r", "Remove"), ], _ => vec![ - ("[i]", "Install"), - ("[r]", "Remove"), + ("i", "Install"), + ("r", "Remove"), ], } }; diff --git a/src/tui/views/scripts.rs b/src/tui/views/scripts.rs index 6ad6c1b..abba685 100644 --- a/src/tui/views/scripts.rs +++ b/src/tui/views/scripts.rs @@ -2,8 +2,9 @@ use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, + style::Style, text::{Line, Span}, - widgets::{Block, Borders, Paragraph, Wrap}, + widgets::{Block, BorderType, Borders, Paragraph, Wrap}, Frame, }; @@ -102,7 +103,9 @@ fn render_script_details(f: &mut Frame, area: Rect, app: &App) { .title(" Script Details ") .title_style(theme::title()) .borders(Borders::ALL) - .border_style(border_style); + .border_type(BorderType::Rounded) + .border_style(border_style) + .style(Style::default().bg(theme::BASE)); let inner = block.inner(area); f.render_widget(block, area); @@ -144,19 +147,19 @@ fn render_script_details(f: &mut Frame, area: Rect, app: &App) { Line::from(Span::styled("Actions:", theme::header())), Line::from(""), Line::from(vec![ - Span::styled("[e] ", theme::keybind()), + Span::styled("e ", theme::keybind()), Span::styled("Enable/disable", theme::keybind_desc()), ]), Line::from(vec![ - Span::styled("[r] ", theme::keybind()), + Span::styled("r ", theme::keybind()), Span::styled("Remove from target", theme::keybind_desc()), ]), Line::from(vec![ - Span::styled("[d] ", theme::keybind()), + Span::styled("d ", theme::keybind()), Span::styled("View in repo", theme::keybind_desc()), ]), Line::from(vec![ - Span::styled("[t] ", theme::keybind()), + Span::styled("t ", theme::keybind()), Span::styled("Filter by target", theme::keybind_desc()), ]), ]; diff --git a/src/tui/views/targets.rs b/src/tui/views/targets.rs index 1328c09..dad7794 100644 --- a/src/tui/views/targets.rs +++ b/src/tui/views/targets.rs @@ -2,8 +2,9 @@ use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, + style::Style, text::{Line, Span}, - widgets::{Block, Borders, Paragraph, Wrap}, + widgets::{Block, BorderType, Borders, Paragraph, Wrap}, Frame, }; @@ -81,7 +82,9 @@ fn render_target_details(f: &mut Frame, area: Rect, app: &App) { .title(" Target Details ") .title_style(theme::title()) .borders(Borders::ALL) - .border_style(border_style); + .border_type(BorderType::Rounded) + .border_style(border_style) + .style(Style::default().bg(theme::BASE)); let inner = block.inner(area); f.render_widget(block, area); @@ -159,13 +162,13 @@ fn render_target_details(f: &mut Frame, area: Rect, app: &App) { if !dirs_exist { lines.push(Line::from(vec![ - Span::styled("[c] ", theme::keybind()), + Span::styled("c ", theme::keybind()), Span::styled("Create directories", theme::keybind_desc()), ])); } lines.push(Line::from(vec![ - Span::styled("[e] ", theme::keybind()), + Span::styled("e ", theme::keybind()), Span::styled( if target.enabled { "Disable" } else { "Enable" }, theme::keybind_desc(), @@ -173,7 +176,7 @@ fn render_target_details(f: &mut Frame, area: Rect, app: &App) { ])); lines.push(Line::from(vec![ - Span::styled("[r] ", theme::keybind()), + Span::styled("r ", theme::keybind()), Span::styled("Remove target", theme::keybind_desc()), ])); diff --git a/src/tui/widgets/help.rs b/src/tui/widgets/help.rs index f337dd4..1b58442 100644 --- a/src/tui/widgets/help.rs +++ b/src/tui/widgets/help.rs @@ -2,8 +2,9 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::Style, text::{Line, Span}, - widgets::{Block, Borders, Clear, Paragraph}, + widgets::{Block, BorderType, Borders, Clear, Paragraph}, Frame, }; @@ -28,8 +29,9 @@ pub fn render_help(f: &mut Frame, area: Rect, current_view: View) { .title(" Help ") .title_style(theme::title()) .borders(Borders::ALL) + .border_type(BorderType::Rounded) .border_style(theme::border_focused()) - .style(ratatui::style::Style::default().bg(theme::SURFACE0)); + .style(Style::default().bg(theme::SURFACE0)); let inner = block.inner(popup_area); f.render_widget(block, popup_area); @@ -46,9 +48,10 @@ pub fn render_help(f: &mut Frame, area: Rect, current_view: View) { Keybind { key: "k/↑", desc: "Move up" }, Keybind { key: "g/Home", desc: "Go to top" }, Keybind { key: "G/End", desc: "Go to bottom" }, - Keybind { key: "Ctrl+d/PgDn", desc: "Page down" }, - Keybind { key: "Ctrl+u/PgUp", desc: "Page up" }, - Keybind { key: "Tab", desc: "Switch panel" }, + Keybind { key: "PgDn", desc: "Page down" }, + Keybind { key: "PgUp", desc: "Page up" }, + Keybind { key: "Tab", desc: "Next view" }, + Keybind { key: "S-Tab", desc: "Prev view" }, Keybind { key: "/", desc: "Filter mode" }, Keybind { key: "Enter", desc: "Select" }, Keybind { key: "Esc", desc: "Back/cancel" }, @@ -98,20 +101,18 @@ pub fn render_help(f: &mut Frame, area: Rect, current_view: View) { // Right column: View-specific bindings let specific_bindings = match current_view { View::Dashboard => vec![ - Keybind { key: "I", desc: "Install all" }, - Keybind { key: "U", desc: "Update all" }, - Keybind { key: "L", desc: "Lock versions" }, - Keybind { key: "X", desc: "Clean orphaned" }, - Keybind { key: "M", desc: "Import scripts" }, - Keybind { key: "H", desc: "Run doctor" }, - Keybind { key: "r", desc: "Reload config" }, + Keybind { key: "^i", desc: "Install all" }, + Keybind { key: "^u", desc: "Update all" }, + Keybind { key: "^l", desc: "Lock versions" }, + Keybind { key: "^x", desc: "Clean orphaned" }, + Keybind { key: "^m", desc: "Import scripts" }, + Keybind { key: "^h", desc: "Run doctor" }, + Keybind { key: "^r", desc: "Reload config" }, ], View::Repos => vec![ Keybind { key: "a", desc: "Add repo" }, Keybind { key: "i", desc: "Install selected" }, - Keybind { key: "I", desc: "Install all" }, Keybind { key: "u", desc: "Update selected" }, - Keybind { key: "U", desc: "Update all" }, Keybind { key: "r", desc: "Remove selected" }, Keybind { key: "p", desc: "Pin to commit" }, Keybind { key: "e", desc: "Enable/disable" }, @@ -125,7 +126,7 @@ pub fn render_help(f: &mut Frame, area: Rect, current_view: View) { ], View::Catalog => vec![ Keybind { key: "a", desc: "Add to config" }, - Keybind { key: "Enter", desc: "View details" }, + Keybind { key: "Enter", desc: "Add & install" }, ], View::Targets => vec![ Keybind { key: "a", desc: "Add target" }, diff --git a/src/tui/widgets/list.rs b/src/tui/widgets/list.rs index b891a14..76af46d 100644 --- a/src/tui/widgets/list.rs +++ b/src/tui/widgets/list.rs @@ -4,7 +4,7 @@ use ratatui::{ layout::Rect, style::{Modifier, Style}, text::{Line, Span}, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, + widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}, Frame, }; @@ -81,7 +81,9 @@ pub fn render_status_list<'a>( .title(title_text) .title_style(theme::title()) .borders(Borders::ALL) - .border_style(border_style); + .border_type(BorderType::Rounded) + .border_style(border_style) + .style(Style::default().bg(theme::BASE)); let list_items: Vec = items .iter() @@ -108,6 +110,7 @@ pub fn render_status_list<'a>( let list = List::new(list_items) .block(block) + .style(Style::default().bg(theme::BASE)) .highlight_style(theme::selected()) .highlight_symbol("> "); @@ -133,7 +136,9 @@ pub fn render_simple_list( .title(format!(" {} ", title)) .title_style(theme::title()) .borders(Borders::ALL) - .border_style(border_style); + .border_type(BorderType::Rounded) + .border_style(border_style) + .style(Style::default().bg(theme::BASE)); let list_items: Vec = items .iter() @@ -142,6 +147,7 @@ pub fn render_simple_list( let list = List::new(list_items) .block(block) + .style(Style::default().bg(theme::BASE)) .highlight_style(theme::selected()) .highlight_symbol("> "); @@ -160,7 +166,9 @@ pub fn render_empty_list(f: &mut Frame, area: Rect, title: &str, message: &str, .title(format!(" {} ", title)) .title_style(theme::title()) .borders(Borders::ALL) - .border_style(border_style); + .border_type(BorderType::Rounded) + .border_style(border_style) + .style(Style::default().bg(theme::BASE)); let inner = block.inner(area); f.render_widget(block, area); diff --git a/src/tui/widgets/popup.rs b/src/tui/widgets/popup.rs index fc138f5..4ecd418 100644 --- a/src/tui/widgets/popup.rs +++ b/src/tui/widgets/popup.rs @@ -4,7 +4,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::Modifier, text::{Line, Span}, - widgets::{Block, Borders, Clear, Paragraph, Wrap}, + widgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap}, Frame, }; @@ -55,6 +55,7 @@ pub fn render_confirm(f: &mut Frame, area: Rect, title: &str, message: &str) { .title(format!(" {} ", title)) .title_style(theme::title()) .borders(Borders::ALL) + .border_type(BorderType::Rounded) .border_style(theme::border_focused()) .style(ratatui::style::Style::default().bg(theme::SURFACE0)); @@ -106,6 +107,7 @@ pub fn render_input(f: &mut Frame, area: Rect, title: &str, prompt: &str, value: .title(format!(" {} ", title)) .title_style(theme::title()) .borders(Borders::ALL) + .border_type(BorderType::Rounded) .border_style(theme::border_focused()) .style(ratatui::style::Style::default().bg(theme::SURFACE0)); @@ -171,6 +173,7 @@ pub fn render_message(f: &mut Frame, area: Rect, title: &str, message: &str, is_ .title(format!(" {} ", title)) .title_style(title_style) .borders(Borders::ALL) + .border_type(BorderType::Rounded) .border_style(if is_error { theme::error() } else { theme::border_focused() }) .style(ratatui::style::Style::default().bg(theme::SURFACE0)); @@ -228,6 +231,7 @@ pub fn render_script_selector( .title(format!(" Scripts: {} ", repo_id)) .title_style(theme::title()) .borders(Borders::ALL) + .border_type(BorderType::Rounded) .border_style(theme::border_focused()) .style(ratatui::style::Style::default().bg(theme::SURFACE0));