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
|
||||
|
||||
## 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("<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
|
||||
[foxxyz’s `loupedeck` library for JavaScript](https://github.com/foxxyz/loupedeck)
|
||||
(licensed under the [MIT license](https://github.com/foxxyz/loupedeck/blob/e41e5d920130d9ef651e47173c68450b9c832b96/LICENSE))
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<H
|
|||
|
||||
loop {
|
||||
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()))
|
||||
.wait();
|
||||
|
||||
|
@ -130,7 +132,10 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv
|
|||
|
||||
let LoupedeckDisplayRect {
|
||||
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 {
|
||||
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_knob_page_id = knob_page_id;
|
||||
|
||||
for button in LoupedeckButton::VARIANTS {
|
||||
let position = ButtonPosition::of(button);
|
||||
|
||||
for button in context.device.characteristics().available_buttons {
|
||||
context
|
||||
.device
|
||||
.set_button_color(*button, get_correct_button_color(context, state, position))
|
||||
.unwrap();
|
||||
.set_button_color(button, get_correct_button_color(context, state, ButtonPosition::of(&button)))
|
||||
.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;
|
||||
|
@ -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>) {
|
||||
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)
|
||||
|
|
|
@ -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,27 +121,31 @@ pub async fn start_mqtt_client(
|
|||
match segments[0] {
|
||||
"keys" => {
|
||||
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
|
||||
.send_async(HandlerCommand::SetKeyStyle {
|
||||
path: KeyPath {
|
||||
page_id: page_id.to_owned(),
|
||||
position: KeyPosition::from_str(position).unwrap(),
|
||||
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();
|
||||
|
||||
if let Ok(position) = KnobPosition::from_str(position) {
|
||||
match property {
|
||||
"style" => {
|
||||
let value = serde_json::from_slice::<Option<KnobStyle>>(&event.payload).unwrap();
|
||||
|
||||
if let Ok(value) = serde_json::from_slice::<Option<KnobStyle>>(&event.payload) {
|
||||
commands_sender
|
||||
.send_async(HandlerCommand::SetKnobStyle {
|
||||
path: KnobPath {
|
||||
|
@ -153,9 +157,9 @@ pub async fn start_mqtt_client(
|
|||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
"value" => {
|
||||
let value = serde_json::from_slice::<Option<f32>>(&event.payload).unwrap();
|
||||
|
||||
if let Ok(value) = serde_json::from_slice::<Option<f32>>(&event.payload) {
|
||||
commands_sender
|
||||
.send_async(HandlerCommand::SetKnobValue {
|
||||
path: KnobPath {
|
||||
|
@ -167,8 +171,12 @@ pub async fn start_mqtt_client(
|
|||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
log::warn!("Invalid knob position in topic name: {}", event.topic);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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::<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 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::error!("Could not deserialize the latest event from {}", event.topic);
|
||||
log::warn!("Invalid key position in topic name: {topic_name}");
|
||||
}
|
||||
} else {
|
||||
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::error!("Could not deserialize the latest event from {}", event.topic);
|
||||
log::warn!("Invalid knob position in topic name: {topic_name}");
|
||||
}
|
||||
} else {
|
||||
log::error!("Could not deserialize the latest event from {}", topic_name);
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
@ -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,16 +79,84 @@ 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();
|
||||
|
||||
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() {
|
||||
return Err(eyre!(
|
||||
"There is no executable file named '{handler_name}' in the handlers directory but these keys have it set as their handler: {}",
|
||||
config_by_key_path.keys().join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
if let Some((handler_name, config_by_knob_path)) = handler_config_by_knob_path_by_handler_name.drain().next() {
|
||||
return Err(eyre!(
|
||||
"There is no executable file named '{handler_name}' in the handlers directory but these knobs have it set as their handler: {}",
|
||||
config_by_knob_path.keys().join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Ok(event) = events_receiver.recv().await {
|
||||
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;
|
||||
|
||||
match result {
|
||||
Err(error) => {
|
||||
error!("Failed to forward an event to its handler: {error}")
|
||||
}
|
||||
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<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.");
|
||||
continue;
|
||||
return Ok(());
|
||||
}
|
||||
(a, b) => (a.unwrap_or_default(), b.unwrap_or_default()),
|
||||
};
|
||||
|
@ -98,20 +166,21 @@ pub async fn start(
|
|||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.wrap_err_with(|| format!("while spawning handler: {handler_name}"))?;
|
||||
.wrap_err("while spawning the handler process")?;
|
||||
|
||||
let mut stdout_lines = BufReader::new(command.stdout.take().unwrap()).lines();
|
||||
let mut stdin = command.stdin.take().unwrap();
|
||||
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.unwrap();
|
||||
stdin.write_u8(b'\n').await.unwrap();
|
||||
stdin.flush().await.unwrap();
|
||||
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")?;
|
||||
|
||||
let result_line = stdout_lines.next_line().await?.unwrap();
|
||||
// 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 {
|
||||
|
@ -135,13 +204,14 @@ pub async fn start(
|
|||
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -161,56 +231,45 @@ pub async fn start(
|
|||
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() {
|
||||
return Err(eyre!(
|
||||
"There is no executable file named '{handler_name}' in the handlers directory but these keys have it set as their handler: {}",
|
||||
config_by_key_path.keys().join(", ")
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if let Some((handler_name, config_by_knob_path)) = handler_config_by_knob_path_by_handler_name.drain().next() {
|
||||
return Err(eyre!(
|
||||
"There is no executable file named '{handler_name}' in the handlers directory but these knobs have it set as their handler: {}",
|
||||
config_by_knob_path.keys().join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Ok(event) = events_receiver.recv().await {
|
||||
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() {
|
||||
handler_stdin.write_all(&serialized_event).await.unwrap();
|
||||
handler_stdin.write_u8(b'\n').await.unwrap();
|
||||
handler_stdin.flush().await.unwrap();
|
||||
// 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;
|
||||
}
|
||||
|
||||
break;
|
||||
return Ok(false);
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
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.unwrap();
|
||||
handler_stdin.write_u8(b'\n').await.unwrap();
|
||||
handler_stdin.flush().await.unwrap();
|
||||
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(())
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
|
|
@ -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<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 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());
|
||||
|
||||
|
|
|
@ -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<T: serde::de::DeserializeOwned>(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")
|
||||
|
|
|
@ -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<Self, Self::Err> {
|
||||
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)?
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
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