updated README and a bunch of other stuff

This commit is contained in:
Sebastian Moser
2026-11-04 20:24:14 +01:00
parent 6eedb1826a
commit 9707286375
46 changed files with 3725 additions and 172 deletions

View File

@@ -8,74 +8,17 @@ Mize is many things.
- a library for COP = Configuration Oriented Programming - a library for COP = Configuration Oriented Programming
- a thing providing one single API through which any data can be interacted with - a thing providing one single API through which any data can be interacted with
All code written for, with or as part as mize (except for the small core) is written as a Mize Part. ALWAYS referr to:
@./docs/general-agent-guide.md
@./USING-MIZE-FOR-AGENTS.md
unless instructed otherwise put new parts into `./parts/vibed.<id-of-part>`.
## Codebase Structure ## Codebase Structure
``` ```
packages/ core/ # Core Mize framework (Rust)
├── src/ # Core Mize framework (Rust) parts/ # Mize parts (cdylibs / crates)
├── marts/ # Mize parts collection (Rust/TypeScript) macros/ # Proc macros
└── macros/ # Mize macros docs/ # documentation
``` ```
## Important
- usage of any git commands is strictly forbidden!
- in case it takes too long finding smth or figuring smth out. stop and ask the user. (goal is not to do stuff as autonomous as possible)
## Key Components
All code written for, with or as part as mize (except for the small core) is written as a Mize Part. see `docs/part-system.md` for how that works.
To understand the main components of the architecture you can refer to `docs/architecture.md`.
Any proc macros should go into `./macros`.
the core mize library is in `./core`
any mize parts are in `./parts` or in their own repositories.
## Require clarification and plan approval before making architectural code changes
Before making any code changes which could be considered to influence the architekture of the application, you must follow this two-step process:
### Step 1: Ask Clarifying Questions
- Always ask at least one clarifying question about the user's request
- Understand the full scope and context of what they're asking for
- Clarify any ambiguous requirements or edge cases
- Ask about preferred approaches if multiple solutions exist
- Confirm the expected behavior and user experience
- Ask the clarifying questions by numbering them, so the user can number their replies.
### Step 2: Present Implementation Plan
- After receiving clarification, present a detailed implementation plan
- Break down the work into specific, actionable steps
- Identify which files will be created, modified, or deleted
- 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
### What are considered architectural changes
TODO: add better examples.
eg: adding a seccond line displaying a different value from the DB is not considered architectural. as it was achievable by only modifying the existing function and rsx! code.
### Approval Requirements
- User must explicitly approve the plan with words like "yes", "approved", "proceed", "go ahead", or similar
- If the user suggests modifications to the plan, incorporate them and seek re-approval
- Do not assume silence or ambiguous responses mean approval
### Exceptions
- This process may be skipped only for trivial changes like fixing obvious typos or formatting
- When in doubt, always follow the full process rather than assuming an exception applies
### Example Workflow
1. User: "Add a login form to the app"
2. Assistant: "I'd like to clarify a few things about the login form: [questions]"
3. User: [provides answers]
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"
6. Assistant: [writes plan to file].
7. Assistant: [proceeds with implementation].

241
Cargo.lock generated
View File

@@ -141,6 +141,12 @@ dependencies = [
"stable_deref_trait", "stable_deref_trait",
] ]
[[package]]
name = "askama_escape"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3df27b8d5ddb458c5fb1bbc1ce172d4a38c614a97d550b0ac89003897fb01de4"
[[package]] [[package]]
name = "async-stream" name = "async-stream"
version = "0.3.6" version = "0.3.6"
@@ -304,6 +310,8 @@ dependencies = [
"http 1.5.0", "http 1.5.0",
"http-body 1.1.0", "http-body 1.1.0",
"http-body-util", "http-body-util",
"hyper 1.12.0",
"hyper-util",
"itoa", "itoa",
"matchit", "matchit",
"memchr", "memchr",
@@ -316,9 +324,11 @@ dependencies = [
"serde_path_to_error", "serde_path_to_error",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 1.0.2", "sync_wrapper 1.0.2",
"tokio",
"tower", "tower",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]
@@ -337,6 +347,28 @@ dependencies = [
"sync_wrapper 1.0.2", "sync_wrapper 1.0.2",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
]
[[package]]
name = "axum-oidc"
version = "1.0.0-dev-1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e035204cbe5a2275fbb47dd0c36e5111963da82a62ab7da05a515f7688951b3"
dependencies = [
"axum",
"axum-core",
"futures-util",
"http 1.5.0",
"openidconnect",
"reqwest 0.12.28",
"serde",
"thiserror 2.0.20",
"tower-layer",
"tower-service",
"tower-sessions 0.14.0",
"tracing",
"urlencoding",
] ]
[[package]] [[package]]
@@ -1304,6 +1336,7 @@ dependencies = [
"dioxus-html", "dioxus-html",
"dioxus-logger", "dioxus-logger",
"dioxus-signals", "dioxus-signals",
"dioxus-ssr",
"dioxus-stores", "dioxus-stores",
"dioxus-web", "dioxus-web",
"manganis", "manganis",
@@ -1787,6 +1820,18 @@ dependencies = [
"warnings", "warnings",
] ]
[[package]]
name = "dioxus-ssr"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4544ecf908962909de403bfdf94f771b036e9e1857e371cd3279841a15a892e6"
dependencies = [
"askama_escape",
"dioxus-core",
"dioxus-core-types",
"rustc-hash 2.1.3",
]
[[package]] [[package]]
name = "dioxus-stores" name = "dioxus-stores"
version = "0.7.10" version = "0.7.10"
@@ -3037,6 +3082,7 @@ dependencies = [
"http 1.5.0", "http 1.5.0",
"http-body 1.1.0", "http-body 1.1.0",
"httparse", "httparse",
"httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"smallvec", "smallvec",
@@ -3714,6 +3760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [ dependencies = [
"scopeguard", "scopeguard",
"serde",
] ]
[[package]] [[package]]
@@ -4043,7 +4090,7 @@ dependencies = [
] ]
[[package]] [[package]]
name = "mize-part-dev" name = "mize-part-mize-dev"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"cmd_lib", "cmd_lib",
@@ -4066,6 +4113,66 @@ dependencies = [
"mize", "mize",
] ]
[[package]]
name = "mize-part-mos-bash-config-cache"
version = "0.1.0"
dependencies = [
"mize",
]
[[package]]
name = "mize-part-ppc"
version = "0.1.0"
dependencies = [
"mize",
]
[[package]]
name = "mize-part-ppc-server"
version = "0.1.0"
dependencies = [
"axum",
"axum-oidc",
"dioxus",
"mize",
"openidconnect",
"serde_json",
"tokio",
"tower",
"tower-http 0.7.1",
"tower-sessions 0.15.0",
"tracing",
]
[[package]]
name = "mize-part-sci"
version = "0.1.0"
dependencies = [
"chrono",
"mize",
]
[[package]]
name = "mize-part-sci-inbox"
version = "0.1.0"
dependencies = [
"mize",
]
[[package]]
name = "mize-part-sci-timetrack"
version = "0.1.0"
dependencies = [
"mize",
]
[[package]]
name = "mize-part-spacetime"
version = "0.1.0"
dependencies = [
"mize",
]
[[package]] [[package]]
name = "mize_macros" name = "mize_macros"
version = "0.1.0" version = "0.1.0"
@@ -5629,7 +5736,7 @@ dependencies = [
"tokio-rustls 0.26.6", "tokio-rustls 0.26.6",
"tokio-util", "tokio-util",
"tower", "tower",
"tower-http", "tower-http 0.6.11",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
@@ -6911,6 +7018,23 @@ dependencies = [
"tokio", "tokio",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
]
[[package]]
name = "tower-cookies"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36"
dependencies = [
"axum-core",
"cookie",
"futures-util",
"http 1.5.0",
"parking_lot",
"pin-project-lite",
"tower-layer",
"tower-service",
] ]
[[package]] [[package]]
@@ -6931,6 +7055,23 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "tower-http"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c"
dependencies = [
"bitflags 2.13.1",
"bytes",
"http 1.5.0",
"http-body 1.1.0",
"percent-encoding",
"pin-project-lite",
"tower-layer",
"tower-service",
"tracing",
]
[[package]] [[package]]
name = "tower-layer" name = "tower-layer"
version = "0.3.3" version = "0.3.3"
@@ -6943,12 +7084,102 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tower-sessions"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3"
dependencies = [
"async-trait",
"http 1.5.0",
"time",
"tokio",
"tower-cookies",
"tower-layer",
"tower-service",
"tower-sessions-core 0.14.0",
"tracing",
]
[[package]]
name = "tower-sessions"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518dca34b74a17cadfcee06e616a09d2bd0c3984eff1769e1e76d58df978fc78"
dependencies = [
"async-trait",
"http 1.5.0",
"time",
"tokio",
"tower-cookies",
"tower-layer",
"tower-service",
"tower-sessions-core 0.15.0",
"tower-sessions-memory-store",
"tracing",
]
[[package]]
name = "tower-sessions-core"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b"
dependencies = [
"async-trait",
"axum-core",
"base64 0.22.1",
"futures",
"http 1.5.0",
"parking_lot",
"rand 0.8.8",
"serde",
"serde_json",
"thiserror 2.0.20",
"time",
"tokio",
"tracing",
]
[[package]]
name = "tower-sessions-core"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "568531ec3dfcf3ffe493de1958ae5662a0284ac5d767476ecdb6a34ff8c6b06c"
dependencies = [
"async-trait",
"axum-core",
"base64 0.22.1",
"futures",
"http 1.5.0",
"parking_lot",
"rand 0.9.5",
"serde",
"serde_json",
"thiserror 2.0.20",
"time",
"tokio",
"tracing",
]
[[package]]
name = "tower-sessions-memory-store"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713fabf882b6560a831e2bbed6204048b35bdd60e50bbb722902c74f8df33460"
dependencies = [
"async-trait",
"time",
"tokio",
"tower-sessions-core 0.15.0",
]
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.44" version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [ dependencies = [
"log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
"tracing-core", "tracing-core",
@@ -7127,6 +7358,12 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"

View File

