mod screens;
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Tabs},
Frame, Terminal,
};
use std::io;
use screens::Screen;
pub struct App {
pub current_screen: Box<dyn Screen>,
pub tabs: Vec<String>,
pub active_tab: usize,
pub should_quit: bool,
}
impl App {
pub fn new() -> Self {
let tabs = vec![
"Projects".to_string(),
"Create".to_string(),
"Log".to_string(),
];
Self {
current_screen: Box::new(screens::ProjectListScreen::new()),
tabs,
active_tab: 0,
should_quit: false,
}
}
pub fn switch_tab(&mut self, index: usize) {
self.active_tab = index;
self.current_screen = match index {
0 => Box::new(screens::ProjectListScreen::new()),
1 => Box::new(screens::ProjectCreatorScreen::new()),
2 => Box::new(screens::LogViewerScreen::new()),
_ => return,
};
}
}
pub fn run() -> Result<(), String> {
enable_raw_mode().map_err(|e| format!("raw mode: {}", e))?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)
.map_err(|e| format!("alt screen: {}", e))?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)
.map_err(|e| format!("terminal: {}", e))?;
let mut app = App::new();
let result = run_event_loop(&mut terminal, &mut app);
disable_raw_mode().ok();
execute!(terminal.backend_mut(), LeaveAlternateScreen).ok();
terminal.show_cursor().ok();
result
}
fn run_event_loop<B: Backend>(
terminal: &mut Terminal<B>,
app: &mut App,
) -> Result<(), String> {
loop {
terminal
.draw(|f| render_frame(f, app))
.map_err(|e| format!("draw: {}", e))?;
if let Event::Key(key) = event::read().map_err(|e| format!("input: {}", e))? {
if key.kind == KeyEventKind::Release {
continue;
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => {
app.should_quit = true;
}
KeyCode::Char('1') => app.switch_tab(0),
KeyCode::Char('2') => app.switch_tab(1),
KeyCode::Char('3') => app.switch_tab(2),
KeyCode::Tab => {
let next = (app.active_tab + 1) % app.tabs.len();
app.switch_tab(next);
}
_ => {
app.current_screen.handle_input(key);
}
}
}
if app.should_quit {
break;
}
}
Ok(())
}
fn render_frame(f: &mut Frame, app: &mut App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Min(1), Constraint::Length(1), ])
.split(f.area());
let tabs: Vec<Line> = app
.tabs
.iter()
.enumerate()
.map(|(i, name)| {
let style = if i == app.active_tab {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Gray)
};
Line::from(Span::styled(format!(" {} ({}) ", name, i + 1), style))
})
.collect();
let tabs_widget = Tabs::new(tabs)
.block(Block::default().borders(Borders::BOTTOM))
.select(app.active_tab);
f.render_widget(tabs_widget, chunks[0]);
app.current_screen.render(f, chunks[1]);
let status = Paragraph::new(Line::from(Span::styled(
" q quit | Tab switch | 1-3 tabs | ? help ",
Style::default().fg(Color::DarkGray),
)))
.block(Block::default());
f.render_widget(status, chunks[2]);
}