From 97072863750fd50e2e30f989acf1f114d24d6fae Mon Sep 17 00:00:00 2001 From: Sebastian Moser Date: Wed, 4 Nov 2026 20:24:14 +0100 Subject: [PATCH] updated README and a bunch of other stuff --- AGENTS.md | 77 +- Cargo.lock | 241 +++- README.md | 47 +- USING-MIZE-FOR-AGENTS.md | 72 ++ apps/os/src/cli.rs | 19 +- apps/os/src/main.rs | 8 +- core/src/c_api.rs | 254 +++- core/src/config.rs | 45 + core/src/include/mize.h | 61 + core/src/instance/mod.rs | 42 +- core/src/instance/tests.rs | 8 +- core/src/item.rs | 3 + core/src/lib.rs | 4 +- core/src/platform/os/mod.rs | 202 ++- docs/general-agent-guide.md | 57 + docs/part-system.md | 10 - docs/plans/2026-11-03-dist-publish.md | 18 + .../2026-11-04-load-part-at-instances.md | 14 + docs/software-factory.md | 0 parts/auth.client/src/lib.rs | 12 + parts/mize.dev/Cargo.toml | 2 +- parts/mize.dev/src/codegen.rs | 18 +- parts/mize.dev/src/lib.rs | 283 ++++- parts/mos.bash_config_cache/Cargo.toml | 19 + parts/mos.bash_config_cache/part | 1 + parts/mos.bash_config_cache/src/lib.rs | 89 ++ parts/ppc.server/Cargo.toml | 29 + parts/ppc.server/src/lib.rs | 176 +++ parts/ppc.server/src/pages.rs | 186 +++ parts/ppc/Cargo.toml | 19 + parts/ppc/part | 2 + parts/ppc/src/lib.rs | 7 + parts/sci.inbox/Cargo.toml | 19 + parts/sci.inbox/src/lib.rs | 6 + parts/sci.timetrack/Cargo.toml | 19 + parts/sci.timetrack/src/generated.rs | 73 ++ parts/sci.timetrack/src/lib.rs | 65 + parts/sci/Cargo.toml | 20 + parts/sci/part | 2 + parts/sci/spacetime/Cargo.lock | 1119 +++++++++++++++++ parts/sci/spacetime/Cargo.toml | 17 + parts/sci/spacetime/src/lib.rs | 394 ++++++ parts/sci/src/generated.rs | 73 ++ parts/sci/src/lib.rs | 37 + parts/spacetime/Cargo.toml | 19 + parts/spacetime/src/lib.rs | 9 + 46 files changed, 3725 insertions(+), 172 deletions(-) create mode 100644 USING-MIZE-FOR-AGENTS.md create mode 100644 core/src/include/mize.h create mode 100644 docs/general-agent-guide.md create mode 100644 docs/plans/2026-11-03-dist-publish.md create mode 100644 docs/plans/2026-11-04-load-part-at-instances.md create mode 100644 docs/software-factory.md create mode 100644 parts/mos.bash_config_cache/Cargo.toml create mode 100644 parts/mos.bash_config_cache/part create mode 100644 parts/mos.bash_config_cache/src/lib.rs create mode 100644 parts/ppc.server/Cargo.toml create mode 100644 parts/ppc.server/src/lib.rs create mode 100644 parts/ppc.server/src/pages.rs create mode 100644 parts/ppc/Cargo.toml create mode 100644 parts/ppc/part create mode 100644 parts/ppc/src/lib.rs create mode 100644 parts/sci.inbox/Cargo.toml create mode 100644 parts/sci.inbox/src/lib.rs create mode 100644 parts/sci.timetrack/Cargo.toml create mode 100644 parts/sci.timetrack/src/generated.rs create mode 100644 parts/sci.timetrack/src/lib.rs create mode 100644 parts/sci/Cargo.toml create mode 100644 parts/sci/part create mode 100644 parts/sci/spacetime/Cargo.lock create mode 100644 parts/sci/spacetime/Cargo.toml create mode 100644 parts/sci/spacetime/src/lib.rs create mode 100644 parts/sci/src/generated.rs create mode 100644 parts/sci/src/lib.rs create mode 100644 parts/spacetime/Cargo.toml create mode 100644 parts/spacetime/src/lib.rs diff --git a/AGENTS.md b/AGENTS.md index b082995..aedf970 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,74 +8,17 @@ Mize is many things. - a library for COP = Configuration Oriented Programming - 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.`. + ## Codebase Structure ``` -packages/ -├── src/ # Core Mize framework (Rust) -├── marts/ # Mize parts collection (Rust/TypeScript) -└── macros/ # Mize macros +core/ # Core Mize framework (Rust) +parts/ # Mize parts (cdylibs / crates) +macros/ # Proc 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]. diff --git a/Cargo.lock b/Cargo.lock index 0688546..19db288 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,6 +141,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "askama_escape" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df27b8d5ddb458c5fb1bbc1ce172d4a38c614a97d550b0ac89003897fb01de4" + [[package]] name = "async-stream" version = "0.3.6" @@ -304,6 +310,8 @@ dependencies = [ "http 1.5.0", "http-body 1.1.0", "http-body-util", + "hyper 1.12.0", + "hyper-util", "itoa", "matchit", "memchr", @@ -316,9 +324,11 @@ dependencies = [ "serde_path_to_error", "serde_urlencoded", "sync_wrapper 1.0.2", + "tokio", "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -337,6 +347,28 @@ dependencies = [ "sync_wrapper 1.0.2", "tower-layer", "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]] @@ -1304,6 +1336,7 @@ dependencies = [ "dioxus-html", "dioxus-logger", "dioxus-signals", + "dioxus-ssr", "dioxus-stores", "dioxus-web", "manganis", @@ -1787,6 +1820,18 @@ dependencies = [ "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]] name = "dioxus-stores" version = "0.7.10" @@ -3037,6 +3082,7 @@ dependencies = [ "http 1.5.0", "http-body 1.1.0", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -3714,6 +3760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", + "serde", ] [[package]] @@ -4043,7 +4090,7 @@ dependencies = [ ] [[package]] -name = "mize-part-dev" +name = "mize-part-mize-dev" version = "0.1.0" dependencies = [ "cmd_lib", @@ -4066,6 +4113,66 @@ dependencies = [ "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]] name = "mize_macros" version = "0.1.0" @@ -5629,7 +5736,7 @@ dependencies = [ "tokio-rustls 0.26.6", "tokio-util", "tower", - "tower-http", + "tower-http 0.6.11", "tower-service", "url", "wasm-bindgen", @@ -6911,6 +7018,23 @@ dependencies = [ "tokio", "tower-layer", "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]] @@ -6931,6 +7055,23 @@ dependencies = [ "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]] name = "tower-layer" version = "0.3.3" @@ -6943,12 +7084,102 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "tracing" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -7127,6 +7358,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" diff --git a/README.md b/README.md index d0b12a8..f032b07 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,32 @@ # 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? -- What annoys me about software these days is, that things are done in so many different ways. - - e.g: Different cloud applications for syncing data (Google Cloud, OneDrive, Dropbox) - - 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) +Mize is many things: +- a "strongly typed filesystem for the age of everyone having many devices" +- "eelevate the UNIX philosophy into the age of having multiple connected devices" +- 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. - - (just as simple bytestreams/files, organized by a folder-structure) - - and NO program stored and synced any data around in unpredictable ways. - -- 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). - -- 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._ +## things +- [MiZe Parts](https://wiki.ppc.social/u/c2vi/MiZe-Part) +- [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) +- [mize lang](https://wiki.ppc.social/u/c2vi/mize-lang) +- [Mize Software Factory](https://wiki.ppc.social/Mize-Software-Factory) +- [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) diff --git a/USING-MIZE-FOR-AGENTS.md b/USING-MIZE-FOR-AGENTS.md new file mode 100644 index 0000000..cdafdb4 --- /dev/null +++ b/USING-MIZE-FOR-AGENTS.md @@ -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 +``` diff --git a/apps/os/src/cli.rs b/apps/os/src/cli.rs index 030c8d2..71410e1 100644 --- a/apps/os/src/cli.rs +++ b/apps/os/src/cli.rs @@ -26,21 +26,16 @@ pub fn get(sub_matches: &ArgMatches, instance: &mut Mize) -> MizeResult<()> { .get_one::("id") .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"); - - println!("{}", item.as_data_full()?); - return Ok(()); - - if recurse_count > 0 { - println!("{}", item.id()); - println!("{}", item.as_data_full()?); + if raw { + print!("{}", data.value_string()?); } else { - println!("{}", item); - }; + println!("{}", data); + } - return Ok(()); + Ok(()) } pub fn call(sub_matches: &ArgMatches, instance: &mut Mize) -> MizeResult<()> { diff --git a/apps/os/src/main.rs b/apps/os/src/main.rs index 3bce146..a53004a 100644 --- a/apps/os/src/main.rs +++ b/apps/os/src/main.rs @@ -147,7 +147,13 @@ fn cli_matches() -> clap::ArgMatches { Command::new("get") .aliases(["g"]) .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( Command::new("set") diff --git a/core/src/c_api.rs b/core/src/c_api.rs index e18f65a..959b15e 100644 --- a/core/src/c_api.rs +++ b/core/src/c_api.rs @@ -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> = 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 { + 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::() { + 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)); + }); +} diff --git a/core/src/config.rs b/core/src/config.rs index 0de921a..b701312 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -98,6 +98,17 @@ impl 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 { let mize = get_global_mize().unwrap(); let mut cur_part = mize.cur_eval_part.lock().unwrap().clone().unwrap(); @@ -162,6 +173,28 @@ pub fn effect MizeResult + Send + Sync + 'static>(n ConfigOptId { id } } +pub fn apply MizeResult + 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 MizeResult + 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(); @@ -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 { unsafe { let c_str = CStr::from_ptr(name); diff --git a/core/src/include/mize.h b/core/src/include/mize.h new file mode 100644 index 0000000..c6763f7 --- /dev/null +++ b/core/src/include/mize.h @@ -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_(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 +#include + +#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 diff --git a/core/src/instance/mod.rs b/core/src/instance/mod.rs index ec7e440..d1cd14f 100644 --- a/core/src/instance/mod.rs +++ b/core/src/instance/mod.rs @@ -105,6 +105,7 @@ pub struct PartMetadata { pub opts: Vec, pub methods: Vec, pub effects: Vec, + pub applies: Vec, pub threads: Vec, pub natives: Vec, } @@ -120,7 +121,7 @@ pub struct OptMetadata { pub struct MethodMetadata { pub name: String, 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 -> ()"`. pub signature: String, /// Parsed argument type names (left of `->`). @@ -339,20 +340,23 @@ impl Mize { // end of platform specific init code // load the parts, as specified in the parts config - match self - .get_config("parts") - { - 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")?; - if part_name.trim().is_empty() || self.has_part(part_name)? { - continue; - } - self.load_part(part_name)?; + // Prefer `parts` when it is an array; otherwise `parts.load` (so `parts.workspaces` / + // `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")? + { + 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)? { + continue; } - } - Err(_err) => { - // no parts to load + self.load_part(part_name)?; } } @@ -534,7 +538,7 @@ impl Mize { pub fn load_part(&mut self, name: &str) -> MizeResult<()> { 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 { @@ -542,9 +546,8 @@ impl Mize { crate::platform::fetch_part(self, name) } - pub fn load_part_at(&mut self, name: &str, path: String) -> MizeResult<()> { - // platform specific init code - crate::platform::load_part(self, name, Some(path)) + pub fn load_part_from_path(&mut self, name: &str, path: String) -> MizeResult<()> { + crate::platform::load_part(name, Some(path), None) } // ===== generate mode accessors ===== @@ -600,6 +603,7 @@ impl Mize { }; match kind { "effect" => metadata.effects.push(m), + "apply" => metadata.applies.push(m), "thread" => metadata.threads.push(m), _ => metadata.methods.push(m), } @@ -1037,9 +1041,7 @@ impl Mize { let calling_part = self.clone().cur_eval_part()?; self.clone().set_cur_eval_part(Some(part_id)); - println!("before call"); let res = fun(args); - println!("after call"); self.clone().set_cur_eval_part(calling_part); let mut fun_values = self.fun_values.lock()?; fun_values.insert(id, fun); diff --git a/core/src/instance/tests.rs b/core/src/instance/tests.rs index 827ca4b..56b964b 100644 --- a/core/src/instance/tests.rs +++ b/core/src/instance/tests.rs @@ -48,14 +48,14 @@ fn test_get_module_hash() -> MizeResult<()> { toolchain_version = "rustc 1.78.0 (9b00956e5 2024-04-29)" "#; - instance.set_blocking("0/config/selector", ItemData::from_toml(&selector_toml)?)?; - - let (hash, selector) = get_part_hash(&mut instance, "String", ItemData::new())?; + // 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", options)?; println!("hash: {}", hash); println!("selector: {}", selector); - assert_eq!(hash, "e96edf60216688314efc263c1aa8d6ce".to_owned()); + assert_eq!(hash, "227b0216f9b7a324a621ff773c5241c9".to_owned()); Ok(()) } diff --git a/core/src/item.rs b/core/src/item.rs index 18cc830..a755b75 100644 --- a/core/src/item.rs +++ b/core/src/item.rs @@ -238,6 +238,9 @@ impl ItemData { pub fn value_string(self) -> MizeResult { match self.cbor() { 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")), } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 0bee2c8..9b2164e 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -292,9 +292,9 @@ pub mod platform { Ok(()) } - pub fn load_part(mize: &mut Mize, part_name: &str, path: Option) -> MizeResult<()> { + pub fn load_part(part_name: &str, path: Option, at_id: Option<&str>) -> MizeResult<()> { #[cfg(feature = "target-os")] { - os::load_part(mize, part_name, path)?; + os::load_part(part_name, path, at_id)?; } Ok(()) diff --git a/core/src/platform/os/mod.rs b/core/src/platform/os/mod.rs index bbe2c1a..064c390 100644 --- a/core/src/platform/os/mod.rs +++ b/core/src/platform/os/mod.rs @@ -171,12 +171,158 @@ pub fn seconds_since_modification(path: &Path) -> MizeResult { 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 { + 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 { + 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( mize: &mut Mize, name: &str, options: ItemData, ) -> 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... 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 { 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 parts_path = format!("{store_path}/parts"); let target_dir = format!("{parts_path}/{part_hash}-{part_name}"); - - let tmp_file_path_gz = format!("{parts_path}/{part_hash}-{part_name}.tar.gz"); + let tmp_file_path_gz = format!("{parts_path}/{artifact}"); + std::fs::create_dir_all(&parts_path)?; let tmp_gz_file = if !PathBuf::from(tmp_file_path_gz.clone()).exists() { 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(); if code != 200.into() { @@ -252,8 +408,8 @@ pub fn fetch_part(mize: &mut Mize, part_name: &str) -> MizeResult { part_selector ) .msg(format!( - "the http request to fetch the part returned a status code of: {}", - code + "the http request to fetch the part from '{}' returned a status code of: {}", + part_url, code ))); } @@ -309,8 +465,11 @@ fn get_part_path(mize: &mut Mize, part_name: &str, _path: Option) -> Miz //Err(mize_err!("part {part_name} not found")) } -pub fn load_part(mize: &mut Mize, part_name: &str, path: Option) -> MizeResult<()> { - let part_path = get_part_path(mize, part_name, path)?; +/// Load a part `.so` for `part_name`, registering it under `at_id` (or `part_name` if None). +/// Uses `get_global_mize()` so nested loads from inside another part's init work. +pub fn load_part(part_name: &str, path: Option, 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)? }; @@ -326,32 +485,35 @@ pub fn load_part(mize: &mut Mize, part_name: &str, path: Option) -> Mize let part_symbol_name = part_name.replace(".", "_"); - let func: libloading::Symbol< - unsafe extern "C" fn() -> MizeResult<()>, - > = unsafe { lib.get(format!("part_{}", part_symbol_name).as_bytes())? }; + let func: libloading::Symbol = + 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()?; opts.entry(part_id.clone()).or_insert_with(|| ConfigOpt { - name: part_name.to_owned(), + name: eval_name.to_owned(), val: Some(ItemData::new()), fun: None, deps: Vec::new(), }); } + let prev_eval = mize.clone().cur_eval_part()?; mize.clone().set_cur_eval_part(Some(part_id))?; - unsafe { func()? }; - mize.clone().set_cur_eval_part(None)?; - - let mut cur_eval_part = mize.cur_eval_part.lock()?; - *cur_eval_part = None; + crate::c_api::clear_err(); + unsafe { func() }; + let c_err = crate::c_api::take_err(); + mize.clone().set_cur_eval_part(prev_eval)?; // 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 std::mem::forget(lib); + if let Some(err) = c_err { + return Err(err); + } Ok(()) } diff --git a/docs/general-agent-guide.md b/docs/general-agent-guide.md new file mode 100644 index 0000000..1429079 --- /dev/null +++ b/docs/general-agent-guide.md @@ -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]. diff --git a/docs/part-system.md b/docs/part-system.md index 68131a1..acae19f 100644 --- a/docs/part-system.md +++ b/docs/part-system.md @@ -1,15 +1,5 @@ # 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. --- diff --git a/docs/plans/2026-11-03-dist-publish.md b/docs/plans/2026-11-03-dist-publish.md new file mode 100644 index 0000000..bf9a9cb --- /dev/null +++ b/docs/plans/2026-11-03-dist-publish.md @@ -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` diff --git a/docs/plans/2026-11-04-load-part-at-instances.md b/docs/plans/2026-11-04-load-part-at-instances.md new file mode 100644 index 0000000..2dfaad8 --- /dev/null +++ b/docs/plans/2026-11-04-load-part-at-instances.md @@ -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`. diff --git a/docs/software-factory.md b/docs/software-factory.md new file mode 100644 index 0000000..e69de29 diff --git a/parts/auth.client/src/lib.rs b/parts/auth.client/src/lib.rs index 4f8764f..9fb59c7 100644 --- a/parts/auth.client/src/lib.rs +++ b/parts/auth.client/src/lib.rs @@ -202,6 +202,18 @@ pub extern "C" fn part_auth_client() -> MizeResult<()> { Ok(ItemData::new()) }); + method("get_id_token", "() -> String", |_args| { + let data = get_config("self.secrets")?; + let secrets = match data.deserialize::>() { + 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()?; // start refresh tread if configured diff --git a/parts/mize.dev/Cargo.toml b/parts/mize.dev/Cargo.toml index e8b4e10..97023f8 100644 --- a/parts/mize.dev/Cargo.toml +++ b/parts/mize.dev/Cargo.toml @@ -1,7 +1,7 @@ cargo-features = ["profile-rustflags"] [package] -name = "mize-part-dev" +name = "mize-part-mize-dev" version = "0.1.0" edition = "2024" diff --git a/parts/mize.dev/src/codegen.rs b/parts/mize.dev/src/codegen.rs index efb327d..3f8ac46 100644 --- a/parts/mize.dev/src/codegen.rs +++ b/parts/mize.dev/src/codegen.rs @@ -91,7 +91,13 @@ fn generate_part_struct(out: &mut String, meta: &PartMetadata) { let _ = writeln!(out); // 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) { continue; } @@ -466,7 +472,12 @@ pub fn extract_mize_dependency(cargo_toml: &str) -> Result { /// /// `mize_dep` is the full assignment line copied from the part Cargo.toml, /// 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!( r#"[package] name = "{package_name}" @@ -479,9 +490,10 @@ path = "src/lib.rs" [dependencies] {mize_dep} -"#, +{spacetime_dep}"#, package_name = package_name, lib_name = package_name.replace('-', "_"), mize_dep = mize_dep.trim(), + spacetime_dep = spacetime_dep, ) } diff --git a/parts/mize.dev/src/lib.rs b/parts/mize.dev/src/lib.rs index a2f45e3..894c7dd 100644 --- a/parts/mize.dev/src/lib.rs +++ b/parts/mize.dev/src/lib.rs @@ -1,8 +1,12 @@ use std::fs::{create_dir_all, write}; +use std::path::{Path, PathBuf}; use mize::prelude::*; use cmd_lib::run_cmd as sh; use mize::tracing::info; 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. // as we need mize.dev to generate anything. @@ -10,8 +14,6 @@ mod generated; mod codegen; 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)] extern "C" fn part_dev() -> MizeResult<()> { let cli = generated::CliPart::get("cli")?; @@ -26,20 +28,245 @@ extern "C" fn part_dev() -> MizeResult<()> { 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, + 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 { + 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> { + 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_.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<()> { let mut mize = get_global_mize()?; let mize_src = mize.get_config("mize.src")?.value_string()?; 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![ - "x86_64-pc-windows-gnu", - "aarch64-pc-windows-gnullvm", - "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", "armv7-unknown-linux-gnueabihf", "arm-unknown-linux-gnueabi", + "x86_64-pc-windows-gnu", + "aarch64-pc-windows-gnullvm", + "aarch64-apple-darwin", "x86_64-apple-darwin", ]; @@ -50,7 +277,7 @@ pub fn publish_mize(_ : ArgMatches) -> MizeResult<()> { "x86_64-apple-darwin" => "darwin", _ => "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(); build_config.set_path("namespace", "mize.buildtime.ns")?; 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()?)?; 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)?; - 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() { 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; }; @@ -106,12 +342,16 @@ pub fn gen_bindings(_ : ArgMatches) -> MizeResult<()> { gen_parts.push(s.to_owned()); } 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 mize.set_generate_mode(true)?; mize.clear_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 { info!("loading part in generate mode: {p}"); mize.load_part(p)?; @@ -136,11 +376,36 @@ pub fn gen_bindings(_ : ArgMatches) -> MizeResult<()> { let mize_dep = codegen::extract_mize_dependency(&part_cargo) .map_err(|e| mize_err!("{e}"))?; 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) .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) .map_err(|e| mize_err!("could not write generated/src/lib.rs: {e}"))?; diff --git a/parts/mos.bash_config_cache/Cargo.toml b/parts/mos.bash_config_cache/Cargo.toml new file mode 100644 index 0000000..945cfbf --- /dev/null +++ b/parts/mos.bash_config_cache/Cargo.toml @@ -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"] diff --git a/parts/mos.bash_config_cache/part b/parts/mos.bash_config_cache/part new file mode 100644 index 0000000..01df530 --- /dev/null +++ b/parts/mos.bash_config_cache/part @@ -0,0 +1 @@ +id = "mos.bash_config_cache" diff --git a/parts/mos.bash_config_cache/src/lib.rs b/parts/mos.bash_config_cache/src/lib.rs new file mode 100644 index 0000000..4fd3547 --- /dev/null +++ b/parts/mos.bash_config_cache/src/lib.rs @@ -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 { + 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 { + 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(()) +} diff --git a/parts/ppc.server/Cargo.toml b/parts/ppc.server/Cargo.toml new file mode 100644 index 0000000..2b620d6 --- /dev/null +++ b/parts/ppc.server/Cargo.toml @@ -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"] diff --git a/parts/ppc.server/src/lib.rs b/parts/ppc.server/src/lib.rs new file mode 100644 index 0000000..54a168b --- /dev/null +++ b/parts/ppc.server/src/lib.rs @@ -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::::new()); + + let oidc_client = OidcClient::::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::)) + .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) -> impl IntoResponse { + format!("Hello {}", claims.subject().as_str()) +} + +async fn maybe_authenticated( + claims: Result, 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 { + Json(json!({ + "m.homeserver": { + "base_url": "https://matrix.ppc.social" + } + })) +} + +async fn handle_matrix_server() -> Json { + Json(json!({ + "m.server": "matrix.ppc.social:443" + })) +} diff --git a/parts/ppc.server/src/pages.rs b/parts/ppc.server/src/pages.rs new file mode 100644 index 0000000..999eeca --- /dev/null +++ b/parts/ppc.server/src/pages.rs @@ -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 { + 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!("\n\n{markup}\n")) +} + +#[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 { + render_page("People Corner", rsx! { MainPageBody {} }) +} + +pub fn mc_page() -> Html { + 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("")); + 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/")); + } +} diff --git a/parts/ppc/Cargo.toml b/parts/ppc/Cargo.toml new file mode 100644 index 0000000..3979877 --- /dev/null +++ b/parts/ppc/Cargo.toml @@ -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"] diff --git a/parts/ppc/part b/parts/ppc/part new file mode 100644 index 0000000..55cc3c2 --- /dev/null +++ b/parts/ppc/part @@ -0,0 +1,2 @@ +id = "ppc" +gen = [ "spacetime" ] diff --git a/parts/ppc/src/lib.rs b/parts/ppc/src/lib.rs new file mode 100644 index 0000000..3282dfd --- /dev/null +++ b/parts/ppc/src/lib.rs @@ -0,0 +1,7 @@ +use mize::prelude::*; + +#[unsafe(no_mangle)] +pub extern "C" fn part_ppc() -> MizeResult<()> { + load_part_at("ppc.spacetime", "spacetime")?; + Ok(()) +} diff --git a/parts/sci.inbox/Cargo.toml b/parts/sci.inbox/Cargo.toml new file mode 100644 index 0000000..1791e4b --- /dev/null +++ b/parts/sci.inbox/Cargo.toml @@ -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"] diff --git a/parts/sci.inbox/src/lib.rs b/parts/sci.inbox/src/lib.rs new file mode 100644 index 0000000..9a17c06 --- /dev/null +++ b/parts/sci.inbox/src/lib.rs @@ -0,0 +1,6 @@ +use mize::prelude::*; + +#[unsafe(no_mangle)] +pub extern "C" fn part_sci_inbox() -> MizeResult<()> { + Ok(()) +} diff --git a/parts/sci.timetrack/Cargo.toml b/parts/sci.timetrack/Cargo.toml new file mode 100644 index 0000000..d10d430 --- /dev/null +++ b/parts/sci.timetrack/Cargo.toml @@ -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"] diff --git a/parts/sci.timetrack/src/generated.rs b/parts/sci.timetrack/src/generated.rs new file mode 100644 index 0000000..e457a1f --- /dev/null +++ b/parts/sci.timetrack/src/generated.rs @@ -0,0 +1,73 @@ +use mize::prelude::*; + +pub struct CliPart { + id: MizeId, +} + +impl MizePart for CliPart { + fn id(&self) -> MizeResult { + Ok(self.id.clone()) + } +} + +impl CliPart { + pub fn get(name: &str) -> MizeResult { + 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 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::()?; + 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 { + Ok(ArgMatches { + native_id: data.get_path("_native")?.value_u32()?, + }) + } +} + +impl Command { + pub fn new(name: &str) -> MizeResult { + 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 }) + } +} diff --git a/parts/sci.timetrack/src/lib.rs b/parts/sci.timetrack/src/lib.rs new file mode 100644 index 0000000..ad186cf --- /dev/null +++ b/parts/sci.timetrack/src/lib.rs @@ -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(()) +} diff --git a/parts/sci/Cargo.toml b/parts/sci/Cargo.toml new file mode 100644 index 0000000..23d4026 --- /dev/null +++ b/parts/sci/Cargo.toml @@ -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"] diff --git a/parts/sci/part b/parts/sci/part new file mode 100644 index 0000000..5323fae --- /dev/null +++ b/parts/sci/part @@ -0,0 +1,2 @@ +id = "sci" +gen = [ "cli", "sci", "spacetime" ] diff --git a/parts/sci/spacetime/Cargo.lock b/parts/sci/spacetime/Cargo.lock new file mode 100644 index 0000000..6b81916 --- /dev/null +++ b/parts/sci/spacetime/Cargo.lock @@ -0,0 +1,1119 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "approx" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375989decec1a0fa424442bed520fa12ede43a48475ce8c7417d2c69b32b5f6b" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "num-traits", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "decorum" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "281759d3c8a14f5c3f0c49363be56810fcd7f910422f97f2db850c2920fde5cf" +dependencies = [ + "approx", + "num-traits", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ethnum" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" +dependencies = [ + "serde", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "humantime" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" + +[[package]] +name = "icu_properties" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "lean_string" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a262b6ae1dd9c2d3cf7977a816578b03bf8fb60b61545c395880f95eefc5b24" +dependencies = [ + "castaway", + "itoa", + "ryu", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mize-spacetime" +version = "0.1.0" +dependencies = [ + "chrono", + "chrono-tz", + "log", + "spacetimedb", + "url", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "second-stack" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4904c83c6e51f1b9b08bfa5a86f35a51798e8307186e6f5513852210a219c0bb" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "smallvec" +version = "1.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" + +[[package]] +name = "spacetimedb" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8037ee5a6fa7348bf0392c728e4de4a0ba6f89c967eff80b666ac3e1e8f2b612" +dependencies = [ + "anyhow", + "bytemuck", + "bytes", + "derive_more", + "getrandom 0.2.17", + "http", + "log", + "rand 0.8.8", + "scoped-tls", + "serde_json", + "spacetimedb-bindings-macro", + "spacetimedb-bindings-sys", + "spacetimedb-lib", + "spacetimedb-primitives", + "spacetimedb-query-builder", +] + +[[package]] +name = "spacetimedb-bindings-macro" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202fb591c7d18fbbd4aec24a34ad6f03e3accc41580aee0fd2f9ad0514681917" +dependencies = [ + "heck 0.4.1", + "humantime", + "proc-macro2", + "quote", + "spacetimedb-primitives", + "syn 2.0.119", +] + +[[package]] +name = "spacetimedb-bindings-sys" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60869317019bbc8fa571e5c8ff21fe72abf9e5bcb579531efdded4e93a76e827" +dependencies = [ + "spacetimedb-primitives", +] + +[[package]] +name = "spacetimedb-lib" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d05a4bef4619afe949f7eac9c3a31f06f21c5139c89920a8f4c7dd7b09c24a8" +dependencies = [ + "anyhow", + "bitflags", + "blake3", + "bytes", + "chrono", + "derive_more", + "enum-as-inner", + "hex", + "itertools", + "log", + "spacetimedb-bindings-macro", + "spacetimedb-primitives", + "spacetimedb-sats", + "thiserror", +] + +[[package]] +name = "spacetimedb-primitives" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c1e4745d32aacb71d29050f1ff6fa24fa149d939b8787f90ec28d2cac3c107b" +dependencies = [ + "bitflags", + "either", + "enum-as-inner", + "itertools", + "nohash-hasher", +] + +[[package]] +name = "spacetimedb-query-builder" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79849bd28750b3351d54f3df4b37c5454cae3c62d0184bc9855650048a80c032" +dependencies = [ + "spacetimedb-lib", +] + +[[package]] +name = "spacetimedb-sats" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ca3327e8cd735f347ef5ff08a61b9806418acdcc0394ddd35a212c0849e4295" +dependencies = [ + "anyhow", + "arrayvec", + "bitflags", + "bytemuck", + "bytes", + "chrono", + "decorum", + "derive_more", + "enum-as-inner", + "ethnum", + "hex", + "itertools", + "lean_string", + "rand 0.9.5", + "second-stack", + "sha3", + "smallvec", + "spacetimedb-bindings-macro", + "spacetimedb-primitives", + "thiserror", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3bd9d3e571c26fcce9d033b87ea5ab061b8d2ffe9b7a5675d810d68f03edf0" +dependencies = [ + "utf_types", +] + +[[package]] +name = "utf_types" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3b425286980a367278453f3b6d28cdcb134b574e5a35824a6ff5eebad50bb6" + +[[package]] +name = "uuid" +version = "1.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" +dependencies = [ + "getrandom 0.4.3", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/parts/sci/spacetime/Cargo.toml b/parts/sci/spacetime/Cargo.toml new file mode 100644 index 0000000..5a0c5d8 --- /dev/null +++ b/parts/sci/spacetime/Cargo.toml @@ -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" diff --git a/parts/sci/spacetime/src/lib.rs b/parts/sci/spacetime/src/lib.rs new file mode 100644 index 0000000..2f128f1 --- /dev/null +++ b/parts/sci/spacetime/src/lib.rs @@ -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, +} + +#[table(accessor = activities, public)] +pub struct Activity { + #[primary_key] + #[auto_inc] + id: u64, + name: String, + task: Option, + parents_ids: Vec, +} + +#[table(accessor = tasks, public)] +pub struct Task { + #[primary_key] + #[auto_inc] + id: u64, + done: bool, + name: String, + parent: Option, +} + +#[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, + task: Option, + start: Option, + end: Option, + duration: Option, +} + +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) { + 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) -> 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 { + 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::(time.format("%Y-%m-%d").to_string()) + })?; + Ok(timestamp) +} + +#[procedure] +fn export_timetrack(ctx: &mut ProcedureContext, username: String, date: String) -> Result { + // 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 = 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) -> Response { + 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) +} diff --git a/parts/sci/src/generated.rs b/parts/sci/src/generated.rs new file mode 100644 index 0000000..e457a1f --- /dev/null +++ b/parts/sci/src/generated.rs @@ -0,0 +1,73 @@ +use mize::prelude::*; + +pub struct CliPart { + id: MizeId, +} + +impl MizePart for CliPart { + fn id(&self) -> MizeResult { + Ok(self.id.clone()) + } +} + +impl CliPart { + pub fn get(name: &str) -> MizeResult { + 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 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::()?; + 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 { + Ok(ArgMatches { + native_id: data.get_path("_native")?.value_u32()?, + }) + } +} + +impl Command { + pub fn new(name: &str) -> MizeResult { + 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 }) + } +} diff --git a/parts/sci/src/lib.rs b/parts/sci/src/lib.rs new file mode 100644 index 0000000..1643e46 --- /dev/null +++ b/parts/sci/src/lib.rs @@ -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)")) +} diff --git a/parts/spacetime/Cargo.toml b/parts/spacetime/Cargo.toml new file mode 100644 index 0000000..28b0d93 --- /dev/null +++ b/parts/spacetime/Cargo.toml @@ -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"] diff --git a/parts/spacetime/src/lib.rs b/parts/spacetime/src/lib.rs new file mode 100644 index 0000000..adf132f --- /dev/null +++ b/parts/spacetime/src/lib.rs @@ -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(()) +}