Complete proof of concept

This commit is contained in:
NGnius (Graham) 2022-09-11 23:45:31 -04:00
parent 0435f14680
commit 569eab5880
46 changed files with 1413 additions and 169 deletions

4
.gitignore vendored
View file

@ -42,8 +42,8 @@ yalc.lock
.vscode/settings.json .vscode/settings.json
# ignore Rust compiler files # ignore Rust compiler files
/server/target /backend/target
backend /backend/out
/bin /bin
# packaged teasers # packaged teasers

View file

@ -2,6 +2,28 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "async-recursion"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "async-trait"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@ -349,10 +371,12 @@ checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
name = "kaylon" name = "kaylon"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait",
"log", "log",
"serde", "serde",
"serde_json", "serde_json",
"simplelog", "simplelog",
"tokio",
"usdpl-back", "usdpl-back",
] ]
@ -959,11 +983,12 @@ dependencies = [
[[package]] [[package]]
name = "usdpl-back" name = "usdpl-back"
version = "0.6.0" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbbc0781e83ba990f8239142e33173a2d2548701775f3db66702d1af4fd0319a"
dependencies = [ dependencies = [
"async-recursion",
"async-trait",
"bytes", "bytes",
"log",
"tokio", "tokio",
"usdpl-core", "usdpl-core",
"warp", "warp",
@ -972,8 +997,6 @@ dependencies = [
[[package]] [[package]]
name = "usdpl-core" name = "usdpl-core"
version = "0.6.0" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862153581fac266458521f49e5906a71c1eee1665cb4c7d71e9586bd34b45394"
dependencies = [ dependencies = [
"base64", "base64",
] ]

View file

@ -6,8 +6,13 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
usdpl-back = { version = "0.6.0", features = ["decky"] } usdpl-back = { version = "0.7.0", features = ["decky"], path = "../../usdpl-rs/usdpl-back" }
# async
tokio = { version = "*", features = ["sync", "time"] }
async-trait = "0.1.57"
# json
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

12
backend/build.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
#cargo build --release --target x86_64-unknown-linux-musl
cargo build --target x86_64-unknown-linux-musl
#cross build --release
mkdir -p ../bin
#cp ./target/x86_64-unknown-linux-musl/release/kaylon ../bin/backend
cp ./target/x86_64-unknown-linux-musl/debug/kaylon ../bin/backend
#cp ./target/release/kaylon ../bin/backend
cp ../kaylon.json ../bin/kaylon.json

25
backend/src/api/about.rs Normal file
View file

@ -0,0 +1,25 @@
use std::sync::{Mutex, mpsc::{Sender, channel}};
use super::ApiParameterType;
use crate::runtime::{QueueAction, QueueItem};
pub fn get_about(sender: Sender<QueueItem>) -> impl Fn(ApiParameterType) -> ApiParameterType {
let sender = Mutex::new(sender);
move |_| {
log::debug!("API: get_about");
let (rx, tx) = channel();
sender.lock().unwrap().send(
QueueItem {
action: QueueAction::GetAbout {
respond_to: rx,
}
}
).unwrap();
vec![
usdpl_back::core::serdes::Primitive::Json(
serde_json::to_string(&tx.recv().unwrap()).unwrap()
)
]
}
}

View file

@ -0,0 +1,72 @@
use std::sync::{Mutex, mpsc::{Sender, channel, self}};
use usdpl_back::core::serdes::Primitive;
use usdpl_back::AsyncCallable;
use super::ApiParameterType;
use crate::runtime::{QueueAction, QueueItem};
pub struct GetDisplayEndpoint {
//sender: tokio::sync::mpsc::Sender<SetCallbackAsync>,
//receiver: Mutex<Option<tokio::sync::mpsc::Receiver<SetCallbackAsync>>>,
sync_sender: Mutex<Sender<QueueItem>>,
}
impl GetDisplayEndpoint {
pub fn new(sender: Sender<QueueItem>) -> Self {
//let (async_tx, async_rx) = tokio::sync::mpsc::channel::<SetCallbackAsync>(64);
Self {
//sender: async_tx,
//receiver: Mutex::new(Some(async_rx)),
sync_sender: Mutex::new(sender),
}
}
}
#[async_trait::async_trait]
impl AsyncCallable for GetDisplayEndpoint {
async fn call(&self, params: ApiParameterType) -> ApiParameterType {
log::debug!("API: get_display");
if let Some(Primitive::F64(index)) = params.get(0) {
let index = *index as usize;
let (respond_to, receiver) = channel();
log::info!("requesting display for item #{}", index);
let send_result = self.sync_sender.lock().unwrap().send(
QueueItem {
action: QueueAction::SetCallback {
index,
respond_to,
}
}
);
if let Ok(_) = send_result {
// TODO: don't poll for response
log::info!("waiting for display for item #{}", index);
let sleep_duration = std::time::Duration::from_millis(10);
let receiver = Mutex::new(receiver);
loop {
let received = receiver.lock().unwrap().try_recv();
match received {
Err(mpsc::TryRecvError::Disconnected) => {
log::info!("Failed to response for get_display for #{}", index);
return vec![Primitive::Empty];
},
Err(_) => {},
Ok(x) => {
log::debug!("got display for item #{}", index);
return vec![x];
},
}
tokio::time::sleep(sleep_duration).await;
}
} else {
log::info!("Failed to get_display for #{}", index);
vec![Primitive::Empty]
}
} else {
vec![Primitive::Empty]
}
}
}

View file

@ -0,0 +1,26 @@
use std::sync::{Mutex, mpsc::{Sender, channel}};
use super::ApiParameterType;
use crate::runtime::{QueueAction, QueueItem};
pub fn get_items(sender: Sender<QueueItem>) -> impl Fn(ApiParameterType) -> ApiParameterType {
let sender = Mutex::new(sender);
move |_| {
log::debug!("API: get_items");
let (rx, tx) = channel();
sender.lock().unwrap().send(
QueueItem {
action: QueueAction::DoReload {
respond_to: rx,
}
}
).unwrap();
log::info!("waiting for items");
vec![
usdpl_back::core::serdes::Primitive::Json(
serde_json::to_string(&tx.recv().unwrap()).unwrap()
)
]
}
}

13
backend/src/api/mod.rs Normal file
View file

@ -0,0 +1,13 @@
mod about;
mod get_display;
mod get_item;
mod on_update;
mod reload;
pub use about::get_about;
pub use get_display::GetDisplayEndpoint;
pub use get_item::get_items;
pub use on_update::on_update;
pub use reload::reload;
pub(super) type ApiParameterType = Vec<usdpl_back::core::serdes::Primitive>;

View file

@ -0,0 +1,34 @@
use std::sync::{Mutex, mpsc::Sender};
use usdpl_back::core::serdes::Primitive;
use super::ApiParameterType;
use crate::runtime::{QueueAction, QueueItem};
pub fn on_update(sender: Sender<QueueItem>) -> impl Fn(ApiParameterType) -> ApiParameterType {
let sender = Mutex::new(sender);
move |mut params: ApiParameterType| {
log::debug!("API: on_update");
if params.len() == 2 {
if let Primitive::F64(index) = params.remove(0) {
let index = index as usize;
let val = params.remove(0);
sender.lock().unwrap().send(
QueueItem {
action: QueueAction::DoUpdate {
index,
value: val,
}
}
).unwrap();
log::info!("Sent update for #{}", index);
vec![true.into()]
} else {
vec![false.into()]
}
} else {
vec![false.into()]
}
}
}

26
backend/src/api/reload.rs Normal file
View file

@ -0,0 +1,26 @@
use std::sync::{Mutex, mpsc::{Sender, channel}};
use super::ApiParameterType;
use crate::runtime::{QueueAction, QueueItem};
pub fn reload(sender: Sender<QueueItem>) -> impl Fn(ApiParameterType) -> ApiParameterType {
let sender = Mutex::new(sender);
move |_| {
log::debug!("API: reload");
let (rx, tx) = channel();
sender.lock().unwrap().send(
QueueItem {
action: QueueAction::DoReload {
respond_to: rx,
}
}
).unwrap();
log::info!("waiting for JSON reload");
vec![
usdpl_back::core::serdes::Primitive::Json(
serde_json::to_string(&tx.recv().unwrap()).unwrap()
)
]
}
}

View file

@ -0,0 +1,24 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone)]
pub struct AboutConfig {
pub name: String,
pub version: String,
pub description: String,
pub url: Option<String>,
pub authors: Vec<String>,
pub license: Option<String>,
}
impl Default for AboutConfig {
fn default() -> Self {
Self {
name: env!("CARGO_PKG_NAME").to_owned(),
version: env!("CARGO_PKG_VERSION").to_owned(),
description: env!("CARGO_PKG_DESCRIPTION").to_owned(),
url: Some(env!("CARGO_PKG_HOMEPAGE").to_owned()),
authors: env!("CARGO_PKG_AUTHORS").split(':').map(|x| x.to_owned()).collect(),
license: Some(env!("CARGO_PKG_LICENSE").to_owned())
}
}
}

View file

@ -0,0 +1,52 @@
use serde::{Serialize, Deserialize};
use super::{ElementConfig, AboutConfig};
#[derive(Serialize, Deserialize, Clone)]
#[serde(tag = "api-version")]
pub enum BaseConfig {
#[serde(rename = "v0.0.0")]
V0 {
items: Vec<ElementConfig>,
about: AboutConfig,
},
}
impl BaseConfig {
pub fn load<P: AsRef<std::path::Path>>(path: P) -> Self {
//let path = std::path::Path::new("./").join(path);
let path = path.as_ref();
match std::fs::File::open(&path) {
Ok(file) => {
let reader = std::io::BufReader::new(file);
match serde_json::from_reader(reader) {
Ok(conf) => return conf,
Err(e) => log::error!("Failed to deserialize {}: {}", path.display(), e),
}
},
Err(e) => log::error!("Failed to open {}: {}", path.display(), e),
}
panic!("Cannot open {}", path.display())
}
#[inline]
pub fn get_about(&self) -> &AboutConfig {
match self {
Self::V0 {about, ..} => about,
}
}
#[inline]
pub fn get_item(&self, index: usize) -> Option<&ElementConfig> {
match self {
Self::V0 {items, ..} => items.get(index),
}
}
#[inline]
pub fn items(&self) -> &Vec<ElementConfig> {
match self {
Self::V0 {items, ..} => items,
}
}
}

View file

@ -1,6 +1,6 @@
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use super::{ButtonConfig, ToggleConfig, SliderConfig, ReadingConfig}; use super::{ButtonConfig, ToggleConfig, SliderConfig, ReadingConfig, ResultDisplayConfig};
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
#[serde(tag = "element")] #[serde(tag = "element")]
@ -12,5 +12,7 @@ pub enum ElementConfig {
#[serde(rename = "slider")] #[serde(rename = "slider")]
Slider(SliderConfig), Slider(SliderConfig),
#[serde(rename = "reading")] #[serde(rename = "reading")]
Reading(ReadingConfig), ReadingDisplay(ReadingConfig),
#[serde(rename = "result-display")]
ResultDisplay(ResultDisplayConfig),
} }

View file

@ -4,6 +4,7 @@ mod base;
mod button; mod button;
mod element; mod element;
mod reading; mod reading;
mod result_display;
mod slider; mod slider;
mod toggle; mod toggle;
@ -13,6 +14,7 @@ pub use base::BaseConfig;
pub use button::ButtonConfig; pub use button::ButtonConfig;
pub use element::ElementConfig; pub use element::ElementConfig;
pub use reading::ReadingConfig; pub use reading::ReadingConfig;
pub use result_display::ResultDisplayConfig;
pub use slider::SliderConfig; pub use slider::SliderConfig;
pub use toggle::ToggleConfig; pub use toggle::ToggleConfig;
@ -31,8 +33,7 @@ mod test {
ElementConfig::Toggle(ToggleConfig { ElementConfig::Toggle(ToggleConfig {
title: "Test Toggle".into(), title: "Test Toggle".into(),
description: Some("Toggle description".into()), description: Some("Toggle description".into()),
on_enable: ActionConfig::Command(CommandAction{run: "echo 'hello toggle 1'".into()}), on_toggle: ActionConfig::Command(CommandAction{run: "echo 'hello toggle $KAYLON_VALUE'".into()}),
on_disable: ActionConfig::Command(CommandAction{run: "echo 'hello toggle 0'".into()}),
}), }),
ElementConfig::Slider(SliderConfig { ElementConfig::Slider(SliderConfig {
title: "Test Slider".into(), title: "Test Slider".into(),
@ -41,18 +42,22 @@ mod test {
notches: None, notches: None,
on_set: ActionConfig::Command(CommandAction{run: "echo 'hello slider'".into()}), on_set: ActionConfig::Command(CommandAction{run: "echo 'hello slider'".into()}),
}), }),
ElementConfig::Reading(ReadingConfig { ElementConfig::ReadingDisplay(ReadingConfig {
title: "Test Reading".into(), title: "Test Reading".into(),
period_ms: 10000, period_ms: 10000,
on_period: ActionConfig::Command(CommandAction{run: "echo 'hello reading'".into()}) on_period: ActionConfig::Command(CommandAction{run: "echo 'hello reading'".into()})
}), }),
ElementConfig::ResultDisplay(ResultDisplayConfig {
title: "Test Reading".into(),
result_of: 1,
}),
], ],
about: AboutConfig { about: AboutConfig {
name: "Test name".into(), name: "Test name".into(),
version: "v0.42.0".into(), version: "v0.42.0".into(),
description: "Test description".into(), description: "Test description".into(),
url: Some("https://github.com/NGnius/kaylon".into()), url: Some("https://github.com/NGnius/kaylon".into()),
author: Some("NGnius <ngniusness@gmail.com>".into()), authors: vec!["NGnius <ngniusness@gmail.com>".into()],
license: Some("MIT".into()), license: Some("MIT".into()),
}, },
}; };

