feat(tui): redesign UI with Posting-style theme and keybinds

- Update color scheme to match Posting's dark purple aesthetic
  - Darker background (#12121c), vibrant magenta accents (#e879f9)
  - Subtle borders, proper RGB colors throughout
- Add rounded borders (BorderType::Rounded) to all panels
- Implement Ctrl+key combinations for Dashboard actions (^i, ^u, ^h, etc.)
- Keep regular key presses for other views (a, i, u, e, r, etc.)
- Add Tab/Shift+Tab for cycling through views
- Move status line above footer keybinds
- Add empty spacer line between header and content
- Update all help text and detail panels with proper key notation
- Fix keybind conflicts (remove ^d/^u from page navigation)
This commit is contained in:
2026-01-26 13:24:25 +01:00
parent 3afabc723b
commit 498786071f
12 changed files with 351 additions and 155 deletions

View File

@@ -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();

View File

@@ -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
}

View File

@@ -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<Span<'static>> {
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<Span<'static>> {
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<Span> = 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);
}
}

View File

@@ -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)
}

View File

@@ -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()),
]));
}

View File

@@ -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()),
]),
];

View File

@@ -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"),
],
}
};

View File

@@ -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()),
]),
];

View File

@@ -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()),
]));

View File

@@ -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" },

View File

@@ -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<ListItem> = 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<ListItem> = 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);

View File

@@ -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));