ppc login works in mme main
This commit is contained in:
1234
Cargo.lock
generated
1234
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,13 +8,13 @@ members = [
|
||||
"parts/mize.ui/dioxus-components",
|
||||
]
|
||||
exclude = [
|
||||
"parts/mize.ui",
|
||||
"parts/mize.ui/upstream",
|
||||
"parts/mize.ui/fluxer",
|
||||
"parts/mos",
|
||||
# "parts/mme",
|
||||
]
|
||||
default-members = [
|
||||
"apps/obsidian-plugin",
|
||||
"apps/os",
|
||||
"parts/*",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ pub struct CliPart {
|
||||
}
|
||||
|
||||
impl MizePart for CliPart {
|
||||
fn id(&self) -> MizeId {
|
||||
self.id.clone()
|
||||
fn id(&self) -> MizeResult<MizeId> {
|
||||
Ok(self.id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ impl CliPart {
|
||||
pub fn get(name: &str) -> MizeResult<CliPart> {
|
||||
let mut mize = get_global_mize()?;
|
||||
let part = mize.get_part(name)?;
|
||||
Ok(CliPart { id: part.id() })
|
||||
Ok(CliPart { id: part.id()? })
|
||||
}
|
||||
pub fn run(&self, cmdline: Vec<String>) -> MizeResult<()> {
|
||||
let cmdline = cmdline.into_item_data();
|
||||
|
||||
@@ -57,7 +57,7 @@ fn tried_main() -> MizeResult<()> {
|
||||
let cli_matches = cli_matches();
|
||||
|
||||
// logging
|
||||
init_logger(&cli_matches);
|
||||
init_logger(Some(&cli_matches));
|
||||
|
||||
// init mize
|
||||
let mut mize = Mize::with_config(config_from_cli_args(&cli_matches)?)?;
|
||||
|
||||
@@ -59,13 +59,13 @@ impl ConfigOptId {
|
||||
opt.fun = Some(fun);
|
||||
self.clone()
|
||||
}
|
||||
pub fn method<F: Fn(ItemData) -> MizeResult<ItemData> + Send + Sync + 'static>(self, name: &str, fun: F) -> ConfigOptId {
|
||||
pub fn method<F: Fn(ItemData) -> MizeResult<ItemData> + Send + Sync + 'static>(self, name: &str, signature: &str, fun: F) -> ConfigOptId {
|
||||
let mize = get_global_mize().unwrap();
|
||||
let mut cur_part = self.id.clone();
|
||||
let id = cur_part.join(name).unwrap();
|
||||
// in generate mode: record method metadata, skip normal registration
|
||||
if mize.is_generate_mode().unwrap_or(false) {
|
||||
mize.record_method(name, id.clone(), "method").ok();
|
||||
mize.record_method(name, id.clone(), "method", signature).ok();
|
||||
return self.clone();
|
||||
}
|
||||
let mut opts = mize.config_opts.lock().unwrap();
|
||||
@@ -118,13 +118,13 @@ pub fn opt(name: &str) -> crate::config::ConfigOptId {
|
||||
ConfigOptId { id }
|
||||
}
|
||||
|
||||
pub fn method<F: Fn(ItemData) -> MizeResult<ItemData> + Send + Sync + 'static>(name: &str, fun: F) -> ConfigOptId {
|
||||
pub fn method<F: Fn(ItemData) -> MizeResult<ItemData> + Send + Sync + 'static>(name: &str, signature: &str, fun: F) -> ConfigOptId {
|
||||
let mize = get_global_mize().unwrap();
|
||||
let mut cur_part = mize.cur_eval_part.lock().unwrap().clone().unwrap();
|
||||
let id = cur_part.join(name).unwrap();
|
||||
// in generate mode: record method metadata, skip normal registration
|
||||
if mize.is_generate_mode().unwrap_or(false) {
|
||||
mize.record_method(name, id.clone(), "method").ok();
|
||||
mize.record_method(name, id.clone(), "method", signature).ok();
|
||||
return ConfigOptId { id };
|
||||
}
|
||||
let mut opts = mize.config_opts.lock().unwrap();
|
||||
@@ -146,7 +146,7 @@ pub fn effect<F: Fn(ItemData) -> MizeResult<ItemData> + Send + Sync + 'static>(n
|
||||
let id = cur_part.join(name).unwrap();
|
||||
// in generate mode: record effect metadata, skip normal registration
|
||||
if mize.is_generate_mode().unwrap_or(false) {
|
||||
mize.record_method(name, id.clone(), "effect").ok();
|
||||
mize.record_method(name, id.clone(), "effect", "").ok();
|
||||
return ConfigOptId { id };
|
||||
}
|
||||
let mut opts = mize.config_opts.lock().unwrap();
|
||||
@@ -168,7 +168,7 @@ pub fn thread<F: Fn(ItemData) -> MizeResult<ItemData> + Send + Sync + 'static>(n
|
||||
let id = cur_part.join(name).unwrap();
|
||||
// in generate mode: record thread metadata, skip normal registration
|
||||
if mize.is_generate_mode().unwrap_or(false) {
|
||||
mize.record_method(name, id.clone(), "thread").ok();
|
||||
mize.record_method(name, id.clone(), "thread", "").ok();
|
||||
return ConfigOptId { id };
|
||||
}
|
||||
let mut opts = mize.config_opts.lock().unwrap();
|
||||
@@ -199,11 +199,15 @@ pub mod c_api {
|
||||
super::opt(&string)
|
||||
}
|
||||
}
|
||||
pub extern "C" fn method(name: *const c_char, c_fun: CFun) -> ConfigOptId {
|
||||
pub extern "C" fn method(name: *const c_char, signature: *const c_char, c_fun: CFun) -> ConfigOptId {
|
||||
unsafe {
|
||||
let c_str = CStr::from_ptr(name);
|
||||
let string = c_str.to_string_lossy();
|
||||
super::method(&string, move |args| {
|
||||
let name_str = CStr::from_ptr(name).to_string_lossy();
|
||||
let sig_str = if signature.is_null() {
|
||||
String::new()
|
||||
} else {
|
||||
CStr::from_ptr(signature).to_string_lossy().into_owned()
|
||||
};
|
||||
super::method(&name_str, &sig_str, move |args| {
|
||||
let raw_ptr = Box::into_raw(Box::new(args));
|
||||
let ret_ptr = c_fun(raw_ptr);
|
||||
let data = *Box::from_raw(ret_ptr);
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::fmt::Debug;
|
||||
use std::fmt::Display;
|
||||
use std::io;
|
||||
use std::string::FromUtf8Error;
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
use crate::prelude::*;
|
||||
|
||||
//use crate::proto::MizeMessage;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ use std::collections::HashMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::{thread, vec};
|
||||
use tracing::{debug, info, trace};
|
||||
|
||||
use crate::set_global_mize;
|
||||
use crate::config::{ConfigOpt, ConfigOptId, MizeFun};
|
||||
@@ -24,6 +23,7 @@ use crate::memstore::MemStore;
|
||||
use crate::mize_err;
|
||||
use crate::proto::MizeMessage;
|
||||
use crate::error::IntoMizeResult;
|
||||
use crate::prelude::*;
|
||||
|
||||
use self::connection::{ConnListener, Connection};
|
||||
use self::updater::handle_operation;
|
||||
@@ -47,7 +47,7 @@ mod tests;
|
||||
|
||||
static MSG_CHANNEL_SIZE: usize = 200;
|
||||
|
||||
static BUILD_TIME_CONFIG: &str = include_str!(std::env!("MIZE_BUILD_CONFIG"));
|
||||
static BUILD_TIME_CONFIG: &str = std::env!("MIZE_BUILD_CONFIG");
|
||||
|
||||
/// The Instance type is the heart of the mize system
|
||||
#[derive(Clone)]
|
||||
@@ -121,6 +121,35 @@ pub struct MethodMetadata {
|
||||
pub name: String,
|
||||
pub id: MizeId,
|
||||
pub kind: String, // "method", "effect", "thread"
|
||||
/// Raw signature string, e.g. `"String -> ()"` or `"Command, Func -> ()"`.
|
||||
pub signature: String,
|
||||
/// Parsed argument type names (left of `->`).
|
||||
pub arg_types: Vec<String>,
|
||||
/// Parsed return type name (right of `->`). Empty means unspecified / ItemData.
|
||||
pub return_type: String,
|
||||
}
|
||||
|
||||
/// Parse a method signature like `"String -> ()"` or `"Command, Func -> ()"`.
|
||||
/// Returns `(arg_types, return_type)` where `return_type` is empty if no `->` was given.
|
||||
pub fn parse_method_signature(signature: &str) -> (Vec<String>, String) {
|
||||
let signature = signature.trim();
|
||||
if signature.is_empty() {
|
||||
return (Vec::new(), String::new());
|
||||
}
|
||||
let (args_part, ret_part) = match signature.split_once("->") {
|
||||
Some((a, r)) => (a.trim(), r.trim().to_string()),
|
||||
None => (signature, String::new()),
|
||||
};
|
||||
let arg_types = if args_part.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
args_part
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
};
|
||||
(arg_types, ret_part)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -129,6 +158,10 @@ pub struct NativeMetadata {
|
||||
pub type_name: String,
|
||||
pub associated: Vec<String>,
|
||||
pub methods: Vec<String>,
|
||||
/// Signature strings parallel to `associated`.
|
||||
pub associated_sigs: Vec<String>,
|
||||
/// Signature strings parallel to `methods`.
|
||||
pub method_sigs: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct InstanceRef {
|
||||
@@ -146,8 +179,8 @@ impl PartRef {
|
||||
}
|
||||
|
||||
impl MizePart for PartRef {
|
||||
fn id(&self) -> MizeId {
|
||||
self.id.clone()
|
||||
fn id(&self) -> MizeResult<MizeId> {
|
||||
Ok(self.id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,11 +193,15 @@ pub struct InstanceAsync {
|
||||
}
|
||||
|
||||
pub trait MizePart {
|
||||
fn id(&self) -> MizeId;
|
||||
fn id(&self) -> MizeResult<MizeId>;
|
||||
}
|
||||
|
||||
impl Mize {
|
||||
pub fn empty() -> MizeResult<Mize> {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread().enable_all().build()?;
|
||||
let guard = runtime.enter();
|
||||
std::mem::forget(guard);
|
||||
|
||||
let id_pool = Arc::new(Mutex::new(VecStringPool::default()));
|
||||
let namespace_pool_raw = StringPool::default();
|
||||
let connections = Arc::new(Mutex::new(Vec::new()));
|
||||
@@ -206,9 +243,7 @@ impl Mize {
|
||||
part_metadata: Arc::new(Mutex::new(HashMap::new())),
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
runtime: Arc::new(Mutex::new(
|
||||
Runtime::new().mres_msg("Could not create async runtime")?,
|
||||
)),
|
||||
runtime: Arc::new(Mutex::new(runtime)),
|
||||
};
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
@@ -244,7 +279,7 @@ impl Mize {
|
||||
console_log!("before loading build time config");
|
||||
|
||||
// load the config from build time
|
||||
let config = ItemData::from_toml(BUILD_TIME_CONFIG)?;
|
||||
let config = ItemData::from_json(BUILD_TIME_CONFIG.to_owned())?;
|
||||
instance.set_blocking("0", config);
|
||||
|
||||
#[cfg(feature = "target-wasm ")]
|
||||
@@ -546,7 +581,7 @@ impl Mize {
|
||||
}
|
||||
|
||||
/// Record a method/effect/thread declaration while in generate mode.
|
||||
pub fn record_method(&self, name: &str, id: MizeId, kind: &str) -> MizeResult<()> {
|
||||
pub fn record_method(&self, name: &str, id: MizeId, kind: &str, signature: &str) -> MizeResult<()> {
|
||||
let cur_part = self.cur_eval_part.lock()?.clone()
|
||||
.ok_or_else(|| mize_err!("no cur_eval_part set"))?;
|
||||
let mut metadata_map = self.part_metadata.lock()?;
|
||||
@@ -554,10 +589,14 @@ impl Mize {
|
||||
part_name: cur_part.dot(),
|
||||
..Default::default()
|
||||
});
|
||||
let (arg_types, return_type) = parse_method_signature(signature);
|
||||
let m = MethodMetadata {
|
||||
name: name.to_string(),
|
||||
id,
|
||||
kind: kind.to_string(),
|
||||
signature: signature.to_string(),
|
||||
arg_types,
|
||||
return_type,
|
||||
};
|
||||
match kind {
|
||||
"effect" => metadata.effects.push(m),
|
||||
@@ -568,7 +607,15 @@ impl Mize {
|
||||
}
|
||||
|
||||
/// Record a native type inclusion while in generate mode.
|
||||
pub fn record_native(&self, name: &str, type_name: &str, associated: Vec<String>, methods: Vec<String>) -> MizeResult<()> {
|
||||
pub fn record_native(
|
||||
&self,
|
||||
name: &str,
|
||||
type_name: &str,
|
||||
associated: Vec<String>,
|
||||
methods: Vec<String>,
|
||||
associated_sigs: Vec<String>,
|
||||
method_sigs: Vec<String>,
|
||||
) -> MizeResult<()> {
|
||||
let cur_part = self.cur_eval_part.lock()?.clone()
|
||||
.ok_or_else(|| mize_err!("no cur_eval_part set"))?;
|
||||
let mut metadata_map = self.part_metadata.lock()?;
|
||||
@@ -581,6 +628,8 @@ impl Mize {
|
||||
type_name: type_name.to_string(),
|
||||
associated,
|
||||
methods,
|
||||
associated_sigs,
|
||||
method_sigs,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -805,12 +854,15 @@ impl Mize {
|
||||
) -> MizeResult<()> {
|
||||
let mut threads_inner = self.threads.lock()?;
|
||||
let mut next_thread_id = self.next_thread_id.lock()?;
|
||||
let tokio_handle = tokio::runtime::Handle::current();
|
||||
|
||||
let my_thread_id_no_mutex_guard = *next_thread_id;
|
||||
let _thread_mutex = self.threads.clone();
|
||||
let name_to_move = name.to_owned();
|
||||
let to_spawn = move || -> () {
|
||||
debug!("spawning thread: {}", name_to_move);
|
||||
let guard = tokio_handle.enter();
|
||||
std::mem::forget(guard);
|
||||
let _my_thread_id = my_thread_id_no_mutex_guard;
|
||||
|
||||
let res = func();
|
||||
@@ -883,8 +935,8 @@ impl Mize {
|
||||
let mut next_thread_id = self.next_thread_id.lock()?;
|
||||
|
||||
threads_inner.push((*next_thread_id, name.to_owned(), None));
|
||||
let runtime_inner = self.runtime.lock().unwrap();
|
||||
runtime_inner.spawn(func);
|
||||
let runtime = tokio::runtime::Handle::current();
|
||||
runtime.spawn(func);
|
||||
|
||||
*next_thread_id += 1;
|
||||
|
||||
@@ -893,9 +945,7 @@ impl Mize {
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
pub fn async_get_handle(&self) -> Handle {
|
||||
let runtime_inner = self.runtime.lock().unwrap();
|
||||
let handle = runtime_inner.handle().to_owned();
|
||||
handle
|
||||
tokio::runtime::Handle::current()
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
@@ -916,16 +966,13 @@ impl Mize {
|
||||
.expect("mutex lock failed in spawn_async_blocking");
|
||||
|
||||
threads_inner.push((*next_thread_id, name.to_owned(), None));
|
||||
let runtime_inner = self.runtime.lock().unwrap();
|
||||
let handle = runtime_inner.handle().to_owned();
|
||||
let runtime = tokio::runtime::Handle::current();
|
||||
*next_thread_id += 1;
|
||||
|
||||
drop(runtime_inner);
|
||||
drop(threads_inner);
|
||||
drop(next_thread_id);
|
||||
|
||||
handle.enter();
|
||||
let result = handle.block_on(func);
|
||||
let result = runtime.block_on(func);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -958,24 +1005,29 @@ impl Mize {
|
||||
|
||||
let mut opt = config_opts
|
||||
.get_mut(&id)
|
||||
.ok_or_else(|| mize_err!("Config opt for {name} not in config_opts"))?;
|
||||
.ok_or_else(|| mize_err!("Config opt for {id} not in config_opts"))?;
|
||||
|
||||
if let Some(val) = opt.val.clone() {
|
||||
return Ok(val);
|
||||
}
|
||||
|
||||
// in case it is not cached run the fun if it exists
|
||||
if let Some(fun) = &opt.fun {
|
||||
if opt.fun.is_some() {
|
||||
let fun = opt.fun.take().unwrap();
|
||||
drop(config_opts);
|
||||
let res = fun()?;
|
||||
let mut config_opts = self.config_opts.lock().unwrap();
|
||||
let mut opt = config_opts.get_mut(&id).unwrap();
|
||||
opt.fun = Some(fun);
|
||||
opt.val = Some(res.clone());
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
Err(mize_err!("ConfigOpt {name} has no fun and no cached value"))
|
||||
Err(mize_err!("ConfigOpt {id} has no fun and no cached value"))
|
||||
}
|
||||
|
||||
pub fn dyn_call(&mut self, part_id: MizeId, method: &str, args: ItemData) -> MizeResult<ItemData> {
|
||||
debug!("dyn call {}.{} with args: {}", part_id, method, args);
|
||||
debug!("dyn call {}/{} with args: {}", part_id, method, args);
|
||||
let mut fun_values = self.fun_values.lock()?;
|
||||
|
||||
let id = part_id.clone().join(method)?;
|
||||
@@ -985,7 +1037,9 @@ impl Mize {
|
||||
|
||||
let calling_part = self.clone().cur_eval_part()?;
|
||||
self.clone().set_cur_eval_part(Some(part_id));
|
||||
println!("before call");
|
||||
let res = fun(args);
|
||||
println!("after call");
|
||||
self.clone().set_cur_eval_part(calling_part);
|
||||
let mut fun_values = self.fun_values.lock()?;
|
||||
fun_values.insert(id, fun);
|
||||
|
||||
@@ -2,13 +2,13 @@ use core::fmt;
|
||||
use flume::Sender;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use tracing::trace;
|
||||
|
||||
use crate::error::MizeResult;
|
||||
use crate::id::MizeId;
|
||||
use crate::instance::connection::Connection;
|
||||
use crate::item::Item;
|
||||
use crate::proto::MizeMessage;
|
||||
use crate::prelude::*;
|
||||
|
||||
use super::Mize;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use flume::Receiver;
|
||||
use std::borrow::BorrowMut;
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, trace, warn};
|
||||
|
||||
use crate::error::{MizeError, MizeResult, MizeResultTrait};
|
||||
use crate::id::MizeId;
|
||||
use crate::mize_err;
|
||||
use crate::prelude::*;
|
||||
use crate::proto::{MessageCmd, MizeMessage};
|
||||
use crate::{instance::Mize, item::ItemData};
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::{config, get_global_mize};
|
||||
use crate::config::{ConfigOpt, FromItemData, MizeFun};
|
||||
use ciborium::into_writer;
|
||||
use ciborium::from_reader;
|
||||
use colored::Colorize;
|
||||
use serde_json::from_reader;
|
||||
use serde::de::DeserializeOwned;
|
||||
use core::fmt;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -13,7 +14,8 @@ use std::sync::MutexGuard;
|
||||
use std::{boxed, io};
|
||||
use std::fmt::Display;
|
||||
use std::i128;
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
use crate::tracing::instrument;
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::error::{IntoMizeResult, MizeError, MizeErrorWhichImplementsError, MizeResult};
|
||||
use crate::id::MizeId;
|
||||
@@ -256,7 +258,12 @@ impl ItemData {
|
||||
let fun_num = self.get_path("_fun_num")?.value_u32()?;
|
||||
let fun_id = mize.clone().id_new_dots("mize.fun")?.join(format!("{fun_num}").as_str())?;
|
||||
let fun = fun_values.remove(&fun_id).ok_or(mize_err!("fun with id {fun_id} not in fun_values"))?;
|
||||
|
||||
drop(fun_values);
|
||||
let res = fun(args)?;
|
||||
let mut fun_values = mize.fun_values.lock()?;
|
||||
fun_values.insert(fun_id, fun);
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
@@ -406,6 +413,13 @@ impl ItemData {
|
||||
return None;
|
||||
}
|
||||
|
||||
pub fn deserialize<T: Serialize + DeserializeOwned>(&self) -> MizeResult<T> {
|
||||
let mut buffer = Vec::new();
|
||||
into_writer(&self, &mut buffer)?;
|
||||
let cbor_val: T = from_reader(buffer.as_slice())?;
|
||||
Ok(cbor_val)
|
||||
}
|
||||
|
||||
/// Get an immutable reference to the native value stored in this ItemData.
|
||||
///
|
||||
/// If the ItemData has a `_native` field (stored via `from_native()`), returns a guard
|
||||
@@ -467,6 +481,7 @@ impl ItemData {
|
||||
///
|
||||
/// Otherwise, constructs the value via `T::from_itemdata(self)`, stores it in the
|
||||
/// registry, and returns a guard to it.
|
||||
/// i hate rust. (as you can't have a generic FromItemData impl and then more specific ones.)
|
||||
pub fn native_from<T: FromItemData + Any + Send + Sync + 'static>(&self) -> MizeResult<NativeRefGuard<T>> {
|
||||
// Fast path: the registry already holds a T for this ItemData
|
||||
if let Some(id) = self.try_get_native_id() {
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::fmt::format;
|
||||
macro_rules! out {
|
||||
($($arg:tt)*) => {
|
||||
// format_args! handles string interpolation safely at compile time
|
||||
if let Err(e) = $crate::write_to_mize_stdout(format_args!($($arg)*)) {
|
||||
if let Err(e) = $crate::write_to_mize_stdout(format_args!("{}\n", format_args!($($arg)*))) {
|
||||
// Choose your error handling behavior here (panic, print to stderr, or ignore)
|
||||
eprintln!("out! macro failed to write!!");
|
||||
}
|
||||
@@ -17,7 +17,7 @@ macro_rules! out {
|
||||
}
|
||||
|
||||
pub fn write_to_mize_stdout(args: std::fmt::Arguments) -> MizeResult<()> {
|
||||
println!("{}", args);
|
||||
print!("{}", args);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -84,8 +84,11 @@ pub mod prelude {
|
||||
pub use config::*;
|
||||
pub use api::*;
|
||||
pub use mize_macros::*;
|
||||
pub use crate::tracing::{trace, warn, debug, info, error};
|
||||
}
|
||||
pub use flume;
|
||||
use tokio::runtime::Builder;
|
||||
use tokio::runtime::Runtime;
|
||||
pub use tracing;
|
||||
|
||||
|
||||
@@ -108,13 +111,15 @@ pub use error::MizeError;
|
||||
pub use error::MizeResult;
|
||||
pub use instance::Mize;
|
||||
pub use instance::{
|
||||
MizePart, PartMetadata, OptMetadata, MethodMetadata, NativeMetadata,
|
||||
MizePart, PartMetadata, OptMetadata, MethodMetadata, NativeMetadata, parse_method_signature,
|
||||
};
|
||||
pub use mize_macros::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use crate::platform::os;
|
||||
|
||||
/// Global Mize instance storage.
|
||||
/// This is set once when the Mize instance is created and can be accessed from anywhere.
|
||||
pub static GLOBAL_MIZE: OnceLock<Arc<Mutex<Mize>>> = OnceLock::new();
|
||||
@@ -144,9 +149,32 @@ pub fn set_global_mize(mize: Mize) -> MizeResult<()> {
|
||||
|
||||
/// C API function to set the global Mize instance from a dynamically loaded part.
|
||||
/// This is a hack to allow Rust parts loaded as .so files to access the global Mize instance.
|
||||
// i hate rust.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn mize_rust_hack_set_global(mize: Arc<Mutex<Mize>>) {
|
||||
pub extern "C" fn mize_rust_hack_set_global(mize: Arc<Mutex<Mize>>, dispatch_ptr: *const std::ffi::c_void) {
|
||||
// and also set the tokio runtime
|
||||
//let mize_locked = mize.lock().unwrap();
|
||||
//let rt = mize_locked.runtime.lock().unwrap();
|
||||
//rt.enter();
|
||||
//drop(rt);
|
||||
//drop(mize_locked);
|
||||
let rt = Builder::new_multi_thread().enable_all().build().unwrap();
|
||||
let guard = rt.enter();
|
||||
std::mem::forget(guard);
|
||||
|
||||
use crate::tracing::Dispatch;
|
||||
let _ = GLOBAL_MIZE.set(mize);
|
||||
|
||||
//and also init tracing
|
||||
unsafe {
|
||||
/// i hate rust.
|
||||
let dispatch = &*(dispatch_ptr as *const Dispatch);
|
||||
if let Err(e) = tracing::dispatcher::set_global_default(dispatch.clone()) {
|
||||
println!("failed to set tracing dispatcher: {e}");
|
||||
}
|
||||
tracing::callsite::rebuild_interest_cache();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Resolve an executable name to its full path in the nixpkgs store.
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::binary_heap::Iter;
|
||||
use std::collections::HashMap;
|
||||
use std::iter::Map;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::{instrument, trace};
|
||||
use crate::tracing::instrument;
|
||||
|
||||
use crate::error::{IntoMizeResult, MizeError, MizeResult};
|
||||
use crate::id::MizeId;
|
||||
@@ -13,6 +13,7 @@ use crate::instance::Mize;
|
||||
use crate::item::get_raw_from_cbor;
|
||||
use crate::item::{Item, ItemData};
|
||||
use crate::mize_err;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MemStore {
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use sysinfo::{Pid, ProcessRefreshKind, RefreshKind, System};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::id::MizeId;
|
||||
use crate::item::{Item, ItemData};
|
||||
@@ -15,6 +14,9 @@ use crate::item::get_raw_from_cbor;
|
||||
use crate::item::IntoItemData;
|
||||
use crate::memstore::MemStore;
|
||||
use crate::{instance::store::Store, mize_err};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -72,6 +74,7 @@ impl FileStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
async fn try_connect_to_sock(store_path: String) -> MizeResult<()> {
|
||||
match UnixStream::connect(PathBuf::from(store_path).join("sock")).await {
|
||||
Err(_) => Err(mize_err!("")),
|
||||
@@ -79,6 +82,21 @@ async fn try_connect_to_sock(store_path: String) -> MizeResult<()> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
async fn try_connect_to_sock(store_path: String) -> MizeResult<()> {
|
||||
use tokio::net::windows::named_pipe::{ClientOptions};
|
||||
let pipe_name = format!(
|
||||
"\\.\\pipe\\mize_{}",
|
||||
store_path.replace('\\', "_").replace('/', "_").replace(':', "_")
|
||||
);
|
||||
// ClientOptions::new().open() is synchronous, not async
|
||||
// So we just try to open and immediately close it
|
||||
match ClientOptions::new().open(&pipe_name) {
|
||||
Err(_) => Err(mize_err!("")),
|
||||
Ok(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
impl Store for FileStore {
|
||||
fn new_id(&self) -> MizeResult<String> {
|
||||
let mut next_id: u64 =
|
||||
@@ -191,6 +209,7 @@ impl Store for FileStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
fn valid_pid_file(path: &Path) -> MizeResult<Option<u32>> {
|
||||
let pid_file_path = path.join("pid");
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing::Event;
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
use tracing::{Dispatch, Event};
|
||||
use tracing::{field::Visit, Id, Subscriber};
|
||||
use tracing::{span_enabled, Instrument, Level};
|
||||
use tracing_core::Field;
|
||||
@@ -29,10 +28,65 @@ use crate::id::MizeId;
|
||||
use crate::item::ItemData;
|
||||
use crate::MizeError;
|
||||
use crate::MizeResult;
|
||||
use crate::prelude::*;
|
||||
|
||||
static DEFAULT_LOG_LEVEL: LevelFilter = LevelFilter::WARN;
|
||||
|
||||
pub fn init_logger(cli_matches: &ArgMatches) {
|
||||
|
||||
/*
|
||||
#[macro_export]
|
||||
macro_rules! trace {
|
||||
($($arg:tt)*) => {
|
||||
// format_args! handles string interpolation safely at compile time
|
||||
if let Err(e) = $crate::write_to_mize_stdout(format_args!("[ {} ] {}\n", colored::Colorize::yellow("TRACE"), format_args!($($arg)*))) {
|
||||
// Choose your error handling behavior here (panic, print to stderr, or ignore)
|
||||
eprintln!("out! macro failed to write!!");
|
||||
}
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! debug {
|
||||
($($arg:tt)*) => {
|
||||
// format_args! handles string interpolation safely at compile time
|
||||
if let Err(e) = $crate::write_to_mize_stdout(format_args!("[ {} ] {}\n", colored::Colorize::green("DEBUG"), format_args!($($arg)*))) {
|
||||
// Choose your error handling behavior here (panic, print to stderr, or ignore)
|
||||
eprintln!("out! macro failed to write!!");
|
||||
}
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
($($arg:tt)*) => {
|
||||
// format_args! handles string interpolation safely at compile time
|
||||
if let Err(e) = $crate::write_to_mize_stdout(format_args!("[ {} ] {}\n", colored::Colorize::blue("INFO"), format_args!($($arg)*))) {
|
||||
// Choose your error handling behavior here (panic, print to stderr, or ignore)
|
||||
eprintln!("out! macro failed to write!!");
|
||||
}
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! warn {
|
||||
($($arg:tt)*) => {
|
||||
// format_args! handles string interpolation safely at compile time
|
||||
if let Err(e) = $crate::write_to_mize_stdout(format_args!("[ {} ] {}\n", colored::Colorize::truecolor("WARN", 245, 164, 66), format_args!($($arg)*))) {
|
||||
// Choose your error handling behavior here (panic, print to stderr, or ignore)
|
||||
eprintln!("out! macro failed to write!!");
|
||||
}
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($($arg:tt)*) => {
|
||||
// format_args! handles string interpolation safely at compile time
|
||||
if let Err(e) = $crate::write_to_mize_stdout(format_args!("[ {} ] {}\n", colored::Colorize::red("ERROR"), format_args!($($arg)*))) {
|
||||
// Choose your error handling behavior here (panic, print to stderr, or ignore)
|
||||
eprintln!("out! macro failed to write!!");
|
||||
}
|
||||
};
|
||||
}
|
||||
*/
|
||||
|
||||
pub fn init_logger(cli_matches: Option<&ArgMatches>) {
|
||||
trace!("init_logger called");
|
||||
// to save log messages, before the logger is setup
|
||||
let mut log_messages: Vec<(Level, String)> = Vec::new();
|
||||
@@ -96,67 +150,69 @@ pub fn init_logger(cli_matches: &ArgMatches) {
|
||||
log_messages.append(&mut val_mize_log.1);
|
||||
|
||||
// check -vvvvv flags
|
||||
let v_flag_count = cli_matches.get_count("verbose");
|
||||
if v_flag_count > 0 {
|
||||
let default_log_level_as_num = match DEFAULT_LOG_LEVEL {
|
||||
LevelFilter::OFF => 0,
|
||||
LevelFilter::ERROR => 1,
|
||||
LevelFilter::WARN => 2,
|
||||
LevelFilter::INFO => 3,
|
||||
LevelFilter::DEBUG => 4,
|
||||
LevelFilter::TRACE => 5,
|
||||
};
|
||||
let new_log_level_num = default_log_level_as_num + v_flag_count;
|
||||
log_level = match new_log_level_num {
|
||||
0 => LevelFilter::OFF,
|
||||
1 => LevelFilter::ERROR,
|
||||
2 => LevelFilter::WARN,
|
||||
3 => LevelFilter::INFO,
|
||||
4 => LevelFilter::DEBUG,
|
||||
5 => LevelFilter::TRACE,
|
||||
_ => {
|
||||
log_messages.push((
|
||||
Level::TRACE,
|
||||
format!(
|
||||
"the number of verbose (-v) flags passed ({}) exeeds the maximumg log_level",
|
||||
v_flag_count
|
||||
),
|
||||
));
|
||||
LevelFilter::TRACE
|
||||
}
|
||||
};
|
||||
log_messages.push((
|
||||
Level::TRACE,
|
||||
format!("set log level from the verbose flags to {}", log_level),
|
||||
));
|
||||
}
|
||||
if let Some(cli_matches) = cli_matches {
|
||||
let v_flag_count = cli_matches.get_count("verbose");
|
||||
if v_flag_count > 0 {
|
||||
let default_log_level_as_num = match DEFAULT_LOG_LEVEL {
|
||||
LevelFilter::OFF => 0,
|
||||
LevelFilter::ERROR => 1,
|
||||
LevelFilter::WARN => 2,
|
||||
LevelFilter::INFO => 3,
|
||||
LevelFilter::DEBUG => 4,
|
||||
LevelFilter::TRACE => 5,
|
||||
};
|
||||
let new_log_level_num = default_log_level_as_num + v_flag_count;
|
||||
log_level = match new_log_level_num {
|
||||
0 => LevelFilter::OFF,
|
||||
1 => LevelFilter::ERROR,
|
||||
2 => LevelFilter::WARN,
|
||||
3 => LevelFilter::INFO,
|
||||
4 => LevelFilter::DEBUG,
|
||||
5 => LevelFilter::TRACE,
|
||||
_ => {
|
||||
log_messages.push((
|
||||
Level::TRACE,
|
||||
format!(
|
||||
"the number of verbose (-v) flags passed ({}) exeeds the maximumg log_level",
|
||||
v_flag_count
|
||||
),
|
||||
));
|
||||
LevelFilter::TRACE
|
||||
}
|
||||
};
|
||||
log_messages.push((
|
||||
Level::TRACE,
|
||||
format!("set log level from the verbose flags to {}", log_level),
|
||||
));
|
||||
}
|
||||
|
||||
// check --log-level arg
|
||||
if let Some(log_level_arg_string) = cli_matches.get_one::<String>("log-level") {
|
||||
let log_level_from_arg = match log_level_arg_string.to_owned().to_lowercase().as_str() {
|
||||
"none" => LevelFilter::OFF,
|
||||
"trace" => LevelFilter::TRACE,
|
||||
"debug" => LevelFilter::DEBUG,
|
||||
"info" => LevelFilter::INFO,
|
||||
"warn" => LevelFilter::WARN,
|
||||
"error" => LevelFilter::ERROR,
|
||||
_ => {
|
||||
log_messages.push((Level::WARN, format!(
|
||||
"The argument to --log-level \"{}\" is not a valid log-level. loglevel stays at: {}"
|
||||
, log_level_arg_string
|
||||
, log_level
|
||||
)));
|
||||
log_level
|
||||
}
|
||||
};
|
||||
log_level = log_level_from_arg;
|
||||
log_messages.push((
|
||||
Level::TRACE,
|
||||
format!(
|
||||
"set log level from the --log-level arg to {}",
|
||||
log_level_from_arg
|
||||
),
|
||||
));
|
||||
// check --log-level arg
|
||||
if let Some(log_level_arg_string) = cli_matches.get_one::<String>("log-level") {
|
||||
let log_level_from_arg = match log_level_arg_string.to_owned().to_lowercase().as_str() {
|
||||
"none" => LevelFilter::OFF,
|
||||
"trace" => LevelFilter::TRACE,
|
||||
"debug" => LevelFilter::DEBUG,
|
||||
"info" => LevelFilter::INFO,
|
||||
"warn" => LevelFilter::WARN,
|
||||
"error" => LevelFilter::ERROR,
|
||||
_ => {
|
||||
log_messages.push((Level::WARN, format!(
|
||||
"The argument to --log-level \"{}\" is not a valid log-level. loglevel stays at: {}"
|
||||
, log_level_arg_string
|
||||
, log_level
|
||||
)));
|
||||
log_level
|
||||
}
|
||||
};
|
||||
log_level = log_level_from_arg;
|
||||
log_messages.push((
|
||||
Level::TRACE,
|
||||
format!(
|
||||
"set log level from the --log-level arg to {}",
|
||||
log_level_from_arg
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// actually set the log_level on the builder
|
||||
@@ -181,13 +237,18 @@ pub fn init_logger(cli_matches: &ArgMatches) {
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
builder
|
||||
let subscriber = builder
|
||||
//.with_env_filter(my_env_filter)
|
||||
.without_time()
|
||||
.event_format(MyFormatter)
|
||||
.fmt_fields(field_formatter)
|
||||
.with_max_level(log_level)
|
||||
.init();
|
||||
.finish();
|
||||
|
||||
let dispatch = Dispatch::new(subscriber);
|
||||
if let Err(e) = tracing::dispatcher::set_global_default(dispatch) {
|
||||
println!("faild to set_global_default(dispatch): {e}");
|
||||
}
|
||||
|
||||
// write out all log messages from log_messages
|
||||
for (level, msg) in log_messages {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use clap::ArgMatches;
|
||||
use colored::Colorize;
|
||||
use tracing::Dispatch;
|
||||
use core::fmt;
|
||||
use flate2::bufread::GzDecoder;
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -12,7 +13,6 @@ use std::path::PathBuf;
|
||||
use std::time::UNIX_EPOCH;
|
||||
use tar::Archive;
|
||||
use tracing::Event;
|
||||
use tracing::{debug, info, trace, warn};
|
||||
use tracing::field::Visit;
|
||||
use tracing::Level;
|
||||
use tracing_core::Field;
|
||||
@@ -25,12 +25,8 @@ use tracing_subscriber::registry::LookupSpan;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::GLOBAL_MIZE;
|
||||
use crate::config::ConfigOpt;
|
||||
use crate::error::{IntoMizeResult, MizeResult};
|
||||
use crate::instance::Mize;
|
||||
use crate::item::{data_from_string, IntoItemData, ItemData};
|
||||
use crate::mize_err;
|
||||
use crate::prelude::*;
|
||||
use crate::item::data_from_string;
|
||||
|
||||
use self::fsstore::FileStore;
|
||||
|
||||
@@ -40,8 +36,14 @@ pub mod logging;
|
||||
#[cfg(target_family = "unix")]
|
||||
mod unix_socket;
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
mod windows_socket;
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
use crate::platform::os::unix_socket::UnixListener;
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
use crate::platform::os::windows_socket::WindowsListener as UnixListener;
|
||||
//mod web;
|
||||
|
||||
pub fn os_instance_init(mize: &mut Mize) -> MizeResult<()> {
|
||||
@@ -133,7 +135,10 @@ pub fn os_instance_init(mize: &mut Mize) -> MizeResult<()> {
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
{
|
||||
error!("CONNECTING... would connect, but that is not implemented on windows yet");
|
||||
info!("CONNECTING");
|
||||
use crate::platform::os::windows_socket;
|
||||
windows_socket::connect(mize, store_path.into())?;
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
// else open it ourselves
|
||||
@@ -149,7 +154,7 @@ pub fn os_instance_init(mize: &mut Mize) -> MizeResult<()> {
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
{
|
||||
warn!("would add a Listener on a local socket, but that is not yet implemented for windows");
|
||||
mize.add_conn_listener(UnixListener::new(path)?)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,31 +278,33 @@ pub fn fetch_part(mize: &mut Mize, part_name: &str) -> MizeResult<String> {
|
||||
|
||||
fn get_part_path(mize: &mut Mize, part_name: &str, _path: Option<String>) -> MizeResult<String> {
|
||||
|
||||
let part_symbol_name = part_name.replace(".", "_");
|
||||
|
||||
// check if $name.src is set
|
||||
if let Ok(itm) = mize.get_config(format!("{part_name}.src").as_str()) {
|
||||
let part_src = itm.value_string().map_err(|e| e.msg(format!("opt {part_name}.src was not a string")))?;
|
||||
// will only work for rust for now
|
||||
let part_path = format!("{part_src}/target/debug/libmize_part_{part_name}.so");
|
||||
let part_path = format!("{part_src}/target/debug/libmize_part_{part_symbol_name}.so");
|
||||
return Ok(part_path);
|
||||
}
|
||||
|
||||
// check if $name.out is set
|
||||
if let Ok(itm) = mize.get_config(format!("{part_name}.out").as_str()) {
|
||||
let part_out = itm.value_string().map_err(|e| e.msg(format!("opt {part_name}.out was not a string")))?;
|
||||
let part_path = format!("{part_out}/lib/libmize_part_{part_name}.so");
|
||||
let part_path = format!("{part_out}/lib/libmize_part_{part_symbol_name}.so");
|
||||
return Ok(part_path);
|
||||
}
|
||||
|
||||
// check if we find the part inside mize.src
|
||||
if let Ok(itm) = mize.get_config("mize.src") {
|
||||
let mize_src = itm.value_string().map_err(|e| e.msg("opt mize.src was not a string"))?;
|
||||
let part_path = format!("{mize_src}/target/debug/libmize_part_{part_name}.so");
|
||||
let part_path = format!("{mize_src}/target/debug/libmize_part_{part_symbol_name}.so");
|
||||
return Ok(part_path);
|
||||
}
|
||||
|
||||
// fetch the part from https://mize.works
|
||||
let out_path = fetch_part(mize, part_name)?;
|
||||
return Ok(format!("{out_path}/lib/libmize_part_{part_name}.so"));
|
||||
return Ok(format!("{out_path}/lib/libmize_part_{part_symbol_name}.so"));
|
||||
|
||||
//Err(mize_err!("part {part_name} not found"))
|
||||
}
|
||||
@@ -308,16 +315,33 @@ pub fn load_part(mize: &mut Mize, part_name: &str, path: Option<String>) -> Mize
|
||||
let lib = unsafe { libloading::Library::new(part_path)? };
|
||||
|
||||
unsafe {
|
||||
if let Ok(rust_hack_func) = lib.get::<libloading::Symbol<unsafe extern "C" fn(Arc<Mutex<Mize>>)>>("mize_rust_hack_set_global".as_bytes()) {
|
||||
rust_hack_func(GLOBAL_MIZE.get().unwrap().clone())
|
||||
if let Ok(rust_hack_func) = lib.get::<libloading::Symbol<unsafe extern "C" fn(Arc<Mutex<Mize>>, *const std::ffi::c_void)>>("mize_rust_hack_set_global".as_bytes()) {
|
||||
|
||||
tracing::dispatcher::get_default(|dispatch| {
|
||||
rust_hack_func(GLOBAL_MIZE.get().unwrap().clone(), dispatch as *const Dispatch as *const std::ffi::c_void);
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
let part_symbol_name = part_name.replace(".", "_");
|
||||
|
||||
let func: libloading::Symbol<
|
||||
unsafe extern "C" fn() -> MizeResult<()>,
|
||||
> = unsafe { lib.get(format!("part_{}", part_name).as_bytes())? };
|
||||
> = unsafe { lib.get(format!("part_{}", part_symbol_name).as_bytes())? };
|
||||
|
||||
mize.clone().set_cur_eval_part(Some(mize.clone().id_new_dots(part_name)?))?;
|
||||
let part_id = mize.clone().id_new_dots(part_name)?;
|
||||
{
|
||||
let mut opts = mize.config_opts.lock()?;
|
||||
opts.entry(part_id.clone()).or_insert_with(|| ConfigOpt {
|
||||
name: part_name.to_owned(),
|
||||
val: Some(ItemData::new()),
|
||||
fun: None,
|
||||
deps: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
mize.clone().set_cur_eval_part(Some(part_id))?;
|
||||
unsafe { func()? };
|
||||
mize.clone().set_cur_eval_part(None)?;
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ use std::path::{Path, PathBuf};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, Interest};
|
||||
use tokio::net::unix::{OwnedReadHalf, OwnedWriteHalf};
|
||||
use tokio::net::{UnixListener as TokioUnixListener, UnixStream};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::error::{IntoMizeResult, MizeError, MizeResult};
|
||||
use crate::instance::connection::{ConnListener, Connection};
|
||||
use crate::instance::{self, Mize};
|
||||
use crate::proto::{self, MizeMessage};
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct UnixListener {
|
||||
sock_path: PathBuf,
|
||||
|
||||
224
core/src/platform/os/windows_socket.rs
Normal file
224
core/src/platform/os/windows_socket.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
// This module is only compiled on Windows
|
||||
// It provides the same functionality as unix_socket.rs but using Windows named pipes
|
||||
|
||||
use ciborium::Value as CborValue;
|
||||
use flume::{unbounded, Receiver, Sender};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::windows::named_pipe::{ClientOptions, ServerOptions};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::error::{IntoMizeResult, MizeError, MizeResult};
|
||||
use crate::instance::connection::{ConnListener, Connection};
|
||||
use crate::instance::{self, Mize};
|
||||
use crate::proto::{self, MizeMessage};
|
||||
|
||||
pub struct WindowsListener {
|
||||
pipe_name: String,
|
||||
}
|
||||
|
||||
impl WindowsListener {
|
||||
pub fn new(store_path: PathBuf) -> MizeResult<WindowsListener> {
|
||||
// On Windows, we use a named pipe with a specific format
|
||||
// The format is \.\pipe\<name> for local pipes
|
||||
// We'll use the store path hash or name to create a unique pipe name
|
||||
let pipe_name = format!(
|
||||
"\\.\\pipe\\mize_{}",
|
||||
store_path.to_string_lossy().replace('\\', "_").replace('/', "_").replace(':', "_")
|
||||
);
|
||||
Ok(WindowsListener { pipe_name })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect(instance: &mut Mize, store_path: PathBuf) -> MizeResult<()> {
|
||||
let conn_id = instance.spawn_async_blocking(
|
||||
"windows_connection",
|
||||
connect_async(instance.clone(), store_path),
|
||||
)?;
|
||||
|
||||
let ns_of_peer_str = instance
|
||||
.get(format!(
|
||||
"inst/con_by_id/{}/peer/self/config/namespace",
|
||||
conn_id
|
||||
))?
|
||||
.value_string()?;
|
||||
let ns_of_peer = instance.namespace_from_string(ns_of_peer_str)?;
|
||||
|
||||
info!(
|
||||
"connect_async ... ns_of_peer: {}",
|
||||
ns_of_peer.clone().as_string()
|
||||
);
|
||||
|
||||
instance.connection_set_namespace(conn_id, ns_of_peer.clone());
|
||||
instance.set_namespace(ns_of_peer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect_async(mut instance: Mize, store_path: PathBuf) -> MizeResult<u64> {
|
||||
let pipe_name = format!(
|
||||
"\\.\\pipe\\mize_{}",
|
||||
store_path.to_string_lossy().replace('\\', "_").replace('/', "_").replace(':', "_")
|
||||
);
|
||||
|
||||
// Create the client synchronously
|
||||
let client = ClientOptions::new().open(&pipe_name)?;
|
||||
|
||||
// Wrap the client in an Arc<Mutex> so it can be shared between incoming and outgoing tasks
|
||||
let shared_client = Arc::new(Mutex::new(client));
|
||||
|
||||
let (send_tx, send_rx) = unbounded::<MizeMessage>();
|
||||
|
||||
let conn_id = instance.new_connection(send_tx)?;
|
||||
|
||||
let cloned_instance = instance.clone();
|
||||
let incoming_client = shared_client.clone();
|
||||
instance.spawn_background("incomming", move || {
|
||||
let result = windows_incomming(incoming_client, cloned_instance, conn_id);
|
||||
if let Err(err) = result {
|
||||
warn!("Connection closing because of EOF: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let outgoing_cloned_instance = instance.clone();
|
||||
let outgoing_client = shared_client.clone();
|
||||
instance.spawn_background("outgoing", move || {
|
||||
let result = windows_outgoing(outgoing_client, send_rx, outgoing_cloned_instance, conn_id);
|
||||
if let Err(err) = result {
|
||||
warn!("Connection closing because of EOF: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Ok(conn_id)
|
||||
}
|
||||
|
||||
impl ConnListener for WindowsListener {
|
||||
fn listen(self, mut instance: Mize) -> MizeResult<()> {
|
||||
instance.spawn_async("windows listen async", windows_listen(self, instance.clone()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn windows_listen(listener: WindowsListener, mut instance: Mize) -> MizeResult<()> {
|
||||
// Create the first server instance
|
||||
let mut server = ServerOptions::new()
|
||||
.first_pipe_instance(true)
|
||||
.create(&listener.pipe_name)?;
|
||||
|
||||
loop {
|
||||
// Wait for a client to connect
|
||||
server.connect().await.mres_msg("Error while accepting Windows named pipe connection")?;
|
||||
info!("new connection");
|
||||
|
||||
let (send_tx, send_rx) = unbounded::<MizeMessage>();
|
||||
let conn_id = instance.new_connection(send_tx)?;
|
||||
|
||||
// Take ownership of the current server (which is now connected)
|
||||
let connected_server = server;
|
||||
|
||||
// Create a new server for the next connection
|
||||
server = ServerOptions::new().create(&listener.pipe_name)?;
|
||||
|
||||
// Wrap the connected server in an Arc<Mutex> so it can be shared
|
||||
let shared_server = Arc::new(Mutex::new(connected_server));
|
||||
|
||||
let cloned_instance = instance.clone();
|
||||
let incoming_server = shared_server.clone();
|
||||
instance.spawn_background("incomming", move || {
|
||||
let result = windows_incomming(incoming_server, cloned_instance, conn_id);
|
||||
if let Err(err) = result {
|
||||
warn!("Connection closing because of EOF: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let outgoing_cloned_instance = instance.clone();
|
||||
let outgoing_server = shared_server.clone();
|
||||
instance.spawn_background("outgoing", move || {
|
||||
let result = windows_outgoing(outgoing_server, send_rx, outgoing_cloned_instance, conn_id);
|
||||
if let Err(err) = result {
|
||||
warn!("Connection closing because of EOF: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn windows_outgoing<W>(shared_pipe: Arc<Mutex<W>>, send_rx: Receiver<MizeMessage>, mut instance: Mize, conn_id: u64) -> MizeResult<()>
|
||||
where
|
||||
W: AsyncWriteExt + Unpin,
|
||||
{
|
||||
for msg in send_rx {
|
||||
debug!("windows outgoing got msg: {}", msg);
|
||||
let adapter = MyCiboriumWriter {
|
||||
inner: &mut *shared_pipe.blocking_lock(),
|
||||
instance: &mut instance,
|
||||
};
|
||||
let value = msg.value();
|
||||
ciborium::into_writer(&value, adapter)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn windows_incomming<R>(shared_pipe: Arc<Mutex<R>>, mut instance: Mize, conn_id: u64) -> MizeResult<()>
|
||||
where
|
||||
R: AsyncReadExt + Unpin,
|
||||
{
|
||||
loop {
|
||||
let adapter = MyCiboriumReader {
|
||||
inner: &mut *shared_pipe.blocking_lock(),
|
||||
instance: &mut instance,
|
||||
};
|
||||
let value: CborValue = ciborium::from_reader(adapter)?;
|
||||
if let CborValue::Integer(_) = value {
|
||||
break;
|
||||
}
|
||||
let msg = MizeMessage::new(value, conn_id);
|
||||
debug!("windows incomming got msg: {}", msg);
|
||||
instance.got_msg(msg);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct MyCiboriumWriter<'a, W> {
|
||||
inner: &'a mut W,
|
||||
instance: &'a mut Mize,
|
||||
}
|
||||
|
||||
impl<'a, W> ciborium_io::Write for MyCiboriumWriter<'a, W>
|
||||
where
|
||||
W: AsyncWriteExt + Unpin,
|
||||
{
|
||||
type Error = MizeError;
|
||||
fn write_all(&mut self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
let runtime = self.instance.runtime.lock()?;
|
||||
let _ = runtime.block_on(self.inner.write_all(data))?;
|
||||
Ok(())
|
||||
}
|
||||
fn flush(&mut self) -> Result<(), Self::Error> {
|
||||
let handle = self.instance.async_get_handle();
|
||||
let _ = handle.block_on(self.inner.flush());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct MyCiboriumReader<'a, R> {
|
||||
inner: &'a mut R,
|
||||
instance: &'a mut Mize,
|
||||
}
|
||||
|
||||
impl<'a, R> ciborium_io::Read for MyCiboriumReader<'a, R>
|
||||
where
|
||||
R: AsyncReadExt + Unpin,
|
||||
{
|
||||
type Error = MizeError;
|
||||
fn read_exact(&mut self, data: &mut [u8]) -> Result<(), Self::Error> {
|
||||
let handle = self.instance.async_get_handle();
|
||||
let _ = handle.block_on(self.inner.read_exact(data))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
18
flake.nix
18
flake.nix
@@ -34,10 +34,8 @@ let
|
||||
|
||||
# mize build time config
|
||||
mizeBuildConfig = {
|
||||
config = {
|
||||
namespace = "mize.buildtime.ns";
|
||||
module_url = "parts.mize.works";
|
||||
};
|
||||
namespace = "mize.buildtime.ns";
|
||||
module_url = "parts.mize.works";
|
||||
};
|
||||
|
||||
|
||||
@@ -161,11 +159,7 @@ in {
|
||||
# shell without spacetime and other dev commands. just all needed to build mize or ppc
|
||||
buildShell = pkgs.mkShell {
|
||||
|
||||
MIZE_BUILD_CONFIG = let
|
||||
settingsFormat = pkgs.formats.toml { };
|
||||
in settingsFormat.generate "mize-build-config.toml" {
|
||||
config = mizeBuildConfig;
|
||||
};
|
||||
MIZE_BUILD_CONFIG = builtins.toJSON mizeBuildConfig;
|
||||
shellHook = ''
|
||||
mize_project_root=$(${pkgs.lib.getExe pkgs.git} rev-parse --show-toplevel)
|
||||
export MIZE_CONFIG_FILES=$mize_project_root/mize_config.toml
|
||||
@@ -234,11 +228,7 @@ in {
|
||||
};
|
||||
default = pkgs.mkShell rec {
|
||||
|
||||
MIZE_BUILD_CONFIG = let
|
||||
settingsFormat = pkgs.formats.toml { };
|
||||
in settingsFormat.generate "mize-build-config.toml" {
|
||||
config = mizeBuildConfig;
|
||||
};
|
||||
MIZE_BUILD_CONFIG = builtins.toJSON mizeBuildConfig;
|
||||
shellHook = ''
|
||||
mize_project_root=$(${pkgs.lib.getExe pkgs.git} rev-parse --show-toplevel)
|
||||
'';
|
||||
|
||||
@@ -227,19 +227,45 @@ fn generate_arg_passing(args: &[(Ident, Type)]) -> Vec<proc_macro2::TokenStream>
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// Last path segment of a Type, for signature strings (e.g. `clap::Command` -> `Command`).
|
||||
fn type_to_sig_name(ty: &Type) -> String {
|
||||
match ty {
|
||||
Type::Path(p) => p
|
||||
.path
|
||||
.segments
|
||||
.last()
|
||||
.map(|s| s.ident.to_string())
|
||||
.unwrap_or_else(|| quote!(#ty).to_string()),
|
||||
_ => quote!(#ty).to_string().replace(' ', ""),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a signature string like `"String, Command"` from FuncSpec args.
|
||||
fn synthesize_signature(args: &[(Ident, Type)]) -> String {
|
||||
args.iter()
|
||||
.map(|(_, ty)| type_to_sig_name(ty))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
fn expand_simple_include_native(simple: IncludeNativeSimple) -> TokenStream {
|
||||
let name = simple.name;
|
||||
let type_path = simple.type_path;
|
||||
let methods = simple.methods;
|
||||
|
||||
|
||||
let type_name_str = quote!(#type_path).to_string().replace(' ', "");
|
||||
let method_names: Vec<String> = methods.iter().map(|m| m.name.to_string()).collect();
|
||||
let method_sigs: Vec<String> = methods.iter().map(|m| synthesize_signature(&m.args)).collect();
|
||||
|
||||
let method_calls = methods.iter().map(|method| {
|
||||
let method_name = method.name.to_string();
|
||||
let method_ident = &method.name;
|
||||
let signature = synthesize_signature(&method.args);
|
||||
let arg_extractions = generate_arg_extraction(&method.args);
|
||||
let arg_passings = generate_arg_passing(&method.args);
|
||||
|
||||
|
||||
quote! {
|
||||
.method(#method_name, |args| {
|
||||
.method(#method_name, #signature, |args| {
|
||||
#(#arg_extractions)*
|
||||
let this = mize::prelude::get_global_mize()?
|
||||
.get_config(#name)?
|
||||
@@ -249,12 +275,26 @@ fn expand_simple_include_native(simple: IncludeNativeSimple) -> TokenStream {
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let expanded = quote! {
|
||||
mize::config::opt(#name)
|
||||
#(#method_calls)*
|
||||
{
|
||||
if let Ok(mize) = mize::prelude::get_global_mize() {
|
||||
if mize.is_generate_mode().unwrap_or(false) {
|
||||
let _ = mize.record_native(
|
||||
#name,
|
||||
#type_name_str,
|
||||
Vec::new(),
|
||||
vec![#(#method_names.to_string()),*],
|
||||
Vec::new(),
|
||||
vec![#(#method_sigs.to_string()),*],
|
||||
);
|
||||
}
|
||||
}
|
||||
mize::config::opt(#name)
|
||||
#(#method_calls)*
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
@@ -263,15 +303,22 @@ fn expand_block_include_native(block: IncludeNativeBlock) -> TokenStream {
|
||||
let type_path = block.type_path;
|
||||
let associated = block.associated;
|
||||
let methods = block.methods;
|
||||
|
||||
|
||||
let type_name_str = quote!(#type_path).to_string().replace(' ', "");
|
||||
let associated_names: Vec<String> = associated.iter().map(|m| m.name.to_string()).collect();
|
||||
let method_names: Vec<String> = methods.iter().map(|m| m.name.to_string()).collect();
|
||||
let associated_sigs: Vec<String> = associated.iter().map(|m| synthesize_signature(&m.args)).collect();
|
||||
let method_sigs: Vec<String> = methods.iter().map(|m| synthesize_signature(&m.args)).collect();
|
||||
|
||||
let method_calls = methods.iter().map(|method| {
|
||||
let method_name = method.name.to_string();
|
||||
let method_ident = &method.name;
|
||||
let signature = synthesize_signature(&method.args);
|
||||
let arg_extractions = generate_arg_extraction(&method.args);
|
||||
let arg_passings = generate_arg_passing(&method.args);
|
||||
|
||||
|
||||
quote! {
|
||||
.method(#method_name, |args| {
|
||||
.method(#method_name, #signature, |args| {
|
||||
#(#arg_extractions)*
|
||||
let this = mize::prelude::get_global_mize()?
|
||||
.get_config(#name)?
|
||||
@@ -281,28 +328,43 @@ fn expand_block_include_native(block: IncludeNativeBlock) -> TokenStream {
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let associated_calls = associated.iter().map(|func| {
|
||||
let func_name = func.name.to_string();
|
||||
let func_ident = &func.name;
|
||||
let signature = synthesize_signature(&func.args);
|
||||
let arg_extractions = generate_arg_extraction(&func.args);
|
||||
let arg_passings = generate_arg_passing(&func.args);
|
||||
|
||||
|
||||
quote! {
|
||||
.method(#func_name, |args| {
|
||||
.method(#func_name, #signature, |args| {
|
||||
#(#arg_extractions)*
|
||||
let result = <#type_path>::#func_ident(#(#arg_passings),*);
|
||||
Ok(mize::item::ItemData::from_native(result)?)
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let expanded = quote! {
|
||||
mize::config::opt(#name)
|
||||
#(#associated_calls)*
|
||||
#(#method_calls)*
|
||||
{
|
||||
if let Ok(mize) = mize::prelude::get_global_mize() {
|
||||
if mize.is_generate_mode().unwrap_or(false) {
|
||||
let _ = mize.record_native(
|
||||
#name,
|
||||
#type_name_str,
|
||||
vec![#(#associated_names.to_string()),*],
|
||||
vec![#(#method_names.to_string()),*],
|
||||
vec![#(#associated_sigs.to_string()),*],
|
||||
vec![#(#method_sigs.to_string()),*],
|
||||
);
|
||||
}
|
||||
}
|
||||
mize::config::opt(#name)
|
||||
#(#associated_calls)*
|
||||
#(#method_calls)*
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
|
||||
149
md
149
md
@@ -1,149 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
MIZE_SRC="/home/me/work/config/mize" # hardcoded for now
|
||||
RSYNC_EXCLUDES="--exclude=node_modules --exclude=target --exclude=mize_config.toml --exclude=rust_dist --exclude=deno_dist --exclude=dist --exclude=result --exclude=pkg"
|
||||
|
||||
function main {
|
||||
if [[ "$1" == "gen" ]]; then
|
||||
generate_sources
|
||||
fi
|
||||
|
||||
if [[ "$1" == "dsp" ]]; then
|
||||
deploy_spacetime
|
||||
fi
|
||||
|
||||
if [[ "$1" == "dlu" ]]; then
|
||||
deploy_lush
|
||||
fi
|
||||
|
||||
if [[ "$1" == "dma" ]]; then
|
||||
deploy_main
|
||||
fi
|
||||
|
||||
if [[ "$1" == "dmac" ]]; then
|
||||
deploy_mac
|
||||
fi
|
||||
|
||||
if [[ "$1" == "dpw" ]]; then
|
||||
deploy_ppc_website
|
||||
fi
|
||||
|
||||
if [[ "$1" == "dmo" ]]; then
|
||||
deploy_main_obsidian
|
||||
fi
|
||||
}
|
||||
|
||||
function deploy_spacetime {
|
||||
spacetime publish -p $MIZE_SRC/packages/spacetime
|
||||
}
|
||||
|
||||
function generate_sources {
|
||||
echo "mize: Generating sources..."
|
||||
mkdir -p $MIZE_SRC/packages/generated/src/spacetime
|
||||
|
||||
# Generate Cargo.toml and lib.rs
|
||||
cat << 'EOF' > $MIZE_SRC/packages/generated/Cargo.toml
|
||||
[package]
|
||||
name = "generated"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "generated"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
spacetimedb-sdk = "2.1.0"
|
||||
EOF
|
||||
echo -e 'pub mod spacetime;\n' > $MIZE_SRC/packages/generated/src/lib.rs
|
||||
|
||||
# Generate spacetime client bindings
|
||||
spacetime generate -p $MIZE_SRC/packages/spacetime -o $MIZE_SRC/packages/generated/src/spacetime --lang rust
|
||||
}
|
||||
|
||||
function deploy_main {
|
||||
mkdir -p ~/here/mize/lib
|
||||
cargo build --manifest-path $MIZE_SRC/packages/mize/Cargo.toml
|
||||
cargo build --manifest-path $MIZE_SRC/packages/ppc/Cargo.toml --features desktop
|
||||
|
||||
systemctl --user stop mize
|
||||
|
||||
cp $MIZE_SRC/target/debug/mize ~/here/mize/
|
||||
cp $MIZE_SRC/target/debug/libppc.so ~/here/mize/lib/libppc_part.so
|
||||
cp $MIZE_SRC/md ~/here/mize/
|
||||
cp $MIZE_SRC/target/debug/ppc ~/here/mize/
|
||||
|
||||
systemctl --user start mize
|
||||
}
|
||||
|
||||
function deploy_mac {
|
||||
echo "c2vi: Deploying to mac..."
|
||||
|
||||
# Copy files to mac
|
||||
rsync -r -u -v -a --delete --include=".git/" --include=".git/**" --mkpath $RSYNC_EXCLUDES $MIZE_SRC mac:here
|
||||
|
||||
# compile
|
||||
ssh mac << EOF
|
||||
if ! tmux has-session -t ppc-deploy 2>/dev/null; then
|
||||
echo "Created new tmux session...."
|
||||
tmux new-session -d -s ppc-deploy "nix develop git+file:///home/me/here/mize#buildShell"
|
||||
tmux send-keys -t ppc-deploy "cargo build --manifest-path /home/me/here/mize/packages/mize/Cargo.toml" Enter
|
||||
tmux send-keys -t ppc-deploy "cargo build --features desktop --manifest-path /home/me/here/mize/packages/ppc/Cargo.toml" Enter
|
||||
tmux send-keys -t ppc-deploy "systemctl --user restart mize" Enter
|
||||
else
|
||||
echo "using existing session..."
|
||||
tmux send-keys -t ppc-deploy "cargo build --manifest-path /home/me/here/mize/packages/mize/Cargo.toml" Enter
|
||||
tmux send-keys -t ppc-deploy "cargo build --features desktop --manifest-path /home/me/here/mize/packages/ppc/Cargo.toml" Enter
|
||||
tmux send-keys -t ppc-deploy "systemctl --user restart mize" Enter
|
||||
fi
|
||||
EOF
|
||||
}
|
||||
|
||||
function deploy_lush {
|
||||
echo "c2vi: Deploying to lush..."
|
||||
|
||||
# Copy files to lush
|
||||
rsync -r -u -v -a --delete --include=".git/" --include=".git/**" --mkpath $RSYNC_EXCLUDES $MIZE_SRC lush:here
|
||||
|
||||
# compile
|
||||
ssh lush << EOF
|
||||
if ! tmux has-session -t ppc-deploy 2>/dev/null; then
|
||||
echo "Created new tmux session...."
|
||||
tmux new-session -d -s ppc-deploy "nix develop git+file:///home/me/here/mize#buildShell"
|
||||
tmux send-keys -t ppc-deploy "cargo build --manifest-path /home/me/here/mize/packages/mize/Cargo.toml" Enter
|
||||
tmux send-keys -t ppc-deploy "cargo build --features desktop --manifest-path /home/me/here/mize/packages/ppc/Cargo.toml" Enter
|
||||
tmux send-keys -t ppc-deploy "systemctl --user restart mize" Enter
|
||||
else
|
||||
echo "using existing session..."
|
||||
tmux send-keys -t ppc-deploy "cargo build --manifest-path /home/me/here/mize/packages/mize/Cargo.toml" Enter
|
||||
tmux send-keys -t ppc-deploy "cargo build --features desktop --manifest-path /home/me/here/mize/packages/ppc/Cargo.toml" Enter
|
||||
tmux send-keys -t ppc-deploy "systemctl --user restart mize" Enter
|
||||
fi
|
||||
EOF
|
||||
|
||||
}
|
||||
|
||||
function deploy_ppc_website {
|
||||
echo "c2vi: Deploying ppc website to fes"
|
||||
|
||||
# compile
|
||||
cargo build --release --no-default-features --features server --manifest-path $MIZE_SRC/packages/ppc/Cargo.toml
|
||||
|
||||
ssh fes rm /home/server/here/ppc
|
||||
# upload
|
||||
scp $MIZE_SRC/target/release/ppc fes:here
|
||||
|
||||
# restart service
|
||||
ssh fe "sudo systemctl restart ppc"
|
||||
}
|
||||
|
||||
function deploy_main_obsidian {
|
||||
echo "c2vi: Deploying to local main obsidian vault..."
|
||||
|
||||
deno run --config $MIZE_SRC/packages/ppc/platform/obsidian/deno.json build
|
||||
|
||||
cp -r $MIZE_SRC/packages/ppc/platform/obsidian/dist/* ~/work/things/.obsidian/plugins/ppc/
|
||||
}
|
||||
|
||||
main "$@"
|
||||
39
parts/auth.client/Cargo.toml
Normal file
39
parts/auth.client/Cargo.toml
Normal file
@@ -0,0 +1,39 @@
|
||||
cargo-features = ["profile-rustflags"]
|
||||
|
||||
[package]
|
||||
name = "mize-part-auth-client"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "mize_part_auth_client"
|
||||
crate-type = [ "cdylib", "rlib" ]
|
||||
|
||||
[features]
|
||||
default = [ "target-os" ]
|
||||
target-os = [ "webbrowser" ]
|
||||
target-js = []
|
||||
target-obsidian = [ "target-js" ]
|
||||
|
||||
[dependencies]
|
||||
mize = { workspace = true }
|
||||
log = "0.4.34"
|
||||
mize-generated-auth = { path = "./generated" }
|
||||
webbrowser = { version = "1.2.0", optional = true }
|
||||
serde = "1.0.228"
|
||||
serde_json = "1.0.149"
|
||||
tokio = { version = "1.49.0", default-features = false, features = ["time"] }
|
||||
openidconnect = { version = "4", features = ["reqwest", "rustls-tls"], default-features = false }
|
||||
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots"] }
|
||||
chrono = { version = "0.4.45", features = [], default-features = false }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[profile.dev]
|
||||
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
|
||||
|
||||
[profile.release]
|
||||
rustflags = ["-C", "prefer-dynamic"]
|
||||
11
parts/auth.client/generated/Cargo.toml
Normal file
11
parts/auth.client/generated/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "mize-generated-auth"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "mize_generated_auth"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
mize = { workspace = true }
|
||||
92
parts/auth.client/generated/src/lib.rs
Normal file
92
parts/auth.client/generated/src/lib.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
// AUTO-GENERATED by mize dev gen -- do not edit manually
|
||||
use mize::prelude::*;
|
||||
use mize::config::MizeFun;
|
||||
|
||||
|
||||
pub struct CliPart {
|
||||
id: MizeId
|
||||
}
|
||||
|
||||
impl MizePart for CliPart {
|
||||
fn id(&self) -> MizeResult<MizeId> {
|
||||
Ok(self.id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl CliPart {
|
||||
pub fn get(name: &str) -> MizeResult<CliPart> {
|
||||
let mut mize = get_global_mize()?;
|
||||
let part = mize.get_part(name)?;
|
||||
Ok(CliPart { id: part.id()? })
|
||||
}
|
||||
|
||||
pub fn set_command(&self, arg0: &str) -> MizeResult<()> {
|
||||
let mut args = ItemData::new();
|
||||
args.set_path("0", arg0.into_item_data())?;
|
||||
dyn_call(self.id.clone(), "set_command", args)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn subcommand(&self, arg0: Command, arg1: MizeFun) -> MizeResult<()> {
|
||||
let mut args = ItemData::new();
|
||||
let mut __arg0 = ItemData::new();
|
||||
__arg0.set_path("_native", arg0.native_num)?;
|
||||
args.set_path("0", __arg0)?;
|
||||
args.set_path("1", ItemData::fun(arg1)?)?;
|
||||
dyn_call(self.id.clone(), "subcommand", args)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run(&self, arg0: Vec<String>) -> MizeResult<()> {
|
||||
let mut args = ItemData::new();
|
||||
args.set_path("0", arg0.into_item_data())?;
|
||||
dyn_call(self.id.clone(), "run", args)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn opt_state(&self) -> MizeResult<ItemData> {
|
||||
get_config(&format!("{}/state", self.id.dot()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// native type: Command (clap::Command)
|
||||
pub struct Command {
|
||||
pub native_num: u32
|
||||
}
|
||||
|
||||
impl FromItemData for Command {
|
||||
fn from_itemdata(data: ItemData) -> MizeResult<Self> {
|
||||
Ok(Command { native_num: data.get_path("_native")?.value_u32()? })
|
||||
}
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn new(arg0: &str) -> MizeResult<Command> {
|
||||
let mut args = ItemData::new();
|
||||
args.set_path("0", arg0.into_item_data())?;
|
||||
let mut mize = get_global_mize()?;
|
||||
let id = mize.id_new_dots("cli.Command")?;
|
||||
let data = dyn_call(id, "new", args)?;
|
||||
let native_num: u32 = data.get_path("_native")?.value_u32()?;
|
||||
Ok(Command { native_num })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// native type: ArgMatches (clap::ArgMatches)
|
||||
pub struct ArgMatches {
|
||||
pub native_num: u32
|
||||
}
|
||||
|
||||
impl FromItemData for ArgMatches {
|
||||
fn from_itemdata(data: ItemData) -> MizeResult<Self> {
|
||||
Ok(ArgMatches { native_num: data.get_path("_native")?.value_u32()? })
|
||||
}
|
||||
}
|
||||
|
||||
impl ArgMatches {
|
||||
}
|
||||
|
||||
// part id string: cli
|
||||
|
||||
2
parts/auth.client/part
Normal file
2
parts/auth.client/part
Normal file
@@ -0,0 +1,2 @@
|
||||
id = "auth.client"
|
||||
gen = [ "cli" ]
|
||||
@@ -1,22 +1,16 @@
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use mize::config::OptVal;
|
||||
use mize::config::thread;
|
||||
use mize::include_native;
|
||||
use mize::prelude::*;
|
||||
use mize::out;
|
||||
use mize::{
|
||||
item::ItemData, mize_err, mize_part, Mize, MizeError, MizePart, MizePartCreate, MizeResult,
|
||||
};
|
||||
use openidconnect::{
|
||||
ClientId, DeviceAuthorizationResponse, EmptyExtraDeviceAuthorizationFields, IssuerUrl,
|
||||
OAuth2TokenResponse, RefreshToken, Scope, TokenResponse,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::runtime::Runtime;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::{path::PathBuf, thread};
|
||||
use tracing::{info, warn};
|
||||
use mize_part_cli::CliPartState;
|
||||
use mize_generated_auth::*;
|
||||
|
||||
// console_log macro
|
||||
// that can be copied into other files for debugging purposes
|
||||
@@ -37,16 +31,6 @@ macro_rules! console_log {
|
||||
($($t:tt)*) => (unsafe { log(&format_args!($($t)*).to_string())})
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
use mize_part_cli::clap::Command;
|
||||
|
||||
#[derive(Default)]
|
||||
#[mize_part]
|
||||
pub struct ClientAuth {
|
||||
mize: Mize,
|
||||
secrets: Option<ClientSecrets>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ClientSecrets {
|
||||
id_token: String,
|
||||
@@ -55,14 +39,6 @@ pub struct ClientSecrets {
|
||||
expiry: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl MizePartCreate for ClientAuth {}
|
||||
|
||||
impl MizePart for ClientAuth {
|
||||
fn name(&self) -> &'static str {
|
||||
"client_auth"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-js")]
|
||||
pub fn store_secrets_js(mize: &Mize, secrets: &ClientSecrets) -> MizeResult<()> {
|
||||
use web_sys::window;
|
||||
@@ -85,6 +61,7 @@ pub fn store_secrets_js(mize: &Mize, secrets: &ClientSecrets) -> MizeResult<()>
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
impl ClientAuth {
|
||||
pub fn secrets(&self) -> Option<&ClientSecrets> {
|
||||
self.secrets.as_ref()
|
||||
@@ -173,20 +150,59 @@ impl ClientAuth {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
async fn test() {
|
||||
let client = reqwest::Client::builder().tls_built_in_root_certs(true).no_proxy().build().unwrap();
|
||||
let req = client.get("https://orf.at").build().unwrap();
|
||||
let res = client.execute(req).await.unwrap();
|
||||
println!("res: {:?}", res);
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn part_auth() -> MizeResult<()> {
|
||||
pub extern "C" fn part_auth_client() -> MizeResult<()> {
|
||||
|
||||
opt("cred")
|
||||
.fun(|| {
|
||||
let mize = get_global_mize()?;
|
||||
let secrets = do_login()?;
|
||||
ItemData::from_serializable(secrets)
|
||||
});
|
||||
opt("secrets")
|
||||
.fun(Box::new(|| {
|
||||
let mut mize = get_global_mize()?;
|
||||
if let Ok(secrets) = load_secrets(&mut mize) {
|
||||
if secrets.expiry < Utc::now() {
|
||||
if let Ok(fresh_secrets) = mize.spawn_async_blocking("refresh", refresh_flow(mize.clone(), secrets.clone())) {
|
||||
return ItemData::from_serializable(Some(fresh_secrets));
|
||||
}
|
||||
}
|
||||
return ItemData::from_serializable(Some(secrets));
|
||||
} else {
|
||||
return ItemData::from_serializable::<Option<ClientSecrets>>(None);
|
||||
}
|
||||
}));
|
||||
|
||||
let mize = get_global_mize()?;
|
||||
method("login", "() -> ()", |_args| {
|
||||
#[cfg(feature = "target-os")] {
|
||||
println!("hiiiii");
|
||||
let mut mize = get_global_mize()?;
|
||||
|
||||
// i hate rust.
|
||||
let runtime = Runtime::new()?;
|
||||
let guard = runtime.enter();
|
||||
std::mem::forget(guard);
|
||||
|
||||
println!("hiiiii");
|
||||
match do_login() {
|
||||
Ok(secrets) => {
|
||||
store_secrets(&mut mize, &secrets)?;
|
||||
notify_mme_login_state(true)?;
|
||||
}
|
||||
Err(err) => {
|
||||
notify_mme_login_state(false)?;
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ItemData::new())
|
||||
});
|
||||
|
||||
let mut mize = get_global_mize()?;
|
||||
|
||||
// start refresh tread if configured
|
||||
if mize
|
||||
@@ -195,41 +211,21 @@ pub extern "C" fn part_auth() -> MizeResult<()> {
|
||||
.value_bool()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
thread("auth.refresh_thread", start_refresh_thread());
|
||||
thread("auth.refresh_thread", start_refresh_thread);
|
||||
}
|
||||
|
||||
let cli = get_part_gen::<generated::CliPart>("cli");
|
||||
|
||||
|
||||
|
||||
// cli command for os
|
||||
#[cfg(feature = "target-os")] {
|
||||
println!("hasdfaslkjfasldkfj");
|
||||
let cli_dyn = mize.get_part("cli")?;
|
||||
println!("hasdfaslkjfasldkfj three ");
|
||||
let mut cli = mize_part_cli::downcast_cli_part(cli_dyn)?;
|
||||
println!("hasdfaslkjfasldkfj two ");
|
||||
let cli = CliPart::get("cli")?;
|
||||
//let mut cli = mize.get_part_native::<CliPart>("cli")?;
|
||||
cli.set_command("ppc");
|
||||
cli.subcommand(Command::new("login"), move |_, mut mize| {
|
||||
let mut auth = mize.get_part_native::<ClientAuth>("client_auth")?;
|
||||
let secrets = if auth.secrets.is_some() {
|
||||
match mize.spawn_async_blocking(
|
||||
"client_auth",
|
||||
refresh_flow(mize.clone(), auth.secrets.clone().unwrap()),
|
||||
) {
|
||||
Ok(secrets) => secrets,
|
||||
Err(err) => {
|
||||
out!("err refreshing secrets: {:?}", err);
|
||||
mize.spawn_async_blocking("client_auth", auth_flow(mize_clone.clone()))?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mize.spawn_async_blocking("client_auth", auth_flow(mize_clone.clone()))?
|
||||
};
|
||||
store_secrets(&mut mize, &secrets)?;
|
||||
auth.secrets = Some(secrets);
|
||||
Ok(())
|
||||
})?;
|
||||
cli.subcommand(Command::new("login")?, Box::new(|_| {
|
||||
let mut mize = get_global_mize()?;
|
||||
dyn_call(mize.id_new_dots("auth.client")?, "login", ItemData::new())?;
|
||||
Ok(ItemData::new())
|
||||
}))?;
|
||||
}
|
||||
|
||||
|
||||
@@ -251,24 +247,20 @@ pub extern "C" fn part_auth() -> MizeResult<()> {
|
||||
})?;
|
||||
}
|
||||
|
||||
|
||||
auth_part.secrets = secrets;
|
||||
|
||||
mize.register_part(Box::new(auth_part))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start_refresh_thread() -> MizeResult<()> {
|
||||
pub fn start_refresh_thread(_args: ItemData) -> MizeResult<ItemData> {
|
||||
|
||||
// background thread for os targets
|
||||
#[cfg(feature = "target-os")] {
|
||||
let mut mize_clone = self.mize.clone();
|
||||
self.mize.spawn_background("auth.refresh", move || loop {
|
||||
let mut mize = get_global_mize()?;
|
||||
mize.spawn_background("auth.refresh", move || loop {
|
||||
let mut mize = get_global_mize()?;
|
||||
info!("hi from auth.refresh thread...");
|
||||
thread::sleep(Duration::from_secs(33)); // TERRIBLE hack
|
||||
let auth = mize_clone.get_part_native::<ClientAuth>("client_auth")?;
|
||||
let expiry = auth.secrets.as_ref().unwrap().expiry.clone();
|
||||
let secrets = get_config("self.secrets")?.native::<ClientSecrets>()?;
|
||||
let expiry = secrets.expiry.clone();
|
||||
info!("auth.refresh thread: now: {}", Utc::now());
|
||||
info!("auth.refresh thread: expiry: {}", expiry);
|
||||
let to_sleep = expiry - Utc::now() - TimeDelta::minutes(20);
|
||||
@@ -279,17 +271,13 @@ pub fn start_refresh_thread() -> MizeResult<()> {
|
||||
}
|
||||
}
|
||||
*/
|
||||
drop(auth);
|
||||
|
||||
info!("auth.refresh: thread going to sleep for: {}", to_sleep);
|
||||
thread::sleep(to_sleep.to_std()?);
|
||||
|
||||
info!("refresh_thread: sleeping done");
|
||||
let mut auth = mize_clone.get_part_native::<ClientAuth>("auth")?;
|
||||
auth.refresh()?;
|
||||
drop(auth);
|
||||
dyn_call(mize.id_new_dots("auth.client")?, "refresh", ItemData::new())?;
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -345,25 +333,52 @@ pub fn start_refresh_thread() -> MizeResult<()> {
|
||||
}
|
||||
*/
|
||||
|
||||
Ok(())
|
||||
Ok(ItemData::new())
|
||||
}
|
||||
|
||||
fn do_login() -> MizeResult<ClientSecrets> {
|
||||
let mut mize = get_global_mize()?;
|
||||
let mut secrets;
|
||||
if let Ok(val) = load_secrets(&mut mize) {
|
||||
if val.clone().unwrap().expiry < Utc::now() {
|
||||
if val.clone().expiry < Utc::now() {
|
||||
// refresh if secrets are expired
|
||||
secrets = mize.spawn_async_blocking("auth.refresh", refresh_flow(mize, val))?;
|
||||
secrets = match mize.spawn_async_blocking("auth.refresh", refresh_flow(mize.clone(), val)) {
|
||||
Ok(secrets) => secrets,
|
||||
Err(err) => {
|
||||
out!("err refreshing secrets: {:?}", err);
|
||||
mize.spawn_async_blocking("auth.login", auth_flow(mize.clone()))?
|
||||
}
|
||||
};
|
||||
} else {
|
||||
secrets = val;
|
||||
}
|
||||
} else {
|
||||
secrets = mize.spawn_async_blocking("auth.login", auth_flow(mize))?;
|
||||
secrets = mize.spawn_async_blocking("auth.login", auth_flow(mize.clone()))?;
|
||||
}
|
||||
Ok(secrets)
|
||||
}
|
||||
|
||||
fn notify_mme_url_and_code(url: &str, code: &str) -> MizeResult<()> {
|
||||
let mut mize = get_global_mize()?;
|
||||
if mize.has_part("mme")? {
|
||||
let mut args = ItemData::new();
|
||||
args.set_path("0", url.to_owned().into_item_data())?;
|
||||
args.set_path("1", code.to_owned().into_item_data())?;
|
||||
dyn_call(mize.id_new_dots("mme")?, "set_url_and_code", args)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notify_mme_login_state(logged_in: bool) -> MizeResult<()> {
|
||||
let mut mize = get_global_mize()?;
|
||||
if mize.has_part("mme")? {
|
||||
let mut args = ItemData::new();
|
||||
args.set_path("0", logged_in.into_item_data())?;
|
||||
dyn_call(mize.id_new_dots("mme")?, "set_login_state", args)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_secrets(mize: &mut Mize) -> MizeResult<ClientSecrets> {
|
||||
#[cfg(feature = "target-os")] {
|
||||
let mize_dir = mize.get_config("data_dir")?.value_string()?;
|
||||
@@ -517,7 +532,8 @@ async fn auth_flow(mut mize: Mize) -> MizeResult<ClientSecrets> {
|
||||
let issuer = IssuerUrl::new(issuer)?;
|
||||
let client_id = ClientId::new(client_id);
|
||||
|
||||
let http_client = HttpClient::new();
|
||||
let http_client = openidconnect::reqwest::Client::builder().tls_built_in_root_certs(true).build()?;
|
||||
|
||||
|
||||
// Teach openidconnect-rs about the device authorization url.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
@@ -550,7 +566,7 @@ async fn auth_flow(mut mize: Mize) -> MizeResult<ClientSecrets> {
|
||||
|
||||
// Discover provider metadata
|
||||
let provider_metadata: MyProviderMetadata =
|
||||
MyProviderMetadata::discover_async(issuer, &http_client).await?;
|
||||
MyProviderMetadata::discover_async(issuer, &http_client).await.map_err(|e| mize_err!("From reqwest::error::Error: {e}"))?;
|
||||
|
||||
let device_auth_url = provider_metadata
|
||||
.additional_metadata()
|
||||
@@ -601,18 +617,23 @@ async fn auth_flow(mut mize: Mize) -> MizeResult<ClientSecrets> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-os")] {
|
||||
let url = details
|
||||
.verification_uri_complete()
|
||||
.map(|u| u.secret().clone())
|
||||
.unwrap_or_default();
|
||||
let code = details.user_code().secret().clone();
|
||||
|
||||
let _ = notify_mme_url_and_code(&url, &code);
|
||||
|
||||
out!(
|
||||
"If your browser does not open automatically, open this URL in it:\n{}\n",
|
||||
details.verification_uri_complete().unwrap().secret()
|
||||
);
|
||||
out!(
|
||||
"Or enter this PPC login code:\n{}\n",
|
||||
details.user_code().secret()
|
||||
url
|
||||
);
|
||||
out!("Or enter this PPC login code:\n{}\n", code);
|
||||
|
||||
if let Some(_) = details.verification_uri_complete() {
|
||||
let _ = webbrowser::open(details.verification_uri_complete().unwrap().secret());
|
||||
}
|
||||
if !url.is_empty() {
|
||||
let _ = webbrowser::open(&url);
|
||||
}
|
||||
}
|
||||
|
||||
let token_response = client
|
||||
@@ -1,6 +0,0 @@
|
||||
|
||||
pub mod auth {
|
||||
pub mod client;
|
||||
pub use client::*;
|
||||
}
|
||||
pub use auth::*;
|
||||
@@ -11,6 +11,8 @@ pub extern "C" fn part_cli() -> MizeResult<()> {
|
||||
//methods: [arg(arg: clap::Arg), subcommand(cmd: clap::Command)]
|
||||
});
|
||||
|
||||
include_native!("ArgMatches", clap::ArgMatches, {});
|
||||
|
||||
opt("state")
|
||||
.fun(Box::new(|| {
|
||||
let mize = get_global_mize().unwrap();
|
||||
@@ -25,7 +27,7 @@ pub extern "C" fn part_cli() -> MizeResult<()> {
|
||||
}));
|
||||
|
||||
|
||||
method("set_command", |args| {
|
||||
method("set_command", "String -> ()", |args| {
|
||||
let mut state = get_config("self.state")?.native_mut::<CliPartState>()?;
|
||||
let cmd = args.get_path("0")?.native::<String>()?;
|
||||
state.current_cmd_name = cmd.to_owned();
|
||||
@@ -36,7 +38,7 @@ pub extern "C" fn part_cli() -> MizeResult<()> {
|
||||
Ok(ItemData::new())
|
||||
});
|
||||
|
||||
method("subcommand", |args| {
|
||||
method("subcommand", "Command, Func -> ()", |args| {
|
||||
let mut state = get_config("self.state")?.native_mut::<CliPartState>()?;
|
||||
let subcmd = args.get_path("0")?.native_take::<clap::Command>()?;
|
||||
let action = args.get_path("1")?;
|
||||
@@ -52,7 +54,7 @@ pub extern "C" fn part_cli() -> MizeResult<()> {
|
||||
Ok(ItemData::new())
|
||||
});
|
||||
|
||||
method("run", |args| {
|
||||
method("run", "Vec<String> -> ()", |args| {
|
||||
let cmdline = args.get_path("0")?.native_take::<Vec<String>>()?;
|
||||
let mut state = get_config("self.state")?.native_mut::<CliPartState>()?;
|
||||
let cmd_name = cmdline.iter().nth(0).ok_or(mize_err!("empty cmdline"))?;
|
||||
|
||||
32
parts/mize.dev/docker/base/Dockerfile
Normal file
32
parts/mize.dev/docker/base/Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
FROM rust:1.95.0
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
RUN apt-get install -y \
|
||||
mingw-w64 \
|
||||
build-essential \
|
||||
gcc-arm-linux-gnueabi \
|
||||
g++-arm-linux-gnueabi
|
||||
|
||||
RUN apt-get install -y \
|
||||
clang \
|
||||
lld \
|
||||
llvm
|
||||
|
||||
# Rust cross-compilation targets
|
||||
RUN rustup target add --toolchain nightly-2026-01-30 x86_64-pc-windows-gnu
|
||||
|
||||
RUN rustup target add --toolchain nightly-2026-01-30 aarch64-pc-windows-gnullvm
|
||||
|
||||
RUN rustup target add --toolchain nightly-2026-01-30 x86_64-unknown-linux-gnu
|
||||
|
||||
RUN rustup target add --toolchain nightly-2026-01-30 aarch64-unknown-linux-gnu
|
||||
|
||||
RUN rustup target add --toolchain nightly-2026-01-30 armv7-unknown-linux-gnueabihf
|
||||
|
||||
RUN rustup target add --toolchain nightly-2026-01-30 arm-unknown-linux-gnueabi
|
||||
|
||||
# Set default Rust toolchain
|
||||
RUN rustup default nightly-2026-01-30
|
||||
|
||||
WORKDIR /src
|
||||
8
parts/mize.dev/docker/darwin/Dockerfile
Normal file
8
parts/mize.dev/docker/darwin/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM joseluisq/rust-linux-darwin-builder:2.0.0-beta.1
|
||||
|
||||
RUN rustup target add --toolchain nightly-2026-01-30 x86_64-apple-darwin
|
||||
RUN rustup target add --toolchain nightly-2026-01-30 aarch64-apple-darwin
|
||||
|
||||
RUN rustup default nightly-2026-01-30
|
||||
|
||||
WORKDIR /src
|
||||
@@ -4,7 +4,8 @@
|
||||
//! a Rust source file that looks like the hand-written `generated.rs`.
|
||||
|
||||
use mize::prelude::*;
|
||||
use mize::instance::{PartMetadata, MethodMetadata, OptMetadata, NativeMetadata};
|
||||
use mize::instance::{PartMetadata, MethodMetadata, OptMetadata, NativeMetadata, parse_method_signature};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
/// Convert a part name like "cli" into a PascalCase struct name like "CliPart".
|
||||
@@ -31,7 +32,8 @@ fn part_name_to_struct(part_name: &str) -> String {
|
||||
pub fn generate_lib_rs(all_metadata: &[PartMetadata]) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("// AUTO-GENERATED by mize dev gen -- do not edit manually\n");
|
||||
out.push_str("use mize::prelude::*;\n\n\n");
|
||||
out.push_str("use mize::prelude::*;\n");
|
||||
out.push_str("use mize::config::MizeFun;\n\n\n");
|
||||
|
||||
for meta in all_metadata {
|
||||
generate_part_struct(&mut out, meta);
|
||||
@@ -40,9 +42,28 @@ pub fn generate_lib_rs(all_metadata: &[PartMetadata]) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
fn native_names(meta: &PartMetadata) -> HashSet<String> {
|
||||
meta.natives.iter().map(|n| n.name.clone()).collect()
|
||||
}
|
||||
|
||||
fn native_fn_names(meta: &PartMetadata) -> HashSet<String> {
|
||||
let mut names = HashSet::new();
|
||||
for n in &meta.natives {
|
||||
for a in &n.associated {
|
||||
names.insert(a.clone());
|
||||
}
|
||||
for m in &n.methods {
|
||||
names.insert(m.clone());
|
||||
}
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
fn generate_part_struct(out: &mut String, meta: &PartMetadata) {
|
||||
let struct_name = part_name_to_struct(&meta.part_name);
|
||||
let part_id_str = meta.part_name.replace('.', ".");
|
||||
let natives = native_names(meta);
|
||||
let native_fns = native_fn_names(meta);
|
||||
|
||||
// struct definition
|
||||
let _ = writeln!(out, "pub struct {struct_name} {{");
|
||||
@@ -52,8 +73,8 @@ fn generate_part_struct(out: &mut String, meta: &PartMetadata) {
|
||||
|
||||
// MizePart impl
|
||||
let _ = writeln!(out, "impl MizePart for {struct_name} {{");
|
||||
let _ = writeln!(out, " fn id(&self) -> MizeId {{");
|
||||
let _ = writeln!(out, " self.id.clone()");
|
||||
let _ = writeln!(out, " fn id(&self) -> MizeResult<MizeId> {{");
|
||||
let _ = writeln!(out, " Ok(self.id.clone())");
|
||||
let _ = writeln!(out, " }}");
|
||||
let _ = writeln!(out, "}}");
|
||||
let _ = writeln!(out);
|
||||
@@ -65,16 +86,23 @@ fn generate_part_struct(out: &mut String, meta: &PartMetadata) {
|
||||
let _ = writeln!(out, " pub fn get(name: &str) -> MizeResult<{struct_name}> {{");
|
||||
let _ = writeln!(out, " let mut mize = get_global_mize()?;");
|
||||
let _ = writeln!(out, " let part = mize.get_part(name)?;");
|
||||
let _ = writeln!(out, " Ok({struct_name} {{ id: part.id() }})");
|
||||
let _ = writeln!(out, " Ok({struct_name} {{ id: part.id()? }})");
|
||||
let _ = writeln!(out, " }}");
|
||||
let _ = writeln!(out);
|
||||
|
||||
// one method per recorded method/effect/thread
|
||||
// part methods (skip ones that belong to include_native)
|
||||
for m in meta.methods.iter().chain(meta.effects.iter()).chain(meta.threads.iter()) {
|
||||
generate_method_fn(out, &struct_name, m);
|
||||
if native_fns.contains(&m.name) {
|
||||
continue;
|
||||
}
|
||||
generate_typed_method_fn(out, &struct_name, m);
|
||||
}
|
||||
|
||||
// one getter-ish fn per recorded opt (opts are config values; expose a dyn_call getter)
|
||||
// opts that are not natives
|
||||
for o in &meta.opts {
|
||||
if natives.contains(&o.name) || o.name.starts_with('_') {
|
||||
continue;
|
||||
}
|
||||
generate_opt_fn(out, &struct_name, o);
|
||||
}
|
||||
|
||||
@@ -83,22 +111,143 @@ fn generate_part_struct(out: &mut String, meta: &PartMetadata) {
|
||||
|
||||
// native type wrappers
|
||||
for n in &meta.natives {
|
||||
generate_native_struct(out, n);
|
||||
generate_native_struct(out, &meta.part_name, n);
|
||||
}
|
||||
|
||||
let _ = writeln!(out, "// part id string: {part_id_str}");
|
||||
let _ = writeln!(out);
|
||||
}
|
||||
|
||||
fn generate_method_fn(out: &mut String, _struct_name: &str, m: &MethodMetadata) {
|
||||
/// Map a signature type name to a Rust parameter type for generated bindings.
|
||||
fn rust_param_type(type_name: &str) -> String {
|
||||
match type_name {
|
||||
"String" => "&str".to_string(),
|
||||
"u32" => "u32".to_string(),
|
||||
"Vec<String>" => "Vec<String>".to_string(),
|
||||
"Func" => "MizeFun".to_string(),
|
||||
"()" => "()".to_string(),
|
||||
other => other.to_string(), // native wrapper type
|
||||
}
|
||||
}
|
||||
|
||||
fn is_native_type(type_name: &str) -> bool {
|
||||
!matches!(
|
||||
type_name,
|
||||
"String" | "u32" | "Vec<String>" | "Func" | "()" | "ItemData" | ""
|
||||
)
|
||||
}
|
||||
|
||||
/// Emit packing of one typed arg into `args` at path index `i`.
|
||||
fn emit_arg_pack(out: &mut String, i: usize, type_name: &str, param_name: &str) {
|
||||
match type_name {
|
||||
"String" => {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" args.set_path(\"{i}\", {param_name}.into_item_data())?;"
|
||||
);
|
||||
}
|
||||
"u32" => {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" args.set_path(\"{i}\", {param_name}.into_item_data())?;"
|
||||
);
|
||||
}
|
||||
"Vec<String>" => {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" args.set_path(\"{i}\", {param_name}.into_item_data())?;"
|
||||
);
|
||||
}
|
||||
"Func" => {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" args.set_path(\"{i}\", ItemData::fun({param_name})?)?;"
|
||||
);
|
||||
}
|
||||
_ if is_native_type(type_name) => {
|
||||
let _ = writeln!(out, " let mut __arg{i} = ItemData::new();");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" __arg{i}.set_path(\"_native\", {param_name}.native_num)?;"
|
||||
);
|
||||
let _ = writeln!(out, " args.set_path(\"{i}\", __arg{i})?;");
|
||||
}
|
||||
_ => {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" args.set_path(\"{i}\", {param_name}.into_item_data())?;"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_typed_method_fn(
|
||||
out: &mut String,
|
||||
_struct_name: &str,
|
||||
m: &MethodMetadata,
|
||||
) {
|
||||
let fn_name = &m.name;
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" pub fn {fn_name}(&self, args: ItemData) -> MizeResult<ItemData> {{"
|
||||
);
|
||||
let _ = writeln!(out, " dyn_call(self.id.clone(), \"{fn_name}\", args)?;");
|
||||
// note: dyn_call returns ItemData already, so wrap:
|
||||
let _ = writeln!(out, " dyn_call(self.id.clone(), \"{fn_name}\", args)");
|
||||
let (arg_types, return_type) = if m.arg_types.is_empty() && m.return_type.is_empty() && !m.signature.is_empty() {
|
||||
parse_method_signature(&m.signature)
|
||||
} else {
|
||||
(m.arg_types.clone(), m.return_type.clone())
|
||||
};
|
||||
|
||||
let params: Vec<(String, String)> = arg_types
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, t)| (format!("arg{i}"), rust_param_type(t)))
|
||||
.collect();
|
||||
|
||||
let ret_ty = match return_type.as_str() {
|
||||
"()" => "()".to_string(),
|
||||
"" | "ItemData" => "ItemData".to_string(),
|
||||
other => other.to_string(),
|
||||
};
|
||||
|
||||
let param_list = params
|
||||
.iter()
|
||||
.map(|(n, t)| format!("{n}: {t}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
if param_list.is_empty() {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" pub fn {fn_name}(&self) -> MizeResult<{ret_ty}> {{"
|
||||
);
|
||||
} else {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" pub fn {fn_name}(&self, {param_list}) -> MizeResult<{ret_ty}> {{"
|
||||
);
|
||||
}
|
||||
|
||||
let _ = writeln!(out, " let mut args = ItemData::new();");
|
||||
for (i, t) in arg_types.iter().enumerate() {
|
||||
emit_arg_pack(out, i, t, &format!("arg{i}"));
|
||||
}
|
||||
|
||||
match ret_ty.as_str() {
|
||||
"()" => {
|
||||
let _ = writeln!(out, " dyn_call(self.id.clone(), \"{fn_name}\", args)?;");
|
||||
let _ = writeln!(out, " Ok(())");
|
||||
}
|
||||
"ItemData" => {
|
||||
let _ = writeln!(out, " dyn_call(self.id.clone(), \"{fn_name}\", args)");
|
||||
}
|
||||
native if is_native_type(native) => {
|
||||
let _ = writeln!(out, " let data = dyn_call(self.id.clone(), \"{fn_name}\", args)?;");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" Ok({native} {{ native_num: data.get_path(\"_native\")?.value_u32()? }})"
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
let _ = writeln!(out, " dyn_call(self.id.clone(), \"{fn_name}\", args)");
|
||||
}
|
||||
}
|
||||
|
||||
let _ = writeln!(out, " }}");
|
||||
let _ = writeln!(out);
|
||||
}
|
||||
@@ -114,40 +263,210 @@ fn generate_opt_fn(out: &mut String, _struct_name: &str, o: &OptMetadata) {
|
||||
let _ = writeln!(out);
|
||||
}
|
||||
|
||||
fn generate_native_struct(out: &mut String, n: &NativeMetadata) {
|
||||
fn generate_native_struct(out: &mut String, part_name: &str, n: &NativeMetadata) {
|
||||
let struct_name = &n.name;
|
||||
let _ = writeln!(out, "// native type: {} ({})", n.name, n.type_name);
|
||||
let _ = writeln!(out, "pub struct {struct_name} {{");
|
||||
let _ = writeln!(out, " native_id: u32");
|
||||
let _ = writeln!(out, " pub native_num: u32");
|
||||
let _ = writeln!(out, "}}");
|
||||
let _ = writeln!(out);
|
||||
|
||||
let _ = writeln!(out, "impl FromItemData for {struct_name} {{");
|
||||
let _ = writeln!(out, " fn from_itemdata(data: ItemData) -> MizeResult<Self> {{");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" Ok({struct_name} {{ native_num: data.get_path(\"_native\")?.value_u32()? }})"
|
||||
);
|
||||
let _ = writeln!(out, " }}");
|
||||
let _ = writeln!(out, "}}");
|
||||
let _ = writeln!(out);
|
||||
|
||||
let _ = writeln!(out, "impl {struct_name} {{");
|
||||
for assoc in &n.associated {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" pub fn {assoc}(args: ItemData) -> MizeResult<ItemData> {{"
|
||||
);
|
||||
let _ = writeln!(out, " let mut mize = get_global_mize()?;");
|
||||
let _ = writeln!(out, " let id = mize.id_new_dots(\"{}.{assoc}\")?;", n.name);
|
||||
let _ = writeln!(out, " dyn_call(id, \"{assoc}\", args)");
|
||||
let _ = writeln!(out, " }}");
|
||||
|
||||
for (i, assoc) in n.associated.iter().enumerate() {
|
||||
let sig = n.associated_sigs.get(i).map(|s| s.as_str()).unwrap_or("");
|
||||
generate_native_associated(out, part_name, n, assoc, sig);
|
||||
}
|
||||
for meth in &n.methods {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" pub fn {meth}(&self, args: ItemData) -> MizeResult<ItemData> {{"
|
||||
);
|
||||
let _ = writeln!(out, " let mut mize = get_global_mize()?;");
|
||||
let _ = writeln!(out, " let id = mize.id_new_dots(\"{}.{meth}\")?;", n.name);
|
||||
let _ = writeln!(out, " dyn_call(id, \"{meth}\", args)");
|
||||
let _ = writeln!(out, " }}");
|
||||
|
||||
for (i, meth) in n.methods.iter().enumerate() {
|
||||
let sig = n.method_sigs.get(i).map(|s| s.as_str()).unwrap_or("");
|
||||
generate_native_instance_method(out, part_name, n, meth, sig);
|
||||
}
|
||||
|
||||
let _ = writeln!(out, "}}");
|
||||
let _ = writeln!(out);
|
||||
}
|
||||
|
||||
fn generate_native_associated(out: &mut String, part_name: &str, n: &NativeMetadata, fn_name: &str, signature: &str) {
|
||||
let (arg_types, return_type) = parse_method_signature(signature);
|
||||
let ret_ty = if return_type.is_empty() {
|
||||
n.name.clone()
|
||||
} else {
|
||||
return_type
|
||||
};
|
||||
|
||||
let params: Vec<(String, String)> = arg_types
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, t)| (format!("arg{i}"), rust_param_type(t)))
|
||||
.collect();
|
||||
let param_list = params
|
||||
.iter()
|
||||
.map(|(name, ty)| format!("{name}: {ty}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
if param_list.is_empty() {
|
||||
let _ = writeln!(out, " pub fn {fn_name}() -> MizeResult<{ret_ty}> {{");
|
||||
} else {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" pub fn {fn_name}({param_list}) -> MizeResult<{ret_ty}> {{"
|
||||
);
|
||||
}
|
||||
|
||||
let _ = writeln!(out, " let mut args = ItemData::new();");
|
||||
for (i, t) in arg_types.iter().enumerate() {
|
||||
emit_arg_pack(out, i, t, &format!("arg{i}"));
|
||||
}
|
||||
let _ = writeln!(out, " let mut mize = get_global_mize()?;");
|
||||
let _ = writeln!(out, " let id = mize.id_new_dots(\"{part_name}.{}\")?;", n.name);
|
||||
|
||||
if ret_ty == "()" {
|
||||
let _ = writeln!(out, " dyn_call(id, \"{fn_name}\", args)?;");
|
||||
let _ = writeln!(out, " Ok(())");
|
||||
} else if ret_ty == "ItemData" {
|
||||
let _ = writeln!(out, " dyn_call(id, \"{fn_name}\", args)");
|
||||
} else if is_native_type(&ret_ty) || ret_ty == n.name {
|
||||
let _ = writeln!(out, " let data = dyn_call(id, \"{fn_name}\", args)?;");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" let native_num: u32 = data.get_path(\"_native\")?.value_u32()?;"
|
||||
);
|
||||
let _ = writeln!(out, " Ok({ret_ty} {{ native_num }})");
|
||||
} else {
|
||||
let _ = writeln!(out, " dyn_call(id, \"{fn_name}\", args)");
|
||||
}
|
||||
let _ = writeln!(out, " }}");
|
||||
let _ = writeln!(out);
|
||||
}
|
||||
|
||||
fn generate_native_instance_method(out: &mut String, part_name: &str, n: &NativeMetadata, fn_name: &str, signature: &str) {
|
||||
let (arg_types, return_type) = parse_method_signature(signature);
|
||||
let ret_ty = if return_type.is_empty() {
|
||||
"ItemData".to_string()
|
||||
} else {
|
||||
return_type
|
||||
};
|
||||
|
||||
let params: Vec<(String, String)> = arg_types
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, t)| (format!("arg{i}"), rust_param_type(t)))
|
||||
.collect();
|
||||
let param_list = params
|
||||
.iter()
|
||||
.map(|(name, ty)| format!("{name}: {ty}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
if param_list.is_empty() {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" pub fn {fn_name}(&self) -> MizeResult<{ret_ty}> {{"
|
||||
);
|
||||
} else {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" pub fn {fn_name}(&self, {param_list}) -> MizeResult<{ret_ty}> {{"
|
||||
);
|
||||
}
|
||||
|
||||
let _ = writeln!(out, " let mut args = ItemData::new();");
|
||||
for (i, t) in arg_types.iter().enumerate() {
|
||||
emit_arg_pack(out, i, t, &format!("arg{i}"));
|
||||
}
|
||||
let _ = writeln!(out, " let mut mize = get_global_mize()?;");
|
||||
let _ = writeln!(out, " let id = mize.id_new_dots(\"{part_name}.{}\")?;", n.name);
|
||||
|
||||
match ret_ty.as_str() {
|
||||
"()" => {
|
||||
let _ = writeln!(out, " dyn_call(id, \"{fn_name}\", args)?;");
|
||||
let _ = writeln!(out, " Ok(())");
|
||||
}
|
||||
"ItemData" => {
|
||||
let _ = writeln!(out, " dyn_call(id, \"{fn_name}\", args)");
|
||||
}
|
||||
native if is_native_type(native) => {
|
||||
let _ = writeln!(out, " let data = dyn_call(id, \"{fn_name}\", args)?;");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" Ok({native} {{ native_num: data.get_path(\"_native\")?.value_u32()? }})"
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
let _ = writeln!(out, " dyn_call(id, \"{fn_name}\", args)");
|
||||
}
|
||||
}
|
||||
let _ = writeln!(out, " }}");
|
||||
let _ = writeln!(out);
|
||||
}
|
||||
|
||||
/// Extract the `mize = ...` dependency assignment from a part's Cargo.toml.
|
||||
///
|
||||
/// Looks under `[dependencies]` (and `[dependencies.mize]` table form) and returns
|
||||
/// a single-line assignment suitable for writing into generated/Cargo.toml, e.g.
|
||||
/// `mize = { workspace = true }`.
|
||||
pub fn extract_mize_dependency(cargo_toml: &str) -> Result<String, String> {
|
||||
let mut in_deps = false;
|
||||
let mut in_mize_table = false;
|
||||
let mut mize_table_fields: Vec<String> = Vec::new();
|
||||
|
||||
for raw_line in cargo_toml.lines() {
|
||||
let line = raw_line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if line.starts_with('[') && line.ends_with(']') {
|
||||
if in_mize_table {
|
||||
break;
|
||||
}
|
||||
in_deps = line == "[dependencies]";
|
||||
in_mize_table = line == "[dependencies.mize]";
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_mize_table {
|
||||
mize_table_fields.push(line.to_string());
|
||||
continue;
|
||||
}
|
||||
|
||||
if !in_deps {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Inline: mize = ...
|
||||
if let Some(rest) = line.strip_prefix("mize") {
|
||||
let rest = rest.trim_start();
|
||||
if rest.starts_with('=') {
|
||||
return Ok(format!("mize {}", rest));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mize_table_fields.is_empty() {
|
||||
return Ok(format!("mize = {{ {} }}", mize_table_fields.join(", ")));
|
||||
}
|
||||
|
||||
Err("no 'mize' dependency found under [dependencies] in part Cargo.toml".to_string())
|
||||
}
|
||||
|
||||
/// Generate the `Cargo.toml` content for the generated package.
|
||||
pub fn generate_cargo_toml(package_name: &str) -> String {
|
||||
///
|
||||
/// `mize_dep` is the full assignment line copied from the part Cargo.toml,
|
||||
/// e.g. `mize = { workspace = true }` or `mize = { path = "../../core" }`.
|
||||
pub fn generate_cargo_toml(package_name: &str, mize_dep: &str) -> String {
|
||||
format!(
|
||||
r#"[package]
|
||||
name = "{package_name}"
|
||||
@@ -159,9 +478,10 @@ name = "{lib_name}"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
mize = {{ path = "../../core" }}
|
||||
{mize_dep}
|
||||
"#,
|
||||
package_name = package_name,
|
||||
lib_name = package_name.replace('-', "_"),
|
||||
mize_dep = mize_dep.trim(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ pub struct CliPart {
|
||||
}
|
||||
|
||||
impl MizePart for CliPart {
|
||||
fn id(&self) -> MizeId {
|
||||
self.id.clone()
|
||||
fn id(&self) -> MizeResult<MizeId> {
|
||||
Ok(self.id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ impl CliPart {
|
||||
pub fn get(name: &str) -> MizeResult<CliPart> {
|
||||
let mut mize = get_global_mize()?;
|
||||
let part = mize.get_part(name)?;
|
||||
Ok(CliPart { id: part.id() })
|
||||
Ok(CliPart { id: part.id()? })
|
||||
}
|
||||
pub fn run(&self, cmdline: Vec<String>) -> MizeResult<()> {
|
||||
let cmdline = cmdline.into_item_data();
|
||||
@@ -34,7 +34,7 @@ impl CliPart {
|
||||
pub fn subcommand<T: Fn(ArgMatches) -> MizeResult<()> + Send + Sync + 'static>(&self, subcmd: Command, action: T) -> MizeResult<()> {
|
||||
let mut subcmd_arg = ItemData::new();
|
||||
subcmd_arg.set_path("_native", subcmd.native_id)?;
|
||||
let mut action_arg = ItemData::fun(Box::new(move |args: ItemData| {
|
||||
let action_arg = ItemData::fun(Box::new(move |args: ItemData| {
|
||||
let matches = args.clone().get_path("0")?.native_take_from::<ArgMatches>()?;
|
||||
action(matches)?;
|
||||
Ok(ItemData::new())
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::fs::{create_dir_all, write};
|
||||
use mize::prelude::*;
|
||||
use cmd_lib::run_cmd as sh;
|
||||
use mize::tracing::info;
|
||||
use mize::tracing::debug;
|
||||
|
||||
// a static version of what would be generated.
|
||||
// as we need mize.dev to generate anything.
|
||||
@@ -13,8 +14,7 @@ use generated::*;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
extern "C" fn part_dev() -> MizeResult<()> {
|
||||
let mut mize = get_global_mize()?;
|
||||
let mut cli = generated::CliPart::get("cli")?;
|
||||
let cli = generated::CliPart::get("cli")?;
|
||||
cli.set_command("dev");
|
||||
|
||||
cli.subcommand(Command::new("test")?, |_| {println!("hello from test command"); Ok(())})?;
|
||||
@@ -50,11 +50,19 @@ pub fn publish_mize(_ : ArgMatches) -> MizeResult<()> {
|
||||
"x86_64-apple-darwin" => "darwin",
|
||||
_ => "base",
|
||||
};
|
||||
sh!(docker build -t mize-build-$docker_image $mize_src/parts/mize.dev/targets/$docker_image)?;
|
||||
let res = sh!(docker run -it --rm -v $cargo_dir/registry:/usr/local/cargo/registry -v $cargo_dir/git:/usr/local/cargo/git -v $mize_src:/src -v $mize_src/target:/src/target mize-build-$docker_image cargo build --target $target_name --release);
|
||||
let tmp = std::env::var("TMP").unwrap();
|
||||
let mut build_config = ItemData::new();
|
||||
build_config.set_path("namespace", "mize.buildtime.ns")?;
|
||||
std::fs::create_dir_all(&tmp)?;
|
||||
std::fs::File::create(format!("{tmp}/mize-part-dev-build-config.toml"))?;
|
||||
std::fs::write(format!("{tmp}/mize-part-dev-build-config.toml"), build_config.to_json()?)?;
|
||||
sh!(docker build -t mize-build-$docker_image $mize_src/parts/mize.dev/docker/$docker_image)?;
|
||||
sh!(echo running in container: docker run -it --rm -v "$tmp/mize-part-dev-build-config.toml:/build-config.toml" -e MIZE_BUILD_CONFIG=/build-config.toml -v $cargo_dir/registry:/usr/local/cargo/registry -v $cargo_dir/git:/usr/local/cargo/git -v $mize_src:/src -v $mize_src/target:/src/target mize-build-$docker_image cargo build --target $target_name --release --workspace)?;
|
||||
let res = sh!(docker run -it --rm -v "$tmp/mize-part-dev-build-config.toml:/build-config.toml" -e MIZE_BUILD_CONFIG=/build-config.toml -v $cargo_dir/registry:/usr/local/cargo/registry -v $cargo_dir/git:/usr/local/cargo/git -v $mize_src:/src -v $mize_src/target:/src/target mize-build-$docker_image cargo build --target $target_name --release);
|
||||
if res.is_err() {
|
||||
info!("build for {target_name} failed...");
|
||||
}
|
||||
break;
|
||||
};
|
||||
|
||||
|
||||
@@ -79,11 +87,11 @@ pub fn gen_bindings(_ : ArgMatches) -> MizeResult<()> {
|
||||
|
||||
// 2. parse the part name and the gen = [...] list
|
||||
let part_name: String = part_data
|
||||
.get_path("name")
|
||||
.get_path("id")
|
||||
.ok()
|
||||
.and_then(|v| v.value_string().ok())
|
||||
.ok_or_else(|| mize_err!("part file must have a 'name' field"))?;
|
||||
info!("part name: {part_name}");
|
||||
println!("part name: {part_name}");
|
||||
|
||||
let gen_data = part_data
|
||||
.get_path("gen")
|
||||
@@ -97,7 +105,7 @@ pub fn gen_bindings(_ : ArgMatches) -> MizeResult<()> {
|
||||
let s = v.as_text().ok_or_else(|| mize_err!("'gen' array must contain only strings"))?;
|
||||
gen_parts.push(s.to_owned());
|
||||
}
|
||||
info!("parts to generate bindings for: {:?}", gen_parts);
|
||||
debug!("parts to generate bindings for: {:?}", gen_parts);
|
||||
|
||||
// 3. enable generate mode and clear any old metadata
|
||||
mize.set_generate_mode(true)?;
|
||||
@@ -122,7 +130,13 @@ pub fn gen_bindings(_ : ArgMatches) -> MizeResult<()> {
|
||||
create_dir_all(&src_dir).map_err(|e| mize_err!("could not create generated/src dir: {e}"))?;
|
||||
|
||||
let package_name = format!("mize-generated-{part_name}");
|
||||
let cargo_toml = codegen::generate_cargo_toml(&package_name);
|
||||
let part_cargo_path = cwd.join("Cargo.toml");
|
||||
let part_cargo = std::fs::read_to_string(&part_cargo_path)
|
||||
.map_err(|e| mize_err!("could not read part Cargo.toml {}: {e}", part_cargo_path.display()))?;
|
||||
let mize_dep = codegen::extract_mize_dependency(&part_cargo)
|
||||
.map_err(|e| mize_err!("{e}"))?;
|
||||
debug!("using mize dependency from part Cargo.toml: {mize_dep}");
|
||||
let cargo_toml = codegen::generate_cargo_toml(&package_name, &mize_dep);
|
||||
write(gen_dir.join("Cargo.toml"), cargo_toml)
|
||||
.map_err(|e| mize_err!("could not write generated/Cargo.toml: {e}"))?;
|
||||
|
||||
|
||||
@@ -8,10 +8,9 @@ license = "MIT OR Apache-2.0"
|
||||
[dependencies]
|
||||
dioxus = { version = "0.7", default-features = false }
|
||||
dioxus-primitives = { git = "https://github.com/DioxusLabs/components", default-features = false }
|
||||
dioxus-code = { version = "0.1.0", default-features = false, features = ["macro", "lang-css"] }
|
||||
dioxus-icons = "0.1.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
web = ["dioxus/web", "dioxus-primitives/web"]
|
||||
desktop = ["dioxus/desktop"]
|
||||
desktop = ["dioxus/desktop", "dioxus-primitives/web"]
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/* mize UI theme — include once; toggle via html[data-theme="light"|"dark"] */
|
||||
|
||||
html[data-theme="dark"] {
|
||||
--dark: initial;
|
||||
--light: ;
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
--dark: ;
|
||||
--light: initial;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) {
|
||||
--dark: initial;
|
||||
--light: ;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root:not([data-theme]) {
|
||||
--dark: ;
|
||||
--light: initial;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: var(--dark, #171d18) var(--light, #f7f8f4);
|
||||
--surface: var(--dark, #242d25) var(--light, #ffffff);
|
||||
--surface-2: var(--dark, #2c362d) var(--light, #eff2ea);
|
||||
--text: var(--dark, #f2f4ea) var(--light, #20251c);
|
||||
--text-2: var(--dark, #aab5a4) var(--light, #66705e);
|
||||
--border: var(--dark, #3a463a) var(--light, #dfe4d9);
|
||||
--accent: var(--dark, #d7e47d) var(--light, #fec251);
|
||||
--accent-text: var(--dark, #d7e47d) var(--light, #79500f);
|
||||
--success: var(--dark, #b9d987) var(--light, #526f32);
|
||||
--info: var(--dark, #b9e4eb) var(--light, #376777);
|
||||
--warning: var(--dark, #ffd77f) var(--light, #a66b13);
|
||||
--danger: var(--dark, #ff9b8e) var(--light, #b94d42);
|
||||
|
||||
/* On-accent fill text: dark ink on light accent, dark surface on dark accent */
|
||||
--on-accent: var(--dark, #171d18) var(--light, #79500f);
|
||||
--focus-ring: var(--accent);
|
||||
}
|
||||
@@ -3,9 +3,92 @@ use dioxus_primitives::dioxus_attributes::attributes;
|
||||
use dioxus_primitives::avatar::{self, AvatarState};
|
||||
use dioxus_primitives::merge_attributes;
|
||||
|
||||
#[css_module("/src/components/avatar/style.css")]
|
||||
struct Styles;
|
||||
|
||||
const STYLE: &str = r#".dx-avatar {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
overflow: hidden;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dx-avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.dx-avatar-sm {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.dx-avatar-md {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dx-avatar-lg {
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.dx-avatar-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dx-avatar-rounded {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.dx-avatar[data-state="loading"] {
|
||||
animation: dx-pulse 1.5s infinite ease-in-out;
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.dx-avatar[data-state="empty"] {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
@keyframes dx-pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dx-avatar-fallback {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--surface-2);
|
||||
color: var(--text-2);
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.dx-avatar[data-state="error"] .dx-avatar-fallback {
|
||||
background: var(--surface-2);
|
||||
color: var(--text-2);
|
||||
}
|
||||
"#;
|
||||
#[derive(Clone, Copy, PartialEq, Default)]
|
||||
pub enum AvatarImageSize {
|
||||
#[default]
|
||||
@@ -17,9 +100,9 @@ pub enum AvatarImageSize {
|
||||
impl AvatarImageSize {
|
||||
fn to_class(self) -> &'static str {
|
||||
match self {
|
||||
AvatarImageSize::Small => Styles::dx_avatar_sm.inner,
|
||||
AvatarImageSize::Medium => Styles::dx_avatar_md.inner,
|
||||
AvatarImageSize::Large => Styles::dx_avatar_lg.inner,
|
||||
AvatarImageSize::Small => "dx-avatar-sm",
|
||||
AvatarImageSize::Medium => "dx-avatar-md",
|
||||
AvatarImageSize::Large => "dx-avatar-lg",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,8 +117,8 @@ pub enum AvatarShape {
|
||||
impl AvatarShape {
|
||||
fn to_class(self) -> &'static str {
|
||||
match self {
|
||||
AvatarShape::Circle => Styles::dx_avatar_circle.inner,
|
||||
AvatarShape::Rounded => Styles::dx_avatar_rounded.inner,
|
||||
AvatarShape::Circle => "dx-avatar-circle",
|
||||
AvatarShape::Rounded => "dx-avatar-rounded",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +156,7 @@ pub struct AvatarProps {
|
||||
pub fn Avatar(props: AvatarProps) -> Element {
|
||||
let class = format!(
|
||||
"{} {} {}",
|
||||
Styles::dx_avatar,
|
||||
"dx-avatar",
|
||||
props.size.to_class(),
|
||||
props.shape.to_class()
|
||||
);
|
||||
@@ -83,6 +166,8 @@ pub fn Avatar(props: AvatarProps) -> Element {
|
||||
let merged = merge_attributes(vec![base, props.attributes]);
|
||||
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/avatar", css: STYLE }
|
||||
|
||||
avatar::Avatar {
|
||||
on_load: props.on_load,
|
||||
on_error: props.on_error,
|
||||
@@ -110,12 +195,14 @@ pub struct AvatarImageProps {
|
||||
#[component]
|
||||
pub fn AvatarImage(props: AvatarImageProps) -> Element {
|
||||
let base = attributes!(img {
|
||||
class: Styles::dx_avatar_image,
|
||||
class: "dx-avatar-image",
|
||||
draggable: "false",
|
||||
});
|
||||
let merged = merge_attributes(vec![base, props.attributes]);
|
||||
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/avatar", css: STYLE }
|
||||
|
||||
avatar::AvatarImage {
|
||||
id: props.id,
|
||||
src: props.src,
|
||||
@@ -136,11 +223,13 @@ pub struct AvatarFallbackProps {
|
||||
#[component]
|
||||
pub fn AvatarFallback(props: AvatarFallbackProps) -> Element {
|
||||
let base = attributes!(span {
|
||||
class: Styles::dx_avatar_fallback,
|
||||
class: "dx-avatar-fallback",
|
||||
});
|
||||
let merged = merge_attributes(vec![base, props.attributes]);
|
||||
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/avatar", css: STYLE }
|
||||
|
||||
avatar::AvatarFallback {
|
||||
attributes: merged,
|
||||
{props.children}
|
||||
@@ -187,6 +276,8 @@ pub struct ImageAvatarProps {
|
||||
#[component]
|
||||
pub fn ImageAvatar(props: ImageAvatarProps) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/avatar", css: STYLE }
|
||||
|
||||
Avatar {
|
||||
on_load: props.on_load,
|
||||
on_error: props.on_error,
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
.dx-avatar {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
overflow: hidden;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dx-avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.dx-avatar-sm {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.dx-avatar-md {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dx-avatar-lg {
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.dx-avatar-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dx-avatar-rounded {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.dx-avatar[data-state="loading"] {
|
||||
animation: dx-pulse 1.5s infinite ease-in-out;
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.dx-avatar[data-state="empty"] {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
@keyframes dx-pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dx-avatar-fallback {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--surface-2);
|
||||
color: var(--text-2);
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.dx-avatar[data-state="error"] .dx-avatar-fallback {
|
||||
background: var(--surface-2);
|
||||
color: var(--text-2);
|
||||
}
|
||||
@@ -1,9 +1,45 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_icons::lucide::BadgeCheck;
|
||||
|
||||
#[css_module("/src/components/badge/style.css")]
|
||||
struct Styles;
|
||||
|
||||
const STYLE: &str = r#".dx-badge {
|
||||
display: inline-flex;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
gap: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dx-badge[padding="true"] {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.dx-badge[data-style="primary"] {
|
||||
background-color: var(--accent);
|
||||
color: var(--on-accent);
|
||||
}
|
||||
|
||||
.dx-badge[data-style="secondary"] {
|
||||
background-color: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-badge[data-style="outline"] {
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--surface);
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.dx-badge[data-style="destructive"] {
|
||||
background-color: var(--danger);
|
||||
color: var(--dark, #171d18) var(--light, #ffffff);
|
||||
}
|
||||
"#;
|
||||
#[derive(Copy, Clone, PartialEq, Default)]
|
||||
#[non_exhaustive]
|
||||
pub enum BadgeVariant {
|
||||
@@ -42,6 +78,8 @@ pub struct BadgeProps {
|
||||
#[component]
|
||||
pub fn Badge(props: BadgeProps) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/badge", css: STYLE }
|
||||
|
||||
BadgeElement {
|
||||
"padding": true,
|
||||
variant: props.variant,
|
||||
@@ -54,8 +92,10 @@ pub fn Badge(props: BadgeProps) -> Element {
|
||||
#[component]
|
||||
fn BadgeElement(props: BadgeProps) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/badge", css: STYLE }
|
||||
|
||||
span {
|
||||
class: Styles::dx_badge,
|
||||
class: "dx-badge",
|
||||
"data-style": props.variant.class(),
|
||||
..props.attributes,
|
||||
{props.children}
|
||||
@@ -66,6 +106,8 @@ fn BadgeElement(props: BadgeProps) -> Element {
|
||||
#[component]
|
||||
pub fn VerifiedIcon() -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/badge", css: STYLE }
|
||||
|
||||
BadgeCheck {
|
||||
size: "12px",
|
||||
stroke: "var(--text)",
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
.dx-badge {
|
||||
display: inline-flex;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
gap: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dx-badge[padding="true"] {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.dx-badge[data-style="primary"] {
|
||||
background-color: var(--accent);
|
||||
color: var(--on-accent);
|
||||
}
|
||||
|
||||
.dx-badge[data-style="secondary"] {
|
||||
background-color: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-badge[data-style="outline"] {
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--surface);
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.dx-badge[data-style="destructive"] {
|
||||
background-color: var(--danger);
|
||||
color: var(--dark, #171d18) var(--light, #ffffff);
|
||||
}
|
||||
@@ -2,9 +2,203 @@ use dioxus::prelude::*;
|
||||
use dioxus_primitives::dioxus_attributes::attributes;
|
||||
use dioxus_primitives::merge_attributes;
|
||||
|
||||
#[css_module("/src/components/button/style.css")]
|
||||
struct Styles;
|
||||
|
||||
const STYLE: &str = r#".dx-button {
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
gap: 0.5rem;
|
||||
line-height: 1.25rem;
|
||||
outline: none;
|
||||
transition-duration: 150ms;
|
||||
transition-property: background-color, color, box-shadow, border-color, opacity;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dx-button svg {
|
||||
flex-shrink: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dx-button svg:not([class*="size-"]) {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="xs"] {
|
||||
height: 1.5rem;
|
||||
padding: 0 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
gap: 0.25rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="xs"]:has(> svg) {
|
||||
padding-inline: 0.375rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="xs"] svg:not([class*="size-"]) {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="sm"] {
|
||||
height: 1.875rem;
|
||||
padding: 0 0.625rem;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="sm"]:has(> svg) {
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="default"] {
|
||||
height: 2rem;
|
||||
padding: 0.375rem 1rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="default"]:has(> svg) {
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="lg"] {
|
||||
height: 2.375rem;
|
||||
padding: 0 1.25rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="lg"]:has(> svg) {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon"] {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon"] svg:not([class*="size-"]) {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon-xs"] {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon-xs"] svg:not([class*="size-"]) {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon-sm"] {
|
||||
width: 1.875rem;
|
||||
height: 1.875rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon-sm"] svg:not([class*="size-"]) {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon-lg"] {
|
||||
width: 2.375rem;
|
||||
height: 2.375rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon-lg"] svg:not([class*="size-"]) {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.dx-button:disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dx-button[data-style="primary"] {
|
||||
background-color: var(--accent);
|
||||
color: var(--on-accent);
|
||||
}
|
||||
|
||||
.dx-button[data-style="primary"]:not(:disabled):hover {
|
||||
background-color: color-mix(in oklab, var(--accent) 88%, var(--text));
|
||||
}
|
||||
|
||||
.dx-button[data-style="secondary"] {
|
||||
background-color: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-button[data-style="secondary"]:not(:disabled):hover {
|
||||
background-color: color-mix(in oklab, var(--surface-2) 85%, var(--text));
|
||||
}
|
||||
|
||||
.dx-button[data-style="ghost"] {
|
||||
background-color: transparent;
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.dx-button[data-style="ghost"]:not(:disabled):hover {
|
||||
background-color: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-button[data-style="outline"] {
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-button[data-style="outline"]:not(:disabled):hover {
|
||||
background-color: var(--surface-2);
|
||||
}
|
||||
|
||||
.dx-button[data-style="destructive"] {
|
||||
background-color: var(--danger);
|
||||
color: var(--dark, #171d18) var(--light, #ffffff);
|
||||
}
|
||||
|
||||
.dx-button[data-style="destructive"]:not(:disabled):hover {
|
||||
background-color: color-mix(in oklab, var(--danger) 88%, var(--text));
|
||||
}
|
||||
|
||||
.dx-button[data-style="destructive"]:focus-visible {
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--danger) 40%, transparent);
|
||||
}
|
||||
|
||||
.dx-button[data-style="link"] {
|
||||
background-color: transparent;
|
||||
color: var(--accent-text);
|
||||
text-decoration: none;
|
||||
text-underline-offset: 0.25rem;
|
||||
}
|
||||
|
||||
.dx-button[data-style="link"]:not(:disabled):hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dx-button:focus-visible {
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--focus-ring) 45%, transparent);
|
||||
}
|
||||
|
||||
.dx-button[aria-invalid="true"] {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.dx-button[aria-invalid="true"]:focus-visible {
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--danger) 40%, transparent);
|
||||
}
|
||||
"#;
|
||||
#[derive(Copy, Clone, PartialEq, Default)]
|
||||
#[non_exhaustive]
|
||||
pub enum ButtonVariant {
|
||||
@@ -73,13 +267,15 @@ pub fn Button(
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let base = attributes!(button {
|
||||
class: Styles::dx_button,
|
||||
class: "dx-button",
|
||||
"data-style": variant.class(),
|
||||
"data-size": size.class(),
|
||||
});
|
||||
let merged = merge_attributes(vec![base, attributes]);
|
||||
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/button", css: STYLE }
|
||||
|
||||
button {
|
||||
onclick: move |event| {
|
||||
if let Some(f) = &onclick {
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
.dx-button {
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
gap: 0.5rem;
|
||||
line-height: 1.25rem;
|
||||
outline: none;
|
||||
transition-duration: 150ms;
|
||||
transition-property: background-color, color, box-shadow, border-color, opacity;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dx-button svg {
|
||||
flex-shrink: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dx-button svg:not([class*="size-"]) {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="xs"] {
|
||||
height: 1.5rem;
|
||||
padding: 0 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
gap: 0.25rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="xs"]:has(> svg) {
|
||||
padding-inline: 0.375rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="xs"] svg:not([class*="size-"]) {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="sm"] {
|
||||
height: 1.875rem;
|
||||
padding: 0 0.625rem;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="sm"]:has(> svg) {
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="default"] {
|
||||
height: 2rem;
|
||||
padding: 0.375rem 1rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="default"]:has(> svg) {
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="lg"] {
|
||||
height: 2.375rem;
|
||||
padding: 0 1.25rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="lg"]:has(> svg) {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon"] {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon"] svg:not([class*="size-"]) {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon-xs"] {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon-xs"] svg:not([class*="size-"]) {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon-sm"] {
|
||||
width: 1.875rem;
|
||||
height: 1.875rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon-sm"] svg:not([class*="size-"]) {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon-lg"] {
|
||||
width: 2.375rem;
|
||||
height: 2.375rem;
|
||||
}
|
||||
|
||||
.dx-button[data-size="icon-lg"] svg:not([class*="size-"]) {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.dx-button:disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dx-button[data-style="primary"] {
|
||||
background-color: var(--accent);
|
||||
color: var(--on-accent);
|
||||
}
|
||||
|
||||
.dx-button[data-style="primary"]:not(:disabled):hover {
|
||||
background-color: color-mix(in oklab, var(--accent) 88%, var(--text));
|
||||
}
|
||||
|
||||
.dx-button[data-style="secondary"] {
|
||||
background-color: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-button[data-style="secondary"]:not(:disabled):hover {
|
||||
background-color: color-mix(in oklab, var(--surface-2) 85%, var(--text));
|
||||
}
|
||||
|
||||
.dx-button[data-style="ghost"] {
|
||||
background-color: transparent;
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.dx-button[data-style="ghost"]:not(:disabled):hover {
|
||||
background-color: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-button[data-style="outline"] {
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-button[data-style="outline"]:not(:disabled):hover {
|
||||
background-color: var(--surface-2);
|
||||
}
|
||||
|
||||
.dx-button[data-style="destructive"] {
|
||||
background-color: var(--danger);
|
||||
color: var(--dark, #171d18) var(--light, #ffffff);
|
||||
}
|
||||
|
||||
.dx-button[data-style="destructive"]:not(:disabled):hover {
|
||||
background-color: color-mix(in oklab, var(--danger) 88%, var(--text));
|
||||
}
|
||||
|
||||
.dx-button[data-style="destructive"]:focus-visible {
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--danger) 40%, transparent);
|
||||
}
|
||||
|
||||
.dx-button[data-style="link"] {
|
||||
background-color: transparent;
|
||||
color: var(--accent-text);
|
||||
text-decoration: none;
|
||||
text-underline-offset: 0.25rem;
|
||||
}
|
||||
|
||||
.dx-button[data-style="link"]:not(:disabled):hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dx-button:focus-visible {
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--focus-ring) 45%, transparent);
|
||||
}
|
||||
|
||||
.dx-button[aria-invalid="true"] {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.dx-button[aria-invalid="true"]:focus-visible {
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--danger) 40%, transparent);
|
||||
}
|
||||
@@ -1,16 +1,69 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[css_module("/src/components/card/style.css")]
|
||||
struct Styles;
|
||||
|
||||
const STYLE: &str = r#".dx-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--surface);
|
||||
box-shadow: 0 1px 0 rgb(0 0 0 / 8%);
|
||||
color: var(--text);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dx-card-header {
|
||||
display: grid;
|
||||
align-items: start;
|
||||
padding: 0 1rem;
|
||||
gap: 0.375rem;
|
||||
grid-auto-rows: min-content;
|
||||
grid-template-rows: auto auto;
|
||||
}
|
||||
|
||||
.dx-card-header:has([data-slot="card-action"]) {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.dx-card-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.dx-card-description {
|
||||
color: var(--text-2);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.dx-card-action {
|
||||
grid-column-start: 2;
|
||||
grid-row: 1 / span 2;
|
||||
place-self: start end;
|
||||
}
|
||||
|
||||
.dx-card-content {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.dx-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
"#;
|
||||
#[component]
|
||||
pub fn Card(
|
||||
#[props(extends=GlobalAttributes)] attributes: Vec<Attribute>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/card", css: STYLE }
|
||||
|
||||
div {
|
||||
class: Styles::dx_card,
|
||||
class: "dx-card",
|
||||
"data-slot": "card",
|
||||
..attributes,
|
||||
{children}
|
||||
@@ -24,8 +77,10 @@ pub fn CardHeader(
|
||||
children: Element,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/card", css: STYLE }
|
||||
|
||||
div {
|
||||
class: Styles::dx_card_header,
|
||||
class: "dx-card-header",
|
||||
"data-slot": "card-header",
|
||||
..attributes,
|
||||
{children}
|
||||
@@ -39,8 +94,10 @@ pub fn CardTitle(
|
||||
children: Element,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/card", css: STYLE }
|
||||
|
||||
div {
|
||||
class: Styles::dx_card_title,
|
||||
class: "dx-card-title",
|
||||
"data-slot": "card-title",
|
||||
..attributes,
|
||||
{children}
|
||||
@@ -54,8 +111,10 @@ pub fn CardDescription(
|
||||
children: Element,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/card", css: STYLE }
|
||||
|
||||
div {
|
||||
class: Styles::dx_card_description,
|
||||
class: "dx-card-description",
|
||||
"data-slot": "card-description",
|
||||
..attributes,
|
||||
{children}
|
||||
@@ -69,8 +128,10 @@ pub fn CardAction(
|
||||
children: Element,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/card", css: STYLE }
|
||||
|
||||
div {
|
||||
class: Styles::dx_card_action,
|
||||
class: "dx-card-action",
|
||||
"data-slot": "card-action",
|
||||
..attributes,
|
||||
{children}
|
||||
@@ -84,8 +145,10 @@ pub fn CardContent(
|
||||
children: Element,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/card", css: STYLE }
|
||||
|
||||
div {
|
||||
class: Styles::dx_card_content,
|
||||
class: "dx-card-content",
|
||||
"data-slot": "card-content",
|
||||
..attributes,
|
||||
{children}
|
||||
@@ -99,8 +162,10 @@ pub fn CardFooter(
|
||||
children: Element,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/card", css: STYLE }
|
||||
|
||||
div {
|
||||
class: Styles::dx_card_footer,
|
||||
class: "dx-card-footer",
|
||||
"data-slot": "card-footer",
|
||||
..attributes,
|
||||
{children}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
.dx-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--surface);
|
||||
box-shadow: 0 1px 0 rgb(0 0 0 / 8%);
|
||||
color: var(--text);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dx-card-header {
|
||||
display: grid;
|
||||
align-items: start;
|
||||
padding: 0 1rem;
|
||||
gap: 0.375rem;
|
||||
grid-auto-rows: min-content;
|
||||
grid-template-rows: auto auto;
|
||||
}
|
||||
|
||||
.dx-card-header:has([data-slot="card-action"]) {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.dx-card-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.dx-card-description {
|
||||
color: var(--text-2);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.dx-card-action {
|
||||
grid-column-start: 2;
|
||||
grid-row: 1 / span 2;
|
||||
place-self: start end;
|
||||
}
|
||||
|
||||
.dx-card-content {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.dx-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
@@ -2,14 +2,49 @@ use dioxus::prelude::*;
|
||||
use dioxus_icons::lucide::Check;
|
||||
use dioxus_primitives::checkbox::{self, CheckboxProps};
|
||||
|
||||
#[css_module("/src/components/checkbox/style.css")]
|
||||
struct Styles;
|
||||
|
||||
const STYLE: &str = r#".dx-checkbox {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
margin: 0;
|
||||
background-color: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--border);
|
||||
color: var(--on-accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dx-checkbox-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dx-checkbox[data-state="checked"] {
|
||||
background-color: var(--accent);
|
||||
box-shadow: none;
|
||||
color: var(--on-accent);
|
||||
}
|
||||
|
||||
.dx-checkbox:focus-visible {
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 45%, transparent);
|
||||
}
|
||||
|
||||
.dx-checkbox[data-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
"#;
|
||||
#[component]
|
||||
pub fn Checkbox(props: CheckboxProps) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/checkbox", css: STYLE }
|
||||
|
||||
checkbox::Checkbox {
|
||||
class: Styles::dx_checkbox,
|
||||
class: "dx-checkbox",
|
||||
checked: props.checked,
|
||||
default_checked: props.default_checked,
|
||||
required: props.required,
|
||||
@@ -18,7 +53,7 @@ pub fn Checkbox(props: CheckboxProps) -> Element {
|
||||
value: props.value,
|
||||
on_checked_change: props.on_checked_change,
|
||||
attributes: props.attributes,
|
||||
checkbox::CheckboxIndicator { class: Styles::dx_checkbox_indicator,
|
||||
checkbox::CheckboxIndicator { class: "dx-checkbox-indicator",
|
||||
Check { size: "1rem" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
.dx-checkbox {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
margin: 0;
|
||||
background-color: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--border);
|
||||
color: var(--on-accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dx-checkbox-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dx-checkbox[data-state="checked"] {
|
||||
background-color: var(--accent);
|
||||
box-shadow: none;
|
||||
color: var(--on-accent);
|
||||
}
|
||||
|
||||
.dx-checkbox:focus-visible {
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 45%, transparent);
|
||||
}
|
||||
|
||||
.dx-checkbox[data-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::{Button, ButtonVariant, Dialog, DialogDescription, DialogTitle};
|
||||
|
||||
|
||||
const STYLE: &str = r#".dx-control {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 5.5rem;
|
||||
min-height: 5.5rem;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.85rem 0.65rem 0.7rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--surface);
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 16%);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
gap: 0.45rem;
|
||||
outline: none;
|
||||
transition: background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease,
|
||||
transform 120ms ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dx-control:hover:not(:disabled):not([data-kind="display"]) {
|
||||
background: var(--surface-2);
|
||||
border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dx-control:focus-visible {
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 40%, transparent),
|
||||
0 4px 16px rgb(0 0 0 / 16%);
|
||||
}
|
||||
|
||||
.dx-control[data-enabled="true"] {
|
||||
border-color: color-mix(in oklab, var(--accent) 70%, var(--border));
|
||||
background: color-mix(in oklab, var(--accent) 16%, var(--surface));
|
||||
}
|
||||
|
||||
.dx-control[data-kind="display"] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dx-control:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.dx-control-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 0.375rem;
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-control[data-enabled="true"] .dx-control-icon {
|
||||
background: color-mix(in oklab, var(--accent) 28%, var(--surface-2));
|
||||
color: var(--accent-text);
|
||||
}
|
||||
|
||||
.dx-control-icon svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.dx-control-name {
|
||||
color: var(--text);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dx-control-value {
|
||||
color: var(--text-2);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dx-control-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
"#;
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Default)]
|
||||
#[non_exhaustive]
|
||||
pub enum ControlKind {
|
||||
/// Click toggles an on/off state (optional confirm).
|
||||
#[default]
|
||||
Toggle,
|
||||
/// Click triggers an action (optional confirm).
|
||||
Action,
|
||||
/// Read-only display of a setting/value.
|
||||
Display,
|
||||
}
|
||||
|
||||
impl ControlKind {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ControlKind::Toggle => "toggle",
|
||||
ControlKind::Action => "action",
|
||||
ControlKind::Display => "display",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Control(
|
||||
/// Visible label under the icon.
|
||||
name: String,
|
||||
#[props(default)]
|
||||
kind: ControlKind,
|
||||
/// Toggle enabled state (Toggle kind).
|
||||
#[props(default)]
|
||||
enabled: bool,
|
||||
/// Optional value line (useful for Display, also ok for others).
|
||||
value: Option<String>,
|
||||
/// If set, show a confirmation dialog before toggling/acting.
|
||||
confirm: Option<String>,
|
||||
/// Fired for Action after confirm (or immediately if no confirm).
|
||||
onclick: Option<EventHandler<()>>,
|
||||
/// Fired for Toggle with the new enabled value after confirm (or immediately).
|
||||
on_enabled_change: Option<EventHandler<bool>>,
|
||||
/// Icon element (e.g. a dioxus-icons lucide component).
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let mut confirm_open = use_signal(|| false);
|
||||
let needs_confirm = confirm.is_some();
|
||||
|
||||
let do_commit = move || {
|
||||
match kind {
|
||||
ControlKind::Toggle => {
|
||||
if let Some(cb) = on_enabled_change {
|
||||
cb.call(!enabled);
|
||||
}
|
||||
}
|
||||
ControlKind::Action => {
|
||||
if let Some(cb) = onclick {
|
||||
cb.call(());
|
||||
}
|
||||
}
|
||||
ControlKind::Display => {}
|
||||
}
|
||||
};
|
||||
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/control", css: STYLE }
|
||||
|
||||
button {
|
||||
class: "dx-control",
|
||||
"data-slot": "control",
|
||||
"data-kind": kind.as_str(),
|
||||
"data-enabled": enabled.to_string(),
|
||||
r#type: "button",
|
||||
disabled: kind == ControlKind::Display,
|
||||
onclick: move |_| {
|
||||
if kind == ControlKind::Display {
|
||||
return;
|
||||
}
|
||||
if needs_confirm {
|
||||
confirm_open.set(true);
|
||||
} else {
|
||||
do_commit();
|
||||
}
|
||||
},
|
||||
span { class: "dx-control-icon", {children} }
|
||||
span { class: "dx-control-name", "{name}" }
|
||||
if let Some(val) = &value {
|
||||
span { class: "dx-control-value", "{val}" }
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(msg) = confirm {
|
||||
Dialog {
|
||||
open: Some(confirm_open()),
|
||||
on_open_change: move |open| confirm_open.set(open),
|
||||
DialogTitle { "Confirm" }
|
||||
DialogDescription { "{msg}" }
|
||||
div { class: "dx-control-actions",
|
||||
Button {
|
||||
variant: ButtonVariant::Ghost,
|
||||
onclick: move |_| confirm_open.set(false),
|
||||
"Cancel"
|
||||
}
|
||||
Button {
|
||||
variant: ButtonVariant::Primary,
|
||||
onclick: move |_| {
|
||||
confirm_open.set(false);
|
||||
do_commit();
|
||||
},
|
||||
"Confirm"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mod component;
|
||||
pub use component::*;
|
||||
@@ -2,19 +2,120 @@ use dioxus::prelude::*;
|
||||
use dioxus_primitives::dialog::{self, DialogDescriptionProps, DialogRootProps, DialogTitleProps};
|
||||
use dioxus_primitives::{dioxus_attributes::attributes, merge_attributes};
|
||||
|
||||
#[css_module("/src/components/dialog/style.css")]
|
||||
struct Styles;
|
||||
|
||||
const STYLE: &str = r#".dx-dialog-backdrop {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
background: rgb(23 29 24 / 55%);
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.dx-dialog-backdrop[data-state="closed"] {
|
||||
animation: dx-dialog-backdrop-animate-out 150ms ease-in forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes dx-dialog-backdrop-animate-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dx-dialog-backdrop[data-state="open"] {
|
||||
animation: dx-dialog-backdrop-animate-in 150ms ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes dx-dialog-backdrop-animate-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dx-dialog {
|
||||
position: fixed;
|
||||
z-index: 1001;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: calc(100% - 2rem);
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem 1.25rem 1.25rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
margin: 0;
|
||||
background: var(--surface);
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 35%);
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
gap: 0.75rem;
|
||||
text-align: center;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.dx-dialog-title {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dx-dialog-description {
|
||||
margin: 0;
|
||||
color: var(--text-2);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (width >= 40rem) {
|
||||
.dx-dialog {
|
||||
max-width: 28rem;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.dx-dialog-close {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
align-self: flex-start;
|
||||
padding: 0;
|
||||
border: none;
|
||||
margin: 0;
|
||||
background: none;
|
||||
color: var(--text-2);
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dx-dialog-close:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
"#;
|
||||
#[component]
|
||||
pub fn Dialog(props: DialogRootProps) -> Element {
|
||||
let base = attributes!(div {
|
||||
class: Styles::dx_dialog,
|
||||
class: "dx-dialog",
|
||||
});
|
||||
let merged = merge_attributes(vec![base, props.attributes]);
|
||||
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/dialog", css: STYLE }
|
||||
|
||||
dialog::DialogRoot {
|
||||
class: Styles::dx_dialog_backdrop,
|
||||
class: "dx-dialog-backdrop",
|
||||
id: props.id,
|
||||
is_modal: props.is_modal,
|
||||
open: props.open,
|
||||
@@ -32,11 +133,13 @@ pub fn Dialog(props: DialogRootProps) -> Element {
|
||||
#[component]
|
||||
pub fn DialogTitle(props: DialogTitleProps) -> Element {
|
||||
let base = attributes!(h2 {
|
||||
class: Styles::dx_dialog_title,
|
||||
class: "dx-dialog-title",
|
||||
});
|
||||
let merged = merge_attributes(vec![base, props.attributes]);
|
||||
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/dialog", css: STYLE }
|
||||
|
||||
dialog::DialogTitle {
|
||||
id: props.id,
|
||||
attributes: merged,
|
||||
@@ -48,11 +151,13 @@ pub fn DialogTitle(props: DialogTitleProps) -> Element {
|
||||
#[component]
|
||||
pub fn DialogDescription(props: DialogDescriptionProps) -> Element {
|
||||
let base = attributes!(p {
|
||||
class: Styles::dx_dialog_description,
|
||||
class: "dx-dialog-description",
|
||||
});
|
||||
let merged = merge_attributes(vec![base, props.attributes]);
|
||||
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/dialog", css: STYLE }
|
||||
|
||||
dialog::DialogDescription {
|
||||
id: props.id,
|
||||
attributes: merged,
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
.dx-dialog-backdrop {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
background: rgb(23 29 24 / 55%);
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.dx-dialog-backdrop[data-state="closed"] {
|
||||
animation: dx-dialog-backdrop-animate-out 150ms ease-in forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes dx-dialog-backdrop-animate-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dx-dialog-backdrop[data-state="open"] {
|
||||
animation: dx-dialog-backdrop-animate-in 150ms ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes dx-dialog-backdrop-animate-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dx-dialog {
|
||||
position: fixed;
|
||||
z-index: 1001;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: calc(100% - 2rem);
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem 1.25rem 1.25rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
margin: 0;
|
||||
background: var(--surface);
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 35%);
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
gap: 0.75rem;
|
||||
text-align: center;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.dx-dialog-title {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dx-dialog-description {
|
||||
margin: 0;
|
||||
color: var(--text-2);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (width >= 40rem) {
|
||||
.dx-dialog {
|
||||
max-width: 28rem;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.dx-dialog-close {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
align-self: flex-start;
|
||||
padding: 0;
|
||||
border: none;
|
||||
margin: 0;
|
||||
background: none;
|
||||
color: var(--text-2);
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dx-dialog-close:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
@@ -1,7 +1,46 @@
|
||||
use dioxus::prelude::*;
|
||||
#[css_module("/src/components/input/style.css")]
|
||||
struct Styles;
|
||||
|
||||
const STYLE: &str = r#".dx-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--border);
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
gap: 0.25rem;
|
||||
outline: none;
|
||||
transition: background-color 100ms ease-out, box-shadow 100ms ease-out;
|
||||
}
|
||||
|
||||
.dx-input::placeholder {
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.dx-input:disabled {
|
||||
color: var(--text-2);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.dx-input:hover:not(:disabled) {
|
||||
background-color: color-mix(in oklab, var(--surface-2) 90%, var(--text));
|
||||
}
|
||||
|
||||
.dx-input:focus-visible {
|
||||
background-color: var(--surface);
|
||||
box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 2px color-mix(in oklab, var(--accent) 35%, transparent);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
"#;
|
||||
#[component]
|
||||
pub fn Input(
|
||||
oninput: Option<EventHandler<FormEvent>>,
|
||||
@@ -29,8 +68,10 @@ pub fn Input(
|
||||
children: Element,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/input", css: STYLE }
|
||||
|
||||
input {
|
||||
class: Styles::dx_input,
|
||||
class: "dx-input",
|
||||
oninput: move |e| _ = oninput.map(|callback| callback(e)),
|
||||
onchange: move |e| _ = onchange.map(|callback| callback(e)),
|
||||
oninvalid: move |e| _ = oninvalid.map(|callback| callback(e)),
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
.dx-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--border);
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
gap: 0.25rem;
|
||||
outline: none;
|
||||
transition: background-color 100ms ease-out, box-shadow 100ms ease-out;
|
||||
}
|
||||
|
||||
.dx-input::placeholder {
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.dx-input:disabled {
|
||||
color: var(--text-2);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.dx-input:hover:not(:disabled) {
|
||||
background-color: color-mix(in oklab, var(--surface-2) 90%, var(--text));
|
||||
}
|
||||
|
||||
.dx-input:focus-visible {
|
||||
background-color: var(--surface);
|
||||
box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 2px color-mix(in oklab, var(--accent) 35%, transparent);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
@@ -1,13 +1,23 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_primitives::label::{self, LabelProps};
|
||||
#[css_module("/src/components/label/style.css")]
|
||||
struct Styles;
|
||||
|
||||
const STYLE: &str = r#".dx-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-2);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
"#;
|
||||
#[component]
|
||||
pub fn Label(props: LabelProps) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/label", css: STYLE }
|
||||
|
||||
label::Label {
|
||||
class: Styles::dx_label,
|
||||
class: "dx-label",
|
||||
html_for: props.html_for,
|
||||
attributes: props.attributes,
|
||||
{props.children}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.dx-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-2);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
@@ -3,9 +3,11 @@ pub mod badge;
|
||||
pub mod button;
|
||||
pub mod card;
|
||||
pub mod checkbox;
|
||||
pub mod control;
|
||||
pub mod dialog;
|
||||
pub mod input;
|
||||
pub mod label;
|
||||
pub mod prompt;
|
||||
pub mod scroll_area;
|
||||
pub mod separator;
|
||||
pub mod switch;
|
||||
@@ -16,9 +18,11 @@ pub use badge::*;
|
||||
pub use button::*;
|
||||
pub use card::*;
|
||||
pub use checkbox::*;
|
||||
pub use control::*;
|
||||
pub use dialog::*;
|
||||
pub use input::*;
|
||||
pub use label::*;
|
||||
pub use prompt::*;
|
||||
pub use scroll_area::*;
|
||||
pub use separator::*;
|
||||
pub use switch::*;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
const STYLE: &str = r#".dx-prompt {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--surface);
|
||||
box-shadow: none;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dx-prompt-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 1.5rem;
|
||||
max-height: 1.5rem;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
border: none;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5rem;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dx-prompt-input::placeholder {
|
||||
color: var(--text-2);
|
||||
}
|
||||
"#;
|
||||
|
||||
#[component]
|
||||
pub fn Prompt(
|
||||
/// Controlled value of the prompt text.
|
||||
#[props(default)]
|
||||
value: String,
|
||||
/// Placeholder shown when empty.
|
||||
#[props(default = "Ask anything…".to_string())]
|
||||
placeholder: String,
|
||||
/// Autofocus the textarea when mounted (default true).
|
||||
#[props(default = true)]
|
||||
autofocus: bool,
|
||||
oninput: Option<EventHandler<FormEvent>>,
|
||||
onkeydown: Option<EventHandler<KeyboardEvent>>,
|
||||
#[props(extends = GlobalAttributes)]
|
||||
attributes: Vec<Attribute>,
|
||||
) -> Element {
|
||||
let mut textarea_el = use_signal(|| None::<std::rc::Rc<MountedData>>);
|
||||
|
||||
use_effect(move || {
|
||||
if !autofocus {
|
||||
return;
|
||||
}
|
||||
if let Some(el) = textarea_el() {
|
||||
spawn(async move {
|
||||
let _ = el.set_focus(true).await;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/prompt", css: STYLE }
|
||||
|
||||
div {
|
||||
class: "dx-prompt",
|
||||
"data-slot": "prompt",
|
||||
..attributes,
|
||||
textarea {
|
||||
class: "dx-prompt-input",
|
||||
"data-slot": "prompt-input",
|
||||
value,
|
||||
placeholder,
|
||||
rows: 1,
|
||||
onmounted: move |e| textarea_el.set(Some(e.data())),
|
||||
oninput: move |e| {
|
||||
if let Some(cb) = &oninput {
|
||||
cb.call(e);
|
||||
}
|
||||
},
|
||||
onkeydown: move |e| {
|
||||
// Enter does nothing for now (no submit).
|
||||
if e.key() == Key::Enter {
|
||||
e.prevent_default();
|
||||
}
|
||||
if let Some(cb) = &onkeydown {
|
||||
cb.call(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mod component;
|
||||
pub use component::*;
|
||||
@@ -1 +0,0 @@
|
||||
/* Scroll area uses unstyled dioxus-primitives; no extra styles for MVP. */
|
||||
@@ -2,17 +2,31 @@ use dioxus::prelude::*;
|
||||
use dioxus_primitives::separator::{self, SeparatorProps};
|
||||
use dioxus_primitives::{dioxus_attributes::attributes, merge_attributes};
|
||||
|
||||
#[css_module("/src/components/separator/style.css")]
|
||||
struct Styles;
|
||||
|
||||
const STYLE: &str = r#".dx-separator {
|
||||
background-color: var(--border);
|
||||
}
|
||||
|
||||
.dx-separator[data-orientation="horizontal"] {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.dx-separator[data-orientation="vertical"] {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
}
|
||||
"#;
|
||||
#[component]
|
||||
pub fn Separator(props: SeparatorProps) -> Element {
|
||||
let base = attributes!(div {
|
||||
class: Styles::dx_separator,
|
||||
class: "dx-separator",
|
||||
});
|
||||
let merged = merge_attributes(vec![base, props.attributes]);
|
||||
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/separator", css: STYLE }
|
||||
|
||||
separator::Separator {
|
||||
horizontal: props.horizontal,
|
||||
decorative: props.decorative,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
.dx-separator {
|
||||
background-color: var(--border);
|
||||
}
|
||||
|
||||
.dx-separator[data-orientation="horizontal"] {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.dx-separator[data-orientation="vertical"] {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,14 +1,56 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_primitives::switch::{self, SwitchProps};
|
||||
|
||||
#[css_module("/src/components/switch/style.css")]
|
||||
struct Styles;
|
||||
|
||||
const STYLE: &str = r#".dx-switch {
|
||||
all: unset;
|
||||
position: relative;
|
||||
width: 2rem;
|
||||
height: 1.15rem;
|
||||
border-radius: 9999px;
|
||||
background-color: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--border);
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms, box-shadow 150ms;
|
||||
}
|
||||
|
||||
.dx-switch[data-state="checked"] {
|
||||
background-color: var(--accent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dx-switch:focus-visible {
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 45%, transparent);
|
||||
}
|
||||
|
||||
.dx-switch-thumb {
|
||||
display: block;
|
||||
width: calc(1.15rem - 2px);
|
||||
height: calc(1.15rem - 2px);
|
||||
border-radius: 9999px;
|
||||
background-color: var(--text);
|
||||
transform: translateX(1px);
|
||||
transition: transform 150ms, background-color 150ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.dx-switch[data-state="checked"] .dx-switch-thumb {
|
||||
background-color: var(--on-accent);
|
||||
transform: translateX(calc(2rem - 1px - (1.15rem - 2px)));
|
||||
}
|
||||
|
||||
.dx-switch[data-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
"#;
|
||||
#[component]
|
||||
pub fn Switch(props: SwitchProps) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/switch", css: STYLE }
|
||||
|
||||
switch::Switch {
|
||||
class: Styles::dx_switch,
|
||||
class: "dx-switch",
|
||||
checked: props.checked,
|
||||
default_checked: props.default_checked,
|
||||
disabled: props.disabled,
|
||||
@@ -17,7 +59,7 @@ pub fn Switch(props: SwitchProps) -> Element {
|
||||
value: props.value,
|
||||
on_checked_change: props.on_checked_change,
|
||||
attributes: props.attributes,
|
||||
switch::SwitchThumb { class: Styles::dx_switch_thumb }
|
||||
switch::SwitchThumb { class: "dx-switch-thumb" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
.dx-switch {
|
||||
all: unset;
|
||||
position: relative;
|
||||
width: 2rem;
|
||||
height: 1.15rem;
|
||||
border-radius: 9999px;
|
||||
background-color: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--border);
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms, box-shadow 150ms;
|
||||
}
|
||||
|
||||
.dx-switch[data-state="checked"] {
|
||||
background-color: var(--accent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dx-switch:focus-visible {
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 45%, transparent);
|
||||
}
|
||||
|
||||
.dx-switch-thumb {
|
||||
display: block;
|
||||
width: calc(1.15rem - 2px);
|
||||
height: calc(1.15rem - 2px);
|
||||
border-radius: 9999px;
|
||||
background-color: var(--text);
|
||||
transform: translateX(1px);
|
||||
transition: transform 150ms, background-color 150ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.dx-switch[data-state="checked"] .dx-switch-thumb {
|
||||
background-color: var(--on-accent);
|
||||
transform: translateX(calc(2rem - 1px - (1.15rem - 2px)));
|
||||
}
|
||||
|
||||
.dx-switch[data-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -1,8 +1,91 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[css_module("/src/components/textarea/style.css")]
|
||||
struct Styles;
|
||||
|
||||
const STYLE: &str = r#".dx-textarea {
|
||||
width: 100%;
|
||||
min-height: 4rem;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
background: none;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
transition: background-color 100ms ease-out, border-color 100ms ease-out, box-shadow 100ms ease-out;
|
||||
}
|
||||
|
||||
.dx-textarea:disabled {
|
||||
color: var(--text-2);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.dx-textarea::placeholder {
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="default"] {
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--border);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="default"]:hover:not(:disabled),
|
||||
.dx-textarea[data-style="default"]:focus {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="default"]:focus {
|
||||
box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 2px color-mix(in oklab, var(--accent) 35%, transparent);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="fade"] {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="fade"]:hover:not(:disabled),
|
||||
.dx-textarea[data-style="fade"]:focus {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="outline"] {
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--surface);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="outline"]:hover:not(:disabled, :focus) {
|
||||
border-color: color-mix(in oklab, var(--border) 70%, var(--text));
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="outline"]:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="outline"]:invalid,
|
||||
.dx-textarea[data-style="outline"][aria-invalid="true"] {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="ghost"] {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="ghost"]:hover:not(:disabled) {
|
||||
background-color: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="ghost"]:focus {
|
||||
box-shadow: inset 0 0 0 1px var(--accent);
|
||||
}
|
||||
"#;
|
||||
#[derive(Copy, Clone, PartialEq, Default)]
|
||||
#[non_exhaustive]
|
||||
pub enum TextareaVariant {
|
||||
@@ -52,8 +135,10 @@ pub fn Textarea(
|
||||
children: Element,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
crate::OnceStyle { href: "dioxus-components/textarea", css: STYLE }
|
||||
|
||||
textarea {
|
||||
class: Styles::dx_textarea,
|
||||
class: "dx-textarea",
|
||||
"data-slot": "textarea",
|
||||
"data-style": variant.class(),
|
||||
oninput: move |e| _ = oninput.map(|callback| callback(e)),
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
.dx-textarea {
|
||||
width: 100%;
|
||||
min-height: 4rem;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
background: none;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
transition: background-color 100ms ease-out, border-color 100ms ease-out, box-shadow 100ms ease-out;
|
||||
}
|
||||
|
||||
.dx-textarea:disabled {
|
||||
color: var(--text-2);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.dx-textarea::placeholder {
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="default"] {
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--border);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="default"]:hover:not(:disabled),
|
||||
.dx-textarea[data-style="default"]:focus {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="default"]:focus {
|
||||
box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 2px color-mix(in oklab, var(--accent) 35%, transparent);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="fade"] {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="fade"]:hover:not(:disabled),
|
||||
.dx-textarea[data-style="fade"]:focus {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="outline"] {
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--surface);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="outline"]:hover:not(:disabled, :focus) {
|
||||
border-color: color-mix(in oklab, var(--border) 70%, var(--text));
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="outline"]:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="outline"]:invalid,
|
||||
.dx-textarea[data-style="outline"][aria-invalid="true"] {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="ghost"] {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="ghost"]:hover:not(:disabled) {
|
||||
background-color: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dx-textarea[data-style="ghost"]:focus {
|
||||
box-shadow: inset 0 0 0 1px var(--accent);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Styled Dioxus UI components using the mize color palette.
|
||||
//!
|
||||
//! Include [`THEME`] once at the app root (e.g. a stylesheet link), and set
|
||||
//! `html[data-theme="light"]` or `html[data-theme="dark"]` to switch themes.
|
||||
//! Include [`THEME_CSS`] once at the app root via `document::Style`, and set
|
||||
//! `html[data-theme="light"]` or `html[data-theme="dark"]` (or a scoped
|
||||
//! `data-theme` ancestor) to switch themes.
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
@@ -10,5 +11,75 @@ use dioxus::prelude::*;
|
||||
pub mod components;
|
||||
pub use components::*;
|
||||
|
||||
/// Global theme stylesheet (`assets/theme.css`). Link this once in your app.
|
||||
pub const THEME: Asset = asset!("/assets/theme.css");
|
||||
/// Global theme CSS variables. Inject with `document::Style { {THEME_CSS} }`.
|
||||
pub const THEME_CSS: &str = r#"/* mize UI theme — include once; toggle via html[data-theme="light"|"dark"] */
|
||||
|
||||
html[data-theme="dark"] {
|
||||
--dark: initial;
|
||||
--light: ;
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
--dark: ;
|
||||
--light: initial;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) {
|
||||
--dark: initial;
|
||||
--light: ;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root:not([data-theme]) {
|
||||
--dark: ;
|
||||
--light: initial;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: var(--dark, #171D18) var(--light, #F7F8F4);
|
||||
--surface: var(--dark, #242D25) var(--light, #FFFFFF);
|
||||
--surface-2: var(--dark, #2C362D) var(--light, #EFF2EA);
|
||||
--text: var(--dark, #F2F4EA) var(--light, #20251C);
|
||||
--text-2: var(--dark, #AAB5A4) var(--light, #66705E);
|
||||
--border: var(--dark, #3A463A) var(--light, #DFE4D9);
|
||||
--accent: var(--dark, #D7E47D) var(--light, #FEC251);
|
||||
--accent-text: var(--dark, #D7E47D) var(--light, #79500F);
|
||||
--success: var(--dark, #B9D987) var(--light, #526F32);
|
||||
--info: var(--dark, #B9E4EB) var(--light, #376777);
|
||||
--warning: var(--dark, #FFD77F) var(--light, #A66B13);
|
||||
--danger: var(--dark, #FF9B8E) var(--light, #B94D42);
|
||||
|
||||
/* On-accent fill text: dark ink on light accent, dark surface on dark accent */
|
||||
--on-accent: var(--dark, #171D18) var(--light, #79500F);
|
||||
--focus-ring: var(--accent);
|
||||
}
|
||||
"#;
|
||||
|
||||
/// Injects a `<style>` into the document head exactly once.
|
||||
///
|
||||
/// Wrap `document::Style` in a component whose props are stable (`&'static str`)
|
||||
/// so parent re-renders do not diff Style props (which dioxus does not support
|
||||
/// and warns about).
|
||||
#[component]
|
||||
pub fn OnceStyle(href: &'static str, css: &'static str) -> Element {
|
||||
rsx! {
|
||||
document::Style {
|
||||
href: "{href}",
|
||||
{css}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience component that injects [`THEME_CSS`] into the document head.
|
||||
#[component]
|
||||
pub fn ThemeStyle() -> Element {
|
||||
rsx! {
|
||||
OnceStyle {
|
||||
href: "dioxus-components/theme",
|
||||
css: THEME_CSS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
cargo-features = ["profile-rustflags"]
|
||||
|
||||
[package]
|
||||
name = "mize-part-auth"
|
||||
name = "mize-part-mme"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "mize_part_auth"
|
||||
crate-type = [ "cdylib", "rlib" ]
|
||||
name = "mize_part_mme"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
mize = { workspace = true }
|
||||
log = "0.4.34"
|
||||
|
||||
mize = { workspace = true }
|
||||
dioxus = { version = "0.7", features = ["desktop"] }
|
||||
dioxus-components = { path = "../mize.ui/dioxus-components", features = ["desktop"] }
|
||||
dioxus-icons = "0.1.0"
|
||||
|
||||
[profile.dev]
|
||||
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
|
||||
|
||||
|
||||
[profile.release]
|
||||
rustflags = ["-C", "prefer-dynamic"]
|
||||
389
parts/mme/src/dashboard.rs
Normal file
389
parts/mme/src/dashboard.rs
Normal file
@@ -0,0 +1,389 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_components::{Button, ButtonSize, ButtonVariant, Control, ControlKind, Prompt};
|
||||
use dioxus_icons::lucide::{Bell, Bot, Check, Copy, LogIn, Mic, Moon, Power, Settings, Shield, Sun, Wifi};
|
||||
|
||||
use crate::{read_hub_state, sleep_ms, start_login};
|
||||
|
||||
fn copy_to_clipboard(text: &str) {
|
||||
let mut literal = String::from('"');
|
||||
for c in text.chars() {
|
||||
match c {
|
||||
'"' => literal.push_str("\\\""),
|
||||
'\\' => literal.push_str("\\\\"),
|
||||
'\n' => literal.push_str("\\n"),
|
||||
'\r' => literal.push_str("\\r"),
|
||||
'\t' => literal.push_str("\\t"),
|
||||
c if c.is_control() => literal.push_str(&format!("\\u{:04x}", c as u32)),
|
||||
c => literal.push(c),
|
||||
}
|
||||
}
|
||||
literal.push('"');
|
||||
let _ = document::eval(&format!("navigator.clipboard.writeText({literal})"));
|
||||
}
|
||||
|
||||
const APP_CSS: &str = r#"
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: #171D18;
|
||||
color: #F2F4EA;
|
||||
font-family: "Segoe UI", "IBM Plex Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
#main, #main > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.mme-hub {
|
||||
/* Palette: dark defaults; light overrides below. Values match the mize token table. */
|
||||
--bg: #171D18;
|
||||
--surface: #242D25;
|
||||
--surface-2: #2C362D;
|
||||
--text: #F2F4EA;
|
||||
--text-2: #AAB5A4;
|
||||
--border: #3A463A;
|
||||
--accent: #D7E47D;
|
||||
--accent-text: #D7E47D;
|
||||
--success: #B9D987;
|
||||
--info: #B9E4EB;
|
||||
--warning: #FFD77F;
|
||||
--danger: #FF9B8E;
|
||||
--on-accent: #171D18;
|
||||
--focus-ring: var(--accent);
|
||||
--tile-size: 6.75rem;
|
||||
--tile-gap: 0.65rem;
|
||||
--prompt-gap: 2.25rem;
|
||||
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 1rem 1.25rem 1.5rem;
|
||||
gap: var(--prompt-gap);
|
||||
overflow: auto;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
.mme-hub[data-theme="light"] {
|
||||
--bg: #F7F8F4;
|
||||
--surface: #FFFFFF;
|
||||
--surface-2: #EFF2EA;
|
||||
--text: #20251C;
|
||||
--text-2: #66705E;
|
||||
--border: #DFE4D9;
|
||||
--accent: #FEC251;
|
||||
--accent-text: #79500F;
|
||||
--success: #526F32;
|
||||
--info: #376777;
|
||||
--warning: #A66B13;
|
||||
--danger: #B94D42;
|
||||
--on-accent: #79500F;
|
||||
}
|
||||
.mme-topbar {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
width: 100%;
|
||||
}
|
||||
.mme-topbar-prompt {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.mme-topbar-actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.mme-login-banner {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
gap: 0.55rem;
|
||||
padding: 0.75rem 0.9rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
.mme-login-banner-label {
|
||||
color: var(--text-2);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.mme-login-banner-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mme-login-banner-code {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
color: var(--accent-text);
|
||||
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.mme-login-banner-url {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
color: var(--info);
|
||||
font-size: 0.8125rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
.mme-tiles {
|
||||
display: grid;
|
||||
flex: 1 1 auto;
|
||||
align-content: start;
|
||||
justify-content: start;
|
||||
justify-items: center;
|
||||
gap: var(--tile-gap);
|
||||
grid-template-columns: repeat(3, var(--tile-size));
|
||||
}
|
||||
.mme-hub-tile {
|
||||
width: var(--tile-size);
|
||||
height: var(--tile-size);
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.mme-hub {
|
||||
--tile-size: 5.75rem;
|
||||
--prompt-gap: 1.35rem;
|
||||
}
|
||||
.mme-topbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mme-topbar-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.mme-tiles {
|
||||
grid-template-columns: repeat(2, var(--tile-size));
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
pub fn launch() {
|
||||
dioxus::LaunchBuilder::new()
|
||||
.with_cfg(
|
||||
dioxus::desktop::Config::new()
|
||||
.with_menu(None)
|
||||
.with_window(
|
||||
dioxus::desktop::WindowBuilder::new().with_title("mme"),
|
||||
),
|
||||
)
|
||||
.launch(App);
|
||||
}
|
||||
|
||||
/// Isolated so hub re-renders never diff `document::Style` props.
|
||||
#[component]
|
||||
fn AppStyle() -> Element {
|
||||
rsx! {
|
||||
document::Style { {APP_CSS} }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
rsx! {
|
||||
AppStyle {}
|
||||
document::Title { "mme" }
|
||||
Hub {}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Hub() -> Element {
|
||||
let mut prompt = use_signal(String::new);
|
||||
let mut mic_on = use_signal(|| false);
|
||||
let mut wifi_on = use_signal(|| true);
|
||||
let mut shield_on = use_signal(|| true);
|
||||
let mut dark = use_signal(|| true);
|
||||
let mut bot_armed = use_signal(|| false);
|
||||
|
||||
let initial = read_hub_state().unwrap_or_default();
|
||||
let mut logged_in = use_signal(|| initial.logged_in);
|
||||
let mut logging_in = use_signal(|| initial.logging_in);
|
||||
let mut login_url = use_signal(|| initial.login_url.clone());
|
||||
let mut login_code = use_signal(|| initial.login_code.clone());
|
||||
|
||||
use_future(move || async move {
|
||||
loop {
|
||||
sleep_ms(200).await;
|
||||
let Ok(hub) = read_hub_state() else {
|
||||
continue;
|
||||
};
|
||||
// Only write signals when values change — avoids re-rendering the
|
||||
// whole hub (and its nested document::Style tags) every tick.
|
||||
if logged_in() != hub.logged_in {
|
||||
logged_in.set(hub.logged_in);
|
||||
}
|
||||
if logging_in() != hub.logging_in {
|
||||
logging_in.set(hub.logging_in);
|
||||
}
|
||||
if login_url() != hub.login_url {
|
||||
login_url.set(hub.login_url.clone());
|
||||
}
|
||||
if login_code() != hub.login_code {
|
||||
login_code.set(hub.login_code.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let theme = if dark() { "dark" } else { "light" };
|
||||
let show_banner = logging_in() && (login_url().is_some() || login_code().is_some());
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "mme-hub",
|
||||
"data-theme": theme,
|
||||
|
||||
div { class: "mme-topbar",
|
||||
div { class: "mme-topbar-prompt",
|
||||
Prompt {
|
||||
value: prompt(),
|
||||
placeholder: "Ask or run a command…".to_string(),
|
||||
oninput: move |e: FormEvent| prompt.set(e.value()),
|
||||
}
|
||||
}
|
||||
div { class: "mme-topbar-actions",
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
onclick: move |_| dark.set(!dark()),
|
||||
if dark() {
|
||||
Moon {}
|
||||
} else {
|
||||
Sun {}
|
||||
}
|
||||
if dark() { "Dark" } else { "Light" }
|
||||
}
|
||||
if logged_in() {
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
onclick: move |_| {},
|
||||
Check {}
|
||||
"Logged in"
|
||||
}
|
||||
} else if logging_in() {
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
onclick: move |_| {},
|
||||
LogIn {}
|
||||
"Waiting…"
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
variant: ButtonVariant::Primary,
|
||||
onclick: move |_| start_login(),
|
||||
LogIn {}
|
||||
"PPC Login"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if show_banner {
|
||||
div { class: "mme-login-banner",
|
||||
span { class: "mme-login-banner-label", "PPC device login" }
|
||||
if let Some(code) = login_code() {
|
||||
div { class: "mme-login-banner-row",
|
||||
span { class: "mme-login-banner-code", "{code}" }
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
size: ButtonSize::Sm,
|
||||
onclick: move |_| copy_to_clipboard(&code),
|
||||
Copy {}
|
||||
"Copy code"
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(url) = login_url() {
|
||||
div { class: "mme-login-banner-row",
|
||||
span { class: "mme-login-banner-url", "{url}" }
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
size: ButtonSize::Sm,
|
||||
onclick: move |_| copy_to_clipboard(&url),
|
||||
Copy {}
|
||||
"Copy link"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "mme-tiles",
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Mic".to_string(),
|
||||
kind: ControlKind::Toggle,
|
||||
enabled: mic_on(),
|
||||
confirm: Some("Enable microphone access for voice input?".into()),
|
||||
on_enabled_change: move |v| mic_on.set(v),
|
||||
Mic {}
|
||||
}
|
||||
}
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Alerts".to_string(),
|
||||
kind: ControlKind::Action,
|
||||
onclick: move |_| {},
|
||||
Bell {}
|
||||
}
|
||||
}
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Network".to_string(),
|
||||
kind: ControlKind::Toggle,
|
||||
enabled: wifi_on(),
|
||||
on_enabled_change: move |v| wifi_on.set(v),
|
||||
Wifi {}
|
||||
}
|
||||
}
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Agent".to_string(),
|
||||
kind: ControlKind::Toggle,
|
||||
enabled: bot_armed(),
|
||||
confirm: Some("Arm the agent for autonomous actions?".into()),
|
||||
on_enabled_change: move |v| bot_armed.set(v),
|
||||
Bot {}
|
||||
}
|
||||
}
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Status".to_string(),
|
||||
kind: ControlKind::Display,
|
||||
value: Some(if bot_armed() { "armed".into() } else { "idle".into() }),
|
||||
Power {}
|
||||
}
|
||||
}
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Shield".to_string(),
|
||||
kind: ControlKind::Toggle,
|
||||
enabled: shield_on(),
|
||||
confirm: Some("Disable shield? This turns off local protections.".into()),
|
||||
on_enabled_change: move |v| shield_on.set(v),
|
||||
Shield {}
|
||||
}
|
||||
}
|
||||
div { class: "mme-hub-tile",
|
||||
Control {
|
||||
name: "Settings".to_string(),
|
||||
kind: ControlKind::Action,
|
||||
onclick: move |_| {},
|
||||
Settings {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
parts/mme/src/generated.rs
Normal file
83
parts/mme/src/generated.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use mize::prelude::*;
|
||||
|
||||
pub struct CliPart {
|
||||
id: MizeId,
|
||||
}
|
||||
|
||||
impl MizePart for CliPart {
|
||||
fn id(&self) -> MizeResult<MizeId> {
|
||||
Ok(self.id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl CliPart {
|
||||
pub fn get(name: &str) -> MizeResult<CliPart> {
|
||||
let mut mize = get_global_mize()?;
|
||||
let part = mize.get_part(name)?;
|
||||
Ok(CliPart { id: part.id()? })
|
||||
}
|
||||
|
||||
pub fn run(&self, cmdline: Vec<String>) -> MizeResult<()> {
|
||||
let cmdline = cmdline.into_item_data();
|
||||
let mut args = ItemData::new();
|
||||
args.set_path("0", cmdline)?;
|
||||
dyn_call(self.id.clone(), "run", args)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_command(&self, name: &str) -> MizeResult<()> {
|
||||
let name = name.into_item_data();
|
||||
let mut args = ItemData::new();
|
||||
args.set_path("0", name)?;
|
||||
dyn_call(self.id.clone(), "set_command", args)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn subcommand<T: Fn(ArgMatches) -> MizeResult<()> + Send + Sync + 'static>(
|
||||
&self,
|
||||
subcmd: Command,
|
||||
action: T,
|
||||
) -> MizeResult<()> {
|
||||
let mut subcmd_arg = ItemData::new();
|
||||
subcmd_arg.set_path("_native", subcmd.native_id)?;
|
||||
let action_arg = ItemData::fun(Box::new(move |args: ItemData| {
|
||||
let matches = args.clone().get_path("0")?.native_take_from::<ArgMatches>()?;
|
||||
action(matches)?;
|
||||
Ok(ItemData::new())
|
||||
}))?;
|
||||
let mut args = ItemData::new();
|
||||
args.set_path("0", subcmd_arg)?;
|
||||
args.set_path("1", action_arg)?;
|
||||
dyn_call(self.id.clone(), "subcommand", args)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Command {
|
||||
native_id: u32,
|
||||
}
|
||||
|
||||
pub struct ArgMatches {
|
||||
native_id: u32,
|
||||
}
|
||||
|
||||
impl FromItemData for ArgMatches {
|
||||
fn from_itemdata(data: ItemData) -> MizeResult<Self> {
|
||||
Ok(ArgMatches {
|
||||
native_id: data.get_path("_native")?.value_u32()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn new(name: &str) -> MizeResult<Command> {
|
||||
let name = name.into_item_data();
|
||||
let mut args = ItemData::new();
|
||||
args.set_path("0", name)?;
|
||||
let mut mize = get_global_mize()?;
|
||||
let id = mize.id_new_dots("cli.Command")?;
|
||||
let data = dyn_call(id.clone(), "new", args)?;
|
||||
let native_id: u32 = data.get_path("_native")?.native_take()?;
|
||||
Ok(Command { native_id })
|
||||
}
|
||||
}
|
||||
97
parts/mme/src/lib.rs
Normal file
97
parts/mme/src/lib.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
mod dashboard;
|
||||
mod generated;
|
||||
|
||||
use generated::{CliPart, Command};
|
||||
use mize::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct HubState {
|
||||
pub login_url: Option<String>,
|
||||
pub login_code: Option<String>,
|
||||
pub logged_in: bool,
|
||||
pub logging_in: bool,
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
extern "C" fn part_mme() -> MizeResult<()> {
|
||||
opt("hub").fun(Box::new(|| Ok(ItemData::from_native(HubState::default())?)));
|
||||
|
||||
method("set_url_and_code", "String, String -> ()", |args| {
|
||||
let url = args.get_path("0")?.value_string()?;
|
||||
let code = args.get_path("1")?.value_string()?;
|
||||
let mut hub = get_config("self.hub")?.native_mut::<HubState>()?;
|
||||
hub.login_url = Some(url);
|
||||
hub.login_code = Some(code);
|
||||
hub.logged_in = false;
|
||||
hub.logging_in = true;
|
||||
Ok(ItemData::new())
|
||||
});
|
||||
|
||||
method("set_login_state", "bool -> ()", |args| {
|
||||
let logged_in = args.get_path("0")?.value_bool()?;
|
||||
let mut hub = get_config("self.hub")?.native_mut::<HubState>()?;
|
||||
hub.logged_in = logged_in;
|
||||
hub.logging_in = false;
|
||||
if logged_in {
|
||||
hub.login_url = None;
|
||||
hub.login_code = None;
|
||||
}
|
||||
Ok(ItemData::new())
|
||||
});
|
||||
|
||||
// Seed logged-in from existing secrets, if any.
|
||||
if let Ok(secrets) = get_config("auth.client.secrets") {
|
||||
if !secrets.cbor().is_null() {
|
||||
let mut hub = get_config("self.hub")?.native_mut::<HubState>()?;
|
||||
hub.logged_in = true;
|
||||
}
|
||||
}
|
||||
|
||||
let cli = CliPart::get("cli")?;
|
||||
cli.set_command("mme")?;
|
||||
cli.subcommand(Command::new("main")?, |_| {
|
||||
dashboard::launch();
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_hub_state() -> MizeResult<HubState> {
|
||||
Ok(get_config("mme.hub")?.native::<HubState>()?.clone())
|
||||
}
|
||||
|
||||
pub fn start_login() {
|
||||
std::thread::spawn(|| {
|
||||
let result = (|| -> MizeResult<()> {
|
||||
{
|
||||
let mut hub = get_config("mme.hub")?.native_mut::<HubState>()?;
|
||||
hub.logging_in = true;
|
||||
hub.logged_in = false;
|
||||
}
|
||||
let mut mize = get_global_mize()?;
|
||||
dyn_call(mize.id_new_dots("auth.client")?, "login", ItemData::new())?;
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
if let Err(err) = result {
|
||||
out!("mme ppc login failed: {:?}", err);
|
||||
let _ = (|| -> MizeResult<()> {
|
||||
let mut hub = get_config("mme.hub")?.native_mut::<HubState>()?;
|
||||
hub.logging_in = false;
|
||||
hub.logged_in = false;
|
||||
Ok(())
|
||||
})();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Tiny async sleep without pulling tokio into the UI crate.
|
||||
pub async fn sleep_ms(ms: u64) {
|
||||
let (tx, rx) = mize::flume::bounded(0);
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(ms));
|
||||
let _ = tx.send(());
|
||||
});
|
||||
let _ = rx.recv_async().await;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ name = "mize_part_mos"
|
||||
crate-type = [ "cdylib", "rlib" ]
|
||||
|
||||
[dependencies]
|
||||
cmd_lib = "2.0.1"
|
||||
log = "0.4.34"
|
||||
mize = { workspace = true }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user