View file

@ -0,0 +1,8 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone)]
pub struct ResultDisplayConfig {
pub title: String,
/// index of element who's action's result will be used
pub result_of: usize,
}

View file

@ -6,6 +6,5 @@ use super::ActionConfig;
pub struct ToggleConfig { pub struct ToggleConfig {
pub title: String, pub title: String,
pub description: Option<String>, pub description: Option<String>,
pub on_enable: ActionConfig, pub on_toggle: ActionConfig,
pub on_disable: ActionConfig,
} }

6
backend/src/consts.rs Normal file
View file

@ -0,0 +1,6 @@
pub const PORT: u16 = 25717;
pub const PACKAGE_NAME: &'static str = env!("CARGO_PKG_NAME");
pub const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION");
pub const FILEPATH: &'static str = "./bin/kaylon.json";

34
backend/src/main.rs Normal file
View file

@ -0,0 +1,34 @@
mod api;
mod config;
mod consts;
mod runtime;
use simplelog::{WriteLogger, LevelFilter};
use usdpl_back::Instance;
use usdpl_back::core::serdes::Primitive;
fn main() -> Result<(), ()> {
let log_filepath = format!("/tmp/{}.log", consts::PACKAGE_NAME);
WriteLogger::init(
#[cfg(debug_assertions)]{LevelFilter::Debug},
#[cfg(not(debug_assertions))]{LevelFilter::Info},
Default::default(),
std::fs::File::create(&log_filepath).unwrap()
).unwrap();
let kaylon_conf = config::BaseConfig::load(consts::FILEPATH);
let (executor, sender) = runtime::RuntimeExecutor::new(kaylon_conf);
log::info!("Starting back-end ({} v{})", consts::PACKAGE_NAME, consts::PACKAGE_VERSION);
println!("Starting back-end ({} v{})", consts::PACKAGE_NAME, consts::PACKAGE_VERSION);
let instance = Instance::new(consts::PORT)
.register("hello", |_: Vec<Primitive>| vec![format!("Hello {}", consts::PACKAGE_NAME).into()])
.register_blocking("get_about", api::get_about(sender.clone()))
.register_async("get_display", api::GetDisplayEndpoint::new(sender.clone()))
.register_blocking("get_items", api::get_items(sender.clone()))
.register("on_update", api::on_update(sender.clone()))
.register_blocking("reload", api::reload(sender.clone()));
let _exec_handle = executor.spawn();
instance.run_blocking()
}

