recent mize changes
This commit is contained in:
10397
Cargo.lock
generated
Normal file
10397
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,27 +4,34 @@ version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0"
|
||||
description = 'A strongly typed "filesystem" for the age of connectivity.'
|
||||
repository = "https://github.com/c2vi/mize"
|
||||
|
||||
[package.metadata.wasm-pack.profile.dev]
|
||||
wasm-opt = false
|
||||
repository = "https://git.ppc.social/ppc/mize"
|
||||
|
||||
[lib]
|
||||
name = "mize"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "mize"
|
||||
path = "src/platform/os/main.rs"
|
||||
required-features = [ "target-os" ]
|
||||
|
||||
[features]
|
||||
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-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", ]
|
||||
|
||||
[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]
|
||||
# core dependencies
|
||||
@@ -39,7 +46,7 @@ toml = { version = "0.8.13", features = ["parse"], optional = false }
|
||||
dyn-clone = "1.0.17"
|
||||
sha2 = "0.10.8"
|
||||
flume = { version = "0.11.1", default-features = false, features = [ "async" ] }
|
||||
mize_macros = { path = "../macros" }
|
||||
mize_macros = { path = "./macros" }
|
||||
async-trait = "0.1.89"
|
||||
|
||||
# wasm dependencies
|
||||
8
apps/obsidian-plugin/Cargo.toml
Normal file
8
apps/obsidian-plugin/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "mize-obsidian-plugin"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
mize = { path = "../.." }
|
||||
marts = { path = "../../marts" }
|
||||
3
apps/obsidian-plugin/src/main.rs
Normal file
3
apps/obsidian-plugin/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
8
apps/os/Cargo.toml
Normal file
8
apps/os/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "mize-bin"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
mize = { path = "../../" }
|
||||
marts = { path = "../../marts" }
|
||||
@@ -17,7 +17,7 @@
|
||||
};
|
||||
|
||||
mize_modules = {
|
||||
url = "github:c2vi/mize-modules?submodules=1";
|
||||
url = "github:c2vi/mize-modules";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
@@ -258,6 +258,7 @@ in {
|
||||
pkgs.rsync
|
||||
pkgs.openssh
|
||||
pkgs.git
|
||||
pkgs.cmake
|
||||
|
||||
# to build ../grid
|
||||
pkgs.rustPlatform.bindgenHook
|
||||
@@ -300,6 +301,7 @@ in {
|
||||
wasm-bindgen-cli
|
||||
lld_20
|
||||
xdotool
|
||||
bzip2
|
||||
];
|
||||
|
||||
LD_LIBRARY_PATH = with pkgs; pkgs.lib.makeLibraryPath [
|
||||
@@ -324,6 +326,9 @@ in {
|
||||
libxcb
|
||||
pipewire
|
||||
wayland
|
||||
|
||||
# for rust-analyzer to work for zed
|
||||
bzip2
|
||||
];
|
||||
|
||||
LIBRARY_PATH = LD_LIBRARY_PATH;
|
||||
|
||||
@@ -11,7 +11,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
clap = { version = "4.5.59", optional = true, features = ["string"] }
|
||||
flume = "0.12.0"
|
||||
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"] }
|
||||
# https://github.com/rscarson/rustyscript/pull/415
|
||||
#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"] }
|
||||
serde = "1.0.228"
|
||||
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 }
|
||||
spacetimedb-sdk = { version = "2.6.0", default-features = false }
|
||||
generated = { path = "./generated" }
|
||||
@@ -39,6 +39,9 @@ log = "0.4"
|
||||
chrono = { version = "0.4.45", features = [], default-features = false }
|
||||
chrono-tz = { version = "0.10.4", default-features = false }
|
||||
url = "2.5.8"
|
||||
serenity = { version = "0.12.5", optional = true }
|
||||
matrix-sdk = "0.18.0"
|
||||
|
||||
|
||||
[features]
|
||||
default = ["target-os", "desktop", "server", "client"]
|
||||
@@ -51,3 +54,4 @@ desktop = ["dioxus/desktop"]
|
||||
client = []
|
||||
server = []
|
||||
clap = ["dep:clap"]
|
||||
serenity = ["dep:serenity"]
|
||||
|
||||
@@ -108,7 +108,7 @@ impl ClientAuth {
|
||||
|
||||
pub fn refresh(&mut self) -> MizeResult<()> {
|
||||
#[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")] {
|
||||
@@ -321,6 +321,7 @@ pub fn part_client_auth(mize: &mut Mize) -> MizeResult<()> {
|
||||
// register command for obsidian
|
||||
#[cfg(feature = "target-obsidian")] {
|
||||
use crate::MartsCommand;
|
||||
console_log!("register obsidian command...");
|
||||
|
||||
let obsidian = mize.get_part_native::<crate::ObsidianPart>("obsidian")?;
|
||||
obsidian.add_command(MartsCommand {
|
||||
|
||||
@@ -52,7 +52,7 @@ impl C2vi {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn c2vi(mize: &mut Mize) -> MizeResult<()> {
|
||||
pub fn part_c2vi(mize: &mut Mize) -> MizeResult<()> {
|
||||
#[cfg(feature = "desktop")]
|
||||
if mize
|
||||
.get_config("c2vi.dash.enable")
|
||||
@@ -63,6 +63,22 @@ pub fn c2vi(mize: &mut Mize) -> MizeResult<()> {
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
167
marts/src/discord/mod.rs
Normal file
167
marts/src/discord/mod.rs
Normal 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,
|
||||
}))
|
||||
}
|
||||
@@ -15,7 +15,6 @@ pub mod c2vi {
|
||||
pub mod part;
|
||||
pub mod dashboard;
|
||||
}
|
||||
#[cfg(feature = "target-os")]
|
||||
pub use c2vi::part::*;
|
||||
|
||||
#[cfg(feature = "target-obsidian")]
|
||||
@@ -36,6 +35,13 @@ pub mod ppc;
|
||||
|
||||
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")]
|
||||
//pub mod js;
|
||||
//#[cfg(feature = "target-os")]
|
||||
|
||||
247
marts/src/matrix/mod.rs
Normal file
247
marts/src/matrix/mod.rs
Normal 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
269
marts/src/matrix/ui.rs
Normal 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());
|
||||
};
|
||||
}
|
||||
@@ -65,7 +65,7 @@ impl ObsidianPart {
|
||||
Reflect::set(&obsidian_cmd, &"id".into(), &"login".into()).mres()?;
|
||||
Reflect::set(&obsidian_cmd, &"name".into(), &"Login".into()).mres()?;
|
||||
|
||||
// callback: () => run_login()
|
||||
// callback
|
||||
let mize_clone = self.mize.clone();
|
||||
let cb = Closure::<dyn FnMut()>::wrap(Box::new(move || {
|
||||
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_client_auth(mize)?;
|
||||
crate::spacetime(mize)?;
|
||||
crate::part_matrix(mize)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
7
marts/src/sc2/mod.rs
Normal file
7
marts/src/sc2/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
use mize::*;
|
||||
|
||||
pub fn part_sc2(mize: &mut Mize) -> MizeResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::config::ConfigOpt;
|
||||
use colored::Colorize;
|
||||
use core::fmt;
|
||||
use serde::Deserialize;
|
||||
@@ -788,3 +789,30 @@ impl PartialEq 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(())
|
||||
}
|
||||
@@ -48,9 +48,63 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
pub fn wasm_instance_init(mize: &mut Mize) -> MizeResult<()> {
|
||||
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(())
|
||||
}
|
||||
|
||||
#[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<()> {
|
||||
console_log!("loading module NOT IMPLEMENTED: {}", name);
|
||||
|
||||
@@ -68,7 +68,7 @@ pub fn os_instance_init(mize: &mut Mize) -> MizeResult<()> {
|
||||
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())?;
|
||||
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") {
|
||||
Ok(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 {
|
||||
VarError::NotPresent => {
|
||||
@@ -195,32 +195,6 @@ pub fn os_instance_init(mize: &mut Mize) -> MizeResult<()> {
|
||||
|
||||
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> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
Reference in New Issue
Block a user