Get rid of preliminary unwraps

This commit is contained in:
Moritz Ruth 2024-03-01 00:06:59 +01:00
parent 0340dddcab
commit 6c284b6365
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
12 changed files with 344 additions and 202 deletions

View file

@ -1,10 +1,18 @@
# Deckster # Deckster
## Terminology
## Contributing
### Terminology
- `handler runner`: Node that is running handlers. - `handler runner`: Node that is running handlers.
- `handler host`: A `handler runner` that is not the `coordinator`. - `handler host`: A `handler runner` that is not the `coordinator`.
- `coordinator`: Node to which the Loupedeck device is physically connected. Always a `handler runner`. - `coordinator`: Node to which the Loupedeck device is physically connected. Always a `handler runner`.
### The different types of `unwrap`
- `expect("<reason>")`: The author thinks that unwrapping will never fail because of `<reason>`.
- `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("<reason>")`, `unwrap()`, or proper error handling later.
## Attribution ## Attribution
[foxxyzs `loupedeck` library for JavaScript](https://github.com/foxxyz/loupedeck) [foxxyzs `loupedeck` library for JavaScript](https://github.com/foxxyz/loupedeck)
(licensed under the [MIT license](https://github.com/foxxyz/loupedeck/blob/e41e5d920130d9ef651e47173c68450b9c832b96/LICENSE)) (licensed under the [MIT license](https://github.com/foxxyz/loupedeck/blob/e41e5d920130d9ef651e47173c68450b9c832b96/LICENSE))

View file

@ -63,11 +63,17 @@ pub fn run<
match initial_message { match initial_message {
Ok(initial_message) => match init_handler(initial_message) { Ok(initial_message) => match init_handler(initial_message) {
Ok(h) => { 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) handler = Either::Left(h)
} }
Err(error) => { 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(()); return Ok(());
} }
}, },
@ -81,7 +87,7 @@ pub fn run<
message: err.to_string().into_boxed_str(), message: err.to_string().into_boxed_str(),
} }
}) })
.unwrap() .expect("no reason to fail")
); );
return Ok(()); return Ok(());
} }
@ -94,5 +100,5 @@ pub fn run<
} }
pub fn send_command(command: HandlerCommand) { pub fn send_command(command: HandlerCommand) {
println!("{}", serde_json::to_string(&command).unwrap()); println!("{}", serde_json::to_string(&command).expect("no reason to fail"));
} }

View file

