Add base project
Some checks failed
Deploy to VPS / deploy (push) Failing after 5s

This commit is contained in:
2025-11-06 19:42:12 +01:00
parent 29c60e4824
commit c0688ca363
8 changed files with 504 additions and 33 deletions

41
src/app.rs Normal file
View 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
View 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
View 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
View 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
View 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]);
}
}