main: switch the app to use a message sink for UI messages, even though this can be blocking

This commit is contained in:
2026-06-03 22:21:39 +02:00
parent 9c2023f6ca
commit 57e3ff9b55
+31 -27
View File
@@ -84,19 +84,22 @@ struct StageEventArgs {
#[derive(Debug)] #[derive(Debug)]
struct App { struct App {
scene: Scene, scene: Scene,
end_time: DateTime<Utc>,
next_reply_options: Vec<PossibleResponse>, next_reply_options: Vec<PossibleResponse>,
reply_state: ListState, reply_state: ListState,
conversation_state: ListState, conversation_state: ListState,
user_input: Input, user_input: Input,
end_time: DateTime<Utc>,
throbber_state: ThrobberState, throbber_state: ThrobberState,
prediction_request_sink: watch::Sender<Scene>,
is_requesting: bool, is_requesting: bool,
audio_level: f64, audio_level: f64,
recording_audio: bool, recording_audio: bool,
audio_control_sink: watch::Sender<AudioRecordRequest>,
focus_state: FocusState, focus_state: FocusState,
tts_request_sink: mpsc::Sender<String>
audio_control_sink: watch::Sender<AudioRecordRequest>,
prediction_request_sink: watch::Sender<Scene>,
tts_request_sink: mpsc::Sender<String>,
sys_message_sink: mpsc::Sender<String>
} }
#[derive(Debug)] #[derive(Debug)]
@@ -106,7 +109,7 @@ enum FocusState {
} }
impl App { impl App {
fn new(prediction_request_sink: watch::Sender<Scene>, audio_control_sink: watch::Sender<AudioRecordRequest>, tts_request_sink: mpsc::Sender<String>) -> Self { fn new(prediction_request_sink: watch::Sender<Scene>, audio_control_sink: watch::Sender<AudioRecordRequest>, tts_request_sink: mpsc::Sender<String>, sys_message_sink: mpsc::Sender<String>) -> Self {
Self { Self {
scene: Scene::default(), scene: Scene::default(),
next_reply_options: Vec::new(), next_reply_options: Vec::new(),
@@ -121,7 +124,8 @@ impl App {
recording_audio: false, recording_audio: false,
audio_control_sink, audio_control_sink,
focus_state: FocusState::UserInput, focus_state: FocusState::UserInput,
tts_request_sink tts_request_sink,
sys_message_sink
} }
} }
@@ -355,65 +359,65 @@ impl App {
match command { match command {
"/bandcamp" => { "/bandcamp" => {
self.add_bandcamp_artifact(arg).await; self.add_bandcamp_artifact(arg).await;
self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Added Bandcamp artifact from {}", arg))); self.sys_message_sink.send(format!("Added Bandcamp artifact from {}", arg)).await.unwrap();
self.scene.insert_conversation(ConversationEntry::ShipComputer(format!("Incoming transmission."))); self.scene.insert_conversation(ConversationEntry::ShipComputer(format!("Incoming transmission.")));
}, },
"/episode" => { "/episode" => {
if let Ok(episode_number) = arg.trim().parse::<u32>() { if let Ok(episode_number) = arg.trim().parse::<u32>() {
self.scene.direction.episode_number = episode_number; self.scene.direction.episode_number = episode_number;
self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Updated episode number: {}", self.scene.direction.episode_number))); self.sys_message_sink.send(format!("Updated episode number: {}", self.scene.direction.episode_number)).await.unwrap();
self.reload_mixxx_playlist(); self.reload_mixxx_playlist();
} else { } else {
self.scene.insert_conversation(ConversationEntry::SystemMessage("Invalid episode number format. Use /episode [number]".into())); self.sys_message_sink.send("Invalid episode number format. Use /episode [number]".into()).await.unwrap();
} }
}, },
"/reload" => { "/reload" => {
self.load(); self.load().await;
self.reload_mixxx_playlist(); self.reload_mixxx_playlist();
}, },
"/timer" => { "/timer" => {
if let Ok(minutes) = arg.trim().parse::<i64>() { if let Ok(minutes) = arg.trim().parse::<i64>() {
self.end_time = Utc::now() + Duration::minutes(minutes); self.end_time = Utc::now() + Duration::minutes(minutes);
self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Set timer for {} minutes.", minutes))); self.sys_message_sink.send(format!("Set timer for {} minutes.", minutes)).await.unwrap();
} else { } else {
self.scene.insert_conversation(ConversationEntry::SystemMessage("Invalid timer format. Use /timer [minutes]".into())); self.sys_message_sink.send("Invalid timer format. Use /timer [minutes]".into()).await.unwrap();
} }
} }
"/clear" => { "/clear" => {
match arg.trim() { match arg.trim() {
"playlist" => { "playlist" => {
self.scene.direction.current_playlist.clear(); self.scene.direction.current_playlist.clear();
self.scene.insert_conversation(ConversationEntry::SystemMessage("Cleared current playlist.".into())); self.sys_message_sink.send("Cleared current playlist.".into()).await.unwrap();
return; return;
}, },
"artifacts" => { "artifacts" => {
self.scene.direction.artifacts.clear(); self.scene.direction.artifacts.clear();
self.scene.insert_conversation(ConversationEntry::SystemMessage("Cleared artifacts.".into())); self.sys_message_sink.send("Cleared artifacts.".into()).await.unwrap();
return; return;
}, },
"all" => { "all" => {
self.scene = Scene::default(); self.scene = Scene::default();
self.scene.insert_conversation(ConversationEntry::SystemMessage("Cleared all data.".into())); self.sys_message_sink.send("Cleared all data.".into()).await.unwrap();
}, },
"conversation" => { "conversation" => {
self.scene.conversation.clear(); self.scene.conversation.clear();
self.scene.insert_conversation(ConversationEntry::SystemMessage("Cleared conversation.".into())); self.sys_message_sink.send("Cleared conversation.".into()).await.unwrap();
}, },
_ => { _ => {
self.scene.insert_conversation(ConversationEntry::SystemMessage("Unknown clear command. Use /clear [playlist|artifacts|all]".into())); self.sys_message_sink.send("Unknown clear command. Use /clear [playlist|artifacts|all]".into()).await.unwrap();
} }
} }
return; return;
}, },
"/narrative" => { "/narrative" => {
self.scene.direction.narrative = arg.to_string(); self.scene.direction.narrative = arg.to_string();
self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Updated stage direction: {}", self.scene.direction.narrative))); self.sys_message_sink.send(format!("Updated stage direction: {}", self.scene.direction.narrative)).await.unwrap();
}, },
"/event" => { "/event" => {
self.scene.insert_conversation(ConversationEntry::StageDirection(arg.to_string())); self.scene.insert_conversation(ConversationEntry::StageDirection(arg.to_string()));
} }
_ => { _ => {
self.scene.insert_conversation(ConversationEntry::SystemMessage("Unknown command. Available commands: /bandcamp [url], /episode [number], /narrative [text], /reset".into())); self.sys_message_sink.send("Unknown command. Available commands: /bandcamp [url], /episode [number], /narrative [text], /reset".into()).await.unwrap();
return; return;
} }
} }
@@ -445,15 +449,15 @@ impl App {
std::fs::write("save.json", save_data).unwrap(); std::fs::write("save.json", save_data).unwrap();
} }
fn load(&mut self) { async fn load(&mut self) {
if let Ok(save_data) = std::fs::read_to_string("save.json") { if let Ok(save_data) = std::fs::read_to_string("save.json") {
if let Ok(scene) = serde_json::from_str(&save_data) { if let Ok(scene) = serde_json::from_str(&save_data) {
self.scene = scene; self.scene = scene;
// FIXME: These should get wiped out when we save as well, or even better, be completely excluded via a custom serde implementation. // FIXME: These should get wiped out when we save as well, or even better, be completely excluded via a custom serde implementation.
self.scene.conversation.retain(|line| { if let ConversationEntry::SystemMessage(_) = line { false } else { true }}); self.scene.conversation.retain(|line| { if let ConversationEntry::SystemMessage(_) = line { false } else { true }});
self.scene.insert_conversation(ConversationEntry::SystemMessage("Loaded stored session.".into())); self.sys_message_sink.send("Loaded stored session.".into()).await.unwrap();
} else { } else {
self.scene.insert_conversation(ConversationEntry::SystemMessage("Failed to load saved session!".into())); self.sys_message_sink.send("Failed to load saved session!".into()).await.unwrap();
} }
} }
} }
@@ -534,13 +538,13 @@ async fn main() {
let mut terminal: Terminal<CrosstermBackend<std::io::Stdout>> = ratatui::init(); let mut terminal: Terminal<CrosstermBackend<std::io::Stdout>> = ratatui::init();
let (sys_message_sender, mut sys_message_receiver) = tokio::sync::mpsc::channel(5); let (sys_message_sink, mut sys_message_src) = tokio::sync::mpsc::channel(32);
let tts_request_sender = start_tts().await; let tts_request_sender = start_tts().await;
let (prediction_request_in, mut prediction_out) = prediction::start_prediction().await; let (prediction_request_in, mut prediction_out) = prediction::start_prediction().await;
let (mut audio_state_receiver, audio_control_in, mut transcription_out) = transcription::start_transcription(sys_message_sender).await; let (mut audio_state_receiver, audio_control_in, mut transcription_out) = transcription::start_transcription(sys_message_sink.clone()).await;
let mut app = App::new(prediction_request_in, audio_control_in, tts_request_sender); let mut app = App::new(prediction_request_in, audio_control_in, tts_request_sender, sys_message_sink);
app.load(); app.load().await;
let mut events = EventStream::new(); let mut events = EventStream::new();
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
@@ -564,7 +568,7 @@ async fn main() {
_ = audio_state_receiver.changed() => { _ = audio_state_receiver.changed() => {
app.audio_level = *audio_state_receiver.borrow_and_update(); app.audio_level = *audio_state_receiver.borrow_and_update();
}, },
maybe_message = sys_message_receiver.recv() => { maybe_message = sys_message_src.recv() => {
if let Some(message) = maybe_message { if let Some(message) = maybe_message {
app.scene.insert_conversation(ConversationEntry::SystemMessage(message)); app.scene.insert_conversation(ConversationEntry::SystemMessage(message));
} }