@ -20,7 +20,7 @@ pub struct GraphicsContext {
} }
pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&Key>) -> Bytes { 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 { if let Some(state) = state {
let style = state.style.as_ref().map(|s| s.merge_over(&state.base_style)); 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 { 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 { let paint = Paint {
shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, color.a)), 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 { 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 { if let Some(state) = state {
let style = state.style.as_ref().map(|s| s.merge_over(&state.base_style)); 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; let y = pixmap.height() as f32 - PADDING_Y - height;
pixmap.fill_rect( 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 { &Paint {
shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, color.a)), shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, color.a)),
..Paint::default() ..Paint::default()
@ -192,7 +193,7 @@ pub mod labels {
cosmic_text::Color::rgb(255, 255, 255), cosmic_text::Color::rgb(255, 255, 255),
|x, y, w, h, color| { |x, y, w, h, color| {
pixmap.fill_rect( 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 { &Paint {
shader: Shader::SolidColor(Color::from_rgba8(color.r(), color.g(), color.b(), color.a())), shader: Shader::SolidColor(Color::from_rgba8(color.r(), color.g(), color.b(), color.a())),
..Paint::default() ..Paint::default()

View file

@ -11,7 +11,7 @@ use tokio::sync::broadcast;
use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent, KeyEvent, KeyTouchEventKind, KnobEvent}; use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent, KeyEvent, KeyTouchEventKind, KnobEvent};
use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition}; use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition};
use deckster_shared::state::{Key, Knob}; 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::device::LoupedeckDevice;
use loupedeck_serial::events::{LoupedeckEvent, RotationDirection}; use loupedeck_serial::events::{LoupedeckEvent, RotationDirection};
@ -68,7 +68,9 @@ pub fn do_io_work(context: IoWorkerContext, commands_receiver: flume::Receiver<H
loop { loop {
let a = flume::Selector::new() let a = flume::Selector::new()
.recv(&device_events_receiver, |e| IoWork::DeviceEvent(e.unwrap())) .recv(&device_events_receiver, |e| {
IoWork::DeviceEvent(e.expect("the device events channel is not closed by the other side"))
})
.recv(&commands_receiver, |c| IoWork::Command(c.unwrap())) .recv(&commands_receiver, |c| IoWork::Command(c.unwrap()))
.wait(); .wait();
@ -130,7 +132,10 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv
let LoupedeckDisplayRect { let LoupedeckDisplayRect {
x: top_left_x, y: top_left_y, .. x: top_left_x, y: top_left_y, ..
} = characteristics.key_grid.get_local_key_rect(key_index).unwrap(); } = characteristics
.key_grid
.get_local_key_rect(key_index)
.expect("the key index is valid because is was returned by get_key_at_global_coordinates");
let kind = if is_end { let kind = if is_end {
state.active_touch_ids.remove(&touch_id); state.active_touch_ids.remove(&touch_id);
@ -202,13 +207,11 @@ fn handle_command(context: &IoWorkerContext, state: &mut State, command: Handler
state.active_key_page_id = key_page_id; state.active_key_page_id = key_page_id;
state.active_knob_page_id = knob_page_id; state.active_knob_page_id = knob_page_id;
for button in LoupedeckButton::VARIANTS { for button in context.device.characteristics().available_buttons {
let position = ButtonPosition::of(button);
context context
.device .device
.set_button_color(*button, get_correct_button_color(context, state, position)) .set_button_color(button, get_correct_button_color(context, state, ButtonPosition::of(&button)))
.unwrap(); .expect("the button is available for this device because that is literally what we are iterating over");
} }
let key_grid = &context.device.characteristics().key_grid; let key_grid = &context.device.characteristics().key_grid;
@ -296,11 +299,16 @@ fn get_key_position_for_index(key_grid: &LoupedeckDeviceKeyGridCharacteristics,
} }
} }
/// Panics when `index` is invalid.
fn draw_key(context: &IoWorkerContext, index: u8, key: Option<&Key>) { fn draw_key(context: &IoWorkerContext, index: u8, key: Option<&Key>) {
let key_grid = &context.device.characteristics().key_grid; 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 context
.device .device
.replace_framebuffer_area_raw(&key_grid.display, rect.x, rect.y, rect.w, rect.h, buffer) .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::RightMiddle => LoupedeckKnob::RightMiddle,
KnobPosition::RightBottom => LoupedeckKnob::RightBottom, 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 context
.device .device
.replace_framebuffer_area_raw(display, rect.x, rect.y, rect.w, rect.h, buffer) .replace_framebuffer_area_raw(display, rect.x, rect.y, rect.w, rect.h, buffer)

View file

@ -2,7 +2,7 @@ use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use log::{error, info}; use log::{error, info};
use rumqttc::{ConnectionError, Event, Incoming, LastWill, MqttOptions, QoS}; use rumqttc::{Event, Incoming, LastWill, MqttOptions, QoS};
use tokio::sync::broadcast; use tokio::sync::broadcast;
use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent}; use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent};
@ -121,53 +121,61 @@ pub async fn start_mqtt_client(
match segments[0] { match segments[0] {
"keys" => { "keys" => {
if property == "style" { if property == "style" {
let value = serde_json::from_slice::<Option<KeyStyle>>(&event.payload).unwrap(); if let Ok(position) = KeyPosition::from_str(position) {
if let Ok(value) = serde_json::from_slice::<Option<KeyStyle>>(&event.payload) {
commands_sender commands_sender
.send_async(HandlerCommand::SetKeyStyle { .send_async(HandlerCommand::SetKeyStyle {
path: KeyPath { path: KeyPath {
page_id: page_id.to_owned(), page_id: page_id.to_owned(),
position: KeyPosition::from_str(position).unwrap(), position,
}, },
value, value,
}) })
.await .await
.unwrap(); .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" => { "knobs" => {
let position = KnobPosition::from_str(position).unwrap(); if let Ok(position) = KnobPosition::from_str(position) {
match property {
match property { "style" => {
"style" => { if let Ok(value) = serde_json::from_slice::<Option<KnobStyle>>(&event.payload) {
let value = serde_json::from_slice::<Option<KnobStyle>>(&event.payload).unwrap(); commands_sender
.send_async(HandlerCommand::SetKnobStyle {
commands_sender path: KnobPath {
.send_async(HandlerCommand::SetKnobStyle { page_id: page_id.to_owned(),
path: KnobPath { position,
page_id: page_id.to_owned(), },
position, value,
}, })
value, .await
}) .unwrap();
.await }
.unwrap(); }
"value" => {
if let Ok(value) = serde_json::from_slice::<Option<f32>>(&event.payload) {
commands_sender
.send_async(HandlerCommand::SetKnobValue {
path: KnobPath {
page_id: page_id.to_owned(),
position,
},
value,
})
.await
.unwrap();
}
}
_ => {}
} }
"value" => { } else {
let value = serde_json::from_slice::<Option<f32>>(&event.payload).unwrap(); log::warn!("Invalid knob position in topic name: {}", event.topic);
commands_sender
.send_async(HandlerCommand::SetKnobValue {
path: KnobPath {
page_id: page_id.to_owned(),
position,
},
value,
})
.await
.unwrap();
}
_ => {}
} }
} }
_ => {} _ => {}

View file

@ -1,7 +1,7 @@
use std::path::Path; use std::path::Path;
use color_eyre::Result; use color_eyre::Result;
use log::{debug, info, trace, warn}; use log::{info, trace, warn};
use tokio::sync::{broadcast, mpsc}; use tokio::sync::{broadcast, mpsc};
use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent}; use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent};
@ -28,7 +28,7 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> {
None => { None => {
if is_running { if is_running {
info!("Stopping handlers…"); 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; is_running = false;
@ -36,7 +36,7 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> {
Some(handler_hosts_config) => { Some(handler_hosts_config) => {
if is_running { if is_running {
warn!("A new configuration was received before the old one was cleared."); 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; is_running = true;

View file

@ -45,7 +45,10 @@ pub async fn start_mqtt_client(
is_first_try = false; is_first_try = false;
} else if is_connected { } else if is_connected {
error!("MQTT connection lost: {error}"); 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; is_connected = false;
@ -63,13 +66,14 @@ pub async fn start_mqtt_client(
client.subscribe(format!("{topic_prefix}/knobs/+/+/events"), QoS::ExactlyOnce).await.unwrap(); client.subscribe(format!("{topic_prefix}/knobs/+/+/events"), QoS::ExactlyOnce).await.unwrap();
} }
Incoming::Publish(event) => { Incoming::Publish(event) => {
let topic_segments = event.topic.strip_prefix(&topic_prefix).unwrap().split('/').skip(1).collect::<Vec<&str>>(); let topic_name = event.topic;
let topic_segments = topic_name.strip_prefix(&topic_prefix).unwrap().split('/').skip(1).collect::<Vec<&str>>();
if topic_segments[0] == "config" { if topic_segments[0] == "config" {
if let Ok(config) = serde_json::from_slice(&event.payload) { if let Ok(config) = serde_json::from_slice(&event.payload) {
handler_hosts_config_sender.send(config).await.unwrap(); handler_hosts_config_sender.send(config).await.unwrap();
} else { } 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(); handler_hosts_config_sender.send(None).await.unwrap();
}; };
} else { } else {
@ -80,32 +84,40 @@ pub async fn start_mqtt_client(
match topic_segments[0] { match topic_segments[0] {
"keys" if property == "events" => { "keys" if property == "events" => {
if let Ok(event) = serde_json::from_slice(&event.payload) { if let Ok(event) = serde_json::from_slice(&event.payload) {
events_sender if let Ok(position) = KeyPosition::from_str(position) {
.send(HandlerEvent::Key { // 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 { path: KeyPath {
page_id: page_id.to_owned(), page_id: page_id.to_owned(),
position: KeyPosition::from_str(position).unwrap(), position,
}, },
event, 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 { } 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" => { "knobs" if property == "events" => {
if let Ok(event) = serde_json::from_slice(&event.payload) { if let Ok(event) = serde_json::from_slice(&event.payload) {
events_sender if let Ok(position) = KnobPosition::from_str(position) {
.send(HandlerEvent::Knob { // 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 { path: KnobPath {
page_id: page_id.to_owned(), page_id: page_id.to_owned(),
position: KnobPosition::from_str(position).unwrap(), position,
}, },
event, 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 { } else {
log::error!("Could not deserialize the latest event from {}", event.topic); log::error!("Could not deserialize the latest event from {}", topic_name);
}; };
} }
_ => {} _ => {}

View file

@ -36,7 +36,7 @@ pub async fn start(
if let Ok(entry) = entry { if let Ok(entry) = entry {
let path = entry.path(); let path = entry.path();
if path.is_executable() { 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 { for handler_name in handler_names {
let handler_name = handler_name let handler_name = handler_name
.into_string() .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(); .into_boxed_str();
let (key_configs, knob_configs) = match ( start_handler(
handler_config_by_key_path_by_handler_name.remove(&handler_name), handlers_directory,
handler_config_by_knob_path_by_handler_name.remove(&handler_name), &mut handler_config_by_key_path_by_handler_name,
) { &mut handler_config_by_knob_path_by_handler_name,
(None, None) => { &mut handler_stdin_by_name,
warn!("Handler '{handler_name}' is not used by any key or knob."); commands_sender.clone(),
continue; Arc::clone(&should_stop),
} handler_name.clone(),
(a, b) => (a.unwrap_or_default(), b.unwrap_or_default()), )
}; .await
.wrap_err_with(|| format!("while starting handler: {handler_name}"))?;
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::<HandlerCommand>(&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:#?}");
}
});
} }
if let Some((handler_name, config_by_key_path)) = handler_config_by_key_path_by_handler_name.drain().next() { 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 { tokio::spawn(async move {
while let Ok(event) = events_receiver.recv().await { while let Ok(event) = events_receiver.recv().await {
let handler_name = match &event { let result = forward_event_to_handler(
HandlerEvent::Stop => { &should_stop,
should_stop.store(true, Ordering::Relaxed); &mut handler_stdin_by_name,
let serialized_event = serde_json::to_string(&event).unwrap().into_boxed_str().into_boxed_bytes(); &handler_name_by_key_path,
for handler_stdin in handler_stdin_by_name.values_mut() { &handler_name_by_knob_path,
handler_stdin.write_all(&serialized_event).await.unwrap(); event,
handler_stdin.write_u8(b'\n').await.unwrap(); )
handler_stdin.flush().await.unwrap(); .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), Ok(should_continue) => {
HandlerEvent::Knob { path, .. } => handler_name_by_knob_path.get(path), if !should_continue {
}; break;
}
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(()) Ok(())
} }
async fn start_handler(
handlers_directory: &Path,
handler_config_by_key_path_by_handler_name: &mut HashMap<Box<str>, HashMap<KeyPath, Arc<toml::Table>>>,
handler_config_by_knob_path_by_handler_name: &mut HashMap<Box<str>, HashMap<KnobPath, Arc<toml::Table>>>,
handler_stdin_by_name: &mut HashMap<Box<str>, ChildStdin>,
commands_sender: flume::Sender<HandlerCommand>,
should_stop: Arc<AtomicBool>,
handler_name: Box<str>,
) -> 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::<HandlerCommand>(&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<Box<str>, ChildStdin>,
handler_name_by_key_path: &HashMap<KeyPath, Box<str>>,
handler_name_by_knob_path: &HashMap<KnobPath, Box<str>>,
event: HandlerEvent,
) -> Result<bool> {
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)] #[derive(Debug, Serialize, Deserialize)]
pub struct KeyOrKnobHandlerConfig { pub struct KeyOrKnobHandlerConfig {
pub host_id: Box<str>, pub host_id: Box<str>,

View file

@ -1,3 +1,4 @@
use std::cmp::max;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
@ -13,6 +14,7 @@ use deckster_shared::icon_descriptor::{IconDescriptor, IconDescriptorSource};
use deckster_shared::image_filter::ImageFilter; use deckster_shared::image_filter::ImageFilter;
use crate::model::coordinator_config::{IconFormat, IconPack}; use crate::model::coordinator_config::{IconFormat, IconPack};
use crate::util::UnwrapExt;
mod destructive_filter; mod destructive_filter;
@ -81,7 +83,10 @@ impl IconManager {
let icon = self.load_icon(descriptor)?; let icon = self.load_icon(descriptor)?;
let scale = icon.effective_filter.transform.scale; 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.draw_pixmap(
(((pixmap.width() as i32 - scaled_size.width() as i32) / 2) as f32 / scale).round() as i32, (((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( fn read_image_and_get_scale(
config_directory: &Path, config_directory: &Path,
dpi: f32, dpi: f32,
fonts_db: &resvg::usvg::fontdb::Database, fonts_db: &fontdb::Database,
source: &IconDescriptorSource, source: &IconDescriptorSource,
icon_pack: Option<&IconPack>, icon_pack: Option<&IconPack>,
preferred_scale: f32, 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<Pixmap> { fn read_image_from_svg(path: &Path, dpi: f32, font_db: &fontdb::Database, scale: f32) -> Result<Pixmap> {
let raw_data = std::fs::read(path)?; let raw_data = std::fs::read(path)?;
let tree = { 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 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()); tree.render(Transform::from_scale(scale, scale), &mut pixmap.as_mut());

View file

@ -16,6 +16,7 @@ mod handler_host;
mod handler_runner; mod handler_runner;
mod icons; mod icons;
mod model; mod model;
mod util;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(name = "deckster", about = "Use Loupedeck devices under Linux.")] #[command(name = "deckster", about = "Use Loupedeck devices under Linux.")]
@ -112,7 +113,7 @@ fn read_and_deserialize_from_directory<T: serde::de::DeserializeOwned>(base: &Pa
let path = entry.path(); let path = entry.path();
if let Some(fallback_id) = path if let Some(fallback_id) = path
.strip_prefix(base) .strip_prefix(base)
.unwrap() .expect("all paths yielded by WalkDir are within the base directory")
.to_str() .to_str()
.expect("Paths must be valid UTF-8") .expect("Paths must be valid UTF-8")
.strip_suffix(".toml") .strip_suffix(".toml")

View file

@ -41,15 +41,20 @@ impl Display for UIntVec2 {
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[error("The input value does not match the required format.")] pub enum IntRectWrapperFromStrErr {
pub struct ParsingError {} #[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)] #[derive(Debug, Copy, Clone, PartialEq, Deref, From, Into, SerializeDisplay, DeserializeFromStr)]
#[repr(transparent)] #[repr(transparent)]
pub struct IntRectWrapper(IntRect); pub struct IntRectWrapper(IntRect);
impl FromStr for IntRectWrapper { impl FromStr for IntRectWrapper {
type Err = ParsingError; type Err = IntRectWrapperFromStrErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let (pairs, is_corner_points_mode) = if let Some(pairs) = s.split_once('+') { 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('-') { } else if let Some(pairs) = s.split_once('-') {
(pairs, true) (pairs, true)
} else { } else {
return Err(ParsingError {}); return Err(IntRectWrapperFromStrErr::ParsingFailed);
}; };
let first_vec = UIntVec2::from_str(pairs.0).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(|_| ParsingError {})?; let second_vec = UIntVec2::from_str(pairs.1).map_err(|_| IntRectWrapperFromStrErr::ParsingFailed)?;
Ok(IntRectWrapper(if is_corner_points_mode { 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 { } 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)?
})) }))
} }
} }

19
src/util.rs Normal file
View file

@ -0,0 +1,19 @@
use std::fmt;
pub trait UnwrapExt<T> {
fn unwrap_todo(self) -> T;
}
impl<T> UnwrapExt<T> for Option<T> {
#[inline]
fn unwrap_todo(self) -> T {
self.expect("TODO: Handle this Option being None")
}
}
impl<T, E: fmt::Debug> UnwrapExt<T> for Result<T, E> {
#[inline]
fn unwrap_todo(self) -> T {
self.expect("TODO: Handle this error")
}
}