View file

@ -0,0 +1,68 @@
use usdpl_back::core::serdes::Primitive;
use crate::config::{ElementConfig, ActionConfig};
pub type ActError = String;
pub trait Act: Sized {
type Param;
type Config: ?Sized;
type Return;
fn build(config: &Self::Config, parameter: Self::Param) -> Result<Self, ActError>;
fn run(self) -> Self::Return;
}
pub struct Actor {
actor_type: ActorType,
index: usize,
}
impl Act for Actor {
type Param = (usize, Primitive);
type Config = ElementConfig;
type Return = Primitive;
fn build(config: &ElementConfig, parameter: Self::Param) -> Result<Self, ActError> {
let a_type = match config {
ElementConfig::Button(b) => ActorType::build(&b.on_click, parameter.1),
ElementConfig::Toggle(t) => ActorType::build(&t.on_toggle, parameter.1),
ElementConfig::Slider(s) => ActorType::build(&s.on_set, parameter.1),
ElementConfig::ReadingDisplay(r) => ActorType::build(&r.on_period, parameter.1),
ElementConfig::ResultDisplay(_) => Err(format!("Item #{} is a ResultDisplay, which can't act", parameter.0)),
}?;
Ok(Self {
actor_type: a_type,
index: parameter.0,
})
}
fn run(self) -> Self::Return {
log::info!("Running act for item {}", self.index);
let result = self.actor_type.run();
log::info!("Completed act for item {}", self.index);
result
}
}
pub enum ActorType {
Command(super::CommandActor),
}
impl Act for ActorType {
type Param = Primitive;
type Config = ActionConfig;
type Return = Primitive;
fn build(config: &Self::Config, parameter: Self::Param) -> Result<Self, ActError> {
Ok(match config {
ActionConfig::Command(c) =>
Self::Command(super::CommandActor::build(c, parameter)?),
})
}
fn run(self) -> Self::Return {
match self {
Self::Command(c) => c.run().into(),
}
}
}

View file

@ -0,0 +1,61 @@
use std::process::Command;
use usdpl_back::core::serdes::Primitive;
use crate::config::CommandAction;
use super::{Act, ActError};
const VALUE_ENV_VAR: &str = "KAYLON_VALUE";
pub struct CommandActor {
shell: String,
run: String,
variable: String,
}
impl CommandActor {
fn primitive_to_string(obj: Primitive) -> String {
match obj {
Primitive::Empty => String::new(),
Primitive::String(s) => s,
Primitive::F32(f) => f.to_string(),
Primitive::F64(f) => f.to_string(),
Primitive::I32(i) => i.to_string(),
Primitive::I64(i) => i.to_string(),
Primitive::U32(u) => u.to_string(),
Primitive::U64(u) => u.to_string(),
Primitive::Bool(b) => b.to_string().to_uppercase(),
Primitive::Json(j) => j,
}
}
}
impl Act for CommandActor {
type Param = Primitive;
type Config = CommandAction;
type Return = String;
fn build(config: &CommandAction, parameter: Primitive) -> Result<Self, ActError> {
Ok(
Self {
shell: "bash".to_owned(),
run: config.run.clone(),
variable: Self::primitive_to_string(parameter),
}
)
}
fn run(self) -> Self::Return {
let output = Command::new(&self.shell)
.args(["-c", &self.run])
.env(VALUE_ENV_VAR, &self.variable)
.output()
.expect(&format!("Cannot run `{}`", &self.run));
if !output.stderr.is_empty() {
log::error!("Error running `{}`: {}", &self.run, String::from_utf8(output.stderr).unwrap_or_else(|_| "<non utf-8 stderr output>".to_owned()))
}
let result = String::from_utf8(output.stdout).expect(&format!("Cannot parse stdout from `{}` as UTF-8", self.run));
log::debug!("CommandActor ran `{}` (${}=\"{}\") -> `{}`", &self.run, VALUE_ENV_VAR, &self.variable, &result);
result
}
}

