use crate::{config::McPaths, tui::terminal::TerminalGuard};
use anyhow::Result;
use crossterm::event::{Event, KeyCode, KeyEventKind, MouseButton, MouseEvent, MouseEventKind};
use ratatui::{
layout::{Constraint, Layout, Rect},
style::{Color, Style},
text::Line,
widgets::Paragraph,
};
use ratatui_comfy_tabs::{
TabDirection, TabNav, TabNavState, TabOrientation, TabWheelDirection, VerticalPosition,
tab_border, vertical_label,
};
use ratatui_core::widgets::StatefulWidget;
mod workspace;
use workspace::WorkspaceState;
pub(crate) fn run_tui(paths: McPaths) -> Result<()> {
let _terminal = TerminalGuard::enter()?;
let mut app = Tui2App::new(paths);
let input_events = crate::tui::TerminalInputBridge::spawn();
let mut terminal =
ratatui::Terminal::new(ratatui::backend::CrosstermBackend::new(std::io::stdout()))?;
loop {
terminal.draw(|frame| app.draw(frame))?;
crossbeam_channel::select! {
recv(input_events.receiver) -> event => {
let Ok(event) = event else { return Ok(()); };
match event {
Event::Key(key) if matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) => {
if app.tab_state.selected == 0 && app.workspace.handle_key(key.code) { continue; }
match key.code {
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
KeyCode::Tab => app.tab_state.select_direction_wrapping(TabDirection::Next, 2),
KeyCode::BackTab => app.tab_state.select_direction_wrapping(TabDirection::Previous, 2),
_ => {}
}
}
Event::Mouse(mouse) => app.handle_mouse(mouse),
_ => {}
}
}
}
}
}
struct Tui2App {
tab_state: TabNavState,
tab_labels: Vec<String>,
tab_hit_area: Rect,
workspace: WorkspaceState,
}
impl Tui2App {
fn new(paths: McPaths) -> Self {
Self {
tab_state: TabNavState::default(),
tab_labels: vec![vertical_label("Workspaces"), vertical_label("Nexus")],
tab_hit_area: Rect::default(),
workspace: WorkspaceState::new(paths),
}
}
fn handle_mouse(&mut self, mouse: MouseEvent) {
let tab_refs: Vec<&str> = self.tab_labels.iter().map(String::as_str).collect();
let nav = TabNav::new(&tab_refs, self.tab_state.selected)
.orientation(TabOrientation::Vertical)
.vertical_position(VerticalPosition::Left)
.mouse_click(true)
.mouse_wheel(true);
match mouse.kind {
MouseEventKind::Down(MouseButton::Left) => {
self.tab_state
.handle_mouse_click(&nav, self.tab_hit_area, mouse.column, mouse.row);
}
MouseEventKind::ScrollDown => {
self.tab_state.handle_mouse_wheel(
&nav,
self.tab_hit_area,
mouse.column,
mouse.row,
TabWheelDirection::Down,
);
}
MouseEventKind::ScrollUp => {
self.tab_state.handle_mouse_wheel(
&nav,
self.tab_hit_area,
mouse.column,
mouse.row,
TabWheelDirection::Up,
);
}
_ => {}
}
}
fn draw(&mut self, frame: &mut ratatui::Frame<'_>) {
let area = frame.area();
let app_bg = Color::Rgb(15, 15, 25);
let bar_bg = Color::Rgb(25, 25, 45);
let tab_border_color = Color::Rgb(50, 50, 80);
frame.render_widget(Paragraph::new("").style(Style::default().bg(app_bg)), area);
let [body_area, footer_area] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area);
let tab_refs: Vec<&str> = self.tab_labels.iter().map(String::as_str).collect();
let nav = TabNav::new(&tab_refs, self.tab_state.selected)
.orientation(TabOrientation::Vertical)
.vertical_position(VerticalPosition::Left)
.style(Style::default().fg(Color::DarkGray).bg(app_bg))
.highlight_style(Style::default().fg(Color::LightBlue).bg(app_bg))
.border_style(Style::default().fg(tab_border_color).bg(app_bg))
.border_set(tab_border::Rnd)
.mouse_click(true)
.mouse_wheel(true);
let rail_width = nav.vertical_rail_width();
let [tabs_area, content_area] =
Layout::horizontal([Constraint::Length(rail_width), Constraint::Fill(1)])
.areas(body_area);
self.tab_hit_area = tabs_area;
self.tab_state.ensure_selected_visible(&nav, tabs_area);
StatefulWidget::render(nav, tabs_area, frame.buffer_mut(), &mut self.tab_state);
match self.tab_state.selected {
0 => self.workspace.render(content_area, frame.buffer_mut()),
_ => {
let content = Paragraph::new("Nexus — coming soon")
.style(Style::default().fg(Color::Gray).bg(app_bg));
frame.render_widget(content, content_area);
}
}
let footer = Paragraph::new(Line::from(
" q/Esc exit | Tab next tab | Shift+Tab previous tab | click tabs",
))
.style(Style::default().fg(Color::Gray).bg(bar_bg));
frame.render_widget(footer, footer_area);
}
}