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
## 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
[foxxyzs `loupedeck` library for JavaScript](https://github.com/foxxyz/loupedeck)
(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 {
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"));
}

View file

@ -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()

View file

@ -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)

View file

@ -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);
}
}
_ => {}
};

View file

@ -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;

View file

@ -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);
};
}
_ => {}

View file

@ -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:#?}");
}
});
Ok(())
}
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 {
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)]

View file

@ -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());

View file

@ -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")

View file

@ -41,15 +41,20 @@ impl Display for UIntVec2 {
}
#[derive(Debug, Error)]
pub enum IntRectWrapperFromStrErr {
#[error("The input value does not match the required format.")]
pub struct ParsingError {}
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
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")
}
}