View file

@ -0,0 +1,26 @@
use std::sync::mpsc::Sender;
use usdpl_back::core::serdes::Primitive;
use crate::config::{AboutConfig, ElementConfig};
pub enum QueueAction {
GetAbout {
respond_to: Sender<AboutConfig>,
},
DoUpdate {
index: usize,
value: Primitive,
},
DoReload {
respond_to: Sender<Vec<ElementConfig>>
},
SetCallback {
index: usize,
respond_to: Sender<Primitive>,
}
}
pub struct QueueItem {
pub action: QueueAction,
}

View file

@ -0,0 +1,121 @@
use std::thread;
use std::sync::mpsc::{self, Receiver, Sender};
use crate::config::{BaseConfig, ElementConfig};
use super::{QueueItem, QueueAction, Act};
use super::{ResultRouter, RouterCommand};
pub struct RuntimeExecutor {
config_data: BaseConfig,
tasks_receiver: Receiver<QueueItem>
}
impl RuntimeExecutor {
pub fn new(conf: BaseConfig) -> (Self, Sender<QueueItem>) {
let (tx, rx) = mpsc::channel();
(Self {
config_data: conf,
tasks_receiver: rx,
}, tx)
}
pub fn spawn(self) -> thread::JoinHandle<()> {
thread::spawn(move || self.run_loop())
}
fn run_loop(self) {
let (mut state, tasks_receiver) = self.split();
state.populate_router();
for item in tasks_receiver.iter() {
state.handle_item(item);
}
}
fn split(self) -> (ExecutorState, Receiver<QueueItem>) {
(
ExecutorState {
result_handler: ExecutorState::build_router(self.config_data.items().len()),
config_data: self.config_data,
},
self.tasks_receiver
)
}
}
struct ExecutorState {
config_data: BaseConfig,
result_handler: Sender<RouterCommand>,
}
impl ExecutorState {
fn handle_item(&mut self, item: QueueItem) {
match item.action {
QueueAction::GetAbout { respond_to } => {
respond_to.send(self.config_data.get_about().clone()).unwrap_or(());
},
QueueAction::DoUpdate { index, value } => {
if let Some(item) = self.config_data.get_item(index) {
match super::Actor::build(item, (index, value)) {
Ok(act) => {
let respond_to = self.result_handler.clone();
thread::spawn(move || {
let result = act.run();
match respond_to.send(RouterCommand::HandleResult{index, result}) {
Ok(_) => {},
Err(_) => log::warn!("Failed to send DoUpdate response for item #{}", index),
}
});
},
Err(e) => log::error!("Failed to build DoUpdate actor for item #{}: {}", index, e)
}
} else {
log::warn!("Received DoUpdate on non-existent item #{} with value `{}`", index, super::primitive_utils::debug(&value))
}
},
QueueAction::DoReload { respond_to } => {
self.config_data = BaseConfig::load(crate::consts::FILEPATH);
self.populate_router();
respond_to.send(self.config_data.items().clone()).unwrap_or(());
},
QueueAction::SetCallback { index, respond_to } => {
if let Some(elem) = self.config_data.get_item(index) {
let display_of = match elem {
ElementConfig::ResultDisplay(c) => c.result_of,
_ => index,
};
if let Err(_) = self.result_handler.send(
RouterCommand::AddSender {
index: display_of,
sender: respond_to,
}) {
log::warn!("Failed to send to ResultRouter, rebuilding router");
self.result_handler = ExecutorState::build_router(self.config_data.items().len());
}
}
}
}
}
fn build_router(items_len: usize) -> Sender<RouterCommand> {
let router = ResultRouter::build(&(), items_len).unwrap();
let result = router.run();
result
}
fn populate_router(&mut self) {
if let Err(_) = self.result_handler.send(RouterCommand::Clear{}) {
return;
}
// start reading displays with periodic actions
for (index, item) in self.config_data.items().iter().enumerate() {
match item {
ElementConfig::ReadingDisplay(r) => {
if let Ok(actor) = super::PeriodicActor::build(r, (index, self.result_handler.clone())) {
actor.run();
}
},
_ => {}
}
}
}
}

View file

@ -0,0 +1,14 @@
mod actor;
mod command_actor;
mod communication;
mod executor;
mod periodic_actor;
mod primitive_utils;
mod result_router;
pub use actor::{Actor, Act, ActError, ActorType};
pub use command_actor::CommandActor;
pub use communication::{QueueItem, QueueAction};
pub use executor::RuntimeExecutor;
pub use periodic_actor::PeriodicActor;
pub use result_router::{ResultRouter, RouterCommand};

View file

@ -0,0 +1,57 @@
use std::sync::mpsc::Sender;
use std::time::Duration;
use usdpl_back::core::serdes::Primitive;
use crate::config::ReadingConfig;
use super::{Act, ActError, ActorType, RouterCommand};
pub struct PeriodicActor {
config: ReadingConfig,
result_handler: Sender<RouterCommand>,
index: usize,
}
impl Act for PeriodicActor {
type Param = (usize, Sender<RouterCommand>);
type Config = ReadingConfig;
type Return = ();
fn build(config: &Self::Config, parameter: Self::Param) -> Result<Self, ActError> {
ActorType::build(&config.on_period, Primitive::Empty)?;
Ok(
Self {
config: config.clone(),
result_handler: parameter.1,
index: parameter.0,
}
)
}
fn run(self) -> Self::Return {
std::thread::spawn(move || {
let sleep_duration = Duration::from_millis(self.config.period_ms);
loop {
let actor = match ActorType::build(&self.config.on_period, Primitive::Empty) {
Ok(x) => x,
Err(e) => {
log::error!("PeriodicActor failed to build for item #{}: {}", self.index, e);
break;
}
};
let result = actor.run();
match self.result_handler.send(RouterCommand::HandleResult {
index: self.index, result
}) {
Ok(_) => {},
Err(_e) => {
log::warn!("PeriodicActor failed to handle result for item #{}", self.index);
break;
}
}
std::thread::sleep(sleep_duration);
}
log::info!("PeriodicActor completed for #{}", self.index);
});
}
}