@@ -1,25 +1,32 @@
# MiZe # MiZe
With Mize I am building the software platform, that I would want to use.
_Elevate the Unix file philosophy into the age of connectivity._ website: https://mize.works
source: https://git.ppc.social/ppc/mize
matrix room: [#mize:ppc.social](https://matrix.to/#/#mize:ppc.social)
matrix room for updates: [#mize-updates:ppc.social](https://matrix.to/#/#mize-updates:ppc.social)
## Why am I doing this? Mize is many things:
- What annoys me about software these days is, that things are done in so many different ways. - a "strongly typed filesystem for the age of everyone having many devices"
- e.g: Different cloud applications for syncing data (Google Cloud, OneDrive, Dropbox) - "eelevate the UNIX philosophy into the age of having multiple connected devices"
- e.g: Every app stores the data it deals with in it's own way (the way that the developer thought is best for this particular app) - a new stdlib api to replace libc (but not just for C)
- a framework to make malleable/extensible/configurable/debugable software fast
- a OS userspace (mainly linux but later also any other kernels)
- a library for COP = Configuration Oriented Programming
- a thing providing one single API through which any data from any soruce can be interacted with
- a way to produce software with agents which is not slop
- (by making humans design architecture, agents implement code)
- (and providing a solid starter set of architecture for agents to use without humans)
- ([Mize Software Factory](https://wiki.ppc.social/Mize-Software-Factory))
- [c2vi](https://c2vi.dev) tinkering on the software he thinks is solid and would want to use
- Back when Unix came out **everything** was stored in one place: the file system of the computer. ## things
- (just as simple bytestreams/files, organized by a folder-structure) - [MiZe Parts](https://wiki.ppc.social/u/c2vi/MiZe-Part)
- and NO program stored and synced any data around in unpredictable ways. - [MiZe Init System](https://wiki.ppc.social/u/c2vi/MiZe-Init-System)
- [Config Oriented Programming|COP = Config Oriented Programming](https://wiki.ppc.social/u/c2vi/Config-Oriented-Programming)
- This philosophy is still used in every Unix/Linux machine, on a local level, but completely abstracted away (especially on mobile phones) from the user (by every app). - [mize lang](https://wiki.ppc.social/u/c2vi/mize-lang)
- [Mize Software Factory](https://wiki.ppc.social/Mize-Software-Factory)
- I would say: The Unix file system just hasn't evolved into the age of connectivity. It is still completely the same. And I think it shouldn't be.
- MiZe aims to bring that idea of storing all data
- in one place
- in one predictable and simple way
- and programs only taking data from there and not storing it everywhere in messy ways
- That's why the slogan is going to be: _Elevate the unix file philosophy into the age of connectivity._
- [The Idea of MiZe](https://wiki.ppc.social/u/c2vi/The-Idea-of-MiZe)
- [How the idea of MiZe came to be](https://wiki.ppc.social/u/c2vi/How-the-idea-of-MiZe-came-to-be)
- [The WHY of Mize](https://wiki.ppc.social/u/c2vi/The-WHY-of-Mize)
- [How MiZe works](https://wiki.ppc.social/u/c2vi/How-MiZe-works) (quite old article, probably no longer fully accurate)

72
USING-MIZE-FOR-AGENTS.md Normal file
View File

@@ -0,0 +1,72 @@
# The Mize software factory framework.
goal: make clean/sane, configurable, extensible, **maintanable**, **human-comprehendable**, debuggable, tracable, pluggable, **well-architectured**, ... software usuing agentic engineering.
@./docs/general-agent-guide.md
## Overview
Mize is built around the **parts architecture** which allows you to compose anc configure functionality modularly. Each part is a self-contained unit that:
- can store and manage its own state
- can expose methods to other parts
- Registers itself with the Mize instance
- Defines configuration options
- define UI components (called presenters)
Parts are the primary way to build any new software using this framework.
## conditions when to stop and check back in with the human (IMPORTANT).
- dealing with/adding dependencies on system libraries or packages is also something to stop for and ask the user how to do it.
- in case it takes too long finding smth or figuring smth out. stop and ask the user. (goal is not to do stuff as autonomous as possible)
- **Stop on architectural dead-ends.** If something does not work out architecture-wise — e.g. one part needs to notify another of a state change, but there is no elegant existing mechanism (`method` / `opt` / `has_part` / listeners / etc.) — do **not** invent a hacky workaround and push on. Stop, describe the gap and the options you see, and ask the user how to proceed.
## some other notes
- Prefer expanding an existing part over creating a new one when it fits — but ask user before expanding.
- when developing a new part. present it's config options and methods to the human first.
When turning a design / whole application into parts, follow **Splitting an app into parts**.
## Splitting an app into parts
A part is a **named capability with its own config namespace and callable surface** — not a technical layer (UI/API/DB) and not a folder dump of screens.
When turning a design document or whole application into parts:
1. **One capability, one part** — Group requirements that share one job and vocabulary (login/tokens, inbox, hub shell). That cluster becomes one part id (`auth.client`, `mme`, `sci.inbox`).
2. **Config namespace = part boundary** — Settings that naturally sit under one prefix (`auth.client.issuer`, …) belong to that part. Two unrelated prefixes → two parts.
3. **Methods are the public API** — Expose capability-shaped methods (`login`, `set_login_state`), not “whatever the dashboard clicked.” CLI and other parts should call the same `method`s via `dyn_call` / generated bindings — don’t use CLI `run` as the only inter-part API.
4. **Product / UI shells are thin** — Dashboards own layout and presentation. Domain work stays in capability parts. Optional UI notify via `has_part("…")` + `dyn_call` so headless/CLI still works.
5. **Split on replaceability or reuse** — Would you swap the IdP but keep the hub? Second app needs login without the hub? Keep that capability separate. Only this screen will ever use X? Keep X in the shell until a second consumer appears.
6. **Don’t split only by layer** — Avoid default `foo.ui` + `foo.logic`. Prefer `foo` (capability) + optional shell. Shared widgets can be a **library** (e.g. `dioxus-components`) until they need mize opts/methods.
7. **Hard vs soft dependency** — Hard: cannot run without the other (load order / deps). Soft: “if UI is loaded, push URL/code.” Soft deps must not break headless use.
8. **Lifecycle is a boundary smell** — Background refresh, blocking window, long device poll with different lifetimes often means the domain side should be its own part with clear methods.
9. **Start coarse; split on friction** — Prefer fewer parts from a design doc. Split when a second consumer appears, config namespaces diverge, or you need to enable one capability without another.
10. **Before coding, present a part table** — For each proposed part: `id`, one-sentence responsibility, `opt`s, `method`s, hard deps, soft notifies. Present that to the human first (same rule as presenting config options and methods).
## UI work
For any UI work:
- Use **Dioxus** (desktop unless the user says otherwise).
- Build with **`parts/mize.ui/dioxus-components`** — reuse existing components (`Button`, `Prompt`, `Control`, …) and extend that library when something reusable is missing; don’t invent a parallel widget set.
- Prefer **inlined CSS** via `OnceStyle` / isolated style components (see that crate). Do **not** put `document::Style` inside components that re-render often; don’t bring back `asset!` / css-modules for these components unless the user asks.
- Don’t block the Dioxus UI thread on long part work (login flows, etc.): background thread / async + part `method`s updating shared state the UI reads.
unless specified diferently any mize UI uses the PPC color pallete (derived from the PPC Logo: https://wiki.ppc.social/PPC-Logo):
```
Token Light mode Dark mode
Background #F7F8F4 #171D18
Surface #FFFFFF #242D25
Surface secondary #EFF2EA #2C362D
Primary text #20251C #F2F4EA
Secondary text #66705E #AAB5A4
Border #DFE4D9 #3A463A
Primary accent #FEC251 #D7E47D
Accent text #79500F #D7E47D
Success #526F32 #B9D987
Info #376777 #B9E4EB
Warning #A66B13 #FFD77F
Danger #B94D42 #FF9B8E
```

View File

@@ -26,21 +26,16 @@ pub fn get(sub_matches: &ArgMatches, instance: &mut Mize) -> MizeResult<()> {
.get_one::<String>("id") .get_one::<String>("id")
.ok_or(MizeError::new().msg("No id Argument specified"))?; .ok_or(MizeError::new().msg("No id Argument specified"))?;
let item = instance.get(id)?; let raw = sub_matches.get_flag("raw");
let data = instance.get_config(id)?;
let recurse_count = sub_matches.get_count("recurse"); if raw {
print!("{}", data.value_string()?);
println!("{}", item.as_data_full()?);
return Ok(());
if recurse_count > 0 {
println!("{}", item.id());
println!("{}", item.as_data_full()?);
} else { } else {
println!("{}", item); println!("{}", data);
}; }
return Ok(()); Ok(())
} }
pub fn call(sub_matches: &ArgMatches, instance: &mut Mize) -> MizeResult<()> { pub fn call(sub_matches: &ArgMatches, instance: &mut Mize) -> MizeResult<()> {

View File

@@ -147,7 +147,13 @@ fn cli_matches() -> clap::ArgMatches {
Command::new("get") Command::new("get")
.aliases(["g"]) .aliases(["g"])
.arg(Arg::new("id").help("The id or path to get")) .arg(Arg::new("id").help("The id or path to get"))
.arg(Arg::new("recurse").short('r').action(ArgAction::Count)), .arg(Arg::new("recurse").short('r').action(ArgAction::Count))
.arg(
Arg::new("raw")
.long("raw")
.action(ArgAction::SetTrue)
.help("print the config opt value as a raw string"),
),
) )
.subcommand( .subcommand(
Command::new("set") Command::new("set")

View File

@@ -1,3 +1,255 @@
//! C ABI for dynamically loaded parts (see `include/mize.h`).
//!
//! Names passed to `opt` / `method` / `thread` / `get_config` / `set_config`
//! are absolute dotted paths (e.g. `"vicinae.open"`).
//!
//! Errors use a thread-local slot (like errno): C code calls `set_err(...)`;
//! Rust checks via [`take_err`] after the call.
use std::cell::RefCell;
use std::ffi::{c_char, c_void, CStr};
use std::ptr;
//pub extern "C" fn use crate::config::{ConfigOpt, MizeFun};
use crate::error::MizeError;
use crate::get_global_mize;
use crate::item::ItemData;
use crate::mize_err;
use crate::write_to_mize_stdout;
type CFun = unsafe extern "C" fn(*mut ItemData) -> *mut ItemData;
thread_local! {
/// Per-thread error set by C via `set_err`, consumed by Rust after the call.
static CUR_ERR: RefCell<Option<MizeError>> = const { RefCell::new(None) };
}
/// Clear the thread-local C error (call before invoking a C part entry / callback).
pub fn clear_err() {
CUR_ERR.with(|slot| {
*slot.borrow_mut() = None;
});
}
/// Take the thread-local C error, if any.
pub fn take_err() -> Option<MizeError> {
CUR_ERR.with(|slot| slot.borrow_mut().take())
}
/// Opaque pointer stored via `data_from_native` / read via `data_as_native`.
struct NativePtr(*mut c_void);
// SAFETY: the C++ part is responsible for only using the pointer on the right thread.
unsafe impl Send for NativePtr {}
unsafe impl Sync for NativePtr {}
fn cstr<'a>(p: *const c_char) -> Option<&'a str> {
if p.is_null() {
return None;
}
unsafe { CStr::from_ptr(p) }.to_str().ok()
}
fn wrap_c_fun(c_fun: CFun) -> MizeFun {
Box::new(move |args| {
clear_err();
let mut args_box = Box::new(args);
let ret_ptr = unsafe { c_fun(args_box.as_mut()) };
drop(args_box);
if let Some(err) = take_err() {
if !ret_ptr.is_null() {
unsafe { drop(Box::from_raw(ret_ptr)) };
}
return Err(err);
}
if ret_ptr.is_null() {
return Ok(ItemData::new());
}
Ok(*unsafe { Box::from_raw(ret_ptr) })
})
}
fn register_fun(name: &str, kind: &str, signature: &str, fun: MizeFun) {
let mut mize = match get_global_mize() {
Ok(m) => m,
Err(e) => {
let _ = write_to_mize_stdout(format_args!("[c_api] get_global_mize failed: {e:?}\n"));
return;
}
};
let id = match mize.id_new_dots(name) {
Ok(id) => id,
Err(e) => {
let _ = write_to_mize_stdout(format_args!("[c_api] bad id '{name}': {e:?}\n"));
return;
}
};
if mize.is_generate_mode().unwrap_or(false) {
let short = name.rsplit('.').next().unwrap_or(name);
let _ = mize.record_method(short, id.clone(), kind, signature);
return;
}
let opt = ConfigOpt {
name: id.dot(),
val: None,
fun: None,
deps: Vec::new(),
};
mize.config_opts.lock().unwrap().insert(id.clone(), opt);
mize.fun_values.lock().unwrap().insert(id, fun);
}
#[unsafe(no_mangle)]
pub extern "C" fn opt(name: *const c_char) {
let Some(name) = cstr(name) else { return };
let mut mize = match get_global_mize() {
Ok(m) => m,
Err(_) => return,
};
let Ok(id) = mize.id_new_dots(name) else { return };
if mize.is_generate_mode().unwrap_or(false) {
let short = name.rsplit('.').next().unwrap_or(name);
let _ = mize.record_opt(short, id.clone(), None);
return;
}
let opt = ConfigOpt {
name: id.dot(),
val: None,
fun: None,
deps: Vec::new(),
};
mize.config_opts.lock().unwrap().insert(id, opt);
}
#[unsafe(no_mangle)]
pub extern "C" fn method(name: *const c_char, signature: *const c_char, c_fun: CFun) {
let Some(name) = cstr(name) else { return };
let sig = cstr(signature).unwrap_or("");
register_fun(name, "method", sig, wrap_c_fun(c_fun));
}
#[unsafe(no_mangle)]
pub extern "C" fn effect(name: *const c_char, c_fun: CFun) {
let Some(name) = cstr(name) else { return };
register_fun(name, "effect", "", wrap_c_fun(c_fun));
}
#[unsafe(no_mangle)]
pub extern "C" fn apply(name: *const c_char, c_fun: CFun) {
let Some(name) = cstr(name) else { return };
register_fun(name, "apply", "", wrap_c_fun(c_fun));
}
#[unsafe(no_mangle)]
pub extern "C" fn thread(name: *const c_char, c_fun: CFun) {
let Some(name) = cstr(name) else { return };
register_fun(name, "thread", "", wrap_c_fun(c_fun));
}
#[unsafe(no_mangle)]
pub extern "C" fn set_config(name: *const c_char, value: *mut ItemData) {
if value.is_null() {
return;
}
let Some(name) = cstr(name) else {
unsafe { drop(Box::from_raw(value)) };
return;
};
let value = unsafe { *Box::from_raw(value) };
let mut mize = match get_global_mize() {
Ok(m) => m,
Err(_) => return,
};
let Ok(id) = mize.id_new_dots(name) else { return };
let mut opts = mize.config_opts.lock().unwrap();
match opts.get_mut(&id) {
Some(opt) => {
opt.val = Some(value);
}
None => {
opts.insert(
id.clone(),
ConfigOpt {
name: id.dot(),
val: Some(value),
fun: None,
deps: Vec::new(),
},
);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn get_config(name: *const c_char) -> *mut ItemData {
let Some(name) = cstr(name) else {
return ptr::null_mut();
};
let mut mize = match get_global_mize() {
Ok(m) => m,
Err(_) => return ptr::null_mut(),
};
match mize.get_config(name) {
Ok(data) => Box::into_raw(Box::new(data)),
Err(_) => ptr::null_mut(),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn data_new() -> *mut ItemData {
Box::into_raw(Box::new(ItemData::new()))
}
#[unsafe(no_mangle)]
pub extern "C" fn data_from_native(ptr: *mut c_void) -> *mut ItemData {
match ItemData::from_native(NativePtr(ptr)) {
Ok(data) => Box::into_raw(Box::new(data)),
Err(e) => {
let _ = write_to_mize_stdout(format_args!("[c_api] data_from_native: {e:?}\n"));
ptr::null_mut()
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn data_as_native(data: *mut ItemData) -> *mut c_void {
if data.is_null() {
return ptr::null_mut();
}
let data = unsafe { &*data };
match data.native::<NativePtr>() {
Ok(guard) => guard.0,
Err(e) => {
let _ = write_to_mize_stdout(format_args!("[c_api] data_as_native: {e:?}\n"));
ptr::null_mut()
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn data_free(data: *mut ItemData) {
if !data.is_null() {
unsafe { drop(Box::from_raw(data)) };
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mize_write_stdout(str_ptr: *const c_char) {
let Some(s) = cstr(str_ptr) else { return };
let _ = write_to_mize_stdout(format_args!("{s}"));
}
/// Set the thread-local error (like errno). `load_part` / C-callback wrappers turn this into `Err`.
#[unsafe(no_mangle)]
pub extern "C" fn set_err(msg: *const c_char) {
let text = cstr(msg).unwrap_or("set_err called with null/invalid message");
CUR_ERR.with(|slot| {
*slot.borrow_mut() = Some(mize_err!("{}", text));
});
}

View File

@@ -98,6 +98,17 @@ impl<T> MizeAny for T {
} }
/// Load the `.so` for `part_name`, but register it under `at_id`
/// (e.g. `load_part_at("priv.spacetime", "spacetime")`).
/// No-op in generate mode — bindings come from listing the part in `gen = [...]`.
pub fn load_part_at(at_id: &str, part_name: &str) -> MizeResult<()> {
let mize = get_global_mize()?;
if mize.is_generate_mode().unwrap_or(false) {
return Ok(());
}
crate::platform::load_part(part_name, None, Some(at_id))
}
pub fn opt(name: &str) -> crate::config::ConfigOptId { pub fn opt(name: &str) -> crate::config::ConfigOptId {
let mize = get_global_mize().unwrap(); let mize = get_global_mize().unwrap();
let mut cur_part = mize.cur_eval_part.lock().unwrap().clone().unwrap(); let mut cur_part = mize.cur_eval_part.lock().unwrap().clone().unwrap();
@@ -162,6 +173,28 @@ pub fn effect<F: Fn(ItemData) -> MizeResult<ItemData> + Send + Sync + 'static>(n
ConfigOptId { id } ConfigOptId { id }
} }
pub fn apply<F: Fn(ItemData) -> MizeResult<ItemData> + Send + Sync + 'static>(name: &str, fun: F) -> ConfigOptId {
let mize = get_global_mize().unwrap();
let mut cur_part = mize.cur_eval_part.lock().unwrap().clone().unwrap();
let id = cur_part.join(name).unwrap();
// in generate mode: record apply metadata, skip normal registration
if mize.is_generate_mode().unwrap_or(false) {
mize.record_method(name, id.clone(), "apply", "").ok();
return ConfigOptId { id };
}
let mut opts = mize.config_opts.lock().unwrap();
let opt = ConfigOpt {
name: id.dot(),
val: None,
fun: None,
deps: Vec::new(),
};
let mut fun_values = mize.fun_values.lock().unwrap();
fun_values.insert(id.clone(), Box::new(fun));
opts.insert(id.clone(), opt);
ConfigOptId { id }
}
pub fn thread<F: Fn(ItemData) -> MizeResult<ItemData> + Send + Sync + 'static>(name: &str, fun: F) -> ConfigOptId { pub fn thread<F: Fn(ItemData) -> MizeResult<ItemData> + Send + Sync + 'static>(name: &str, fun: F) -> ConfigOptId {
let mize = get_global_mize().unwrap(); let mize = get_global_mize().unwrap();
let mut cur_part = mize.cur_eval_part.lock().unwrap().clone().unwrap(); let mut cur_part = mize.cur_eval_part.lock().unwrap().clone().unwrap();
@@ -227,6 +260,18 @@ pub mod c_api {
}) })
} }
} }
pub extern "C" fn apply(name: *const c_char, c_fun: CFun) -> ConfigOptId {
unsafe {
let c_str = CStr::from_ptr(name);
let string = c_str.to_string_lossy();
super::apply(&string, move |args| {
let raw_ptr = Box::into_raw(Box::new(args));
let ret_ptr = c_fun(raw_ptr);
let data = *Box::from_raw(ret_ptr);
Ok(data)
})
}
}
pub extern "C" fn thread(name: *const c_char, c_fun: CFun) -> ConfigOptId { pub extern "C" fn thread(name: *const c_char, c_fun: CFun) -> ConfigOptId {
unsafe { unsafe {
let c_str = CStr::from_ptr(name); let c_str = CStr::from_ptr(name);

61
core/src/include/mize.h Normal file
View File

@@ -0,0 +1,61 @@
#pragma once
/**
* Mize C API — thin header over libmize.so exports (core/src/c_api.rs).
*
* Names for opt / method / thread / get_config / set_config are absolute
* dotted paths for now (e.g. "vicinae.open", "vicinae.nav").
*
* Errors: thread-local, like errno. Call set_err("...") from C; Rust
* (load_part / method wrappers) turns that into Err. Part entry is
* `void part_<name>(void)` — no sret / Result return.
*
* ItemData (`DATA`) is opaque. Ownership:
* - Callbacks receive a borrowed `args` (do not free).
* - Callbacks must return an owned DATA* (e.g. data_new()); may return NULL for empty.
* - get_config returns owned DATA*; free with data_free.
* - set_config takes ownership of the value pointer.
*/
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct ItemData DATA;
/** C method / thread / effect callback. args is borrowed; return owned DATA* or NULL. */
typedef DATA *(*CFun)(DATA *args);
void opt(const char *name);
void method(const char *name, const char *signature, CFun c_fun);
void effect(const char *name, CFun c_fun);
void thread(const char *name, CFun c_fun);
void set_config(const char *name, DATA *value);
DATA *get_config(const char *name);
DATA *data_new(void);
/** Store an opaque pointer in the native-value registry. */
DATA *data_from_native(void *ptr);
/** Read back a pointer previously stored with data_from_native. Returns NULL on error. */
void *data_as_native(DATA *data);
void data_free(DATA *data);
void mize_write_stdout(const char *str);
/** Set the thread-local error message (consumed by Rust after the call). */
void set_err(const char *msg);
#ifdef __cplusplus
}
#endif

View File

@@ -105,6 +105,7 @@ pub struct PartMetadata {
pub opts: Vec<OptMetadata>, pub opts: Vec<OptMetadata>,
pub methods: Vec<MethodMetadata>, pub methods: Vec<MethodMetadata>,
pub effects: Vec<MethodMetadata>, pub effects: Vec<MethodMetadata>,
pub applies: Vec<MethodMetadata>,
pub threads: Vec<MethodMetadata>, pub threads: Vec<MethodMetadata>,
pub natives: Vec<NativeMetadata>, pub natives: Vec<NativeMetadata>,
} }
@@ -120,7 +121,7 @@ pub struct OptMetadata {
pub struct MethodMetadata { pub struct MethodMetadata {
pub name: String, pub name: String,
pub id: MizeId, pub id: MizeId,
pub kind: String, // "method", "effect", "thread" pub kind: String, // "method", "effect", "apply", "thread"
/// Raw signature string, e.g. `"String -> ()"` or `"Command, Func -> ()"`. /// Raw signature string, e.g. `"String -> ()"` or `"Command, Func -> ()"`.
pub signature: String, pub signature: String,
/// Parsed argument type names (left of `->`). /// Parsed argument type names (left of `->`).
@@ -339,11 +340,18 @@ impl Mize {
// end of platform specific init code // end of platform specific init code
// load the parts, as specified in the parts config // load the parts, as specified in the parts config
match self // Prefer `parts` when it is an array; otherwise `parts.load` (so `parts.workspaces` /
.get_config("parts") // `parts.dist.cd` can coexist under a `parts` table in TOML).
let parts_to_load = match self.get_config("parts") {
Ok(data) if data.cbor().as_array().is_some() => Some(data),
_ => self.get_config("parts.load").ok(),
};
if let Some(parts_to_load) = parts_to_load {
for part_name in parts_to_load
.cbor()
.as_array()
.ok_or("config parts/parts.load was not an array")?
{ {
Ok(parts_to_load) => {
for part_name in parts_to_load.cbor().as_array().ok_or("config parts was not an array")? {
let part_name = part_name.as_text().ok_or("part name was not a string")?; let part_name = part_name.as_text().ok_or("part name was not a string")?;
if part_name.trim().is_empty() || self.has_part(part_name)? { if part_name.trim().is_empty() || self.has_part(part_name)? {
continue; continue;
@@ -351,10 +359,6 @@ impl Mize {
self.load_part(part_name)?; self.load_part(part_name)?;
} }
} }
Err(_err) => {
// no parts to load
}
}
debug!("MIZE INIT DONE"); debug!("MIZE INIT DONE");
Ok(()) Ok(())
@@ -534,7 +538,7 @@ impl Mize {
pub fn load_part(&mut self, name: &str) -> MizeResult<()> { pub fn load_part(&mut self, name: &str) -> MizeResult<()> {
tracing::debug!("loading part: {name}"); tracing::debug!("loading part: {name}");
crate::platform::load_part(self, name, None) crate::platform::load_part(name, None, None)
} }
pub fn fetch_part(&mut self, name: &str) -> MizeResult<String> { pub fn fetch_part(&mut self, name: &str) -> MizeResult<String> {
@@ -542,9 +546,8 @@ impl Mize {
crate::platform::fetch_part(self, name) crate::platform::fetch_part(self, name)
} }
pub fn load_part_at(&mut self, name: &str, path: String) -> MizeResult<()> { pub fn load_part_from_path(&mut self, name: &str, path: String) -> MizeResult<()> {
// platform specific init code crate::platform::load_part(name, Some(path), None)
crate::platform::load_part(self, name, Some(path))
} }
// ===== generate mode accessors ===== // ===== generate mode accessors =====
@@ -600,6 +603,7 @@ impl Mize {
}; };
match kind { match kind {
"effect" => metadata.effects.push(m), "effect" => metadata.effects.push(m),
"apply" => metadata.applies.push(m),
"thread" => metadata.threads.push(m), "thread" => metadata.threads.push(m),
_ => metadata.methods.push(m), _ => metadata.methods.push(m),
} }
@@ -1037,9 +1041,7 @@ impl Mize {
let calling_part = self.clone().cur_eval_part()?; let calling_part = self.clone().cur_eval_part()?;
self.clone().set_cur_eval_part(Some(part_id)); self.clone().set_cur_eval_part(Some(part_id));
println!("before call");
let res = fun(args); let res = fun(args);
println!("after call");
self.clone().set_cur_eval_part(calling_part); self.clone().set_cur_eval_part(calling_part);
let mut fun_values = self.fun_values.lock()?; let mut fun_values = self.fun_values.lock()?;
fun_values.insert(id, fun); fun_values.insert(id, fun);

View File

@@ -48,14 +48,14 @@ fn test_get_module_hash() -> MizeResult<()> {
toolchain_version = "rustc 1.78.0 (9b00956e5 2024-04-29)" toolchain_version = "rustc 1.78.0 (9b00956e5 2024-04-29)"
"#; "#;
instance.set_blocking("0/config/selector", ItemData::from_toml(&selector_toml)?)?; // Pass selector fields as hash options (merges over defaults).
let options = ItemData::from_toml(&selector_toml)?;
let (hash, selector) = get_part_hash(&mut instance, "String", ItemData::new())?; let (hash, selector) = get_part_hash(&mut instance, "String", options)?;
println!("hash: {}", hash); println!("hash: {}", hash);
println!("selector: {}", selector); println!("selector: {}", selector);
assert_eq!(hash, "e96edf60216688314efc263c1aa8d6ce".to_owned()); assert_eq!(hash, "227b0216f9b7a324a621ff773c5241c9".to_owned());
Ok(()) Ok(())
} }

View File

@@ -238,6 +238,9 @@ impl ItemData {
pub fn value_string(self) -> MizeResult<String> { pub fn value_string(self) -> MizeResult<String> {
match self.cbor() { match self.cbor() {
CborValue::Text(string) => Ok(string.to_owned()), CborValue::Text(string) => Ok(string.to_owned()),
CborValue::Bool(val) => Ok(format!("{val}")),
CborValue::Integer(val) => Ok(format!("{}", u32::try_from(val.clone())?)),
CborValue::Float(val) => Ok(format!("{val}")),
_ => Err(mize_err!("this ItemData has no string value")), _ => Err(mize_err!("this ItemData has no string value")),
} }
} }

View File

@@ -292,9 +292,9 @@ pub mod platform {
Ok(()) Ok(())
} }
pub fn load_part(mize: &mut Mize, part_name: &str, path: Option<String>) -> MizeResult<()> { pub fn load_part(part_name: &str, path: Option<String>, at_id: Option<&str>) -> MizeResult<()> {
#[cfg(feature = "target-os")] { #[cfg(feature = "target-os")] {
os::load_part(mize, part_name, path)?; os::load_part(part_name, path, at_id)?;
} }
Ok(()) Ok(())

View File

@@ -171,12 +171,158 @@ pub fn seconds_since_modification(path: &Path) -> MizeResult<u64> {
Ok(duration.as_secs()) Ok(duration.as_secs())
} }
/// Best-effort rustc target triple for the currently running binary.
pub fn host_rust_target() -> &'static str {
#[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))]
{
"x86_64-unknown-linux-gnu"
}
#[cfg(all(target_arch = "aarch64", target_os = "linux", target_env = "gnu"))]
{
"aarch64-unknown-linux-gnu"
}
#[cfg(all(target_arch = "x86_64", target_os = "macos"))]
{
"x86_64-apple-darwin"
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
{
"aarch64-apple-darwin"
}
#[cfg(all(target_arch = "x86_64", target_os = "windows"))]
{
"x86_64-pc-windows-gnu"
}
#[cfg(all(target_arch = "aarch64", target_os = "windows"))]
{
"aarch64-pc-windows-gnullvm"
}
#[cfg(not(any(
all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"),
all(target_arch = "aarch64", target_os = "linux", target_env = "gnu"),
all(target_arch = "x86_64", target_os = "macos"),
all(target_arch = "aarch64", target_os = "macos"),
all(target_arch = "x86_64", target_os = "windows"),
all(target_arch = "aarch64", target_os = "windows"),
)))]
{
"unknown"
}
}
/// Dist artifact filename: `{hash}-{part_id}-cd.tar.gz` or `{hash}-{part_id}-{version}.tar.gz`.
pub fn part_dist_artifact_name(hash: &str, part_id: &str, version: &str, cd: bool) -> String {
if cd {
format!("{hash}-{part_id}-cd.tar.gz")
} else {
format!("{hash}-{part_id}-{version}.tar.gz")
}
}
pub fn parts_dist_cd(mize: &mut Mize) -> bool {
mize.get_config("parts.dist.cd")
.ok()
.and_then(|d| d.value_bool().ok())
.unwrap_or(false)
}
/// Read `version` from a Cargo.toml's `[package]` table.
pub fn cargo_package_version(cargo_toml_path: &str) -> MizeResult<String> {
let contents = fs::read_to_string(cargo_toml_path)
.map_err(|e| mize_err!("could not read {cargo_toml_path}: {e}"))?;
let data = ItemData::from_toml(&contents)?;
data.get_path("package.version")?
.value_string()
.map_err(|e| e.msg(format!("no package.version in {cargo_toml_path}")))
}
/// Resolve a part's cargo version for dist URLs (non-cd).
pub fn resolve_part_version(mize: &mut Mize, part_name: &str) -> MizeResult<String> {
if let Ok(v) = mize.get_config(&format!("{part_name}.version")) {
if let Ok(s) = v.value_string() {
if !s.is_empty() {
return Ok(s);
}
}
}
if part_name == "mize" {
return Ok(env!("CARGO_PKG_VERSION").to_owned());
}
if let Ok(workspaces) = mize.get_config("parts.workspaces") {
if let Some(arr) = workspaces.cbor().as_array() {
for src in arr {
let Some(dir) = src.as_text() else { continue };
for candidate in [
format!("{dir}/{part_name}/Cargo.toml"),
format!("{dir}/mize.{part_name}/Cargo.toml"),
] {
if PathBuf::from(&candidate).exists() {
return cargo_package_version(&candidate);
}
}
}
}
}
if let Ok(mize_src) = mize.get_config("mize.src").and_then(|d| d.value_string()) {
for candidate in [
format!("{mize_src}/parts/{part_name}/Cargo.toml"),
format!("{mize_src}/parts/mize.{part_name}/Cargo.toml"),
format!("{mize_src}/core/Cargo.toml"),
] {
if part_name != "mize" && candidate.ends_with("core/Cargo.toml") {
continue;
}
if PathBuf::from(&candidate).exists() {
return cargo_package_version(&candidate);
}
}
}
Err(mize_err!(
"could not resolve cargo version for part '{part_name}' (set {part_name}.version or parts.workspaces, or enable parts.dist.cd)"
))
}
fn load_selector_base(mize: &mut Mize) -> ItemData {
// Prefer config opt; fall back to store path used by tests / older setups.
if let Ok(sel) = mize.get("self/config/selector").and_then(|i| i.as_data_full()) {
if !sel.cbor().is_null() {
return sel;
}
}
mize.get("0/config/selector")
.and_then(|i| i.as_data_full())
.unwrap_or_else(|_| ItemData::new())
}
pub fn get_part_hash( pub fn get_part_hash(
mize: &mut Mize, mize: &mut Mize,
name: &str, name: &str,
options: ItemData, options: ItemData,
) -> MizeResult<(String, String)> { ) -> MizeResult<(String, String)> {
let mut selector_data = mize.get("self/config/selector")?.as_data_full()?; let mut selector_data = load_selector_base(mize);
if selector_data
.get_path("mize_version")
.ok()
.and_then(|v| v.value_string().ok())
.filter(|s| !s.is_empty())
.is_none()
{
selector_data.set_path("mize_version", env!("CARGO_PKG_VERSION"))?;
}
if selector_data
.get_path("system")
.ok()
.and_then(|v| v.value_string().ok())
.filter(|s| !s.is_empty())
.is_none()
{
selector_data.set_path("system", host_rust_target())?;
}
// handle the case, when we want a part for not the system we are running on... // handle the case, when we want a part for not the system we are running on...
debug!("get_part_hash: name: {}", name); debug!("get_part_hash: name: {}", name);
@@ -227,21 +373,31 @@ pub fn get_part_hash(
pub fn fetch_part(mize: &mut Mize, part_name: &str) -> MizeResult<String> { pub fn fetch_part(mize: &mut Mize, part_name: &str) -> MizeResult<String> {
let (part_hash, part_selector) = get_part_hash(mize, part_name, ItemData::new())?; let (part_hash, part_selector) = get_part_hash(mize, part_name, ItemData::new())?;
let part_url = "https://mize.works/mize/dist/{part_hash}-{part_name}.tar.gz"; let cd = parts_dist_cd(mize);
let version = if cd {
String::new()
} else {
resolve_part_version(mize, part_name)?
};
let artifact = part_dist_artifact_name(&part_hash, part_name, &version, cd);
let part_url = format!("https://dist.mize.works/{artifact}");
let store_path = mize.get_config("store")?.value_string()?; let store_path = mize.get_config("store")?.value_string()?;
let parts_path = format!("{store_path}/parts"); let parts_path = format!("{store_path}/parts");
let target_dir = format!("{parts_path}/{part_hash}-{part_name}"); let target_dir = format!("{parts_path}/{part_hash}-{part_name}");
let tmp_file_path_gz = format!("{parts_path}/{artifact}");
let tmp_file_path_gz = format!("{parts_path}/{part_hash}-{part_name}.tar.gz"); std::fs::create_dir_all(&parts_path)?;
let tmp_gz_file = if !PathBuf::from(tmp_file_path_gz.clone()).exists() { let tmp_gz_file = if !PathBuf::from(tmp_file_path_gz.clone()).exists() {
let mut tmp_gz_file = File::create(&tmp_file_path_gz)?; let mut tmp_gz_file = File::create(&tmp_file_path_gz)?;
debug!("downloading part '{}' with hash: {}", part_name, part_hash); debug!(
"downloading part '{}' from {} (selector: {})",
part_name, part_url, part_selector
);
let response = http_req::request::get(part_url, &mut tmp_gz_file)?; let response = http_req::request::get(&part_url, &mut tmp_gz_file)?;
let code = response.status_code(); let code = response.status_code();
if code != 200.into() { if code != 200.into() {
@@ -252,8 +408,8 @@ pub fn fetch_part(mize: &mut Mize, part_name: &str) -> MizeResult<String> {
part_selector part_selector
) )
.msg(format!( .msg(format!(
"the http request to fetch the part returned a status code of: {}", "the http request to fetch the part from '{}' returned a status code of: {}",
code part_url, code
))); )));
} }
@@ -309,8 +465,11 @@ fn get_part_path(mize: &mut Mize, part_name: &str, _path: Option<String>) -> Miz
//Err(mize_err!("part {part_name} not found")) //Err(mize_err!("part {part_name} not found"))
} }
pub fn load_part(mize: &mut Mize, part_name: &str, path: Option<String>) -> MizeResult<()> { /// Load a part `.so` for `part_name`, registering it under `at_id` (or `part_name` if None).
let part_path = get_part_path(mize, part_name, path)?; /// Uses `get_global_mize()` so nested loads from inside another part's init work.
pub fn load_part(part_name: &str, path: Option<String>, at_id: Option<&str>) -> MizeResult<()> {
let mut mize = crate::get_global_mize()?;
let part_path = get_part_path(&mut mize, part_name, path)?;
let lib = unsafe { libloading::Library::new(part_path)? }; let lib = unsafe { libloading::Library::new(part_path)? };
@@ -326,32 +485,35 @@ pub fn load_part(mize: &mut Mize, part_name: &str, path: Option<String>) -> Mize
let part_symbol_name = part_name.replace(".", "_"); let part_symbol_name = part_name.replace(".", "_");
let func: libloading::Symbol< let func: libloading::Symbol<unsafe extern "C" fn()> =
unsafe extern "C" fn() -> MizeResult<()>, unsafe { lib.get(format!("part_{}", part_symbol_name).as_bytes())? };
> = unsafe { lib.get(format!("part_{}", part_symbol_name).as_bytes())? };
let part_id = mize.clone().id_new_dots(part_name)?; let eval_name = at_id.unwrap_or(part_name);
let part_id = mize.clone().id_new_dots(eval_name)?;
{ {
let mut opts = mize.config_opts.lock()?; let mut opts = mize.config_opts.lock()?;
opts.entry(part_id.clone()).or_insert_with(|| ConfigOpt { opts.entry(part_id.clone()).or_insert_with(|| ConfigOpt {
name: part_name.to_owned(), name: eval_name.to_owned(),
val: Some(ItemData::new()), val: Some(ItemData::new()),
fun: None, fun: None,
deps: Vec::new(), deps: Vec::new(),
}); });
} }
let prev_eval = mize.clone().cur_eval_part()?;
mize.clone().set_cur_eval_part(Some(part_id))?; mize.clone().set_cur_eval_part(Some(part_id))?;
unsafe { func()? }; crate::c_api::clear_err();
mize.clone().set_cur_eval_part(None)?; unsafe { func() };
let c_err = crate::c_api::take_err();
let mut cur_eval_part = mize.cur_eval_part.lock()?; mize.clone().set_cur_eval_part(prev_eval)?;
*cur_eval_part = None;
// dropping the lib, would (i suspect) free all memory, of the library's code, which would // dropping the lib, would (i suspect) free all memory, of the library's code, which would
// make the part's vtable point into empty memory // make the part's vtable point into empty memory
std::mem::forget(lib); std::mem::forget(lib);
if let Some(err) = c_err {
return Err(err);
}
Ok(()) Ok(())
} }

View File

@@ -0,0 +1,57 @@
## Require clarification and plan approval before making architectural code changes
Before making any code changes which could be considered to influence the architekture of the application, you must follow this two-step process:
### Step 1: Ask Clarifying Questions
- Always ask at least one clarifying question about the user's request
- Understand the full scope and context of what they're asking for
- Clarify any ambiguous requirements or edge cases
- Ask about preferred approaches if multiple solutions exist
- Confirm the expected behavior and user experience
- Ask the clarifying questions by numbering them, so the user can number their replies.
### Step 2: Present Implementation Plan
- After receiving clarification, present a detailed implementation plan
- Break down the work into specific, actionable steps
- Identify which files will be created, modified, or deleted
- 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
### What are considered architectural changes
**Architectural** (clarify → plan → wait for approval), for example:
- New parts, or new cross-part `method` / `opt` surfaces
- Wiring one part into another (e.g. UI shell calling `auth.client.login`, soft-notify via `has_part`)
- Changes to core loading / `has_part` / part-system semantics
- Splitting or merging capabilities across part boundaries
**Not architectural** (implement directly when the intent is clear), for example:
- Layout / CSS / copy tweaks in an existing screen
- Adding another display line or button that only uses existing local state or already-exposed methods
- Bugfixes that don’t change the part API (e.g. `document::Style` re-render warnings)
### Approval Requirements
- User must explicitly approve the plan with words like "yes", "approved", "proceed", "go ahead", or similar
- If the user suggests modifications to the plan, incorporate them and seek re-approval
- Do not assume silence or ambiguous responses mean approval
### Exceptions
- This process may be skipped only for trivial changes like fixing obvious typos or formatting
- When in doubt, always follow the full process rather than assuming an exception applies
### Example Workflow
1. User: "Add a login form to the app"
2. Assistant: "I'd like to clarify a few things about the login form: [questions]"
3. User: [provides answers]
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"
6. Assistant: [writes plan to file].
7. Assistant: [proceeds with implementation].

View File

@@ -1,15 +1,5 @@
# Mize Part System (LLM generated guide) # Mize Part System (LLM generated guide)
## Overview
Mize is built around a **parts architecture** that allows you to compose functionality modularly. Each part is a self-contained unit that can:
- Store and manage its own state
- Expose methods to other parts
- Register itself with the Mize instance
- Define configuration options
Parts are the primary way to extend Mize's functionality. The system is designed so that parts can depend on each other, and users can enable/disable parts as needed.
--- ---

View File

@@ -0,0 +1,18 @@
# Plan: dist publish + dist.mize.works (approved)
## Config
- `parts.workspaces`: list of dirs; each subdir is a part to build/upload
- `parts.dist.cd`: bool; when true artifact uses `-cd` instead of `-{version}`
## Publish (`mize.dev`)
- After build: pack each part `.so` under `lib/`, special part `mize` gets `libmize.so` + `bin/mize`
- Hash via `get_part_hash` with modName, system (target), mize_version
- Filename: `{hash}-{id}-{version}.tar.gz` or `{hash}-{id}-cd.tar.gz`
- rsync to `ocib:host/mize-dist/`
## fetch_part
- Base URL `https://dist.mize.works/`
- Same naming; ensure mize_version in selector
## ocib
- `~/host/docker-compose.yaml`: Traefik + kugland/darkhttpd serving `mize-dist` for `dist.mize.works`

View File

@@ -0,0 +1,14 @@
# load_part_at instances (priv.spacetime / ppc.spacetime)
Approved 2026-11-04.
## Goal
Instantiate the same `spacetime` `.so` under different part ids (`priv.spacetime`, `ppc.spacetime`), each with its own config namespace. Dependents use `Spacetime::get("priv.spacetime")` etc.
## Changes
1. Platform `load_part(part_name, path, at_id)` via `get_global_mize()`; save/restore `cur_eval_part`.
2. Rename `Mize::load_part_at` → `load_part_from_path`.
3. `config::load_part_at(at_id, part_name)` — no-op in generate mode.
4. Codegen: special SpacetimeDB client gen when `gen` contains `"spacetime"`, module at `{cwd}/spacetime` if present.
5. `sci`: `gen += spacetime`, `load_part_at("priv.spacetime", "spacetime")`.
6. New `mize/parts/ppc`: loads `ppc.spacetime`.

0
docs/software-factory.md Normal file
View File

View File

@@ -202,6 +202,18 @@ pub extern "C" fn part_auth_client() -> MizeResult<()> {
Ok(ItemData::new()) Ok(ItemData::new())
}); });
method("get_id_token", "() -> String", |_args| {
let data = get_config("self.secrets")?;
let secrets = match data.deserialize::<Option<ClientSecrets>>() {
Ok(Some(secrets)) => secrets,
Ok(None) | Err(_) => {
let mut mize = get_global_mize()?;
load_secrets(&mut mize).map_err(|_| mize_err!("not logged in"))?
}
};
Ok(secrets.id_token.into_item_data())
});
let mut mize = get_global_mize()?; let mut mize = get_global_mize()?;
// start refresh tread if configured // start refresh tread if configured

View File

@@ -1,7 +1,7 @@
cargo-features = ["profile-rustflags"] cargo-features = ["profile-rustflags"]
[package] [package]
name = "mize-part-dev" name = "mize-part-mize-dev"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"

View File

@@ -91,7 +91,13 @@ fn generate_part_struct(out: &mut String, meta: &PartMetadata) {
let _ = writeln!(out); let _ = writeln!(out);
// part methods (skip ones that belong to include_native) // part methods (skip ones that belong to include_native)
for m in meta.methods.iter().chain(meta.effects.iter()).chain(meta.threads.iter()) { for m in meta
.methods
.iter()
.chain(meta.effects.iter())
.chain(meta.applies.iter())
.chain(meta.threads.iter())
{
if native_fns.contains(&m.name) { if native_fns.contains(&m.name) {
continue; continue;
} }
@@ -466,7 +472,12 @@ pub fn extract_mize_dependency(cargo_toml: &str) -> Result<String, String> {
/// ///
/// `mize_dep` is the full assignment line copied from the part Cargo.toml, /// `mize_dep` is the full assignment line copied from the part Cargo.toml,
/// e.g. `mize = { workspace = true }` or `mize = { path = "../../core" }`. /// e.g. `mize = { workspace = true }` or `mize = { path = "../../core" }`.
pub fn generate_cargo_toml(package_name: &str, mize_dep: &str) -> String { pub fn generate_cargo_toml(package_name: &str, mize_dep: &str, spacetime_sdk: bool) -> String {
let spacetime_dep = if spacetime_sdk {
"spacetimedb-sdk = \"2.6.0\"\n"
} else {
""
};
format!( format!(
r#"[package] r#"[package]
name = "{package_name}" name = "{package_name}"
@@ -479,9 +490,10 @@ path = "src/lib.rs"
[dependencies] [dependencies]
{mize_dep} {mize_dep}
"#, {spacetime_dep}"#,
package_name = package_name, package_name = package_name,
lib_name = package_name.replace('-', "_"), lib_name = package_name.replace('-', "_"),
mize_dep = mize_dep.trim(), mize_dep = mize_dep.trim(),
spacetime_dep = spacetime_dep,
) )
} }

View File

@@ -1,8 +1,12 @@
use std::fs::{create_dir_all, write}; use std::fs::{create_dir_all, write};
use std::path::{Path, PathBuf};
use mize::prelude::*; use mize::prelude::*;
use cmd_lib::run_cmd as sh; use cmd_lib::run_cmd as sh;
use mize::tracing::info; use mize::tracing::info;
use mize::tracing::debug; use mize::tracing::debug;
use mize::platform::os::{
cargo_package_version, get_part_hash, part_dist_artifact_name, parts_dist_cd,
};
// a static version of what would be generated. // a static version of what would be generated.
// as we need mize.dev to generate anything. // as we need mize.dev to generate anything.
@@ -10,8 +14,6 @@ mod generated;
mod codegen; mod codegen;
use generated::*; use generated::*;
// spacetime gen command: sh!(RUST_LOG=error RUST_BACKTRACE=0 spacetime generate --build-options="--lint-dir=" -p $SRC/mize/marts/spacetime -o $SRC/mize/marts/generated/src/spacetime --lang rust)?;
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
extern "C" fn part_dev() -> MizeResult<()> { extern "C" fn part_dev() -> MizeResult<()> {
let cli = generated::CliPart::get("cli")?; let cli = generated::CliPart::get("cli")?;
@@ -26,20 +28,245 @@ extern "C" fn part_dev() -> MizeResult<()> {
Ok(()) Ok(())
} }
struct PartToPublish {
/// Runtime part id (e.g. `dev`, `auth.client`, `mize`)
id: String,
/// Directory containing Cargo.toml (None for the special `mize` core part)
dir: Option<PathBuf>,
version: String,
}
fn dylib_ext(target: &str) -> &'static str {
if target.contains("windows") {
"dll"
} else if target.contains("apple") || target.contains("darwin") {
"dylib"
} else {
"so"
}
}
fn part_lib_filename(part_id: &str, target: &str) -> String {
let sym = part_id.replace('.', "_");
let ext = dylib_ext(target);
if target.contains("windows") {
format!("mize_part_{sym}.{ext}")
} else {
format!("libmize_part_{sym}.{ext}")
}
}
fn mize_lib_filename(target: &str) -> String {
let ext = dylib_ext(target);
if target.contains("windows") {
format!("mize.{ext}")
} else {
format!("libmize.{ext}")
}
}
fn mize_bin_filename(target: &str) -> String {
if target.contains("windows") {
"mize.exe".to_owned()
} else {
"mize".to_owned()
}
}
fn release_dir(mize_src: &str, target: &str) -> PathBuf {
PathBuf::from(mize_src)
.join("target")
.join(target)
.join("release")
}
fn resolve_part_id(dir: &Path) -> MizeResult<String> {
let part_file = dir.join("part");
if part_file.exists() {
let toml = std::fs::read_to_string(&part_file)
.map_err(|e| mize_err!("could not read {}: {e}", part_file.display()))?;
let data = ItemData::from_toml(&toml)?;
if let Ok(id) = data.get_path("id").and_then(|v| v.value_string()) {
if !id.is_empty() {
return Ok(id);
}
}
}
let cargo_path = dir.join("Cargo.toml");
let cargo = std::fs::read_to_string(&cargo_path)
.map_err(|e| mize_err!("could not read {}: {e}", cargo_path.display()))?;
let data = ItemData::from_toml(&cargo)?;
if let Ok(lib_name) = data.get_path("lib.name").and_then(|v| v.value_string()) {
if let Some(rest) = lib_name.strip_prefix("mize_part_") {
return Ok(rest.replace('_', "."));
}
}
dir.file_name()
.and_then(|s| s.to_str())
.map(|s| s.to_owned())
.ok_or_else(|| mize_err!("could not resolve part id for {}", dir.display()))
}
fn collect_parts_to_publish(mize: &mut Mize, mize_src: &str) -> MizeResult<Vec<PartToPublish>> {
let mut out = Vec::new();
// Special core part "mize"
let mize_version = cargo_package_version(&format!("{mize_src}/core/Cargo.toml"))?;
out.push(PartToPublish {
id: "mize".to_owned(),
dir: None,
version: mize_version.clone(),
});
let workspaces = match mize.get_config("parts.workspaces") {
Ok(data) => data,
Err(_) => {
info!("parts.workspaces not set; only publishing special part 'mize'");
return Ok(out);
}
};
let arr = workspaces
.cbor()
.as_array()
.ok_or_else(|| mize_err!("parts.workspaces must be an array of directory paths"))?;
for src in arr {
let dir_str = src
.as_text()
.ok_or_else(|| mize_err!("parts.workspaces entries must be strings"))?;
let dir = PathBuf::from(dir_str);
if !dir.is_dir() {
info!("parts.workspaces entry is not a directory, skipping: {dir_str}");
continue;
}
let entries = std::fs::read_dir(&dir)
.map_err(|e| mize_err!("could not read parts.workspaces dir {dir_str}: {e}"))?;
for entry in entries {
let entry = entry.map_err(|e| mize_err!("read_dir error in {dir_str}: {e}"))?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let cargo = path.join("Cargo.toml");
if !cargo.exists() {
debug!("skipping {} (no Cargo.toml)", path.display());
continue;
}
let id = resolve_part_id(&path)?;
let version = cargo_package_version(cargo.to_str().unwrap())?;
out.push(PartToPublish {
id,
dir: Some(path),
version,
});
}
}
Ok(out)
}
fn pack_and_upload_part(
mize: &mut Mize,
mize_src: &str,
target: &str,
part: &PartToPublish,
mize_version: &str,
) -> MizeResult<()> {
let rel = release_dir(mize_src, target);
let tmp = std::env::var("TMP").unwrap_or_else(|_| "/tmp".to_owned());
let staging = PathBuf::from(&tmp).join(format!("mize-dist-{}-{}", part.id, target));
if staging.exists() {
std::fs::remove_dir_all(&staging)
.map_err(|e| mize_err!("could not clear staging {}: {e}", staging.display()))?;
}
let lib_dir = staging.join("lib");
create_dir_all(&lib_dir)
.map_err(|e| mize_err!("could not create {}: {e}", lib_dir.display()))?;
if part.id == "mize" {
let lib_name = mize_lib_filename(target);
let bin_name = mize_bin_filename(target);
let lib_src = rel.join(&lib_name);
let bin_src = rel.join(&bin_name);
if !lib_src.exists() {
return Err(mize_err!("missing built artifact {}", lib_src.display()));
}
if !bin_src.exists() {
return Err(mize_err!("missing built artifact {}", bin_src.display()));
}
std::fs::copy(&lib_src, lib_dir.join(&lib_name))
.map_err(|e| mize_err!("copy {}: {e}", lib_src.display()))?;
let bin_dir = staging.join("bin");
create_dir_all(&bin_dir)?;
std::fs::copy(&bin_src, bin_dir.join(&bin_name))
.map_err(|e| mize_err!("copy {}: {e}", bin_src.display()))?;
} else {
let lib_name = part_lib_filename(&part.id, target);
let lib_src = rel.join(&lib_name);
if !lib_src.exists() {
info!(
"skipping part '{}': missing {} (not built for this target?)",
part.id,
lib_src.display()
);
return Ok(());
}
// Archive always uses the fetch_part layout: lib/libmize_part_<sym>.so
// For non-linux targets keep the native filename inside lib/.
let dest_name = if dylib_ext(target) == "so" {
format!("libmize_part_{}.so", part.id.replace('.', "_"))
} else {
lib_name.clone()
};
std::fs::copy(&lib_src, lib_dir.join(&dest_name))
.map_err(|e| mize_err!("copy {}: {e}", lib_src.display()))?;
}
let mut hash_opts = ItemData::new();
hash_opts.set_path("system", target)?;
hash_opts.set_path("mize_version", mize_version)?;
let (hash, selector) = get_part_hash(mize, &part.id, hash_opts)?;
debug!("part {} selector: {}", part.id, selector);
let cd = parts_dist_cd(mize);
let artifact = part_dist_artifact_name(&hash, &part.id, &part.version, cd);
let archive_path = PathBuf::from(&tmp).join(&artifact);
let staging_str = staging.to_string_lossy().to_string();
let archive_str = archive_path.to_string_lossy().to_string();
sh!(tar -C $staging_str -czf $archive_str .)?;
info!("uploading {artifact} -> ocib:host/mize-dist/");
sh!(rsync -av $archive_str ocib:host/mize-dist/)?;
let _ = std::fs::remove_dir_all(&staging);
let _ = std::fs::remove_file(&archive_path);
Ok(())
}
pub fn publish_mize(_ : ArgMatches) -> MizeResult<()> { pub fn publish_mize(_ : ArgMatches) -> MizeResult<()> {
let mut mize = get_global_mize()?; let mut mize = get_global_mize()?;
let mize_src = mize.get_config("mize.src")?.value_string()?; let mize_src = mize.get_config("mize.src")?.value_string()?;
let cargo_dir = std::env::home_dir().unwrap().join(".cargo"); let cargo_dir = std::env::home_dir().unwrap().join(".cargo");
let mize_version = cargo_package_version(&format!("{mize_src}/core/Cargo.toml"))?;
let parts = collect_parts_to_publish(&mut mize, &mize_src)?;
info!(
"publishing {} part(s) (mize_version={mize_version}, cd={})",
parts.len(),
parts_dist_cd(&mut mize)
);
let targets = vec![ let targets = vec![
"x86_64-pc-windows-gnu",
"aarch64-pc-windows-gnullvm",
"x86_64-unknown-linux-gnu", "x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu", "aarch64-unknown-linux-gnu",
"armv7-unknown-linux-gnueabihf", "armv7-unknown-linux-gnueabihf",
"arm-unknown-linux-gnueabi", "arm-unknown-linux-gnueabi",
"x86_64-pc-windows-gnu",
"aarch64-pc-windows-gnullvm",
"aarch64-apple-darwin", "aarch64-apple-darwin",
"x86_64-apple-darwin", "x86_64-apple-darwin",
]; ];
@@ -50,7 +277,7 @@ pub fn publish_mize(_ : ArgMatches) -> MizeResult<()> {
"x86_64-apple-darwin" => "darwin", "x86_64-apple-darwin" => "darwin",
_ => "base", _ => "base",
}; };
let tmp = std::env::var("TMP").unwrap(); let tmp = std::env::var("TMP").unwrap_or_else(|_| "/tmp".to_owned());
let mut build_config = ItemData::new(); let mut build_config = ItemData::new();
build_config.set_path("namespace", "mize.buildtime.ns")?; build_config.set_path("namespace", "mize.buildtime.ns")?;
std::fs::create_dir_all(&tmp)?; std::fs::create_dir_all(&tmp)?;
@@ -58,10 +285,19 @@ pub fn publish_mize(_ : ArgMatches) -> MizeResult<()> {
std::fs::write(format!("{tmp}/mize-part-dev-build-config.toml"), build_config.to_json()?)?; std::fs::write(format!("{tmp}/mize-part-dev-build-config.toml"), build_config.to_json()?)?;
sh!(docker build -t mize-build-$docker_image $mize_src/parts/mize.dev/docker/$docker_image)?; sh!(docker build -t mize-build-$docker_image $mize_src/parts/mize.dev/docker/$docker_image)?;
sh!(echo running in container: docker run -it --rm -v "$tmp/mize-part-dev-build-config.toml:/build-config.toml" -e MIZE_BUILD_CONFIG=/build-config.toml -v $cargo_dir/registry:/usr/local/cargo/registry -v $cargo_dir/git:/usr/local/cargo/git -v $mize_src:/src -v $mize_src/target:/src/target mize-build-$docker_image cargo build --target $target_name --release --workspace)?; sh!(echo running in container: docker run -it --rm -v "$tmp/mize-part-dev-build-config.toml:/build-config.toml" -e MIZE_BUILD_CONFIG=/build-config.toml -v $cargo_dir/registry:/usr/local/cargo/registry -v $cargo_dir/git:/usr/local/cargo/git -v $mize_src:/src -v $mize_src/target:/src/target mize-build-$docker_image cargo build --target $target_name --release --workspace)?;
let res = sh!(docker run -it --rm -v "$tmp/mize-part-dev-build-config.toml:/build-config.toml" -e MIZE_BUILD_CONFIG=/build-config.toml -v $cargo_dir/registry:/usr/local/cargo/registry -v $cargo_dir/git:/usr/local/cargo/git -v $mize_src:/src -v $mize_src/target:/src/target mize-build-$docker_image cargo build --target $target_name --release); let res = sh!(docker run -it --rm -v "$tmp/mize-part-dev-build-config.toml:/build-config.toml" -e MIZE_BUILD_CONFIG=/build-config.toml -v $cargo_dir/registry:/usr/local/cargo/registry -v $cargo_dir/git:/usr/local/cargo/git -v $mize_src:/src -v $mize_src/target:/src/target mize-build-$docker_image cargo build --target $target_name --release --workspace);
if res.is_err() { if res.is_err() {
info!("build for {target_name} failed..."); info!("build for {target_name} failed...");
break;
} }
for part in &parts {
if let Err(e) = pack_and_upload_part(&mut mize, &mize_src, target_name, part, &mize_version) {
info!("pack/upload for part '{}' target '{target_name}' failed", part.id);
e.log();
}
}
break; break;
}; };
@@ -106,12 +342,16 @@ pub fn gen_bindings(_ : ArgMatches) -> MizeResult<()> {
gen_parts.push(s.to_owned()); gen_parts.push(s.to_owned());
} }
debug!("parts to generate bindings for: {:?}", gen_parts); debug!("parts to generate bindings for: {:?}", gen_parts);
let gen_spacetime = gen_parts.iter().any(|p| p == "spacetime");
let spacetime_module_dir = cwd.join("spacetime");
let gen_spacetime_sdk = gen_spacetime && spacetime_module_dir.is_dir();
// 3. enable generate mode and clear any old metadata // 3. enable generate mode and clear any old metadata
mize.set_generate_mode(true)?; mize.set_generate_mode(true)?;
mize.clear_metadata()?; mize.clear_metadata()?;
// 4. load each part — in generate mode their part_smth funcs only record metadata // 4. load each part — in generate mode their part_smth funcs only record metadata
// (`"spacetime"` still produces normal SpacetimePart bindings; SDK client codegen is extra)
for p in &gen_parts { for p in &gen_parts {
info!("loading part in generate mode: {p}"); info!("loading part in generate mode: {p}");
mize.load_part(p)?; mize.load_part(p)?;
@@ -136,11 +376,36 @@ pub fn gen_bindings(_ : ArgMatches) -> MizeResult<()> {
let mize_dep = codegen::extract_mize_dependency(&part_cargo) let mize_dep = codegen::extract_mize_dependency(&part_cargo)
.map_err(|e| mize_err!("{e}"))?; .map_err(|e| mize_err!("{e}"))?;
debug!("using mize dependency from part Cargo.toml: {mize_dep}"); debug!("using mize dependency from part Cargo.toml: {mize_dep}");
let cargo_toml = codegen::generate_cargo_toml(&package_name, &mize_dep); let cargo_toml = codegen::generate_cargo_toml(&package_name, &mize_dep, gen_spacetime_sdk);
write(gen_dir.join("Cargo.toml"), cargo_toml) write(gen_dir.join("Cargo.toml"), cargo_toml)
.map_err(|e| mize_err!("could not write generated/Cargo.toml: {e}"))?; .map_err(|e| mize_err!("could not write generated/Cargo.toml: {e}"))?;
let lib_rs = codegen::generate_lib_rs(&all_metadata); let mut lib_rs = codegen::generate_lib_rs(&all_metadata);
// 8. if gen includes "spacetime" and {cwd}/spacetime exists, emit SpacetimeDB rust client bindings
if gen_spacetime_sdk {
let module_path = spacetime_module_dir.to_string_lossy().to_string();
let spacetime_out = src_dir.join("spacetime");
create_dir_all(&spacetime_out)
.map_err(|e| mize_err!("could not create {}: {e}", spacetime_out.display()))?;
info!(
"running spacetime generate -p {} -o {}",
module_path,
spacetime_out.display()
);
let spacetime_out_str = spacetime_out.to_string_lossy().to_string();
sh!(
RUST_LOG=error RUST_BACKTRACE=0 spacetime generate
--build-options="--lint-dir="
-p $module_path
-o $spacetime_out_str
--lang rust
)?;
lib_rs.push_str("\npub mod spacetime;\npub use spacetime::*;\n");
}
write(src_dir.join("lib.rs"), lib_rs) write(src_dir.join("lib.rs"), lib_rs)
.map_err(|e| mize_err!("could not write generated/src/lib.rs: {e}"))?; .map_err(|e| mize_err!("could not write generated/src/lib.rs: {e}"))?;

View File

@@ -0,0 +1,19 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-mos-bash-config-cache"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_mos_bash_config_cache"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

View File

@@ -0,0 +1 @@
id = "mos.bash_config_cache"

View File

@@ -0,0 +1,89 @@
use std::fs::{create_dir_all, write};
use std::path::PathBuf;
use mize::prelude::*;
fn bash_double_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'\\' | '"' | '`' => {
out.push('\\');
out.push(c);
}
'\n' => out.push_str("\\n"),
// leave `$` unescaped so values like $CONFIG_DIR expand when sourced
_ => out.push(c),
}
}
out.push('"');
out
}
fn opt_value_for_bash(mize: &mut Mize, opt_id: &str) -> MizeResult<String> {
println!("hiiii");
let val = mize.get_config(opt_id)?;
if let Some(arr) = val.cbor().as_array() {
println!("as arr!!");
let mut quoted = Vec::with_capacity(arr.len());
for entry in arr {
let s = entry.as_text().ok_or_else(|| {
mize_err!("mos.bash_config_cache: list opt '{opt_id}' has a non-string entry")
})?;
quoted.push(bash_double_quote(s));
}
return Ok(quoted.join(" "));
}
val.value_string()
}
fn write_cache(_args: ItemData) -> MizeResult<ItemData> {
let mut mize = get_global_mize()?;
let store = mize.get_config("store")?.value_string()?;
let opts_data = mize.get_config("mos.bash_config_cache.opts")?;
let arr = opts_data
.cbor()
.as_array()
.ok_or_else(|| mize_err!("mos.bash_config_cache.opts is not a list"))?;
let mut body = String::from("# generated by mos.bash_config_cache.write_cache — do not edit\n");
body.push_str("declare -A myconfig\n");
for entry in arr {
let opt_id = entry
.as_text()
.ok_or_else(|| mize_err!("mos.bash_config_cache.opts entry is not a string"))?;
let val = opt_value_for_bash(&mut mize, opt_id)?;
body.push_str(&format!(
"myconfig[{}]={}\n",
bash_double_quote(opt_id),
bash_double_quote(&val)
));
}
let out_dir = PathBuf::from(&store).join("misc");
create_dir_all(&out_dir)?;
let out_path = out_dir.join("mos.bash_config_cache");
write(&out_path, body)?;
Ok(ItemData::new())
}
#[unsafe(no_mangle)]
pub extern "C" fn part_mos_bash_config_cache() -> MizeResult<()> {
// toml may already have populated opts; opt() inserts empty and would clobber it
let preexisting = get_global_mize()?
.get_config("mos.bash_config_cache.opts")
.ok();
let opts_id = opt("opts");
if let Some(val) = preexisting {
opts_id.default_val(val);
}
apply("write_cache", write_cache);
// until apply hooks run at startup automatically
write_cache(ItemData::new())?;
Ok(())
}

View File

@@ -0,0 +1,29 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-ppc-server"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_ppc_server"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
axum = "0.8.9"
axum-oidc = "=1.0.0-dev-1"
openidconnect = { version = "4", features = ["reqwest", "rustls-tls"], default-features = false }
tower = "0.5.3"
tower-sessions = "0.15.0"
tower-http = { version = "0.7.1", features = ["cors", "trace"] }
serde_json = "1.0"
tokio = { version = "1", features = ["rt-multi-thread", "net", "macros"] }
tracing = "0.1"
dioxus = { version = "0.7", default-features = false, features = ["ssr", "macro", "html", "signals"] }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

176
parts/ppc.server/src/lib.rs Normal file
View File

@@ -0,0 +1,176 @@
mod pages;
use std::str::FromStr;
use std::thread;
use axum::{
Json, Router,
error_handling::HandleErrorLayer,
http::Uri,
response::IntoResponse,
routing::{any, get},
};
use axum_oidc::handle_oidc_redirect;
use axum_oidc::{
EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcClient, OidcLoginLayer,
OidcRpInitiatedLogout, error::MiddlewareError,
};
use mize::prelude::*;
use openidconnect::Scope;
use openidconnect::{ClientId, ClientSecret, IssuerUrl};
use serde_json::{Value, json};
use tokio::runtime::Runtime;
use tower::ServiceBuilder;
use tower_sessions::{
Expiry, MemoryStore, SessionManagerLayer,
cookie::{SameSite, time::Duration},
};
fn ensure_default(rel: &str, default: ItemData) -> MizeResult<()> {
let mut mize = get_global_mize()?;
let cur = mize
.cur_eval_part()?
.ok_or_else(|| mize_err!("no cur_eval_part"))?;
let full = format!("{}.{}", cur.dot(), rel);
if mize.get_config(&full).is_ok() {
return Ok(());
}
let segs: Vec<&str> = rel.split('.').filter(|s| !s.is_empty()).collect();
let last = *segs.last().ok_or_else(|| mize_err!("empty opt path"))?;
let mut parent = cur.clone();
for seg in &segs[..segs.len() - 1] {
parent = parent.join(seg)?;
}
let original = Some(cur);
mize.set_cur_eval_part(Some(parent))?;
opt(last).default_val(default);
mize.set_cur_eval_part(original)?;
Ok(())
}
#[unsafe(no_mangle)]
pub extern "C" fn part_ppc_server() -> MizeResult<()> {
ensure_default("url", "http://localhost:3000".into_item_data())?;
ensure_default("bind", "0.0.0.0:8010".into_item_data())?;
thread::spawn(|| {
let rt = match Runtime::new() {
Ok(rt) => rt,
Err(e) => {
eprintln!("ppc.server: failed to create runtime: {e}");
return;
}
};
if let Err(e) = rt.block_on(start_server()) {
e.log();
}
});
Ok(())
}
async fn start_server() -> MizeResult<()> {
let mut mize = get_global_mize()?;
let issuer = mize.get_config("auth.server.issuer")?.value_string()?;
let client_id = mize.get_config("auth.server.client_id")?.value_string()?;
let client_secret = mize.get_config("auth.server.client_secret")?.value_string()?;
let url = mize.get_config("ppc.server.url")?.value_string()?;
let bind = mize.get_config("ppc.server.bind")?.value_string()?;
let redirect_url = format!("{url}/oidc");
let session_store = MemoryStore::default();
let session_layer = SessionManagerLayer::new(session_store)
.with_secure(false)
.with_same_site(SameSite::Lax)
.with_expiry(Expiry::OnInactivity(Duration::seconds(120)));
let oidc_login_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
dbg!(&e);
e.into_response()
}))
.layer(OidcLoginLayer::<EmptyAdditionalClaims>::new());
let oidc_client = OidcClient::<EmptyAdditionalClaims>::builder()
.with_default_http_client()
.with_redirect_url(Uri::from_str(redirect_url.as_str())?)
.with_client_id(ClientId::new(client_id))
.add_scope(Scope::new("profile".into()))
.add_scope(Scope::new("email".into()))
.with_client_secret(ClientSecret::new(client_secret))
.discover(IssuerUrl::new(issuer).expect("Invalid IssuerUrl"))
.await
.map_err(|e| mize_err!("oidc discover failed: {:?}", e))?
.build();
let oidc_auth_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
dbg!(&e);
e.into_response()
}))
.layer(OidcAuthLayer::new(oidc_client));
let app = Router::new()
.route("/foo", get(authenticated))
.route("/logout", get(logout))
.layer(oidc_login_service)
.route("/bar", get(maybe_authenticated))
.route("/oidc", any(handle_oidc_redirect::<EmptyAdditionalClaims>))
.layer(oidc_auth_service)
.layer(session_layer)
.layer(tower_http::trace::TraceLayer::new_for_http())
.route("/", get(pages::main_page()))
.route("/mc", get(pages::mc_page()))
.route(
"/.well-known/matrix/client",
get(handle_matrix_client).layer(tower_http::cors::CorsLayer::permissive()),
)
.route("/.well-known/matrix/server", get(handle_matrix_server));
tracing::info!("ppc.server listening on http://{bind}");
let listener = tokio::net::TcpListener::bind(&bind)
.await
.map_err(|e| mize_err!("bind {bind}: {e}"))?;
axum::serve(listener, app.into_make_service())
.await
.map_err(|e| mize_err!("axum serve: {e}"))?;
Ok(())
}
async fn authenticated(claims: OidcClaims<EmptyAdditionalClaims>) -> impl IntoResponse {
format!("Hello {}", claims.subject().as_str())
}
async fn maybe_authenticated(
claims: Result<OidcClaims<EmptyAdditionalClaims>, axum_oidc::error::ExtractorError>,
) -> impl IntoResponse {
if let Ok(claims) = claims {
format!(
"Hello {}! You are already logged in from another Handler.",
claims.subject().as_str()
)
} else {
"Hello anon!".to_string()
}
}
async fn logout(logout: OidcRpInitiatedLogout) -> impl IntoResponse {
logout.with_post_logout_redirect(Uri::from_static("https://localhost:3000"))
}
async fn handle_matrix_client() -> Json<Value> {
Json(json!({
"m.homeserver": {
"base_url": "https://matrix.ppc.social"
}
}))
}
async fn handle_matrix_server() -> Json<Value> {
Json(json!({
"m.server": "matrix.ppc.social:443"
}))
}

View File

@@ -0,0 +1,186 @@
use axum::response::Html;
use dioxus::prelude::*;
const SITE_CSS: &str = r#"
*, *::before, *::after { box-sizing: border-box; }
html { color-scheme: light dark; }
body {
margin: 0;
padding: 0 1.25rem 2.5rem;
max-width: 60ch;
margin-inline: auto;
font-family: Georgia, "Times New Roman", serif;
font-size: 1.25rem;
line-height: 1.6;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
background-color: #F7F8F4;
color: #20251C;
word-wrap: break-word;
}
h1, h2 {
line-height: 1.2;
font-weight: 700;
color: #20251C;
}
h1 { margin: 2.5rem 0 1.5rem; font-size: 1.75rem; }
h2 { margin: 2.5rem 0 1rem; font-size: 1.35rem; }
p { margin: 0.85rem 0; }
.tagline { color: #66705E; font-style: italic; }
.meta { color: #66705E; font-size: 0.95rem; }
a {
color: #376777;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
}
a:hover { color: #79500F; }
.accent { color: #79500F; }
.accent-bar {
display: inline-block;
width: 2.5rem;
height: 0.35rem;
background: #FEC251;
border-radius: 2px;
margin-bottom: 0.75rem;
}
@media (prefers-color-scheme: dark) {
body { background-color: #171D18; color: #F2F4EA; }
h1, h2 { color: #F2F4EA; }
.tagline, .meta { color: #AAB5A4; }
a { color: #B9E4EB; }
a:hover { color: #D7E47D; }
.accent { color: #D7E47D; }
.accent-bar { background: #D7E47D; }
}
"#;
const PLAUSIBLE_SRC: &str = "https://plausible.c2vi.dev/js/script.file-downloads.hash.outbound-links.pageview-props.revenue.tagged-events.js";
const PLAUSIBLE_BOOT: &str =
"window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }";
fn render_page(title: &str, body: Element) -> Html<String> {
let markup = dioxus::ssr::render_element(rsx! {
head {
meta { charset: "UTF-8" }
meta { name: "viewport", content: "width=device-width, initial-scale=1.0" }
title { "{title}" }
script {
defer: true,
"data-domain": "ppc.social",
src: PLAUSIBLE_SRC,
}
script { dangerous_inner_html: PLAUSIBLE_BOOT }
style { {SITE_CSS} }
}
body { {body} }
});
Html(format!("<!DOCTYPE html>\n<html lang=\"en\">\n{markup}\n</html>"))
}
#[component]
fn MainPageBody() -> Element {
rsx! {
div { class: "accent-bar" }
h1 { "People Corner" }
p { class: "tagline", "\"Cool People doing Cool things together\"" }
p {
"Read what this is all about: "
a { href: "https://wiki.ppc.social/u/c2vi/The-Idea-of-PPC", "About" }
}
p {
"There is a Discord Server: "
a { href: "https://discord.gg/cMKQNJMkRe", "https://discord.gg/cMKQNJMkRe" }
}
p {
"There is a "
a { href: "https://ppc.social/mc", "Minecraft server" }
"."
}
p {
"There is "
a { href: "https://wiki.ppc.social", "The PPC Wiki Projekt" }
"."
}
p {
"There is an "
a { href: "https://www.instagram.com/ppc.social/", "Instagram" }
" and a "
a { href: "https://www.youtube.com/@ppc.social", "Youtube Channel" }
}
p { class: "meta", "and that's about all for now..." }
}
}
#[component]
fn McPageBody() -> Element {
rsx! {
div { class: "accent-bar" }
h1 { "Welcome to the PPC Minecraft Cluster" }
h2 { "Address" }
p { class: "accent", "first.ppc.social" }
p { class: "meta", "Java 1.21.11 and Bedrock (bedrock not working rn)" }
h2 { "Discord" }
p {
a {
href: "https://discord.gg/cMKQNJMkRe",
target: "_blank",
"https://discord.gg/cMKQNJMkRe"
}
}
p { class: "meta", "come and say hi!" }
p { "See you ingame and invite all your friends to play as well!!" }
h2 { "SimpleVoicechat" }
p {
"This is "
span { class: "accent", "not required" }
", but makes playing soooo much more fun, so it's "
span { class: "accent", "highly encouraged" }
"."
}
p {
"Instructions can be found here:"
}
p {
a { href: "https://fabricmc.net/use/installer/", "Fabric" }
}
p {
a {
href: "https://modrepo.de/minecraft/voicechat/wiki/installation#fabric",
"SimpleVoiceChat"
}
}
}
}
pub fn main_page() -> Html<String> {
render_page("People Corner", rsx! { MainPageBody {} })
}
pub fn mc_page() -> Html<String> {
render_page("PPC Minecraft Server", rsx! { McPageBody {} })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn main_page_renders() {
let html = main_page().0;
assert!(html.contains("<!DOCTYPE html>"));
assert!(html.contains("People Corner"));
assert!(html.contains("Cool People doing Cool things together"));
assert!(html.contains("#F7F8F4"));
assert!(html.contains("#376777"));
assert!(html.contains("https://wiki.ppc.social/u/c2vi/The-Idea-of-PPC"));
}
#[test]
fn mc_page_renders() {
let html = mc_page().0;
assert!(html.contains("PPC Minecraft Cluster"));
assert!(html.contains("first.ppc.social"));
assert!(html.contains("SimpleVoicechat"));
assert!(html.contains("https://fabricmc.net/use/installer/"));
}
}

19
parts/ppc/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-ppc"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_ppc"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

2
parts/ppc/part Normal file
View File

@@ -0,0 +1,2 @@
id = "ppc"
gen = [ "spacetime" ]

7
parts/ppc/src/lib.rs Normal file
View File

@@ -0,0 +1,7 @@
use mize::prelude::*;
#[unsafe(no_mangle)]
pub extern "C" fn part_ppc() -> MizeResult<()> {
load_part_at("ppc.spacetime", "spacetime")?;
Ok(())
}

View File

@@ -0,0 +1,19 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-sci-inbox"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_sci_inbox"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

View File

@@ -0,0 +1,6 @@
use mize::prelude::*;
#[unsafe(no_mangle)]
pub extern "C" fn part_sci_inbox() -> MizeResult<()> {
Ok(())
}

View File

@@ -0,0 +1,19 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-sci-timetrack"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_sci_timetrack"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

View File

@@ -0,0 +1,73 @@
use mize::prelude::*;
pub struct CliPart {
id: MizeId,
}
impl MizePart for CliPart {
fn id(&self) -> MizeResult<MizeId> {
Ok(self.id.clone())
}
}
impl CliPart {
pub fn get(name: &str) -> MizeResult<CliPart> {
let mut mize = get_global_mize()?;
let part = mize.get_part(name)?;
Ok(CliPart { id: part.id()? })
}
pub fn set_command(&self, name: &str) -> MizeResult<()> {
let mut args = ItemData::new();
args.set_path("0", name.into_item_data())?;
dyn_call(self.id.clone(), "set_command", args)?;
Ok(())
}
pub fn subcommand<T: Fn(ArgMatches) -> MizeResult<()> + Send + Sync + 'static>(
&self,
subcmd: Command,
action: T,
) -> MizeResult<()> {
let mut subcmd_arg = ItemData::new();
subcmd_arg.set_path("_native", subcmd.native_id)?;
let action_arg = ItemData::fun(Box::new(move |args: ItemData| {
let matches = args.clone().get_path("0")?.native_take_from::<ArgMatches>()?;
action(matches)?;
Ok(ItemData::new())
}))?;
let mut args = ItemData::new();
args.set_path("0", subcmd_arg)?;
args.set_path("1", action_arg)?;
dyn_call(self.id.clone(), "subcommand", args)?;
Ok(())
}
}
pub struct Command {
native_id: u32,
}
pub struct ArgMatches {
native_id: u32,
}
impl FromItemData for ArgMatches {
fn from_itemdata(data: ItemData) -> MizeResult<Self> {
Ok(ArgMatches {
native_id: data.get_path("_native")?.value_u32()?,
})
}
}
impl Command {
pub fn new(name: &str) -> MizeResult<Command> {
let mut args = ItemData::new();
args.set_path("0", name.into_item_data())?;
let mut mize = get_global_mize()?;
let id = mize.id_new_dots("cli.Command")?;
let data = dyn_call(id, "new", args)?;
let native_id: u32 = data.get_path("_native")?.native_take()?;
Ok(Command { native_id })
}
}

View File

@@ -0,0 +1,65 @@
mod generated;
use generated::{ArgMatches, CliPart, Command};
use mize::prelude::*;
use mize::proto::MizeMessage;
fn ensure_default(rel: &str, default: ItemData) -> MizeResult<()> {
let mut mize = get_global_mize()?;
let cur = mize
.cur_eval_part()?
.ok_or_else(|| mize_err!("no cur_eval_part"))?;
let full = format!("{}.{}", cur.dot(), rel);
if mize.get_config(&full).is_ok() {
return Ok(());
}
let segs: Vec<&str> = rel.split('.').filter(|s| !s.is_empty()).collect();
let last = *segs.last().ok_or_else(|| mize_err!("empty opt path"))?;
let mut parent = cur.clone();
for seg in &segs[..segs.len() - 1] {
parent = parent.join(seg)?;
}
let original = Some(cur);
mize.set_cur_eval_part(Some(parent))?;
opt(last).default_val(default);
mize.set_cur_eval_part(original)?;
Ok(())
}
fn spacetime_unwired(_: ArgMatches) -> MizeResult<()> {
Err(mize_err!("spacetime SDK not wired yet"))
}
#[unsafe(no_mangle)]
pub extern "C" fn part_sci_timetrack() -> MizeResult<()> {
ensure_default("daily_notes_path", "".into_item_data())?;
ensure_default("profile", "c2vi".into_item_data())?;
let cli = CliPart::get("cli")?;
cli.set_command("sci")?;
for (name, alias) in [
("editActionList", "aal"),
("nextAction", "ne"),
("printCurrentAction", "cu"),
("listActions", "la"),
("listActivities", "lac"),
("editActivities", "eac"),
("timetrackExport", "te"),
] {
cli.subcommand(Command::new(name)?, spacetime_unwired)?;
cli.subcommand(Command::new(alias)?, spacetime_unwired)?;
}
let mut mize = get_global_mize()?;
mize.add_listener(
"timetrack.sub_current",
Box::new(|_msg: MizeMessage, _mize: &mut Mize| {
Err(mize_err!(
"spacetime SDK not wired yet (timetrack.sub_current)"
))
}),
)?;
Ok(())
}

20
parts/sci/Cargo.toml Normal file
View File

@@ -0,0 +1,20 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-sci"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_sci"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
chrono = { version = "0.4", default-features = false, features = ["clock"] }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

2
parts/sci/part Normal file
View File

@@ -0,0 +1,2 @@
id = "sci"
gen = [ "cli", "sci", "spacetime" ]

1119
parts/sci/spacetime/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
[package]
name = "mize-spacetime"
version = "0.1.0"
edition = "2021"
# Standalone SpacetimeDB module crate (not a mize workspace member / not a dep of sci).
[workspace]
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb = { version = "=2.6.0", features = ["unstable"] }
log = "0.4"
chrono = { version = "0.4.45", features = [], default-features = false }
chrono-tz = { version = "0.10.4", default-features = false }
url = "2.5.8"

View File

@@ -0,0 +1,394 @@
use std::collections::HashMap;
use std::time::Duration;
use chrono::{DateTime, TimeZone};
use chrono_tz::Europe;
use log::*;
use spacetimedb::SpacetimeType;
use spacetimedb::sys::raw::index_id_from_name;
use spacetimedb::{Identity, ReducerContext, ProcedureContext, Table, Timestamp, reducer, table, procedure};
use spacetimedb::http::*;
#[table(accessor = profiles, public)]
pub struct Profile {
#[primary_key]
identity: Identity,
#[unique]
username: String,
}
#[table(accessor = task_part, public)]
pub struct TaskPart {
#[primary_key]
identity: Identity,
start_of_current: Timestamp,
current: Action,
upcoming: Vec<Action>,
}
#[table(accessor = activities, public)]
pub struct Activity {
#[primary_key]
#[auto_inc]
id: u64,
name: String,
task: Option<u64>,
parents_ids: Vec<u64>,
}
#[table(accessor = tasks, public)]
pub struct Task {
#[primary_key]
#[auto_inc]
id: u64,
done: bool,
name: String,
parent: Option<u64>,
}
#[table(accessor = thoughts, public)]
pub struct Thought {
#[primary_key]
#[auto_inc]
id: u64,
text: String,
user: Identity,
#[index(btree)]
time: Timestamp,
}
#[table(accessor = action)]
#[derive(Clone)]
pub struct Action {
name: String,
activity: Option<u64>,
task: Option<u64>,
start: Option<Timestamp>,
end: Option<Timestamp>,
duration: Option<u64>,
}
impl Action {
pub fn name_only(name: String) -> Self {
Action {
name,
activity: None,
task: None,
start: None,
end: None,
duration: None,
}
}
}
#[reducer]
pub fn add_thought(ctx: &ReducerContext, thought: String) -> Result<(), String> {
ctx.db.thoughts().try_insert(Thought {
id: 0,
text: thought,
user: ctx.sender(),
time: ctx.timestamp,
})?;
Ok(())
}
#[table(accessor = log_events, public, index(accessor = log_events_index, btree(columns = [user, end])))]
pub struct LogEvent {
#[primary_key]
#[auto_inc]
id: u64,
start: Timestamp,
end: Timestamp,
action: Action,
user: Identity,
}
#[reducer(init)]
pub fn init(_ctx: &ReducerContext) {
// Called when the module is initially published
info!("hiiiiiii from ppc module");
}
// the one from ppc, hardcoded rn, as we for now only support config at run time.
// this needs to be loaded at build time. (this code runs as a wasm module on the spacetimedb server)
const PPC_AUTH_CLIENT_ID: &str = "363331597203734531";
#[reducer(client_connected)]
pub fn connect(ctx: &ReducerContext) -> Result<(), String> {
check_auth(ctx)?;
info!("hooooo");
// add user
let _ = ctx.db.profiles().try_insert(Profile {
identity: ctx.sender(),
username: ctx.sender().to_string(),
});
Ok(())
}
fn check_auth(ctx: &ReducerContext) -> Result<(), String> {
let jwt = ctx
.sender_auth()
.jwt()
.ok_or("Authentication required".to_string())?;
if jwt.issuer() != "https://auth.ppc.social" {
return Err("Invalid issuer".to_string());
}
if !jwt.audience().iter().any(|a| a == PPC_AUTH_CLIENT_ID) {
return Err("Invalid audience".to_string());
}
Ok(())
}
#[reducer(client_disconnected)]
pub fn identity_disconnected(_ctx: &ReducerContext) {
// Called everytime a client disconnects
}
#[reducer]
pub fn add_activity(ctx: &ReducerContext, name: String) {
ctx.db.activities().insert(Activity {
name,
task: None,
id: 0, // auto-increments
parents_ids: Vec::new(),
});
}
#[reducer]
pub fn set_activity_name(ctx: &ReducerContext, id: u64, name: String) {
if let Some(old_ac) = ctx.db.activities().id().find(id) {
ctx.db.activities().id().update(Activity { name, ..old_ac });
} else {
return ();
}
}
#[reducer]
pub fn update_upcoming(ctx: &ReducerContext, upcoming: Vec<Action>) {
if ctx.db.task_part().identity().find(ctx.sender()).is_none() {
ctx.db.task_part().insert(TaskPart {
upcoming,
identity: ctx.sender(),
current: Action {
name: "filler".to_string(),
activity: None,
task: None,
start: None,
end: None,
duration: None,
},
start_of_current: ctx.timestamp,
});
} else {
ctx.db.task_part().identity().update(TaskPart {
upcoming,
..ctx.db.task_part().identity().find(&ctx.sender()).unwrap()
});
}
}
#[reducer]
pub fn next_action(ctx: &ReducerContext, break_flag: bool, action: Option<Action>) -> Result<(), String> {
if let Some(mut task_part) = ctx.db.task_part().identity().find(&ctx.sender()) {
if task_part.upcoming.is_empty() {
return Err("No upcoming actions".to_string());
}
let new_action = match action {
Some(action) => action,
// when we do "ppc nextAction -b" ... we want to do a generic break "b" action.
None => if break_flag { Action::name_only("idk".to_string()) } else { task_part.upcoming.remove(0) },
};
// when break flag, push current_action again to the front of upcoming
if break_flag {
// insert generic "b" action when no one is specified
task_part.upcoming.insert(0, task_part.current.clone());
}
let start_of_old_action = task_part.start_of_current;
ctx.db.task_part().identity().update(TaskPart {
upcoming: task_part.upcoming,
identity: task_part.identity,
current: new_action.clone(),
start_of_current: ctx.timestamp,
});
// create log event
ctx.db.log_events().insert(LogEvent {
start: start_of_old_action,
end: ctx.timestamp,
action: task_part.current,
id: 0,
user: ctx.sender(),
});
Ok(())
} else {
Err("Task part data not found for this user".to_string())
}
}
#[reducer]
pub fn update_activity(ctx: &ReducerContext, activity: Activity) {
if activity.id == 0 {
ctx.db.activities().insert(activity);
} else {
ctx.db.activities().id().update(activity);
}
}
#[reducer]
pub fn remove_activity(ctx: &ReducerContext, activity: Activity) {
ctx.db.activities().delete(activity);
}
//////////////////// daily metric ///////////////////
#[table(accessor = daily_task, public)]
pub struct DailyTask {
id: i64,
name: String,
owner: Identity,
}
#[table(accessor = daily_task_log, public)]
pub struct DailyTaskLog {
daily_task_id: i64,
done: Timestamp,
}
#[procedure]
fn first_timetrack_date(ctx: &mut ProcedureContext, username: String) -> Result<String, String> {
let timestamp = ctx.try_with_tx(|tx| {
let profile = tx.db.profiles().username().find(username.clone()).ok_or("No such Profile".to_string())?;
let event = tx.db.log_events()
.log_events_index()
.filter(&profile.identity)
.nth(0)
.ok_or("Profile has no row in log_events")?;
let time = event.start
.to_chrono_date_time().map_err(|e| format!("chrono error: {e}"))?
.with_timezone(&chrono_tz::Europe::Vienna);
Ok::<String, String>(time.format("%Y-%m-%d").to_string())
})?;
Ok(timestamp)
}
#[procedure]
fn export_timetrack(ctx: &mut ProcedureContext, username: String, date: String) -> Result<String, String> {
// CEST only rn
let start = chrono::NaiveDate::parse_from_str(&date, "%Y-%m-%d").unwrap().and_hms(5, 0, 0);
let start_local = chrono_tz::Europe::Vienna.from_local_datetime(&start).unwrap();
let start_utc = start_local.with_timezone(&chrono::Utc);
let start_timestamp = Timestamp::from_micros_since_unix_epoch(start_utc.timestamp_micros());
let end = chrono::NaiveDate::parse_from_str(&date, "%Y-%m-%d").unwrap().and_hms(4, 0, 0) + Duration::from_hours(24);
let end_local = chrono_tz::Europe::Vienna.from_local_datetime(&end).unwrap();
let end_utc = end_local.with_timezone(&chrono::Utc);
let end_timestamp = Timestamp::from_micros_since_unix_epoch(end_utc.timestamp_micros());
info!("start_of_day: {}", start_timestamp);
info!("end_of_day: {}", end_timestamp);
let mut export = String::new();
//export.push_str(&format!("### actions\n"));
let profile = ctx.with_tx(|tx| tx.db.profiles().username().find(&username)).ok_or(format!("Profile with username {} not found", username))?;
let events = ctx.with_tx(|tx|
tx.db.log_events()
.log_events_index()
.filter((profile.identity, start_timestamp..end_timestamp))
);
for event in events {
let start = event.start
.to_chrono_date_time()
.map_err(|err| err.to_string())?
.with_timezone(&Europe::Vienna)
.format("%H:%M");
let end = event.end
.to_chrono_date_time()
.map_err(|err| err.to_string())?
.with_timezone(&Europe::Vienna)
.format("%H:%M");
let line = format!("{} to {} ... {}\n", start, end, event.action.name);
export.push_str(line.as_str());
}
return Ok(export); // don't do thoughts right now
export.push_str("\n");
export.push_str("## thoughts\n");
let thoughts = ctx.with_tx(|tx| {
tx.db.thoughts().time().filter(start_timestamp..end_timestamp)
});
for thought in thoughts {
let line = format!("- {}\n\n", thought.text);
export.push_str(line.as_str());
}
Ok(export)
}
#[procedure]
pub fn migrate(ctx: &mut ProcedureContext) -> String {
info!("migrate procedure called");
//let msg = "Nothing to migrate".to_string();
let msg: Result<String, String> = ctx.try_with_tx(|tx| {
let mut out = String::new();
let mut count = 0;
//for event in tx.db.log_events_tmp().iter() {
//count += 1;
//}
out.push_str(format!("Migrated {} rows................", count).as_str());
Ok(out)
});
return match msg {
Ok(msg) => msg,
Err(err) => err,
};
}
#[procedure]
pub fn test(ctx: &mut ProcedureContext) -> String {
let mut s = String::from("hoooooooooooooooooooooo\n");
let a: String = ctx.with_tx(|_tx| {
let s = String::new();
return s;
});
s.push_str(&format!("{}\n\n", a));
s.push_str("\n");
s.push_str(&format!("sender: {}", ctx.sender()));
s
}
#[handler]
pub fn test_handler(ctx: &mut HandlerContext, _req: Request<Body>) -> Response<Body> {
let res = ctx.try_with_tx(|tx| {
let mut s = String::new();
for a in tx.db.profiles().iter() {
s.push_str(&format!("{} ... {}\n", a.identity, a.username));
}
s.push_str(&format!("sender: {}", tx.sender()));
return Ok(s);
});
return Response::new(Body::from_bytes(match res {
Ok(body) => body,
Err(e) => e,
}));
}
#[spacetimedb::http::router]
fn router() -> Router {
Router::new()
.get("/test", test_handler)
}

View File

@@ -0,0 +1,73 @@
use mize::prelude::*;
pub struct CliPart {
id: MizeId,
}
impl MizePart for CliPart {
fn id(&self) -> MizeResult<MizeId> {
Ok(self.id.clone())
}
}
impl CliPart {
pub fn get(name: &str) -> MizeResult<CliPart> {
let mut mize = get_global_mize()?;
let part = mize.get_part(name)?;
Ok(CliPart { id: part.id()? })
}
pub fn set_command(&self, name: &str) -> MizeResult<()> {
let mut args = ItemData::new();
args.set_path("0", name.into_item_data())?;
dyn_call(self.id.clone(), "set_command", args)?;
Ok(())
}
pub fn subcommand<T: Fn(ArgMatches) -> MizeResult<()> + Send + Sync + 'static>(
&self,
subcmd: Command,
action: T,
) -> MizeResult<()> {
let mut subcmd_arg = ItemData::new();
subcmd_arg.set_path("_native", subcmd.native_id)?;
let action_arg = ItemData::fun(Box::new(move |args: ItemData| {
let matches = args.clone().get_path("0")?.native_take_from::<ArgMatches>()?;
action(matches)?;
Ok(ItemData::new())
}))?;
let mut args = ItemData::new();
args.set_path("0", subcmd_arg)?;
args.set_path("1", action_arg)?;
dyn_call(self.id.clone(), "subcommand", args)?;
Ok(())
}
}
pub struct Command {
native_id: u32,
}
pub struct ArgMatches {
native_id: u32,
}
impl FromItemData for ArgMatches {
fn from_itemdata(data: ItemData) -> MizeResult<Self> {
Ok(ArgMatches {
native_id: data.get_path("_native")?.value_u32()?,
})
}
}
impl Command {
pub fn new(name: &str) -> MizeResult<Command> {
let mut args = ItemData::new();
args.set_path("0", name.into_item_data())?;
let mut mize = get_global_mize()?;
let id = mize.id_new_dots("cli.Command")?;
let data = dyn_call(id, "new", args)?;
let native_id: u32 = data.get_path("_native")?.native_take()?;
Ok(Command { native_id })
}
}

37
parts/sci/src/lib.rs Normal file
View File

@@ -0,0 +1,37 @@
mod generated;
use generated::{ArgMatches, CliPart, Command};
use mize::prelude::*;
#[unsafe(no_mangle)]
pub extern "C" fn part_sci() -> MizeResult<()> {
load_part_at("priv.spacetime", "spacetime")?;
let cli = CliPart::get("cli")?;
cli.set_command("sci")?;
cli.subcommand(Command::new("inputThought")?, input_thought)?;
cli.subcommand(Command::new("ti")?, input_thought)?;
Ok(())
}
fn input_thought(_matches: ArgMatches) -> MizeResult<()> {
let tmp_file = std::env::temp_dir().join(format!(
"sc2-input-thought-{}.txt",
chrono::Utc::now().timestamp()
));
let status = std::process::Command::new("nvim")
.arg(&tmp_file)
.status()
.map_err(|e| mize_err!("failed to spawn nvim: {}", e))?;
if !status.success() {
return Err(mize_err!("nvim exited with {}", status));
}
let _content = 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);
// spacetime SDK not wired yet — reducer add_thought comes later
Err(mize_err!("spacetime SDK not wired yet (add_thought)"))
}

View File

@@ -0,0 +1,19 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-spacetime"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_spacetime"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

View File

@@ -0,0 +1,9 @@
use mize::prelude::*;
/// Stub SpacetimeDB client part.
/// SDK wiring (connect with auth.client.get_id_token) comes later; dependents may fail until then.
#[unsafe(no_mangle)]
pub extern "C" fn part_spacetime() -> MizeResult<()> {
opt("url");
Ok(())
}