feat: add configuration management and panel rendering

- Introduced a new `Config` struct for loading application configuration from a TOML file.
- Added support for keybindings and panel configurations, including status widgets.
- Implemented `PtyTerminal` for handling pseudo-terminal interactions.
- Created a rendering module to manage the display of application windows, panels, and dropdowns.
- Refactored the main application loop to utilize the new configuration and rendering logic.
- Removed legacy code related to event handling and rendering from `main.rs`.
This commit is contained in:
Björn Blomberg
2026-03-27 17:01:06 +01:00
parent d7c70baf35
commit 4440ad79bd
15 changed files with 2154 additions and 177 deletions

View File

@@ -1,77 +1,17 @@
mod app;
mod config;
mod pty;
mod render;
use app::App;
use config::Config;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph},
Terminal,
};
use std::io;
struct App {
should_quit: bool,
button_hovered: bool,
}
impl App {
fn new() -> Self {
Self {
should_quit: false,
button_hovered: false,
}
}
fn handle_event(&mut self, event: Event, button_rect: Rect) {
match event {
Event::Key(key) => {
if key.code == KeyCode::Char('q') || key.code == KeyCode::Esc {
self.should_quit = true;
}
}
Event::Mouse(mouse) => {
let mx = mouse.column;
let my = mouse.row;
let in_button = mx >= button_rect.x
&& mx < button_rect.x + button_rect.width
&& my >= button_rect.y
&& my < button_rect.y + button_rect.height;
match mouse.kind {
MouseEventKind::Moved | MouseEventKind::Drag(_) => {
self.button_hovered = in_button;
}
MouseEventKind::Down(MouseButton::Left) => {
if in_button {
self.should_quit = true;
}
}
_ => {}
}
}
_ => {}
}
}
}
fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
let x = area.x + area.width.saturating_sub(width) / 2;
let y = area.y + area.height.saturating_sub(height) / 2;
Rect::new(x, y, width.min(area.width), height.min(area.height))
}
fn button_rect_inside_dialog(dialog: Rect) -> Rect {
// Centered button, 16 wide, 3 tall, 2 rows from bottom of dialog
let btn_w: u16 = 16;
let btn_h: u16 = 3;
let x = dialog.x + dialog.width.saturating_sub(btn_w) / 2;
let y = dialog.y + dialog.height.saturating_sub(btn_h + 2);
Rect::new(x, y, btn_w, btn_h)
}
use ratatui::{backend::CrosstermBackend, Terminal};
use std::{io, time::Duration};
fn main() -> io::Result<()> {
enable_raw_mode()?;
@@ -81,86 +21,10 @@ fn main() -> io::Result<()> {
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new();
// Initialize button_rect so the first event has something to compare against.
let mut last_button_rect = Rect::default();
let config = Config::load_or_default("config.toml");
let mut app = App::new(config);
loop {
terminal.draw(|frame| {
let area = frame.area();
// Dialog box: 40 wide, 10 tall, centered
let dialog = centered_rect(40, 10, area);
let button = button_rect_inside_dialog(dialog);
last_button_rect = button;
// Draw dialog
let block = Block::default()
.title(" TUI-WM ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan));
frame.render_widget(Clear, dialog);
frame.render_widget(block, dialog);
// Welcome text
let text = Paragraph::new(vec![
Line::from(""),
Line::from(vec![Span::styled(
"Välkommen till TUI-WM",
Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
"Tryck Q eller klicka Exit",
Style::default().fg(Color::DarkGray),
)]),
])
.alignment(ratatui::layout::Alignment::Center);
// Inner area for text (leave room for button)
let text_area = Rect::new(
dialog.x + 1,
dialog.y + 1,
dialog.width.saturating_sub(2),
dialog.height.saturating_sub(5),
);
frame.render_widget(text, text_area);
// Exit button with hover animation
let (btn_style, btn_border_style, label) = if app.button_hovered {
(
Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD),
Style::default().fg(Color::White),
" [ EXIT ] ",
)
} else {
(
Style::default().fg(Color::Cyan),
Style::default().fg(Color::DarkGray),
" Exit ",
)
};
let btn_widget = Paragraph::new(Line::from(vec![Span::styled(label, btn_style)]))
.block(
Block::default()
.borders(Borders::ALL)
.border_style(btn_border_style),
)
.alignment(ratatui::layout::Alignment::Center);
frame.render_widget(btn_widget, button);
})?;
if event::poll(std::time::Duration::from_millis(16))? {
let ev = event::read()?;
app.handle_event(ev, last_button_rect);
}
if app.should_quit {
break;
}
}
let result = run(&mut terminal, &mut app);
disable_raw_mode()?;
execute!(
@@ -170,5 +34,32 @@ fn main() -> io::Result<()> {
)?;
terminal.show_cursor()?;
result
}
fn run(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut App,
) -> io::Result<()> {
loop {
// Uppdatera layout från aktuell terminalstorlek
let size = terminal.size()?;
app.update_layout(ratatui::layout::Rect::new(0, 0, size.width, size.height));
// Rendera
terminal.draw(|frame| render::render(frame, app))?;
// Töm PTY-output och uppdatera terminalparsers
app.tick();
// Hantera inkommande events (kort timeout → snabb PTY-uppdatering)
if event::poll(Duration::from_millis(16))? {
app.handle_event(event::read()?);
}
if app.should_quit {
break;
}
}
Ok(())
}