View file

@ -0,0 +1,84 @@
use usdpl_back::core::serdes::Primitive;
//use super::ActError;
/*macro_rules! map_primitive_number_impl {
($type:ty, $type_name:literal, $fn_name:ident) => {
pub fn $fn_name (param: Primitive) -> Result<$type, ActError> {
match param {
Primitive::I64(a) => Ok(a as $type),
Primitive::I32(a) => Ok(a as $type),
Primitive::U64(a) => Ok(a as $type),
Primitive::U32(a) => Ok(a as $type),
Primitive::F64(a) => Ok(a as $type),
Primitive::F32(a) => Ok(a as $type),
_ => Err(format!("Parameter must be {} type", $type_name))
}
}
}
}*/
/*macro_rules! map_primitive_impl {
($type:ty, $primitive:ident, $type_name:literal, $fn_name:ident) => {
pub fn $fn_name (param: Primitive) -> Result<$type, ActError> {
match param {
Primitive::$primitive(a) => Ok(a),
_ => Err(format!("Parameter must be {} type", $type_name))
}
}
}
}*/
//map_primitive_impl!{bool, Bool, "boolean", try_primitive_bool}
//map_primitive_impl!{String, String, "string", try_primitive_string}
//map_primitive_number_impl!{usize, "uinteger", try_primitive_usize}
#[inline]
pub fn debug(primitive: &Primitive) -> String {
match primitive {
Primitive::Empty => "Primitive::Empty".to_owned(),
Primitive::String(x) => format!("Primitive::String(`{}`)", x),
Primitive::F32(x) => format!("Primitive::F32(`{}`)", x),
Primitive::F64(x) => format!("Primitive::F64(`{}`)", x),
Primitive::U32(x) => format!("Primitive::U32(`{}`)", x),
Primitive::U64(x) => format!("Primitive::U64(`{}`)", x),
Primitive::I32(x) => format!("Primitive::I32(`{}`)", x),
Primitive::I64(x) => format!("Primitive::I64(`{}`)", x),
Primitive::Bool(x) => format!("Primitive::Bool(`{}`)", x),
Primitive::Json(x) => format!("Primitive::Json(`{}`)", x),
}
}
/*#[inline]
pub fn display(primitive: Primitive) -> String {
match primitive {
Primitive::Empty => "".to_owned(),
Primitive::String(x) => x,
Primitive::F32(x) => x.to_string(),
Primitive::F64(x) => x.to_string(),
Primitive::U32(x) => x.to_string(),
Primitive::U64(x) => x.to_string(),
Primitive::I32(x) => x.to_string(),
Primitive::I64(x) => x.to_string(),
Primitive::Bool(x) => x.to_string(),
Primitive::Json(x) => x,
}
}*/
#[inline]
pub fn clone(primitive: &Primitive) -> Primitive {
match primitive {
Primitive::Empty => Primitive::Empty,
Primitive::String(x) => Primitive::String(x.clone()),
Primitive::F32(x) => Primitive::F32(*x),
Primitive::F64(x) => Primitive::F64(*x),
Primitive::U32(x) => Primitive::U32(*x),
Primitive::U64(x) => Primitive::U64(*x),
Primitive::I32(x) => Primitive::I32(*x),
Primitive::I64(x) => Primitive::I64(*x),
Primitive::Bool(x) => Primitive::Bool(*x),
Primitive::Json(x) => Primitive::Json(x.clone()),
}
}

View file

@ -0,0 +1,133 @@
use std::sync::mpsc::{self, Receiver, Sender};
use usdpl_back::core::serdes::Primitive;
//use crate::config::ElementConfig;
use super::{Act, ActError};
const MAX_HANDLERS_PER_ITEM: usize = 8;
pub enum RouterCommand {
AddSender {
index: usize,
sender: Sender<Primitive>,
},
HandleResult {
index: usize,
result: Primitive,
},
Clear{}
}
pub struct ResultRouter {
comm: Receiver<RouterCommand>,
senders: Vec<[Option<Sender<Primitive>>; MAX_HANDLERS_PER_ITEM]>,
comm_tx: Option<Sender<RouterCommand>>,
cache: Vec<Option<Primitive>>,
}
impl ResultRouter {
fn all_senders_none(senders: &[Option<Sender<Primitive>>]) -> bool {
let mut all_none = true;
for s in senders.iter() {
all_none &= s.is_none();
}
all_none
}
}
impl Act for ResultRouter {
type Param = usize;
type Config = ();
type Return = Sender<RouterCommand>;
fn build(_config: &Self::Config, parameter: Self::Param) -> Result<Self, ActError> {
let (tx, rx) = mpsc::channel();
let mut cache_vec = Vec::with_capacity(parameter);
for _ in 0..parameter {
cache_vec.push(None);
}
Ok(Self {
comm: rx,
senders: vec![[(); MAX_HANDLERS_PER_ITEM].map(|_| None); parameter],
comm_tx: Some(tx),
cache: cache_vec,
})
}
fn run(mut self) -> Self::Return {
let result = self.comm_tx.take().unwrap();
std::thread::spawn(move || {
log::debug!("ResultRouter starting");
for command in self.comm.iter() {
match command {
RouterCommand::AddSender { index, sender } => {
log::debug!("Handling AddSender for item #{}", index);
if let Some(senders) = self.senders.get_mut(index) {
// send cached value, if available
if self.cache[index].is_some() {
log::debug!("Routing cached result for item #{}", index);
let result = self.cache[index].take().unwrap();
match sender.send(result) {
Ok(_) => {},
Err(e) => {
self.cache[index] = Some(e.0);
log::debug!("ResultRouter ignoring AddSender since sending cached value failed");
continue;
},
}
}
// save sender for future results
let mut was_set = false;
'inner_loop: for sender_opt in senders {
if sender_opt.is_none() {
*sender_opt = Some(sender);
was_set = true;
break 'inner_loop;
}
}
if !was_set {
log::warn!("ResultRouter could not add another sender for index {}", index);
}
} else {
log::warn!("ResultRouter got AddSender command for invalid index {} (max: {})", index, self.senders.len());
}
}
RouterCommand::HandleResult {index, result} => {
log::debug!("Handling HandleResult for item #{}", index);
if let Some(senders) = self.senders.get_mut(index) {
if Self::all_senders_none(senders) {
self.cache[index] = Some(result);
log::debug!("Cached result for item #{}", index);
} else {
for (i, sender_opt) in senders.iter_mut().enumerate() {
if let Some(sender) = sender_opt {
match sender.send(super::primitive_utils::clone(&result)) {
Ok(_) => {},
Err(_) => {
log::debug!("Removing sender {} because it seems closed", i);
*sender_opt = None;
}
}
}
}
log::debug!("Routed result for item #{}", index);
}
} else {
log::warn!("ResultRouter got AddSender command for invalid index {} (max: {})", index, self.senders.len());
}
},
RouterCommand::Clear {} => {
log::debug!("Handling Clear");
for i in 0..self.senders.len() {
self.senders[i] = [(); MAX_HANDLERS_PER_ITEM].map(|_| None);
self.cache[i] = None;
}
}
}
}
log::warn!("ResultRouter completed");
});
result
}
}

