Merge pull request #2 from knz/knz-changes

Knz changes
This commit is contained in:
Sebastian Moser
2026-03-09 20:08:24 +01:00
committed by GitHub
8 changed files with 691 additions and 264 deletions

129
AGENTS.md
View File

@@ -1,6 +1,5 @@
# Mize Codebase Architecture Guide for AI Agents # Mize
## Overview
Mize is a strongly typed "filesystem" for the age of connectivity, elevating the Unix file philosophy into modern distributed computing. Mize is a strongly typed "filesystem" for the age of connectivity, elevating the Unix file philosophy into modern distributed computing.
## Codebase Structure ## Codebase Structure
@@ -17,109 +16,47 @@ packages/
## Key Components ## Key Components
### 1. Core Mize Framework (`packages/mize/`) To understand the main components of the architecture you canr efer to `architecture.md`.
- **Language**: Rust
- **Target platforms**: OS (native), WASM (web), bare metal, JVM
- **Key features**:
- Strongly typed filesystem abstraction
- Module system with dynamic loading
- Memory store implementation
- Protocol versioning and serialization
- Cross-platform support
**Architecture layers:** ## Require clarification and plan approval before making code changes
- `core/`: Platform-independent logic
- `config/`: Configuration management
- `error/`: Error handling
- `id/`: Unique identifier system
- `instance/`: Mize runtime instances
- `item/`: Filesystem item abstractions
- `memstore/`: In-memory storage
- `proto/`: Protocol definitions
- `types/`: Core type definitions
- `platform/`: Platform-specific implementations Before making any code changes other than the changelog, you must follow this two-step process:
- `os/`: Native OS integration
- `wasm/`: WebAssembly support
- `bare/`: Bare metal targets
- `jvm/`: Java Virtual Machine integration
### 2. Marts (`packages/marts/`) ### Step 1: Ask Clarifying Questions
- **Language**: Rust + TypeScript
- **Purpose**: Collection of Mize framework parts and utilities
- **Key features**:
- CLI tools
- JavaScript/TypeScript integration
- Habitica integration
- Deno-based scripting support
### 3. MME - Mize Module Environment (`packages/mme/`) - Always ask at least one clarifying question about the user's request
- **Language**: Rust - Understand the full scope and context of what they're asking for
- **Purpose**: Module execution environment - Clarify any ambiguous requirements or edge cases
- **Key features**: - Ask about preferred approaches if multiple solutions exist
- Cross-platform module loading - Confirm the expected behavior and user experience
- Qt and Slint UI support
- Web view integration (tao/wry)
- Command system (comandr)
### 4. PPC - Platform Components (`packages/ppc/`) ### Step 2: Present Implementation Plan
- **Language**: Rust + TypeScript
- **Purpose**: Platform-specific implementations
- **Targets**: Obsidian plugin, OS integration
### 5. Vic - Victor CLI (`packages/vic/`) - After receiving clarification, present a detailed implementation plan
- **Language**: Rust - Break down the work into specific, actionable steps
- **Purpose**: Command-line interface tool - Identify which files will be created, modified, or deleted
- **Features**: Build, run, test, and manage Mize applications - Explain the technical approach and any architectural decisions
- Highlight any potential risks, trade-offs, or dependencies
- Estimate the complexity and scope of changes
- **Wait for explicit user approval** before proceeding with any code changes
## Build System ### Approval Requirements
- **Rust**: Cargo workspace with feature flags for different targets - User must explicitly approve the plan with words like "yes", "approved", "proceed", "go ahead", or similar
- **TypeScript**: Deno-based build system - If the user suggests modifications to the plan, incorporate them and seek re-approval
- **Cross-compilation**: Support for WASM and multiple OS targets - Do not assume silence or ambiguous responses mean approval
## Key Technologies ### Exceptions
- **Rust crates**: tokio, serde, wasm-bindgen, tracing, clap - This process may be skipped only for trivial changes like fixing obvious typos or formatting
- **WASM**: wasm-pack, web-sys - When in doubt, always follow the full process rather than assuming an exception applies
- **UI**: tao, wry, slint, Qt
- **Scripting**: Deno, rustyscript
- **Serialization**: ciborium (CBOR), serde_json, toml
## Development Patterns ### Example Workflow
1. **Feature flags**: Extensive use of Cargo features for platform targeting 1. User: "Add a login form to the app"
2. **Cross-platform abstraction**: Core logic separated from platform implementations 2. Assistant: "I'd like to clarify a few things about the login form: [questions]"
3. **Module system**: Dynamic loading of functionality 3. User: [provides answers]
4. **Strong typing**: Rust's type system used for filesystem safety 4. Assistant: "Based on your requirements, here's my implementation plan: [detailed plan]. Does this approach look good to you?"
5. User: "Yes, that looks good"
## Next Steps Discussion 6. Assistant: [writes plan to file].
7. Assistant: [proceeds with implementation].
The codebase appears to be in active development with these key areas for future work:
1. **Platform completion**: Finish OS, WASM, and other platform implementations
2. **Module ecosystem**: Develop more built-in modules
3. **Performance optimization**: Memory management and serialization
4. **Testing**: Comprehensive test coverage across platforms
5. **Documentation**: User-facing documentation and examples
6. **Integration**: Better integration with existing tools and systems
## Working with the Codebase
**For AI Agents:**
- Focus on the `core/` directory for platform-independent logic
- Use feature flags appropriately when building/testing
- Understand the module system for extending functionality
- Pay attention to cross-platform considerations
**Build targets:**
- `target-os`: Native operating system builds
- `target-wasm`: WebAssembly builds
- Other platform-specific targets as needed
## Key Files for Understanding
- `packages/mize/Cargo.toml`: Core dependencies and features
- `packages/mize/src/lib.rs`: Main library entry point
- `packages/mize/src/core/`: Core platform-independent logic
- `packages/mize/src/platform/`: Platform-specific implementations

