use plushie_core::key::{MouseButton, PointerKind};
use serde_json::Value;
pub use plushie_core::pointer::{
KeyData, PointerBoundary, PointerDrag, PointerMove, PointerPress, PointerRelease,
PointerScroll, ResizeDimensions, ScrollPosition,
};
use crate::types::KeyModifiers;
fn get_captured(obj: Option<&serde_json::Map<String, Value>>) -> bool {
obj.and_then(|o| o.get("captured"))
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
fn parse_pointer_press(value: &Value) -> PointerPress {
let obj = value.as_object();
let get_f32 = |k: &str| -> f32 {
obj.and_then(|o| o.get(k))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32
};
let get_str = |k: &str| -> &str {
obj.and_then(|o| o.get(k))
.and_then(|v| v.as_str())
.unwrap_or("")
};
PointerPress {
x: get_f32("x"),
y: get_f32("y"),
button: MouseButton::from(
obj.and_then(|o| o.get("button"))
.and_then(|v| v.as_str())
.unwrap_or("left"),
),
pointer: PointerKind::from(get_str("pointer")),
finger: obj.and_then(|o| o.get("finger")).and_then(|v| v.as_u64()),
modifiers: parse_modifiers(obj),
captured: get_captured(obj),
}
}
fn parse_pointer_release(value: &Value) -> PointerRelease {
let obj = value.as_object();
let p = parse_pointer_press(value);
PointerRelease {
x: p.x,
y: p.y,
button: p.button,
pointer: p.pointer,
finger: p.finger,
modifiers: p.modifiers,
captured: p.captured,
lost: obj.and_then(|o| o.get("lost")).and_then(|v| v.as_bool()),
}
}
fn parse_pointer_move(value: &Value) -> PointerMove {
let obj = value.as_object();
let get_f32 = |k: &str| -> f32 {
obj.and_then(|o| o.get(k))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32
};
let get_str = |k: &str| -> &str {
obj.and_then(|o| o.get(k))
.and_then(|v| v.as_str())
.unwrap_or("")
};
PointerMove {
x: get_f32("x"),
y: get_f32("y"),
pointer: PointerKind::from(get_str("pointer")),
finger: obj.and_then(|o| o.get("finger")).and_then(|v| v.as_u64()),
modifiers: parse_modifiers(obj),
captured: get_captured(obj),
}
}
fn parse_pointer_scroll(value: &Value) -> PointerScroll {
let obj = value.as_object();
let get_f32 = |k: &str| -> f32 {
obj.and_then(|o| o.get(k))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32
};
let get_str = |k: &str| -> &str {
obj.and_then(|o| o.get(k))
.and_then(|v| v.as_str())
.unwrap_or("")
};
PointerScroll {
x: get_f32("x"),
y: get_f32("y"),
delta_x: get_f32("delta_x"),
delta_y: get_f32("delta_y"),
pointer: PointerKind::from(get_str("pointer")),
modifiers: parse_modifiers(obj),
captured: get_captured(obj),
}
}
fn parse_pointer_drag(value: &Value) -> PointerDrag {
let obj = value.as_object();
let get_f32 = |k: &str| -> f32 {
obj.and_then(|o| o.get(k))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32
};
let get_str = |k: &str| -> &str {
obj.and_then(|o| o.get(k))
.and_then(|v| v.as_str())
.unwrap_or("")
};
PointerDrag {
x: get_f32("x"),
y: get_f32("y"),
pointer: PointerKind::from(get_str("pointer")),
modifiers: parse_modifiers(obj),
captured: get_captured(obj),
}
}
fn parse_pointer_boundary(value: &Value) -> PointerBoundary {
let obj = value.as_object();
let get_opt_f32 = |k: &str| -> Option<f32> {
obj.and_then(|o| o.get(k))
.and_then(|v| v.as_f64())
.map(|n| n as f32)
};
PointerBoundary {
x: get_opt_f32("x"),
y: get_opt_f32("y"),
captured: get_captured(obj),
}
}
fn parse_scroll_position(value: &Value) -> ScrollPosition {
let obj = value.as_object();
let get_f32 = |k: &str| -> f32 {
obj.and_then(|o| o.get(k))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32
};
ScrollPosition {
absolute_x: get_f32("absolute_x"),
absolute_y: get_f32("absolute_y"),
relative_x: get_f32("relative_x"),
relative_y: get_f32("relative_y"),
bounds_width: get_f32("bounds_width"),
bounds_height: get_f32("bounds_height"),
content_width: get_f32("content_width"),
content_height: get_f32("content_height"),
}
}
fn parse_key_data(value: &Value) -> KeyData {
let obj = value.as_object();
let get_str = |k: &str| -> Option<&str> { obj.and_then(|o| o.get(k)).and_then(|v| v.as_str()) };
let get_key =
|k: &str| -> Option<plushie_core::Key> { get_str(k).map(plushie_core::Key::from) };
KeyData {
key: get_key("key").unwrap_or(plushie_core::Key::Named(String::new())),
modified_key: get_key("modified_key"),
physical_key: get_key("physical_key"),
modifiers: parse_modifiers(obj),
text: get_str("text").map(String::from),
repeat: obj
.and_then(|o| o.get("repeat"))
.and_then(|v| v.as_bool())
.unwrap_or(false),
}
}
fn parse_resize(value: &Value) -> ResizeDimensions {
let obj = value.as_object();
let get_f32 = |k: &str| -> f32 {
obj.and_then(|o| o.get(k))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32
};
ResizeDimensions {
width: get_f32("width"),
height: get_f32("height"),
}
}
fn expect_link<'a>(value: &'a Value, id: &str) -> &'a str {
match value.get("link").and_then(Value::as_str) {
Some(link) => link,
None => {
log::warn!(
"event value type mismatch: link_click event for \"{id}\" expected {{\"link\": ...}}, got {value}"
);
""
}
}
}
fn expect_str<'a>(value: &'a Value, family: &str, id: &str) -> &'a str {
match value.as_str() {
Some(s) => s,
None => {
log::warn!(
"event value type mismatch: {family} event for \"{id}\" expected string, got {value}"
);
""
}
}
}
fn expect_bool(value: &Value, family: &str, id: &str) -> bool {
match value.as_bool() {
Some(b) => b,
None => {
log::warn!(
"event value type mismatch: {family} event for \"{id}\" expected bool, got {value}"
);
false
}
}
}
fn expect_f64(value: &Value, family: &str, id: &str) -> f64 {
match value.as_f64() {
Some(n) => n,
None => {
log::warn!(
"event value type mismatch: {family} event for \"{id}\" expected number, got {value}"
);
0.0
}
}
}
fn parse_modifiers(obj: Option<&serde_json::Map<String, Value>>) -> KeyModifiers {
let mods = obj.and_then(|o| o.get("modifiers"));
KeyModifiers {
shift: mods
.and_then(|m| m.get("shift"))
.and_then(|v| v.as_bool())
.unwrap_or(false),
ctrl: mods
.and_then(|m| m.get("ctrl"))
.and_then(|v| v.as_bool())
.unwrap_or(false),
alt: mods
.and_then(|m| m.get("alt"))
.and_then(|v| v.as_bool())
.unwrap_or(false),
logo: mods
.and_then(|m| m.get("logo"))
.and_then(|v| v.as_bool())
.unwrap_or(false),
command: mods
.and_then(|m| m.get("command"))
.and_then(|v| v.as_bool())
.unwrap_or(false),
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Event {
Widget(WidgetEvent),
Key(KeyEvent),
Window(WindowEvent),
Timer(TimerEvent),
Async(AsyncEvent),
Stream(StreamEvent),
Effect(EffectEvent),
System(SystemEvent),
CommandError(CommandError),
Modifiers(ModifiersEvent),
Ime(ImeEvent),
}
impl Event {
pub fn widget_match(&self) -> Option<WidgetMatch<'_>> {
match self {
Event::Widget(w) => Some(w.to_match()),
Event::Timer(t) => Some(WidgetMatch::Timer(&t.tag)),
_ => None,
}
}
pub fn as_widget(&self) -> Option<&WidgetEvent> {
match self {
Event::Widget(w) => Some(w),
_ => None,
}
}
pub fn as_key_press(&self) -> Option<&KeyEvent> {
match self {
Event::Key(k) if k.event_type == KeyEventType::Press => Some(k),
_ => None,
}
}
pub fn as_key_release(&self) -> Option<&KeyEvent> {
match self {
Event::Key(k) if k.event_type == KeyEventType::Release => Some(k),
_ => None,
}
}
pub fn as_window(&self) -> Option<&WindowEvent> {
match self {
Event::Window(w) => Some(w),
_ => None,
}
}
pub fn as_timer(&self) -> Option<&TimerEvent> {
match self {
Event::Timer(t) => Some(t),
_ => None,
}
}
pub fn as_async(&self) -> Option<&AsyncEvent> {
match self {
Event::Async(a) => Some(a),
_ => None,
}
}
pub fn as_stream(&self) -> Option<&StreamEvent> {
match self {
Event::Stream(s) => Some(s),
_ => None,
}
}
pub fn as_effect(&self) -> Option<&EffectEvent> {
match self {
Event::Effect(e) => Some(e),
_ => None,
}
}
pub fn as_system(&self) -> Option<&SystemEvent> {
match self {
Event::System(s) => Some(s),
_ => None,
}
}
pub fn scope(&self) -> Option<&[String]> {
self.as_widget().map(|w| w.scoped_id.scope.as_slice())
}
}
pub use plushie_core::EventType;
#[derive(Debug, Clone)]
pub struct WidgetEvent {
pub event_type: EventType,
pub scoped_id: plushie_core::ScopedId,
pub value: Value,
}
impl WidgetEvent {
pub fn value_string(&self) -> Option<String> {
self.value.as_str().map(|s| s.to_string())
}
pub fn value_bool(&self) -> Option<bool> {
self.value.as_bool()
}
pub fn value_f64(&self) -> Option<f64> {
self.value.as_f64()
}
pub fn target(&self) -> &str {
&self.scoped_id.full
}
fn to_match(&self) -> WidgetMatch<'_> {
let id = &self.scoped_id.id;
use EventType::*;
match &self.event_type {
Click => WidgetMatch::Click(id),
DoubleClick => WidgetMatch::DoubleClick(id, parse_pointer_press(&self.value)),
Input => WidgetMatch::Input(id, expect_str(&self.value, "input", id)),
Submit => WidgetMatch::Submit(id, expect_str(&self.value, "submit", id)),
Toggle => WidgetMatch::Toggle(id, expect_bool(&self.value, "toggle", id)),
Select => WidgetMatch::Select(id, expect_str(&self.value, "select", id)),
Slide => WidgetMatch::Slide(id, expect_f64(&self.value, "slide", id)),
SlideRelease => {
WidgetMatch::SlideRelease(id, expect_f64(&self.value, "slide_release", id))
}
Paste => WidgetMatch::Paste(id, expect_str(&self.value, "paste", id)),
Press => WidgetMatch::Press(id, parse_pointer_press(&self.value)),
Release => WidgetMatch::Release(id, parse_pointer_release(&self.value)),
Move => WidgetMatch::Move(id, parse_pointer_move(&self.value)),
Scroll => WidgetMatch::Scroll(id, parse_pointer_scroll(&self.value)),
Scrolled => WidgetMatch::Scrolled(id, parse_scroll_position(&self.value)),
Enter => WidgetMatch::Enter(id, parse_pointer_boundary(&self.value)),
Exit => WidgetMatch::Exit(id, parse_pointer_boundary(&self.value)),
Drag => WidgetMatch::Drag(id, parse_pointer_drag(&self.value)),
DragEnd => WidgetMatch::DragEnd(id, parse_pointer_drag(&self.value)),
Focused => WidgetMatch::Focused(id),
Blurred => WidgetMatch::Blurred(id),
Resize => WidgetMatch::Resize(id, parse_resize(&self.value)),
KeyPress => WidgetMatch::KeyPress(id, parse_key_data(&self.value)),
KeyRelease => WidgetMatch::KeyRelease(id, parse_key_data(&self.value)),
Sort => WidgetMatch::Sort(id, expect_str(&self.value, "sort", id)),
Status => WidgetMatch::Status(id, &self.value),
OptionHovered => WidgetMatch::OptionHovered(id, &self.value),
Open => WidgetMatch::Open(id),
Close => WidgetMatch::Close(id),
KeyBinding => WidgetMatch::KeyBinding(id, &self.value),
LinkClick => WidgetMatch::LinkClicked(id, expect_link(&self.value, id)),
TransitionComplete => WidgetMatch::TransitionComplete(id),
PaneResized => WidgetMatch::PaneResized(id, &self.value),
PaneDragged => WidgetMatch::PaneDragged(id, &self.value),
PaneClicked => WidgetMatch::PaneClicked(id, &self.value),
PaneFocusCycle => WidgetMatch::PaneFocusCycle(id),
Custom(family) => WidgetMatch::Custom {
id,
family,
value: &self.value,
},
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum WidgetMatch<'a> {
Click(&'a str),
DoubleClick(&'a str, PointerPress),
Input(&'a str, &'a str),
Submit(&'a str, &'a str),
Toggle(&'a str, bool),
Select(&'a str, &'a str),
Slide(&'a str, f64),
SlideRelease(&'a str, f64),
Paste(&'a str, &'a str),
Press(&'a str, PointerPress),
Release(&'a str, PointerRelease),
Move(&'a str, PointerMove),
Scroll(&'a str, PointerScroll),
Scrolled(&'a str, ScrollPosition),
Enter(&'a str, PointerBoundary),
Exit(&'a str, PointerBoundary),
Drag(&'a str, PointerDrag),
DragEnd(&'a str, PointerDrag),
Focused(&'a str),
Blurred(&'a str),
Resize(&'a str, ResizeDimensions),
KeyPress(&'a str, KeyData),
KeyRelease(&'a str, KeyData),
Sort(&'a str, &'a str),
Status(&'a str, &'a Value),
OptionHovered(&'a str, &'a Value),
Open(&'a str),
Close(&'a str),
KeyBinding(&'a str, &'a Value),
TransitionComplete(&'a str),
PaneResized(&'a str, &'a Value),
PaneDragged(&'a str, &'a Value),
PaneClicked(&'a str, &'a Value),
PaneFocusCycle(&'a str),
LinkClicked(&'a str, &'a str),
Timer(&'a str),
Custom {
id: &'a str,
family: &'a str,
value: &'a Value,
},
}
pub fn family_to_event_type(family: &str) -> EventType {
EventType::from_family(family)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum KeyEventType {
Press,
Release,
}
#[derive(Debug, Clone)]
pub struct KeyEvent {
pub event_type: KeyEventType,
pub key: plushie_core::Key,
pub modified_key: Option<plushie_core::Key>,
pub physical_key: Option<plushie_core::Key>,
pub location: KeyLocation,
pub modifiers: KeyModifiers,
pub text: Option<String>,
pub repeat: bool,
pub captured: bool,
pub window_id: Option<String>,
}
impl KeyEvent {
pub fn is_press(&self) -> bool {
self.event_type == KeyEventType::Press
}
pub fn is_release(&self) -> bool {
self.event_type == KeyEventType::Release
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum KeyLocation {
#[default]
Standard,
Left,
Right,
Numpad,
}
#[derive(Debug, Clone)]
pub struct WindowEvent {
pub event_type: WindowEventType,
pub window_id: String,
pub x: Option<f32>,
pub y: Option<f32>,
pub width: Option<f32>,
pub height: Option<f32>,
pub path: Option<String>,
pub scale_factor: Option<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum WindowEventType {
Opened,
Closed,
CloseRequested,
Moved,
Resized,
Focused,
Unfocused,
Rescaled,
FileHovered,
FileDropped,
FilesHoveredLeft,
}
#[derive(Debug, Clone)]
pub struct TimerEvent {
pub tag: String,
pub timestamp: u64,
}
#[derive(Debug, Clone)]
pub struct AsyncEvent {
pub tag: String,
pub result: Result<Value, Value>,
}
#[derive(Debug, Clone)]
pub struct StreamEvent {
pub tag: String,
pub value: Value,
}
#[derive(Debug, Clone)]
pub struct EffectEvent {
pub tag: String,
pub result: EffectResult,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum EffectResult {
FileOpened {
path: String,
},
FilesOpened {
paths: Vec<String>,
},
FileSaved {
path: String,
},
DirectorySelected {
path: String,
},
DirectoriesSelected {
paths: Vec<String>,
},
ClipboardText {
text: String,
},
ClipboardHtml {
html: String,
},
ClipboardWritten,
ClipboardCleared,
NotificationShown,
Cancelled,
Timeout,
Error(String),
RendererRestarted,
Unsupported,
Shutdown,
Other(Value),
Orphaned(Value),
}
impl EffectResult {
pub fn parse(kind: &str, status: &str, value: Option<&Value>) -> Self {
match status {
"cancelled" => Self::Cancelled,
"unsupported" => Self::Unsupported,
"error" => {
let msg = value
.and_then(|v| v.as_str())
.unwrap_or("unknown error")
.to_string();
Self::Error(msg)
}
"ok" => Self::parse_ok(kind, value),
other => {
log::warn!("unknown effect status: {other}");
Self::Other(value.cloned().unwrap_or(Value::Null))
}
}
}
fn parse_ok(kind: &str, value: Option<&Value>) -> Self {
match kind {
"file_open" => {
let path = value
.and_then(|v| v.get("path"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
Self::FileOpened { path }
}
"file_open_multiple" => {
let paths = value
.and_then(|v| v.get("paths"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
Self::FilesOpened { paths }
}
"file_save" => {
let path = value
.and_then(|v| v.get("path"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
Self::FileSaved { path }
}
"directory_select" => {
let path = value
.and_then(|v| v.get("path"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
Self::DirectorySelected { path }
}
"directory_select_multiple" => {
let paths = value
.and_then(|v| v.get("paths"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
Self::DirectoriesSelected { paths }
}
"clipboard_read" | "clipboard_read_primary" => {
let text = value
.and_then(|v| v.get("text"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
Self::ClipboardText { text }
}
"clipboard_read_html" => {
let html = value
.and_then(|v| v.get("html"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
Self::ClipboardHtml { html }
}
"clipboard_write" | "clipboard_write_html" | "clipboard_write_primary" => {
Self::ClipboardWritten
}
"clipboard_clear" => Self::ClipboardCleared,
"notification" => Self::NotificationShown,
_ => Self::Other(value.cloned().unwrap_or(Value::Null)),
}
}
}
#[derive(Debug, Clone)]
pub struct SystemEvent {
pub event_type: SystemEventType,
pub tag: Option<String>,
pub value: Option<Value>,
pub id: Option<String>,
pub window_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SystemEventType {
SystemInfo,
SystemTheme,
AnimationFrame,
ThemeChanged,
AllWindowsClosed,
ImageList,
TreeHash,
FindFocused,
Announce,
Diagnostic,
RecoveryFailed,
SessionError,
SessionClosed,
Error,
}
#[derive(Debug, Clone)]
pub struct CommandError {
pub reason: String,
pub id: Option<String>,
pub family: Option<String>,
pub widget_type: Option<String>,
pub message: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ModifiersEvent {
pub modifiers: KeyModifiers,
pub captured: bool,
pub window_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ImeEvent {
pub event_type: ImeEventType,
pub id: Option<String>,
pub scope: Vec<String>,
pub text: Option<String>,
pub cursor: Option<(usize, usize)>,
pub captured: bool,
pub window_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ImeEventType {
Opened,
Preedit,
Commit,
Closed,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parse_file_opened() {
let value = json!({"path": "/tmp/readme.txt"});
let result = EffectResult::parse("file_open", "ok", Some(&value));
match result {
EffectResult::FileOpened { path } => assert_eq!(path, "/tmp/readme.txt"),
other => panic!("expected FileOpened, got {other:?}"),
}
}
#[test]
fn parse_files_opened() {
let value = json!({"paths": ["/a.txt", "/b.txt"]});
let result = EffectResult::parse("file_open_multiple", "ok", Some(&value));
match result {
EffectResult::FilesOpened { paths } => {
assert_eq!(paths, vec!["/a.txt", "/b.txt"]);
}
other => panic!("expected FilesOpened, got {other:?}"),
}
}
#[test]
fn parse_file_saved() {
let value = json!({"path": "/tmp/out.csv"});
let result = EffectResult::parse("file_save", "ok", Some(&value));
match result {
EffectResult::FileSaved { path } => assert_eq!(path, "/tmp/out.csv"),
other => panic!("expected FileSaved, got {other:?}"),
}
}
#[test]
fn parse_directory_selected() {
let value = json!({"path": "/home/user/docs"});
let result = EffectResult::parse("directory_select", "ok", Some(&value));
match result {
EffectResult::DirectorySelected { path } => assert_eq!(path, "/home/user/docs"),
other => panic!("expected DirectorySelected, got {other:?}"),
}
}
#[test]
fn parse_directories_selected() {
let value = json!({"paths": ["/a", "/b", "/c"]});
let result = EffectResult::parse("directory_select_multiple", "ok", Some(&value));
match result {
EffectResult::DirectoriesSelected { paths } => {
assert_eq!(paths, vec!["/a", "/b", "/c"]);
}
other => panic!("expected DirectoriesSelected, got {other:?}"),
}
}
#[test]
fn parse_clipboard_text() {
let value = json!({"text": "hello world"});
let result = EffectResult::parse("clipboard_read", "ok", Some(&value));
match result {
EffectResult::ClipboardText { text } => assert_eq!(text, "hello world"),
other => panic!("expected ClipboardText, got {other:?}"),
}
}
#[test]
fn parse_clipboard_primary_text() {
let value = json!({"text": "primary selection"});
let result = EffectResult::parse("clipboard_read_primary", "ok", Some(&value));
match result {
EffectResult::ClipboardText { text } => assert_eq!(text, "primary selection"),
other => panic!("expected ClipboardText, got {other:?}"),
}
}
#[test]
fn parse_clipboard_html() {
let value = json!({"html": "<b>bold</b>"});
let result = EffectResult::parse("clipboard_read_html", "ok", Some(&value));
match result {
EffectResult::ClipboardHtml { html } => assert_eq!(html, "<b>bold</b>"),
other => panic!("expected ClipboardHtml, got {other:?}"),
}
}
#[test]
fn parse_clipboard_written() {
let result = EffectResult::parse("clipboard_write", "ok", None);
assert!(matches!(result, EffectResult::ClipboardWritten));
}
#[test]
fn parse_clipboard_html_written() {
let result = EffectResult::parse("clipboard_write_html", "ok", None);
assert!(matches!(result, EffectResult::ClipboardWritten));
}
#[test]
fn parse_clipboard_cleared() {
let result = EffectResult::parse("clipboard_clear", "ok", None);
assert!(matches!(result, EffectResult::ClipboardCleared));
}
#[test]
fn parse_notification_shown() {
let result = EffectResult::parse("notification", "ok", None);
assert!(matches!(result, EffectResult::NotificationShown));
}
#[test]
fn parse_cancelled() {
let result = EffectResult::parse("file_open", "cancelled", None);
assert!(matches!(result, EffectResult::Cancelled));
}
#[test]
fn parse_unsupported() {
let result = EffectResult::parse("file_open", "unsupported", None);
assert!(matches!(result, EffectResult::Unsupported));
}
#[test]
fn parse_error_with_message() {
let value = json!("permission denied");
let result = EffectResult::parse("clipboard_read", "error", Some(&value));
match result {
EffectResult::Error(msg) => assert_eq!(msg, "permission denied"),
other => panic!("expected Error, got {other:?}"),
}
}
#[test]
fn parse_error_without_value() {
let result = EffectResult::parse("clipboard_read", "error", None);
match result {
EffectResult::Error(msg) => assert_eq!(msg, "unknown error"),
other => panic!("expected Error, got {other:?}"),
}
}
#[test]
fn parse_unknown_status_falls_back_to_other() {
let value = json!(42);
let result = EffectResult::parse("file_open", "pending", Some(&value));
match result {
EffectResult::Other(v) => assert_eq!(v, json!(42)),
other => panic!("expected Other, got {other:?}"),
}
}
#[test]
fn parse_unknown_kind_ok_falls_back_to_other() {
let value = json!({"custom": true});
let result = EffectResult::parse("future_effect", "ok", Some(&value));
match result {
EffectResult::Other(v) => assert_eq!(v, json!({"custom": true})),
other => panic!("expected Other, got {other:?}"),
}
}
}