Refactor App to use unified AppChannels for events

App now receives an AppChannels struct; AppEvent enum removed.

Event handling, tick generation, and audio communication are now
performed through dedicated channels.
This commit is contained in:
2026-05-03 22:27:08 +02:00
parent 7fb001faab
commit 7541db79d8
9 changed files with 123 additions and 105 deletions
+18 -23
View File
@@ -1,17 +1,13 @@
use crate::{ use crate::{
app::{ app::{
GameStates, handle_keybindings, GameStates, handle_keybindings,
threads::{AppEvent, AudioCmd}, threads::{AppChannels, AudioCmd},
view::View, view::View,
}, },
cli::Cli, cli::Cli,
}; };
use ratatui::{DefaultTerminal, Frame, crossterm::event::KeyEvent, layout::Rect}; use ratatui::{DefaultTerminal, Frame, crossterm::event::KeyEvent, layout::Rect};
use std::{ use std::{io::Result, sync::mpsc::Sender, thread::sleep, time::Duration};
io::Result,
sync::mpsc::{Receiver, RecvTimeoutError, Sender},
time::Duration,
};
pub struct App { pub struct App {
pub exit: bool, pub exit: bool,
@@ -42,31 +38,29 @@ impl App {
self.states.as_mut() self.states.as_mut()
} }
pub fn run(&mut self, terminal: &mut DefaultTerminal, rx: Receiver<AppEvent>) -> Result<()> { pub fn run(&mut self, terminal: &mut DefaultTerminal, channels: AppChannels) -> Result<()> {
while !self.exit { while !self.exit {
terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?; terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?;
let event = match rx.recv_timeout(Duration::from_millis(100)) { if let Ok(key) = channels.input_rx.try_recv() {
Ok(ev) => ev, self.handle_key_event(key)?;
Err(RecvTimeoutError::Timeout) => { }
continue;
} if let Ok((_, _)) = channels.resize_rx.try_recv() {
Err(_) => break, let window_area: Rect = self.window_area;
}; if let Some(state) = self.states_mut() {
match event {
AppEvent::Input(key_event) => self.handle_key_event(key_event)?,
AppEvent::Resize(_, _) => {
let window_area: Rect = self.window_area;
let Some(state) = self.states_mut() else {
panic!("State issue")
};
state state
.skirmish .skirmish
.board .board
.change_resize(&window_area, state.skirmish.side_panel); .change_resize(&window_area, state.skirmish.side_panel);
} }
AppEvent::Tick => self.update()?,
} }
if let Ok(()) = channels.tick_rx.try_recv() {
self.update()?;
}
sleep(Duration::from_millis(10));
} }
Ok(()) Ok(())
@@ -94,8 +88,9 @@ impl App {
fn update(&mut self) -> Result<()> { fn update(&mut self) -> Result<()> {
if let Some(state) = self.states_mut() { if let Some(state) = self.states_mut() {
state.skirmish.update(&self.args); state.skirmish.tick_update();
} }
Ok(()) Ok(())
} }
} }
+3 -3
View File
@@ -1,4 +1,4 @@
use crate::{app::states::skirmish_states::BoardState, cli::Cli}; use crate::app::states::skirmish_states::BoardState;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkirmishState { pub struct SkirmishState {
@@ -10,8 +10,8 @@ pub struct SkirmishState {
} }
impl SkirmishState { impl SkirmishState {
pub fn update(&mut self, cli: &Cli) { pub fn tick_update(&mut self) {
self.board.advance_turn(); // self.board.advance_turn();
// if self.board.is_victory() {} // if self.board.is_victory() {}
+37
View File
@@ -0,0 +1,37 @@
use crate::app::threads::AudioCmd;
use ratatui::crossterm::event::KeyEvent;
use std::sync::mpsc::{Receiver, Sender, channel};
pub struct AppChannels {
pub input_tx: Sender<KeyEvent>,
pub input_rx: Receiver<KeyEvent>,
pub resize_tx: Sender<(u16, u16)>,
pub resize_rx: Receiver<(u16, u16)>,
pub tick_tx: Sender<()>,
pub tick_rx: Receiver<()>,
pub audio_tx: Sender<AudioCmd>,
pub audio_rx: Receiver<AudioCmd>,
}
impl AppChannels {
pub fn new() -> Self {
let (input_tx, input_rx) = channel::<KeyEvent>();
let (resize_tx, resize_rx) = channel::<(u16, u16)>();
let (tick_tx, tick_rx) = channel::<()>();
let (audio_tx, audio_rx) = channel::<AudioCmd>();
Self {
input_tx,
input_rx,
resize_tx,
resize_rx,
tick_tx,
tick_rx,
audio_tx,
audio_rx,
}
}
}
+11 -21
View File
@@ -1,30 +1,20 @@
use ratatui::crossterm::event::KeyEvent; use ratatui::crossterm::event::{Event, KeyEvent, read};
use ratatui::crossterm::event::{Event, read};
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
pub enum AppEvent { pub fn handle_ct_events(input_tx: Sender<KeyEvent>, resize_tx: Sender<(u16, u16)>) {
Input(KeyEvent),
Resize(u16, u16),
Tick,
}
/// Reads *all* crossterm events and forwards the ones we care about.
pub fn handle_events(tx: Sender<AppEvent>) {
loop { loop {
match read() { match read() {
Ok(ev) => match ev { Ok(Event::Key(k)) => {
Event::Key(key) => { if input_tx.send(k).is_err() {
if tx.send(AppEvent::Input(key)).is_err() { break;
break;
}
} }
Event::Resize(cols, rows) => { }
if tx.send(AppEvent::Resize(cols, rows)).is_err() { Ok(Event::Resize(cols, rows)) => {
break; if resize_tx.send((cols, rows)).is_err() {
} break;
} }
_ => {} }
}, Ok(_) => {}
Err(_) => continue, Err(_) => continue,
} }
} }
+4 -2
View File
@@ -1,7 +1,9 @@
mod app_channels;
mod audio; mod audio;
mod events; mod events;
mod tick; mod tick;
pub use app_channels::AppChannels;
pub use audio::{AudioCmd, SoundrackParts, Soundtrack, handle_audio}; pub use audio::{AudioCmd, SoundrackParts, Soundtrack, handle_audio};
pub use events::{AppEvent, handle_events}; pub use events::handle_ct_events;
pub use tick::spawn_tick_thread; pub use tick::handle_tick_event;
+11 -14
View File
@@ -1,22 +1,19 @@
use crate::app::threads::AppEvent;
use std::{ use std::{
sync::mpsc::Sender, sync::mpsc::Sender,
thread, thread,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
pub fn spawn_tick_thread(tx: Sender<AppEvent>, interval_ms: u64) { pub fn handle_tick_event(tx: Sender<()>, interval_ms: u8) {
thread::spawn(move || { let interval: Duration = Duration::from_millis(interval_ms as u64);
let interval: Duration = Duration::from_millis(interval_ms); loop {
loop { if tx.send(()).is_err() {
if tx.send(AppEvent::Tick).is_err() { break;
break;
}
let elapsed: Duration = Instant::now().elapsed();
if interval > elapsed {
thread::sleep(interval - elapsed);
}
} }
});
let elapsed: Duration = Instant::now().elapsed();
if interval > elapsed {
thread::sleep(interval - elapsed);
}
}
} }
+2 -1
View File
@@ -72,7 +72,8 @@ pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) {
"Skills points: {} ({}/{}) | ", "Skills points: {} ({}/{}) | ",
1, 20, states.settings.skill_points_limit 1, 20, states.settings.skill_points_limit
), ),
format!("Perk Deck: {}/9", 5), format!("Perk Deck: {}/9 | ", 5),
format!("Tick: {}", states.skirmish.turn_counter),
]), ]),
]); ]);
+17 -8
View File
@@ -1,10 +1,13 @@
use crate::app::{ use crate::{
states::{ app::{
PerkDecks, states::{
skirmish_states::{GameMode, ZoomLevel}, PerkDecks,
skirmish_states::{GameMode, ZoomLevel},
},
threads::Soundtrack,
view::View,
}, },
threads::Soundtrack, logs::init_logger,
view::View,
}; };
use clap::{Error, Parser, error::ErrorKind, value_parser}; use clap::{Error, Parser, error::ErrorKind, value_parser};
use std::num::ParseFloatError; use std::num::ParseFloatError;
@@ -15,7 +18,7 @@ use std::num::ParseFloatError;
/// The `clap` attributes describe the flag name, help text, default value, /// The `clap` attributes describe the flag name, help text, default value,
/// and any validation constraints. The struct derives `Parser` so that /// and any validation constraints. The struct derives `Parser` so that
/// `Cli::parse()` can be called directly to obtain a populated instance. /// `Cli::parse()` can be called directly to obtain a populated instance.
#[derive(Parser, Debug)] #[derive(Parser, Debug, Clone)]
#[command(version, about = "War in Tunnels", long_about = "War in Tunnels")] #[command(version, about = "War in Tunnels", long_about = "War in Tunnels")]
pub struct Cli { pub struct Cli {
/// The initial view/window to display. /// The initial view/window to display.
@@ -169,7 +172,13 @@ pub struct Cli {
/// handles argument validation and displays helpful error messages if /// handles argument validation and displays helpful error messages if
/// the user supplies invalid input. /// the user supplies invalid input.
pub fn get_args() -> Cli { pub fn get_args() -> Cli {
Cli::parse() let args: Cli = Cli::parse();
if args.log {
init_logger();
}
args
} }
/// Parses a string into a floatingpoint XP modifier and validates that it /// Parses a string into a floatingpoint XP modifier and validates that it
+20 -33
View File
@@ -1,22 +1,19 @@
use ratatui::{Terminal, prelude::CrosstermBackend}; use ratatui::{Terminal, crossterm::event::KeyEvent, prelude::CrosstermBackend};
use std::{ use std::{
io::{Result, Stdout}, io::{Result, Stdout},
sync::mpsc::channel, mem::replace,
thread::{ sync::mpsc::{Receiver, Sender, channel},
self, thread::{self, JoinHandle},
// JoinHandle
},
}; };
use war_in_tunnels::{ use war_in_tunnels::{
app::{ app::{
App, App,
threads::{AppEvent, AudioCmd, handle_audio, handle_events, spawn_tick_thread}, threads::{AppChannels, AudioCmd, handle_audio, handle_ct_events, handle_tick_event},
}, },
cli::{Cli, get_args}, cli::{Cli, get_args},
logs::init_logger,
}; };
const TICK_MS: u64 = 33; const TICK_MS: u8 = 33;
/// Starts the terminal UI application. /// Starts the terminal UI application.
/// ///
@@ -25,43 +22,33 @@ const TICK_MS: u64 = 33;
/// terminal, or while running the `App`. /// terminal, or while running the `App`.
fn main() -> Result<()> { fn main() -> Result<()> {
let args: Cli = get_args(); let args: Cli = get_args();
if args.log {
init_logger();
}
let (app_event_tx, app_event_rx) = channel::<AppEvent>(); let mut channels: AppChannels = AppChannels::new();
// let app_event_thread: JoinHandle<()> = thread::spawn(move || { let input_tx: Sender<KeyEvent> = channels.input_tx.clone();
// handle_events(app_event_tx); let resize_tx: Sender<(u16, u16)> = channels.resize_tx.clone();
// }); let events_thread: JoinHandle<()> =
thread::spawn(move || handle_ct_events(input_tx, resize_tx));
thread::spawn(move || { let tick_tx: Sender<()> = channels.tick_tx.clone();
handle_events(app_event_tx.clone()); let tick_thread: JoinHandle<()> = thread::spawn(move || handle_tick_event(tick_tx, TICK_MS));
});
thread::spawn(move || {
spawn_tick_thread(app_event_tx, TICK_MS);
});
let (audio_tx, audio_rx) = channel::<AudioCmd>();
// let audio_event_thread: JoinHandle<()> = thread::spawn(move || {
// handle_audio(audio_rx);
// });
let audio_rx: Receiver<AudioCmd> = replace(&mut channels.audio_rx, channel().1);
// let audio_thread: JoinHandle<()> =
thread::spawn(move || { thread::spawn(move || {
handle_audio(audio_rx, args.mute, args.sound_track); handle_audio(audio_rx, args.mute, args.sound_track);
}); });
let mut terminal: Terminal<CrosstermBackend<Stdout>> = ratatui::init(); let mut terminal: Terminal<CrosstermBackend<Stdout>> = ratatui::init();
let mut app: App = App::new(args, audio_tx); let mut app: App = App::new(args, channels.audio_tx.clone());
let app_result: Result<()> = app.run(&mut terminal, app_event_rx); let app_result: Result<()> = app.run(&mut terminal, channels);
ratatui::restore(); ratatui::restore();
// let _ = app_event_thread.; let _ = events_thread.join();
// let _ = audio_event_thread.join(); let _ = tick_thread.join();
// let _ = audio_thread.join(); // TODO: kill playing music
app_result app_result
} }