diff --git a/README.md b/README.md index ce47d08..a7b46e2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ # Deckster -## Terminology + +## Contributing +### Terminology - `handler runner`: Node that is running handlers. - `handler host`: A `handler runner` that is not the `coordinator`. - `coordinator`: Node to which the Loupedeck device is physically connected. Always a `handler runner`. +### The different types of `unwrap` +- `expect("")`: The author thinks that unwrapping will never fail because of ``. +- `unwrap()`: The author assumes that unwrapping will never fail but explaining why is either obvious or too complicated. +- `unwrap_todo()`: The author has not yet thought about how to handle this value being `None` or `Err`. + They will replace this unwrapping with `expect("")`, `unwrap()`, or proper error handling later. + ## Attribution [foxxyz’s `loupedeck` library for JavaScript](https://github.com/foxxyz/loupedeck) (licensed under the [MIT license](https://github.com/foxxyz/loupedeck/blob/e41e5d920130d9ef651e47173c68450b9c832b96/LICENSE)) diff --git a/crates/deckster_mode/src/lib.rs b/crates/deckster_mode/src/lib.rs index f30d055..fa82932 100644 --- a/crates/deckster_mode/src/lib.rs +++ b/crates/deckster_mode/src/lib.rs @@ -63,11 +63,17 @@ pub fn run< match initial_message { Ok(initial_message) => match init_handler(initial_message) { Ok(h) => { - println!("{}", serde_json::to_string(&HandlerInitializationResultMessage::Ready).unwrap()); + println!( + "{}", + serde_json::to_string(&HandlerInitializationResultMessage::Ready).expect("serialization of a known value always works") + ); handler = Either::Left(h) } Err(error) => { - println!("{}", serde_json::to_string(&HandlerInitializationResultMessage::Error { error }).unwrap()); + println!( + "{}", + serde_json::to_string(&HandlerInitializationResultMessage::Error { error }).expect("no reason to fail") + ); return Ok(()); } }, @@ -81,7 +87,7 @@ pub fn run< message: err.to_string().into_boxed_str(), } }) - .unwrap() + .expect("no reason to fail") ); return Ok(()); } @@ -94,5 +100,5 @@ pub fn run< } pub fn send_command(command: HandlerCommand) { - println!("{}", serde_json::to_string(&command).unwrap()); + println!("{}", serde_json::to_string(&command).expect("no reason to fail")); } diff --git a/src/coordinator/graphics.rs b/src/coordinator/graphics.rs index 67d86be..1d549c0 100644 --- a/src/coordinator/graphics.rs +++ b/src/coordinator/graphics.rs @@ -20,7 +20,7 @@ pub struct GraphicsContext { } pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&Key>) -> Bytes { - let mut pixmap = Pixmap::new(key_size.width(), key_size.height()).unwrap(); + let mut pixmap = Pixmap::new(key_size.width(), key_size.height()).expect("constraints were already asserted by IntSize"); if let Some(state) = state { let style = state.style.as_ref().map(|s| s.merge_over(&state.base_style)); @@ -39,7 +39,8 @@ pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&K } if let Some(color) = style.border { - let path = PathBuilder::from_rect(Rect::from_xywh(-1.0, -2.0, pixmap.width() as f32, pixmap.height() as f32).unwrap()); + let path = + PathBuilder::from_rect(Rect::from_xywh(-1.0, -2.0, pixmap.width() as f32, pixmap.height() as f32).expect("width and height are not negative")); let paint = Paint { shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, color.a)), @@ -62,7 +63,7 @@ pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&K } pub fn render_knob(context: &GraphicsContext, screen_size: IntSize, state: Option<&Knob>) -> Bytes { - let mut pixmap = Pixmap::new(screen_size.width(), screen_size.height()).unwrap(); + let mut pixmap = Pixmap::new(screen_size.width(), screen_size.height()).expect("constraints were already asserted by IntSize"); if let Some(state) = state { let style = state.style.as_ref().map(|s| s.merge_over(&state.base_style)); @@ -104,7 +105,7 @@ pub fn render_knob(context: &GraphicsContext, screen_size: IntSize, state: Optio let y = pixmap.height() as f32 - PADDING_Y - height; pixmap.fill_rect( - Rect::from_xywh(x, y, WIDTH, height).unwrap(), + Rect::from_xywh(x, y, WIDTH, height).expect("WIDTH and height are not negative"), &Paint { shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, color.a)), ..Paint::default() @@ -192,7 +193,7 @@ pub mod labels { cosmic_text::Color::rgb(255, 255, 255), |x, y, w, h, color| { pixmap.fill_rect( - Rect::from_xywh(x as f32 + PADDING, y as f32 + PADDING, w as f32, h as f32).unwrap(), + Rect::from_xywh(x as f32 + PADDING, y as f32 + PADDING, w as f32, h as f32).expect("w and h are unsigned -> not negative"), &Paint { shader: Shader::SolidColor(Color::from_rgba8(color.r(), color.g(), color.b(), color.a())), ..Paint::default() diff --git a/src/coordinator/io_worker.rs b/src/coordinator/io_worker.rs index 67ad4fd..a7c024f 100644 --- a/src/coordinator/io_worker.rs +++ b/src/coordinator/io_worker.rs @@ -11,7 +11,7 @@ use tokio::sync::broadcast; use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent, KeyEvent, KeyTouchEventKind, KnobEvent}; use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition}; use deckster_shared::state::{Key, Knob}; -use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics, LoupedeckDisplayRect, LoupedeckKnob}; +use loupedeck_serial::characteristics::{LoupedeckDeviceKeyGridCharacteristics, LoupedeckDisplayRect, LoupedeckKnob}; use loupedeck_serial::device::LoupedeckDevice; use loupedeck_serial::events::{LoupedeckEvent, RotationDirection}; @@ -68,7 +68,9 @@ pub fn do_io_work(context: IoWorkerContext, commands_receiver: flume::Receiver) { let key_grid = &context.device.characteristics().key_grid; - let rect = key_grid.get_local_key_rect(index).unwrap(); + let rect = key_grid.get_local_key_rect(index).expect("index is assumed to be valid"); - let buffer = render_key(&context.graphics, IntSize::from_wh(rect.w as u32, rect.h as u32).unwrap(), key); + let buffer = render_key( + &context.graphics, + IntSize::from_wh(rect.w as u32, rect.h as u32).expect("rect.w and rect.h are not zero"), + key, + ); context .device .replace_framebuffer_area_raw(&key_grid.display, rect.x, rect.y, rect.w, rect.h, buffer) @@ -336,7 +344,11 @@ fn draw_knob(context: &IoWorkerContext, position: KnobPosition, knob: Option<&Kn KnobPosition::RightMiddle => LoupedeckKnob::RightMiddle, KnobPosition::RightBottom => LoupedeckKnob::RightBottom, }) { - let buffer = render_knob(&context.graphics, IntSize::from_wh(rect.w as u32, rect.h as u32).unwrap(), knob); + let buffer = render_knob( + &context.graphics, + IntSize::from_wh(rect.w as u32, rect.h as u32).expect("rect.w and rect.h are not zero."), + knob, + ); context .device .replace_framebuffer_area_raw(display, rect.x, rect.y, rect.w, rect.h, buffer) diff --git a/src/coordinator/mqtt.rs b/src/coordinator/mqtt.rs index 7c166d9..ab907eb 100644 --- a/src/coordinator/mqtt.rs +++ b/src/coordinator/mqtt.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use std::time::Duration; use log::{error, info}; -use rumqttc::{ConnectionError, Event, Incoming, LastWill, MqttOptions, QoS}; +use rumqttc::{Event, Incoming, LastWill, MqttOptions, QoS}; use tokio::sync::broadcast; use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent}; @@ -121,53 +121,61 @@ pub async fn start_mqtt_client( match segments[0] { "keys" => { if property == "style" { - let value = serde_json::from_slice::>(&event.payload).unwrap(); - - commands_sender - .send_async(HandlerCommand::SetKeyStyle { - path: KeyPath { - page_id: page_id.to_owned(), - position: KeyPosition::from_str(position).unwrap(), - }, - value, - }) - .await - .unwrap(); + if let Ok(position) = KeyPosition::from_str(position) { + if let Ok(value) = serde_json::from_slice::>(&event.payload) { + commands_sender + .send_async(HandlerCommand::SetKeyStyle { + path: KeyPath { + page_id: page_id.to_owned(), + position, + }, + value, + }) + .await + .unwrap(); + } else { + log::error!("Could not deserialize the latest key style object from {}", event.topic); + } + } else { + log::warn!("Invalid key position in topic name: {}", event.topic); + } } } "knobs" => { - let position = KnobPosition::from_str(position).unwrap(); - - match property { - "style" => { - let value = serde_json::from_slice::>(&event.payload).unwrap(); - - commands_sender - .send_async(HandlerCommand::SetKnobStyle { - path: KnobPath { - page_id: page_id.to_owned(), - position, - }, - value, - }) - .await - .unwrap(); + if let Ok(position) = KnobPosition::from_str(position) { + match property { + "style" => { + if let Ok(value) = serde_json::from_slice::>(&event.payload) { + commands_sender + .send_async(HandlerCommand::SetKnobStyle { + path: KnobPath { + page_id: page_id.to_owned(), + position, + }, + value, + }) + .await + .unwrap(); + } + } + "value" => { + if let Ok(value) = serde_json::from_slice::>(&event.payload) { + commands_sender + .send_async(HandlerCommand::SetKnobValue { + path: KnobPath { + page_id: page_id.to_owned(), + position, + }, + value, + }) + .await + .unwrap(); + } + } + _ => {} } - "value" => { - let value = serde_json::from_slice::>(&event.payload).unwrap(); - - commands_sender - .send_async(HandlerCommand::SetKnobValue { - path: KnobPath { - page_id: page_id.to_owned(), - position, - }, - value, - }) - .await - .unwrap(); - } - _ => {} + } else { + log::warn!("Invalid knob position in topic name: {}", event.topic); } } _ => {} diff --git a/src/handler_host/mod.rs b/src/handler_host/mod.rs index 86b8a6a..d0d30dd 100644 --- a/src/handler_host/mod.rs +++ b/src/handler_host/mod.rs @@ -1,7 +1,7 @@ use std::path::Path; use color_eyre::Result; -use log::{debug, info, trace, warn}; +use log::{info, trace, warn}; use tokio::sync::{broadcast, mpsc}; use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent}; @@ -28,7 +28,7 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> { None => { if is_running { info!("Stopping handlers…"); - events_sender.send(HandlerEvent::Stop).unwrap(); + events_sender.send(HandlerEvent::Stop).ok(); // only fails when all handlers are already stopped. } is_running = false; @@ -36,7 +36,7 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> { Some(handler_hosts_config) => { if is_running { warn!("A new configuration was received before the old one was cleared."); - events_sender.send(HandlerEvent::Stop).unwrap(); + events_sender.send(HandlerEvent::Stop).ok(); // only fails when all handlers are already stopped. } is_running = true; diff --git a/src/handler_host/mqtt.rs b/src/handler_host/mqtt.rs index 5f5fca2..963aa1a 100644 --- a/src/handler_host/mqtt.rs +++ b/src/handler_host/mqtt.rs @@ -45,7 +45,10 @@ pub async fn start_mqtt_client( is_first_try = false; } else if is_connected { error!("MQTT connection lost: {error}"); - handler_hosts_config_sender.send(None).await.unwrap(); + handler_hosts_config_sender + .send(None) + .await + .expect("this channel is not closed for the entire lifetime of the program"); } is_connected = false; @@ -63,13 +66,14 @@ pub async fn start_mqtt_client( client.subscribe(format!("{topic_prefix}/knobs/+/+/events"), QoS::ExactlyOnce).await.unwrap(); } Incoming::Publish(event) => { - let topic_segments = event.topic.strip_prefix(&topic_prefix).unwrap().split('/').skip(1).collect::>(); + let topic_name = event.topic; + let topic_segments = topic_name.strip_prefix(&topic_prefix).unwrap().split('/').skip(1).collect::>(); if topic_segments[0] == "config" { if let Ok(config) = serde_json::from_slice(&event.payload) { handler_hosts_config_sender.send(config).await.unwrap(); } else { - log::error!("Could not deserialize the latest configuration from {}", event.topic); + log::error!("Could not deserialize the latest configuration from {}", topic_name); handler_hosts_config_sender.send(None).await.unwrap(); }; } else { @@ -80,32 +84,40 @@ pub async fn start_mqtt_client( match topic_segments[0] { "keys" if property == "events" => { if let Ok(event) = serde_json::from_slice(&event.payload) { - events_sender - .send(HandlerEvent::Key { + if let Ok(position) = KeyPosition::from_str(position) { + // This can be Err when events are received before the configuration + // but in that case we just ignore the event. + _ = events_sender.send(HandlerEvent::Key { path: KeyPath { page_id: page_id.to_owned(), - position: KeyPosition::from_str(position).unwrap(), + position, }, event, }) - .ok(); // This can be Err when events are received before the configuration + } else { + log::warn!("Invalid key position in topic name: {topic_name}"); + } } else { - log::error!("Could not deserialize the latest event from {}", event.topic); + log::error!("Could not deserialize the latest event from {topic_name}"); }; } "knobs" if property == "events" => { if let Ok(event) = serde_json::from_slice(&event.payload) { - events_sender - .send(HandlerEvent::Knob { + if let Ok(position) = KnobPosition::from_str(position) { + // This can be Err when events are received before the configuration + // but in that case we just ignore the event. + _ = events_sender.send(HandlerEvent::Knob { path: KnobPath { page_id: page_id.to_owned(), - position: KnobPosition::from_str(position).unwrap(), + position, }, event, - }) - .ok(); // This can be Err when events are received before the configuration + }); + } else { + log::warn!("Invalid knob position in topic name: {topic_name}"); + } } else { - log::error!("Could not deserialize the latest event from {}", event.topic); + log::error!("Could not deserialize the latest event from {}", topic_name); }; } _ => {} diff --git a/src/handler_runner.rs b/src/handler_runner.rs index 49c81ba..cdb0388 100644 --- a/src/handler_runner.rs +++ b/src/handler_runner.rs @@ -36,7 +36,7 @@ pub async fn start( if let Ok(entry) = entry { let path = entry.path(); if path.is_executable() { - return Some(path.file_name().unwrap().to_os_string()); + return Some(path.file_name().expect("the path does not end in `..`").to_os_string()); } } @@ -79,88 +79,25 @@ pub async fn start( for handler_name in handler_names { let handler_name = handler_name .into_string() - .map_err(|_| eyre!("Command names must be valid Unicode."))? + .map_err(|string| { + eyre!( + "Handler names must be valid Unicode. Offending name (with non-Unicode characters replaced): {}", + string.to_string_lossy() + ) + })? .into_boxed_str(); - let (key_configs, knob_configs) = match ( - handler_config_by_key_path_by_handler_name.remove(&handler_name), - handler_config_by_knob_path_by_handler_name.remove(&handler_name), - ) { - (None, None) => { - warn!("Handler '{handler_name}' is not used by any key or knob."); - continue; - } - (a, b) => (a.unwrap_or_default(), b.unwrap_or_default()), - }; - - let mut command = Command::new(handlers_directory.join(handler_name.to_string())) - .arg("deckster-run") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .wrap_err_with(|| format!("while spawning handler: {handler_name}"))?; - - let mut stdout_lines = BufReader::new(command.stdout.take().unwrap()).lines(); - let mut stdin = command.stdin.take().unwrap(); - - let initial_handler_message = InitialHandlerMessage { key_configs, knob_configs }; - - let serialized_message = serde_json::to_string(&initial_handler_message).unwrap().into_boxed_str().into_boxed_bytes(); - - stdin.write_all(&serialized_message).await.unwrap(); - stdin.write_u8(b'\n').await.unwrap(); - stdin.flush().await.unwrap(); - - let result_line = stdout_lines.next_line().await?.unwrap(); - let result: HandlerInitializationResultMessage = serde_json::from_str(&result_line)?; - - if let HandlerInitializationResultMessage::Error { error } = result { - #[rustfmt::skip] - if let HandlerInitializationError::InvalidConfig { supports_keys, supports_knobs, .. } = error { - if !supports_keys && !initial_handler_message.key_configs.is_empty() { - return Err(eyre!( - "The '{handler_name}' handler does not support keys, but these keys tried to use it: {}", - initial_handler_message.key_configs.keys().map(|k| k.to_string()).join(", ") - )); - } else if !supports_knobs && !initial_handler_message.knob_configs.is_empty() { - return Err(eyre!( - "The '{handler_name}' handler does not support knobs, but these knobs tried to use it: {}", - initial_handler_message.knob_configs.keys().map(|k| k.to_string()).join(", ") - )); - } - }; - - return Err(eyre!("Starting the '{handler_name}' handler failed: {error}")); - } - - handler_stdin_by_name.insert(handler_name.clone(), stdin); - - let commands_sender = commands_sender.clone(); - tokio::spawn(async move { - while let Ok(Some(line)) = stdout_lines.next_line().await { - if line.starts_with('{') { - let command = serde_json::from_str::(&line).unwrap(); - - commands_sender.send_async(command).await.unwrap(); - } else { - println!("{}", line); - } - } - }); - - let should_stop = Arc::clone(&should_stop); - tokio::spawn(async move { - let exit_status = command.wait().await.unwrap(); - - if !should_stop.load(Ordering::Relaxed) { - match exit_status.code() { - None => error!("The '{handler_name}' handler was unexpectedly terminated by a signal."), - Some(code) => error!("The '{handler_name}' handler exited unexpectedly with status code {code}"), - } - } else { - debug!("The '{handler_name}' handler exited: {exit_status:#?}"); - } - }); + start_handler( + handlers_directory, + &mut handler_config_by_key_path_by_handler_name, + &mut handler_config_by_knob_path_by_handler_name, + &mut handler_stdin_by_name, + commands_sender.clone(), + Arc::clone(&should_stop), + handler_name.clone(), + ) + .await + .wrap_err_with(|| format!("while starting handler: {handler_name}"))?; } if let Some((handler_name, config_by_key_path)) = handler_config_by_key_path_by_handler_name.drain().next() { @@ -179,40 +116,162 @@ pub async fn start( tokio::spawn(async move { while let Ok(event) = events_receiver.recv().await { - let handler_name = match &event { - HandlerEvent::Stop => { - should_stop.store(true, Ordering::Relaxed); - let serialized_event = serde_json::to_string(&event).unwrap().into_boxed_str().into_boxed_bytes(); - for handler_stdin in handler_stdin_by_name.values_mut() { - handler_stdin.write_all(&serialized_event).await.unwrap(); - handler_stdin.write_u8(b'\n').await.unwrap(); - handler_stdin.flush().await.unwrap(); - } + let result = forward_event_to_handler( + &should_stop, + &mut handler_stdin_by_name, + &handler_name_by_key_path, + &handler_name_by_knob_path, + event, + ) + .await; - break; + match result { + Err(error) => { + error!("Failed to forward an event to its handler: {error}") } - HandlerEvent::Key { path, .. } => handler_name_by_key_path.get(path), - HandlerEvent::Knob { path, .. } => handler_name_by_knob_path.get(path), - }; - - let handler_name = if let Some(n) = handler_name { - n - } else { - continue; - }; - - let handler_stdin = handler_stdin_by_name.get_mut(handler_name).expect("was already checked above"); - let serialized_event = serde_json::to_string(&event).unwrap().into_boxed_str().into_boxed_bytes(); - - handler_stdin.write_all(&serialized_event).await.unwrap(); - handler_stdin.write_u8(b'\n').await.unwrap(); - handler_stdin.flush().await.unwrap(); + Ok(should_continue) => { + if !should_continue { + break; + } + } + } } }); Ok(()) } +async fn start_handler( + handlers_directory: &Path, + handler_config_by_key_path_by_handler_name: &mut HashMap, HashMap>>, + handler_config_by_knob_path_by_handler_name: &mut HashMap, HashMap>>, + handler_stdin_by_name: &mut HashMap, ChildStdin>, + commands_sender: flume::Sender, + should_stop: Arc, + handler_name: Box, +) -> Result<()> { + let (key_configs, knob_configs) = match ( + handler_config_by_key_path_by_handler_name.remove(&handler_name), + handler_config_by_knob_path_by_handler_name.remove(&handler_name), + ) { + (None, None) => { + warn!("Handler '{handler_name}' is not used by any key or knob."); + return Ok(()); + } + (a, b) => (a.unwrap_or_default(), b.unwrap_or_default()), + }; + + let mut command = Command::new(handlers_directory.join(handler_name.to_string())) + .arg("deckster-run") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .wrap_err("while spawning the handler process")?; + + let mut stdout_lines = BufReader::new(command.stdout.take().expect("stdout is explicitly captured and has not yet been taken")).lines(); + let mut stdin = command.stdin.take().expect("stdin is explicitly captured and has not yet been taken"); + + let initial_handler_message = InitialHandlerMessage { key_configs, knob_configs }; + + let serialized_message = serde_json::to_string(&initial_handler_message).unwrap().into_boxed_str().into_boxed_bytes(); + + stdin.write_all(&serialized_message).await.wrap_err("while writing to stdin")?; + stdin.write_u8(b'\n').await.wrap_err("while writing to stdin")?; + stdin.flush().await.wrap_err("while flushing stdin")?; + + // TODO: It is not clear when the returned Option is None. We just unwrap and hope for the best. + let result_line = stdout_lines.next_line().await.wrap_err("while reading from stdout")?.unwrap(); + let result: HandlerInitializationResultMessage = serde_json::from_str(&result_line)?; + + if let HandlerInitializationResultMessage::Error { error } = result { + #[rustfmt::skip] + if let HandlerInitializationError::InvalidConfig { supports_keys, supports_knobs, .. } = error { + if !supports_keys && !initial_handler_message.key_configs.is_empty() { + return Err(eyre!( + "The '{handler_name}' handler does not support keys, but these keys tried to use it: {}", + initial_handler_message.key_configs.keys().map(|k| k.to_string()).join(", ") + )); + } else if !supports_knobs && !initial_handler_message.knob_configs.is_empty() { + return Err(eyre!( + "The '{handler_name}' handler does not support knobs, but these knobs tried to use it: {}", + initial_handler_message.knob_configs.keys().map(|k| k.to_string()).join(", ") + )); + } + }; + + return Err(eyre!("Starting the '{handler_name}' handler failed: {error}")); + } + + handler_stdin_by_name.insert(handler_name.clone(), stdin); + + tokio::spawn(async move { + while let Ok(Some(line)) = stdout_lines.next_line().await { + if line.starts_with('{') { + if let Ok(command) = serde_json::from_str::(&line) { + commands_sender.send_async(command).await.unwrap(); + } else { + log::error!("Failed to deserialize command object: {line}"); + } + } else { + println!("{}", line); + } + } + }); + + let should_stop = Arc::clone(&should_stop); + tokio::spawn(async move { + let exit_status = command.wait().await.unwrap(); + + if !should_stop.load(Ordering::Relaxed) { + match exit_status.code() { + None => error!("The '{handler_name}' handler was unexpectedly terminated by a signal."), + Some(code) => error!("The '{handler_name}' handler exited unexpectedly with status code {code}"), + } + } else { + debug!("The '{handler_name}' handler exited: {exit_status:#?}"); + } + }); + + Ok(()) +} + +async fn forward_event_to_handler( + should_stop: &AtomicBool, + handler_stdin_by_name: &mut HashMap, ChildStdin>, + handler_name_by_key_path: &HashMap>, + handler_name_by_knob_path: &HashMap>, + event: HandlerEvent, +) -> Result { + let handler_name = match &event { + HandlerEvent::Stop => { + should_stop.store(true, Ordering::Relaxed); + let serialized_event = serde_json::to_string(&event).unwrap().into_boxed_str().into_boxed_bytes(); + for handler_stdin in handler_stdin_by_name.values_mut() { + // We assume that stdin being closed means that the handler process is no longer running. + // If that is the case, we do not need to stop it as it is already stopped. + _ = handler_stdin.write_all(&serialized_event).await; + _ = handler_stdin.write_u8(b'\n').await; + _ = handler_stdin.flush().await; + } + + return Ok(false); + } + HandlerEvent::Key { path, .. } => handler_name_by_key_path.get(path), + HandlerEvent::Knob { path, .. } => handler_name_by_knob_path.get(path), + }; + + if let Some(handler_name) = handler_name { + let handler_stdin = handler_stdin_by_name.get_mut(handler_name).expect("was already checked above"); + let serialized_event = serde_json::to_string(&event).unwrap().into_boxed_str().into_boxed_bytes(); + + handler_stdin.write_all(&serialized_event).await.wrap_err("while writing to stdin")?; + handler_stdin.write_u8(b'\n').await.wrap_err("while writing to stdin")?; + handler_stdin.flush().await.wrap_err("while flushing stdin")?; + } + + Ok(true) +} + #[derive(Debug, Serialize, Deserialize)] pub struct KeyOrKnobHandlerConfig { pub host_id: Box, diff --git a/src/icons/mod.rs b/src/icons/mod.rs index cc2ba00..338869f 100644 --- a/src/icons/mod.rs +++ b/src/icons/mod.rs @@ -1,3 +1,4 @@ +use std::cmp::max; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -13,6 +14,7 @@ use deckster_shared::icon_descriptor::{IconDescriptor, IconDescriptorSource}; use deckster_shared::image_filter::ImageFilter; use crate::model::coordinator_config::{IconFormat, IconPack}; +use crate::util::UnwrapExt; mod destructive_filter; @@ -81,7 +83,10 @@ impl IconManager { let icon = self.load_icon(descriptor)?; let scale = icon.effective_filter.transform.scale; - let scaled_size = IntSize::from_wh(icon.pixmap.width(), icon.pixmap.height()).unwrap().scale_by(scale).unwrap(); + let scaled_size = IntSize::from_wh(icon.pixmap.width(), icon.pixmap.height()) + .expect("width and height are not zero") + .scale_by(scale) + .unwrap_todo(); pixmap.draw_pixmap( (((pixmap.width() as i32 - scaled_size.width() as i32) / 2) as f32 / scale).round() as i32, @@ -104,10 +109,11 @@ impl IconManager { } } +// Panics when `source` is `IconPack` and `icon_pack` is `None`. fn read_image_and_get_scale( config_directory: &Path, dpi: f32, - fonts_db: &resvg::usvg::fontdb::Database, + fonts_db: &fontdb::Database, source: &IconDescriptorSource, icon_pack: Option<&IconPack>, preferred_scale: f32, @@ -142,7 +148,7 @@ fn read_image_and_get_scale( }) } -fn read_image_from_svg(path: &Path, dpi: f32, font_db: &resvg::usvg::fontdb::Database, scale: f32) -> Result { +fn read_image_from_svg(path: &Path, dpi: f32, font_db: &fontdb::Database, scale: f32) -> Result { let raw_data = std::fs::read(path)?; let tree = { @@ -163,7 +169,11 @@ fn read_image_from_svg(path: &Path, dpi: f32, font_db: &resvg::usvg::fontdb::Dat }; let size = tree.size.to_int_size(); - let mut pixmap = Pixmap::new((size.width() as f32 * scale).ceil() as u32, (size.height() as f32 * scale).ceil() as u32).unwrap(); + let mut pixmap = Pixmap::new( + max(1, (size.width() as f32 * scale).ceil() as u32), + max(1, (size.height() as f32 * scale).ceil() as u32), + ) + .expect("width and height can never be zero."); tree.render(Transform::from_scale(scale, scale), &mut pixmap.as_mut()); diff --git a/src/main.rs b/src/main.rs index 8701858..288f372 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ mod handler_host; mod handler_runner; mod icons; mod model; +mod util; #[derive(Debug, Parser)] #[command(name = "deckster", about = "Use Loupedeck devices under Linux.")] @@ -112,7 +113,7 @@ fn read_and_deserialize_from_directory(base: &Pa let path = entry.path(); if let Some(fallback_id) = path .strip_prefix(base) - .unwrap() + .expect("all paths yielded by WalkDir are within the base directory") .to_str() .expect("Paths must be valid UTF-8") .strip_suffix(".toml") diff --git a/src/model/geometry.rs b/src/model/geometry.rs index e068dbe..8edb750 100644 --- a/src/model/geometry.rs +++ b/src/model/geometry.rs @@ -41,15 +41,20 @@ impl Display for UIntVec2 { } #[derive(Debug, Error)] -#[error("The input value does not match the required format.")] -pub struct ParsingError {} +pub enum IntRectWrapperFromStrErr { + #[error("The input value does not match the required format.")] + ParsingFailed, + + #[error("The right bottom coordinates are not strictly higher than the left top coordinates.")] + ConstraintsFailed, +} #[derive(Debug, Copy, Clone, PartialEq, Deref, From, Into, SerializeDisplay, DeserializeFromStr)] #[repr(transparent)] pub struct IntRectWrapper(IntRect); impl FromStr for IntRectWrapper { - type Err = ParsingError; + type Err = IntRectWrapperFromStrErr; fn from_str(s: &str) -> Result { let (pairs, is_corner_points_mode) = if let Some(pairs) = s.split_once('+') { @@ -57,16 +62,17 @@ impl FromStr for IntRectWrapper { } else if let Some(pairs) = s.split_once('-') { (pairs, true) } else { - return Err(ParsingError {}); + return Err(IntRectWrapperFromStrErr::ParsingFailed); }; - let first_vec = UIntVec2::from_str(pairs.0).map_err(|_| ParsingError {})?; - let second_vec = UIntVec2::from_str(pairs.1).map_err(|_| ParsingError {})?; + let first_vec = UIntVec2::from_str(pairs.0).map_err(|_| IntRectWrapperFromStrErr::ParsingFailed)?; + let second_vec = UIntVec2::from_str(pairs.1).map_err(|_| IntRectWrapperFromStrErr::ParsingFailed)?; Ok(IntRectWrapper(if is_corner_points_mode { - IntRect::from_ltrb(first_vec.x as i32, first_vec.y as i32, second_vec.x as i32, second_vec.y as i32).unwrap() + IntRect::from_ltrb(first_vec.x as i32, first_vec.y as i32, second_vec.x as i32, second_vec.y as i32) + .ok_or(IntRectWrapperFromStrErr::ConstraintsFailed)? } else { - IntRect::from_xywh(first_vec.x as i32, first_vec.y as i32, second_vec.x, second_vec.y).unwrap() + IntRect::from_xywh(first_vec.x as i32, first_vec.y as i32, second_vec.x, second_vec.y).ok_or(IntRectWrapperFromStrErr::ConstraintsFailed)? })) } } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..57646e8 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,19 @@ +use std::fmt; + +pub trait UnwrapExt { + fn unwrap_todo(self) -> T; +} + +impl UnwrapExt for Option { + #[inline] + fn unwrap_todo(self) -> T { + self.expect("TODO: Handle this Option being None") + } +} + +impl UnwrapExt for Result { + #[inline] + fn unwrap_todo(self) -> T { + self.expect("TODO: Handle this error") + } +}