Get rid of preliminary unwraps
This commit is contained in:
parent
0340dddcab
commit
6c284b6365
12 changed files with 344 additions and 202 deletions
10
README.md
10
README.md
|
@ -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
|
||||||
[foxxyz’s `loupedeck` library for JavaScript](https://github.com/foxxyz/loupedeck)
|
[foxxyz’s `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))
|
||||||
|
|
|
@ -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"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
19
src/util.rs
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue