recent mize changes

This commit is contained in:
Sebastian Moser
2026-09-16 21:32:53 +00:00
parent 05d410949c
commit d559159397
45 changed files with 11247 additions and 45 deletions

10397
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,27 +4,34 @@ version = "0.0.1"
edition = "2021" edition = "2021"
license = "GPL-3.0" license = "GPL-3.0"
description = 'A strongly typed "filesystem" for the age of connectivity.' description = 'A strongly typed "filesystem" for the age of connectivity.'
repository = "https://github.com/c2vi/mize" repository = "https://git.ppc.social/ppc/mize"
[package.metadata.wasm-pack.profile.dev]
wasm-opt = false
[lib] [lib]
name = "mize" name = "mize"
path = "src/lib.rs" path = "src/lib.rs"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[[bin]]
name = "mize"
path = "src/platform/os/main.rs"
required-features = [ "target-os" ]
[features] [features]
default = [ "target-os" ] default = [ "target-os" ]
target-os = [ "clap", "home", "nix", "sysinfo", "ciborium/default", "tracing-subscriber", "async", "tracing-subscriber/env-filter", "libloading", "ciborium-io", "tracing-core", "tar", "flate2", "http_req", "tokio/net", "tokio", "tokio/rt-multi-thread", "tokio/io-util" ] target-os = [ "clap", "home", "nix", "sysinfo", "ciborium/default", "tracing-subscriber", "async", "tracing-subscriber/env-filter", "libloading", "ciborium-io", "tracing-core", "tar", "flate2", "http_req", "tokio/net", "tokio", "tokio/rt-multi-thread", "tokio/io-util" ]
target-js = [ "wasm-bindgen", "console_error_panic_hook", "web-sys", "web-sys/Worker", "web-sys/Window", "web-sys/Request", "web-sys/RequestInit", "web-sys/RequestMode", "web-sys/Response", "web-sys/WorkerOptions", "web-sys/WorkerType", "serde-wasm-bindgen", "wasm-bindgen-futures", "wee_alloc", "ciborium/default" ] target-js = [ "wasm-bindgen", "console_error_panic_hook", "web-sys", "web-sys/Worker", "web-sys/Window", "web-sys/Request", "web-sys/RequestInit", "web-sys/RequestMode", "web-sys/Response", "web-sys/WorkerOptions", "web-sys/WorkerType", "serde-wasm-bindgen", "wasm-bindgen-futures", "wee_alloc", "ciborium/default" ]
async = [ "tokio", ] async = [ "tokio", ]
[workspace]
members = [
"marts",
"macros",
"apps/os",
"apps/obsidian-plugin",
]
default-members = [
"apps/obsidian-plugin",
]
resolver = "2"
[package.metadata.wasm-pack.profile.dev]
wasm-opt = false # for debug build times
[dependencies] [dependencies]
# core dependencies # core dependencies
@@ -39,7 +46,7 @@ toml = { version = "0.8.13", features = ["parse"], optional = false }
dyn-clone = "1.0.17" dyn-clone = "1.0.17"
sha2 = "0.10.8" sha2 = "0.10.8"
flume = { version = "0.11.1", default-features = false, features = [ "async" ] } flume = { version = "0.11.1", default-features = false, features = [ "async" ] }
mize_macros = { path = "../macros" } mize_macros = { path = "./macros" }
async-trait = "0.1.89" async-trait = "0.1.89"
# wasm dependencies # wasm dependencies

View File

@@ -0,0 +1,8 @@
[package]
name = "mize-obsidian-plugin"
version = "0.1.0"
edition = "2024"
[dependencies]
mize = { path = "../.." }
marts = { path = "../../marts" }

View File

@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

8
apps/os/Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "mize-bin"
version = "0.1.0"
edition = "2024"
[dependencies]
mize = { path = "../../" }
marts = { path = "../../marts" }

View File