47
Cargo.lock generated
View File

@@ -2728,6 +2728,18 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.3.0"
@@ -3483,6 +3495,15 @@ dependencies = [
"foldhash", "foldhash",
] ]
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown 0.14.5",
]
[[package]] [[package]]
name = "headers" name = "headers"
version = "0.3.9" version = "0.3.9"
@@ -4260,6 +4281,17 @@ dependencies = [
"redox_syscall 0.7.0", "redox_syscall 0.7.0",
] ]
[[package]]
name = "libsqlite3-sys"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "libxdo" name = "libxdo"
version = "0.6.0" version = "0.6.0"
@@ -4430,6 +4462,7 @@ dependencies = [
"futures", "futures",
"mize", "mize",
"reqwest 0.13.2", "reqwest 0.13.2",
"rusqlite",
"rustyscript", "rustyscript",
"serde", "serde",
"serde_json", "serde_json",
@@ -6036,6 +6069,20 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rusqlite"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
dependencies = [
"bitflags 2.11.0",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"

48
architecture.md Normal file
View File

@@ -0,0 +1,48 @@
# Source code architecture
## 1. Core Mize Framework (`packages/mize/`)
**Architecture layers:**
- `core/`: Platform-independent logic
- `config/`: Configuration management
- `error/`: Error handling
- `id/`: Unique identifier system
- `instance/`: Mize runtime instances
- `item/`: Filesystem item abstractions
- `memstore/`: In-memory storage
- `proto/`: Protocol definitions
- `types/`: Core type definitions
- `platform/`: Platform-specific implementations
- `os/`: Native OS integration
- `wasm/`: WebAssembly support
- `bare/`: Bare metal targets
- `jvm/`: Java Virtual Machine integration
### 2. Marts (`packages/marts/`)
- **Language**: Rust + TypeScript
- **Purpose**: Collection of Mize framework parts and utilities
- **Key features**:
- CLI tools
- JavaScript/TypeScript integration
- Habitica integration
- Deno-based scripting support
## 3. MME - Mize Module Environment (`packages/mme/`)
- **Language**: Rust
- **Purpose**: Module execution environment
- **Key features**:
- Cross-platform module loading
- Qt and Slint UI support
- Web view integration (tao/wry)
- Command system (comandr)
## 4. PPC - Platform Components (`packages/ppc/`)
- **Language**: Rust + TypeScript
- **Purpose**: Platform-specific implementations
- **Targets**: Obsidian plugin, OS integration
## 5. Vic - Victor CLI (`packages/vic/`)
- **Language**: Rust
- **Purpose**: Command-line interface tool
- **Features**: Build, run, test, and manage Mize applications

View File

@@ -17,10 +17,11 @@ chrono = "0.4.44"
reqwest = { version = "0.13.2", features = ["blocking", "json"] } reqwest = { version = "0.13.2", features = ["blocking", "json"] }
serde = "1.0.228" serde = "1.0.228"
serde_json = "1.0.149" serde_json = "1.0.149"
rusqlite = { version = "0.31", features = ["bundled"], optional = true }
[features] [features]
default = ["target-os"] default = ["target-os"]
target-os = [ "clap", "mize/target-os", "deno_core", "rustyscript" ] target-os = [ "clap", "mize/target-os", "deno_core", "rustyscript", "rusqlite" ]
target-wasm = [ "mize/target-wasm" ] target-wasm = [ "mize/target-wasm" ]
clap = ["dep:clap"] clap = ["dep:clap"]

View File

@@ -1,125 +1,66 @@
use chrono::{Datelike, Timelike, Utc}; use std::sync::Mutex;
use chrono::{Timelike, Utc};
use clap::{Arg, Command}; use clap::{Arg, Command};
use rusqlite::{params, Connection, Result}; use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize}; use serde_json::{json, Value};
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command as StdCommand;
// Assuming PPC and other modules are structured like this. use mize::{mize_err, mize_part, Mize, MizeError, MizePart, MizeResult};
// This might need adjustment based on the actual definitions in other files.
use crate::habitica::Habitica; // Assuming this exists
use crate::cli::CliPart;
use crate::habitica::Habitica;
#[mize_part]
#[derive(Default)]
pub struct C2vi { pub struct C2vi {
pub db: Connection, mize: Mize,
db: Mutex<Option<Connection>>,
} }
impl C2vi { impl MizePart for C2vi {
pub fn new(local_storage_path: &str) -> Result<Self> { fn opts(&self, mize: &mut Mize) {
let db_path = PathBuf::from(local_storage_path).join("data.db"); mize.new_opt("c2vi.local_storage_path");
let conn = Connection::open(db_path)?;
Ok(C2vi { db: conn })
} }
} }
pub fn extend_cli_c2vi(mut cli: Command) -> Command { impl C2vi {
cli.subcommand( fn with_db<F, R>(&self, f: F) -> MizeResult<R>
Command::new("bed") where
.about("Command to run when c2vi goes to bed") F: FnOnce(&Connection) -> MizeResult<R>,
.action(async || { {
// Placeholder for actual PPC context let guard = self
// await save_habitica_dailies(ppc); .db
// await save_habitica_logs(ppc); .lock()
}), .map_err(|e| mize_err!("DB lock poisoned: {}", e))?;
) let conn = guard
.subcommand( .as_ref()
Command::new("clearTodos") .ok_or_else(|| mize_err!("DB not initialized"))?;
.about("Clear all todos in Habitica") f(conn)
.alias("clt") }
.action(async || {
// Placeholder
}),
)
.subcommand(
Command::new("listTodos")
.about("List all todos in Habitica")
.alias("dut")
.action(async || {
// Placeholder
}),
)
.subcommand(
Command::new("buyHealthPotion")
.about("Buy a health potion in Habitica")
.alias("buyh")
.action(async || {
// Placeholder
}),
)
.subcommand(
Command::new("addActionItems")
.about("Add action items from a temp file")
.alias("aal")
.action(async || {
// add_action_items();
}),
)
.subcommand(
Command::new("skip")
.about("Skip a Habitica todo")
.alias("sk")
.arg(Arg::new("num").default_value("1"))
.action(async || {
// let num = matches.value_of("num").unwrap().parse::<i32>().unwrap();
// skip_habitica_todo(ppc, num);
}),
)
.subcommand(
Command::new("dumpLog")
.about("Dump the complete Habitica log")
.action(async || {
// dump_habitica_logs(ppc);
}),
)
.subcommand(
Command::new("listDailies")
.about("List all dailies from the local db")
.action(async || {
// Placeholder
}),
)
.subcommand(
Command::new("printTask")
.about("Print a specific todo from Habitica")
.arg(Arg::new("id").required(true))
.action(async || {
// let id = matches.value_of("id").unwrap();
// Placeholder
}),
)
} }
fn format_date(date: chrono::DateTime<Utc>) -> String { pub fn c2vi(mize: &mut Mize) -> MizeResult<()> {
date.format("%Y-%m-%d").to_string() let storage_path = mize.get_config("c2vi.local_storage_path")?.to_string();
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(())
} }
#[derive(Serialize, Deserialize)] fn setup_tables(conn: &Connection) -> MizeResult<()> {
struct HabiticaDaily {
id: String,
text: String,
notes: String,
priority: Option<String>,
frequency: String,
}
#[derive(Serialize, Deserialize)]
struct HabiticaLog {
date: String,
data: String, // JSON string
}
fn setup_tables(conn: &Connection) -> Result<()> {
conn.execute( conn.execute(
"CREATE TABLE IF NOT EXISTS habitica_dailies ( "CREATE TABLE IF NOT EXISTS habitica_dailies (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@@ -129,97 +70,525 @@ fn setup_tables(conn: &Connection) -> Result<()> {
frequency TEXT frequency TEXT
)", )",
[], [],
)?; )
.map_err(|e| mize_err!("DB error creating habitica_dailies: {}", e))?;
conn.execute( conn.execute(
"CREATE TABLE IF NOT EXISTS habitica_log ( "CREATE TABLE IF NOT EXISTS habitica_log (
date TEXT PRIMARY KEY, date TEXT PRIMARY KEY,
data TEXT NOT NULL data TEXT NOT NULL
)", )",
[], [],
)?; )
.map_err(|e| mize_err!("DB error creating habitica_log: {}", e))?;
Ok(()) Ok(())
} }
// In-progress translation. Many functions will need the PPC context passed in. fn register_subcommands(cli: &mut CliPart) {
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(
async fn save_habitica_dailies(ppc: &PPC) -> Result<()> { Command::new("clearTodos")
println!("######### cli bed cmd #########"); .about("Clear all Habitica todos")
setup_tables(&ppc.c2vi.db)?; .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(())
},
);
// Assuming ppc.habitica.get_tasks returns a Result cli.subcommand(
let dailies = ppc.habitica.get_tasks("dailys").await?; Command::new("listTodos")
write_dailies_to_database(&dailies, &ppc.c2vi.db).await?; .about("List all Habitica todos")
Ok(()) .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(())
},
);
} }
async fn write_dailies_to_database(dailies: &[HabiticaTask], db: &Connection) -> Result<()> { // ============= Business Logic =============
println!("Syncing {} dailies to database...", dailies.len());
let tx = db.transaction()?; fn format_date(dt: chrono::DateTime<Utc>) -> String {
for daily in dailies { dt.format("%Y-%m-%d").to_string()
tx.execute( }
"INSERT INTO habitica_dailies (id, text, notes, priority, frequency)
VALUES (?1, ?2, ?3, ?4, ?5) /// Get the target date, adjusted for "save for yesterday" logic (before noon = yesterday)
ON CONFLICT(id) DO UPDATE SET fn get_target_date() -> (String, bool) {
text=excluded.text, let now = Utc::now();
notes=excluded.notes, let save_for_yesterday = now.hour() < 12;
priority=excluded.priority, let date = if save_for_yesterday {
frequency=excluded.frequency", now - chrono::Duration::days(1)
params![ } else {
daily.id, now
daily.text, };
daily.notes.as_deref().unwrap_or(""), if save_for_yesterday {
daily.priority.map(|p| p.to_string()), println!(
daily.frequency.as_deref().unwrap_or("daily"), "Saving logs for yesterday because it is {} hours of the day",
], now.hour()
)?; );
} }
tx.commit() (format_date(date), save_for_yesterday)
} }
async fn add_action_items(ppc: &PPC) -> Result<()> { /// Load existing log entry from DB, or return a default empty one
let initial_content = " 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 gu 10min TIMER
uw 15min FROG uw 15min FROG
b coffee/tee/kakau b coffee/tee/kakau
uw FROG uw FROG
b breakfast, brush teeth, lüften b breakfast, brush teeth, lüften
uw FROG uw FROG
lunch lunch
uw 15min TIMER of next days FROG uw 15min TIMER of next days FROG
fw fw
dt 30min TIMER dt 30min TIMER
fun 20:00 (smth that can be stopped easily) fun 20:00 (smth that can be stopped easily)
se 21:40 se 21:40
read read
bed 22:10 bed 22:10";
";
let tmp_file = env::temp_dir().join(format!("habitica-tasks-{}.md", Utc::now().timestamp()));
fs::write(&tmp_file, initial_content.trim())?;
StdCommand::new("nvim").arg(&tmp_file).status()?; 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))?;
let list_str = fs::read_to_string(&tmp_file)?; // Open in nvim for editing
fs::remove_file(&tmp_file)?; std::process::Command::new("nvim")
.arg(&tmp_file)
.status()
.map_err(|e| mize_err!("Failed to run nvim: {}", e))?;
let tasks: Vec<&str> = list_str.lines().map(|t| t.trim()).filter(|t| !t.is_empty()).collect(); // Read back and parse
let mut task_ids = vec![]; 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);
for todo_text in tasks { let tasks: Vec<&str> = list_str
// Assuming api_request exists and works like this .lines()
let data = ppc.habitica.api_request("POST", "tasks/user", Some(serde_json::json!({ "text": todo_text, "type": "todo" }))).await?; .map(|l| l.trim())
let task_id = data["id"].as_str().unwrap().to_string(); .filter(|l| !l.is_empty())
println!("Added to-do: '{}' with id '{}'", todo_text, task_id); .collect();
task_ids.push(task_id);
}
for task_id in task_ids { // Add tasks to Habitica
ppc.habitica.api_request("POST", &format!("tasks/{}/move/to/-1", task_id), None).await?; 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(()) 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(())
})
}

