omega restructuring + a bunch of changes i have made over the last weeks
This commit is contained in:
671
marts/src/auth/client.rs
Normal file
671
marts/src/auth/client.rs
Normal file
@@ -0,0 +1,671 @@
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
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 std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::{path::PathBuf, thread};
|
||||
use tracing::info;
|
||||
|
||||
// console_log macro
|
||||
// that can be copied into other files for debugging purposes
|
||||
#[cfg(feature = "target-obsidian")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(feature = "target-obsidian")]
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
fn log(s: &str);
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-obsidian")]
|
||||
macro_rules! console_log {
|
||||
// Note that this is using the `log` function imported above during
|
||||
// `bare_bones`
|
||||
($($t:tt)*) => (unsafe { log(&format_args!($($t)*).to_string())})
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
use 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,
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
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;
|
||||
|
||||
// Serialize struct to JSON string
|
||||
let serialized = serde_json::to_string(&secrets)?;
|
||||
|
||||
// Get browser window object
|
||||
let window = window().ok_or_else(|| mize_err!("No global window found"))?;
|
||||
|
||||
// Access local storage
|
||||
let storage = window
|
||||
.local_storage().map_err(|_| mize_err!("couldn't get local_storage"))?
|
||||
.ok_or_else(|| mize_err!("LocalStorage is not available"))?;
|
||||
|
||||
// Store the JSON string
|
||||
storage.set_item("mize_client_secrets", &serialized).map_err(|_| mize_err!("couldn't set item in local_storage"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
impl ClientAuth {
|
||||
pub fn secrets(&self) -> Option<&ClientSecrets> {
|
||||
self.secrets.as_ref()
|
||||
}
|
||||
|
||||
pub fn id_token(&self) -> MizeResult<&str> {
|
||||
if self.secrets.is_none() {
|
||||
return Err(mize_err!("Client not logged in"));
|
||||
}
|
||||
Ok(&self.secrets.as_ref().unwrap().id_token)
|
||||
}
|
||||
|
||||
pub fn access_token(&self) -> MizeResult<&str> {
|
||||
if self.secrets.is_none() {
|
||||
return Err(mize_err!("Client not logged in"));
|
||||
}
|
||||
Ok(&self.secrets.as_ref().unwrap().access_token)
|
||||
}
|
||||
|
||||
pub fn refresh_token(&self) -> MizeResult<&str> {
|
||||
if self.secrets.is_none() {
|
||||
return Err(mize_err!("Client not logged in"));
|
||||
}
|
||||
Ok(&self.secrets.as_ref().unwrap().refresh_token)
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self) -> MizeResult<()> {
|
||||
#[cfg(feature = "target-js")] {
|
||||
return Err(mize_err!("refresh is not supported on wasm"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-os")] {
|
||||
let secrets = self.secrets.clone().ok_or(mize_err!(
|
||||
"no client secrets, meaning client_auth.refresh() thread can't use the refresh token"
|
||||
))?;
|
||||
let new_secrets = self.mize.spawn_async_blocking(
|
||||
"auth.refresh_flow",
|
||||
refresh_flow(self.mize.clone(), secrets),
|
||||
)?;
|
||||
store_secrets(&mut self.mize, &new_secrets)?;
|
||||
self.secrets = Some(new_secrets);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn login(&mut self) -> MizeResult<()> {
|
||||
#[cfg(feature = "target-os")] {
|
||||
if self.secrets().is_none() {
|
||||
let secrets = self
|
||||
.mize
|
||||
.spawn_async_blocking("auth.login_flow", auth_flow(self.mize.clone()))?;
|
||||
store_secrets(&mut self.mize, &secrets)?;
|
||||
self.secrets = Some(secrets);
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "target-js")] {
|
||||
if self.secrets().is_none() {
|
||||
let mut mize_clone = self.mize.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
console_log!("before auth_flow");
|
||||
let secrets = match auth_flow(mize_clone.clone()).await {
|
||||
Ok(secrets) => secrets,
|
||||
Err(e) => {
|
||||
mize_clone.report_err(e);
|
||||
return ();
|
||||
}
|
||||
};
|
||||
console_log!("after auth_flow");
|
||||
if let Err(e) = store_secrets_js(&mut mize_clone, &secrets) {
|
||||
mize_clone.report_err(e);
|
||||
return ();
|
||||
}
|
||||
let mut auth = match mize_clone.get_part_native::<ClientAuth>("client_auth") {
|
||||
Ok(auth) => auth,
|
||||
Err(e) => {
|
||||
mize_clone.report_err(e);
|
||||
return ();
|
||||
}
|
||||
};
|
||||
auth.secrets = Some(secrets);
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start_refresh_thread(&mut self) -> MizeResult<()> {
|
||||
|
||||
// background thread for os targets
|
||||
#[cfg(not(feature = "target-js"))]
|
||||
{
|
||||
let mut mize_clone = self.mize.clone();
|
||||
self.mize.spawn_background("auth.refresh", move || loop {
|
||||
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();
|
||||
info!("auth.refresh thread: now: {}", Utc::now());
|
||||
info!("auth.refresh thread: expiry: {}", expiry);
|
||||
let to_sleep = expiry - Utc::now() - TimeDelta::minutes(20);
|
||||
/*
|
||||
if to_sleep < TimeDelta::from_std(Duration::from_secs(0))? {
|
||||
if let Err(e) = auth.refresh() {
|
||||
e.log();
|
||||
}
|
||||
}
|
||||
*/
|
||||
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);
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// spawn an async background task doing the refresh
|
||||
#[cfg(feature = "target-js")] {
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
// Get the window object
|
||||
let window = web_sys::window().expect("no global `window` exists");
|
||||
|
||||
// Clone mize for the closure
|
||||
let mize_clone = self.mize.clone();
|
||||
|
||||
// Create a closure that will be called periodically
|
||||
let refresh_closure = Closure::wrap(Box::new(move || {
|
||||
let mut mize_clone = mize_clone.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let mut auth = mize_clone.get_part_native::<ClientAuth>("client_auth");
|
||||
match auth {
|
||||
Ok(mut auth_part) => {
|
||||
if let Some(secrets) = auth_part.secrets() {
|
||||
let now = Utc::now();
|
||||
let expiry = secrets.expiry;
|
||||
let time_until_expiry = expiry - now;
|
||||
|
||||
// Refresh if token will expire in less than 20 minutes
|
||||
if time_until_expiry < TimeDelta::minutes(20) {
|
||||
if let Err(e) = auth_part.refresh() {
|
||||
console_log!("Error refreshing token: {:?}", e);
|
||||
} else {
|
||||
console_log!("Token refreshed successfully");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
console_log!("Error getting auth part: {:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}) as Box<dyn FnMut()>);
|
||||
|
||||
// Set up interval to run every 50 minutes (300000 ms)
|
||||
let interval_ms = 3000000;
|
||||
let _ = window.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||
refresh_closure.as_ref().unchecked_ref(),
|
||||
interval_ms,
|
||||
);
|
||||
|
||||
// Leak the closure to prevent it from being dropped
|
||||
refresh_closure.forget();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub fn part_client_auth(mize: &mut Mize) -> MizeResult<()> {
|
||||
let mize_clone = mize.clone();
|
||||
|
||||
let mut auth_part = ClientAuth {
|
||||
secrets: None,
|
||||
mize: mize.clone(),
|
||||
};
|
||||
|
||||
let mut secrets = match load_secrets(mize) {
|
||||
Ok(secrets) => Some(secrets),
|
||||
Err(err) => {
|
||||
tracing::warn!("err reading secrets: {:?}", err);
|
||||
None
|
||||
}
|
||||
};
|
||||
auth_part.secrets = secrets.clone();
|
||||
|
||||
// do a refresh if the tokens have expired
|
||||
if secrets.is_some() && secrets.clone().unwrap().expiry < Utc::now() {
|
||||
auth_part.refresh()?;
|
||||
}
|
||||
|
||||
|
||||
// start refresh tread if configured
|
||||
if mize
|
||||
.get_config("auth.client.refresh_thread")
|
||||
.unwrap_or(ItemData::new())
|
||||
.value_bool()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
auth_part.start_refresh_thread()?;
|
||||
}
|
||||
|
||||
|
||||
// cli command for os
|
||||
#[cfg(feature = "target-os")] {
|
||||
let mut cli = mize.get_part_native::<crate::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(())
|
||||
})?;
|
||||
}
|
||||
|
||||
|
||||
// register command for obsidian
|
||||
#[cfg(feature = "target-obsidian")] {
|
||||
use crate::MartsCommand;
|
||||
|
||||
let obsidian = mize.get_part_native::<crate::ObsidianPart>("obsidian")?;
|
||||
obsidian.add_command(MartsCommand {
|
||||
id: "mize-login".to_string(),
|
||||
name: "Mize Login".to_string(),
|
||||
callback: Box::new(|mut mize| {
|
||||
let mut auth = mize.get_part_native::<crate::ClientAuth>("client_auth")?;
|
||||
let res = auth.login();
|
||||
console_log!("auth.login done: {:?}", res);
|
||||
res
|
||||
}),
|
||||
})?;
|
||||
}
|
||||
|
||||
|
||||
auth_part.secrets = secrets;
|
||||
|
||||
mize.register_part(Box::new(auth_part))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_secrets(mize: &mut Mize) -> MizeResult<ClientSecrets> {
|
||||
#[cfg(feature = "target-os")] {
|
||||
let mize_dir = mize.get_config("data_dir")?.value_string()?;
|
||||
let secrets_file_path = PathBuf::from(mize_dir).join("client_secrets.json");
|
||||
|
||||
let secrets_string = std::fs::read_to_string(secrets_file_path)?;
|
||||
let secrets: ClientSecrets = serde_json::from_str(&secrets_string)?;
|
||||
return Ok(secrets);
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-js")] {
|
||||
use web_sys::window;
|
||||
|
||||
// Get browser window object
|
||||
let window = window().ok_or_else(|| mize_err!("No global window found"))?;
|
||||
|
||||
// Access local storage
|
||||
let storage = window
|
||||
.local_storage().map_err(|_| mize_err!("couldn't get local_storage"))?
|
||||
.ok_or_else(|| mize_err!("LocalStorage is not available"))?;
|
||||
|
||||
// Retrieve the string item
|
||||
return match storage.get_item("mize_client_secrets").map_err(|_| mize_err!("couldn't get item from local_storage"))? {
|
||||
Some(json_str) => {
|
||||
// Deserialize JSON string back into Client struct
|
||||
let secrets: ClientSecrets = serde_json::from_str(json_str.as_str())?;
|
||||
Ok(secrets)
|
||||
}
|
||||
None => Err(mize_err!("no secrets")), // Key does not exist
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn store_secrets(mize: &mut Mize, secrets: &ClientSecrets) -> MizeResult<()> {
|
||||
|
||||
#[cfg(feature = "target-os")] {
|
||||
let mize_dir = mize.get_config("store")?.value_string()?;
|
||||
let secrets_file_path = PathBuf::from(mize_dir.clone())
|
||||
.join("misc")
|
||||
.join("client_secrets.json");
|
||||
std::fs::create_dir_all(PathBuf::from(mize_dir).join("misc"))?;
|
||||
|
||||
let secrets_string = serde_json::to_string(secrets)?;
|
||||
log::info!("secrets_file_path: {}", secrets_file_path.display());
|
||||
//std::fs::write(secrets_file_path, secrets_string)?;
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(secrets_file_path)?
|
||||
.write_all(&secrets_string.as_bytes())?;
|
||||
|
||||
// update the spacetimedb_token in XDG_CONFIG_HOME/spacetime/cli.toml config file if configured to do so
|
||||
if mize
|
||||
.get_config("auth.client.update_spacetime_token")
|
||||
.unwrap_or(ItemData::empty())
|
||||
.as_bool()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
update_spacetimedb_token(&secrets.id_token.as_str())?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
use openidconnect::reqwest::Client as HttpClient;
|
||||
use openidconnect::DeviceAuthorizationUrl;
|
||||
use std::time::Duration;
|
||||
//use wasm_bindgen::JsCast;
|
||||
|
||||
|
||||
async fn refresh_flow(mut mize: Mize, old_secrets: ClientSecrets) -> MizeResult<ClientSecrets> {
|
||||
out!("Refresh flow...");
|
||||
let issuer = mize.get_config("auth.client.issuer")?.value_string()?;
|
||||
let client_id = mize.get_config("auth.client.client_id")?.value_string()?;
|
||||
|
||||
let issuer = IssuerUrl::new(issuer)?;
|
||||
let client_id = ClientId::new(client_id);
|
||||
|
||||
let http_client = HttpClient::new();
|
||||
|
||||
// Teach openidconnect-rs about the device authorization url.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
struct MyExtraProviderMetadata {
|
||||
device_authorization_endpoint: String,
|
||||
}
|
||||
use openidconnect::core::{
|
||||
CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClient, CoreClientAuthMethod,
|
||||
CoreGrantType, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm,
|
||||
CoreJweKeyManagementAlgorithm, CoreResponseMode, CoreResponseType,
|
||||
CoreSubjectIdentifierType,
|
||||
};
|
||||
use openidconnect::AdditionalProviderMetadata;
|
||||
use openidconnect::ProviderMetadata;
|
||||
impl AdditionalProviderMetadata for MyExtraProviderMetadata {}
|
||||
type MyProviderMetadata = ProviderMetadata<
|
||||
MyExtraProviderMetadata,
|
||||
CoreAuthDisplay,
|
||||
CoreClientAuthMethod,
|
||||
CoreClaimName,
|
||||
CoreClaimType,
|
||||
CoreGrantType,
|
||||
CoreJweContentEncryptionAlgorithm,
|
||||
CoreJweKeyManagementAlgorithm,
|
||||
CoreJsonWebKey,
|
||||
CoreResponseMode,
|
||||
CoreResponseType,
|
||||
CoreSubjectIdentifierType,
|
||||
>;
|
||||
|
||||
// Discover provider metadata
|
||||
let provider_metadata: MyProviderMetadata =
|
||||
MyProviderMetadata::discover_async(issuer, &http_client).await?;
|
||||
|
||||
let device_auth_url = provider_metadata
|
||||
.additional_metadata()
|
||||
.device_authorization_endpoint
|
||||
.clone();
|
||||
|
||||
let client = CoreClient::from_provider_metadata(
|
||||
provider_metadata,
|
||||
client_id,
|
||||
None, // public client
|
||||
)
|
||||
.set_device_authorization_url(DeviceAuthorizationUrl::new(device_auth_url)?);
|
||||
|
||||
let refresh_token = RefreshToken::new(old_secrets.refresh_token);
|
||||
let res = client
|
||||
.exchange_refresh_token(&refresh_token)?
|
||||
.request_async(&http_client)
|
||||
.await?;
|
||||
let expires_in = res.expires_in().ok_or(mize_err!(
|
||||
"no expiry information was in token response of the refresh flow"
|
||||
))?;
|
||||
|
||||
return Ok(ClientSecrets {
|
||||
id_token: res.id_token().unwrap().to_string(),
|
||||
access_token: res.access_token().secret().to_owned(),
|
||||
refresh_token: res.refresh_token().unwrap().secret().to_owned(),
|
||||
expiry: Utc::now() + expires_in,
|
||||
});
|
||||
}
|
||||
|
||||
async fn auth_flow(mut mize: Mize) -> MizeResult<ClientSecrets> {
|
||||
let issuer = mize.get_config("auth.client.issuer")?.value_string()?;
|
||||
let client_id = mize.get_config("auth.client.client_id")?.value_string()?;
|
||||
|
||||
let issuer = IssuerUrl::new(issuer)?;
|
||||
let client_id = ClientId::new(client_id);
|
||||
|
||||
let http_client = HttpClient::new();
|
||||
|
||||
// Teach openidconnect-rs about the device authorization url.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
struct MyExtraProviderMetadata {
|
||||
device_authorization_endpoint: String,
|
||||
}
|
||||
use openidconnect::core::{
|
||||
CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClient, CoreClientAuthMethod,
|
||||
CoreGrantType, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm,
|
||||
CoreJweKeyManagementAlgorithm, CoreResponseMode, CoreResponseType,
|
||||
CoreSubjectIdentifierType,
|
||||
};
|
||||
use openidconnect::AdditionalProviderMetadata;
|
||||
use openidconnect::ProviderMetadata;
|
||||
impl AdditionalProviderMetadata for MyExtraProviderMetadata {}
|
||||
type MyProviderMetadata = ProviderMetadata<
|
||||
MyExtraProviderMetadata,
|
||||
CoreAuthDisplay,
|
||||
CoreClientAuthMethod,
|
||||
CoreClaimName,
|
||||
CoreClaimType,
|
||||
CoreGrantType,
|
||||
CoreJweContentEncryptionAlgorithm,
|
||||
CoreJweKeyManagementAlgorithm,
|
||||
CoreJsonWebKey,
|
||||
CoreResponseMode,
|
||||
CoreResponseType,
|
||||
CoreSubjectIdentifierType,
|
||||
>;
|
||||
|
||||
// Discover provider metadata
|
||||
let provider_metadata: MyProviderMetadata =
|
||||
MyProviderMetadata::discover_async(issuer, &http_client).await?;
|
||||
|
||||
let device_auth_url = provider_metadata
|
||||
.additional_metadata()
|
||||
.device_authorization_endpoint
|
||||
.clone();
|
||||
|
||||
let client = CoreClient::from_provider_metadata(
|
||||
provider_metadata,
|
||||
client_id,
|
||||
None, // public client
|
||||
)
|
||||
.set_device_authorization_url(DeviceAuthorizationUrl::new(device_auth_url)?);
|
||||
|
||||
// ===== DEVICE AUTH REQUEST =====
|
||||
let details: Result<DeviceAuthorizationResponse<EmptyExtraDeviceAuthorizationFields>, _> =
|
||||
client
|
||||
.exchange_device_code()
|
||||
.add_scope(Scope::new("openid".into()))
|
||||
.add_scope(Scope::new("profile".into()))
|
||||
.add_scope(Scope::new("offline_access".into()))
|
||||
.request_async(&http_client)
|
||||
.await;
|
||||
|
||||
let details = match details {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
out!("err: {:?}", err);
|
||||
out!("err: {}", err);
|
||||
panic!();
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(feature = "target-obsidian")] {
|
||||
console_log!("details: {:?}", details);
|
||||
use std::str::FromStr;
|
||||
|
||||
use js_sys::{Array, Function, JsString, Reflect};
|
||||
use mize::error::IntoMizeResultJsValue;
|
||||
use wasm_bindgen::convert::TryFromJsValue;
|
||||
|
||||
let obsidian = mize.get_part_native::<crate::ObsidianPart>("obsidian")?;
|
||||
let show_device_code = Reflect::get(&obsidian.plugin(), &JsValue::from_str("showDeviceCode")).mres()?;
|
||||
let args = Array::new();
|
||||
args.push(&JsValue::from_str(details.user_code().secret().as_str()));
|
||||
args.push(&JsValue::from_str(details.verification_uri_complete().unwrap().secret().as_str()));
|
||||
|
||||
Reflect::apply(&show_device_code.dyn_into::<Function>().mres()?, obsidian.plugin(), &args).mres()?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-os")] {
|
||||
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()
|
||||
);
|
||||
|
||||
if let Some(_) = details.verification_uri_complete() {
|
||||
let _ = webbrowser::open(details.verification_uri_complete().unwrap().secret());
|
||||
}
|
||||
}
|
||||
|
||||
let token_response = client
|
||||
.exchange_device_access_token(&details)?
|
||||
.request_async(
|
||||
&http_client,
|
||||
async |dur: Duration| {
|
||||
#[cfg(feature = "target-os")] {
|
||||
tokio::time::sleep(dur).await;
|
||||
}
|
||||
#[cfg(feature = "target-js")] {
|
||||
console_log!("sleeping");
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, dur.as_millis() as i32)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Await the JS Promise like a Rust Future
|
||||
let _ = JsFuture::from(promise).await;
|
||||
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
// ===== TOKENS =====
|
||||
let id_token = token_response
|
||||
.extra_fields()
|
||||
.id_token()
|
||||
.ok_or(mize_err!("no id_token"))?;
|
||||
|
||||
let refresh = token_response
|
||||
.refresh_token()
|
||||
.ok_or(mize_err!("no refresh token present"))?;
|
||||
|
||||
let expires_in = token_response.expires_in().ok_or(mize_err!(
|
||||
"no expiry information was in token response of the refresh flow"
|
||||
))?;
|
||||
|
||||
Ok(ClientSecrets {
|
||||
id_token: id_token.to_string(),
|
||||
access_token: token_response.access_token().secret().to_string(),
|
||||
refresh_token: refresh.secret().to_owned(),
|
||||
expiry: Utc::now() + expires_in,
|
||||
})
|
||||
}
|
||||
|
||||
fn update_spacetimedb_token(new_token: &str) -> MizeResult<()> {
|
||||
// Resolve config directory
|
||||
let config_dir = match std::env::var("XDG_CONFIG_HOME") {
|
||||
Ok(val) => PathBuf::from(val),
|
||||
Err(_) => {
|
||||
let home = std::env::var("HOME")?;
|
||||
std::path::PathBuf::from(home).join(".config")
|
||||
}
|
||||
};
|
||||
|
||||
let file_path = config_dir.join("spacetime").join("cli.toml");
|
||||
|
||||
// Read existing file (or create empty document if it doesn't exist)
|
||||
let content = std::fs::read_to_string(&file_path).unwrap_or_default();
|
||||
let mut data = ItemData::from_toml(content.as_str())?;
|
||||
|
||||
// Update the field
|
||||
data.set_path("spacetimedb_token", new_token)?;
|
||||
|
||||
// Write back
|
||||
std::fs::write(&file_path, data.to_toml()?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
9
marts/src/c2vi/dashboard.rs
Normal file
9
marts/src/c2vi/dashboard.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
pub fn main() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
h1 { "my dashbaord" }
|
||||
}
|
||||
}
|
||||
}
|
||||
431
marts/src/c2vi/deno.ts
Normal file
431
marts/src/c2vi/deno.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
import { createClient } from "@libsql/client";
|
||||
import { drizzle } from "drizzle-orm/libsql";
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
||||
import { eq } from "drizzle-orm";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
export async function init(ppc: PPC) {
|
||||
ppc.c2vi = new C2vi(ppc);
|
||||
|
||||
ppc.cli
|
||||
.command("bed", "command to run when c2vi goes to bed")
|
||||
.action(async () => {
|
||||
await save_habitica_dailies(ppc);
|
||||
await save_habitica_logs(ppc);
|
||||
});
|
||||
|
||||
ppc.cli
|
||||
.command("clearTodos", "command to run when c2vi goes to bed")
|
||||
.alias("clt")
|
||||
.action(async () => {
|
||||
const todos = await ppc.habitica.get_tasks();
|
||||
for (const todo of todos) {
|
||||
await ppc.habitica.delete_task(todo.id);
|
||||
}
|
||||
});
|
||||
|
||||
ppc.cli
|
||||
.command("listTodos", "command to run when c2vi goes to bed")
|
||||
.alias("dut")
|
||||
.action(async () => {
|
||||
const todos = await ppc.habitica.get_tasks();
|
||||
for (const todo of todos) {
|
||||
console.log(todo.text);
|
||||
}
|
||||
});
|
||||
|
||||
ppc.cli
|
||||
.command("buyHealthPotion", "command to run when c2vi goes to bed")
|
||||
.alias("buyh")
|
||||
.action(async () => {
|
||||
await ppc.habitica.api_request("POST", "/user/buy-health-potion");
|
||||
});
|
||||
|
||||
ppc.cli
|
||||
.command("addActionItems", "command to run when c2vi goes to bed")
|
||||
.alias("aal")
|
||||
.action(async () => {
|
||||
await add_action_items(ppc);
|
||||
});
|
||||
|
||||
ppc.cli
|
||||
.command("test", "a command to run some test code")
|
||||
.alias("t")
|
||||
.action(async () => {});
|
||||
|
||||
ppc.cli
|
||||
.command("skip [num]", "command to skip a habitica todo")
|
||||
.alias("sk")
|
||||
.action((num = 1) => {
|
||||
skip_habitica_todo(ppc, parseInt(num));
|
||||
});
|
||||
|
||||
ppc.cli.command("dumpLog", "dump the complete habitica log").action(() => {
|
||||
dump_habitica_logs(ppc);
|
||||
});
|
||||
|
||||
ppc.cli
|
||||
.command("listDailies", "list all dailies")
|
||||
.action(async (folder, options) => {
|
||||
const allDailies = await ppc.c2vi.db.select().from(dailiesTable).all();
|
||||
for (const task of allDailies) {
|
||||
console.log(task.id + ": " + task.text);
|
||||
}
|
||||
});
|
||||
|
||||
ppc.cli
|
||||
.command("printTask <id>", "print a specific todo")
|
||||
.action(async (id) => {
|
||||
const data = await ppc.habitica.api_request("GET", `/tasks/${id}`, {});
|
||||
console.log(data.history);
|
||||
});
|
||||
}
|
||||
|
||||
class C2vi {
|
||||
[key: string]: any;
|
||||
|
||||
constructor(ppc: PPC) {
|
||||
this.ppc = ppc;
|
||||
this.db = drizzle(
|
||||
createClient({
|
||||
url: "file://" + ppc.config.local_storage_path + "/data.db",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(d) {
|
||||
const pad = (n) => n.toString().padStart(2, "0");
|
||||
|
||||
// Custom YYYY-MM-DD format
|
||||
const formatted = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||
return formatted;
|
||||
}
|
||||
|
||||
// ============= SCHEMA DEFINITION =============
|
||||
const dailiesTable = sqliteTable("habitica_dailies", {
|
||||
id: text("id").primaryKey(),
|
||||
text: text("text").notNull(),
|
||||
notes: text("notes"),
|
||||
priority: text("priority"), // 0.1, 1, 1.5, 2
|
||||
frequency: text("frequency"), // 'daily', 'weekly', etc.
|
||||
});
|
||||
const habiticaLogTable = sqliteTable("habitica_log", {
|
||||
date: text("date").primaryKey(),
|
||||
data: text("data").notNull(),
|
||||
});
|
||||
|
||||
async function save_habitica_dailies(ppc) {
|
||||
console.log("######### cli bed cmd #########");
|
||||
await ppc.c2vi.db.run(
|
||||
"CREATE TABLE IF NOT EXISTS habitica_dailies (id TEXT PRIMARY KEY, text TEXT NOT NULL, notes TEXT, priority TEXT, frequency TEXT)",
|
||||
);
|
||||
await ppc.c2vi.db.run(
|
||||
"CREATE TABLE IF NOT EXISTS habitica_log (date TEXT PRIMARY KEY, data TEXT NOT NULL)",
|
||||
);
|
||||
|
||||
// ============= DATABASE INITIALIZATION =============
|
||||
if (!ppc.config.habitica.user_id || !ppc.config.habitica.api_token) {
|
||||
console.error(
|
||||
"Error: HABITICA_USER_ID and HABITICA_API_TOKEN env vars required",
|
||||
);
|
||||
console.error(
|
||||
"Get these from Habitica Settings > API and configure them in the ppc.config",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dailies = await ppc.habitica.get_tasks("dailys");
|
||||
|
||||
await writeDailiesToDatabase(dailies, ppc);
|
||||
}
|
||||
|
||||
async function dump_habitica_logs(ppc) {
|
||||
const entries = await ppc.c2vi.db.select().from(habiticaLogTable).all();
|
||||
for (const entry of entries) {
|
||||
console.log(entry.date + ": ", JSON.parse(entry.data));
|
||||
}
|
||||
}
|
||||
|
||||
async function skip_habitica_todo(ppc, num) {
|
||||
await ppc.c2vi.db.run(
|
||||
"CREATE TABLE IF NOT EXISTS habitica_dailies (id TEXT PRIMARY KEY, text TEXT NOT NULL, notes TEXT, priority TEXT, frequency TEXT)",
|
||||
);
|
||||
await ppc.c2vi.db.run(
|
||||
"CREATE TABLE IF NOT EXISTS habitica_log (date TEXT PRIMARY KEY, data TEXT NOT NULL)",
|
||||
);
|
||||
|
||||
const todos = await ppc.habitica.get_tasks("todos");
|
||||
|
||||
const date = new Date();
|
||||
const save_for_yesterday = date.getHours() < 12;
|
||||
if (save_for_yesterday) {
|
||||
console.log(
|
||||
"Saving logs for yesterday!!!! because it is " +
|
||||
date.getHours() +
|
||||
"hours of the day",
|
||||
);
|
||||
date.setDate(date.getDate() - 1);
|
||||
}
|
||||
|
||||
const result = await ppc.c2vi.db
|
||||
.select()
|
||||
.from(habiticaLogTable)
|
||||
.where(eq(habiticaLogTable.date, formatDate(date)));
|
||||
|
||||
const data = result[0]
|
||||
? JSON.parse(result[0].data)
|
||||
: {
|
||||
dailies_done: [],
|
||||
dailies_skipped: [],
|
||||
todos_done: [],
|
||||
todos_skipped: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i < num; i++) {
|
||||
const todo = todos[i];
|
||||
if (!todo) continue;
|
||||
await ppc.habitica.delete_task(todo.id);
|
||||
await incrementHabit(ppc);
|
||||
|
||||
data.todos_skipped.push({
|
||||
id: todo.id,
|
||||
text: todo.text,
|
||||
notes: todo.notes || "",
|
||||
checklist: todo.checklist || [],
|
||||
tags: todo.tags || [],
|
||||
});
|
||||
}
|
||||
|
||||
// saving to db
|
||||
await ppc.c2vi.db
|
||||
.insert(habiticaLogTable)
|
||||
.values({ date: formatDate(date), data: JSON.stringify(data) })
|
||||
.onConflictDoUpdate({
|
||||
target: habiticaLogTable.date,
|
||||
set: {
|
||||
data: JSON.stringify(data),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function save_habitica_logs(ppc) {
|
||||
const date = new Date();
|
||||
const save_for_yesterday = date.getHours() < 12;
|
||||
if (save_for_yesterday) {
|
||||
console.log(
|
||||
"Saving logs for yesterday!!!! because it is " +
|
||||
date.getHours() +
|
||||
"hours of the day",
|
||||
);
|
||||
date.setDate(date.getDate() - 1);
|
||||
}
|
||||
|
||||
const result = await ppc.c2vi.db
|
||||
.select()
|
||||
.from(habiticaLogTable)
|
||||
.where(eq(habiticaLogTable.date, formatDate(date)));
|
||||
|
||||
const data = result[0]
|
||||
? JSON.parse(result[0].data)
|
||||
: {
|
||||
dailies_done: [],
|
||||
dailies_skipped: [],
|
||||
todos_done: [],
|
||||
todos_skipped: [],
|
||||
};
|
||||
|
||||
// dailies
|
||||
const dailies = await ppc.habitica.get_tasks("dailys");
|
||||
for (const daily of dailies) {
|
||||
let completed = daily.completed;
|
||||
let isDue = daily.isDue;
|
||||
if (save_for_yesterday) {
|
||||
const yesterday = daily.history.reduce((prev, current) => {
|
||||
return prev.date > current.date ? prev : current;
|
||||
});
|
||||
completed = yesterday.completed;
|
||||
isDue = yesterday.isDue;
|
||||
}
|
||||
if (!isDue) {
|
||||
continue;
|
||||
}
|
||||
if (completed) {
|
||||
data.dailies_done.push(daily.id);
|
||||
console.log("daily-done: " + daily.text);
|
||||
} else {
|
||||
data.dailies_skipped.push(daily.id);
|
||||
console.log("daily-skipped: " + daily.text);
|
||||
}
|
||||
}
|
||||
|
||||
// todos done
|
||||
const todos = await ppc.habitica.get_tasks("completedTodos");
|
||||
for (const todo of todos) {
|
||||
const dateCompleted = new Date(todo.dateCompleted);
|
||||
|
||||
// check if this todo is already saved in yesterday's log entry
|
||||
if (save_for_yesterday) {
|
||||
// get yesterday log entry
|
||||
const result = await ppc.c2vi.db
|
||||
.select()
|
||||
.from(habiticaLogTable)
|
||||
.where(eq(habiticaLogTable.date, formatDate(date)));
|
||||
|
||||
const data = result[0]
|
||||
? JSON.parse(result[0].data)
|
||||
: {
|
||||
dailies_done: [],
|
||||
dailies_skipped: [],
|
||||
todos_done: [],
|
||||
todos_skipped: [],
|
||||
};
|
||||
|
||||
// check if this todo is already saved in yesterday's log entry
|
||||
const todoIndex = data.todos_done.findIndex((t) => t.id === todo.id);
|
||||
if (todoIndex !== -1) {
|
||||
continue; // skipp saving this todo
|
||||
}
|
||||
}
|
||||
|
||||
if (dateCompleted.toDateString() == date.toDateString()) {
|
||||
console.log("todo-done:", todo.text);
|
||||
data.todos_done.push({
|
||||
id: todo.id,
|
||||
text: todo.text,
|
||||
notes: todo.notes || "",
|
||||
checklist: todo.checklist || [],
|
||||
tags: todo.tags || [],
|
||||
});
|
||||
}
|
||||
|
||||
// we save_for_yesterday but the todo was completed today... also save that for this day
|
||||
if (
|
||||
save_for_yesterday &&
|
||||
dateCompleted.toDateString() == new Date().toDateString()
|
||||
) {
|
||||
console.log("todo-done:", todo.text);
|
||||
data.todos_done.push({
|
||||
id: todo.id,
|
||||
text: todo.text,
|
||||
notes: todo.notes || "",
|
||||
checklist: todo.checklist || [],
|
||||
tags: todo.tags || [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// saving to db
|
||||
await ppc.c2vi.db
|
||||
.insert(habiticaLogTable)
|
||||
.values({ date: formatDate(date), data: JSON.stringify(data) })
|
||||
.onConflictDoUpdate({
|
||||
target: habiticaLogTable.date,
|
||||
set: {
|
||||
data: JSON.stringify(data),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function writeDailiesToDatabase(dailies, ppc) {
|
||||
console.log(`Syncing ${dailies.length} dailies to database...`);
|
||||
|
||||
const records = dailies.map((daily) => ({
|
||||
id: daily.id,
|
||||
text: daily.text,
|
||||
notes: daily.notes || "",
|
||||
priority: daily.priority?.toString() || null,
|
||||
frequency: daily.frequency || "daily",
|
||||
}));
|
||||
|
||||
async function upsertMany(records) {
|
||||
for (const record of records) {
|
||||
await ppc.c2vi.db
|
||||
.insert(dailiesTable)
|
||||
.values(record)
|
||||
.onConflictDoUpdate({
|
||||
target: dailiesTable.id,
|
||||
set: {
|
||||
text: record.text,
|
||||
notes: record.notes,
|
||||
priority: record.priority,
|
||||
frequency: record.frequency,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await upsertMany(records);
|
||||
}
|
||||
|
||||
async function incrementHabit(ppc) {
|
||||
const habits = await ppc.habitica.get_tasks("habits");
|
||||
let id = "";
|
||||
for (const habit of habits) {
|
||||
if ((habit.text = "skipped a planned task")) {
|
||||
id = habit.id;
|
||||
}
|
||||
}
|
||||
await ppc.habitica.api_request("POST", `tasks/${id}/score/down`);
|
||||
}
|
||||
|
||||
async function add_action_items(ppc: PPC) {
|
||||
let task_ids = [];
|
||||
|
||||
const tmpFile = path.join(os.tmpdir(), `habitica-tasks-${Date.now()}.md`);
|
||||
const initialContent = `
|
||||
gu 10min TIMER
|
||||
uw 15min FROG
|
||||
b coffee/tee/kakau
|
||||
uw FROG
|
||||
b breakfast, brush teeth, lüften
|
||||
uw FROG
|
||||
|
||||
lunch
|
||||
|
||||
uw 15min TIMER of next days FROG
|
||||
fw
|
||||
dt 30min TIMER
|
||||
|
||||
fun 20:00 (smth that can be stopped easily)
|
||||
se 21:40
|
||||
read
|
||||
bed 22:10
|
||||
`;
|
||||
|
||||
fs.writeFileSync(tmpFile, initialContent.trim());
|
||||
|
||||
spawnSync("nvim", [tmpFile], { stdio: "inherit" });
|
||||
|
||||
// read file
|
||||
const listStr = fs.readFileSync(tmpFile, "utf8");
|
||||
|
||||
// delete file
|
||||
fs.unlinkSync(tmpFile);
|
||||
|
||||
// Simple parser: filter out empty lines
|
||||
const tasks = listStr
|
||||
.split("\n")
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t.length > 0);
|
||||
|
||||
// 5. Add tasks to Habitica
|
||||
for (const todoText of tasks) {
|
||||
const data = await ppc.habitica.api_request(
|
||||
"POST",
|
||||
"tasks/user",
|
||||
{},
|
||||
{ text: todoText, type: "todo" },
|
||||
);
|
||||
task_ids.push(data.id);
|
||||
console.log(`Added to-do: '${todoText}' with id '${data.id}'`);
|
||||
}
|
||||
|
||||
// 6. Move tasks to bottom
|
||||
for (const taskId of task_ids) {
|
||||
await ppc.habitica.api_request("POST", `tasks/${taskId}/move/to/-1`);
|
||||
}
|
||||
}
|
||||
170
marts/src/c2vi/obsidian-canvas-patch.ts
Normal file
170
marts/src/c2vi/obsidian-canvas-patch.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
export function c2vi_obsidian_canvas_patch(mize: any) {
|
||||
const plugin = mize.get_part_native("obsidian");
|
||||
|
||||
window.pc = patchCanvas;
|
||||
window.upc = () => {
|
||||
unpatchCanvas(plugin, plugin.app.workspace.activeLeaf);
|
||||
};
|
||||
|
||||
console.log("hereeeeeeeeeeeeeeeee");
|
||||
// canvas patching
|
||||
plugin.registerEvent(
|
||||
plugin.app.workspace.on("file-open", (file: any) => {
|
||||
if (file?.extension === "canvas") {
|
||||
maybePathCanvas(plugin, plugin.app.workspace.activeLeaf);
|
||||
}
|
||||
}),
|
||||
);
|
||||
plugin.registerEvent(
|
||||
plugin.app.workspace.on("active-leaf-change", (file: any) => {
|
||||
if (file?.extension === "canvas") {
|
||||
maybePathCanvas(plugin, plugin.app.workspace.activeLeaf);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const maybePathCanvas = (plugin: PPCObsidianPlugin, leaf: any | null) => {
|
||||
console.log("maybePathCanvas called");
|
||||
const file = plugin.app.workspace.getActiveFile();
|
||||
|
||||
if (file?.extension !== "canvas") return;
|
||||
console.log("patching canvas.....");
|
||||
|
||||
const canvas = plugin.app.workspace.activeLeaf.view.canvas;
|
||||
if ("isPatched" in canvas) return;
|
||||
|
||||
patchCanvas(plugin, leaf, canvas);
|
||||
};
|
||||
|
||||
function patchCanvas(plugin: PPCObsidianPlugin, leaf: any, canvas: any) {
|
||||
canvas.isPatched = true;
|
||||
const canvasEl = canvas.canvasEl;
|
||||
const canvasWrapperEl = canvasEl.parentElement;
|
||||
const overlay = createDiv();
|
||||
overlay.setAttr("class", "c2vi-canvas-overlay");
|
||||
const canvas_card_menue = canvasWrapperEl.childNodes[1];
|
||||
canvasWrapperEl.insertBefore(overlay, canvas_card_menue);
|
||||
Object.assign(overlay.style, {
|
||||
position: "absolute",
|
||||
inset: "0",
|
||||
//zIndex: "1337",
|
||||
});
|
||||
overlay.addEventListener("contextmenu", (e) => {
|
||||
if (e.button === 2) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
});
|
||||
overlay.addEventListener("mouseout", (e) => {
|
||||
const menu = document.getElementsByClassName("menu")[0];
|
||||
if (!menu) return;
|
||||
menu.addEventListener("pointerup", (e) => {
|
||||
console.log("pointerup on menu", e);
|
||||
e.target.click();
|
||||
/*
|
||||
menu.dispatchEvent(
|
||||
new MouseEvent("mousedown", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
button: e.button,
|
||||
buttons: e.buttons,
|
||||
view: window,
|
||||
}),
|
||||
);
|
||||
*/
|
||||
});
|
||||
});
|
||||
|
||||
setupEventPropagation("pointerdown", PointerEvent, overlay, canvasWrapperEl);
|
||||
setupEventPropagation("pointerup", PointerEvent, overlay, canvasWrapperEl);
|
||||
setupEventPropagation("mousedown", MouseEvent, overlay, canvasWrapperEl);
|
||||
setupEventPropagation("mouseup", MouseEvent, overlay, canvasWrapperEl);
|
||||
setupEventPropagation("click", MouseEvent, overlay, canvasWrapperEl);
|
||||
setupEventPropagation("dragstart", DragEvent, overlay, canvasWrapperEl);
|
||||
setupEventPropagation("dragstop", DragEvent, overlay, canvasWrapperEl);
|
||||
|
||||
window.a = canvasWrapperEl;
|
||||
window.b = overlay;
|
||||
|
||||
const mouseEvents = [
|
||||
"mousedown",
|
||||
"mouseup",
|
||||
"click",
|
||||
"dblclick",
|
||||
//"mousemove",
|
||||
"mouseover",
|
||||
"mouseout",
|
||||
"wheel",
|
||||
];
|
||||
|
||||
mouseEvents.forEach((eventType) => {
|
||||
canvasWrapperEl.addEventListener(
|
||||
eventType,
|
||||
(event) => {
|
||||
console.log(`Mouse Event Triggered: ${event.type}`, event);
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function unpatchCanvas(plugin: PPCObsidianPlugin, leaf: any) {
|
||||
const canvas = plugin.app.workspace.activeLeaf.view.canvas;
|
||||
if (!canvas.isPatched) return;
|
||||
canvas.isPatched = false;
|
||||
const canvasEl = canvas.canvasEl;
|
||||
const canvasWrapperEl = canvasEl.parentElement;
|
||||
|
||||
const targets = canvasEl.querySelectorAll(":scope > .c2vi-canvas-overlay");
|
||||
|
||||
targets.forEach((child) => child.remove());
|
||||
}
|
||||
|
||||
function setupEventPropagation(type: string, evClass, overlay: any, target) {
|
||||
overlay.addEventListener(type, (originalEvent) => {
|
||||
originalEvent.stopPropagation();
|
||||
originalEvent.stopImmediatePropagation();
|
||||
originalEvent.preventDefault();
|
||||
const newEvent = new evClass(type, originalEvent);
|
||||
if (type == "pointerdown" && originalEvent.ctrlKey == true) {
|
||||
dispatchContextMenue(
|
||||
target,
|
||||
originalEvent.clientX,
|
||||
originalEvent.clientY,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (type === "pointerdown") {
|
||||
window.downEv = newEvent;
|
||||
}
|
||||
if (type === "pointerup") {
|
||||
window.upEv = newEvent;
|
||||
}
|
||||
newEvent.isTrusted = true;
|
||||
console.log("original Event of", type, originalEvent);
|
||||
console.log("new Event of", type, newEvent);
|
||||
target.dispatchEvent(newEvent);
|
||||
});
|
||||
}
|
||||
|
||||
function dispatchContextMenue(target, x, y) {
|
||||
const newEvent = new PointerEvent("contextmenu", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
view: window,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
pointerType: "mouse",
|
||||
button: 2,
|
||||
buttons: 2,
|
||||
pointerId: 1,
|
||||
isPrimary: true,
|
||||
});
|
||||
target.dispatchEvent(newEvent);
|
||||
}
|
||||
693
marts/src/c2vi/part.rs
Normal file
693
marts/src/c2vi/part.rs
Normal file
@@ -0,0 +1,693 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use chrono::{Timelike, Utc};
|
||||
use clap::{Arg, Command};
|
||||
#[cfg(feature = "desktop")]
|
||||
use mize::item::ItemData;
|
||||
use rusqlite::{params, Connection};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use mize::{mize_err, mize_part, Mize, MizeError, MizePart, MizePartCreate, MizeResult};
|
||||
use tracing::instrument::WithSubscriber;
|
||||
|
||||
use crate::cli::CliPart;
|
||||
use crate::habitica::Habitica;
|
||||
use crate::SpacetimePart;
|
||||
use generated::spacetime::{
|
||||
Action, ActivitiesTableAccess, Activity, DbConnection, LogEventsTableAccess, TaskPart, TaskPartTableAccess, activitiesQueryTableAccess, next_action, remove_activity, task_partQueryTableAccess, update_activity, update_upcoming
|
||||
};
|
||||
use spacetimedb_sdk::{
|
||||
credentials, DbContext, Error, Event, Identity, Status, SubscriptionHandle, Table,
|
||||
TableWithPrimaryKey,
|
||||
};
|
||||
|
||||
#[mize_part]
|
||||
#[derive(Default)]
|
||||
pub struct C2vi {
|
||||
mize: Mize,
|
||||
db: Mutex<Option<Connection>>,
|
||||
}
|
||||
|
||||
impl MizePart for C2vi {
|
||||
fn opts(&self, mize: &mut Mize) {
|
||||
mize.new_opt("c2vi.local_storage_path");
|
||||
}
|
||||
}
|
||||
impl MizePartCreate for C2vi {}
|
||||
|
||||
impl C2vi {
|
||||
fn with_db<F, R>(&self, f: F) -> MizeResult<R>
|
||||
where
|
||||
F: FnOnce(&Connection) -> MizeResult<R>,
|
||||
{
|
||||
let guard = self
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| mize_err!("DB lock poisoned: {}", e))?;
|
||||
let conn = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| mize_err!("DB not initialized"))?;
|
||||
f(conn)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn c2vi(mize: &mut Mize) -> MizeResult<()> {
|
||||
#[cfg(feature = "desktop")]
|
||||
if mize
|
||||
.get_config("c2vi.dash.enable")
|
||||
.unwrap_or(ItemData::empty())
|
||||
.as_bool()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
launch_c2vi_dashboard(mize);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
pub fn launch_c2vi_dashboard(mize: &mut Mize) {
|
||||
use dioxus::desktop::{muda::Menu, Config, WindowBuilder};
|
||||
let mize_clone = mize.clone();
|
||||
LaunchBuilder::new()
|
||||
.with_context_provider(move || Box::new(mize_clone.clone()))
|
||||
.with_cfg(desktop! {
|
||||
Config::new().with_window(WindowBuilder::new().with_decorations(false).with_title("PPC"))
|
||||
})
|
||||
.launch(c2vi_dash_main);
|
||||
}
|
||||
|
||||
//use spacetimedb_sdk::spacetime_module::QueryBuilder;
|
||||
|
||||
/*
|
||||
pub fn use_spacetime<T: Fn(QueryBuilder)>(query: T) {
|
||||
let mut mize = use_context::<Mize>();
|
||||
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime").unwrap();
|
||||
let con = spacetime.con();
|
||||
con.subscription_builder()
|
||||
.on_applied(move |ctx| {
|
||||
let data = ctx.db.task_part().identity().find(&ctx.identity()).unwrap();
|
||||
current.set(data.current.name.clone());
|
||||
})
|
||||
.add_query(query)
|
||||
.subscribe();
|
||||
con.db
|
||||
.task_part()
|
||||
.on_update(move |_, old: &TaskPart, new: &TaskPart| {
|
||||
current.set(new.current.name.clone());
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
//pub fn use_by_pk(identity: Identity)
|
||||
//pub fn use_one<T>
|
||||
|
||||
pub fn c2vi_dash_main() -> Element {
|
||||
let mut current = use_signal_sync(|| "".to_string());
|
||||
//TaskPart::
|
||||
//use_one()
|
||||
use_effect(move || {
|
||||
let mut mize = use_context::<Mize>();
|
||||
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime").unwrap();
|
||||
let con = spacetime.con();
|
||||
con.subscription_builder()
|
||||
.on_applied(move |ctx| {
|
||||
let data = ctx.db.task_part().identity().find(&ctx.identity()).unwrap();
|
||||
current.set(data.current.name.clone());
|
||||
})
|
||||
.add_query(|q| q.from.task_part().r#where(|t| t.identity.eq(con.identity())))
|
||||
.subscribe();
|
||||
con.db
|
||||
.task_part()
|
||||
.on_update(move |_, old: &TaskPart, new: &TaskPart| {
|
||||
current.set(new.current.name.clone());
|
||||
});
|
||||
});
|
||||
rsx! {
|
||||
div {
|
||||
style: "text-align: center; padding: 2rem; font-size: 50px;",
|
||||
p {
|
||||
"{current}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn c2vi_client(mize: &mut Mize) -> MizeResult<()> {
|
||||
//let storage_path = mize.get_config("c2vi.local_storage_path")?.as_
|
||||
let storage_path = "/home/me/work/app-data/ppc";
|
||||
//println!("storage_path: {}", storage_path);
|
||||
let db_path = std::path::PathBuf::from(&storage_path).join("data.db");
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| mize_err!("Failed to open C2vi database at {:?}: {}", db_path, e))?;
|
||||
|
||||
setup_tables(&conn)?;
|
||||
|
||||
let c2vi_part = C2vi {
|
||||
mize: mize.clone(),
|
||||
db: Mutex::new(Some(conn)),
|
||||
};
|
||||
mize.register_part(Box::new(c2vi_part))?;
|
||||
|
||||
// Register CLI subcommands
|
||||
let mut cli = mize.get_part_native::<CliPart>("cli")?;
|
||||
register_subcommands(&mut cli);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_tables(conn: &Connection) -> MizeResult<()> {
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS habitica_dailies (
|
||||
id TEXT PRIMARY KEY,
|
||||
text TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
priority TEXT,
|
||||
frequency TEXT
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| mize_err!("DB error creating habitica_dailies: {}", e))?;
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS habitica_log (
|
||||
date TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| mize_err!("DB error creating habitica_log: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_subcommands(cli: &mut CliPart) {
|
||||
cli.subcommand(
|
||||
Command::new("c2vi-dash").about("Run the c2vi dashboard gui"),
|
||||
|_matches, mut _mize| Ok(dioxus::launch(super::dashboard::main)),
|
||||
);
|
||||
cli.subcommand(
|
||||
Command::new("bed").about("Command to run when c2vi goes to bed"),
|
||||
|_matches, mut mize| {
|
||||
save_habitica_dailies(&mut mize)?;
|
||||
save_habitica_logs(&mut mize)?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
|
||||
cli.subcommand(
|
||||
Command::new("clearTodos")
|
||||
.about("Clear all Habitica todos")
|
||||
.alias("clt"),
|
||||
|_matches, mut mize| {
|
||||
let todos = {
|
||||
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||
hab.get_tasks("todos")?
|
||||
};
|
||||
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||
if let Some(arr) = todos.as_array() {
|
||||
for todo in arr {
|
||||
if let Some(id) = todo["id"].as_str() {
|
||||
hab.delete_task(id)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
|
||||
cli.subcommand(
|
||||
Command::new("listTodos")
|
||||
.about("List all Habitica todos")
|
||||
.alias("dut"),
|
||||
|_matches, mut mize| {
|
||||
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||
let todos = hab.get_tasks("todos")?;
|
||||
if let Some(arr) = todos.as_array() {
|
||||
for todo in arr {
|
||||
if let Some(text) = todo["text"].as_str() {
|
||||
println!("{}", text);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
|
||||
cli.subcommand(
|
||||
Command::new("buyHealthPotion")
|
||||
.about("Buy a health potion")
|
||||
.alias("buyh"),
|
||||
|_matches, mut mize| {
|
||||
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||
hab.api_request(
|
||||
reqwest::Method::POST,
|
||||
"user/buy-health-potion".to_string(),
|
||||
json!({}),
|
||||
)?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
|
||||
cli.subcommand(
|
||||
Command::new("addActionItems")
|
||||
.about("Add action items from temp file")
|
||||
.alias("aal"),
|
||||
|_matches, mut mize| add_action_items(&mut mize),
|
||||
);
|
||||
|
||||
cli.subcommand(
|
||||
Command::new("skip")
|
||||
.about("Skip a Habitica todo")
|
||||
.alias("sk")
|
||||
.arg(Arg::new("num").default_value("1")),
|
||||
|matches, mut mize| {
|
||||
let num: usize = matches
|
||||
.get_one::<String>("num")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap_or(1);
|
||||
skip_habitica_todo(&mut mize, num)
|
||||
},
|
||||
);
|
||||
|
||||
cli.subcommand(
|
||||
Command::new("dumpLog").about("Dump the complete Habitica log"),
|
||||
|_matches, mut mize| dump_habitica_logs(&mut mize),
|
||||
);
|
||||
|
||||
cli.subcommand(
|
||||
Command::new("listDailies").about("List all dailies from local DB"),
|
||||
|_matches, mut mize| {
|
||||
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
|
||||
c2vi.with_db(|conn| {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, text FROM habitica_dailies")
|
||||
.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
})
|
||||
.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||
for row in rows {
|
||||
let (id, text) = row.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||
println!("{}: {}", id, text);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
cli.subcommand(
|
||||
Command::new("printTask")
|
||||
.about("Print a specific task from Habitica")
|
||||
.arg(Arg::new("id").required(true)),
|
||||
|matches, mut mize| {
|
||||
let id = matches.get_one::<String>("id").unwrap();
|
||||
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||
let data = hab.api_request(reqwest::Method::GET, format!("tasks/{}", id), json!({}))?;
|
||||
if let Some(history) = data.get("history") {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(history).unwrap_or_default()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&data).unwrap_or_default()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ============= Business Logic =============
|
||||
|
||||
fn format_date(dt: chrono::DateTime<Utc>) -> String {
|
||||
dt.format("%Y-%m-%d").to_string()
|
||||
}
|
||||
|
||||
/// Get the target date, adjusted for "save for yesterday" logic (before noon = yesterday)
|
||||
fn get_target_date() -> (String, bool) {
|
||||
let now = Utc::now();
|
||||
let save_for_yesterday = now.hour() < 12;
|
||||
let date = if save_for_yesterday {
|
||||
now - chrono::Duration::days(1)
|
||||
} else {
|
||||
now
|
||||
};
|
||||
if save_for_yesterday {
|
||||
println!(
|
||||
"Saving logs for yesterday because it is {} hours of the day",
|
||||
now.hour()
|
||||
);
|
||||
}
|
||||
(format_date(date), save_for_yesterday)
|
||||
}
|
||||
|
||||
/// Load existing log entry from DB, or return a default empty one
|
||||
fn get_or_create_log(conn: &Connection, date: &str) -> MizeResult<Value> {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT data FROM habitica_log WHERE date = ?1")
|
||||
.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||
let result: Option<String> = stmt.query_row(params![date], |row| row.get(0)).ok();
|
||||
match result {
|
||||
Some(data_str) => {
|
||||
serde_json::from_str(&data_str).map_err(|e| mize_err!("JSON parse error: {}", e))
|
||||
}
|
||||
None => Ok(json!({
|
||||
"dailies_done": [],
|
||||
"dailies_skipped": [],
|
||||
"todos_done": [],
|
||||
"todos_skipped": []
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Upsert a log entry into the database
|
||||
fn upsert_log(conn: &Connection, date: &str, data: &Value) -> MizeResult<()> {
|
||||
let data_str =
|
||||
serde_json::to_string(data).map_err(|e| mize_err!("JSON serialize error: {}", e))?;
|
||||
conn.execute(
|
||||
"INSERT INTO habitica_log (date, data) VALUES (?1, ?2)
|
||||
ON CONFLICT(date) DO UPDATE SET data=excluded.data",
|
||||
params![date, data_str],
|
||||
)
|
||||
.map_err(|e| mize_err!("DB error upserting log: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_habitica_dailies(mize: &mut Mize) -> MizeResult<()> {
|
||||
println!("######### cli bed cmd #########");
|
||||
|
||||
// Fetch dailies from Habitica
|
||||
let dailies = {
|
||||
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||
hab.get_tasks("dailys")?
|
||||
};
|
||||
|
||||
// Write to database
|
||||
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
|
||||
c2vi.with_db(|conn| {
|
||||
if let Some(arr) = dailies.as_array() {
|
||||
println!("Syncing {} dailies to database...", arr.len());
|
||||
for daily in arr {
|
||||
let id = daily["id"].as_str().unwrap_or("");
|
||||
let text = daily["text"].as_str().unwrap_or("");
|
||||
let notes = daily["notes"].as_str().unwrap_or("");
|
||||
let priority = daily["priority"].to_string();
|
||||
let frequency = daily["frequency"].as_str().unwrap_or("daily");
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO habitica_dailies (id, text, notes, priority, frequency)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
text=excluded.text,
|
||||
notes=excluded.notes,
|
||||
priority=excluded.priority,
|
||||
frequency=excluded.frequency",
|
||||
params![id, text, notes, priority, frequency],
|
||||
)
|
||||
.map_err(|e| mize_err!("DB error writing daily: {}", e))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn save_habitica_logs(mize: &mut Mize) -> MizeResult<()> {
|
||||
let (date_str, save_for_yesterday) = get_target_date();
|
||||
|
||||
// Fetch dailies and completed todos from Habitica
|
||||
let (dailies, completed_todos) = {
|
||||
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||
let dailies = hab.get_tasks("dailys")?;
|
||||
let completed_todos = hab.get_tasks("completedTodos")?;
|
||||
(dailies, completed_todos)
|
||||
};
|
||||
|
||||
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
|
||||
c2vi.with_db(|conn| {
|
||||
let mut data = get_or_create_log(conn, &date_str)?;
|
||||
|
||||
// Process dailies
|
||||
if let Some(arr) = dailies.as_array() {
|
||||
for daily in arr {
|
||||
let is_due = if save_for_yesterday {
|
||||
// Look at history for yesterday's status
|
||||
daily["history"]
|
||||
.as_array()
|
||||
.and_then(|h| h.iter().max_by_key(|e| e["date"].as_i64().unwrap_or(0)))
|
||||
.map(|e| e["isDue"].as_bool().unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
daily["isDue"].as_bool().unwrap_or(false)
|
||||
};
|
||||
|
||||
if !is_due {
|
||||
continue;
|
||||
}
|
||||
|
||||
let completed = if save_for_yesterday {
|
||||
daily["history"]
|
||||
.as_array()
|
||||
.and_then(|h| h.iter().max_by_key(|e| e["date"].as_i64().unwrap_or(0)))
|
||||
.map(|e| e["completed"].as_bool().unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
daily["completed"].as_bool().unwrap_or(false)
|
||||
};
|
||||
|
||||
let daily_id = daily["id"].as_str().unwrap_or("");
|
||||
let daily_text = daily["text"].as_str().unwrap_or("");
|
||||
|
||||
if completed {
|
||||
data["dailies_done"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(json!(daily_id));
|
||||
println!("daily-done: {}", daily_text);
|
||||
} else {
|
||||
data["dailies_skipped"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(json!(daily_id));
|
||||
println!("daily-skipped: {}", daily_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process completed todos
|
||||
if let Some(arr) = completed_todos.as_array() {
|
||||
let today_str = format_date(Utc::now());
|
||||
for todo in arr {
|
||||
let date_completed_str = todo["dateCompleted"].as_str().unwrap_or("");
|
||||
|
||||
// Parse the completion date (ISO 8601 format from Habitica)
|
||||
let date_completed = chrono::DateTime::parse_from_rfc3339(date_completed_str)
|
||||
.map(|d| format_date(d.with_timezone(&Utc)))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Check if already in the log for this date
|
||||
if save_for_yesterday {
|
||||
let existing_todos = data["todos_done"].as_array().unwrap();
|
||||
let todo_id = todo["id"].as_str().unwrap_or("");
|
||||
if existing_todos
|
||||
.iter()
|
||||
.any(|t| t["id"].as_str() == Some(todo_id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let todo_entry = json!({
|
||||
"id": todo["id"],
|
||||
"text": todo["text"],
|
||||
"notes": todo.get("notes").unwrap_or(&json!("")),
|
||||
"checklist": todo.get("checklist").unwrap_or(&json!([])),
|
||||
"tags": todo.get("tags").unwrap_or(&json!([])),
|
||||
});
|
||||
|
||||
// Save if completed on the target date
|
||||
if date_completed == date_str {
|
||||
println!("todo-done: {}", todo["text"].as_str().unwrap_or(""));
|
||||
data["todos_done"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(todo_entry.clone());
|
||||
}
|
||||
|
||||
// If saving for yesterday but todo was completed today, also record it
|
||||
if save_for_yesterday && date_completed == today_str {
|
||||
println!("todo-done: {}", todo["text"].as_str().unwrap_or(""));
|
||||
data["todos_done"].as_array_mut().unwrap().push(todo_entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
upsert_log(conn, &date_str, &data)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn skip_habitica_todo(mize: &mut Mize, num: usize) -> MizeResult<()> {
|
||||
let (date_str, _) = get_target_date();
|
||||
|
||||
// Fetch todos from Habitica
|
||||
let todos = {
|
||||
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||
hab.get_tasks("todos")?
|
||||
};
|
||||
|
||||
let arr = todos
|
||||
.as_array()
|
||||
.ok_or_else(|| mize_err!("Expected array of todos"))?;
|
||||
|
||||
// Delete and log each skipped todo
|
||||
for i in 0..num {
|
||||
let todo = match arr.get(i) {
|
||||
Some(t) => t,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let todo_id = todo["id"].as_str().unwrap_or("");
|
||||
|
||||
// Delete the todo and score the skip habit
|
||||
{
|
||||
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||
hab.delete_task(todo_id)?;
|
||||
}
|
||||
increment_habit(mize)?;
|
||||
|
||||
// Record in log
|
||||
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
|
||||
c2vi.with_db(|conn| {
|
||||
let mut data = get_or_create_log(conn, &date_str)?;
|
||||
data["todos_skipped"].as_array_mut().unwrap().push(json!({
|
||||
"id": todo["id"],
|
||||
"text": todo["text"],
|
||||
"notes": todo.get("notes").unwrap_or(&json!("")),
|
||||
"checklist": todo.get("checklist").unwrap_or(&json!([])),
|
||||
"tags": todo.get("tags").unwrap_or(&json!([])),
|
||||
}));
|
||||
upsert_log(conn, &date_str, &data)?;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn increment_habit(mize: &mut Mize) -> MizeResult<()> {
|
||||
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||
let habits = hab.get_tasks("habits")?;
|
||||
if let Some(arr) = habits.as_array() {
|
||||
for habit in arr {
|
||||
if habit["text"].as_str() == Some("skipped a planned task") {
|
||||
let id = habit["id"].as_str().unwrap_or("");
|
||||
hab.api_request(
|
||||
reqwest::Method::POST,
|
||||
format!("tasks/{}/score/down", id),
|
||||
json!({}),
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_action_items(mize: &mut Mize) -> MizeResult<()> {
|
||||
let initial_content = "\
|
||||
gu 10min TIMER
|
||||
uw 15min FROG
|
||||
b coffee/tee/kakau
|
||||
uw FROG
|
||||
b breakfast, brush teeth, lüften
|
||||
uw FROG
|
||||
|
||||
lunch
|
||||
|
||||
uw 15min TIMER of next days FROG
|
||||
fw
|
||||
dt 30min TIMER
|
||||
|
||||
fun 20:00 (smth that can be stopped easily)
|
||||
se 21:40
|
||||
read
|
||||
bed 22:10";
|
||||
|
||||
let tmp_file =
|
||||
std::env::temp_dir().join(format!("habitica-tasks-{}.md", Utc::now().timestamp()));
|
||||
std::fs::write(&tmp_file, initial_content)
|
||||
.map_err(|e| mize_err!("Failed to write temp file: {}", e))?;
|
||||
|
||||
// Open in nvim for editing
|
||||
std::process::Command::new("nvim")
|
||||
.arg(&tmp_file)
|
||||
.status()
|
||||
.map_err(|e| mize_err!("Failed to run nvim: {}", e))?;
|
||||
|
||||
// Read back and parse
|
||||
let list_str = std::fs::read_to_string(&tmp_file)
|
||||
.map_err(|e| mize_err!("Failed to read temp file: {}", e))?;
|
||||
let _ = std::fs::remove_file(&tmp_file);
|
||||
|
||||
let tasks: Vec<&str> = list_str
|
||||
.lines()
|
||||
.map(|l| l.trim())
|
||||
.filter(|l| !l.is_empty())
|
||||
.collect();
|
||||
|
||||
// Add tasks to Habitica
|
||||
let mut task_ids = Vec::new();
|
||||
{
|
||||
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||
for todo_text in &tasks {
|
||||
let data = hab.api_request(
|
||||
reqwest::Method::POST,
|
||||
"tasks/user".to_string(),
|
||||
json!({ "text": todo_text, "type": "todo" }),
|
||||
)?;
|
||||
let id = data["id"].as_str().unwrap_or("").to_string();
|
||||
println!("Added to-do: '{}' with id '{}'", todo_text, id);
|
||||
task_ids.push(id);
|
||||
}
|
||||
|
||||
// Move all tasks to bottom
|
||||
for task_id in &task_ids {
|
||||
hab.api_request(
|
||||
reqwest::Method::POST,
|
||||
format!("tasks/{}/move/to/-1", task_id),
|
||||
json!({}),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dump_habitica_logs(mize: &mut Mize) -> MizeResult<()> {
|
||||
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
|
||||
c2vi.with_db(|conn| {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT date, data FROM habitica_log ORDER BY date")
|
||||
.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
})
|
||||
.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||
for row in rows {
|
||||
let (date, data_str) = row.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||
let data: Value =
|
||||
serde_json::from_str(&data_str).unwrap_or(json!({"parse_error": data_str}));
|
||||
println!(
|
||||
"{}: {}",
|
||||
date,
|
||||
serde_json::to_string_pretty(&data).unwrap_or_default()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
277
marts/src/cli/mod.rs
Normal file
277
marts/src/cli/mod.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
use clap::{ArgMatches, Command};
|
||||
use dioxus::core::SpawnIfAsync;
|
||||
use std::{cell::RefCell, collections::HashMap};
|
||||
|
||||
use mize::{
|
||||
item::ItemData, mize_err, mize_part, proto::MizeMessage, Mize, MizeError, MizePart,
|
||||
MizePartCreate, MizeResult,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
pub static MIZE_CLI_SPAWN: RefCell<Option<CliSpawn>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
struct CliSpawn {
|
||||
wait_rx: flume::Receiver<()>,
|
||||
conn_id: Option<u64>,
|
||||
mize: Mize,
|
||||
}
|
||||
|
||||
pub fn cli_spawn_setup(mize: Mize, conn_id: u64, wait_rx: flume::Receiver<()>) -> MizeResult<()> {
|
||||
MIZE_CLI_SPAWN.with(|cell| {
|
||||
*cell.borrow_mut() = Some(CliSpawn {
|
||||
wait_rx,
|
||||
conn_id: Some(conn_id),
|
||||
mize,
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cli_spawn(cmd: String) -> MizeResult<()> {
|
||||
MIZE_CLI_SPAWN.with(|cell| {
|
||||
if let Some(spawn) = cell.borrow().as_ref() {
|
||||
let conn_id = spawn.conn_id.ok_or(mize_err!("no conn_id set"))?;
|
||||
let conn = spawn.mize.get_connection(conn_id)?;
|
||||
conn.send(MizeMessage::new_custom("cli.spawn", cmd, conn_id))?;
|
||||
|
||||
log::info!("before wait_rx.recv()");
|
||||
spawn.wait_rx.recv()?;
|
||||
log::info!("after wait_rx.recv()");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(mize_err!("no cli spawn thread_local available"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[mize_part("cli")]
|
||||
#[derive(Default)]
|
||||
pub struct CliPart {
|
||||
mize: Mize,
|
||||
cmds: HashMap<String, Command>,
|
||||
current_cmd_name: String,
|
||||
actions:
|
||||
HashMap<String, Box<dyn Fn(&ArgMatches, Mize) -> MizeResult<()> + Send + Sync + 'static>>,
|
||||
parsers: Option<Vec<Box<dyn Fn(Mize, Vec<String>) -> MizeResult<()> + Send + Sync + 'static>>>,
|
||||
}
|
||||
|
||||
pub fn cli(mize: &mut Mize) -> MizeResult<()> {
|
||||
let command = Command::new("marts-cli").allow_external_subcommands(true);
|
||||
let (spawn_wait_tx, spawn_wait_rx) = flume::unbounded::<()>();
|
||||
let mut cli_part = CliPart {
|
||||
mize: mize.clone(),
|
||||
cmds: HashMap::new(),
|
||||
current_cmd_name: "marts-cli".to_string(),
|
||||
actions: HashMap::new(),
|
||||
parsers: Some(Vec::new()),
|
||||
};
|
||||
cli_part.cmds.insert("marts-cli".to_string(), command);
|
||||
|
||||
mize.add_listener(
|
||||
"cli",
|
||||
Box::new(move |msg, mize| cli_msg_handler(msg, mize, spawn_wait_rx.clone())),
|
||||
)?;
|
||||
mize.add_listener(
|
||||
"cli.spawn_done",
|
||||
Box::new(move |mut msg, mize| {
|
||||
log::info!("before send");
|
||||
spawn_wait_tx.send(());
|
||||
log::info!("after send");
|
||||
Ok(())
|
||||
}),
|
||||
)?;
|
||||
mize.add_part(Box::new(cli_part))
|
||||
}
|
||||
|
||||
impl MizePartCreate for CliPart {}
|
||||
|
||||
impl MizePart for CliPart {
|
||||
fn init(&mut self, _mize: &mut Mize) -> MizeResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn run(&mut self, mize: &mut Mize) -> MizeResult<()> {
|
||||
if mize
|
||||
.get_config("cli.daemonMode")
|
||||
.unwrap_or(ItemData::new())
|
||||
.as_bool()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let matches = self
|
||||
.cmds
|
||||
.remove(&self.current_cmd_name)
|
||||
.unwrap()
|
||||
.get_matches();
|
||||
let sub_cmd = match matches.subcommand_name() {
|
||||
Some(sub_cmd) => sub_cmd,
|
||||
None => {
|
||||
log::info!("No subcommand provided");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let action = match self.actions.remove(sub_cmd) {
|
||||
Some(action) => action,
|
||||
None => {
|
||||
// external parsers
|
||||
for parser in self.parsers.take().unwrap() {
|
||||
parser(
|
||||
mize.clone(),
|
||||
matches
|
||||
.subcommand_matches(sub_cmd)
|
||||
.unwrap()
|
||||
.get_many::<String>("")
|
||||
.unwrap()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
action(matches.subcommand_matches(sub_cmd).unwrap(), mize.clone())?;
|
||||
Ok(())
|
||||
}
|
||||
fn opts(&self, mize: &mut Mize) {
|
||||
mize.new_opt("cli.name");
|
||||
}
|
||||
}
|
||||
|
||||
fn cli_msg_handler(
|
||||
mut msg: MizeMessage,
|
||||
mize: &mut Mize,
|
||||
spawn_wait_rx: flume::Receiver<()>,
|
||||
) -> MizeResult<()> {
|
||||
let binding = msg.data()?;
|
||||
let data = binding
|
||||
.cbor()
|
||||
.as_array()
|
||||
.ok_or(mize_err!("cli message data was not an array"))?;
|
||||
let full_cmd = data
|
||||
.iter()
|
||||
.nth(0)
|
||||
.cloned()
|
||||
.ok_or(mize_err!("cli message data was empty"))?;
|
||||
let full_cmd_text = full_cmd
|
||||
.as_text()
|
||||
.ok_or(mize_err!("cli cmd was not text"))?;
|
||||
let cmd = full_cmd_text
|
||||
.split("/")
|
||||
.last()
|
||||
.ok_or(mize_err!("failed to get cmd"))?;
|
||||
let mut cli = mize.get_part_native::<CliPart>("cli").unwrap();
|
||||
let command = cli
|
||||
.cmds
|
||||
.get(cmd)
|
||||
.ok_or(mize_err!("no command {cmd} in cli part"))?;
|
||||
let matches = command
|
||||
.clone()
|
||||
.get_matches_from(data.iter().map(|v| v.as_text().unwrap()));
|
||||
let sub_cmd_name = match matches.subcommand_name() {
|
||||
Some(sub_cmd) => sub_cmd,
|
||||
None => {
|
||||
cli.send_stderr(&msg, "No subcommand provided".to_string())?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let action = match cli.actions.get_mut(sub_cmd_name) {
|
||||
Some(action) => action,
|
||||
None => {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let original_msg = msg.clone();
|
||||
let mize_clone = mize.clone();
|
||||
mize::MIZE_STDOUT.with(move |stdout| {
|
||||
*stdout.borrow_mut() = Some(Box::new(move |string| {
|
||||
let conn_id = original_msg.conn_id;
|
||||
let msg = MizeMessage::new_custom("cli.stdout", string, conn_id);
|
||||
let conn = mize_clone.get_connection(conn_id)?;
|
||||
conn.send(msg)?;
|
||||
Ok(())
|
||||
}))
|
||||
});
|
||||
|
||||
cli_spawn_setup(mize.clone(), msg.conn_id, spawn_wait_rx)?;
|
||||
|
||||
action(
|
||||
matches.subcommand_matches(sub_cmd_name).unwrap(),
|
||||
mize.clone(),
|
||||
)?;
|
||||
cli.send_done(&msg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl CliPart {
|
||||
pub fn set_command(&mut self, cmd: &str) {
|
||||
self.current_cmd_name = cmd.to_owned();
|
||||
if !self.cmds.contains_key(cmd) {
|
||||
self.cmds
|
||||
.insert(cmd.to_owned(), Command::new(cmd.to_owned()));
|
||||
}
|
||||
}
|
||||
pub fn send_stderr(&mut self, original_msg: &MizeMessage, msg: String) -> MizeResult<()> {
|
||||
let conn_id = original_msg.conn_id;
|
||||
let msg = MizeMessage::new_custom("cli.stderr", msg, conn_id);
|
||||
let conn = self.mize.get_connection(conn_id)?;
|
||||
conn.send(msg)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn send_stdout(&mut self, original_msg: &MizeMessage, msg: String) -> MizeResult<()> {
|
||||
let conn_id = original_msg.conn_id;
|
||||
let msg = MizeMessage::new_custom("cli.stdout", msg, conn_id);
|
||||
let conn = self.mize.get_connection(conn_id)?;
|
||||
conn.send(msg)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn send_spawn(&mut self, original_msg: &MizeMessage, msg: String) -> MizeResult<()> {
|
||||
let conn_id = original_msg.conn_id;
|
||||
let msg = MizeMessage::new_custom("cli.spawn", msg, conn_id);
|
||||
let conn = self.mize.get_connection(conn_id)?;
|
||||
conn.send(msg)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn send_done(&mut self, original_msg: &MizeMessage) -> MizeResult<()> {
|
||||
let conn_id = original_msg.conn_id;
|
||||
let msg = MizeMessage::new_custom("cli.done", ItemData::new(), conn_id);
|
||||
let conn = self.mize.get_connection(conn_id)?;
|
||||
conn.send(msg)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn subcommand<T: Fn(&ArgMatches, Mize) -> MizeResult<()> + Send + Sync + 'static>(
|
||||
&mut self,
|
||||
subcmd: Command,
|
||||
action: T,
|
||||
) -> MizeResult<()> {
|
||||
let name = subcmd.get_name().to_string();
|
||||
let mut cmd = self
|
||||
.cmds
|
||||
.remove(&self.current_cmd_name)
|
||||
.unwrap()
|
||||
.subcommand(subcmd);
|
||||
self.cmds.insert(self.current_cmd_name.clone(), cmd);
|
||||
self.actions.insert(name, Box::new(action));
|
||||
Ok(())
|
||||
}
|
||||
pub fn add_sub_parser<T: Fn(Mize, Vec<String>) -> MizeResult<()> + Send + Sync + 'static>(
|
||||
&mut self,
|
||||
parser: T,
|
||||
) -> MizeResult<()> {
|
||||
self.parsers.as_mut().unwrap().push(Box::new(parser));
|
||||
Ok(())
|
||||
}
|
||||
pub fn with_cmd(
|
||||
&mut self,
|
||||
func: fn(mize: Mize, cmd: Command) -> MizeResult<Command>,
|
||||
) -> MizeResult<()> {
|
||||
let cmd = self.cmds.remove(&self.current_cmd_name).ok_or(mize_err!(
|
||||
"current command {} not found",
|
||||
self.current_cmd_name
|
||||
))?;
|
||||
let cmd = func(self.mize.clone(), cmd)?;
|
||||
self.cmds.insert(self.current_cmd_name.clone(), cmd);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
128
marts/src/habitica.rs
Normal file
128
marts/src/habitica.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use mize::{mize_err, mize_part, Mize, MizeError, MizePart, MizePartCreate, MizeResult};
|
||||
use reqwest::{blocking::Client, blocking::Response, header, Method};
|
||||
use serde_json::{json, Value};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
#[mize_part]
|
||||
#[derive(Default)]
|
||||
pub struct Habitica {
|
||||
mize: Mize,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
pub fn habitica(mize: &mut Mize) -> MizeResult<()> {
|
||||
let client = Client::new();
|
||||
mize.register_part(Box::new(Habitica {
|
||||
mize: mize.clone(),
|
||||
client,
|
||||
}))
|
||||
}
|
||||
|
||||
impl MizePartCreate for Habitica {}
|
||||
impl MizePart for Habitica {
|
||||
fn opts(&self, mize: &mut Mize) {
|
||||
mize.new_opt("habitica.api_url");
|
||||
mize.new_opt("habitica.user_id");
|
||||
mize.new_opt("habitica.api_token");
|
||||
mize.new_opt("habitica.client_name");
|
||||
}
|
||||
}
|
||||
|
||||
impl Habitica {
|
||||
pub fn api_request(&mut self, method: Method, path: String, data: Value) -> MizeResult<Value> {
|
||||
let api_url = self.mize.get_config("habitica.api_url")?.to_string();
|
||||
let user_id = self.mize.get_config("habitica.user_id")?.to_string();
|
||||
let api_token = self.mize.get_config("habitica.api_token")?.to_string();
|
||||
let client_name = self.mize.get_config("habitica.client_name")?.to_string();
|
||||
|
||||
let mut headers = header::HeaderMap::new();
|
||||
headers.insert(
|
||||
"Content-Type",
|
||||
header::HeaderValue::from_static("application/json"),
|
||||
);
|
||||
headers.insert("x-api-user", header::HeaderValue::from_str(&user_id)?);
|
||||
headers.insert("x-api-key", header::HeaderValue::from_str(&api_token)?);
|
||||
headers.insert("x-client", header::HeaderValue::from_str(&client_name)?);
|
||||
|
||||
let url = format!("{}/{}", api_url, path.trim_start_matches('/'));
|
||||
|
||||
let mut request_builder = self.client.request(method, &url).headers(headers);
|
||||
|
||||
if data != json!({}) {
|
||||
request_builder = request_builder.json(&data);
|
||||
}
|
||||
|
||||
let response = request_builder.send()?;
|
||||
|
||||
handle_rate_limit(&response);
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let text = response.text().unwrap_or_default();
|
||||
log::info!("request_url: {}", url);
|
||||
return Err(mize_err!(
|
||||
"Habitica API error: {} {} - {}",
|
||||
status.as_u16(),
|
||||
status.canonical_reason().unwrap_or(""),
|
||||
text
|
||||
));
|
||||
}
|
||||
|
||||
let json_response: Value = response.json()?;
|
||||
Ok(json_response.get("data").cloned().unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
pub fn get_tasks(&mut self, task_type: &str) -> MizeResult<Value> {
|
||||
self.api_request(
|
||||
Method::GET,
|
||||
format!("tasks/user?type={}", task_type),
|
||||
json!({}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delete_task(&mut self, id: &str) -> MizeResult<Value> {
|
||||
self.api_request(Method::DELETE, format!("tasks/{}", id), json!({}))
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_rate_limit(response: &Response) {
|
||||
let headers = response.headers();
|
||||
let limit = headers
|
||||
.get("X-RateLimit-Limit")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("NONE");
|
||||
let remaining = headers
|
||||
.get("X-RateLimit-Remaining")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
.unwrap_or(10);
|
||||
let reset = headers
|
||||
.get("X-RateLimit-Reset")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
log::info!(
|
||||
"RateLimit: {} | Remaining: {} | Reset: {}",
|
||||
limit,
|
||||
remaining,
|
||||
reset.unwrap_or("N/A")
|
||||
);
|
||||
|
||||
if remaining < 2 {
|
||||
if let Some(reset_str) = reset {
|
||||
if let Ok(reset_date) = DateTime::parse_from_rfc2822(reset_str) {
|
||||
let now = Utc::now();
|
||||
let wait_duration = reset_date.signed_duration_since(now).to_std();
|
||||
if let Ok(wait_duration) = wait_duration {
|
||||
let wait_ms = wait_duration.as_millis() as u64 + 1000;
|
||||
log::info!(
|
||||
"Waiting {} secs for next rate limit window...",
|
||||
(wait_ms / 1000)
|
||||
);
|
||||
sleep(Duration::from_millis(wait_ms));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
4
marts/src/index.ts
Normal file
4
marts/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./task/obsidian-otask-parse";
|
||||
export * from "./c2vi/obsidian-canvas-patch";
|
||||
|
||||
export * from "./js/stub_deno";
|
||||
53
marts/src/js/glue_deno.rs
Normal file
53
marts/src/js/glue_deno.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use deno_core::extension;
|
||||
use deno_core::op2;
|
||||
use deno_core::OpState;
|
||||
use mize::{Mize, MizeError};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::CliPart;
|
||||
use crate::JsPart;
|
||||
|
||||
#[op2(fast)]
|
||||
fn op_mize_get_part(#[string] name: &str) {
|
||||
log::info!("js wants the part {}", name);
|
||||
}
|
||||
|
||||
#[op2]
|
||||
#[string]
|
||||
fn op_mize_get_config(state: &mut OpState, #[string] key: &str) -> String {
|
||||
let mize: Mize = state.borrow::<Mize>().clone();
|
||||
format!("config_value_for_{}", key)
|
||||
}
|
||||
|
||||
#[op2(fast)]
|
||||
fn op_mize_add_part(state: &mut OpState, #[string] key: &str) {
|
||||
let mize: Mize = state.borrow::<Mize>().clone();
|
||||
log::info!("adding part {}", key);
|
||||
}
|
||||
|
||||
#[op2(fast)]
|
||||
fn op_cli_subcommand(state: &mut OpState, #[string] name: String) {
|
||||
let mut mize: Mize = state.borrow::<Mize>().clone();
|
||||
let mut cli = mize.get_part_native::<CliPart>("cli").unwrap();
|
||||
cli.subcommand(
|
||||
clap::Command::new(name.clone()),
|
||||
move |_sub_matches, mut mize| {
|
||||
let mut js = mize.get_part_native::<JsPart>("js").unwrap();
|
||||
js.eval("log(\"cli command being called\")".to_string())?;
|
||||
js.eval(format!("mize.vars.cli.subcommand({name})"))?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Create an extension with your custom ops
|
||||
extension!(
|
||||
my_extension,
|
||||
ops = [op_mize_get_part, op_mize_get_config],
|
||||
options = {
|
||||
mize: Mize,
|
||||
},
|
||||
state = |state: &mut OpState, config: Config| {
|
||||
state.put::<Mize>(config.mize);
|
||||
},
|
||||
);
|
||||
11
marts/src/js/glue_deno.ts
Normal file
11
marts/src/js/glue_deno.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Mize } from "./stub_deno.ts";
|
||||
|
||||
function argsToMessage(...args) {
|
||||
return args.map((arg) => JSON.stringify(arg)).join(" ");
|
||||
}
|
||||
|
||||
globalThis.log = (...args) => {
|
||||
Deno.core.print(`[out]: ${argsToMessage(...args)}\n`, false);
|
||||
};
|
||||
|
||||
globalThis.mize = new Mize();
|
||||
136
marts/src/js/mod.rs
Normal file
136
marts/src/js/mod.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use deno_core::error::AnyError;
|
||||
use deno_core::url::Url;
|
||||
use deno_core::v8::OneByteConst;
|
||||
use deno_core::{
|
||||
ascii_str_include, extension, op2, JsRuntime, ModuleSpecifier, PollEventLoopOptions,
|
||||
RuntimeOptions,
|
||||
};
|
||||
use deno_core::{FastStaticString, FsModuleLoader};
|
||||
use mize::async_trait;
|
||||
use mize::instance::MizePartCreate;
|
||||
use mize::{mize_part, Mize, MizePart, MizeResult};
|
||||
use std::future::Future;
|
||||
use std::path::Path;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
use tokio::runtime::Builder;
|
||||
use tokio::task::{spawn_local, LocalSet};
|
||||
|
||||
mod glue_deno;
|
||||
|
||||
#[mize_part("js")]
|
||||
#[derive(Default)]
|
||||
pub struct JsPart {
|
||||
mize: Mize,
|
||||
sender: Option<flume::Sender<JsRuntimeThreadMessage>>,
|
||||
}
|
||||
|
||||
pub type BoxFuture<'a> = Pin<Box<dyn Future<Output = MizeResult<()>> + 'a>>;
|
||||
pub type BoxClosure = Box<dyn for<'a> FnOnce(&'a mut JsRuntime) -> BoxFuture<'a> + Send + 'static>;
|
||||
|
||||
impl MizePart for JsPart {
|
||||
fn run(&mut self, mize: &mut Mize) -> MizeResult<()> {
|
||||
log::info!("js part run");
|
||||
self.sender
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.send(JsRuntimeThreadMessage::DoRunPhase)
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn js(mize: &mut Mize) -> MizeResult<()> {
|
||||
let (sender, receiver) = flume::unbounded::<JsRuntimeThreadMessage>();
|
||||
let mize_clone = mize.clone();
|
||||
|
||||
// the thread which will run any js
|
||||
mize.spawn_and_wait("js_runtime_thread", || {
|
||||
js_runtime_thread(mize_clone, receiver)
|
||||
})?;
|
||||
|
||||
mize.add_part(Box::new(JsPart {
|
||||
mize: mize.clone(),
|
||||
sender: Some(sender),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn part_from_file(
|
||||
mize: &mut Mize,
|
||||
name: &'static str,
|
||||
code: FastStaticString,
|
||||
) -> MizeResult<()> {
|
||||
let mut js = mize.get_part_native::<JsPart>("js")?;
|
||||
js.execute_init_js(code)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl JsPart {
|
||||
pub fn execute_init_js(&mut self, code: FastStaticString) -> MizeResult<()> {
|
||||
self.sender
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.send(JsRuntimeThreadMessage::RunInitJs(code))
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
pub fn eval(&mut self, code: String) -> MizeResult<()> {
|
||||
self.sender
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.send(JsRuntimeThreadMessage::RunJs(code))
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn js_runtime_thread(
|
||||
mize: Mize,
|
||||
receiver: flume::Receiver<JsRuntimeThreadMessage>,
|
||||
) -> MizeResult<()> {
|
||||
let mut js_runtime = JsRuntime::new(RuntimeOptions {
|
||||
//module_loader: Some(Rc::new(FsModuleLoader)),
|
||||
//extensions: vec![glue_deno::my_extension::init(mize.clone())],
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
js_runtime
|
||||
.execute_script("[stub]", ascii_str_include!("../deno_dist/glue_deno.js"))
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
log::info!("js thread waiting for smth");
|
||||
let msg = receiver.recv().unwrap();
|
||||
log::info!("js thread got smth");
|
||||
match msg {
|
||||
JsRuntimeThreadMessage::RunInitJs(js_code) => {
|
||||
if let Err(err) = js_runtime.execute_script("[init]", js_code) {
|
||||
log::info!("err: {}", err);
|
||||
}
|
||||
log::info!("done running js");
|
||||
}
|
||||
JsRuntimeThreadMessage::RunJs(js_code) => {
|
||||
if let Err(err) = js_runtime.execute_script("[idk]", js_code) {
|
||||
log::info!("err: {}", err);
|
||||
}
|
||||
log::info!("done running js");
|
||||
}
|
||||
JsRuntimeThreadMessage::DoRunPhase => {
|
||||
if let Err(err) = js_runtime.execute_script("[runPhase]", "mize.runPhase()") {
|
||||
log::info!("err: {}", err);
|
||||
}
|
||||
log::info!("done with runPhase");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum JsRuntimeThreadMessage {
|
||||
//Closure(Box<dyn FnOnce(&mut JsRuntime) -> MizeResult<()> + Send>),
|
||||
//AsyncClosure(BoxClosure),
|
||||
RunInitJs(FastStaticString),
|
||||
RunJs(String),
|
||||
DoRunPhase,
|
||||
}
|
||||
18
marts/src/js/stub_deno.ts
Normal file
18
marts/src/js/stub_deno.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export class Mize {
|
||||
async get_config(key: string): Promise<any> {
|
||||
return "hiiiiiii";
|
||||
}
|
||||
async add_opts(opts: any): Promise<void> {}
|
||||
async add_part(part: any): Promise<void> {}
|
||||
async report_err(err: MizeError) {}
|
||||
runPhase() {
|
||||
Deno.core.print("runPhase in js \n");
|
||||
}
|
||||
}
|
||||
|
||||
type MizeError = string;
|
||||
|
||||
export class Cli {
|
||||
constructor(mize: Mize) {}
|
||||
async sub_command(name: string, cmd: any): Promise<void> {}
|
||||
}
|
||||
44
marts/src/lib.rs
Normal file
44
marts/src/lib.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
#![feature(oneshot_channel)]
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
pub mod cli;
|
||||
#[cfg(feature = "target-os")]
|
||||
pub use cli::*;
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
pub mod habitica;
|
||||
#[cfg(feature = "target-os")]
|
||||
pub use habitica::*;
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
pub mod c2vi {
|
||||
pub mod part;
|
||||
pub mod dashboard;
|
||||
}
|
||||
#[cfg(feature = "target-os")]
|
||||
pub use c2vi::part::*;
|
||||
|
||||
#[cfg(feature = "target-obsidian")]
|
||||
pub mod obsidian;
|
||||
#[cfg(feature = "target-obsidian")]
|
||||
pub use obsidian::*;
|
||||
|
||||
pub mod spacetime;
|
||||
pub use spacetime::*;
|
||||
|
||||
pub mod auth {
|
||||
pub mod client;
|
||||
pub use client::*;
|
||||
}
|
||||
pub use auth::*;
|
||||
|
||||
pub mod ppc;
|
||||
|
||||
pub mod timetrack;
|
||||
|
||||
//#[cfg(feature = "target-os")]
|
||||
//pub mod js;
|
||||
//#[cfg(feature = "target-os")]
|
||||
//pub use js::*;
|
||||
|
||||
pub use generated;
|
||||
88
marts/src/obsidian.rs
Normal file
88
marts/src/obsidian.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use mize::{Mize, MizeResult, MizePart, MizePartCreate, mize_part};
|
||||
use wasm_bindgen::JsValue;
|
||||
use mize::error::IntoMizeResultJsValue;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
|
||||
|
||||
// console_log macro
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
fn log(s: &str);
|
||||
}
|
||||
|
||||
macro_rules! console_log {
|
||||
// Note that this is using the `log` function imported above during
|
||||
// `bare_bones`
|
||||
($($t:tt)*) => ({ log(&format_args!($($t)*).to_string())})
|
||||
}
|
||||
|
||||
pub fn part_obsidian(mize: &mut Mize, plugin: JsValue, app: JsValue) -> MizeResult<()> {
|
||||
|
||||
mize.register_part(Box::new(ObsidianPart {
|
||||
mize: mize.clone(),
|
||||
plugin,
|
||||
app,
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[mize_part("obsidian")]
|
||||
#[derive(Default)]
|
||||
pub struct ObsidianPart {
|
||||
mize: Mize,
|
||||
plugin: JsValue, // the jsValue of the obsidian plugin
|
||||
app: JsValue, // the jsValue of the obsidian instance
|
||||
}
|
||||
|
||||
pub struct MartsCommand {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub callback: Box<dyn FnMut(Mize) -> MizeResult<()>>,
|
||||
}
|
||||
|
||||
impl MizePart for ObsidianPart {}
|
||||
impl MizePartCreate for ObsidianPart {}
|
||||
|
||||
impl ObsidianPart {
|
||||
pub fn plugin(&self) -> &JsValue {
|
||||
&self.plugin
|
||||
}
|
||||
pub fn add_command(&self, mut cmd: MartsCommand) -> MizeResult<()> {
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use js_sys::{Function, Reflect, Object};
|
||||
let obsidian_cmd = Object::new();
|
||||
|
||||
Reflect::set(&obsidian_cmd, &"id".into(), &"login".into()).mres()?;
|
||||
Reflect::set(&obsidian_cmd, &"name".into(), &"Login".into()).mres()?;
|
||||
|
||||
// callback: () => run_login()
|
||||
let mize_clone = self.mize.clone();
|
||||
let cb = Closure::<dyn FnMut()>::wrap(Box::new(move || {
|
||||
if let Err(e) = (cmd.callback)(mize_clone.clone()) {
|
||||
mize_clone.report_err(e);
|
||||
}
|
||||
}));
|
||||
|
||||
Reflect::set(&obsidian_cmd, &"callback".into(), cb.as_ref()).mres()?;
|
||||
|
||||
// plugin.addCommand(cmd)
|
||||
let add_command_fn = Reflect::get(&self.plugin, &"addCommand".into()).mres()?
|
||||
.dyn_into::<Function>().mres()?;
|
||||
|
||||
add_command_fn
|
||||
.call1(&self.plugin, &obsidian_cmd).mres()?;
|
||||
|
||||
|
||||
// IMPORTANT: prevent callback from being dropped
|
||||
cb.forget();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
183
marts/src/ppc/main_page.rs
Normal file
183
marts/src/ppc/main_page.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
|
||||
pub fn main_page() -> Element {
|
||||
rsx! {
|
||||
"hello world"
|
||||
div {
|
||||
class: "min-h-screen bg-gray-100 items-center flex h-screen items-center justify-center",
|
||||
|
||||
// Header
|
||||
header {
|
||||
class: "bg-blue-600 text-white py-4",
|
||||
div {
|
||||
class: "container mx-auto px-4",
|
||||
h1 {
|
||||
class: "text-2xl font-bold",
|
||||
"People Corner (PPC)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main content
|
||||
main {
|
||||
class: "container mx-auto px-4 py-8",
|
||||
|
||||
// Hero section
|
||||
section {
|
||||
class: "bg-white rounded-lg shadow-md p-6 mb-8",
|
||||
h2 {
|
||||
class: "text-xl font-semibold mb-4 text-blue-600",
|
||||
"Welcome to People Corner"
|
||||
}
|
||||
p {
|
||||
class: "text-gray-700 mb-4",
|
||||
"PPC is a social platform for intentional people who want to get more out of life together."
|
||||
}
|
||||
p {
|
||||
class: "text-gray-700",
|
||||
"We're building a community-driven social media network that brings people together to do cool things, share resources, and support each other."
|
||||
}
|
||||
}
|
||||
|
||||
// Mission section
|
||||
section {
|
||||
class: "bg-white rounded-lg shadow-md p-6 mb-8",
|
||||
h2 {
|
||||
class: "text-xl font-semibold mb-4 text-blue-600",
|
||||
"Our Mission"
|
||||
}
|
||||
ul {
|
||||
class: "list-disc pl-6 space-y-2",
|
||||
li { "Bring people together to get more out of life" }
|
||||
li { "Create meaningful connections beyond superficial social media" }
|
||||
li { "Support intentional living and personal growth" }
|
||||
li { "Build a platform by people, for people" }
|
||||
li { "Foster real-world meetups and collaborations" }
|
||||
li { "Create a social media for diligent, intentional people" }
|
||||
}
|
||||
}
|
||||
|
||||
// Key concepts section
|
||||
section {
|
||||
class: "bg-white rounded-lg shadow-md p-6",
|
||||
h2 {
|
||||
class: "text-xl font-semibold mb-4 text-blue-600",
|
||||
"Key Concepts"
|
||||
}
|
||||
ul {
|
||||
class: "list-disc pl-6 space-y-2",
|
||||
li {
|
||||
strong { "Trust Network: " }
|
||||
"A system for building and maintaining trust between members"
|
||||
}
|
||||
li {
|
||||
strong { "Intentional Communities: " }
|
||||
"Both online and physical spaces for like-minded people"
|
||||
}
|
||||
li {
|
||||
strong { "Resource Sharing: " }
|
||||
"Platform for sharing skills, knowledge, and physical resources"
|
||||
}
|
||||
li {
|
||||
strong { "Real Connections: " }
|
||||
"Focus on meaningful relationships over superficial interactions"
|
||||
}
|
||||
li {
|
||||
strong { "Personal Growth: " }
|
||||
"Support for individuals to achieve their life goals"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
footer {
|
||||
class: "bg-blue-600 text-white py-4 mt-8",
|
||||
div {
|
||||
class: "container mx-auto px-4 text-center",
|
||||
p { "© 2024 People Corner. All rights reserved." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// About page component
|
||||
pub fn about_page() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
class: "container mx-auto px-4 py-8",
|
||||
h1 {
|
||||
class: "text-2xl font-bold mb-4 text-blue-600",
|
||||
"About PPC"
|
||||
}
|
||||
p {
|
||||
class: "text-gray-700 mb-4",
|
||||
"People Corner (PPC) is more than just a social platform - it's a movement to bring intentional people together to create meaningful connections and achieve more in life."
|
||||
}
|
||||
p {
|
||||
class: "text-gray-700",
|
||||
"Our vision is to build a world where people support each other, share resources, and grow together through real-world interactions and digital connections."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contact page component
|
||||
pub fn contact_page() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
class: "container mx-auto px-4 py-8",
|
||||
h1 {
|
||||
class: "text-2xl font-bold mb-4 text-blue-600",
|
||||
"Contact Us"
|
||||
}
|
||||
p {
|
||||
class: "text-gray-700 mb-4",
|
||||
"We'd love to hear from you! Whether you have questions, feedback, or want to get involved, please reach out."
|
||||
}
|
||||
form {
|
||||
class: "max-w-md space-y-4",
|
||||
div {
|
||||
label {
|
||||
class: "block text-gray-700 mb-2",
|
||||
"Name"
|
||||
}
|
||||
input {
|
||||
class: "w-full px-3 py-2 border rounded-lg",
|
||||
r#type: "text",
|
||||
placeholder: "Your name"
|
||||
}
|
||||
}
|
||||
div {
|
||||
label {
|
||||
class: "block text-gray-700 mb-2",
|
||||
"Email"
|
||||
}
|
||||
input {
|
||||
class: "w-full px-3 py-2 border rounded-lg",
|
||||
r#type: "email",
|
||||
placeholder: "Your email"
|
||||
}
|
||||
}
|
||||
div {
|
||||
label {
|
||||
class: "block text-gray-700 mb-2",
|
||||
"Message"
|
||||
}
|
||||
textarea {
|
||||
class: "w-full px-3 py-2 border rounded-lg h-32",
|
||||
placeholder: "Your message"
|
||||
}
|
||||
}
|
||||
button {
|
||||
class: "bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700",
|
||||
r#type: "submit",
|
||||
"Send Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
4
marts/src/ppc/mod.rs
Normal file
4
marts/src/ppc/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
pub mod main_page;
|
||||
|
||||
pub use main_page::main_page;
|
||||
107
marts/src/spacetime.rs
Normal file
107
marts/src/spacetime.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use std::sync::oneshot::Sender;
|
||||
|
||||
use mize::{mize_part, Mize, MizePart, MizePartCreate, MizeResult};
|
||||
|
||||
use crate::auth::client::ClientAuth;
|
||||
use generated::spacetime::DbConnection;
|
||||
|
||||
#[mize_part]
|
||||
#[derive(Default)]
|
||||
pub struct SpacetimePart {
|
||||
mize: Mize,
|
||||
con: Option<DbConnection>,
|
||||
}
|
||||
|
||||
impl MizePartCreate for SpacetimePart {}
|
||||
impl MizePart for SpacetimePart {
|
||||
fn name(&self) -> &'static str {
|
||||
"spacetime"
|
||||
}
|
||||
}
|
||||
|
||||
impl SpacetimePart {
|
||||
pub fn con_mut(&mut self) -> &mut DbConnection {
|
||||
self.con.as_mut().unwrap()
|
||||
}
|
||||
pub fn con(&self) -> &DbConnection {
|
||||
self.con.as_ref().unwrap()
|
||||
}
|
||||
// does not work, because i can't use the QueryBuilder type in my code.... it's private
|
||||
/*
|
||||
pub fn get<T>(
|
||||
&self,
|
||||
on_applied: impl FnOnce(&generated::spacetime::SubscriptionEventContext, Sender<T>)+ Send + 'static,
|
||||
query: impl FnOnce(QueryBuilder)+ Send + 'static
|
||||
) -> MizeResult<T> {
|
||||
let (done_tx, done_rx) = std::sync::oneshot::channel::<T>();
|
||||
use spacetimedb_sdk::DbContext;
|
||||
self.con().subscription_builder().on_applied(move |ctx| {
|
||||
on_applied(ctx, done_tx);
|
||||
}).add_query(build);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
pub fn spacetime(mize: &mut Mize) -> MizeResult<()> {
|
||||
let auth = mize.get_part_native::<ClientAuth>("client_auth")?;
|
||||
let db_url = mize.get_config("spacetime.url")?.value_string()?;
|
||||
|
||||
let con: Option<DbConnection>;
|
||||
|
||||
#[cfg(feature = "target-os")] {
|
||||
con = Some(DbConnection::builder()
|
||||
.with_token(Some(auth.id_token()?))
|
||||
.with_database_name("mize")
|
||||
.with_uri(db_url)
|
||||
//.on_connect(callback)
|
||||
//.on_disconnect(callback)
|
||||
//.on_connect_error(callback)
|
||||
//.with_compression(...)
|
||||
.build()?);
|
||||
con.as_ref().unwrap().run_threaded();
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-js")] {
|
||||
let mut mize = mize.clone();
|
||||
con = None;
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let id_token = match auth.id_token() {
|
||||
Ok(val) => val,
|
||||
Err(e) => {
|
||||
mize.report_err(e.into());
|
||||
return;
|
||||
}
|
||||
};
|
||||
let con = match DbConnection::builder()
|
||||
.with_token(Some(id_token))
|
||||
.with_database_name("mize")
|
||||
.with_uri(db_url)
|
||||
//.on_connect(callback)
|
||||
//.on_disconnect(callback)
|
||||
//.on_connect_error(callback)
|
||||
//.with_compression(...)
|
||||
.build().await {
|
||||
Ok(val) => val,
|
||||
Err(e) => {
|
||||
mize.report_err(e.into());
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut spacetime = match mize.get_part_native::<SpacetimePart>("spacetime") {
|
||||
Ok(val) => val,
|
||||
Err(e) => {
|
||||
mize.report_err(e.into());
|
||||
return;
|
||||
}
|
||||
};
|
||||
con.run_async();
|
||||
spacetime.con = Some(con);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
mize.register_part(Box::new(SpacetimePart {
|
||||
mize: mize.clone(),
|
||||
con: con,
|
||||
}))
|
||||
}
|
||||
0
marts/src/task/mod.rs
Normal file
0
marts/src/task/mod.rs
Normal file
538
marts/src/task/obsidian-otask-parse.ts
Normal file
538
marts/src/task/obsidian-otask-parse.ts
Normal file
@@ -0,0 +1,538 @@
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkStringify from "remark-stringify";
|
||||
import { unified } from "unified";
|
||||
import { visit } from "unist-util-visit";
|
||||
import { Root, Content, Heading, List, ListItem, Text } from "mdast";
|
||||
import remarkFrontmatter from "remark-frontmatter";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import wikiLinkPlugin from "@flowershow/remark-wiki-link";
|
||||
import { TFile } from "obsidian";
|
||||
|
||||
const OTASK_PROP_NAMES = ["short", "outcome", "priority"];
|
||||
const ACTION_PROP_NAMES = [];
|
||||
|
||||
const REMARK = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkFrontmatter)
|
||||
.use(remarkGfm)
|
||||
.use(remarkStringify, { bullet: "-" });
|
||||
|
||||
export function task_obsidian_otask_parse(mize: any) {
|
||||
const obsidian_plugin = mize.get_part_native("obsidian");
|
||||
|
||||
obsidian_plugin.addCommand({
|
||||
id: "full-parse",
|
||||
name: "Parse all otask files into the db",
|
||||
callback: async () => {
|
||||
await full_parse();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function full_parse() {
|
||||
console.log("full parse.......................... yay");
|
||||
let otasks = [];
|
||||
OTASK_PARSE_QUEUE = [];
|
||||
OTASK_LIST = [];
|
||||
|
||||
const root_file = app.vault.getAbstractFileByPath("task/my-projects.md");
|
||||
|
||||
const content = await app.vault.read(root_file);
|
||||
const list = await getListUnderHeading(content, "root");
|
||||
|
||||
for (const entry of list) {
|
||||
queue_otasks_from_list_item(entry, []);
|
||||
}
|
||||
|
||||
// go through the queue of otaskParseTasks
|
||||
// using a queue instead of recursive calls, to make it more debuggable...
|
||||
do {
|
||||
const task = OTASK_PARSE_QUEUE.shift();
|
||||
dbg("task", task);
|
||||
if (task.file) {
|
||||
const otask = await otasks_from_file(
|
||||
task.file,
|
||||
task.otask_path,
|
||||
task.priority,
|
||||
);
|
||||
dbg("otask", otask);
|
||||
if (otask) {
|
||||
OTASK_LIST.push(otask);
|
||||
}
|
||||
} else {
|
||||
const otask = await otasks_from_list_item(
|
||||
task.listItem,
|
||||
task.otask_path,
|
||||
task.priority,
|
||||
);
|
||||
dbg("otask", otask);
|
||||
if (otask) {
|
||||
OTASK_LIST.push(otask);
|
||||
}
|
||||
}
|
||||
} while (OTASK_PARSE_QUEUE.length > 0);
|
||||
|
||||
console.log("otasks::", OTASK_LIST);
|
||||
|
||||
render_sub0_file(OTASK_LIST);
|
||||
}
|
||||
|
||||
let OTASK_PARSE_QUEUE: any = [];
|
||||
let OTASK_LIST = [];
|
||||
let DBG = false;
|
||||
|
||||
function dbg() {
|
||||
if (DBG) {
|
||||
console.log(arguments);
|
||||
}
|
||||
}
|
||||
|
||||
function queue_otasks_from_list_item(listItem, otask_path, priority = 0) {
|
||||
OTASK_PARSE_QUEUE.push({
|
||||
listItem,
|
||||
otask_path,
|
||||
priority,
|
||||
});
|
||||
}
|
||||
|
||||
function queue_otasks_from_file(file, otask_path, priority = 0) {
|
||||
OTASK_PARSE_QUEUE.push({
|
||||
file,
|
||||
otask_path,
|
||||
priority,
|
||||
});
|
||||
}
|
||||
|
||||
// parsing helper functions
|
||||
|
||||
async function render_sub0_file(otasks) {
|
||||
const sub0_file = app.vault.getAbstractFileByPath("private/sub0-tasks.md");
|
||||
if (!(sub0_file instanceof TFile)) {
|
||||
console.error(
|
||||
"SystemC2: sub0_file not found:",
|
||||
"private/sub0-tasks.md",
|
||||
sub0_file,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
//render sub0
|
||||
const sub0_otasks = otasks.filter(
|
||||
(otask) => otask.priority == 0 && !otask.is_file,
|
||||
);
|
||||
console.log("sub0 otasks...", sub0_otasks);
|
||||
|
||||
let rendered_otasks_string = "";
|
||||
|
||||
for (const otask of sub0_otasks) {
|
||||
const path_part = otask.path.length > 0 ? otask.path.join(": ") + ": " : "";
|
||||
|
||||
let content_part = otask.content ? otask.content + "\n" : "";
|
||||
content_part = content_part
|
||||
.split("\n")
|
||||
.map((line) => "\t" + line)
|
||||
.join("\n"); // add a \t in front of every otask.content line
|
||||
|
||||
rendered_otasks_string += `- ${path_part}${otask.longName}\n${content_part}\n`;
|
||||
}
|
||||
//console.log("newContent...", rendered_otasks_string)
|
||||
|
||||
await replaceHeading(sub0_file, "sub0", rendered_otasks_string);
|
||||
|
||||
//render sub-1
|
||||
}
|
||||
|
||||
async function replaceHeading(
|
||||
file: TFile,
|
||||
heading: string,
|
||||
newContent: string,
|
||||
) {
|
||||
const data = await this.app.vault.read(file);
|
||||
|
||||
// Parse the Markdown into an AST
|
||||
const tree = REMARK.parse(data) as Root;
|
||||
|
||||
// Split new content into AST nodes
|
||||
const newContentAst = REMARK.parse(newContent) as Root;
|
||||
|
||||
const children = tree.children;
|
||||
const newChildren: Content[] = [];
|
||||
let insideTarget = false;
|
||||
let targetLevel = 0;
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const node = children[i];
|
||||
|
||||
if (node.type === "heading") {
|
||||
const headingNode = node as Heading;
|
||||
const text = headingNode.children
|
||||
.filter((c) => c.type === "text")
|
||||
.map((c: any) => c.value)
|
||||
.join("");
|
||||
|
||||
// If we encounter our target heading
|
||||
if (text.trim() === heading) {
|
||||
insideTarget = true;
|
||||
targetLevel = headingNode.depth;
|
||||
newChildren.push(node); // Keep the heading itself
|
||||
// Insert new content right after
|
||||
newChildren.push(...newContentAst.children);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we’re currently inside target section and see a heading of same or higher level
|
||||
if (insideTarget && headingNode.depth <= targetLevel) {
|
||||
insideTarget = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If we’re not inside the target section, keep the node
|
||||
if (!insideTarget) {
|
||||
newChildren.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
const newTree: Root = { ...tree, children: newChildren };
|
||||
|
||||
// Convert AST back to Markdown
|
||||
const newMarkdown = await REMARK.stringify(newTree);
|
||||
|
||||
await this.app.vault.modify(file, newMarkdown);
|
||||
}
|
||||
|
||||
async function otasks_from_list_item(listItem, otask_path, priority = 0) {
|
||||
try {
|
||||
// the main otask this listItem is about
|
||||
let otask = {
|
||||
description: [],
|
||||
actions: [],
|
||||
path: [],
|
||||
priority,
|
||||
};
|
||||
dbg("SystemC2: otasks_from_list_item", listItem, otask_path);
|
||||
|
||||
if (!listItem.children[0].type === "paragraph") {
|
||||
dbg(
|
||||
"listItem[0] of a otask item is not a paragraph... the otask:",
|
||||
listItem,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
let longName = nodeToString(listItem.children[0]);
|
||||
|
||||
dbg("longName:", longName);
|
||||
|
||||
// check for [[anotherotask]], call otasks_from_file, return those
|
||||
const match = longName.match(/\[\[([^\]]+)\]\]/);
|
||||
if (match) {
|
||||
const file = app.metadataCache.getFirstLinkpathDest(match[1], "");
|
||||
if (file) {
|
||||
const metadata = app.metadataCache.getFileCache(file);
|
||||
|
||||
if (metadata?.frontmatter?.tags?.includes("t/otask")) {
|
||||
queue_otasks_from_file(file, otask_path);
|
||||
return null;
|
||||
} else {
|
||||
dbg(
|
||||
"SystemC2: listItem with Link to '",
|
||||
match[1],
|
||||
"', which is not an otask",
|
||||
longName,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//check for [x] and remove from name
|
||||
if (longName.startsWith("[ ") || longName.startsWith("[x")) {
|
||||
longName = longName.slice(4);
|
||||
}
|
||||
|
||||
//check for "ot:" and remove from name
|
||||
if (longName.startsWith("ot:")) {
|
||||
longName = longName.slice(4);
|
||||
}
|
||||
|
||||
otask.longName = longName;
|
||||
otask.short = longName;
|
||||
otask.path = otask_path;
|
||||
let new_otask_path = [...otask_path, otask.short];
|
||||
|
||||
// go through sub list items
|
||||
// - if it's a prop for the otask... handle that
|
||||
// - if it's an "ac:" handle that
|
||||
// - if it's "ot: " handle as another otask -> recursive call
|
||||
// - if it's "[ ]" handle as another otask -> recursive call
|
||||
// - if it's a link to a otask file... handle as another otask -> call otasks_from_file()
|
||||
// - else add it as description item list to otask
|
||||
if (!listItem.children || listItem.children.length < 2) {
|
||||
return otask;
|
||||
}
|
||||
|
||||
// add content as text to otask
|
||||
if (otask.longName == "hosting updates") {
|
||||
console.log("listItem... ", listItem.children[1]);
|
||||
let hii = REMARK.stringify(listItem.children[1]);
|
||||
console.log("text...", hii);
|
||||
}
|
||||
let content_text = REMARK.stringify(listItem.children[1]);
|
||||
otask.content = content_text;
|
||||
|
||||
for (const subListItem of listItem.children[1].children) {
|
||||
let text = nodeToString(subListItem.children[0]);
|
||||
|
||||
// check prop
|
||||
if (check_otask_prop_from_list_item(subListItem, otask)) {
|
||||
new_otask_path = [...otask_path, otask.short]; // have to redo it here, in case short changed from aprop
|
||||
continue; // don't do any other processing on this sub-listItem
|
||||
}
|
||||
|
||||
// remove "[ ]"
|
||||
if (text.startsWith("[ ") || text.startsWith("[x")) {
|
||||
text = text.slice(4);
|
||||
}
|
||||
|
||||
// check ac
|
||||
if (text.startsWith("ac:")) {
|
||||
otask.actions = [
|
||||
...otask.actions,
|
||||
action_from_list_item(subListItem, new_otask_path, otask.priority),
|
||||
];
|
||||
|
||||
continue; // don't do any other processing on this sub-listItem
|
||||
}
|
||||
|
||||
// check ot
|
||||
if (text.startsWith("ot:")) {
|
||||
queue_otasks_from_list_item(
|
||||
subListItem,
|
||||
new_otask_path,
|
||||
otask.priority,
|
||||
);
|
||||
|
||||
continue; // don't do any other processing on this sub-listItem
|
||||
}
|
||||
|
||||
// check [ ]
|
||||
if (text.startsWith("[ ] ") || text.startsWith("[x] ")) {
|
||||
queue_otasks_from_list_item(
|
||||
subListItem,
|
||||
new_otask_path,
|
||||
otask.priority,
|
||||
);
|
||||
|
||||
continue; // don't do any other processing on this sub-listItem
|
||||
}
|
||||
|
||||
// check link to otask file
|
||||
const match = text.match(/\[\[([^\]]+)\]\]/);
|
||||
if (match) {
|
||||
const file = app.metadataCache.getFirstLinkpathDest(match[1], "");
|
||||
if (file) {
|
||||
const metadata = app.metadataCache.getFileCache(file);
|
||||
|
||||
if (metadata?.frontmatter?.tags?.includes("t/otask")) {
|
||||
queue_otasks_from_file(file, new_otask_path, otask.priority);
|
||||
continue;
|
||||
} else {
|
||||
dbg(
|
||||
"SystemC2: listItem with Link to '",
|
||||
match[1],
|
||||
"', which is not an otask",
|
||||
longName,
|
||||
);
|
||||
}
|
||||
|
||||
continue; // don't do any other processing on this sub-listItem
|
||||
}
|
||||
}
|
||||
|
||||
// add as description
|
||||
const desc = REMARK.stringify(subListItem);
|
||||
otask.description.push(desc);
|
||||
}
|
||||
|
||||
return otask;
|
||||
} catch (error) {
|
||||
console.log("failed to parse list item:", listItem);
|
||||
}
|
||||
}
|
||||
|
||||
async function otasks_from_file(file: TFile, otask_path, priority = 0) {
|
||||
try {
|
||||
dbg("SystemC2: otasks_from_file", file, otask_path);
|
||||
|
||||
// the main otask this file is about
|
||||
let otask = {
|
||||
description: [],
|
||||
actions: [],
|
||||
path: [],
|
||||
priority,
|
||||
is_file: true,
|
||||
};
|
||||
|
||||
otask.longName = file.basename;
|
||||
otask.short = otask.longName;
|
||||
otask.path = otask_path;
|
||||
let new_otask_path = [...otask_path, otask.short];
|
||||
|
||||
const content = await app.vault.read(file);
|
||||
|
||||
// parse listItems at the top... for props
|
||||
const topListItems = getListItemsBeforeFirstHeading(content);
|
||||
for (const topListItem of topListItems) {
|
||||
const text = nodeToString(topListItem);
|
||||
|
||||
// check for prop
|
||||
if (check_otask_prop_from_list_item(topListItem, otask)) {
|
||||
new_otask_path = [...otask_path, otask.short];
|
||||
continue; // don't do any other processing on this sub-listItem
|
||||
}
|
||||
}
|
||||
|
||||
// parse all the sub sections
|
||||
const SUB_COUNT = 10;
|
||||
for (let sub_number = 0; sub_number < SUB_COUNT; sub_number++) {
|
||||
let listItems = await getListUnderHeading(content, "sub" + sub_number);
|
||||
|
||||
// "sub" without a number should have priority 5
|
||||
if (sub_number == 5) {
|
||||
listItems = listItems.concat(await getListUnderHeading(content, "sub"));
|
||||
}
|
||||
|
||||
//dbg(`sub section ${sub_number} has items:`, listItems)
|
||||
|
||||
for (const listItem of listItems) {
|
||||
// check if listItem is an action...
|
||||
let text = nodeToString(listItem.children[0]);
|
||||
if (
|
||||
text.startsWith("ac:") ||
|
||||
text.startsWith("[ ] ac:") ||
|
||||
text.startsWith("[x] ac:")
|
||||
) {
|
||||
const action = action_from_list_item(listItem, otask_path);
|
||||
otask.actions.push(action);
|
||||
}
|
||||
|
||||
queue_otasks_from_list_item(
|
||||
listItem,
|
||||
new_otask_path,
|
||||
otask.priority + sub_number,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return otask;
|
||||
} catch (error) {
|
||||
console.log("failed to parse file: ", file.path);
|
||||
}
|
||||
}
|
||||
|
||||
function action_from_list_item(listItem, otask_path) {
|
||||
dbg("SystemC2: action_from_list_item", listItem, otask_path);
|
||||
let action = {};
|
||||
let text = nodeToString(listItem.children[0]).slice(4);
|
||||
|
||||
if (text.startsWith("[")) {
|
||||
text = text.slice(4);
|
||||
}
|
||||
|
||||
action.name = text;
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
function check_otask_prop_from_list_item(listItem, otask) {
|
||||
let text = nodeToString(listItem.children[0]);
|
||||
|
||||
for (const prop_name of OTASK_PROP_NAMES) {
|
||||
if (text.startsWith(prop_name + ":")) {
|
||||
const value = text.slice(prop_name.length + 2);
|
||||
|
||||
if (prop_name == "priority") {
|
||||
// parse priority as int
|
||||
otask[prop_name] = parseInt(value);
|
||||
} else if (prop_name == "outcome") {
|
||||
// add sub items to it
|
||||
let fullText = REMARK.stringify(listItem).slice(prop_name.length + 4);
|
||||
otask[prop_name] = fullText;
|
||||
} else {
|
||||
otask[prop_name] = value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract plain text from a node
|
||||
function nodeToString(node: any): string {
|
||||
let result = "";
|
||||
visit(node, "text", (textNode: Text) => {
|
||||
result += textNode.value;
|
||||
});
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
async function getListUnderHeading(
|
||||
markdown: string,
|
||||
headingText: string,
|
||||
): string[] {
|
||||
const tree = REMARK.parse(markdown);
|
||||
//dbg("tree", tree)
|
||||
|
||||
let capture = false;
|
||||
let items: string[] = [];
|
||||
|
||||
for (let i = 0; i < tree.children.length; i++) {
|
||||
const node: Content = tree.children[i];
|
||||
|
||||
// Detect the target heading
|
||||
if (
|
||||
node.type === "heading" &&
|
||||
nodeToString(node).toLowerCase() === headingText.toLowerCase()
|
||||
) {
|
||||
capture = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stop if another heading of the same or higher level comes
|
||||
if (capture && node.type === "heading") {
|
||||
break;
|
||||
}
|
||||
|
||||
// Collect list items if capturing
|
||||
if (capture && node.type === "list") {
|
||||
for (const li of (node as List).children) {
|
||||
if (li.type === "listItem") {
|
||||
items.push(li);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function getListItemsBeforeFirstHeading(markdown: string) {
|
||||
const tree = REMARK.parse(markdown);
|
||||
|
||||
let items = [];
|
||||
|
||||
for (const node of tree.children) {
|
||||
if (node.type === "heading") {
|
||||
// stop at first heading
|
||||
break;
|
||||
}
|
||||
if (node.type === "list") {
|
||||
for (const li of (node as List).children) {
|
||||
if (li.type === "listItem") {
|
||||
items.push(li);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
544
marts/src/timetrack/mod.rs
Normal file
544
marts/src/timetrack/mod.rs
Normal file
@@ -0,0 +1,544 @@
|
||||
use std::sync::oneshot;
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use generated::spacetime::{Profile, TaskPart};
|
||||
use mize::out;
|
||||
use mize::proto::MizeMessage;
|
||||
use mize::{Mize, MizeError, MizePart, MizePartCreate, MizeResult, mize_err, mize_part};
|
||||
use spacetimedb_sdk::{DbContext, Table};
|
||||
|
||||
use generated::spacetime::{
|
||||
Action, ActivitiesTableAccess, Activity, DbConnection, TaskPartTableAccess,
|
||||
activitiesQueryTableAccess, next_action, remove_activity, task_partQueryTableAccess,
|
||||
update_activity, update_upcoming,
|
||||
};
|
||||
use crate::spacetime::SpacetimePart;
|
||||
use spacetimedb_sdk::TableWithPrimaryKey;
|
||||
|
||||
#[mize_part]
|
||||
#[derive(Default)]
|
||||
pub struct TimeTrack {
|
||||
mize: Mize,
|
||||
}
|
||||
|
||||
pub fn timetrack(mize: &mut Mize) -> MizeResult<()> {
|
||||
#[cfg(feature = "target-os")] {
|
||||
let mut cli = mize.get_part_native::<crate::CliPart>("cli")?;
|
||||
cli.set_command("ppc");
|
||||
|
||||
cli.subcommand(
|
||||
Command::new("editActionList").alias("aal").arg(
|
||||
Arg::new("defaultTemplate")
|
||||
.short('d')
|
||||
.action(ArgAction::SetTrue),
|
||||
),
|
||||
command_edit_action_list,
|
||||
)?;
|
||||
cli.subcommand(Command::new("nextAction").alias("ne").arg(
|
||||
Arg::new("break")
|
||||
.short('b')
|
||||
.action(ArgAction::SetTrue),
|
||||
).arg(
|
||||
Arg::new("action")
|
||||
.trailing_var_arg(true)
|
||||
.allow_hyphen_values(true)
|
||||
.num_args(0..), // Accepts 0 or more arguments
|
||||
), command_next_action)?;
|
||||
cli.subcommand(
|
||||
Command::new("printCurrentAction").alias("cu"),
|
||||
command_current_action,
|
||||
)?;
|
||||
cli.subcommand(
|
||||
Command::new("listActions").alias("la"),
|
||||
command_list_actions,
|
||||
)?;
|
||||
cli.subcommand(
|
||||
Command::new("listActivities").alias("lac"),
|
||||
command_list_activities,
|
||||
)?;
|
||||
cli.subcommand(
|
||||
Command::new("editActivities").alias("eac"),
|
||||
command_edit_activities,
|
||||
)?;
|
||||
mize.add_listener(
|
||||
"timetrack.sub_current",
|
||||
Box::new(timetrack_sub_current_listener),
|
||||
)?;
|
||||
|
||||
|
||||
}
|
||||
mize.register_part(Box::new(TimeTrack { mize: mize.clone() }))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_current_update_msg(conn_id: u64, data: TaskPart, mize: &mut Mize) -> MizeResult<()> {
|
||||
let now = fixed_len(data.current.name.clone(), 48);
|
||||
let next = fixed_len(
|
||||
data.upcoming
|
||||
.iter()
|
||||
.nth(0)
|
||||
.unwrap_or(&Action {
|
||||
name: "".to_string(),
|
||||
activity: None,
|
||||
task: None,
|
||||
start: None,
|
||||
end: None,
|
||||
duration: None,
|
||||
})
|
||||
.name
|
||||
.clone(),
|
||||
48,
|
||||
);
|
||||
let msg = MizeMessage::new_custom(
|
||||
"timetrack.current",
|
||||
format!(" {} NEXT: {} ", now, next),
|
||||
conn_id,
|
||||
);
|
||||
let con = mize.get_connection(conn_id)?;
|
||||
con.send(msg)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fixed_len(mut buf: String, len: usize) -> String {
|
||||
if buf.len() > len {
|
||||
buf.truncate(len - 4); // Cut off extra characters
|
||||
buf.push_str(" ...");
|
||||
} else {
|
||||
while buf.len() < len {
|
||||
buf.push(' '); // Fill with whitespace
|
||||
}
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn timetrack_sub_current_listener(msg: MizeMessage, mize: &mut Mize) -> MizeResult<()> {
|
||||
let spacetime = mize.get_part_native::<SpacetimePart>("spacetime").unwrap();
|
||||
let con = spacetime.con();
|
||||
let mut mize_clone = mize.clone();
|
||||
let mut mize_clone_two = mize.clone();
|
||||
con.subscription_builder()
|
||||
.on_applied(move |ctx| {
|
||||
let data = ctx.db.task_part().identity().find(&ctx.identity()).unwrap();
|
||||
if let Err(e) = send_current_update_msg(msg.conn_id, data, &mut mize_clone) {
|
||||
e.log();
|
||||
}
|
||||
})
|
||||
.add_query(|q| q.from.task_part())
|
||||
.subscribe();
|
||||
con.db
|
||||
.task_part()
|
||||
.on_update(move |_, _old: &TaskPart, new: &TaskPart| {
|
||||
if let Err(e) = send_current_update_msg(msg.conn_id, new.clone(), &mut mize_clone_two) {
|
||||
e.log();
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl MizePartCreate for TimeTrack {}
|
||||
|
||||
impl MizePart for TimeTrack {
|
||||
fn name(&self) -> &'static str {
|
||||
"timetrack"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
fn command_next_action(matches: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
||||
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
||||
let con = spacetime.con();
|
||||
|
||||
let break_flag = matches.get_flag("break");
|
||||
let action = matches.get_many::<String>("action")
|
||||
.map(|vals| vals.map(|s| s.as_str()).collect::<Vec<&str>>().join(" "));
|
||||
let action = match action {
|
||||
Some(action) => Some(Action {
|
||||
name: action.clone(),
|
||||
activity: None,
|
||||
task: None,
|
||||
start: None,
|
||||
end: None,
|
||||
duration: None,
|
||||
}),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let old_action = get_current_action(&mut mize, con)?;
|
||||
out!("before action: {}", old_action.name);
|
||||
let (wait_tx, waiter_rx) = std::sync::oneshot::channel::<()>();
|
||||
con.reducers.next_action_then(break_flag, action, |_, _| {
|
||||
wait_tx.send(()).unwrap();
|
||||
})?;
|
||||
waiter_rx.recv()?;
|
||||
|
||||
let new_action = get_current_action(&mut mize, con)?;
|
||||
out!("new action: {}", new_action.name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
fn command_current_action(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
||||
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
||||
let action = get_current_action(&mut mize, spacetime.con())?;
|
||||
out!("{}", action.name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_current_action(_mize: &mut Mize, con: &DbConnection) -> MizeResult<Action> {
|
||||
let (wait_tx, wait_rx) = std::sync::oneshot::channel::<Action>();
|
||||
con.subscription_builder()
|
||||
.on_applied(|ctx| {
|
||||
let task_part_data = ctx.db.task_part().identity().find(&ctx.identity());
|
||||
if task_part_data.is_none() {
|
||||
wait_tx
|
||||
.send(Action {
|
||||
name: "fillier".to_string(),
|
||||
activity: None,
|
||||
task: None,
|
||||
start: None,
|
||||
end: None,
|
||||
duration: None,
|
||||
})
|
||||
.unwrap();
|
||||
} else {
|
||||
wait_tx.send(task_part_data.unwrap().current).unwrap();
|
||||
}
|
||||
})
|
||||
.add_query(|q| {
|
||||
q.from.task_part()
|
||||
//.r#where(|row| row.identity.eq(con.identity()))
|
||||
})
|
||||
.subscribe();
|
||||
let action = wait_rx.recv().unwrap();
|
||||
Ok(action)
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
fn command_list_activities(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
||||
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
||||
let activities = get_activities(&mut mize, spacetime.con())?;
|
||||
|
||||
for ac in activities.clone() {
|
||||
print!("ac {}: {}", ac.id, ac.name);
|
||||
if !ac.parents_ids.is_empty() {
|
||||
print!(" -");
|
||||
}
|
||||
for child_id in ac.parents_ids {
|
||||
let child_ac = activities.iter().find(|ac| ac.id == child_id).unwrap();
|
||||
print!(" {}", child_ac.name);
|
||||
}
|
||||
print!("\n")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
fn command_list_actions(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
||||
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
||||
|
||||
let actions = get_actions(spacetime.con())?;
|
||||
|
||||
for action in actions {
|
||||
out!("{}", action.name)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
fn command_edit_action_list(matches: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
||||
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
||||
|
||||
// maybe append a template to the action list
|
||||
if matches.get_flag("defaultTemplate") {
|
||||
out!("Appending template...");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// get list of actions
|
||||
let mut actions = get_actions(spacetime.con())?;
|
||||
|
||||
// write to tmp file
|
||||
let tmp_file_content = actions_to_string(&actions)?;
|
||||
let tmp_file = std::env::temp_dir().join(format!(
|
||||
"ppc-edit-actions-{}.txt",
|
||||
chrono::Utc::now().timestamp()
|
||||
));
|
||||
std::fs::write(&tmp_file, tmp_file_content)
|
||||
.map_err(|e| mize_err!("Failed to write temp file: {}", e))?;
|
||||
|
||||
// open $EDITOR blocking
|
||||
crate::cli_spawn(format!("nvim {}", tmp_file.display()))?;
|
||||
|
||||
// read file and update db
|
||||
let new_file_content = std::fs::read_to_string(&tmp_file)
|
||||
.map_err(|e| mize_err!("Failed to read temp file: {}", e))?;
|
||||
|
||||
actions = actions_from_string(new_file_content)?;
|
||||
set_actions(actions, spacetime.con())?;
|
||||
|
||||
// delete tmpfile
|
||||
let _ = std::fs::remove_file(&tmp_file);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_actions(con: &DbConnection) -> MizeResult<Vec<Action>> {
|
||||
let (exit_tx, exit_rx) = std::sync::oneshot::channel::<Vec<Action>>();
|
||||
con.subscription_builder()
|
||||
.on_applied(|ctx| {
|
||||
let task_part_data = ctx.db.task_part().identity().find(&ctx.identity());
|
||||
if task_part_data.is_none() {
|
||||
exit_tx.send(Vec::new()).unwrap();
|
||||
} else {
|
||||
exit_tx.send(task_part_data.unwrap().upcoming).unwrap();
|
||||
}
|
||||
})
|
||||
.add_query(|q| {
|
||||
q.from.task_part()
|
||||
//.r#where(|row| row.identity.eq(con.identity()))
|
||||
})
|
||||
.subscribe();
|
||||
let activities = exit_rx.recv().unwrap();
|
||||
Ok(activities)
|
||||
}
|
||||
|
||||
fn set_actions(actions: Vec<Action>, con: &DbConnection) -> MizeResult<()> {
|
||||
let (wait_tx, waiter_rx) = std::sync::oneshot::channel::<()>();
|
||||
con.reducers
|
||||
.update_upcoming_then(actions, |_, _| wait_tx.send(()).unwrap())?;
|
||||
waiter_rx.recv().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn actions_to_string(actions: &Vec<Action>) -> MizeResult<String> {
|
||||
let mut string = String::new();
|
||||
|
||||
for action in actions.iter() {
|
||||
string.push_str(format!("{}\n", action.name).as_str());
|
||||
}
|
||||
|
||||
Ok(string)
|
||||
}
|
||||
|
||||
fn actions_from_string(string: String) -> MizeResult<Vec<Action>> {
|
||||
let mut new_actions = Vec::new();
|
||||
for line in string.lines() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut action = Action {
|
||||
name: line.to_owned(),
|
||||
activity: None,
|
||||
task: None,
|
||||
start: None,
|
||||
end: None,
|
||||
duration: None,
|
||||
};
|
||||
parse_action(&mut action);
|
||||
new_actions.push(action);
|
||||
}
|
||||
Ok(new_actions)
|
||||
}
|
||||
|
||||
fn parse_action(action: &mut Action) {
|
||||
let _parts: Vec<&str> = action.name.split_whitespace().collect();
|
||||
}
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
fn command_edit_activities(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
||||
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
||||
|
||||
// get list of activities
|
||||
let activities = get_activities(&mut mize, spacetime.con())?;
|
||||
|
||||
let con = spacetime.con();
|
||||
|
||||
// write to tmp file
|
||||
let mut tmp_file_content = String::new();
|
||||
for ac in activities.iter() {
|
||||
tmp_file_content.push_str(format!("{} {}", ac.id, ac.name).as_str());
|
||||
if !ac.parents_ids.is_empty() {
|
||||
tmp_file_content.push_str(" -");
|
||||
for parent_id in ac.parents_ids.iter() {
|
||||
let parent_ac = activities.iter().find(|ac| ac.id == *parent_id).unwrap();
|
||||
tmp_file_content.push_str(" ");
|
||||
tmp_file_content.push_str(&parent_ac.name);
|
||||
}
|
||||
}
|
||||
tmp_file_content.push_str("\n");
|
||||
}
|
||||
let tmp_file = std::env::temp_dir().join(format!(
|
||||
"ppc-edit-activities-{}.txt",
|
||||
chrono::Utc::now().timestamp()
|
||||
));
|
||||
std::fs::write(&tmp_file, tmp_file_content)
|
||||
.map_err(|e| mize_err!("Failed to write temp file: {}", e))?;
|
||||
|
||||
loop {
|
||||
// open $EDITOR blocking
|
||||
crate::cli_spawn(format!("nvim {}", tmp_file.display()))?;
|
||||
|
||||
let new_file_content = std::fs::read_to_string(&tmp_file)
|
||||
.map_err(|e| mize_err!("Failed to read temp file: {}", e))?;
|
||||
|
||||
let mut input = String::new();
|
||||
|
||||
let old_activities = get_activities(&mut mize, con)?;
|
||||
if let Err(err) =
|
||||
parse_activities_from_text(&mut mize, con, new_file_content, old_activities)
|
||||
{
|
||||
out!("error parsing the activities from the file: {:?}", err);
|
||||
out!("press ENTER to open the file again");
|
||||
|
||||
// wait for user input
|
||||
// so that they can se the err msg, before running nvim again
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// delete tmpfile
|
||||
let _ = std::fs::remove_file(&tmp_file);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_activities(_mize: &mut Mize, con: &DbConnection) -> MizeResult<Vec<Activity>> {
|
||||
let (exit_tx, exit_rx) = std::sync::oneshot::channel::<Vec<Activity>>();
|
||||
con.subscription_builder()
|
||||
.on_applied(|ctx| {
|
||||
exit_tx.send(ctx.db.activities().iter().collect()).unwrap();
|
||||
})
|
||||
.add_query(|q| q.from.activities())
|
||||
.subscribe();
|
||||
let activities = exit_rx.recv().unwrap();
|
||||
Ok(activities)
|
||||
}
|
||||
|
||||
fn parse_activities_from_text(
|
||||
mize: &mut Mize,
|
||||
con: &DbConnection,
|
||||
text: String,
|
||||
old_activities: Vec<Activity>,
|
||||
) -> MizeResult<()> {
|
||||
let mut activities_to_delete = old_activities.clone();
|
||||
let mut wait_revievers = Vec::new();
|
||||
for line in text.lines() {
|
||||
if line == "" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// split
|
||||
let mut parts = line.split(" ");
|
||||
let id: u64 = parts.next().ok_or(mize_err!(".."))?.parse()?;
|
||||
let name = parts.next().ok_or(mize_err!(".."))?.to_string();
|
||||
|
||||
// remove from to-delete list
|
||||
if let Some(pos) = activities_to_delete.iter().position(|ac| ac.id == id) {
|
||||
activities_to_delete.remove(pos);
|
||||
}
|
||||
|
||||
// wait rx
|
||||
let (wait_tx, wait_rx) = oneshot::channel::<()>();
|
||||
wait_revievers.push(wait_rx);
|
||||
|
||||
// update or create activity
|
||||
if let Some(old_activity) = old_activities.iter().find(|ac| ac.id == id) {
|
||||
con.reducers.update_activity_then(
|
||||
Activity {
|
||||
id,
|
||||
name,
|
||||
task: old_activity.task,
|
||||
parents_ids: old_activity.parents_ids.clone(),
|
||||
},
|
||||
move |_, _| {
|
||||
let _ = wait_tx.send(());
|
||||
},
|
||||
)?;
|
||||
} else {
|
||||
con.reducers.update_activity_then(
|
||||
Activity {
|
||||
id: 0,
|
||||
name,
|
||||
task: None,
|
||||
parents_ids: Vec::new(),
|
||||
},
|
||||
move |_, _| {
|
||||
let _ = wait_tx.send(());
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// remove deleted activities
|
||||
for activity in activities_to_delete {
|
||||
let (wait_tx, wait_rx) = oneshot::channel::<()>();
|
||||
wait_revievers.push(wait_rx);
|
||||
con.reducers.remove_activity_then(activity, move |_, _| {
|
||||
let _ = wait_tx.send(());
|
||||
})?;
|
||||
}
|
||||
|
||||
// wait for db stuff to finish
|
||||
for wait_rx in wait_revievers {
|
||||
wait_rx.recv().unwrap();
|
||||
}
|
||||
let mut wait_revievers = Vec::new();
|
||||
|
||||
// check for declared parents
|
||||
let new_activities = get_activities(mize, con)?;
|
||||
out!("new_activities: {:?}", new_activities);
|
||||
for line in text.lines() {
|
||||
if line == "" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// split
|
||||
let mut parts = line.split(" ");
|
||||
let _id: u64 = parts.next().ok_or(mize_err!(".."))?.parse()?;
|
||||
let name = parts.next().ok_or(mize_err!(".."))?.to_string();
|
||||
|
||||
// parse parents
|
||||
let seperator = parts.next();
|
||||
if seperator.is_some() && seperator.unwrap() == "-" {
|
||||
let mut parents_ids = Vec::new();
|
||||
for parent_name in parts {
|
||||
// find id from old_parts by this name
|
||||
let res_from_old = old_activities.iter().find(|ac| ac.name == parent_name);
|
||||
let res_from_new = new_activities.iter().find(|ac| ac.name == parent_name);
|
||||
let parent_activity = res_from_old.or(res_from_new).ok_or(mize_err!(
|
||||
"activity '{name}' declares parent '{parent_name}'"
|
||||
))?;
|
||||
parents_ids.push(parent_activity.id);
|
||||
}
|
||||
let child_activity = new_activities
|
||||
.iter()
|
||||
.find(|ac| ac.name == name)
|
||||
.ok_or(mize_err!(".."))?;
|
||||
let (wait_tx, wait_rx) = oneshot::channel::<()>();
|
||||
wait_revievers.push(wait_rx);
|
||||
con.reducers.update_activity_then(
|
||||
Activity {
|
||||
parents_ids,
|
||||
..child_activity.to_owned()
|
||||
},
|
||||
move |_, _| {
|
||||
let _ = wait_tx.send(());
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// wait for db stuff to finish
|
||||
for wait_rx in wait_revievers {
|
||||
wait_rx.recv().unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user