Basic TUI and other bits

This commit is contained in:
Luca 2026-09-15 21:16:23 +01:00
parent 0280ea3b85
commit 2f489d5466
4 changed files with 2160 additions and 2 deletions

1961
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,3 +4,10 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
color-eyre = "0.6.5"
crossterm = "0.29.0"
directories = "6.0.0"
rand = { version = "0.10.2", features = ["serde"] }
ratatui = "0.30.2"
serde = "1.0.229"
serde_json = "1.0.151"

View file

@ -1,3 +1,146 @@
fn main() { pub mod termi;
println!("Hello, world!");
use std::{io, path::Path};
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
use directories::BaseDirs;
use ratatui::{
DefaultTerminal, Frame,
layout::{Constraint, Direction, Layout, Margin},
style::Stylize,
widgets::{Clear, Fill},
};
use crate::termi::Termi;
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
let mut app = TermiApp::default();
ratatui::run(|term| app.run(term))?;
Ok(())
}
#[derive(Debug, Default)]
pub enum TermiAppState {
#[default]
Start,
Exiting,
}
impl TermiAppState {
pub fn is_exiting(&self) -> bool {
match self {
Self::Exiting => true,
_ => false,
}
}
pub fn is_running(&self) -> bool {
!self.is_exiting()
}
pub fn is_starting(&self) -> bool {
match self {
Self::Start => true,
_ => false,
}
}
}
#[derive(Debug, Default)]
pub struct TermiApp {
termi: Option<Termi>,
state: TermiAppState,
}
impl TermiApp {
pub fn try_load(&mut self) {
// currently use PWD
// ./termi.json
let garden_path = Path::new("./garden.json");
if garden_path.exists() {
if garden_path.is_file() {
todo!()
} else {
panic!()
}
}
// create new termi
self.termi = {
let mut _new_termi = Termi::default();
_new_termi.name = "john termi".to_string();
Some(_new_termi)
};
}
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> std::io::Result<()> {
if self.state.is_starting() {
self.try_load();
}
while self.state.is_running() {
terminal.draw(|frame| self.draw(frame))?;
self.handle_events()?;
}
Ok(())
}
pub fn draw(&self, frame: &mut Frame) {
// background: the void
frame.render_widget(Fill::new("·"), frame.area());
let cols = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![
Constraint::Fill(1),
Constraint::Min(24),
Constraint::Fill(1),
])
.split(frame.area());
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Fill(1),
Constraint::Min(14),
Constraint::Fill(1),
])
.split(frame.area());
let centre_stage_area = rows[1].intersection(cols[1]);
let [sprite_area, status_area] = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Fill(1), Constraint::Length(1)])
.areas(centre_stage_area.inner(Margin::new(2, 2)));
frame.render_widget(Fill::new(" "), centre_stage_area);
let termi: &Termi = self.termi.as_ref().unwrap();
frame.render_widget(termi.get_sprite(), sprite_area);
frame.render_widget(termi.get_status_line(), status_area);
}
pub fn handle_events(&mut self) -> io::Result<()> {
match event::read()? {
// it's important to check that the event is a key press event as
// crossterm also emits key release and repeat events on Windows.
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
self.handle_key_event(key_event)
}
_ => {}
};
Ok(())
}
pub fn handle_key_event(&mut self, key_event: KeyEvent) {
match key_event.code {
KeyCode::Char('q') => self.exit(),
_ => {}
}
}
pub fn exit(&mut self) {
self.state = TermiAppState::Exiting;
}
} }

47
src/termi.rs Normal file
View file

@ -0,0 +1,47 @@
use ratatui::{
style::{Color, Style, Stylize},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Widget},
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Termi {
pub name: String,
}
impl Termi {
pub fn new(name: String) -> Termi {
Termi { name }
}
pub fn get_sprite(&self) -> TermiSprite {
TermiSprite {
name: self.name.clone(),
}
}
pub fn get_status_line(&self) -> Line<'_> {
Line::from(vec![
Span::styled(self.name.clone(), Style::new().yellow()),
Span::raw(" is "),
Span::styled("happy", Style::new().green().bold()),
])
}
}
pub struct TermiSprite {
// TODO should this be a reference to Termi/Termi's name instead?
pub name: String,
}
impl Widget for TermiSprite {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) {
// placeholder
let placeholder_block = Block::<'_>::bordered()
.border_type(BorderType::Rounded)
.title(self.name);
placeholder_block.render(area, buf);
}
}