@@ -17,7 +17,7 @@
}; };
mize_modules = { mize_modules = {
url = "github:c2vi/mize-modules?submodules=1"; url = "github:c2vi/mize-modules";
flake = false; flake = false;
}; };
@@ -258,6 +258,7 @@ in {
pkgs.rsync pkgs.rsync
pkgs.openssh pkgs.openssh
pkgs.git pkgs.git
pkgs.cmake
# to build ../grid # to build ../grid
pkgs.rustPlatform.bindgenHook pkgs.rustPlatform.bindgenHook
@@ -300,6 +301,7 @@ in {
wasm-bindgen-cli wasm-bindgen-cli
lld_20 lld_20
xdotool xdotool
bzip2
]; ];
LD_LIBRARY_PATH = with pkgs; pkgs.lib.makeLibraryPath [ LD_LIBRARY_PATH = with pkgs; pkgs.lib.makeLibraryPath [
@@ -324,6 +326,9 @@ in {
libxcb libxcb
pipewire pipewire
wayland wayland
# for rust-analyzer to work for zed
bzip2
]; ];
LIBRARY_PATH = LD_LIBRARY_PATH; LIBRARY_PATH = LD_LIBRARY_PATH;

View File

@@ -11,7 +11,7 @@ crate-type = ["cdylib", "rlib"]
clap = { version = "4.5.59", optional = true, features = ["string"] } clap = { version = "4.5.59", optional = true, features = ["string"] }
flume = "0.12.0" flume = "0.12.0"
futures = "0.3.32" futures = "0.3.32"
mize = { path = "../mize/", default-features = false, features = [] } mize = { path = "../", default-features = false, features = [] }
tokio = { version = "1.49.0", default-features = false, features = ["time"] } tokio = { version = "1.49.0", default-features = false, features = ["time"] }
# https://github.com/rscarson/rustyscript/pull/415 # https://github.com/rscarson/rustyscript/pull/415
#rustyscript = { git = 'https://github.com/rscarson/rustyscript', rev = 'e39529eac2c5d37cfcbb7d7d63da7264d7a5afdb', optional = true } #rustyscript = { git = 'https://github.com/rscarson/rustyscript', rev = 'e39529eac2c5d37cfcbb7d7d63da7264d7a5afdb', optional = true }
@@ -19,7 +19,7 @@ tokio = { version = "1.49.0", default-features = false, features = ["time"] }
reqwest = { version = "0.13.2", features = ["blocking", "json"] } reqwest = { version = "0.13.2", features = ["blocking", "json"] }
serde = "1.0.228" serde = "1.0.228"
serde_json = "1.0.149" serde_json = "1.0.149"
rusqlite = { version = "0.31", features = ["bundled"], optional = true } rusqlite = { version = "0.37", features = ["bundled"], optional = true }
dioxus = { version = "0.7.1", optional = true } dioxus = { version = "0.7.1", optional = true }
spacetimedb-sdk = { version = "2.6.0", default-features = false } spacetimedb-sdk = { version = "2.6.0", default-features = false }
generated = { path = "./generated" } generated = { path = "./generated" }
@@ -39,7 +39,10 @@ log = "0.4"
chrono = { version = "0.4.45", features = [], default-features = false } chrono = { version = "0.4.45", features = [], default-features = false }
chrono-tz = { version = "0.10.4", default-features = false } chrono-tz = { version = "0.10.4", default-features = false }
url = "2.5.8" url = "2.5.8"
serenity = { version = "0.12.5", optional = true }
matrix-sdk = "0.18.0"
[features] [features]
default = ["target-os", "desktop", "server", "client"] default = ["target-os", "desktop", "server", "client"]
#default = ["target-js", "target-obsidian"] # to get rust analyzer to analyzer js only parts #default = ["target-js", "target-obsidian"] # to get rust analyzer to analyzer js only parts
@@ -51,3 +54,4 @@ desktop = ["dioxus/desktop"]
client = [] client = []
server = [] server = []
clap = ["dep:clap"] clap = ["dep:clap"]
serenity = ["dep:serenity"]

View File

@@ -108,7 +108,7 @@ impl ClientAuth {
pub fn refresh(&mut self) -> MizeResult<()> { pub fn refresh(&mut self) -> MizeResult<()> {
#[cfg(feature = "target-js")] { #[cfg(feature = "target-js")] {
return Err(mize_err!("refresh is not supported on wasm")); console_log!("refresh is not implemented yet on wasm");
} }
#[cfg(feature = "target-os")] { #[cfg(feature = "target-os")] {
@@ -321,6 +321,7 @@ pub fn part_client_auth(mize: &mut Mize) -> MizeResult<()> {
// register command for obsidian // register command for obsidian
#[cfg(feature = "target-obsidian")] { #[cfg(feature = "target-obsidian")] {
use crate::MartsCommand; use crate::MartsCommand;
console_log!("register obsidian command...");
let obsidian = mize.get_part_native::<crate::ObsidianPart>("obsidian")?; let obsidian = mize.get_part_native::<crate::ObsidianPart>("obsidian")?;
obsidian.add_command(MartsCommand { obsidian.add_command(MartsCommand {

View File

@@ -52,7 +52,7 @@ impl C2vi {
} }
} }
pub fn c2vi(mize: &mut Mize) -> MizeResult<()> { pub fn part_c2vi(mize: &mut Mize) -> MizeResult<()> {
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
if mize if mize
.get_config("c2vi.dash.enable") .get_config("c2vi.dash.enable")
@@ -62,6 +62,22 @@ pub fn c2vi(mize: &mut Mize) -> MizeResult<()> {
{ {
launch_c2vi_dashboard(mize); launch_c2vi_dashboard(mize);
} }
#[cfg(feature = "target-obsidian")] {
use crate::MartsCommand;
let obsidian = mize.get_part_native::<crate::ObsidianPart>("obsidian").unwrap();
obsidian.add_command(MartsCommand {
id: "sc2-publish-daily-journal".to_string(),
name: "SC2 Publish Daily Journal".to_string(),
callback: Box::new(|mut mize| {
let matrix = mize.get_part_native::<crate::matrix::MatrixPart>("matrix")?;
matrix.send_message(room_id);
Ok(())
}),
})?;
}
Ok(()) Ok(())
} }

167
marts/src/discord/mod.rs Normal file
View File

@@ -0,0 +1,167 @@
pub use mize::*;
// ========== WASM-specific imports (Obsidian plugin) ==========
#[cfg(feature = "target-obsidian")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "target-obsidian")]
use js_sys;
#[cfg(feature = "target-obsidian")]
use web_sys;
#[cfg(feature = "target-obsidian")]
use crate::obsidian::{ObsidianPart, MartsCommand};
// ========== Native-specific imports (OS daemon) ==========
#[cfg(feature = "target-os")]
use serenity::{
async_trait as serenity_async_trait,
client::{Client, Context, EventHandler},
model::{
channel::Message,
gateway::Ready,
id::ChannelId,
},
prelude::GatewayIntents,
};
#[cfg(feature = "target-os")]
use mize::async_trait;
#[derive(Clone)]
#[mize_part]
pub struct DiscordPart {
pub mize: Mize,
// Native: serenity client
#[cfg(feature = "target-os")]
pub client: Option<Client>,
}
impl DiscordPart {
// ========== WASM (Obsidian plugin) methods ==========
/// Read a file from the Obsidian vault using the Obsidian API
#[cfg(feature = "target-obsidian")]
pub fn read_obsidian_file(&self, path: &str) -> MizeResult<String> {
// TODO: Implement using js_sys to call Obsidian's vault.read() or similar
// Example approach:
use wasm_bindgen::JsCast;
use js_sys::Reflect;
let obsidian_part = self.mize.get_part_native::<ObsidianPart>("obsidian")?;
let plugin = &obsidian_part.plugin();
let app = Reflect::get(plugin, &"app".into())?;
let vault = Reflect::get(&app, &"vault".into())?;
let content = Reflect::call(&vault, &"read".into(), &path.into())?;
content.as_string().ok_or_else(|| mize_err!("Failed to read file"))
}
/// Send content to Discord channel using Discord REST API
#[cfg(feature = "target-obsidian")]
pub fn send_to_discord_rest(&self, content: String, channel_id: u64) -> MizeResult<()> {
// TODO: Implement using web_sys to call Discord REST API
// 1. Get token: mize.get_config("discord.token")
// 2. POST to https://discord.com/api/v10/channels/{channel_id}/messages
// 3. Headers: Authorization: Bot <token>, Content-Type: application/json
// 4. Body: {"content": content}
Ok(())
}
// ========== Native (OS daemon) methods ==========
/// Start the serenity Discord bot
#[cfg(feature = "target-os")]
pub async fn start_serenity_bot(&mut self, mize: &mut Mize) -> MizeResult<()> {
let token = mize.get_config("discord.token")?.value_string()?;
// Define event handler
struct Handler;
#[serenity_async_trait]
impl EventHandler for Handler {
async fn ready(&self, _: Context, ready: Ready) {
println!("Discord bot logged in as {}", ready.user.name);
}
// TODO: Add message handler for receiving commands from plugin
// This is where you'll implement communication with the Obsidian plugin
// Example:
// async fn message(&self, ctx: Context, msg: Message) {
// if msg.content.starts_with("!send") {
// // Parse command and send file to channel
// }
// }
}
let mut client = Client::builder(&token, GatewayIntents::non_privileged())
.event_handler(Handler)
.await?;
// Spawn the client in a tokio task
tokio::spawn({
let client_clone = client.clone();
async move {
if let Err(why) = client_clone.start().await {
eprintln!("Discord client error: {:?}", why);
}
}
});
self.client = Some(client);
Ok(())
}
}
impl MizePart for DiscordPart {
fn init(&mut self, mize: &mut Mize) -> MizeResult<()> {
// WASM: Add command to Obsidian
#[cfg(feature = "target-obsidian")]
{
let obsidian_part = mize.get_part_native::<ObsidianPart>("obsidian")?;
let callback = {
let mize_clone = mize.clone();
move |mize_inner: Mize| -> MizeResult<()> {
// Get configuration values
let file_path = mize_inner.get_config("sc2.shared_daily_journal_file")?
.value_string()?;
let channel_id_str = mize_inner.get_config("sc2.shared_daily_journal_channel")?
.value_string()?;
let channel_id = channel_id_str.parse::<u64>()?;
// Get DiscordPart and send the file
let mut mize_mut = mize_inner;
let discord_part = mize_mut.get_part_native::<DiscordPart>("discord")?;
let content = discord_part.read_obsidian_file(&file_path)?;
// TODO: COMMUNICATION WITH DAEMON GOES HERE
// For now, we send directly using REST API.
// Replace this with your plugin-to-daemon messaging logic.
discord_part.send_to_discord_rest(content, channel_id)?;
Ok(())
}
};
obsidian_part.add_command(MartsCommand {
id: "send_to_discord".to_string(),
name: "Send to Discord".to_string(),
callback: Box::new(callback),
})?;
}
Ok(())
}
// Native: Override async_init to start serenity bot
#[cfg(feature = "target-os")]
async fn async_init(&mut self, mize: &mut Mize) -> MizeResult<()> {
self.start_serenity_bot(mize).await
}
}
impl MizePartCreate for DiscordPart {}
pub fn part_discord(mize: &mut Mize) -> MizeResult<()> {
mize.register_part(Box::new(DiscordPart {
mize: mize.clone(),
#[cfg(feature = "target-os")]
client: None,
}))
}

View File

@@ -15,7 +15,6 @@ pub mod c2vi {
pub mod part; pub mod part;
pub mod dashboard; pub mod dashboard;
} }
#[cfg(feature = "target-os")]
pub use c2vi::part::*; pub use c2vi::part::*;
#[cfg(feature = "target-obsidian")] #[cfg(feature = "target-obsidian")]
@@ -36,6 +35,13 @@ pub mod ppc;
pub mod timetrack; pub mod timetrack;
#[cfg(feature = "target-obsidian")]
pub mod matrix;
#[cfg(feature = "target-obsidian")]
pub use matrix::part_matrix;
pub mod sc2;
//#[cfg(feature = "target-os")] //#[cfg(feature = "target-os")]
//pub mod js; //pub mod js;
//#[cfg(feature = "target-os")] //#[cfg(feature = "target-os")]

247
marts/src/matrix/mod.rs Normal file
View File

@@ -0,0 +1,247 @@
use mize::{Mize, MizeResult, MizePart, MizePartCreate, mize_part};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use js_sys::Function;
use web_sys::HtmlElement;
use matrix_sdk::{
Client, ClientConfig, SyncState,
ruma::{
user_id::UserId,
events::room::message::RoomMessageEventContent,
},
};
#[cfg(feature = "target-js")]
mod ui;
use ui::mount_chat_app;
const ROOM_ID: &str = "!XwqdXGPlmaLatdGUiN:ppc.social";
const ACCOUNT_NAME: &str = "default";
const INITIAL_LOAD_LIMIT: usize = 50;
/// Matrix part that manages Matrix client and UI
#[mize_part("matrix")]
#[derive(Clone)]
pub struct MatrixPart {
mize: Mize,
pub client: Option<Client>,
pub timeline: Vec<matrix_sdk::SyncRoomEvent>,
pub message_draft: String,
pub edit_event_id: Option<String>,
}
impl MizePart for MatrixPart {}
impl MizePartCreate for MatrixPart {}
impl MatrixPart {
/// Create a new MatrixPart
pub fn new(mize: Mize) -> Self {
Self {
mize,
client: None,
timeline: Vec::new(),
message_draft: String::new(),
edit_event_id: None,
}
}
/// Initialize the Matrix client from config
pub async fn init_client(&mut self) -> Result<(), String> {
let homeserver = self.mize
.get_config(&format!("matrix.accounts.{}.homeserver", ACCOUNT_NAME))
.map_err(|e| format!("Failed to get homeserver config: {:?}", e))?;
let access_token = self.mize
.get_config(&format!("matrix.accounts.{}.access_token", ACCOUNT_NAME))
.map_err(|e| format!("Failed to get access_token config: {:?}", e))?;
let user_id_str = self.mize
.get_config(&format!("matrix.accounts.{}.user_id", ACCOUNT_NAME))
.map_err(|e| format!("Failed to get user_id config: {:?}", e))?;
let device_id = self.mize
.get_config(&format!("matrix.accounts.{}.device_id", ACCOUNT_NAME))
.map_err(|e| format!("Failed to get device_id config: {:?}", e))?;
let user_id = UserId::parse(&user_id_str)
.map_err(|e| format!("Invalid user_id: {:?}", e))?;
let config = ClientConfig::new()
.homeserver_url(homeserver)
.device_id(device_id);
let client = Client::new_with_config(user_id, Some(access_token), config)
.await
.map_err(|e| format!("Failed to create client: {:?}", e))?;
self.client = Some(client.clone());
// Start sync in background
self.start_sync(client).await?;
Ok(())
}
/// Start sync and set up event listeners
async fn start_sync(&mut self, client: Client) -> Result<(), String> {
let room_id = ROOM_ID.to_string();
let mize = self.mize.clone();
// Start sync
let sync_token = client.sync_token().await;
let mut sync_state = SyncState::new(sync_token);
let client_clone = client.clone();
spawn_local(async move {
loop {
match client_clone.sync(&mut sync_state).await {
Ok(response) => {
// Process sync response - update MatrixPart via mize
if let Some(rooms) = response.rooms {
if let Some(join) = rooms.join {
if let Some(room_events) = join.get(&room_id) {
// Get MatrixPart and update timeline
if let Ok(mut matrix_part) = mize.get_part_native::<MatrixPart>("matrix") {
for event in &room_events.timeline.events {
if event.room_id() == room_id {
matrix_part.timeline.push(event.clone());
// Keep only last INITIAL_LOAD_LIMIT events
if matrix_part.timeline.len() > INITIAL_LOAD_LIMIT {
matrix_part.timeline.remove(0);
}
}
}
}
}
}
}
// Check if we should stop syncing
if sync_state.should_stop() {
break;
}
}
Err(e) => {
console_log!("Sync error: {:?}", e);
break;
}
}
}
});
Ok(())
}
/// Send a message to the room
pub async fn send_message(&mut self, room_id: &str, content: Option<>) -> MizeResult<()> {
let client = self.client.as_ref()
.ok_or_else(|| mize::error::Error::new("Client not initialized"))?;
if self.message_draft.is_empty() {
return Ok(());
}
let content = RoomMessageEventContent::text_plain(&self.message_draft);
if let Some(_event_id) = &self.edit_event_id {
// Editing existing message - placeholder for now
console_log!("Editing message (not yet implemented)");
}
// Send message
client.send_room_message(room_id, content).await?;
// Clear draft after sending
self.message_draft.clear();
self.edit_event_id = None;
Ok(())
}
}
/// Entry point for the Matrix part
pub fn part_matrix(mize: &mut Mize) -> MizeResult<()> {
#[cfg(feature = "target-obsidian")] {
use crate::ObsidianPart;
// Get the Obsidian part
let obsidian = mize.get_part_native::<ObsidianPart>("obsidian")?;
// Create and register MatrixPart
let matrix_part = MatrixPart::new(mize.clone());
mize.register_part(Box::new(matrix_part));
// Get the registered MatrixPart to initialize it
let mize_clone = mize.clone();
spawn_local(async move {
if let Ok(mut matrix_part) = mize_clone.get_part_native::<MatrixPart>("matrix") {
if let Err(e) = matrix_part.init_client().await {
console_log!("Failed to initialize Matrix client: {}", e);
}
}
});
// Create a JS function that will be called when the view is opened
let mize_for_view = mize.clone();
let view_creator = Closure::<dyn Fn(JsValue)>::wrap(Box::new(move |leaf: JsValue| {
if let Some(element) = extract_dom_element(leaf) {
// Get MatrixPart state for the UI
if let Ok(matrix_part) = mize_for_view.get_part_native::<MatrixPart>("matrix") {
let timeline = matrix_part.timeline.clone();
let message_draft = matrix_part.message_draft.clone();
let edit_event_id = matrix_part.edit_event_id.clone();
mount_chat_app(element, timeline, message_draft, edit_event_id, mize_for_view.clone());
}
} else {
console_log!("Failed to extract DOM element from leaf");
}
}));
// Convert closure to JS Function
let view_creator_fn: &Function = view_creator.as_ref().unchecked_ref();
// Register the view with Obsidian
ObsidianPart::register_view(&obsidian.plugin(), "matrix-chat-view", view_creator_fn)?;
// Prevent the closure from being dropped
view_creator.forget();
}
Ok(())
}
/// Extract DOM element from Obsidian leaf JSValue
fn extract_dom_element(leaf: JsValue) -> Option<HtmlElement> {
use wasm_bindgen::JsCast;
use js_sys::Reflect;
// Try to get the containerEl from the leaf
let container_value = Reflect::get(&leaf, &"containerEl".into()).ok()?;
let container = container_value.dyn_into::<HtmlElement>().ok()?;
// Create a div to mount into
let doc = web_sys::window()?.document()?;
let mount_point = doc.create_element("div").ok()?;
mount_point.set_attribute("style", "height: 100%; width: 100%;").ok()?;
container.append_child(&mount_point).ok()?;
mount_point.dyn_into::<HtmlElement>().ok()
}
// Console log macros
#[macro_export]
macro_rules! console_log {
($($t:tt)*) => {
web_sys::console::log_1(&format!($($t)*).into());
};
}
#[macro_export]
macro_rules! console_error {
($($t:tt)*) => {
web_sys::console::error_1(&format!($($t)*).into());
};
}

269
marts/src/matrix/ui.rs Normal file
View File

@@ -0,0 +1,269 @@
use dioxus::prelude::*;
use dioxus_web::events::FormEvent;
use mize::Mize;
use matrix_sdk::SyncRoomEvent;
const ROOM_ID: &str = "!XwqdXGPlmaLatdGUiN:ppc.social";
/// Props for the chat component
#[derive(Props, Clone)]
pub struct ChatProps {
pub mize: Mize,
pub initial_timeline: Vec<SyncRoomEvent>,
pub initial_draft: String,
pub initial_edit_id: Option<String>,
}
/// Message component - renders a single message
#[component]
pub fn Message(cx: Scope, event: SyncRoomEvent) -> Element {
let sender = event.sender().to_string();
let timestamp = event.origin_server_ts();
// Extract display name from MXID (basic version)
let display_name = if let Some(at_pos) = sender.find('@') {
if let Some(colon_pos) = sender.rfind(':') {
&sender[at_pos + 1..colon_pos]
} else {
&sender[at_pos + 1..]
}
} else {
&sender
};
// Format timestamp
let time_str = {
let dt = js_sys::Date::new(&(timestamp as f64));
dt.to_locale_string()
};
// Extract message content
let content = event
.as_original()
.and_then(|e| e.content().deserialize().ok())
.and_then(|c: matrix_sdk::ruma::events::room::message::RoomMessageEventContent| {
c.body().map(|s| s.to_string())
})
.unwrap_or_default();
// Check if this is an edit
let is_edit = event
.as_original()
.map(|e| {
e.content()
.get("m.relates_to")
.and_then(|v| v.as_object())
.and_then(|o| o.get("rel_type"))
.and_then(|v| v.as_str())
== Some("m.replace")
})
.unwrap_or(false);
cx.render(rsx! {
div {
class: "matrix-chat-event",
style: "margin: 14px 16px; padding-bottom: 10px; border-bottom: 1px solid var(--background-modifier-border);",
div {
class: "matrix-chat-msghead",
style: "font-size: 1em; font-weight: 500; color: var(--interactive-accent); display: flex; align-items: center; gap: 10px;",
span { display_name }
span {
style: "font-size: 0.85em; color: var(--text-muted); margin-left: auto;",
{time_str.clone()}
if is_edit { " · edited" }
}
}
div {
class: "matrix-chat-msgbody",
style: "margin: 6px 0 0 0; font-size: 1em;",
{content}
}
}
})
}
/// Editor component - message input area
#[component]
pub fn Editor(cx: Scope, mize: Mize, draft: UseSignal<String>, edit_id: UseSignal<Option<String>>) -> Element {
let on_input = move |evt: FormEvent| {
draft.set(evt.value.clone());
};
let on_send = move |_| {
let mize_clone = mize.clone();
let draft_val = draft.read().clone();
let edit_id_val = edit_id.read().clone();
cx.spawn(async move {
if let Ok(mut matrix_part) = mize_clone.get_part_native::<crate::matrix::MatrixPart>("matrix") {
matrix_part.message_draft = draft_val;
matrix_part.edit_event_id = edit_id_val;
if let Err(e) = matrix_part.send_message().await {
console_log!("Failed to send message: {:?}", e);
}
// Clear local draft
draft.set(String::new());
edit_id.set(None);
}
});
};
let on_keydown = move |evt: dioxus_web::events::KeyboardEvent| {
if (evt.ctrl_key() || evt.meta_key()) && evt.key() == "Enter" {
on_send(evt);
}
};
let on_cancel = move |_| {
draft.set(String::new());
edit_id.set(None);
};
cx.render(rsx! {
div {
class: "matrix-chat-editor",
style: "border-top: 1px solid var(--background-modifier-border); background: var(--background-primary); padding: 8px 12px;",
div {
class: "matrix-chat-editor-inner",
style: "border: 1px solid var(--background-modifier-border); border-radius: 6px; overflow: hidden; background: var(--background-secondary);",
textarea {
r#type: "text",
rows: "3",
style: "width: 99%; min-height: 40px; resize: vertical; background: transparent; border: none; outline: none; padding: 8px; font-family: inherit; font-size: inherit; color: var(--text-normal);",
value: "{draft}",
oninput: on_input,
onkeydown: on_keydown,
}
}
button {
r#type: "button",
style: "margin-top: 3px; float: right; padding: 6px 14px; font-size: 0.97em; border-radius: 5px; border: none; background: var(--interactive-accent); color: white; cursor: pointer;",
onclick: on_send,
if edit_id.read().is_some() { "Save" } else { "Send" }
}
if edit_id.read().is_some() {
button {
r#type: "button",
style: "margin-right: 12px; padding: 6px 12px; font-size: 0.93em; border-radius: 5px; border: none; color: var(--text-normal); background: var(--background-secondary); cursor: pointer;",
onclick: on_cancel,
"Cancel"
}
}
}
})
}
/// Timeline component - list of messages
#[component]
pub fn Timeline(cx: Scope, mize: Mize, timeline: UseSignal<Vec<SyncRoomEvent>>) -> Element {
// Periodically refresh timeline from MatrixPart
cx.use_effect((), move |_| {
let mize_clone = mize.clone();
let timeline_clone = timeline.clone();
// Set up interval to poll for updates
let interval = js_sys::set_interval(
&Closure::<dyn FnMut()>::wrap(Box::new(move || {
if let Ok(matrix_part) = mize_clone.get_part_native::<crate::matrix::MatrixPart>("matrix") {
*timeline_clone.write() = matrix_part.timeline.clone();
}
})),
1000, // 1 second
);
// Cleanup on unmount
move || {
js_sys::clear_interval(interval);
}
});
cx.render(rsx! {
div {
class: "matrix-chat-scroll",
style: "flex-grow: 1; overflow-y: auto; padding: 8px 0; background: var(--background-secondary);",
// Room name header
div {
class: "matrix-chat-roomname",
style: "font-weight: 600; text-align: center; font-size: 1.1em; margin: 2px 0 12px 0; border-bottom: 1px solid var(--background-modifier-border); color: var(--text-accent); background: var(--background-primary);",
"Matrix Chat - {ROOM_ID}"
}
// Messages
for event in timeline.read().iter() {
rsx! { Message { event: event.clone() } }
}
}
})
}
/// Root chat component
#[component]
pub fn Chat(cx: Scope, props: ChatProps) -> Element {
let timeline = use_signal(&cx, || props.initial_timeline);
let draft = use_signal(&cx, || props.initial_draft);
let edit_id = use_signal(&cx, || props.initial_edit_id);
cx.render(rsx! {
div {
class: "matrix-chat-root",
style: "height: 100%; display: flex; flex-direction: column; background: var(--background-primary); color: var(--text-normal);",
// Title
div {
class: "matrix-chat-title",
style: "padding: 12px; font-weight: bold; font-size: 1.2em; text-align: center; border-bottom: 1px solid var(--background-modifier-border);",
"Matrix Chat"
}
// Info
div {
class: "matrix-chat-info",
style: "font-size: 0.85em; color: var(--text-muted); text-align: center; margin-bottom: 5px;",
"Room: {ROOM_ID}"
}
// Timeline
Timeline { mize: props.mize.clone(), timeline }
// Editor
Editor { mize: props.mize.clone(), draft, edit_id }
}
})
}
/// Mount the Dioxus app into a DOM element
pub fn mount_chat_app(
element: web_sys::HtmlElement,
initial_timeline: Vec<SyncRoomEvent>,
initial_draft: String,
initial_edit_id: Option<String>,
mize: Mize,
) {
dioxus_web::launch(
dioxus::prelude::App::new(move || rsx! {
Chat {
mize: mize.clone(),
initial_timeline: initial_timeline.clone(),
initial_draft: initial_draft.clone(),
initial_edit_id: initial_edit_id.clone(),
}
}),
element,
);
}
// Console log macro
#[macro_export]
macro_rules! console_log {
($($t:tt)*) => {
web_sys::console::log_1(&format!($($t)*).into());
};
}

View File

@@ -65,7 +65,7 @@ impl ObsidianPart {
Reflect::set(&obsidian_cmd, &"id".into(), &"login".into()).mres()?; Reflect::set(&obsidian_cmd, &"id".into(), &"login".into()).mres()?;
Reflect::set(&obsidian_cmd, &"name".into(), &"Login".into()).mres()?; Reflect::set(&obsidian_cmd, &"name".into(), &"Login".into()).mres()?;
// callback: () => run_login() // callback
let mize_clone = self.mize.clone(); let mize_clone = self.mize.clone();
let cb = Closure::<dyn FnMut()>::wrap(Box::new(move || { let cb = Closure::<dyn FnMut()>::wrap(Box::new(move || {
if let Err(e) = (cmd.callback)(mize_clone.clone()) { if let Err(e) = (cmd.callback)(mize_clone.clone()) {
@@ -127,6 +127,7 @@ fn obsidian_mize_main(mize: &mut Mize, plugin: JsValue, app: JsValue) -> MizeRes
crate::part_obsidian(mize, plugin, app)?; crate::part_obsidian(mize, plugin, app)?;
crate::part_client_auth(mize)?; crate::part_client_auth(mize)?;
crate::spacetime(mize)?; crate::spacetime(mize)?;
crate::part_matrix(mize)?;
Ok(()) Ok(())
} }

7
marts/src/sc2/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
use mize::*;
pub fn part_sc2(mize: &mut Mize) -> MizeResult<()> {
Ok(())
}

View File

@@ -1,3 +1,4 @@
use crate::config::ConfigOpt;
use colored::Colorize; use colored::Colorize;
use core::fmt; use core::fmt;
use serde::Deserialize; use serde::Deserialize;
@@ -788,3 +789,30 @@ impl PartialEq for ItemData {
} }
} }
impl Eq for ItemData {} impl Eq for ItemData {}
pub fn set_config_from_data(mize: &mut Mize, mut data: ItemData) -> MizeResult<()> {
let mut config_opts = mize.config_opts.lock().unwrap();
for path in data.get_paths_recursive()? {
let mut conf_name = path.replace("/", ".");
if conf_name.starts_with(".") {
conf_name.remove(0);
}
let val = data.get_path(path.split("/").filter(|s| s != &"").collect::<Vec<&str>>())?;
match config_opts.get_mut(&conf_name) {
Some(opt) => {
opt.val = Some(val);
}
None => {
config_opts.insert(
conf_name.clone(),
ConfigOpt {
name: conf_name,
val: Some(val),
thunk: None,
},
);
}
};
}
Ok(())
}

View File

@@ -47,10 +47,64 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
pub fn wasm_instance_init(mize: &mut Mize) -> MizeResult<()> { pub fn wasm_instance_init(mize: &mut Mize) -> MizeResult<()> {
console_log!("Hello world from wasm_instance_init!!!!!!!!!!"); console_log!("Hello world from wasm_instance_init!!!!!!!!!!");
#[cfg(feature = "target-obsidian")] {
////// populate values from $MIZE_CONFIG_FILES
if let Ok(config_file_paths) = obsidian_get_env_var("MIZE_CONFIG_FILES") {
for config_file_path in config_file_paths.split(":") {
println!("reading config file: {config_file_path}");
let content = std::fs::read_to_string(config_file_path)?;
let mut data = ItemData::from_toml(content.as_str())?;
crate::core::item::set_config_from_data(mize, data)?;
}
}
////// load config from MIZE_CONFIG env var
match std::env::var("MIZE_CONFIG") {
Ok(config_string) => {
let data = data_from_string(config_string)?;
crate::core::item::set_config_from_data(mize, data)?;
}
Err(var_err) => match var_err {
VarError::NotPresent => {
debug!("env var MIZE_CONFIG NOT present")
}
VarError::NotUnicode(_) => {
warn!("env var MIZE_CONFIG is not Unicode, so not reading it")
}
},
};
}
Ok(()) Ok(())
} }
#[cfg(feature = "target-obsidian")]
pub fn obsidian_get_env_var(name: &str) -> Option<String> {
use js_sys::{Reflect, Object};
// 1. Get the global `process` object
let global = js_sys::global();
let process = Reflect::get(&global, &JsValue::from_str("process")).ok()?;
if process.is_undefined() {
return None;
}
// 2. Get the `env` object from `process`
let env = Reflect::get(&process, &JsValue::from_str("env")).ok()?;
if env.is_undefined() {
return None;
}
// 3. Get the `MIZE_STORE` value from `env`
let mize_store = Reflect::get(&env, &JsValue::from_str("MIZE_STORE")).ok()?;
// 4. Convert the JsValue to a Rust String if it exists
mize_store.as_string()
}
pub fn load_module(mize: &mut Mize, name: &str, path: Option<String>) -> MizeResult<()> { pub fn load_module(mize: &mut Mize, name: &str, path: Option<String>) -> MizeResult<()> {
console_log!("loading module NOT IMPLEMENTED: {}", name); console_log!("loading module NOT IMPLEMENTED: {}", name);

View File

@@ -68,7 +68,7 @@ pub fn os_instance_init(mize: &mut Mize) -> MizeResult<()> {
println!("reading config file: {config_file_path}"); println!("reading config file: {config_file_path}");
let content = std::fs::read_to_string(config_file_path)?; let content = std::fs::read_to_string(config_file_path)?;
let mut data = ItemData::from_toml(content.as_str())?; let mut data = ItemData::from_toml(content.as_str())?;
set_config_from_data(mize, data)?; crate::core::item::set_config_from_data(mize, data)?;
} }
} }
@@ -76,7 +76,7 @@ pub fn os_instance_init(mize: &mut Mize) -> MizeResult<()> {
match std::env::var("MIZE_CONFIG") { match std::env::var("MIZE_CONFIG") {
Ok(config_string) => { Ok(config_string) => {
let data = data_from_string(config_string)?; let data = data_from_string(config_string)?;
set_config_from_data(mize, data)?; crate::core::item::set_config_from_data(mize, data)?;
} }
Err(var_err) => match var_err { Err(var_err) => match var_err {
VarError::NotPresent => { VarError::NotPresent => {
@@ -195,32 +195,6 @@ pub fn os_instance_init(mize: &mut Mize) -> MizeResult<()> {
Ok(()) Ok(())
} }
fn set_config_from_data(mize: &mut Mize, mut data: ItemData) -> MizeResult<()> {
let mut config_opts = mize.config_opts.lock().unwrap();
for path in data.get_paths_recursive()? {
let mut conf_name = path.replace("/", ".");
if conf_name.starts_with(".") {
conf_name.remove(0);
}
let val = data.get_path(path.split("/").filter(|s| s != &"").collect::<Vec<&str>>())?;
match config_opts.get_mut(&conf_name) {
Some(opt) => {
opt.val = Some(val);
}
None => {
config_opts.insert(
conf_name.clone(),
ConfigOpt {
name: conf_name,
val: Some(val),
thunk: None,
},
);
}
};
}
Ok(())
}
pub fn seconds_since_modification(path: &Path) -> MizeResult<u64> { pub fn seconds_since_modification(path: &Path) -> MizeResult<u64> {
let metadata = fs::metadata(path)?; let metadata = fs::metadata(path)?;