View File

@@ -20,10 +20,17 @@ pub fn habitica(mize: &mut Mize) -> MizeResult<()> {
})) }))
} }
impl MizePart 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 { impl Habitica {
fn api_request(&mut self, method: Method, path: String, data: Value) -> MizeResult<Value> { 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 api_url = self.mize.get_config("habitica.api_url")?.to_string();
let user_id = self.mize.get_config("habitica.user_id")?.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 api_token = self.mize.get_config("habitica.api_token")?.to_string();
@@ -65,6 +72,18 @@ impl Habitica {
let json_response: Value = response.json()?; let json_response: Value = response.json()?;
Ok(json_response.get("data").cloned().unwrap_or(Value::Null)) 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) { fn handle_rate_limit(response: &Response) {

View File

@@ -10,5 +10,10 @@ pub mod habitica;
#[cfg(feature = "target-os")] #[cfg(feature = "target-os")]
pub use habitica::*; pub use habitica::*;
#[cfg(feature = "target-os")]
pub mod c2vi;
#[cfg(feature = "target-os")]
pub use c2vi::*;
pub mod js; pub mod js;
pub use js::*; pub use js::*;

View File

@@ -18,6 +18,7 @@ fn os_main(mize: &mut Mize) -> MizeResult<()> {
marts::cli(mize)?; marts::cli(mize)?;
//marts::js(mize)?; //marts::js(mize)?;
marts::habitica(mize)?; marts::habitica(mize)?;
marts::c2vi(mize)?;
let mut cli = mize.get_part_native::<marts::CliPart>("cli")?; let mut cli = mize.get_part_native::<marts::CliPart>("cli")?;