This commit is contained in:
41
src/app.rs
Normal file
41
src/app.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use crate::theme::Theme;
|
||||
|
||||
pub const NUM_BOXES: usize = 4;
|
||||
|
||||
pub struct App {
|
||||
pub focus: usize,
|
||||
pub theme: Theme,
|
||||
pub show_help: bool,
|
||||
pub show_welcome: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
focus: 0,
|
||||
theme: Theme::gruvbox_dark(),
|
||||
show_help: false,
|
||||
show_welcome: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_left(&mut self) {
|
||||
self.focus = (self.focus + 3) % NUM_BOXES;
|
||||
}
|
||||
|
||||
pub fn move_right(&mut self) {
|
||||
self.focus = (self.focus + 1) % NUM_BOXES;
|
||||
}
|
||||
|
||||
pub fn move_down(&mut self) {
|
||||
if self.focus < 2 {
|
||||
self.focus += 2;
|
||||
} else {
|
||||
self.focus -= 2;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self) {
|
||||
self.move_down(); // symmetric
|
||||
}
|
||||
}
|
||||
35
src/input.rs
Normal file
35
src/input.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use crossterm::event::{Event, KeyCode};
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
pub fn handle_input(app: &mut App, event: Event) -> bool {
|
||||
if app.show_welcome {
|
||||
if let Event::Key(_) = event {
|
||||
app.show_welcome = false;
|
||||
}
|
||||
} else if app.show_help {
|
||||
if let Event::Key(key) = event {
|
||||
match key.code {
|
||||
KeyCode::Char('q')
|
||||
| KeyCode::Esc
|
||||
| KeyCode::Char('?')
|
||||
| KeyCode::Enter
|
||||
| KeyCode::Backspace => app.show_help = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Event::Key(key) = event {
|
||||
match key.code {
|
||||
KeyCode::Char('h') | KeyCode::Left => app.move_left(),
|
||||
KeyCode::Char('l') | KeyCode::Right => app.move_right(),
|
||||
KeyCode::Char('j') | KeyCode::Down => app.move_down(),
|
||||
KeyCode::Char('k') | KeyCode::Up => app.move_up(),
|
||||
KeyCode::Char('?') => app.show_help = true,
|
||||
KeyCode::Char('q') | KeyCode::Esc => return false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
49
src/main.rs
Normal file
49
src/main.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
mod app;
|
||||
mod input;
|
||||
mod theme;
|
||||
mod ui;
|
||||
|
||||
use crossterm::{
|
||||
event, execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
use std::io::stdout;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
enable_raw_mode()?;
|
||||
let (width, height) = crossterm::terminal::size()?;
|
||||
if width < 15 || height < 15 {
|
||||
disable_raw_mode()?;
|
||||
println!("Your console is too small.");
|
||||
return Ok(());
|
||||
}
|
||||
let mut stdout = stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let mut app = app::App::new();
|
||||
let mut blink_counter = 0;
|
||||
|
||||
loop {
|
||||
let blink = (blink_counter / 3) % 2 == 0;
|
||||
terminal.draw(|f| ui::draw(f, &app, blink))?;
|
||||
blink_counter += 1;
|
||||
let timeout = if app.show_welcome {
|
||||
std::time::Duration::from_millis(70)
|
||||
} else {
|
||||
std::time::Duration::from_millis(60)
|
||||
};
|
||||
if event::poll(timeout)? {
|
||||
let event = event::read()?;
|
||||
if !input::handle_input(&mut app, event) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
32
src/theme.rs
Normal file
32
src/theme.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use ratatui::style::Color;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Theme {
|
||||
pub bg: Color,
|
||||
pub fg: Color,
|
||||
pub red: Color,
|
||||
pub green: Color,
|
||||
pub yellow: Color,
|
||||
pub blue: Color,
|
||||
pub purple: Color,
|
||||
pub aqua: Color,
|
||||
pub orange: Color,
|
||||
pub gray: Color,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn gruvbox_dark() -> Self {
|
||||
Self {
|
||||
bg: Color::Rgb(40, 40, 40),
|
||||
fg: Color::Rgb(235, 219, 178),
|
||||
red: Color::Rgb(204, 36, 29),
|
||||
green: Color::Rgb(152, 151, 26),
|
||||
yellow: Color::Rgb(215, 153, 33),
|
||||
blue: Color::Rgb(69, 133, 136),
|
||||
purple: Color::Rgb(177, 98, 134),
|
||||
aqua: Color::Rgb(104, 157, 106),
|
||||
orange: Color::Rgb(214, 93, 14),
|
||||
gray: Color::Rgb(146, 131, 116),
|
||||
}
|
||||
}
|
||||
}
|
||||
108
src/ui.rs
Normal file
108
src/ui.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::Style,
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
pub fn draw(f: &mut Frame, app: &App, blink: bool) {
|
||||
let bg_block = Block::default().style(Style::default().bg(app.theme.bg));
|
||||
f.render_widget(bg_block, f.size());
|
||||
|
||||
if app.show_welcome {
|
||||
let fg = app.theme.fg;
|
||||
let accent_fg = if blink { app.theme.blue } else { app.theme.bg };
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(40),
|
||||
Constraint::Percentage(20),
|
||||
Constraint::Percentage(40),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
let welcome_text = Paragraph::new(vec![
|
||||
Line::from(vec![
|
||||
Span::styled("Welcome to ", Style::default().fg(fg).bg(app.theme.bg)),
|
||||
Span::styled(
|
||||
"dcorral.com",
|
||||
Style::default().fg(app.theme.orange).bg(app.theme.bg),
|
||||
),
|
||||
Span::styled("!", Style::default().fg(fg).bg(app.theme.bg)),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("press ", Style::default().fg(fg).bg(app.theme.bg)),
|
||||
Span::styled("any key", Style::default().fg(accent_fg).bg(app.theme.bg)),
|
||||
Span::styled(" to continue", Style::default().fg(fg).bg(app.theme.bg)),
|
||||
]),
|
||||
])
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(welcome_text, chunks[1]);
|
||||
} else if app.show_help {
|
||||
let help_content = "Navigation:\n\
|
||||
h / ←: Move left\n\
|
||||
l / →: Move right\n\
|
||||
j / ↓: Move down\n\
|
||||
k / ↑: Move up\n\
|
||||
? : Show help\n\
|
||||
q / Esc: Quit";
|
||||
let help_text = Paragraph::new(help_content)
|
||||
.alignment(ratatui::layout::Alignment::Center)
|
||||
.style(Style::default().fg(app.theme.fg).bg(app.theme.bg));
|
||||
f.render_widget(help_text, f.size());
|
||||
} else {
|
||||
let size = f.size();
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(1), Constraint::Length(1)].as_ref())
|
||||
.split(size);
|
||||
|
||||
let box_area = chunks[0];
|
||||
|
||||
let box_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(box_area);
|
||||
|
||||
let top_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(box_chunks[0]);
|
||||
|
||||
let bottom_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(box_chunks[1]);
|
||||
|
||||
let areas = [
|
||||
top_chunks[0],
|
||||
top_chunks[1],
|
||||
bottom_chunks[0],
|
||||
bottom_chunks[1],
|
||||
];
|
||||
|
||||
for (i, area) in areas.iter().enumerate() {
|
||||
let (bg, fg) = if app.focus == i {
|
||||
(app.theme.blue, app.theme.bg)
|
||||
} else {
|
||||
(app.theme.bg, app.theme.fg)
|
||||
};
|
||||
let block = Block::default()
|
||||
.title(format!("Box {}", i))
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(bg).fg(fg));
|
||||
f.render_widget(block, *area);
|
||||
}
|
||||
|
||||
let help_text = Paragraph::new("Use hjkl or arrows to navigate, ? for help, q to quit")
|
||||
.style(Style::default().fg(app.theme.fg).bg(app.theme.bg));
|
||||
f.render_widget(help_text, chunks[1]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user