diff --git a/config/scripts.lua b/config/scripts.lua index 69ec907..42211b4 100644 --- a/config/scripts.lua +++ b/config/scripts.lua @@ -1,6 +1,6 @@ local function volume() local function display_device(widgets, offset, device, device_type) - if device and device.Connected then + if device then table.insert(widgets, Widget.Text { text = device.Name, scrolling = true, diff --git a/omni-led-api/proto/plugin.proto b/omni-led-api/proto/plugin.proto index 5b79511..c691db1 100644 --- a/omni-led-api/proto/plugin.proto +++ b/omni-led-api/proto/plugin.proto @@ -16,6 +16,7 @@ message EventResponse {} message Field { oneof field { + None f_none = 8; bool f_bool = 1; int64 f_integer = 2; double f_float = 3; @@ -87,3 +88,5 @@ enum LogLevelFilter { LOG_LEVEL_FILTER_DEBUG = 5; LOG_LEVEL_FILTER_TRACE = 6; } + +message None {} \ No newline at end of file diff --git a/omni-led-api/src/types.rs b/omni-led-api/src/types.rs index 7342a1d..d4aa41a 100644 --- a/omni-led-api/src/types.rs +++ b/omni-led-api/src/types.rs @@ -80,6 +80,9 @@ macro_rules! into_field { }; } +// None +into_field!(None, field::Field::FNone); + // Boolean values into_field!(bool, field::Field::FBool); diff --git a/omni-led-applications/audio/README.md b/omni-led-applications/audio/README.md index 21c46b3..8ce3286 100644 --- a/omni-led-applications/audio/README.md +++ b/omni-led-applications/audio/README.md @@ -16,19 +16,18 @@ Audio application sends `AUDIO` events in two forms: 1. Full update for both devices on startup and on main input/output device change `AUDIO`: table - - `Input`: table - - `Connected`: bool + - `Input`: table | none - `IsMuted`: bool - `Volume`: integer - `Name`: string - - `Output`: table - - `Connected`: bool + - `Output`: table | none - `IsMuted`: bool - `Volume`: integer - `Name`: string - > `Connected` field tells if the device is actually connected or not. If `Connected` is `false` then rest of the data - > for the relevant device type is filled with dummy values. + > `Input` and `Output` fields are only sent if the devices are found. If the device is disconnected during the + lifetime of the application the fields will be set with value `none` so that they are cleaned up in the scripting + environment. 2. Partial update on main input/output device's volume change `AUDIO`: table diff --git a/omni-led-applications/audio/src/main.rs b/omni-led-applications/audio/src/main.rs index 758482e..f98b126 100644 --- a/omni-led-applications/audio/src/main.rs +++ b/omni-led-applications/audio/src/main.rs @@ -2,8 +2,8 @@ use audio::Audio; use clap::Parser; use log::debug; use omni_led_api::plugin::Plugin; +use omni_led_api::types::Table; use omni_led_derive::IntoProto; -use std::error::Error; use tokio::runtime::Handle; use tokio::sync::mpsc; use tokio::sync::mpsc::{Receiver, Sender}; @@ -13,9 +13,9 @@ mod audio; const NAME: &str = "AUDIO"; #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() { let options = Options::parse(); - let mut plugin = Plugin::new(NAME, &options.address).await?; + let mut plugin = Plugin::new(NAME, &options.address).await.unwrap(); let (tx, mut rx): ( Sender<(DeviceData, DeviceType)>, @@ -33,21 +33,23 @@ async fn main() -> Result<(), Box> { ); } - let event = match device_type { - DeviceType::Input => AudioEvent { - input: Some(data), - output: None, - }, - DeviceType::Output => AudioEvent { - input: None, - output: Some(data), - }, + let event_data = if data.connected { + Some(EventData { + is_muted: data.is_muted, + volume: data.volume, + name: data.name, + }) + } else { + None + }; + + let event: Table = match device_type { + DeviceType::Input => InputAudioEvent { input: event_data }.into(), + DeviceType::Output => OutputAudioEvent { output: event_data }.into(), }; plugin.update(event.into()).await.unwrap(); } - - Ok(()) } #[derive(Copy, Clone, Debug)] @@ -58,13 +60,26 @@ pub enum DeviceType { #[derive(IntoProto)] #[proto(rename_all = PascalCase)] -struct AudioEvent { - input: Option, - output: Option, +struct InputAudioEvent { + #[proto(strong_none)] + input: Option, +} + +#[derive(IntoProto)] +#[proto(rename_all = PascalCase)] +struct OutputAudioEvent { + #[proto(strong_none)] + output: Option, } #[derive(IntoProto)] #[proto(rename_all = PascalCase)] +struct EventData { + is_muted: bool, + volume: i32, + name: Option, +} + struct DeviceData { connected: bool, is_muted: bool, diff --git a/omni-led-derive/src/into_proto.rs b/omni-led-derive/src/into_proto.rs index 8dc8374..acae45f 100644 --- a/omni-led-derive/src/into_proto.rs +++ b/omni-led-derive/src/into_proto.rs @@ -3,7 +3,7 @@ use proc_macro2::TokenStream; use quote::quote; use syn::{Attribute, Data, DeriveInput}; -use crate::common::{get_attribute, is_option, parse_attributes}; +use crate::common::{get_attribute, get_attribute_with_default_value, is_option, parse_attributes}; pub fn expand_into_proto_derive(input: DeriveInput) -> proc_macro::TokenStream { let name = input.ident; @@ -47,10 +47,11 @@ fn generate_assignments(data: &Data, struct_attrs: &StructAttributes) -> TokenSt None => field_name, }; - let is_option = is_option(&field.ty); - let attrs = get_field_attributes(&field.attrs); + let is_option = is_option(&field.ty); + let propagate_none = attrs.strong_none.is_some(); + let value_accessor = if is_option { quote! { value } } else { @@ -65,17 +66,30 @@ fn generate_assignments(data: &Data, struct_attrs: &StructAttributes) -> TokenSt }; let insertion = quote! { - table.items.insert(#renamed.to_string(), #transformed.into()); + table.items.insert(#renamed.to_string(), #transformed.into()) + }; + + let none_insertion = quote! { + table.items.insert(#renamed.to_string(), omni_led_api::types::None{}.into()) }; if is_option { - quote! { - if let Some(value) = self.#field_identifier { - #insertion + if propagate_none { + quote! { + match self.#field_identifier { + Some(value) => #insertion, + None => #none_insertion, + }; + } + } else { + quote! { + if let Some(value) = self.#field_identifier { + #insertion; + } } } } else { - insertion + quote! { #insertion; } } }); quote! { #(#assignments)* } @@ -99,6 +113,7 @@ fn get_struct_attributes(attributes: &Vec) -> StructAttributes { } struct FieldAttributes { + strong_none: Option, transform: Option, } @@ -106,6 +121,7 @@ fn get_field_attributes(attributes: &Vec) -> FieldAttributes { let mut attributes = parse_attributes("proto", attributes); FieldAttributes { + strong_none: get_attribute_with_default_value(&mut attributes, "strong_none", quote! {}), transform: get_attribute(&mut attributes, "transform"), } } diff --git a/omni-led-lib/Cargo.toml b/omni-led-lib/Cargo.toml index 42dfaf1..4f27dae 100644 --- a/omni-led-lib/Cargo.toml +++ b/omni-led-lib/Cargo.toml @@ -44,6 +44,7 @@ libc = "0.2" windres = "0.2" [dev-dependencies] +omni-led-derive = { path = "../omni-led-derive", features = ["into-proto"] } test-case = "3.3.1" [features] diff --git a/omni-led-lib/src/common/common.rs b/omni-led-lib/src/common/common.rs index d7a2c9c..0a7abf2 100644 --- a/omni-led-lib/src/common/common.rs +++ b/omni-led-lib/src/common/common.rs @@ -1,9 +1,4 @@ use mlua::{Lua, Table, Value, chunk}; -use omni_led_api::types::Field; -use omni_led_api::types::field::Field as FieldEntry; -use std::hash::{DefaultHasher, Hash, Hasher}; - -use crate::script_handler::script_data_types::ImageData; #[macro_export] macro_rules! create_table { @@ -51,8 +46,6 @@ macro_rules! create_table_with_defaults { }}; } -pub const KEY_VAL_TABLE: &str = "key-val-table"; - pub fn load_internal_functions(lua: &Lua) { let dump = lua .create_function(|_, value: Value| { @@ -90,53 +83,3 @@ pub fn load_internal_functions(lua: &Lua) { ) .unwrap(); } - -pub fn proto_to_lua_value(lua: &Lua, field: Field) -> mlua::Result { - match field.field { - None => Ok(mlua::Nil), - Some(FieldEntry::FBool(bool)) => Ok(Value::Boolean(bool)), - Some(FieldEntry::FInteger(integer)) => Ok(Value::Integer(integer)), - Some(FieldEntry::FFloat(float)) => Ok(Value::Number(float)), - Some(FieldEntry::FString(string)) => { - let string = lua.create_string(string)?; - Ok(Value::String(string)) - } - Some(FieldEntry::FArray(array)) => { - let size = array.items.len(); - let table = lua.create_table_with_capacity(size, 0)?; - for value in array.items { - table.push(proto_to_lua_value(lua, value)?)?; - } - Ok(Value::Table(table)) - } - Some(FieldEntry::FTable(map)) => { - let size = map.items.len(); - let table = lua.create_table_with_capacity(0, size)?; - for (key, value) in map.items { - table.set(key, proto_to_lua_value(lua, value)?)?; - } - - let meta = lua.create_table_with_capacity(0, 1)?; - meta.set(KEY_VAL_TABLE, true)?; - _ = table.set_metatable(Some(meta)); - - Ok(Value::Table(table)) - } - Some(FieldEntry::FImageData(image)) => { - let hash = hash(&image.data); - let image_data = ImageData { - format: image.format().try_into().map_err(mlua::Error::external)?, - bytes: image.data, - hash: Some(hash), - }; - let user_data = lua.create_any_userdata(image_data)?; - Ok(Value::UserData(user_data)) - } - } -} - -pub fn hash(t: &T) -> u64 { - let mut s = DefaultHasher::new(); - t.hash(&mut s); - s.finish() -} diff --git a/omni-led-lib/src/events/events.rs b/omni-led-lib/src/events/events.rs index 53072a6..ff9e7c0 100644 --- a/omni-led-lib/src/events/events.rs +++ b/omni-led-lib/src/events/events.rs @@ -1,11 +1,13 @@ -use mlua::{ErrorContext, Function, Lua, UserData, UserDataMethods, Value}; +use mlua::{ErrorContext, Function, Lua, Table, UserData, UserDataMethods, Value}; +use omni_led_api::types::field::Field as FieldEntry; use omni_led_api::types::{Field, field}; use omni_led_derive::UniqueUserData; +use std::hash::{DefaultHasher, Hash, Hasher}; -use crate::common::common::{KEY_VAL_TABLE, proto_to_lua_value}; use crate::common::user_data::UniqueUserData; use crate::events::event_queue::Event; use crate::keyboard::keyboard::{KeyboardEvent, KeyboardEventEventType}; +use crate::script_handler::script_data_types::ImageData; #[derive(UniqueUserData)] pub struct Events { @@ -59,8 +61,11 @@ impl Events { match value { Value::Table(table) => match table.metatable() { Some(metatable) => { - if !metatable.contains_key(KEY_VAL_TABLE)? { - unreachable!("Only key-value tables should have a metatable") + if !metatable.contains_key(CLEANUP_ENTRIES)? { + return Err(mlua::Error::runtime(format!( + "Unexpected metatable {:#?}", + metatable + ))); } table.for_each(|key: String, val: Value| { @@ -109,3 +114,187 @@ struct EventEntry { event: String, on_match: Function, } + +const CLEANUP_ENTRIES: &str = "__cleanup_entries"; + +pub fn get_cleanup_entries_metatable(table: &Table) -> mlua::Result> { + match table.metatable() { + Some(metatable) => metatable.get(CLEANUP_ENTRIES), + None => Ok(None), + } +} + +pub fn proto_to_lua_value(lua: &Lua, field: Field) -> mlua::Result { + match field.field { + Some(FieldEntry::FNone(_)) | None => Ok(mlua::Nil), + Some(FieldEntry::FBool(bool)) => Ok(Value::Boolean(bool)), + Some(FieldEntry::FInteger(integer)) => Ok(Value::Integer(integer)), + Some(FieldEntry::FFloat(float)) => Ok(Value::Number(float)), + Some(FieldEntry::FString(string)) => { + let string = lua.create_string(string)?; + Ok(Value::String(string)) + } + Some(FieldEntry::FArray(array)) => { + let size = array.items.len(); + let table = lua.create_table_with_capacity(size, 0)?; + for value in array.items { + table.push(proto_to_lua_value(lua, value)?)?; + } + Ok(Value::Table(table)) + } + Some(FieldEntry::FTable(map)) => { + let size = map.items.len(); + + let table = lua.create_table_with_capacity(0, size)?; + let cleanup_entries = lua.create_table_with_capacity(0, size)?; + + for (key, value) in map.items { + match proto_to_lua_value(lua, value)? { + Value::Nil => cleanup_entries.set(key, true)?, + value => table.set(key, value)?, + } + } + + let meta = lua.create_table_with_capacity(0, 1)?; + meta.set(CLEANUP_ENTRIES, cleanup_entries)?; + _ = table.set_metatable(Some(meta)); + + Ok(Value::Table(table)) + } + Some(FieldEntry::FImageData(image)) => { + let hash = hash(&image.data); + let image_data = ImageData { + format: image.format().try_into().map_err(mlua::Error::external)?, + bytes: image.data, + hash: Some(hash), + }; + let user_data = lua.create_any_userdata(image_data)?; + Ok(Value::UserData(user_data)) + } + } +} +fn hash(t: &T) -> u64 { + let mut s = DefaultHasher::new(); + t.hash(&mut s); + s.finish() +} + +#[cfg(test)] +mod tests { + use super::*; + use omni_led_api::types::None; + use omni_led_derive::IntoProto; + + #[test] + fn convert_nil() { + let lua = Lua::new(); + assert_eq!( + proto_to_lua_value(&lua, None {}.into()).unwrap(), + Value::Nil + ) + } + + #[test] + fn convert_bool() { + let lua = Lua::new(); + assert_eq!( + proto_to_lua_value(&lua, true.into()).unwrap(), + Value::Boolean(true) + ) + } + + #[test] + fn convert_integer() { + let lua = Lua::new(); + assert_eq!( + proto_to_lua_value(&lua, 68.into()).unwrap(), + Value::Integer(68) + ) + } + + #[test] + fn convert_float() { + let lua = Lua::new(); + assert_eq!( + proto_to_lua_value(&lua, 6.8.into()).unwrap(), + Value::Number(6.8) + ) + } + + #[test] + fn convert_string() { + let lua = Lua::new(); + let string = "Omegalul"; + assert_eq!( + proto_to_lua_value(&lua, string.into()).unwrap(), + Value::String(lua.create_string(string).unwrap()) + ) + } + + #[test] + fn convert_array() { + let lua = Lua::new(); + let array = vec![1, 2, 3, 4]; + + let result = proto_to_lua_value(&lua, array.clone().into()).unwrap(); + assert!(result.is_table()); + + let result = result.as_table().unwrap(); + let cleanup_entries = get_cleanup_entries_metatable(result).unwrap(); + + assert!(cleanup_entries.is_none()); + assert_eq!(result.len().unwrap() as usize, array.len()); + result + .for_each(|lua_index: usize, value: i64| { + let index = lua_index - 1; + assert_eq!(value, array[index], "Missmatch at index {}", index); + Ok(()) + }) + .unwrap(); + } + + #[test] + fn convert_table() { + #[derive(IntoProto)] + struct Input { + a: i64, + b: String, + c: Option, + d: Option, + #[proto(strong_none)] + e: Option, + #[proto(strong_none)] + f: Option, + } + + let lua = Lua::new(); + let input = Input { + a: 0, + b: "b".to_string(), + c: None, + d: Some(true), + e: None, + f: Some(1.23), + }; + + let result = proto_to_lua_value(&lua, input.into()).unwrap(); + assert!(result.is_table()); + + let result = result.as_table().unwrap(); + let cleanup_entries = get_cleanup_entries_metatable(result).unwrap(); + + assert!(cleanup_entries.is_some()); + + let cleanup_entries = cleanup_entries.unwrap(); + + assert_eq!(cleanup_entries.pairs::().count(), 1); + // We only care that the key is present. As long as it's there, the condition will hold. + assert_ne!(cleanup_entries.get::("e").unwrap(), Value::Nil); + + assert_eq!(result.pairs::().count(), 4); + assert_eq!(result.get::("a").unwrap(), 0); + assert_eq!(result.get::("b").unwrap(), String::from("b")); + assert_eq!(result.get::>("d").unwrap(), Some(true)); + assert_eq!(result.get::>("f").unwrap(), Some(1.23)); + } +} diff --git a/omni-led-lib/src/script_handler/script_handler.rs b/omni-led-lib/src/script_handler/script_handler.rs index 7e621c8..1f8c9fb 100644 --- a/omni-led-lib/src/script_handler/script_handler.rs +++ b/omni-led-lib/src/script_handler/script_handler.rs @@ -7,13 +7,12 @@ use std::collections::HashMap; use std::rc::Rc; use std::time::Duration; -use crate::common::common::KEY_VAL_TABLE; use crate::common::user_data::{UniqueUserData, UserDataRef}; use crate::constants::config::{ConfigType, load_config}; use crate::create_table_with_defaults; use crate::devices::device::Device; use crate::devices::devices::{DeviceStatus, Devices}; -use crate::events::events::Events; +use crate::events::events::{Events, get_cleanup_entries_metatable}; use crate::events::shortcuts::Shortcuts; use crate::renderer::animation::State; use crate::renderer::animation_group::AnimationGroup; @@ -88,20 +87,22 @@ impl ScriptHandler { value: Value, ) -> mlua::Result<()> { match value { - Value::Table(table) => match table.metatable() { - Some(metatable) => { - if !metatable.contains_key(KEY_VAL_TABLE)? { - unreachable!("Only key-value tables should have a metatable") - } - + Value::Table(table) => match get_cleanup_entries_metatable(&table)? { + Some(cleanup_entries) => { if !parent.contains_key(value_name)? { let empty = lua.create_table()?; parent.set(value_name, empty)?; } let entry: Table = parent.get(value_name)?; + println!("{value_name}"); + table.for_each(|key: String, val: Value| { Self::set_value_impl(lua, &entry, &key, val) + })?; + + cleanup_entries.for_each(|key: String, _: Value| { + Self::set_value_impl(lua, &entry, &key, Value::Nil) }) } None => { @@ -109,7 +110,10 @@ impl ScriptHandler { parent.set(value_name, Value::Table(table)) } }, - value => parent.set(value_name, value), + value => { + println!(" - {value_name}: {value:?}"); + parent.set(value_name, value) + } } } @@ -491,3 +495,233 @@ impl UserData for ScreenBuilderImpl { }); } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::create_table; + use crate::events::events::proto_to_lua_value; + use omni_led_derive::IntoProto; + + fn assert_tables_equal(lua: &Lua, left: &Table, right: &Table, line: u32) { + assert_tables_equal_impl(lua, left, right, &format!("{line}")) + } + + fn assert_tables_equal_impl(lua: &Lua, left: &Table, right: &Table, path: &str) { + let left_count = left.pairs::().count(); + let right_count = right.pairs::().count(); + assert_eq!( + left_count, right_count, + "Tables at '{}' have different sizes: {} vs {}", + path, left_count, right_count + ); + + for pair in left.pairs::() { + let (key, left_value) = pair.unwrap(); + let new_path = format!("{}.{}", path, key); + let right_value: Value = right.raw_get(key).unwrap(); + + assert_values_equal(lua, &left_value, &right_value, &new_path); + } + } + + fn assert_values_equal(lua: &Lua, left: &Value, right: &Value, path: &str) { + match (left, right) { + (Value::Nil, Value::Nil) => {} + (Value::Boolean(l), Value::Boolean(r)) => { + assert_eq!(l, r, "Booleans at '{}' differ: {} vs {}", path, l, r); + } + (Value::Integer(l), Value::Integer(r)) => { + assert_eq!(l, r, "Integers at '{}' differ: {} vs {}", path, l, r); + } + (Value::Number(l), Value::Number(r)) => { + assert!( + (l - r).abs() < f64::EPSILON, + "Numbers at '{}' differ: {} vs {}", + path, + l, + r + ); + } + (Value::String(l), Value::String(r)) => { + assert_eq!( + l.to_str().unwrap(), + r.to_str().unwrap(), + "Strings at '{}' differ: {:?} vs {:?}", + path, + l.to_str().unwrap(), + r.to_str().unwrap() + ); + } + (Value::Table(l), Value::Table(r)) => assert_tables_equal_impl(lua, l, r, path), + (l, r) => { + // Types don't match, or we have unhandled types, good enough for testing purposes + panic!( + "Unexpected value at '{}': {}({:?}) vs {}({:?})", + path, + l.type_name(), + l, + r.type_name(), + r + ); + } + } + } + + #[derive(IntoProto)] + struct InputA { + a: Option, + b: Option, + } + + #[derive(IntoProto)] + struct InputB { + b: Option, + c: Option, + } + + #[derive(IntoProto)] + struct InputC { + c: Option, + } + + #[derive(IntoProto)] + struct InputStrongA { + a: Option, + #[proto(strong_none)] + b: Option, + } + + #[test] + fn recursive_set() { + let lua = Lua::new(); + let env = lua.create_table().unwrap(); + + let input = InputA { + a: Some(1), + b: Some(InputB { + b: Some(2), + c: Some(InputC { c: None }), + }), + }; + let input = proto_to_lua_value(&lua, input.into()).unwrap(); + ScriptHandler::set_value_impl(&lua, &env, "a", input).unwrap(); + + let expected = create_table! { + lua, + { + a = { + a = 1, + b = { + b = 2, + c = { } + } + } + } + }; + assert_tables_equal(&lua, &expected, &env, line!()); + } + + #[test] + fn partial_update() { + let lua = Lua::new(); + let env = lua.create_table().unwrap(); + + let input = InputA { + a: None, + b: Some(InputB { + b: Some(2), + c: None, + }), + }; + let input = proto_to_lua_value(&lua, input.into()).unwrap(); + ScriptHandler::set_value_impl(&lua, &env, "a", input).unwrap(); + + let expected = create_table! { + lua, + { + a = { + b = { + b = 2, + } + } + } + }; + assert_tables_equal(&lua, &expected, &env, line!()); + + let input = InputA { + a: Some(1), + b: Some(InputB { + b: None, + c: Some(InputC { c: Some(3) }), + }), + }; + let input = proto_to_lua_value(&lua, input.into()).unwrap(); + ScriptHandler::set_value_impl(&lua, &env, "a", input).unwrap(); + + let expected = create_table! { + lua, + { + a = { + a = 1, + b = { + b = 2, + c = { + c = 3, + }, + } + } + } + }; + assert_tables_equal(&lua, &expected, &env, line!()); + } + + #[test] + fn partial_update_remove() { + let lua = Lua::new(); + let env = lua.create_table().unwrap(); + + let input = InputA { + a: Some(1), + b: Some(InputB { + b: Some(2), + c: Some(InputC { c: Some(3) }), + }), + }; + let input = proto_to_lua_value(&lua, input.into()).unwrap(); + ScriptHandler::set_value_impl(&lua, &env, "a", input).unwrap(); + + let expected = create_table! { + lua, + { + a = { + a = 1, + b = { + b = 2, + c = { + c = 3, + } + } + } + } + }; + assert_tables_equal(&lua, &expected, &env, line!()); + + let input = InputStrongA { + a: Some(1), + b: None, + }; + let input = proto_to_lua_value(&lua, input.into()).unwrap(); + ScriptHandler::set_value_impl(&lua, &env, "a", input).unwrap(); + + let expected = create_table! { + lua, + { + a = { + a = 1, + } + } + }; + assert_tables_equal(&lua, &expected, &env, line!()); + } +}