57
kaylon.json Normal file
View file

@ -0,0 +1,57 @@
{
"api-version": "v0.0.0",
"items": [
{
"element": "button",
"title": "Test Button",
"on_click": {
"action": "command",
"run": "echo 'hello button'"
}
},
{
"element": "toggle",
"title": "Test Toggle",
"description": "Toggle description",
"on_toggle": {
"action": "command",
"run": "echo 'hello toggle ${KAYLON_VALUE}'"
}
},
{
"element": "slider",
"title": "Test Slider",
"min": 0,
"max": 3,
"notches": null,
"on_set": {
"action": "command",
"run": "echo 'hello slider'"
}
},
{
"element": "reading",
"title": "Test Reading",
"period_ms": 10000,
"on_period": {
"action": "command",
"run": "echo 'hello reading'"
}
},
{
"element": "result-display",
"title": "Test Result",
"result_of": 1
}
],
"about": {
"name": "Test name",
"version": "v0.42.0",
"description": "Test description",
"url": "https://github.com/NGnius/kaylon",
"authors": [
"NGnius <ngniusness@gmail.com>"
],
"license": "MIT"
}
}

View file

@ -11,6 +11,6 @@ class Plugin:
# Asyncio-compatible long-running code, executed in a task when the plugin is loaded # Asyncio-compatible long-running code, executed in a task when the plugin is loaded
async def _main(self): async def _main(self):
# startup # startup
self.backend_proc = subprocess.Popen([PARENT_DIR + "/bin/backend"]) #self.backend_proc = subprocess.Popen([PARENT_DIR + "/bin/backend"])
while True: while True:
asyncio.sleep(1) await asyncio.sleep(1)

View file

@ -38,7 +38,7 @@
"typescript": "^4.6.4" "typescript": "^4.6.4"
}, },
"dependencies": { "dependencies": {
"decky-frontend-lib": "^1.0.1", "decky-frontend-lib": "*",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"usdpl-front": "file:./src/usdpl_front" "usdpl-front": "file:./src/usdpl_front"
}, },

View file

@ -1,6 +0,0 @@
#!/bin/bash
cargo build --release
mkdir ../bin
# TODO replace "backend" \/ with binary name
cp ./target/release/backend ../bin/backend

View file

@ -1,11 +0,0 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone)]
pub struct AboutConfig {
pub name: String,
pub version: String,
pub description: String,
pub url: Option<String>,
pub author: Option<String>,
pub license: Option<String>,
}

View file

@ -1,13 +0,0 @@
use serde::{Serialize, Deserialize};
use super::{ElementConfig, AboutConfig};
#[derive(Serialize, Deserialize, Clone)]
#[serde(tag = "api-version")]
pub enum BaseConfig {
#[serde(rename = "v0.0.0")]
V0 {
items: Vec<ElementConfig>,
about: AboutConfig,
},
}

View file

@ -1,27 +0,0 @@
mod config;
use simplelog::{WriteLogger, LevelFilter};
use usdpl_back::Instance;
use usdpl_back::core::serdes::Primitive;
const PORT: u16 = 54321; // TODO replace with something unique
const PACKAGE_NAME: &'static str = env!("CARGO_PKG_NAME");
const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION");
fn main() -> Result<(), ()> {
let log_filepath = format!("/tmp/{}.log", PACKAGE_NAME);
WriteLogger::init(
#[cfg(debug_assertions)]{LevelFilter::Debug},
#[cfg(not(debug_assertions))]{LevelFilter::Info},
Default::default(),
std::fs::File::create(&log_filepath).unwrap()
).unwrap();
log::info!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION);
println!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION);
Instance::new(PORT)
.register("hello", |_: Vec<Primitive>| vec![format!("Hello {}", PACKAGE_NAME).into()])
.run_blocking()
}

View file

@ -1,4 +1,4 @@
import {init_usdpl, target, init_embedded, call_backend} from "usdpl-front"; import {init_usdpl, target_usdpl, init_embedded, call_backend} from "usdpl-front";
const USDPL_PORT: number = 25717; const USDPL_PORT: number = 25717;
@ -20,7 +20,7 @@ export async function initBackend() {
// init usdpl // init usdpl
await init_embedded(); await init_embedded();
init_usdpl(USDPL_PORT); init_usdpl(USDPL_PORT);
console.log("USDPL started for framework: " + target()); console.log("USDPL started for framework: " + target_usdpl());
//setReady(true); //setReady(true);
} }
@ -29,7 +29,7 @@ export type CAbout = {
version: string; version: string;
description: string; description: string;
url: string | null; url: string | null;
author: string | null; authors: string[];
license: string | null; license: string | null;
} }
@ -58,18 +58,24 @@ export type CReading = {
period_ms: number; period_ms: number;
} }
export type CElement = CButton | CToggle | CSlider | CReading; export type CResultDisplay = {
element: string; // "result-display"
title: string;
result_of: number;
}
export type CElement = CButton | CToggle | CSlider | CReading | CResultDisplay;
export async function getElements(): Promise<CElement[]> { export async function getElements(): Promise<CElement[]> {
return await call_backend("get_items", []); return (await call_backend("get_items", []))[0];
} }
export async function onUpdate(index: number, value: any): Promise<any> { export async function onUpdate(index: number, value: any): Promise<any> {
return (await call_backend("on_update", [index, value]))[0]; return (await call_backend("on_update", [index, value]))[0];
} }
export async function getReading(index: number): Promise<string | null> { export async function getDisplay(index: number): Promise<string | null> {
return (await call_backend("get_reading", [index]))[0]; return (await call_backend("get_display", [index]))[0];
} }
export async function getAbout(): Promise<CAbout> { export async function getAbout(): Promise<CAbout> {
@ -77,5 +83,5 @@ export async function getAbout(): Promise<CAbout> {
} }
export async function reload(): Promise<CElement[]> { export async function reload(): Promise<CElement[]> {
return await call_backend("reload", []); return (await call_backend("reload", []))[0];
} }

View file

@ -1,7 +1,7 @@
import { import {
ButtonItem, ButtonItem,
definePlugin, definePlugin,
DialogButton, //DialogButton,
//Menu, //Menu,
//MenuItem, //MenuItem,
PanelSection, PanelSection,
@ -19,72 +19,93 @@ import {
import { VFC, useState } from "react"; import { VFC, useState } from "react";
import { GiWashingMachine } from "react-icons/gi"; import { GiWashingMachine } from "react-icons/gi";
import { call_backend } from "usdpl-front"; import { get_value, set_value } from "usdpl-front";
import * as backend from "./backend"; import * as backend from "./backend";
// interface AddMethodArgs {
// left: number;
// right: number;
// }
const FieldWithSeparator = joinClassNames(gamepadDialogClasses.Field, gamepadDialogClasses.WithBottomSeparatorStandard); const FieldWithSeparator = joinClassNames(gamepadDialogClasses.Field, gamepadDialogClasses.WithBottomSeparatorStandard);
const DISPLAY_KEY = "display";
const VALUE_KEY = "value";
let items: backend.CElement[] = []; let items: backend.CElement[] = [];
let about: backend.CAbout | null = null;
let update = () => {};
function displayCallback(index: number) {
return (newVal: any) => {
set_value(DISPLAY_KEY + index.toString(), newVal);
backend.resolve(backend.getDisplay(index), displayCallback(index));
console.log("Got display for " + index.toString(), newVal);
update();
}
}
// init USDPL WASM frontend
// this is required to interface with the backend
(async () => {
await backend.initBackend();
let about_promise = backend.getAbout();
let elements_promise = backend.getElements();
about = await about_promise;
console.log("KAYLON: got about", about);
let result = await elements_promise;
console.log("KAYLON: got elements", result);
if (result != null) {
items = await backend.getElements();
for (let i = 0; i < items.length; i++) {
console.log("KAYLON: req display for item #" + i.toString());
backend.resolve(backend.getDisplay(i), displayCallback(i));
}
} else {
console.warn("KAYLON: backend connection failed");
}
})();
const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => {
// const [result, setResult] = useState<number | undefined>();
// const onClick = async () => {
// const result = await serverAPI.callPluginMethod<AddMethodArgs, number>(
// "add",
// {
// left: 2,
// right: 2,
// }
// );
// if (result.success) {
// setResult(result.result);
// }
// };
const [triggerInternal, updateInternal] = useState<boolean>(false); const [triggerInternal, updateInternal] = useState<boolean>(false);
function update() { update = () => {
updateInternal(!triggerInternal); updateInternal(!triggerInternal);
} }
function updateIdc(_: any) { function updateIdc(_: any) {
update(); update();
} }
// call hello callback on backend
(async () => {
let response = await call_backend("hello", []);
console.log("Backend says:", response);
})();
return ( return (
<PanelSection title="Panel Section"> <PanelSection>
{items.map( {items.map(
(elem, i) => { (elem, i) => {
return <PanelSectionRow>{buildHtmlElement(elem, i, updateIdc)}</PanelSectionRow> return <PanelSectionRow>{buildHtmlElement(elem, i, updateIdc)}</PanelSectionRow>
}) })
} }
{ about != null && buildAbout() }
<PanelSectionRow>
<ButtonItem
layout="below"
onClick={(_: MouseEvent) => {
backend.resolve(backend.reload(),
(reload_items: backend.CElement[]) => {
items = reload_items;
console.log("KAYLON: got elements", reload_items);
update();
});
backend.resolve(backend.getAbout(),
(new_about: backend.CAbout) => {
about = new_about;
console.log("KAYLON: got about", about);
update();
});
}}>
Reload
</ButtonItem>
</PanelSectionRow>
</PanelSection> </PanelSection>
); );
}; };
const DeckyPluginRouterTest: VFC = () => {
return (
<div style={{ marginTop: "50px", color: "white" }}>
Hello World!
<DialogButton onClick={() => {}}>
Go to Store
</DialogButton>
</div>
);
};
function buildHtmlElement(element: backend.CElement, index: number, updateIdc: any) { function buildHtmlElement(element: backend.CElement, index: number, updateIdc: any) {
switch (element.element) { switch (element.element) {
case "button": case "button":
@ -95,8 +116,11 @@ function buildHtmlElement(element: backend.CElement, index: number, updateIdc: a
return buildToggle(element as backend.CToggle, index, updateIdc); return buildToggle(element as backend.CToggle, index, updateIdc);
case "reading": case "reading":
return buildReading(element as backend.CReading, index, updateIdc); return buildReading(element as backend.CReading, index, updateIdc);
case "result-display":
return buildResultDisplay(element as backend.CResultDisplay, index, updateIdc);
} }
return "Unsupported"; console.error("KAYLON: Unsupported element", element);
return <div>Unsupported</div>;
} }
function buildButton(element: backend.CButton, index: number, updateIdc: any) { function buildButton(element: backend.CButton, index: number, updateIdc: any) {
@ -110,62 +134,175 @@ function buildButton(element: backend.CButton, index: number, updateIdc: any) {
} }
function buildSlider(element: backend.CSlider, index: number, updateIdc: any) { function buildSlider(element: backend.CSlider, index: number, updateIdc: any) {
const KEY = VALUE_KEY + index.toString();
if (get_value(KEY) == null) {
set_value(KEY, element.min);
}
return ( return (
<SliderField <SliderField
label={element.title} label={element.title}
value={element.min} value={get_value(KEY)}
max={element.max} max={element.max}
min={element.min} min={element.min}
showValue={true} showValue={true}
onChange={(value: number) => { onChange={(value: number) => {
backend.resolve(backend.onUpdate(index, value), updateIdc) backend.resolve(backend.onUpdate(index, value), updateIdc);
set_value(KEY, value);
}} }}
/> />
); );
} }
function buildToggle(element: backend.CToggle, index: number, updateIdc: any) { function buildToggle(element: backend.CToggle, index: number, updateIdc: any) {
const KEY = VALUE_KEY + index.toString();
if (get_value(KEY) == null) {
set_value(KEY, false);
}
return ( return (
<ToggleField <ToggleField
checked={false} checked={get_value(KEY)}
label={element.title} label={element.title}
description={element.description!} description={element.description!}
onChange={(value: boolean) => { onChange={(value: boolean) => {
backend.resolve(backend.onUpdate(index, value), updateIdc) backend.resolve(backend.onUpdate(index, value), updateIdc);
set_value(KEY, value);
}} }}
/> />
); );
} }
function buildReading(element: backend.CReading, _index: number, _updateIdc: any) { function buildReading(element: backend.CReading, index: number, _updateIdc: any) {
return ( return (
<div className={FieldWithSeparator}> <div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}> <div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>{element.title}</div> <div className={gamepadDialogClasses.FieldLabel}>{element.title}</div>
<div className={gamepadDialogClasses.FieldChildren}>{"idk"}</div> <div className={gamepadDialogClasses.FieldChildren}>{get_value(DISPLAY_KEY + index.toString())}</div>
</div> </div>
</div> </div>
); );
} }
export default definePlugin((serverApi: ServerAPI) => { function buildResultDisplay(element: backend.CResultDisplay, index: number, _updateIdc: any) {
serverApi.routerHook.addRoute("/decky-plugin-test", DeckyPluginRouterTest, { return (
exact: true, <div className={FieldWithSeparator}>
}); <div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>{element.title}</div>
// init USDPL WASM frontend <div className={gamepadDialogClasses.FieldChildren}>{get_value(DISPLAY_KEY + index.toString())}</div>
// this is required to interface with the backend </div>
(async () => { </div>
await backend.initBackend(); );
items = await backend.getElements(); }
})();
function buildAbout() {
if (about == null) {
return [];
} else {
let elements = [
<div className={staticClasses.PanelSectionTitle}>
About
</div>,
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>Name</div>
<div className={gamepadDialogClasses.FieldChildren}>{about.name}</div>
</div>
</div>
</PanelSectionRow>,
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>Version</div>
<div className={gamepadDialogClasses.FieldChildren}>{about.version}</div>
</div>
</div>
</PanelSectionRow>,
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>Description</div>
<div className={gamepadDialogClasses.FieldDescription}>{about.description}</div>
</div>
</div>
</PanelSectionRow>
];
if (about.url != null) {
elements.push(
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>URL</div>
<div className={gamepadDialogClasses.FieldDescription}>{about.url}</div>
</div>
</div>
</PanelSectionRow>
);
}
if (about.authors.length > 1) {
let authors = about.authors.map((elem, i) => {
if (i == about!.authors.length - 1) {
return <p>{elem}</p>;
} else {
return <span>{elem}</span>;
}
});
elements.push(
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>Authors</div>
<div className={gamepadDialogClasses.FieldDescription}>{authors}</div>
</div>
</div>
</PanelSectionRow>
);
} else if (about.authors.length == 1) {
elements.push(
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>Author</div>
<div className={gamepadDialogClasses.FieldDescription}>{about.authors[0]}</div>
</div>
</div>
</PanelSectionRow>
);
} else {
elements.push(
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>Author</div>
<div className={gamepadDialogClasses.FieldDescription}>NGnius</div>
</div>
</div>
</PanelSectionRow>
);
}
if (about.license != null) {
elements.push(
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>License</div>
<div className={gamepadDialogClasses.FieldChildren}>{about.license}</div>
</div>
</div>
</PanelSectionRow>
);
}
return elements;
}
}
export default definePlugin((serverApi: ServerAPI) => {
return { return {
title: <div className={staticClasses.Title}>Example Plugin</div>, title: <div className={staticClasses.Title}>{about == null? "Kaylon": about.name}</div>,
content: <Content serverAPI={serverApi} />, content: <Content serverAPI={serverApi} />,
icon: <GiWashingMachine />, icon: <GiWashingMachine />,
onDismount() { onDismount() {
serverApi.routerHook.removeRoute("/decky-plugin-test"); //serverApi.routerHook.removeRoute("/decky-plugin-test");
}, },
}; };
}); });

View file

@ -0,0 +1,9 @@
[![Crates.io](https://img.shields.io/crates/v/usdpl-front?style=flat-square)](https://crates.io/crates/usdpl-front)
# usdpl-front-front
Front-end library to be called from Javascript.
Targets WASM.
In true Javascript tradition, this part of the library does not support error handling.

View file

@ -4,7 +4,7 @@
"NGnius (Graham) <ngniusness@gmail.com>" "NGnius (Graham) <ngniusness@gmail.com>"
], ],
"description": "Universal Steam Deck Plugin Library front-end designed for WASM", "description": "Universal Steam Deck Plugin Library front-end designed for WASM",
"version": "0.6.0", "version": "0.6.2",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -9,7 +9,25 @@ export function init_usdpl(port: number): void;
* Get the targeted plugin framework, or "any" if unknown * Get the targeted plugin framework, or "any" if unknown
* @returns {string} * @returns {string}
*/ */
export function target(): string; export function target_usdpl(): string;
/**
* Get the UDSPL front-end version
* @returns {string}
*/
export function version_usdpl(): string;
/**
* Get the targeted plugin framework, or "any" if unknown
* @param {string} key
* @param {any} value
* @returns {any}
*/
export function set_value(key: string, value: any): any;
/**
* Get the targeted plugin framework, or "any" if unknown
* @param {string} key
* @returns {any}
*/
export function get_value(key: string): any;
/** /**
* Call a function on the back-end. * Call a function on the back-end.
* Returns null (None) if this fails for any reason. * Returns null (None) if this fails for any reason.
@ -24,7 +42,10 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
export interface InitOutput { export interface InitOutput {
readonly memory: WebAssembly.Memory; readonly memory: WebAssembly.Memory;
readonly init_usdpl: (a: number) => void; readonly init_usdpl: (a: number) => void;
readonly target: (a: number) => void; readonly target_usdpl: (a: number) => void;
readonly version_usdpl: (a: number) => void;
readonly set_value: (a: number, b: number, c: number) => number;
readonly get_value: (a: number, b: number) => number;
readonly call_backend: (a: number, b: number, c: number, d: number) => number; readonly call_backend: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_export_0: (a: number) => number; readonly __wbindgen_export_0: (a: number) => number;
readonly __wbindgen_export_1: (a: number, b: number, c: number) => number; readonly __wbindgen_export_1: (a: number, b: number, c: number) => number;

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -2,7 +2,10 @@
/* eslint-disable */ /* eslint-disable */
export const memory: WebAssembly.Memory; export const memory: WebAssembly.Memory;
export function init_usdpl(a: number): void; export function init_usdpl(a: number): void;
export function target(a: number): void; export function target_usdpl(a: number): void;
export function version_usdpl(a: number): void;
export function set_value(a: number, b: number, c: number): number;
export function get_value(a: number, b: number): number;
export function call_backend(a: number, b: number, c: number, d: number): number; export function call_backend(a: number, b: number, c: number, d: number): number;
export function __wbindgen_export_0(a: number): number; export function __wbindgen_export_0(a: number): number;
export function __wbindgen_export_1(a: number, b: number, c: number): number; export function __wbindgen_export_1(a: number, b: number, c: number): number;