Compare commits
10 Commits
8af0880e15
...
562b1abe9c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
562b1abe9c | ||
|
|
94f0a960f6 | ||
|
|
299f00c94f | ||
|
|
f1f53e3857 | ||
|
|
d9e0e81e61 | ||
|
|
9ad752c16f | ||
|
|
d7a0780095 | ||
|
|
0dc09e7618 | ||
|
|
8e7f2d1ddc | ||
|
|
b4bfedf372 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,12 +1,16 @@
|
|||||||
|
|
||||||
target
|
target
|
||||||
|
.direnv
|
||||||
|
|
||||||
# for nix
|
# for nix
|
||||||
result
|
result
|
||||||
dist
|
dist
|
||||||
rust_dist
|
rust_dist
|
||||||
|
deno_dist
|
||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
|
mize_config.toml
|
||||||
|
|
||||||
# I put testing stuff for this projekt that I don't want to push to github into a folder called gitignore
|
# I put testing stuff for this projekt that I don't want to push to github into a folder called gitignore
|
||||||
gitignore
|
gitignore
|
||||||
|
|
||||||
|
|||||||
62
AGENTS.md
Normal file
62
AGENTS.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Mize
|
||||||
|
|
||||||
|
Mize is a strongly typed "filesystem" for the age of connectivity, elevating the Unix file philosophy into modern distributed computing.
|
||||||
|
|
||||||
|
## Codebase Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/
|
||||||
|
├── mize/ # Core Mize framework (Rust)
|
||||||
|
├── marts/ # Mize parts collection (Rust/TypeScript)
|
||||||
|
├── mme/ # Mize Module Environment (Rust)
|
||||||
|
├── ppc/ # Platform-specific components
|
||||||
|
├── vic/ # Victor CLI tool
|
||||||
|
└── ac_mize_macros/# Mize macros
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Components
|
||||||
|
|
||||||
|
To understand the main components of the architecture you canr efer to `architecture.md`.
|
||||||
|
|
||||||
|
## Require clarification and plan approval before making code changes
|
||||||
|
|
||||||
|
Before making any code changes other than the changelog, 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
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### 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].
|
||||||
8094
Cargo.lock
generated
8094
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -1,10 +1,9 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
|
||||||
members = [
|
members = [
|
||||||
"packages/mize",
|
|
||||||
#"packages/vic",
|
|
||||||
"packages/ppc",
|
|
||||||
"packages/marts",
|
|
||||||
"packages/ac_mize_macros",
|
"packages/ac_mize_macros",
|
||||||
#"packages/mme",
|
"packages/marts",
|
||||||
|
"packages/mize",
|
||||||
|
"packages/ppc",
|
||||||
|
"packages/ppc/spacetimedb",
|
||||||
]
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|||||||
48
architecture.md
Normal file
48
architecture.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Source code architecture
|
||||||
|
|
||||||
|
## 1. Core Mize Framework (`packages/mize/`)
|
||||||
|
|
||||||
|
**Architecture layers:**
|
||||||
|
- `core/`: Platform-independent logic
|
||||||
|
- `config/`: Configuration management
|
||||||
|
- `error/`: Error handling
|
||||||
|
- `id/`: Unique identifier system
|
||||||
|
- `instance/`: Mize runtime instances
|
||||||
|
- `item/`: Filesystem item abstractions
|
||||||
|
- `memstore/`: In-memory storage
|
||||||
|
- `proto/`: Protocol definitions
|
||||||
|
- `types/`: Core type definitions
|
||||||
|
|
||||||
|
- `platform/`: Platform-specific implementations
|
||||||
|
- `os/`: Native OS integration
|
||||||
|
- `wasm/`: WebAssembly support
|
||||||
|
- `bare/`: Bare metal targets
|
||||||
|
- `jvm/`: Java Virtual Machine integration
|
||||||
|
|
||||||
|
### 2. Marts (`packages/marts/`)
|
||||||
|
- **Language**: Rust + TypeScript
|
||||||
|
- **Purpose**: Collection of Mize framework parts and utilities
|
||||||
|
- **Key features**:
|
||||||
|
- CLI tools
|
||||||
|
- JavaScript/TypeScript integration
|
||||||
|
- Habitica integration
|
||||||
|
- Deno-based scripting support
|
||||||
|
|
||||||
|
## 3. MME - Mize Module Environment (`packages/mme/`)
|
||||||
|
- **Language**: Rust
|
||||||
|
- **Purpose**: Module execution environment
|
||||||
|
- **Key features**:
|
||||||
|
- Cross-platform module loading
|
||||||
|
- Qt and Slint UI support
|
||||||
|
- Web view integration (tao/wry)
|
||||||
|
- Command system (comandr)
|
||||||
|
|
||||||
|
## 4. PPC - Platform Components (`packages/ppc/`)
|
||||||
|
- **Language**: Rust + TypeScript
|
||||||
|
- **Purpose**: Platform-specific implementations
|
||||||
|
- **Targets**: Obsidian plugin, OS integration
|
||||||
|
|
||||||
|
## 5. Vic - Victor CLI (`packages/vic/`)
|
||||||
|
- **Language**: Rust
|
||||||
|
- **Purpose**: Command-line interface tool
|
||||||
|
- **Features**: Build, run, test, and manage Mize applications
|
||||||
@@ -3,6 +3,5 @@
|
|||||||
"./packages/ppc/platform/obsidian",
|
"./packages/ppc/platform/obsidian",
|
||||||
"./packages/marts",
|
"./packages/marts",
|
||||||
"./packages/mize"
|
"./packages/mize"
|
||||||
],
|
]
|
||||||
"imports": {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
deno.lock
generated
11
deno.lock
generated
@@ -1,16 +1,19 @@
|
|||||||
{
|
{
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
|
"jsr:@deno/esbuild-plugin@*": "1.2.1",
|
||||||
"jsr:@deno/esbuild-plugin@^1.2.1": "1.2.1",
|
"jsr:@deno/esbuild-plugin@^1.2.1": "1.2.1",
|
||||||
"jsr:@deno/loader@~0.3.10": "0.3.12",
|
"jsr:@deno/loader@~0.3.10": "0.3.12",
|
||||||
"jsr:@std/internal@^1.0.12": "1.0.12",
|
"jsr:@std/internal@^1.0.12": "1.0.12",
|
||||||
"jsr:@std/path@^1.1.1": "1.1.4",
|
"jsr:@std/path@^1.1.1": "1.1.4",
|
||||||
"npm:@flowershow/remark-wiki-link@^3.4.0": "3.4.0",
|
"npm:@flowershow/remark-wiki-link@^3.4.0": "3.4.0",
|
||||||
"npm:builtin-modules@5": "5.0.0",
|
"npm:builtin-modules@5": "5.0.0",
|
||||||
|
"npm:esbuild@*": "0.27.3",
|
||||||
"npm:esbuild@~0.25.5": "0.25.12",
|
"npm:esbuild@~0.25.5": "0.25.12",
|
||||||
"npm:esbuild@~0.27.3": "0.27.3",
|
"npm:esbuild@~0.27.3": "0.27.3",
|
||||||
"npm:mdast@3": "3.0.0",
|
"npm:mdast@3": "3.0.0",
|
||||||
"npm:obsidian@^1.12.2": "1.12.2_@codemirror+state@6.5.0_@codemirror+view@6.38.6",
|
"npm:obsidian@^1.12.2": "1.12.3_@codemirror+state@6.5.0_@codemirror+view@6.38.6",
|
||||||
|
"npm:obsidian@^1.12.3": "1.12.3_@codemirror+state@6.5.0_@codemirror+view@6.38.6",
|
||||||
"npm:remark-frontmatter@5": "5.0.0",
|
"npm:remark-frontmatter@5": "5.0.0",
|
||||||
"npm:remark-gfm@^4.0.1": "4.0.1",
|
"npm:remark-gfm@^4.0.1": "4.0.1",
|
||||||
"npm:remark-parse@11": "11.0.0",
|
"npm:remark-parse@11": "11.0.0",
|
||||||
@@ -874,8 +877,8 @@
|
|||||||
"ms@2.1.3": {
|
"ms@2.1.3": {
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
},
|
},
|
||||||
"obsidian@1.12.2_@codemirror+state@6.5.0_@codemirror+view@6.38.6": {
|
"obsidian@1.12.3_@codemirror+state@6.5.0_@codemirror+view@6.38.6": {
|
||||||
"integrity": "sha512-DGAzpt6vo+sMDET/o8Zj26Bj2hbHrFsNUU8TtnzCyerO/OHksMFQnU9QEnPHVsOMstt/WnHXfC56j2r1syObWg==",
|
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@codemirror/state",
|
"@codemirror/state",
|
||||||
"@codemirror/view",
|
"@codemirror/view",
|
||||||
@@ -999,7 +1002,7 @@
|
|||||||
"npm:builtin-modules@5",
|
"npm:builtin-modules@5",
|
||||||
"npm:esbuild@~0.27.3",
|
"npm:esbuild@~0.27.3",
|
||||||
"npm:mdast@3",
|
"npm:mdast@3",
|
||||||
"npm:obsidian@^1.12.2",
|
"npm:obsidian@^1.12.3",
|
||||||
"npm:remark-frontmatter@5",
|
"npm:remark-frontmatter@5",
|
||||||
"npm:remark-gfm@^4.0.1",
|
"npm:remark-gfm@^4.0.1",
|
||||||
"npm:remark-parse@11",
|
"npm:remark-parse@11",
|
||||||
|
|||||||
6
packages/mize/flake.lock → flake.lock
generated
6
packages/mize/flake.lock → flake.lock
generated
@@ -72,11 +72,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770115704,
|
"lastModified": 1774709303,
|
||||||
"narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=",
|
"narHash": "sha256-D3Q07BbIA2KnTcSXIqqu9P586uWxN74zNoCH3h2ESHg=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e6eae2ee2110f3d31110d5c222cd395303343b08",
|
"rev": "8110df5ad7abf5d4c0f6fb0f8f978390e77f9685",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -212,7 +212,86 @@ in {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
default = (mizeLib.buildMizeForSystem system).modules.mize.devShell;
|
#default = (mizeLib.buildMizeForSystem system).modules.mize.devShell;
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
|
||||||
|
MIZE_BUILD_CONFIG = let
|
||||||
|
settingsFormat = pkgs.formats.toml { };
|
||||||
|
in settingsFormat.generate "mize-build-config.toml" {
|
||||||
|
config = {
|
||||||
|
namespace = "mize.buildtime.ns";
|
||||||
|
module_url = "c2vi.dev";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
shellHook = ''
|
||||||
|
mize_project_root=$(${pkgs.lib.getExe pkgs.git} rev-parse --show-toplevel)
|
||||||
|
export MIZE_CONFIG_FILES=$mize_project_root/mize_config.toml
|
||||||
|
'';
|
||||||
|
#_shell_type = "rust";
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.wasm-pack
|
||||||
|
pkgs.dioxus-cli
|
||||||
|
pkgs.spacetimedb
|
||||||
|
pkgs.deno
|
||||||
|
pkgs.pkg-config
|
||||||
|
pkgs.gobject-introspection
|
||||||
|
pkgs.gobject-introspection.dev
|
||||||
|
|
||||||
|
# thanks to: https://github.com/NixOS/nixpkgs/pull/496279/changes
|
||||||
|
(pkgs.buildWasmBindgenCli rec {
|
||||||
|
src = pkgs.fetchCrate {
|
||||||
|
pname = "wasm-bindgen-cli";
|
||||||
|
version = "0.2.114";
|
||||||
|
hash = "sha256-xrCym+rFY6EUQFWyWl6OPA+LtftpUAE5pIaElAIVqW0=";
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoDeps = pkgs.rustPlatform.fetchCargoVendor {
|
||||||
|
inherit src;
|
||||||
|
inherit (src) pname version;
|
||||||
|
hash = "sha256-Z8+dUXPQq7S+Q7DWNr2Y9d8GMuEdSnq00quUR0wDNPM=";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
(fenix.packages.${system}.combine [
|
||||||
|
fenix.packages.${system}.latest.toolchain
|
||||||
|
fenix.packages.${system}.targets.wasm32-unknown-unknown.latest.toolchain
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
at-spi2-atk
|
||||||
|
atkmm
|
||||||
|
cairo
|
||||||
|
gdk-pixbuf
|
||||||
|
glib
|
||||||
|
gtk3
|
||||||
|
harfbuzz
|
||||||
|
librsvg
|
||||||
|
libsoup_3
|
||||||
|
pango
|
||||||
|
webkitgtk_4_1
|
||||||
|
openssl
|
||||||
|
wasm-bindgen-cli
|
||||||
|
lld_20
|
||||||
|
xdotool
|
||||||
|
];
|
||||||
|
|
||||||
|
LD_LIBRARY_PATH = with pkgs; pkgs.lib.makeLibraryPath [
|
||||||
|
xdotool
|
||||||
|
webkitgtk_4_1
|
||||||
|
pango
|
||||||
|
openssl
|
||||||
|
at-spi2-atk
|
||||||
|
atkmm
|
||||||
|
cairo
|
||||||
|
gdk-pixbuf
|
||||||
|
glib
|
||||||
|
gtk3
|
||||||
|
harfbuzz
|
||||||
|
librsvg
|
||||||
|
libsoup_3
|
||||||
|
];
|
||||||
|
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
}) // {
|
}) // {
|
||||||
@@ -267,12 +267,34 @@ rec {
|
|||||||
################# dev Shels ###################
|
################# dev Shels ###################
|
||||||
|
|
||||||
mkMizeRustShell = attrs: mkMizeModuleShell (attrs // {
|
mkMizeRustShell = attrs: mkMizeModuleShell (attrs // {
|
||||||
|
shellHook = ''
|
||||||
|
mize_project_root=$(${lib.getExe pkgs.git} rev-parse --show-toplevel)
|
||||||
|
export MIZE_CONFIG_FILES=$mize_project_root/mize_config.toml
|
||||||
|
'';
|
||||||
#_shell_type = "rust";
|
#_shell_type = "rust";
|
||||||
nativeBuildInputs = attrs.nativeBuildInputs or [] ++ [
|
nativeBuildInputs = attrs.nativeBuildInputs or [] ++ [
|
||||||
pkgs.wasm-pack
|
pkgs.wasm-pack
|
||||||
|
pkgs.dioxus-cli
|
||||||
|
pkgs.spacetimedb
|
||||||
pkgs.deno
|
pkgs.deno
|
||||||
pkgs.pkg-config
|
pkgs.pkg-config
|
||||||
pkgs.wasm-bindgen-cli
|
pkgs.gobject-introspection
|
||||||
|
pkgs.gobject-introspection.dev
|
||||||
|
|
||||||
|
# thanks to: https://github.com/NixOS/nixpkgs/pull/496279/changes
|
||||||
|
(pkgs.buildWasmBindgenCli rec {
|
||||||
|
src = pkgs.fetchCrate {
|
||||||
|
pname = "wasm-bindgen-cli";
|
||||||
|
version = "0.2.114";
|
||||||
|
hash = "sha256-xrCym+rFY6EUQFWyWl6OPA+LtftpUAE5pIaElAIVqW0=";
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoDeps = pkgs.rustPlatform.fetchCargoVendor {
|
||||||
|
inherit src;
|
||||||
|
inherit (src) pname version;
|
||||||
|
hash = "sha256-Z8+dUXPQq7S+Q7DWNr2Y9d8GMuEdSnq00quUR0wDNPM=";
|
||||||
|
};
|
||||||
|
})
|
||||||
(fenix.packages."x86_64-linux".combine [
|
(fenix.packages."x86_64-linux".combine [
|
||||||
fenix.packages."x86_64-linux".latest.toolchain
|
fenix.packages."x86_64-linux".latest.toolchain
|
||||||
fenix.packages."x86_64-linux".targets.wasm32-unknown-unknown.latest.toolchain
|
fenix.packages."x86_64-linux".targets.wasm32-unknown-unknown.latest.toolchain
|
||||||
@@ -293,8 +315,38 @@ rec {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
buildInputs = [
|
buildInputs = with pkgs; [
|
||||||
pkgsCross.openssl
|
at-spi2-atk
|
||||||
|
atkmm
|
||||||
|
cairo
|
||||||
|
gdk-pixbuf
|
||||||
|
glib
|
||||||
|
gtk3
|
||||||
|
harfbuzz
|
||||||
|
librsvg
|
||||||
|
libsoup_3
|
||||||
|
pango
|
||||||
|
webkitgtk_4_1
|
||||||
|
openssl
|
||||||
|
wasm-bindgen-cli
|
||||||
|
lld_20
|
||||||
|
xdotool
|
||||||
|
];
|
||||||
|
|
||||||
|
LD_LIBRARY_PATH = with pkgs; pkgs.lib.makeLibraryPath [
|
||||||
|
xdotool
|
||||||
|
webkitgtk_4_1
|
||||||
|
pango
|
||||||
|
openssl
|
||||||
|
at-spi2-atk
|
||||||
|
atkmm
|
||||||
|
cairo
|
||||||
|
gdk-pixbuf
|
||||||
|
glib
|
||||||
|
gtk3
|
||||||
|
harfbuzz
|
||||||
|
librsvg
|
||||||
|
libsoup_3
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
55
md
55
md
@@ -1,5 +1,56 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
if [[ "$1" == "dob" ]]; then
|
MIZE_SRC="/home/me/work/mize" # hardcoded for now
|
||||||
|
RSYNC_EXCLUDES="--exclude=.git --exclude=node_modules --exclude=target --exclude=mize_config.toml --exclude=rust_dist --exclude=deno_dist --exclude=dist --exclude=result --exclude=pkg"
|
||||||
|
|
||||||
fi
|
function main {
|
||||||
|
if [[ "$1" == "dlu" ]]; then
|
||||||
|
deploy_lush
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$1" == "dma" ]]; then
|
||||||
|
deploy_main_website
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$1" == "dmo" ]]; then
|
||||||
|
deploy_main_obsidian
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function deploy_lush {
|
||||||
|
echo "c2vi: Deploying to lush..."
|
||||||
|
|
||||||
|
# Copy files to lush
|
||||||
|
rsync -r -u -v --delete --mkpath $RSYNC_EXCLUDES $MIZE_SRC/* lush:here/mize
|
||||||
|
|
||||||
|
# compile
|
||||||
|
ssh lush nix develop /home/me/here/mize --impure -v -L -c sh -c "cargo build --release --manifest-path /home/me/here/mize/packages/ppc/Cargo.toml"
|
||||||
|
|
||||||
|
# restart service
|
||||||
|
ssh lush "sudo systemctl restart ppc"
|
||||||
|
}
|
||||||
|
|
||||||
|
function deploy_main_website {
|
||||||
|
echo "c2vi: Deploying website to fes"
|
||||||
|
|
||||||
|
# compile
|
||||||
|
cargo build --release --no-default-features --features server --manifest-path $MIZE_SRC/packages/ppc/Cargo.toml
|
||||||
|
|
||||||
|
# upload
|
||||||
|
scp $MIZE_SRC/target/release/ppc fes:here
|
||||||
|
|
||||||
|
# restart service
|
||||||
|
ssh fe "sudo systemctl restart ppc"
|
||||||
|
}
|
||||||
|
|
||||||
|
function deploy_main_obsidian {
|
||||||
|
echo "c2vi: Deploying to local main obsidian vault..."
|
||||||
|
|
||||||
|
deno run --config $MIZE_SRC/packages/ppc/platform/obsidian/deno.json build
|
||||||
|
|
||||||
|
cp -r $MIZE_SRC/packages/ppc/platform/obsidian/dist/* ~/work/things/.obsidian/plugins/ppc/
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|||||||
@@ -5,17 +5,28 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.59", optional = true }
|
clap = { version = "4.5.59", optional = true, features = ["string"] }
|
||||||
deno_core = { version = "0.385.0", optional = true }
|
|
||||||
flume = "0.12.0"
|
flume = "0.12.0"
|
||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
mize = { path = "../mize/", default-features = false }
|
mize = { path = "../mize/", default-features = false }
|
||||||
tokio = "1.49.0"
|
tokio = "1.49.0"
|
||||||
|
# https://github.com/rscarson/rustyscript/pull/415
|
||||||
|
rustyscript = { git = 'https://github.com/rscarson/rustyscript', rev = 'e39529eac2c5d37cfcbb7d7d63da7264d7a5afdb', optional = true }
|
||||||
|
#deno_core = { version = "0.380.1", optional = true }
|
||||||
|
chrono = "0.4.44"
|
||||||
|
reqwest = { version = "0.13.2", features = ["blocking", "json"] }
|
||||||
|
serde = "1.0.228"
|
||||||
|
serde_json = "1.0.149"
|
||||||
|
rusqlite = { version = "0.31", features = ["bundled"], optional = true }
|
||||||
|
dioxus = { version = "0.7.1", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["target-os"]
|
default = ["target-os", "desktop", "server"]
|
||||||
target-os = [ "clap", "mize/target-os", "deno_core" ]
|
target-os = [ "clap", "mize/target-os", "rustyscript", "rusqlite", "dioxus" ]
|
||||||
target-wasm = [ "mize/target-wasm" ]
|
desktop = ["dioxus/desktop"]
|
||||||
|
server = []
|
||||||
|
target-js = [ "mize/target-js" ]
|
||||||
|
clap = ["dep:clap"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
path = "lib.rs"
|
path = "lib.rs"
|
||||||
|
|||||||
9
packages/marts/c2vi/dashboard.rs
Normal file
9
packages/marts/c2vi/dashboard.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
pub fn main() -> Element {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
h1 { "my dashbaord" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
431
packages/marts/c2vi/deno.ts
Normal file
431
packages/marts/c2vi/deno.ts
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
import { createClient } from "@libsql/client";
|
||||||
|
import { drizzle } from "drizzle-orm/libsql";
|
||||||
|
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import os from "node:os";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
|
||||||
|
export async function init(ppc: PPC) {
|
||||||
|
ppc.c2vi = new C2vi(ppc);
|
||||||
|
|
||||||
|
ppc.cli
|
||||||
|
.command("bed", "command to run when c2vi goes to bed")
|
||||||
|
.action(async () => {
|
||||||
|
await save_habitica_dailies(ppc);
|
||||||
|
await save_habitica_logs(ppc);
|
||||||
|
});
|
||||||
|
|
||||||
|
ppc.cli
|
||||||
|
.command("clearTodos", "command to run when c2vi goes to bed")
|
||||||
|
.alias("clt")
|
||||||
|
.action(async () => {
|
||||||
|
const todos = await ppc.habitica.get_tasks();
|
||||||
|
for (const todo of todos) {
|
||||||
|
await ppc.habitica.delete_task(todo.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ppc.cli
|
||||||
|
.command("listTodos", "command to run when c2vi goes to bed")
|
||||||
|
.alias("dut")
|
||||||
|
.action(async () => {
|
||||||
|
const todos = await ppc.habitica.get_tasks();
|
||||||
|
for (const todo of todos) {
|
||||||
|
console.log(todo.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ppc.cli
|
||||||
|
.command("buyHealthPotion", "command to run when c2vi goes to bed")
|
||||||
|
.alias("buyh")
|
||||||
|
.action(async () => {
|
||||||
|
await ppc.habitica.api_request("POST", "/user/buy-health-potion");
|
||||||
|
});
|
||||||
|
|
||||||
|
ppc.cli
|
||||||
|
.command("addActionItems", "command to run when c2vi goes to bed")
|
||||||
|
.alias("aal")
|
||||||
|
.action(async () => {
|
||||||
|
await add_action_items(ppc);
|
||||||
|
});
|
||||||
|
|
||||||
|
ppc.cli
|
||||||
|
.command("test", "a command to run some test code")
|
||||||
|
.alias("t")
|
||||||
|
.action(async () => {});
|
||||||
|
|
||||||
|
ppc.cli
|
||||||
|
.command("skip [num]", "command to skip a habitica todo")
|
||||||
|
.alias("sk")
|
||||||
|
.action((num = 1) => {
|
||||||
|
skip_habitica_todo(ppc, parseInt(num));
|
||||||
|
});
|
||||||
|
|
||||||
|
ppc.cli.command("dumpLog", "dump the complete habitica log").action(() => {
|
||||||
|
dump_habitica_logs(ppc);
|
||||||
|
});
|
||||||
|
|
||||||
|
ppc.cli
|
||||||
|
.command("listDailies", "list all dailies")
|
||||||
|
.action(async (folder, options) => {
|
||||||
|
const allDailies = await ppc.c2vi.db.select().from(dailiesTable).all();
|
||||||
|
for (const task of allDailies) {
|
||||||
|
console.log(task.id + ": " + task.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ppc.cli
|
||||||
|
.command("printTask <id>", "print a specific todo")
|
||||||
|
.action(async (id) => {
|
||||||
|
const data = await ppc.habitica.api_request("GET", `/tasks/${id}`, {});
|
||||||
|
console.log(data.history);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class C2vi {
|
||||||
|
[key: string]: any;
|
||||||
|
|
||||||
|
constructor(ppc: PPC) {
|
||||||
|
this.ppc = ppc;
|
||||||
|
this.db = drizzle(
|
||||||
|
createClient({
|
||||||
|
url: "file://" + ppc.config.local_storage_path + "/data.db",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(d) {
|
||||||
|
const pad = (n) => n.toString().padStart(2, "0");
|
||||||
|
|
||||||
|
// Custom YYYY-MM-DD format
|
||||||
|
const formatted = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= SCHEMA DEFINITION =============
|
||||||
|
const dailiesTable = sqliteTable("habitica_dailies", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
text: text("text").notNull(),
|
||||||
|
notes: text("notes"),
|
||||||
|
priority: text("priority"), // 0.1, 1, 1.5, 2
|
||||||
|
frequency: text("frequency"), // 'daily', 'weekly', etc.
|
||||||
|
});
|
||||||
|
const habiticaLogTable = sqliteTable("habitica_log", {
|
||||||
|
date: text("date").primaryKey(),
|
||||||
|
data: text("data").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function save_habitica_dailies(ppc) {
|
||||||
|
console.log("######### cli bed cmd #########");
|
||||||
|
await ppc.c2vi.db.run(
|
||||||
|
"CREATE TABLE IF NOT EXISTS habitica_dailies (id TEXT PRIMARY KEY, text TEXT NOT NULL, notes TEXT, priority TEXT, frequency TEXT)",
|
||||||
|
);
|
||||||
|
await ppc.c2vi.db.run(
|
||||||
|
"CREATE TABLE IF NOT EXISTS habitica_log (date TEXT PRIMARY KEY, data TEXT NOT NULL)",
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============= DATABASE INITIALIZATION =============
|
||||||
|
if (!ppc.config.habitica.user_id || !ppc.config.habitica.api_token) {
|
||||||
|
console.error(
|
||||||
|
"Error: HABITICA_USER_ID and HABITICA_API_TOKEN env vars required",
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Get these from Habitica Settings > API and configure them in the ppc.config",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dailies = await ppc.habitica.get_tasks("dailys");
|
||||||
|
|
||||||
|
await writeDailiesToDatabase(dailies, ppc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dump_habitica_logs(ppc) {
|
||||||
|
const entries = await ppc.c2vi.db.select().from(habiticaLogTable).all();
|
||||||
|
for (const entry of entries) {
|
||||||
|
console.log(entry.date + ": ", JSON.parse(entry.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function skip_habitica_todo(ppc, num) {
|
||||||
|
await ppc.c2vi.db.run(
|
||||||
|
"CREATE TABLE IF NOT EXISTS habitica_dailies (id TEXT PRIMARY KEY, text TEXT NOT NULL, notes TEXT, priority TEXT, frequency TEXT)",
|
||||||
|
);
|
||||||
|
await ppc.c2vi.db.run(
|
||||||
|
"CREATE TABLE IF NOT EXISTS habitica_log (date TEXT PRIMARY KEY, data TEXT NOT NULL)",
|
||||||
|
);
|
||||||
|
|
||||||
|
const todos = await ppc.habitica.get_tasks("todos");
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
const save_for_yesterday = date.getHours() < 12;
|
||||||
|
if (save_for_yesterday) {
|
||||||
|
console.log(
|
||||||
|
"Saving logs for yesterday!!!! because it is " +
|
||||||
|
date.getHours() +
|
||||||
|
"hours of the day",
|
||||||
|
);
|
||||||
|
date.setDate(date.getDate() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ppc.c2vi.db
|
||||||
|
.select()
|
||||||
|
.from(habiticaLogTable)
|
||||||
|
.where(eq(habiticaLogTable.date, formatDate(date)));
|
||||||
|
|
||||||
|
const data = result[0]
|
||||||
|
? JSON.parse(result[0].data)
|
||||||
|
: {
|
||||||
|
dailies_done: [],
|
||||||
|
dailies_skipped: [],
|
||||||
|
todos_done: [],
|
||||||
|
todos_skipped: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < num; i++) {
|
||||||
|
const todo = todos[i];
|
||||||
|
if (!todo) continue;
|
||||||
|
await ppc.habitica.delete_task(todo.id);
|
||||||
|
await incrementHabit(ppc);
|
||||||
|
|
||||||
|
data.todos_skipped.push({
|
||||||
|
id: todo.id,
|
||||||
|
text: todo.text,
|
||||||
|
notes: todo.notes || "",
|
||||||
|
checklist: todo.checklist || [],
|
||||||
|
tags: todo.tags || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// saving to db
|
||||||
|
await ppc.c2vi.db
|
||||||
|
.insert(habiticaLogTable)
|
||||||
|
.values({ date: formatDate(date), data: JSON.stringify(data) })
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: habiticaLogTable.date,
|
||||||
|
set: {
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save_habitica_logs(ppc) {
|
||||||
|
const date = new Date();
|
||||||
|
const save_for_yesterday = date.getHours() < 12;
|
||||||
|
if (save_for_yesterday) {
|
||||||
|
console.log(
|
||||||
|
"Saving logs for yesterday!!!! because it is " +
|
||||||
|
date.getHours() +
|
||||||
|
"hours of the day",
|
||||||
|
);
|
||||||
|
date.setDate(date.getDate() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ppc.c2vi.db
|
||||||
|
.select()
|
||||||
|
.from(habiticaLogTable)
|
||||||
|
.where(eq(habiticaLogTable.date, formatDate(date)));
|
||||||
|
|
||||||
|
const data = result[0]
|
||||||
|
? JSON.parse(result[0].data)
|
||||||
|
: {
|
||||||
|
dailies_done: [],
|
||||||
|
dailies_skipped: [],
|
||||||
|
todos_done: [],
|
||||||
|
todos_skipped: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// dailies
|
||||||
|
const dailies = await ppc.habitica.get_tasks("dailys");
|
||||||
|
for (const daily of dailies) {
|
||||||
|
let completed = daily.completed;
|
||||||
|
let isDue = daily.isDue;
|
||||||
|
if (save_for_yesterday) {
|
||||||
|
const yesterday = daily.history.reduce((prev, current) => {
|
||||||
|
return prev.date > current.date ? prev : current;
|
||||||
|
});
|
||||||
|
completed = yesterday.completed;
|
||||||
|
isDue = yesterday.isDue;
|
||||||
|
}
|
||||||
|
if (!isDue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (completed) {
|
||||||
|
data.dailies_done.push(daily.id);
|
||||||
|
console.log("daily-done: " + daily.text);
|
||||||
|
} else {
|
||||||
|
data.dailies_skipped.push(daily.id);
|
||||||
|
console.log("daily-skipped: " + daily.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todos done
|
||||||
|
const todos = await ppc.habitica.get_tasks("completedTodos");
|
||||||
|
for (const todo of todos) {
|
||||||
|
const dateCompleted = new Date(todo.dateCompleted);
|
||||||
|
|
||||||
|
// check if this todo is already saved in yesterday's log entry
|
||||||
|
if (save_for_yesterday) {
|
||||||
|
// get yesterday log entry
|
||||||
|
const result = await ppc.c2vi.db
|
||||||
|
.select()
|
||||||
|
.from(habiticaLogTable)
|
||||||
|
.where(eq(habiticaLogTable.date, formatDate(date)));
|
||||||
|
|
||||||
|
const data = result[0]
|
||||||
|
? JSON.parse(result[0].data)
|
||||||
|
: {
|
||||||
|
dailies_done: [],
|
||||||
|
dailies_skipped: [],
|
||||||
|
todos_done: [],
|
||||||
|
todos_skipped: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// check if this todo is already saved in yesterday's log entry
|
||||||
|
const todoIndex = data.todos_done.findIndex((t) => t.id === todo.id);
|
||||||
|
if (todoIndex !== -1) {
|
||||||
|
continue; // skipp saving this todo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateCompleted.toDateString() == date.toDateString()) {
|
||||||
|
console.log("todo-done:", todo.text);
|
||||||
|
data.todos_done.push({
|
||||||
|
id: todo.id,
|
||||||
|
text: todo.text,
|
||||||
|
notes: todo.notes || "",
|
||||||
|
checklist: todo.checklist || [],
|
||||||
|
tags: todo.tags || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// we save_for_yesterday but the todo was completed today... also save that for this day
|
||||||
|
if (
|
||||||
|
save_for_yesterday &&
|
||||||
|
dateCompleted.toDateString() == new Date().toDateString()
|
||||||
|
) {
|
||||||
|
console.log("todo-done:", todo.text);
|
||||||
|
data.todos_done.push({
|
||||||
|
id: todo.id,
|
||||||
|
text: todo.text,
|
||||||
|
notes: todo.notes || "",
|
||||||
|
checklist: todo.checklist || [],
|
||||||
|
tags: todo.tags || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// saving to db
|
||||||
|
await ppc.c2vi.db
|
||||||
|
.insert(habiticaLogTable)
|
||||||
|
.values({ date: formatDate(date), data: JSON.stringify(data) })
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: habiticaLogTable.date,
|
||||||
|
set: {
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeDailiesToDatabase(dailies, ppc) {
|
||||||
|
console.log(`Syncing ${dailies.length} dailies to database...`);
|
||||||
|
|
||||||
|
const records = dailies.map((daily) => ({
|
||||||
|
id: daily.id,
|
||||||
|
text: daily.text,
|
||||||
|
notes: daily.notes || "",
|
||||||
|
priority: daily.priority?.toString() || null,
|
||||||
|
frequency: daily.frequency || "daily",
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function upsertMany(records) {
|
||||||
|
for (const record of records) {
|
||||||
|
await ppc.c2vi.db
|
||||||
|
.insert(dailiesTable)
|
||||||
|
.values(record)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: dailiesTable.id,
|
||||||
|
set: {
|
||||||
|
text: record.text,
|
||||||
|
notes: record.notes,
|
||||||
|
priority: record.priority,
|
||||||
|
frequency: record.frequency,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await upsertMany(records);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function incrementHabit(ppc) {
|
||||||
|
const habits = await ppc.habitica.get_tasks("habits");
|
||||||
|
let id = "";
|
||||||
|
for (const habit of habits) {
|
||||||
|
if ((habit.text = "skipped a planned task")) {
|
||||||
|
id = habit.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await ppc.habitica.api_request("POST", `tasks/${id}/score/down`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function add_action_items(ppc: PPC) {
|
||||||
|
let task_ids = [];
|
||||||
|
|
||||||
|
const tmpFile = path.join(os.tmpdir(), `habitica-tasks-${Date.now()}.md`);
|
||||||
|
const initialContent = `
|
||||||
|
gu 10min TIMER
|
||||||
|
uw 15min FROG
|
||||||
|
b coffee/tee/kakau
|
||||||
|
uw FROG
|
||||||
|
b breakfast, brush teeth, lüften
|
||||||
|
uw FROG
|
||||||
|
|
||||||
|
lunch
|
||||||
|
|
||||||
|
uw 15min TIMER of next days FROG
|
||||||
|
fw
|
||||||
|
dt 30min TIMER
|
||||||
|
|
||||||
|
fun 20:00 (smth that can be stopped easily)
|
||||||
|
se 21:40
|
||||||
|
read
|
||||||
|
bed 22:10
|
||||||
|
`;
|
||||||
|
|
||||||
|
fs.writeFileSync(tmpFile, initialContent.trim());
|
||||||
|
|
||||||
|
spawnSync("nvim", [tmpFile], { stdio: "inherit" });
|
||||||
|
|
||||||
|
// read file
|
||||||
|
const listStr = fs.readFileSync(tmpFile, "utf8");
|
||||||
|
|
||||||
|
// delete file
|
||||||
|
fs.unlinkSync(tmpFile);
|
||||||
|
|
||||||
|
// Simple parser: filter out empty lines
|
||||||
|
const tasks = listStr
|
||||||
|
.split("\n")
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter((t) => t.length > 0);
|
||||||
|
|
||||||
|
// 5. Add tasks to Habitica
|
||||||
|
for (const todoText of tasks) {
|
||||||
|
const data = await ppc.habitica.api_request(
|
||||||
|
"POST",
|
||||||
|
"tasks/user",
|
||||||
|
{},
|
||||||
|
{ text: todoText, type: "todo" },
|
||||||
|
);
|
||||||
|
task_ids.push(data.id);
|
||||||
|
console.log(`Added to-do: '${todoText}' with id '${data.id}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Move tasks to bottom
|
||||||
|
for (const taskId of task_ids) {
|
||||||
|
await ppc.habitica.api_request("POST", `tasks/${taskId}/move/to/-1`);
|
||||||
|
}
|
||||||
|
}
|
||||||
607
packages/marts/c2vi/mod.rs
Normal file
607
packages/marts/c2vi/mod.rs
Normal file
@@ -0,0 +1,607 @@
|
|||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use chrono::{Timelike, Utc};
|
||||||
|
use clap::{Arg, Command};
|
||||||
|
use rusqlite::{params, Connection};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
use mize::{mize_err, mize_part, Mize, MizeError, MizePart, MizeResult};
|
||||||
|
|
||||||
|
use crate::cli::CliPart;
|
||||||
|
use crate::habitica::Habitica;
|
||||||
|
|
||||||
|
mod dashboard;
|
||||||
|
|
||||||
|
#[mize_part]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct C2vi {
|
||||||
|
mize: Mize,
|
||||||
|
db: Mutex<Option<Connection>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MizePart for C2vi {
|
||||||
|
fn opts(&self, mize: &mut Mize) {
|
||||||
|
mize.new_opt("c2vi.local_storage_path");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl C2vi {
|
||||||
|
fn with_db<F, R>(&self, f: F) -> MizeResult<R>
|
||||||
|
where
|
||||||
|
F: FnOnce(&Connection) -> MizeResult<R>,
|
||||||
|
{
|
||||||
|
let guard = self
|
||||||
|
.db
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| mize_err!("DB lock poisoned: {}", e))?;
|
||||||
|
let conn = guard
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| mize_err!("DB not initialized"))?;
|
||||||
|
f(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn c2vi(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
c2vi_client(mize)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn c2vi_client(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
//let storage_path = mize.get_config("c2vi.local_storage_path")?.as_
|
||||||
|
let storage_path = "/home/me/work/app-data/ppc";
|
||||||
|
println!("storage_path: {}", storage_path);
|
||||||
|
let db_path = std::path::PathBuf::from(&storage_path).join("data.db");
|
||||||
|
let conn = Connection::open(&db_path)
|
||||||
|
.map_err(|e| mize_err!("Failed to open C2vi database at {:?}: {}", db_path, e))?;
|
||||||
|
|
||||||
|
setup_tables(&conn)?;
|
||||||
|
|
||||||
|
let c2vi_part = C2vi {
|
||||||
|
mize: mize.clone(),
|
||||||
|
db: Mutex::new(Some(conn)),
|
||||||
|
};
|
||||||
|
mize.register_part(Box::new(c2vi_part))?;
|
||||||
|
|
||||||
|
// Register CLI subcommands
|
||||||
|
let mut cli = mize.get_part_native::<CliPart>("cli")?;
|
||||||
|
register_subcommands(&mut cli);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_tables(conn: &Connection) -> MizeResult<()> {
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS habitica_dailies (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
priority TEXT,
|
||||||
|
frequency TEXT
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.map_err(|e| mize_err!("DB error creating habitica_dailies: {}", e))?;
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS habitica_log (
|
||||||
|
date TEXT PRIMARY KEY,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.map_err(|e| mize_err!("DB error creating habitica_log: {}", e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_subcommands(cli: &mut CliPart) {
|
||||||
|
cli.subcommand(
|
||||||
|
Command::new("c2vi-dash").about("Run the c2vi dashboard gui"),
|
||||||
|
|_matches, mut mize| Ok(dioxus::launch(dashboard::main)),
|
||||||
|
);
|
||||||
|
cli.subcommand(
|
||||||
|
Command::new("bed").about("Command to run when c2vi goes to bed"),
|
||||||
|
|_matches, mut mize| {
|
||||||
|
save_habitica_dailies(&mut mize)?;
|
||||||
|
save_habitica_logs(&mut mize)?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
cli.subcommand(
|
||||||
|
Command::new("clearTodos")
|
||||||
|
.about("Clear all Habitica todos")
|
||||||
|
.alias("clt"),
|
||||||
|
|_matches, mut mize| {
|
||||||
|
let todos = {
|
||||||
|
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||||
|
hab.get_tasks("todos")?
|
||||||
|
};
|
||||||
|
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||||
|
if let Some(arr) = todos.as_array() {
|
||||||
|
for todo in arr {
|
||||||
|
if let Some(id) = todo["id"].as_str() {
|
||||||
|
hab.delete_task(id)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
cli.subcommand(
|
||||||
|
Command::new("listTodos")
|
||||||
|
.about("List all Habitica todos")
|
||||||
|
.alias("dut"),
|
||||||
|
|_matches, mut mize| {
|
||||||
|
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||||
|
let todos = hab.get_tasks("todos")?;
|
||||||
|
if let Some(arr) = todos.as_array() {
|
||||||
|
for todo in arr {
|
||||||
|
if let Some(text) = todo["text"].as_str() {
|
||||||
|
println!("{}", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
cli.subcommand(
|
||||||
|
Command::new("buyHealthPotion")
|
||||||
|
.about("Buy a health potion")
|
||||||
|
.alias("buyh"),
|
||||||
|
|_matches, mut mize| {
|
||||||
|
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||||
|
hab.api_request(
|
||||||
|
reqwest::Method::POST,
|
||||||
|
"user/buy-health-potion".to_string(),
|
||||||
|
json!({}),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
cli.subcommand(
|
||||||
|
Command::new("addActionItems")
|
||||||
|
.about("Add action items from temp file")
|
||||||
|
.alias("aal"),
|
||||||
|
|_matches, mut mize| add_action_items(&mut mize),
|
||||||
|
);
|
||||||
|
|
||||||
|
cli.subcommand(
|
||||||
|
Command::new("skip")
|
||||||
|
.about("Skip a Habitica todo")
|
||||||
|
.alias("sk")
|
||||||
|
.arg(Arg::new("num").default_value("1")),
|
||||||
|
|matches, mut mize| {
|
||||||
|
let num: usize = matches
|
||||||
|
.get_one::<String>("num")
|
||||||
|
.unwrap()
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(1);
|
||||||
|
skip_habitica_todo(&mut mize, num)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
cli.subcommand(
|
||||||
|
Command::new("dumpLog").about("Dump the complete Habitica log"),
|
||||||
|
|_matches, mut mize| dump_habitica_logs(&mut mize),
|
||||||
|
);
|
||||||
|
|
||||||
|
cli.subcommand(
|
||||||
|
Command::new("listDailies").about("List all dailies from local DB"),
|
||||||
|
|_matches, mut mize| {
|
||||||
|
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
|
||||||
|
c2vi.with_db(|conn| {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT id, text FROM habitica_dailies")
|
||||||
|
.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||||
|
let rows = stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||||
|
})
|
||||||
|
.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||||
|
for row in rows {
|
||||||
|
let (id, text) = row.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||||
|
println!("{}: {}", id, text);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
cli.subcommand(
|
||||||
|
Command::new("printTask")
|
||||||
|
.about("Print a specific task from Habitica")
|
||||||
|
.arg(Arg::new("id").required(true)),
|
||||||
|
|matches, mut mize| {
|
||||||
|
let id = matches.get_one::<String>("id").unwrap();
|
||||||
|
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||||
|
let data = hab.api_request(reqwest::Method::GET, format!("tasks/{}", id), json!({}))?;
|
||||||
|
if let Some(history) = data.get("history") {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(history).unwrap_or_default()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(&data).unwrap_or_default()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= Business Logic =============
|
||||||
|
|
||||||
|
fn format_date(dt: chrono::DateTime<Utc>) -> String {
|
||||||
|
dt.format("%Y-%m-%d").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the target date, adjusted for "save for yesterday" logic (before noon = yesterday)
|
||||||
|
fn get_target_date() -> (String, bool) {
|
||||||
|
let now = Utc::now();
|
||||||
|
let save_for_yesterday = now.hour() < 12;
|
||||||
|
let date = if save_for_yesterday {
|
||||||
|
now - chrono::Duration::days(1)
|
||||||
|
} else {
|
||||||
|
now
|
||||||
|
};
|
||||||
|
if save_for_yesterday {
|
||||||
|
println!(
|
||||||
|
"Saving logs for yesterday because it is {} hours of the day",
|
||||||
|
now.hour()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
(format_date(date), save_for_yesterday)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load existing log entry from DB, or return a default empty one
|
||||||
|
fn get_or_create_log(conn: &Connection, date: &str) -> MizeResult<Value> {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT data FROM habitica_log WHERE date = ?1")
|
||||||
|
.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||||
|
let result: Option<String> = stmt.query_row(params![date], |row| row.get(0)).ok();
|
||||||
|
match result {
|
||||||
|
Some(data_str) => {
|
||||||
|
serde_json::from_str(&data_str).map_err(|e| mize_err!("JSON parse error: {}", e))
|
||||||
|
}
|
||||||
|
None => Ok(json!({
|
||||||
|
"dailies_done": [],
|
||||||
|
"dailies_skipped": [],
|
||||||
|
"todos_done": [],
|
||||||
|
"todos_skipped": []
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upsert a log entry into the database
|
||||||
|
fn upsert_log(conn: &Connection, date: &str, data: &Value) -> MizeResult<()> {
|
||||||
|
let data_str =
|
||||||
|
serde_json::to_string(data).map_err(|e| mize_err!("JSON serialize error: {}", e))?;
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO habitica_log (date, data) VALUES (?1, ?2)
|
||||||
|
ON CONFLICT(date) DO UPDATE SET data=excluded.data",
|
||||||
|
params![date, data_str],
|
||||||
|
)
|
||||||
|
.map_err(|e| mize_err!("DB error upserting log: {}", e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_habitica_dailies(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
println!("######### cli bed cmd #########");
|
||||||
|
|
||||||
|
// Fetch dailies from Habitica
|
||||||
|
let dailies = {
|
||||||
|
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||||
|
hab.get_tasks("dailys")?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write to database
|
||||||
|
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
|
||||||
|
c2vi.with_db(|conn| {
|
||||||
|
if let Some(arr) = dailies.as_array() {
|
||||||
|
println!("Syncing {} dailies to database...", arr.len());
|
||||||
|
for daily in arr {
|
||||||
|
let id = daily["id"].as_str().unwrap_or("");
|
||||||
|
let text = daily["text"].as_str().unwrap_or("");
|
||||||
|
let notes = daily["notes"].as_str().unwrap_or("");
|
||||||
|
let priority = daily["priority"].to_string();
|
||||||
|
let frequency = daily["frequency"].as_str().unwrap_or("daily");
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO habitica_dailies (id, text, notes, priority, frequency)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
text=excluded.text,
|
||||||
|
notes=excluded.notes,
|
||||||
|
priority=excluded.priority,
|
||||||
|
frequency=excluded.frequency",
|
||||||
|
params![id, text, notes, priority, frequency],
|
||||||
|
)
|
||||||
|
.map_err(|e| mize_err!("DB error writing daily: {}", e))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_habitica_logs(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
let (date_str, save_for_yesterday) = get_target_date();
|
||||||
|
|
||||||
|
// Fetch dailies and completed todos from Habitica
|
||||||
|
let (dailies, completed_todos) = {
|
||||||
|
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||||
|
let dailies = hab.get_tasks("dailys")?;
|
||||||
|
let completed_todos = hab.get_tasks("completedTodos")?;
|
||||||
|
(dailies, completed_todos)
|
||||||
|
};
|
||||||
|
|
||||||
|
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
|
||||||
|
c2vi.with_db(|conn| {
|
||||||
|
let mut data = get_or_create_log(conn, &date_str)?;
|
||||||
|
|
||||||
|
// Process dailies
|
||||||
|
if let Some(arr) = dailies.as_array() {
|
||||||
|
for daily in arr {
|
||||||
|
let is_due = if save_for_yesterday {
|
||||||
|
// Look at history for yesterday's status
|
||||||
|
daily["history"]
|
||||||
|
.as_array()
|
||||||
|
.and_then(|h| h.iter().max_by_key(|e| e["date"].as_i64().unwrap_or(0)))
|
||||||
|
.map(|e| e["isDue"].as_bool().unwrap_or(false))
|
||||||
|
.unwrap_or(false)
|
||||||
|
} else {
|
||||||
|
daily["isDue"].as_bool().unwrap_or(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_due {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let completed = if save_for_yesterday {
|
||||||
|
daily["history"]
|
||||||
|
.as_array()
|
||||||
|
.and_then(|h| h.iter().max_by_key(|e| e["date"].as_i64().unwrap_or(0)))
|
||||||
|
.map(|e| e["completed"].as_bool().unwrap_or(false))
|
||||||
|
.unwrap_or(false)
|
||||||
|
} else {
|
||||||
|
daily["completed"].as_bool().unwrap_or(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
let daily_id = daily["id"].as_str().unwrap_or("");
|
||||||
|
let daily_text = daily["text"].as_str().unwrap_or("");
|
||||||
|
|
||||||
|
if completed {
|
||||||
|
data["dailies_done"]
|
||||||
|
.as_array_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push(json!(daily_id));
|
||||||
|
println!("daily-done: {}", daily_text);
|
||||||
|
} else {
|
||||||
|
data["dailies_skipped"]
|
||||||
|
.as_array_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push(json!(daily_id));
|
||||||
|
println!("daily-skipped: {}", daily_text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process completed todos
|
||||||
|
if let Some(arr) = completed_todos.as_array() {
|
||||||
|
let today_str = format_date(Utc::now());
|
||||||
|
for todo in arr {
|
||||||
|
let date_completed_str = todo["dateCompleted"].as_str().unwrap_or("");
|
||||||
|
|
||||||
|
// Parse the completion date (ISO 8601 format from Habitica)
|
||||||
|
let date_completed = chrono::DateTime::parse_from_rfc3339(date_completed_str)
|
||||||
|
.map(|d| format_date(d.with_timezone(&Utc)))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Check if already in the log for this date
|
||||||
|
if save_for_yesterday {
|
||||||
|
let existing_todos = data["todos_done"].as_array().unwrap();
|
||||||
|
let todo_id = todo["id"].as_str().unwrap_or("");
|
||||||
|
if existing_todos
|
||||||
|
.iter()
|
||||||
|
.any(|t| t["id"].as_str() == Some(todo_id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let todo_entry = json!({
|
||||||
|
"id": todo["id"],
|
||||||
|
"text": todo["text"],
|
||||||
|
"notes": todo.get("notes").unwrap_or(&json!("")),
|
||||||
|
"checklist": todo.get("checklist").unwrap_or(&json!([])),
|
||||||
|
"tags": todo.get("tags").unwrap_or(&json!([])),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save if completed on the target date
|
||||||
|
if date_completed == date_str {
|
||||||
|
println!("todo-done: {}", todo["text"].as_str().unwrap_or(""));
|
||||||
|
data["todos_done"]
|
||||||
|
.as_array_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push(todo_entry.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If saving for yesterday but todo was completed today, also record it
|
||||||
|
if save_for_yesterday && date_completed == today_str {
|
||||||
|
println!("todo-done: {}", todo["text"].as_str().unwrap_or(""));
|
||||||
|
data["todos_done"].as_array_mut().unwrap().push(todo_entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
upsert_log(conn, &date_str, &data)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip_habitica_todo(mize: &mut Mize, num: usize) -> MizeResult<()> {
|
||||||
|
let (date_str, _) = get_target_date();
|
||||||
|
|
||||||
|
// Fetch todos from Habitica
|
||||||
|
let todos = {
|
||||||
|
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||||
|
hab.get_tasks("todos")?
|
||||||
|
};
|
||||||
|
|
||||||
|
let arr = todos
|
||||||
|
.as_array()
|
||||||
|
.ok_or_else(|| mize_err!("Expected array of todos"))?;
|
||||||
|
|
||||||
|
// Delete and log each skipped todo
|
||||||
|
for i in 0..num {
|
||||||
|
let todo = match arr.get(i) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let todo_id = todo["id"].as_str().unwrap_or("");
|
||||||
|
|
||||||
|
// Delete the todo and score the skip habit
|
||||||
|
{
|
||||||
|
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||||
|
hab.delete_task(todo_id)?;
|
||||||
|
}
|
||||||
|
increment_habit(mize)?;
|
||||||
|
|
||||||
|
// Record in log
|
||||||
|
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
|
||||||
|
c2vi.with_db(|conn| {
|
||||||
|
let mut data = get_or_create_log(conn, &date_str)?;
|
||||||
|
data["todos_skipped"].as_array_mut().unwrap().push(json!({
|
||||||
|
"id": todo["id"],
|
||||||
|
"text": todo["text"],
|
||||||
|
"notes": todo.get("notes").unwrap_or(&json!("")),
|
||||||
|
"checklist": todo.get("checklist").unwrap_or(&json!([])),
|
||||||
|
"tags": todo.get("tags").unwrap_or(&json!([])),
|
||||||
|
}));
|
||||||
|
upsert_log(conn, &date_str, &data)?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_habit(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||||
|
let habits = hab.get_tasks("habits")?;
|
||||||
|
if let Some(arr) = habits.as_array() {
|
||||||
|
for habit in arr {
|
||||||
|
if habit["text"].as_str() == Some("skipped a planned task") {
|
||||||
|
let id = habit["id"].as_str().unwrap_or("");
|
||||||
|
hab.api_request(
|
||||||
|
reqwest::Method::POST,
|
||||||
|
format!("tasks/{}/score/down", id),
|
||||||
|
json!({}),
|
||||||
|
)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_action_items(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
let initial_content = "\
|
||||||
|
gu 10min TIMER
|
||||||
|
uw 15min FROG
|
||||||
|
b coffee/tee/kakau
|
||||||
|
uw FROG
|
||||||
|
b breakfast, brush teeth, lüften
|
||||||
|
uw FROG
|
||||||
|
|
||||||
|
lunch
|
||||||
|
|
||||||
|
uw 15min TIMER of next days FROG
|
||||||
|
fw
|
||||||
|
dt 30min TIMER
|
||||||
|
|
||||||
|
fun 20:00 (smth that can be stopped easily)
|
||||||
|
se 21:40
|
||||||
|
read
|
||||||
|
bed 22:10";
|
||||||
|
|
||||||
|
let tmp_file =
|
||||||
|
std::env::temp_dir().join(format!("habitica-tasks-{}.md", Utc::now().timestamp()));
|
||||||
|
std::fs::write(&tmp_file, initial_content)
|
||||||
|
.map_err(|e| mize_err!("Failed to write temp file: {}", e))?;
|
||||||
|
|
||||||
|
// Open in nvim for editing
|
||||||
|
std::process::Command::new("nvim")
|
||||||
|
.arg(&tmp_file)
|
||||||
|
.status()
|
||||||
|
.map_err(|e| mize_err!("Failed to run nvim: {}", e))?;
|
||||||
|
|
||||||
|
// Read back and parse
|
||||||
|
let list_str = std::fs::read_to_string(&tmp_file)
|
||||||
|
.map_err(|e| mize_err!("Failed to read temp file: {}", e))?;
|
||||||
|
let _ = std::fs::remove_file(&tmp_file);
|
||||||
|
|
||||||
|
let tasks: Vec<&str> = list_str
|
||||||
|
.lines()
|
||||||
|
.map(|l| l.trim())
|
||||||
|
.filter(|l| !l.is_empty())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Add tasks to Habitica
|
||||||
|
let mut task_ids = Vec::new();
|
||||||
|
{
|
||||||
|
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
|
||||||
|
for todo_text in &tasks {
|
||||||
|
let data = hab.api_request(
|
||||||
|
reqwest::Method::POST,
|
||||||
|
"tasks/user".to_string(),
|
||||||
|
json!({ "text": todo_text, "type": "todo" }),
|
||||||
|
)?;
|
||||||
|
let id = data["id"].as_str().unwrap_or("").to_string();
|
||||||
|
println!("Added to-do: '{}' with id '{}'", todo_text, id);
|
||||||
|
task_ids.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move all tasks to bottom
|
||||||
|
for task_id in &task_ids {
|
||||||
|
hab.api_request(
|
||||||
|
reqwest::Method::POST,
|
||||||
|
format!("tasks/{}/move/to/-1", task_id),
|
||||||
|
json!({}),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dump_habitica_logs(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
|
||||||
|
c2vi.with_db(|conn| {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT date, data FROM habitica_log ORDER BY date")
|
||||||
|
.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||||
|
let rows = stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||||
|
})
|
||||||
|
.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||||
|
for row in rows {
|
||||||
|
let (date, data_str) = row.map_err(|e| mize_err!("DB error: {}", e))?;
|
||||||
|
let data: Value =
|
||||||
|
serde_json::from_str(&data_str).unwrap_or(json!({"parse_error": data_str}));
|
||||||
|
println!(
|
||||||
|
"{}: {}",
|
||||||
|
date,
|
||||||
|
serde_json::to_string_pretty(&data).unwrap_or_default()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -8,28 +8,59 @@ use mize::{mize_part, Mize, MizePart, MizeResult};
|
|||||||
pub struct CliPart {
|
pub struct CliPart {
|
||||||
mize: Mize,
|
mize: Mize,
|
||||||
cmd: Option<Command>,
|
cmd: Option<Command>,
|
||||||
actions: HashMap<String, fn(&ArgMatches) -> MizeResult<()>>,
|
actions: HashMap<
|
||||||
|
String,
|
||||||
|
Box<dyn FnOnce(&ArgMatches, Mize) -> MizeResult<()> + Send + Sync + 'static>,
|
||||||
|
>,
|
||||||
|
parsers:
|
||||||
|
Option<Vec<Box<dyn FnOnce(Mize, Vec<String>) -> MizeResult<()> + Send + Sync + 'static>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cli(mize: &mut Mize) -> MizeResult<()> {
|
pub fn cli(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
let command = Command::new("marts-cli").allow_external_subcommands(true);
|
||||||
let cli_part = CliPart {
|
let cli_part = CliPart {
|
||||||
mize: mize.clone(),
|
mize: mize.clone(),
|
||||||
cmd: Some(Command::new("marts-cli")),
|
cmd: Some(command),
|
||||||
actions: HashMap::new(),
|
actions: HashMap::new(),
|
||||||
|
parsers: Some(Vec::new()),
|
||||||
};
|
};
|
||||||
|
|
||||||
mize.register_part(Box::new(cli_part))
|
mize.add_part(Box::new(cli_part))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MizePart for CliPart {
|
impl MizePart for CliPart {
|
||||||
fn init(&mut self, _mize: &mut Mize) -> MizeResult<()> {
|
fn init(&mut self, _mize: &mut Mize) -> MizeResult<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn run(&mut self, _mize: &mut Mize) -> MizeResult<()> {
|
fn run(&mut self, mize: &mut Mize) -> MizeResult<()> {
|
||||||
let matches = self.cmd.take().unwrap().get_matches();
|
let matches = self.cmd.take().unwrap().get_matches();
|
||||||
let sub_cmd = matches.subcommand_name().unwrap();
|
let sub_cmd = match matches.subcommand_name() {
|
||||||
let action = self.actions.get(sub_cmd).unwrap();
|
Some(sub_cmd) => sub_cmd,
|
||||||
action(matches.subcommand_matches(sub_cmd).unwrap())?;
|
None => {
|
||||||
|
println!("No subcommand provided");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let action = match self.actions.remove(sub_cmd) {
|
||||||
|
Some(action) => action,
|
||||||
|
None => {
|
||||||
|
// external parsers
|
||||||
|
for parser in self.parsers.take().unwrap() {
|
||||||
|
parser(
|
||||||
|
mize.clone(),
|
||||||
|
matches
|
||||||
|
.subcommand_matches(sub_cmd)
|
||||||
|
.unwrap()
|
||||||
|
.get_many::<String>("")
|
||||||
|
.unwrap()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect::<Vec<String>>(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
action(matches.subcommand_matches(sub_cmd).unwrap(), mize.clone())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn opts(&self, mize: &mut Mize) {
|
fn opts(&self, mize: &mut Mize) {
|
||||||
@@ -38,17 +69,26 @@ impl MizePart for CliPart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CliPart {
|
impl CliPart {
|
||||||
pub fn subcommand(
|
pub fn subcommand<T: FnOnce(&ArgMatches, Mize) -> MizeResult<()> + Send + Sync + 'static>(
|
||||||
&mut self,
|
&mut self,
|
||||||
subcmd: Command,
|
subcmd: Command,
|
||||||
action: fn(sub_matches: &ArgMatches) -> MizeResult<()>,
|
action: T,
|
||||||
) -> &mut Command {
|
) -> &mut Command {
|
||||||
let name = subcmd.get_name().to_string();
|
let name = subcmd.get_name().to_string();
|
||||||
let cmd = self.cmd.take().unwrap().subcommand(subcmd);
|
let cmd = self.cmd.take().unwrap().subcommand(subcmd);
|
||||||
self.actions.insert(name, action);
|
self.actions.insert(name, Box::new(action));
|
||||||
self.cmd = Some(cmd);
|
self.cmd = Some(cmd);
|
||||||
self.cmd.as_mut().unwrap()
|
self.cmd.as_mut().unwrap()
|
||||||
}
|
}
|
||||||
|
pub fn add_sub_parser<
|
||||||
|
T: FnOnce(Mize, Vec<String>) -> MizeResult<()> + Send + Sync + 'static,
|
||||||
|
>(
|
||||||
|
&mut self,
|
||||||
|
parser: T,
|
||||||
|
) -> MizeResult<()> {
|
||||||
|
self.parsers.as_mut().unwrap().push(Box::new(parser));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
pub fn with_cmd(
|
pub fn with_cmd(
|
||||||
&mut self,
|
&mut self,
|
||||||
func: fn(mize: Mize, cmd: Command) -> MizeResult<Command>,
|
func: fn(mize: Mize, cmd: Command) -> MizeResult<Command>,
|
||||||
|
|||||||
@@ -2,10 +2,7 @@
|
|||||||
"name": "@ppc/marts",
|
"name": "@ppc/marts",
|
||||||
"imports": {},
|
"imports": {},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts",
|
".": "./index.ts"
|
||||||
"stub_deno": "./js/stub_deno.ts"
|
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {}
|
||||||
"build": "deno bundle -o ./deno_dist/habitica.js ./habitica.ts"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// habitica.ts
|
(() => {
|
||||||
var opts = {
|
// habitica.ts
|
||||||
|
var opts = {
|
||||||
"habitica.api_url": {
|
"habitica.api_url": {
|
||||||
env_var_name: "PPC_HABITICA_API_URL",
|
env_var_name: "PPC_HABITICA_API_URL",
|
||||||
default_val: "https://habitica.com/api/v3"
|
default_val: "https://habitica.com/api/v3"
|
||||||
@@ -13,13 +14,14 @@ var opts = {
|
|||||||
"habitica.api_token": {
|
"habitica.api_token": {
|
||||||
env_var_name: "PPC_HABITICA_API_TOKEN"
|
env_var_name: "PPC_HABITICA_API_TOKEN"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
async function habitica(mize) {
|
async function habitica(mize) {
|
||||||
console.log("hiiiiiiiiiiiiiiiiiiii");
|
Deno.core.print("hiiiiiiiiiiiiiiiiiiii from habitica");
|
||||||
|
log("hiiiiiiiiiiiiiiiiiiii");
|
||||||
mize.add_opts(opts);
|
mize.add_opts(opts);
|
||||||
mize.add_part(new Habitica(mize));
|
mize.add_part(new Habitica(mize));
|
||||||
}
|
}
|
||||||
async function handleRateLimit(response) {
|
async function handleRateLimit(response) {
|
||||||
const limit = response.headers.get("X-RateLimit-Limit") || "NONE";
|
const limit = response.headers.get("X-RateLimit-Limit") || "NONE";
|
||||||
const remaining = parseInt(response.headers.get("X-RateLimit-Remaining") || "10", 10);
|
const remaining = parseInt(response.headers.get("X-RateLimit-Remaining") || "10", 10);
|
||||||
const reset = response.headers.get("X-RateLimit-Reset");
|
const reset = response.headers.get("X-RateLimit-Reset");
|
||||||
@@ -33,8 +35,8 @@ async function handleRateLimit(response) {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var Habitica = class {
|
var Habitica = class {
|
||||||
mize;
|
mize;
|
||||||
constructor(mize) {
|
constructor(mize) {
|
||||||
this.mize = mize;
|
this.mize = mize;
|
||||||
@@ -68,8 +70,7 @@ var Habitica = class {
|
|||||||
}
|
}
|
||||||
return (await response.json()).data;
|
return (await response.json()).data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export {
|
habitica(globalThis.mize);
|
||||||
habitica,
|
Deno.core.print("hiiiiiiiiiiiiiiiiiiii from habitica one");
|
||||||
opts
|
})();
|
||||||
};
|
|
||||||
|
|||||||
12
packages/marts/esbuild.config.mjs
Normal file
12
packages/marts/esbuild.config.mjs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import * as esbuild from "npm:esbuild";
|
||||||
|
|
||||||
|
import { denoPlugin } from "jsr:@deno/esbuild-plugin";
|
||||||
|
|
||||||
|
await esbuild.build({
|
||||||
|
plugins: [denoPlugin()],
|
||||||
|
entryPoints: ["./habitica.ts"],
|
||||||
|
outfile: "./deno_dist/habitica.js",
|
||||||
|
bundle: true,
|
||||||
|
format: "iife",
|
||||||
|
});
|
||||||
|
esbuild.stop();
|
||||||
127
packages/marts/habitica.rs
Normal file
127
packages/marts/habitica.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use mize::{mize_err, mize_part, Mize, MizeError, MizePart, MizeResult};
|
||||||
|
use reqwest::{blocking::Client, blocking::Response, header, Method};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use std::thread::sleep;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[mize_part]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Habitica {
|
||||||
|
mize: Mize,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn habitica(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
let client = Client::new();
|
||||||
|
mize.register_part(Box::new(Habitica {
|
||||||
|
mize: mize.clone(),
|
||||||
|
client,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MizePart for Habitica {
|
||||||
|
fn opts(&self, mize: &mut Mize) {
|
||||||
|
mize.new_opt("habitica.api_url");
|
||||||
|
mize.new_opt("habitica.user_id");
|
||||||
|
mize.new_opt("habitica.api_token");
|
||||||
|
mize.new_opt("habitica.client_name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Habitica {
|
||||||
|
pub fn api_request(&mut self, method: Method, path: String, data: Value) -> MizeResult<Value> {
|
||||||
|
let api_url = self.mize.get_config("habitica.api_url")?.to_string();
|
||||||
|
let user_id = self.mize.get_config("habitica.user_id")?.to_string();
|
||||||
|
let api_token = self.mize.get_config("habitica.api_token")?.to_string();
|
||||||
|
let client_name = self.mize.get_config("habitica.client_name")?.to_string();
|
||||||
|
|
||||||
|
let mut headers = header::HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
"Content-Type",
|
||||||
|
header::HeaderValue::from_static("application/json"),
|
||||||
|
);
|
||||||
|
headers.insert("x-api-user", header::HeaderValue::from_str(&user_id)?);
|
||||||
|
headers.insert("x-api-key", header::HeaderValue::from_str(&api_token)?);
|
||||||
|
headers.insert("x-client", header::HeaderValue::from_str(&client_name)?);
|
||||||
|
|
||||||
|
let url = format!("{}/{}", api_url, path.trim_start_matches('/'));
|
||||||
|
|
||||||
|
let mut request_builder = self.client.request(method, &url).headers(headers);
|
||||||
|
|
||||||
|
if data != json!({}) {
|
||||||
|
request_builder = request_builder.json(&data);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = request_builder.send()?;
|
||||||
|
|
||||||
|
handle_rate_limit(&response);
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status = response.status();
|
||||||
|
let text = response.text().unwrap_or_default();
|
||||||
|
println!("request_url: {}", url);
|
||||||
|
return Err(mize_err!(
|
||||||
|
"Habitica API error: {} {} - {}",
|
||||||
|
status.as_u16(),
|
||||||
|
status.canonical_reason().unwrap_or(""),
|
||||||
|
text
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_response: Value = response.json()?;
|
||||||
|
Ok(json_response.get("data").cloned().unwrap_or(Value::Null))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tasks(&mut self, task_type: &str) -> MizeResult<Value> {
|
||||||
|
self.api_request(
|
||||||
|
Method::GET,
|
||||||
|
format!("tasks/user?type={}", task_type),
|
||||||
|
json!({}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_task(&mut self, id: &str) -> MizeResult<Value> {
|
||||||
|
self.api_request(Method::DELETE, format!("tasks/{}", id), json!({}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_rate_limit(response: &Response) {
|
||||||
|
let headers = response.headers();
|
||||||
|
let limit = headers
|
||||||
|
.get("X-RateLimit-Limit")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("NONE");
|
||||||
|
let remaining = headers
|
||||||
|
.get("X-RateLimit-Remaining")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.and_then(|s| s.parse::<i32>().ok())
|
||||||
|
.unwrap_or(10);
|
||||||
|
let reset = headers
|
||||||
|
.get("X-RateLimit-Reset")
|
||||||
|
.and_then(|v| v.to_str().ok());
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"RateLimit: {} | Remaining: {} | Reset: {}",
|
||||||
|
limit,
|
||||||
|
remaining,
|
||||||
|
reset.unwrap_or("N/A")
|
||||||
|
);
|
||||||
|
|
||||||
|
if remaining < 2 {
|
||||||
|
if let Some(reset_str) = reset {
|
||||||
|
if let Ok(reset_date) = DateTime::parse_from_rfc2822(reset_str) {
|
||||||
|
let now = Utc::now();
|
||||||
|
let wait_duration = reset_date.signed_duration_since(now).to_std();
|
||||||
|
if let Ok(wait_duration) = wait_duration {
|
||||||
|
let wait_ms = wait_duration.as_millis() as u64 + 1000;
|
||||||
|
println!(
|
||||||
|
"Waiting {} secs for next rate limit window...",
|
||||||
|
(wait_ms / 1000)
|
||||||
|
);
|
||||||
|
sleep(Duration::from_millis(wait_ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import { Mize } from "@ppc/marts/stub_deno";
|
|
||||||
|
|
||||||
export const opts = {
|
|
||||||
"habitica.api_url": {
|
|
||||||
env_var_name: "PPC_HABITICA_API_URL",
|
|
||||||
default_val: "https://habitica.com/api/v3",
|
|
||||||
},
|
|
||||||
"habitica.user_id": {
|
|
||||||
env_var_name: "PPC_HABITICA_USER_ID",
|
|
||||||
},
|
|
||||||
"habitica.client_name": {
|
|
||||||
default_val: "3544a0b8-d71a-46e0-9bb1-6ddbb2abcddb-PPC-Software",
|
|
||||||
},
|
|
||||||
"habitica.api_token": {
|
|
||||||
env_var_name: "PPC_HABITICA_API_TOKEN",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function habitica(mize: Mize) {
|
|
||||||
console.log("hiiiiiiiiiiiiiiiiiiii");
|
|
||||||
mize.add_opts(opts);
|
|
||||||
|
|
||||||
mize.add_part(new Habitica(mize));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRateLimit(response) {
|
|
||||||
const limit = response.headers.get("X-RateLimit-Limit") || "NONE";
|
|
||||||
const remaining = parseInt(
|
|
||||||
response.headers.get("X-RateLimit-Remaining") || "10",
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
const reset = response.headers.get("X-RateLimit-Reset");
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`RateLimit: ${limit} | Remaining: ${remaining} | Reset: ${reset}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (remaining < 2 && reset) {
|
|
||||||
const resetDate = new Date(reset);
|
|
||||||
const now = new Date();
|
|
||||||
const waitMs = resetDate.getTime() - now.getTime() + 1000;
|
|
||||||
|
|
||||||
if (waitMs > 0) {
|
|
||||||
console.log(
|
|
||||||
`Waiting ${Math.round(waitMs / 1000)} secs for next rate limit window...`,
|
|
||||||
);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Habitica {
|
|
||||||
mize: Mize;
|
|
||||||
|
|
||||||
constructor(mize: Mize) {
|
|
||||||
this.mize = mize;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get_tasks(type: string = "todos") {
|
|
||||||
return await this.api_request("GET", `tasks/user?type=${type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete_task(id: string) {
|
|
||||||
await this.api_request("DELETE", `tasks/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async api_request(
|
|
||||||
method: string,
|
|
||||||
path: string,
|
|
||||||
extraHeaders: object = {},
|
|
||||||
data: object = {},
|
|
||||||
) {
|
|
||||||
const mize = this.mize;
|
|
||||||
|
|
||||||
if (!path.startsWith("/")) {
|
|
||||||
path = "/" + path;
|
|
||||||
}
|
|
||||||
const headers = Object.assign(
|
|
||||||
{
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-api-user": mize.get_config("habitica.user_id"),
|
|
||||||
"x-api-key": mize.get_config("habitica.api_token"),
|
|
||||||
"x-client": mize.get_config("habitica.client_name"),
|
|
||||||
},
|
|
||||||
extraHeaders,
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await fetch(mize.get_config("habitica.api_url") + path, {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
await handleRateLimit(response);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.log("request_url:", mize.get_config("habitica.api_url") + path);
|
|
||||||
throw new Error(
|
|
||||||
`Habitica API error: ${response.status} ${response.statusText}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await response.json()).data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
export { task_obsidian_otask_parse } from "./task/obsidian-otask-parse";
|
export * from "./task/obsidian-otask-parse";
|
||||||
export { c2vi_obsidian_canvas_patch } from "./c2vi/obsidian-canvas-patch";
|
export * from "./c2vi/obsidian-canvas-patch";
|
||||||
|
|
||||||
|
export * from "./js/stub_deno";
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ use deno_core::OpState;
|
|||||||
use mize::{Mize, MizeError};
|
use mize::{Mize, MizeError};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::CliPart;
|
||||||
|
use crate::JsPart;
|
||||||
|
|
||||||
#[op2(fast)]
|
#[op2(fast)]
|
||||||
fn op_mize_get_part(#[string] name: &str) {
|
fn op_mize_get_part(#[string] name: &str) {
|
||||||
println!("js wants the part {}", name);
|
println!("js wants the part {}", name);
|
||||||
@@ -22,6 +25,21 @@ fn op_mize_add_part(state: &mut OpState, #[string] key: &str) {
|
|||||||
println!("adding part {}", key);
|
println!("adding part {}", key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[op2(fast)]
|
||||||
|
fn op_cli_subcommand(state: &mut OpState, #[string] name: String) {
|
||||||
|
let mut mize: Mize = state.borrow::<Mize>().clone();
|
||||||
|
let mut cli = mize.get_part_native::<CliPart>("cli").unwrap();
|
||||||
|
cli.subcommand(
|
||||||
|
clap::Command::new(name.clone()),
|
||||||
|
move |_sub_matches, mut mize| {
|
||||||
|
let mut js = mize.get_part_native::<JsPart>("js").unwrap();
|
||||||
|
js.eval("log(\"cli command being called\")".to_string())?;
|
||||||
|
js.eval(format!("mize.vars.cli.subcommand({name})"))?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Create an extension with your custom ops
|
// Create an extension with your custom ops
|
||||||
extension!(
|
extension!(
|
||||||
my_extension,
|
my_extension,
|
||||||
|
|||||||
11
packages/marts/js/glue_deno.ts
Normal file
11
packages/marts/js/glue_deno.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Mize } from "./stub_deno.ts";
|
||||||
|
|
||||||
|
function argsToMessage(...args) {
|
||||||
|
return args.map((arg) => JSON.stringify(arg)).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.log = (...args) => {
|
||||||
|
Deno.core.print(`[out]: ${argsToMessage(...args)}\n`, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.mize = new Mize();
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::url::Url;
|
use deno_core::url::Url;
|
||||||
use deno_core::{extension, op2, JsRuntime, ModuleSpecifier, PollEventLoopOptions, RuntimeOptions};
|
use deno_core::v8::OneByteConst;
|
||||||
|
use deno_core::{
|
||||||
|
ascii_str_include, extension, op2, JsRuntime, ModuleSpecifier, PollEventLoopOptions,
|
||||||
|
RuntimeOptions,
|
||||||
|
};
|
||||||
use deno_core::{FastStaticString, FsModuleLoader};
|
use deno_core::{FastStaticString, FsModuleLoader};
|
||||||
use mize::async_trait;
|
use mize::async_trait;
|
||||||
use mize::instance::MizePartCreate;
|
use mize::instance::MizePartCreate;
|
||||||
@@ -19,35 +23,36 @@ mod glue_deno;
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct JsPart {
|
pub struct JsPart {
|
||||||
mize: Mize,
|
mize: Mize,
|
||||||
closure_sender: Option<flume::Sender<Box<dyn FnOnce(&mut JsRuntime) -> MizeResult<()> + Send>>>,
|
sender: Option<flume::Sender<JsRuntimeThreadMessage>>,
|
||||||
async_closure_sender: Option<flume::Sender<BoxClosure>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type BoxFuture<'a> = Pin<Box<dyn Future<Output = MizeResult<()>> + 'a>>;
|
pub type BoxFuture<'a> = Pin<Box<dyn Future<Output = MizeResult<()>> + 'a>>;
|
||||||
pub type BoxClosure = Box<dyn for<'a> FnOnce(&'a mut JsRuntime) -> BoxFuture<'a> + Send + 'static>;
|
pub type BoxClosure = Box<dyn for<'a> FnOnce(&'a mut JsRuntime) -> BoxFuture<'a> + Send + 'static>;
|
||||||
|
|
||||||
impl MizePart for JsPart {
|
impl MizePart for JsPart {
|
||||||
fn init(&mut self, mize: &mut Mize) -> MizeResult<()> {
|
fn run(&mut self, mize: &mut Mize) -> MizeResult<()> {
|
||||||
println!("js part init");
|
println!("js part run");
|
||||||
|
self.sender
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.send(JsRuntimeThreadMessage::DoRunPhase)
|
||||||
|
.unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn js(mize: &mut Mize) -> MizeResult<()> {
|
pub fn js(mize: &mut Mize) -> MizeResult<()> {
|
||||||
let (closure_sender, closure_receiver) =
|
let (sender, receiver) = flume::unbounded::<JsRuntimeThreadMessage>();
|
||||||
flume::unbounded::<Box<dyn FnOnce(&mut JsRuntime) -> MizeResult<()> + Send>>();
|
|
||||||
let (async_closure_sender, async_closure_receiver) = flume::unbounded::<BoxClosure>();
|
|
||||||
let mize_clone = mize.clone();
|
let mize_clone = mize.clone();
|
||||||
|
|
||||||
// the thread which will run any js
|
// the thread which will run any js
|
||||||
mize.spawn("js_runtime_thread", || {
|
mize.spawn_and_wait("js_runtime_thread", || {
|
||||||
js_runtime_thread(mize_clone, closure_receiver, async_closure_receiver)
|
js_runtime_thread(mize_clone, receiver)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
mize.add_part(Box::new(JsPart {
|
mize.add_part(Box::new(JsPart {
|
||||||
mize: mize.clone(),
|
mize: mize.clone(),
|
||||||
closure_sender: Some(closure_sender),
|
sender: Some(sender),
|
||||||
async_closure_sender: Some(async_closure_sender),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,169 +62,75 @@ pub fn part_from_file(
|
|||||||
code: FastStaticString,
|
code: FastStaticString,
|
||||||
) -> MizeResult<()> {
|
) -> MizeResult<()> {
|
||||||
let mut js = mize.get_part_native::<JsPart>("js")?;
|
let mut js = mize.get_part_native::<JsPart>("js")?;
|
||||||
js.with_runtime_async(move |runtime: &mut JsRuntime| {
|
js.execute_init_js(code)?;
|
||||||
let module_spec = ModuleSpecifier::from_str(name).unwrap();
|
|
||||||
return Box::pin(async move {
|
|
||||||
runtime
|
|
||||||
.load_side_es_module_from_code(&module_spec, code)
|
|
||||||
.await;
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JsPart {
|
impl JsPart {
|
||||||
/*
|
pub fn execute_init_js(&mut self, code: FastStaticString) -> MizeResult<()> {
|
||||||
pub fn part_from_js_file(
|
self.sender
|
||||||
&mut self,
|
.as_ref()
|
||||||
name: &'static str,
|
|
||||||
path: &'static str,
|
|
||||||
) -> Box<dyn MizePart + Send + Sync> {
|
|
||||||
Box::new(PartFromJsFileAdapter {
|
|
||||||
mize: self.mize.clone(),
|
|
||||||
name,
|
|
||||||
js_file: path,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
pub fn with_runtime<T: FnOnce(&mut JsRuntime) -> MizeResult<()> + Send + 'static>(
|
|
||||||
&mut self,
|
|
||||||
func: T,
|
|
||||||
) {
|
|
||||||
self.closure_sender
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.send(Box::new(func))
|
.send(JsRuntimeThreadMessage::RunInitJs(code))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
|
||||||
pub fn with_runtime_async<F>(&mut self, func: F)
|
|
||||||
where
|
|
||||||
F: for<'a> FnOnce(&'a mut JsRuntime) -> BoxFuture<'a> + Send + 'static,
|
|
||||||
{
|
|
||||||
self.async_closure_sender
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.send(Box::new(func))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
#[mize_part]
|
|
||||||
#[derive(Default)]
|
|
||||||
struct PartFromJsFileAdapter {
|
|
||||||
mize: Mize,
|
|
||||||
name: &'static str,
|
|
||||||
js_file: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl MizePart for PartFromJsFileAdapter {
|
|
||||||
fn deps(&self) -> &'static [&'static str] {
|
|
||||||
&["js"]
|
|
||||||
}
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
self.name
|
|
||||||
}
|
|
||||||
async fn async_init(&mut self, mize: &mut Mize) -> MizeResult<()> {
|
|
||||||
let js = mize.get_part_native::<JsPart>("js")?;
|
|
||||||
let path = self.js_file;
|
|
||||||
let js_code = format!(
|
|
||||||
r#"
|
|
||||||
import {{ opts, deps, create }} from "{path}";
|
|
||||||
|
|
||||||
const isPromise = (value) => {{
|
|
||||||
return !!value && (typeof value === 'object' || typeof value === 'function') && typeof value.then === 'function';
|
|
||||||
}};
|
|
||||||
|
|
||||||
// Call the async function
|
|
||||||
let create_result = create(mize);
|
|
||||||
if (isPromise(create_result)) {{
|
|
||||||
create_result = await create_result;
|
|
||||||
}}
|
|
||||||
const result = {{
|
|
||||||
opts: opts(mize),
|
|
||||||
deps: deps(mize),
|
|
||||||
create: create_result,
|
|
||||||
}};
|
|
||||||
|
|
||||||
result;
|
|
||||||
"#
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute the script
|
|
||||||
let result = js.runtime().execute_script("[main]", js_code)?;
|
|
||||||
|
|
||||||
// Resolve the promise and run event loop
|
|
||||||
let resolved_value = js.runtime().resolve_value(result).await?;
|
|
||||||
|
|
||||||
// Run the event loop to completion
|
|
||||||
js.runtime()
|
|
||||||
.run_event_loop(PollEventLoopOptions::default())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("Execution of part {} completed successfully", self.name());
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn async_run(&mut self, mize: &mut Mize) -> MizeResult<()> {
|
pub fn eval(&mut self, code: String) -> MizeResult<()> {
|
||||||
let js = mize.get_part_native::<JsPart>("js")?;
|
self.sender
|
||||||
let path = self.js_file;
|
.as_ref()
|
||||||
let js_code = format!(
|
.unwrap()
|
||||||
r#"
|
.send(JsRuntimeThreadMessage::RunJs(code))
|
||||||
import {{ run }} from "{path}";
|
.unwrap();
|
||||||
|
|
||||||
const result = run()
|
|
||||||
|
|
||||||
result;
|
|
||||||
"#
|
|
||||||
);
|
|
||||||
// Execute the script
|
|
||||||
let result = js.runtime().execute_script("[main]", js_code)?;
|
|
||||||
|
|
||||||
// Resolve the promise and run event loop
|
|
||||||
let resolved_value = js.runtime().resolve_value(result).await?;
|
|
||||||
|
|
||||||
// Run the event loop to completion
|
|
||||||
js.runtime()
|
|
||||||
.run_event_loop(PollEventLoopOptions::default())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("Running of part {} completed successfully", self.name());
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
fn js_runtime_thread(
|
fn js_runtime_thread(
|
||||||
mize: Mize,
|
mize: Mize,
|
||||||
closure_receiver: flume::Receiver<Box<dyn FnOnce(&mut JsRuntime) -> MizeResult<()> + Send>>,
|
receiver: flume::Receiver<JsRuntimeThreadMessage>,
|
||||||
async_closure_receiver: flume::Receiver<BoxClosure>,
|
|
||||||
) -> MizeResult<()> {
|
) -> MizeResult<()> {
|
||||||
let mut js_runtime = JsRuntime::new(RuntimeOptions {
|
let mut js_runtime = JsRuntime::new(RuntimeOptions {
|
||||||
module_loader: Some(Rc::new(FsModuleLoader)),
|
//module_loader: Some(Rc::new(FsModuleLoader)),
|
||||||
extensions: vec![glue_deno::my_extension::init(mize.clone())],
|
//extensions: vec![glue_deno::my_extension::init(mize.clone())],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
let poll_opts = PollEventLoopOptions::default();
|
js_runtime
|
||||||
|
.execute_script("[stub]", ascii_str_include!("../deno_dist/glue_deno.js"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let tokio_runtime = Builder::new_current_thread().enable_all().build().unwrap();
|
|
||||||
|
|
||||||
let local = LocalSet::new();
|
|
||||||
local.block_on(&tokio_runtime, async move {
|
|
||||||
loop {
|
loop {
|
||||||
// Drain queued closures quickly (no await here if possible)
|
println!("js thread waiting for smth");
|
||||||
if let Ok(func) = closure_receiver.recv_async().await {
|
let msg = receiver.recv().unwrap();
|
||||||
if let Err(e) = func(&mut js_runtime) {
|
println!("js thread got smth");
|
||||||
mize.report_err(e);
|
match msg {
|
||||||
|
JsRuntimeThreadMessage::RunInitJs(js_code) => {
|
||||||
|
if let Err(err) = js_runtime.execute_script("[init]", js_code) {
|
||||||
|
println!("err: {}", err);
|
||||||
|
}
|
||||||
|
println!("done running js");
|
||||||
|
}
|
||||||
|
JsRuntimeThreadMessage::RunJs(js_code) => {
|
||||||
|
if let Err(err) = js_runtime.execute_script("[idk]", js_code) {
|
||||||
|
println!("err: {}", err);
|
||||||
|
}
|
||||||
|
println!("done running js");
|
||||||
|
}
|
||||||
|
JsRuntimeThreadMessage::DoRunPhase => {
|
||||||
|
if let Err(err) = js_runtime.execute_script("[runPhase]", "mize.runPhase()") {
|
||||||
|
println!("err: {}", err);
|
||||||
|
}
|
||||||
|
println!("done with runPhase");
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drive JS one tick
|
|
||||||
let _ = futures::future::poll_fn(|cx| js_runtime.poll_event_loop(cx, poll_opts)).await;
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
Ok(())
|
enum JsRuntimeThreadMessage {
|
||||||
|
//Closure(Box<dyn FnOnce(&mut JsRuntime) -> MizeResult<()> + Send>),
|
||||||
|
//AsyncClosure(BoxClosure),
|
||||||
|
RunInitJs(FastStaticString),
|
||||||
|
RunJs(String),
|
||||||
|
DoRunPhase,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ export class Mize {
|
|||||||
async add_opts(opts: any): Promise<void> {}
|
async add_opts(opts: any): Promise<void> {}
|
||||||
async add_part(part: any): Promise<void> {}
|
async add_part(part: any): Promise<void> {}
|
||||||
async report_err(err: MizeError) {}
|
async report_err(err: MizeError) {}
|
||||||
|
runPhase() {
|
||||||
|
Deno.core.print("runPhase in js \n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MizeError = string;
|
type MizeError = string;
|
||||||
|
|
||||||
|
export class Cli {
|
||||||
|
constructor(mize: Mize) {}
|
||||||
|
async sub_command(name: string, cmd: any): Promise<void> {}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
|
|
||||||
|
use mize::MizeResult;
|
||||||
|
|
||||||
#[cfg(feature = "target-os")]
|
#[cfg(feature = "target-os")]
|
||||||
mod cli;
|
mod cli;
|
||||||
#[cfg(feature = "target-os")]
|
#[cfg(feature = "target-os")]
|
||||||
pub use cli::cli;
|
pub use cli::*;
|
||||||
|
|
||||||
mod js;
|
|
||||||
|
|
||||||
#[cfg(feature = "target-os")]
|
#[cfg(feature = "target-os")]
|
||||||
pub fn habitica(mize: &mut mize::Mize) {
|
pub mod habitica;
|
||||||
use deno_core::{ascii_str_include, include_js_files};
|
#[cfg(feature = "target-os")]
|
||||||
|
pub use habitica::*;
|
||||||
|
|
||||||
js::part_from_file(
|
#[cfg(feature = "target-os")]
|
||||||
mize,
|
pub mod c2vi;
|
||||||
"habitica",
|
#[cfg(feature = "target-os")]
|
||||||
ascii_str_include!("./deno_dist/habitica.js"),
|
pub use c2vi::*;
|
||||||
);
|
|
||||||
}
|
//#[cfg(feature = "target-os")]
|
||||||
|
//pub mod js;
|
||||||
|
//#[cfg(feature = "target-os")]
|
||||||
|
//pub use js::*;
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "marts",
|
"name": "@ppc/marts",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"packageManager": "pnpm@10.15.1",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@flowershow/remark-wiki-link": "^3.4.0",
|
"@flowershow/remark-wiki-link": "^3.4.0",
|
||||||
"builtin-modules": "^5.0.0",
|
"builtin-modules": "^5.0.0",
|
||||||
"esbuild": "^0.27.3",
|
"esbuild": "^0.27.3",
|
||||||
"mdast": "^3.0.0",
|
"mdast": "^3.0.0",
|
||||||
"obsidian": "^1.12.2",
|
"obsidian": "^1.12.3",
|
||||||
"remark-frontmatter": "^5.0.0",
|
"remark-frontmatter": "^5.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { id, i, init, InstaQLEntity } from "@instantdb/react";
|
|
||||||
import remarkParse from "remark-parse";
|
import remarkParse from "remark-parse";
|
||||||
import remarkStringify from "remark-stringify";
|
import remarkStringify from "remark-stringify";
|
||||||
import { unified } from "unified";
|
import { unified } from "unified";
|
||||||
@@ -7,6 +6,7 @@ import { Root, Content, Heading, List, ListItem, Text } from "mdast";
|
|||||||
import remarkFrontmatter from "remark-frontmatter";
|
import remarkFrontmatter from "remark-frontmatter";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import wikiLinkPlugin from "@flowershow/remark-wiki-link";
|
import wikiLinkPlugin from "@flowershow/remark-wiki-link";
|
||||||
|
import { TFile } from "obsidian";
|
||||||
|
|
||||||
const OTASK_PROP_NAMES = ["short", "outcome", "priority"];
|
const OTASK_PROP_NAMES = ["short", "outcome", "priority"];
|
||||||
const ACTION_PROP_NAMES = [];
|
const ACTION_PROP_NAMES = [];
|
||||||
@@ -77,7 +77,7 @@ async function full_parse() {
|
|||||||
render_sub0_file(OTASK_LIST);
|
render_sub0_file(OTASK_LIST);
|
||||||
}
|
}
|
||||||
|
|
||||||
let OTASK_PARSE_QUEUE = [];
|
let OTASK_PARSE_QUEUE: any = [];
|
||||||
let OTASK_LIST = [];
|
let OTASK_LIST = [];
|
||||||
let DBG = false;
|
let DBG = false;
|
||||||
|
|
||||||
@@ -201,6 +201,7 @@ async function replaceHeading(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function otasks_from_list_item(listItem, otask_path, priority = 0) {
|
async function otasks_from_list_item(listItem, otask_path, priority = 0) {
|
||||||
|
try {
|
||||||
// the main otask this listItem is about
|
// the main otask this listItem is about
|
||||||
let otask = {
|
let otask = {
|
||||||
description: [],
|
description: [],
|
||||||
@@ -303,14 +304,22 @@ async function otasks_from_list_item(listItem, otask_path, priority = 0) {
|
|||||||
|
|
||||||
// check ot
|
// check ot
|
||||||
if (text.startsWith("ot:")) {
|
if (text.startsWith("ot:")) {
|
||||||
queue_otasks_from_list_item(subListItem, new_otask_path, otask.priority);
|
queue_otasks_from_list_item(
|
||||||
|
subListItem,
|
||||||
|
new_otask_path,
|
||||||
|
otask.priority,
|
||||||
|
);
|
||||||
|
|
||||||
continue; // don't do any other processing on this sub-listItem
|
continue; // don't do any other processing on this sub-listItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// check [ ]
|
// check [ ]
|
||||||
if (text.startsWith("[ ] ") || text.startsWith("[x] ")) {
|
if (text.startsWith("[ ] ") || text.startsWith("[x] ")) {
|
||||||
queue_otasks_from_list_item(subListItem, new_otask_path, otask.priority);
|
queue_otasks_from_list_item(
|
||||||
|
subListItem,
|
||||||
|
new_otask_path,
|
||||||
|
otask.priority,
|
||||||
|
);
|
||||||
|
|
||||||
continue; // don't do any other processing on this sub-listItem
|
continue; // don't do any other processing on this sub-listItem
|
||||||
}
|
}
|
||||||
@@ -344,9 +353,13 @@ async function otasks_from_list_item(listItem, otask_path, priority = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return otask;
|
return otask;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("failed to parse list item:", listItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function otasks_from_file(file: TFile, otask_path, priority = 0) {
|
async function otasks_from_file(file: TFile, otask_path, priority = 0) {
|
||||||
|
try {
|
||||||
dbg("SystemC2: otasks_from_file", file, otask_path);
|
dbg("SystemC2: otasks_from_file", file, otask_path);
|
||||||
|
|
||||||
// the main otask this file is about
|
// the main otask this file is about
|
||||||
@@ -410,6 +423,9 @@ async function otasks_from_file(file: TFile, otask_path, priority = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return otask;
|
return otask;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("failed to parse file: ", file.path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function action_from_list_item(listItem, otask_path) {
|
function action_from_list_item(listItem, otask_path) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ required-features = [ "target-os" ]
|
|||||||
[features]
|
[features]
|
||||||
default = [ "target-os" ]
|
default = [ "target-os" ]
|
||||||
target-os= [ "clap", "home", "nix", "sysinfo", "ciborium/default", "tracing-subscriber", "async", "tracing-subscriber/env-filter", "libloading", "ciborium-io", "tracing-core", "tar", "flate2", "http_req" ]
|
target-os= [ "clap", "home", "nix", "sysinfo", "ciborium/default", "tracing-subscriber", "async", "tracing-subscriber/env-filter", "libloading", "ciborium-io", "tracing-core", "tar", "flate2", "http_req" ]
|
||||||
target-wasm = [ "wasm-bindgen", "console_error_panic_hook", "web-sys", "web-sys/Worker", "web-sys/Window", "web-sys/Request", "web-sys/RequestInit", "web-sys/RequestMode", "web-sys/Response", "web-sys/WorkerOptions", "web-sys/WorkerType", "serde-wasm-bindgen", "wasm-bindgen-futures", "wee_alloc"]
|
target-js = [ "wasm-bindgen", "console_error_panic_hook", "web-sys", "web-sys/Worker", "web-sys/Window", "web-sys/Request", "web-sys/RequestInit", "web-sys/RequestMode", "web-sys/Response", "web-sys/WorkerOptions", "web-sys/WorkerType", "serde-wasm-bindgen", "wasm-bindgen-futures", "wee_alloc"]
|
||||||
async = ["tokio/net", "tokio", "tokio/rt-multi-thread", "tokio/io-util"]
|
async = ["tokio/net", "tokio", "tokio/rt-multi-thread", "tokio/io-util"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@ppc/mize",
|
"name": "@ppc/mize",
|
||||||
"imports": {},
|
"imports": {},
|
||||||
"exports": {}
|
"exports": "./src/platform/js/index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
export function add(a: number, b: number): number {
|
|
||||||
return a + b;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts
|
|
||||||
if (import.meta.main) {
|
|
||||||
console.log("Add 2 + 3 =", add(2, 3));
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { assertEquals } from "@std/assert";
|
|
||||||
import { add } from "./main.ts";
|
|
||||||
|
|
||||||
Deno.test(function addTest() {
|
|
||||||
assertEquals(add(2, 3), 5);
|
|
||||||
});
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
|
use crate::mize_err;
|
||||||
use crate::MizeResult;
|
use crate::MizeResult;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use crate::item::ItemData;
|
use crate::item::ItemData;
|
||||||
use crate::Mize;
|
use crate::Mize;
|
||||||
|
use crate::MizeError;
|
||||||
|
|
||||||
type ConfigThunk = Box<dyn Fn() -> ItemData + Send + Sync>;
|
type ConfigThunk = Box<dyn Fn() -> ItemData + Send + Sync>;
|
||||||
|
|
||||||
@@ -42,5 +45,38 @@ pub fn gather_config(mize: &mut Mize) -> MizeResult<()> {
|
|||||||
part.as_deref().unwrap().opts(&mut mize.clone());
|
part.as_deref().unwrap().opts(&mut mize.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// populate values from config files
|
||||||
|
let config_file_paths = env::var("MIZE_CONFIG_FILES")?;
|
||||||
|
for config_file_path in config_file_paths.split(":") {
|
||||||
|
println!("reading config file: {config_file_path}");
|
||||||
|
let content = std::fs::read_to_string(config_file_path)?;
|
||||||
|
let mut data = ItemData::from_toml(content.as_str())?;
|
||||||
|
println!("config data: {data}");
|
||||||
|
for path in data.get_paths_recursive()? {
|
||||||
|
let mut conf_name = path.replace("/", ".");
|
||||||
|
if conf_name.starts_with(".") {
|
||||||
|
conf_name.remove(0);
|
||||||
|
}
|
||||||
|
println!("adding config {conf_name} from config file {config_file_path}");
|
||||||
|
println!("path: {:?}", path);
|
||||||
|
let val = data.get_path(path.split("/").filter(|s| s != &"").collect::<Vec<&str>>())?;
|
||||||
|
match config_opts.get_mut(&conf_name) {
|
||||||
|
Some(opt) => {
|
||||||
|
opt.val = Some(val);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
config_opts.insert(
|
||||||
|
conf_name.clone(),
|
||||||
|
ConfigOpt {
|
||||||
|
name: conf_name,
|
||||||
|
val: Some(val),
|
||||||
|
thunk: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ pub struct Mize {
|
|||||||
// TODO: set to a random uuid
|
// TODO: set to a random uuid
|
||||||
pub(crate) self_namespace: Arc<Mutex<Namespace>>,
|
pub(crate) self_namespace: Arc<Mutex<Namespace>>,
|
||||||
pub(crate) op_tx: Sender<Operation>,
|
pub(crate) op_tx: Sender<Operation>,
|
||||||
threads: Arc<Mutex<Vec<(u32, String)>>>,
|
threads: Arc<Mutex<Vec<(u32, String, Option<JoinHandle<MizeResult<()>>>)>>>,
|
||||||
next_thread_id: Arc<Mutex<u32>>,
|
next_thread_id: Arc<Mutex<u32>>,
|
||||||
give_msg_wait: Arc<Mutex<HashMap<MizeId, Vec<Sender<ItemData>>>>>,
|
give_msg_wait: Arc<Mutex<HashMap<MizeId, Vec<Sender<ItemData>>>>>,
|
||||||
create_msg_wait: Arc<Mutex<Option<Sender<MizeId>>>>,
|
create_msg_wait: Arc<Mutex<Option<Sender<MizeId>>>>,
|
||||||
@@ -254,11 +254,11 @@ impl Mize {
|
|||||||
let instance_clone = instance.clone();
|
let instance_clone = instance.clone();
|
||||||
let op_rx_clone = op_rx.clone();
|
let op_rx_clone = op_rx.clone();
|
||||||
let closure = move || updater_thread(op_rx_clone, &instance_clone);
|
let closure = move || updater_thread(op_rx_clone, &instance_clone);
|
||||||
instance.spawn("updater_thread", closure)?;
|
instance.spawn_background("updater_thread", closure)?;
|
||||||
|
|
||||||
let instance_clone_two = instance.clone();
|
let instance_clone_two = instance.clone();
|
||||||
let closure_two = move || updater_thread(op_rx, &instance_clone_two);
|
let closure_two = move || updater_thread(op_rx, &instance_clone_two);
|
||||||
instance.spawn("updater_thread", closure_two)?;
|
instance.spawn_background("updater_thread", closure_two)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up async update "threads" when using wasm
|
// set up async update "threads" when using wasm
|
||||||
@@ -296,7 +296,7 @@ impl Mize {
|
|||||||
|
|
||||||
let mut instance = Mize::empty()?;
|
let mut instance = Mize::empty()?;
|
||||||
|
|
||||||
instance.init();
|
instance.init()?;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"instance inited with config: {}",
|
"instance inited with config: {}",
|
||||||
@@ -312,9 +312,12 @@ impl Mize {
|
|||||||
part: Some(part.take().unwrap()),
|
part: Some(part.take().unwrap()),
|
||||||
mize: self.clone(),
|
mize: self.clone(),
|
||||||
}),
|
}),
|
||||||
None => Err(mize_err!("Part not found or currently taken")),
|
None => Err(mize_err!("Part '{name}' not found or currently taken")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn has_part(&mut self, name: &str) -> bool {
|
||||||
|
self.parts.lock().unwrap().contains_key(name)
|
||||||
|
}
|
||||||
pub fn add_part(&mut self, part: Box<dyn MizePart + Send + Sync>) -> MizeResult<()> {
|
pub fn add_part(&mut self, part: Box<dyn MizePart + Send + Sync>) -> MizeResult<()> {
|
||||||
self.part_names.lock().unwrap().push(part.name());
|
self.part_names.lock().unwrap().push(part.name());
|
||||||
self.parts.lock().unwrap().insert(part.name(), Some(part));
|
self.parts.lock().unwrap().insert(part.name(), Some(part));
|
||||||
@@ -566,7 +569,7 @@ impl Mize {
|
|||||||
|
|
||||||
pub fn add_listener<T: ConnListener + 'static>(&mut self, listener: T) -> MizeResult<()> {
|
pub fn add_listener<T: ConnListener + 'static>(&mut self, listener: T) -> MizeResult<()> {
|
||||||
let mut instance_clone = self.clone();
|
let mut instance_clone = self.clone();
|
||||||
self.spawn("some_listener", move || listener.listen(instance_clone));
|
self.spawn_background("some_listener", move || listener.listen(instance_clone));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,7 +667,60 @@ impl Mize {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn(
|
pub fn spawn_and_wait(
|
||||||
|
&mut self,
|
||||||
|
name: &str,
|
||||||
|
func: impl FnOnce() -> MizeResult<()> + Send + 'static,
|
||||||
|
) -> MizeResult<()> {
|
||||||
|
let mize_clone = self.clone();
|
||||||
|
let mut threads_inner = self.threads.lock()?;
|
||||||
|
let mut next_thread_id = self.next_thread_id.lock()?;
|
||||||
|
|
||||||
|
let my_thread_id_no_mutex_guard = *next_thread_id;
|
||||||
|
let thread_mutex = self.threads.clone();
|
||||||
|
let name_to_move = name.to_owned();
|
||||||
|
let to_spawn = move || -> MizeResult<()> {
|
||||||
|
debug!("spawning thread: {}", name_to_move);
|
||||||
|
let my_thread_id = my_thread_id_no_mutex_guard;
|
||||||
|
|
||||||
|
if let Err(err) = func() {
|
||||||
|
mize_clone.report_err(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
//let mut threads_inner = thread_mutex.lock()?;
|
||||||
|
/*
|
||||||
|
*threads_inner = threads_inner
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|el| match el {
|
||||||
|
(my_thread_id, _) => false,
|
||||||
|
(_, _) => true,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
*/
|
||||||
|
debug!("thread '{}' stopped", name_to_move);
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
*next_thread_id += 1;
|
||||||
|
|
||||||
|
#[cfg(feature = "target-os")]
|
||||||
|
let handle = thread::spawn(move || to_spawn());
|
||||||
|
|
||||||
|
#[cfg(feature = "target-os")]
|
||||||
|
threads_inner.push((*next_thread_id, name.to_owned(), Some(handle)));
|
||||||
|
|
||||||
|
#[cfg(feature = "target-wasm ")]
|
||||||
|
{
|
||||||
|
//console_log!("in instance::spawn with wasm target")
|
||||||
|
}
|
||||||
|
//NOT WELL SUPPORTED
|
||||||
|
//crate::platform::wasm::wasm_spawn(to_spawn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_background(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: &str,
|
name: &str,
|
||||||
func: impl FnOnce() -> MizeResult<()> + Send + 'static,
|
func: impl FnOnce() -> MizeResult<()> + Send + 'static,
|
||||||
@@ -672,8 +728,6 @@ impl Mize {
|
|||||||
let mut threads_inner = self.threads.lock()?;
|
let mut threads_inner = self.threads.lock()?;
|
||||||
let mut next_thread_id = self.next_thread_id.lock()?;
|
let mut next_thread_id = self.next_thread_id.lock()?;
|
||||||
|
|
||||||
threads_inner.push((*next_thread_id, name.to_owned()));
|
|
||||||
|
|
||||||
let my_thread_id_no_mutex_guard = *next_thread_id;
|
let my_thread_id_no_mutex_guard = *next_thread_id;
|
||||||
let thread_mutex = self.threads.clone();
|
let thread_mutex = self.threads.clone();
|
||||||
let name_to_move = name.to_owned();
|
let name_to_move = name.to_owned();
|
||||||
@@ -684,6 +738,7 @@ impl Mize {
|
|||||||
func()?;
|
func()?;
|
||||||
|
|
||||||
let mut threads_inner = thread_mutex.lock()?;
|
let mut threads_inner = thread_mutex.lock()?;
|
||||||
|
/*
|
||||||
*threads_inner = threads_inner
|
*threads_inner = threads_inner
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -692,6 +747,7 @@ impl Mize {
|
|||||||
(_, _) => true,
|
(_, _) => true,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
*/
|
||||||
debug!("thread '{}' stopped", name_to_move);
|
debug!("thread '{}' stopped", name_to_move);
|
||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
@@ -701,6 +757,8 @@ impl Mize {
|
|||||||
#[cfg(feature = "target-os")]
|
#[cfg(feature = "target-os")]
|
||||||
thread::spawn(move || to_spawn());
|
thread::spawn(move || to_spawn());
|
||||||
|
|
||||||
|
threads_inner.push((*next_thread_id, name.to_owned(), None));
|
||||||
|
|
||||||
#[cfg(feature = "target-wasm ")]
|
#[cfg(feature = "target-wasm ")]
|
||||||
{
|
{
|
||||||
//console_log!("in instance::spawn with wasm target")
|
//console_log!("in instance::spawn with wasm target")
|
||||||
@@ -744,7 +802,7 @@ impl Mize {
|
|||||||
let mut threads_inner = self.threads.lock()?;
|
let mut threads_inner = self.threads.lock()?;
|
||||||
let mut next_thread_id = self.next_thread_id.lock()?;
|
let mut next_thread_id = self.next_thread_id.lock()?;
|
||||||
|
|
||||||
threads_inner.push((*next_thread_id, name.to_owned()));
|
threads_inner.push((*next_thread_id, name.to_owned(), None));
|
||||||
let runtime_inner = self.runtime.lock().unwrap();
|
let runtime_inner = self.runtime.lock().unwrap();
|
||||||
runtime_inner.spawn(func);
|
runtime_inner.spawn(func);
|
||||||
|
|
||||||
@@ -777,7 +835,7 @@ impl Mize {
|
|||||||
.lock()
|
.lock()
|
||||||
.expect("mutex lock failed in spawn_async_blocking");
|
.expect("mutex lock failed in spawn_async_blocking");
|
||||||
|
|
||||||
threads_inner.push((*next_thread_id, name.to_owned()));
|
threads_inner.push((*next_thread_id, name.to_owned(), None));
|
||||||
let runtime_inner = self.runtime.lock().unwrap();
|
let runtime_inner = self.runtime.lock().unwrap();
|
||||||
let handle = runtime_inner.handle().to_owned();
|
let handle = runtime_inner.handle().to_owned();
|
||||||
*next_thread_id += 1;
|
*next_thread_id += 1;
|
||||||
@@ -811,6 +869,7 @@ impl Mize {
|
|||||||
|
|
||||||
// get the cached value
|
// get the cached value
|
||||||
if let Some(val) = opt.val.clone() {
|
if let Some(val) = opt.val.clone() {
|
||||||
|
println!("get_config: {name} has cached value: {val}");
|
||||||
return Ok(val);
|
return Ok(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,12 +888,17 @@ impl Mize {
|
|||||||
|
|
||||||
pub fn new_opt(&mut self, name: &str) -> ConfigOptNameAndMize {
|
pub fn new_opt(&mut self, name: &str) -> ConfigOptNameAndMize {
|
||||||
let mut config_opts = self.config_opts.lock().unwrap();
|
let mut config_opts = self.config_opts.lock().unwrap();
|
||||||
|
match config_opts.get_mut(name) {
|
||||||
|
Some(opt) => {}
|
||||||
|
None => {
|
||||||
let opt = ConfigOpt {
|
let opt = ConfigOpt {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
val: None,
|
val: None,
|
||||||
thunk: None,
|
thunk: None,
|
||||||
};
|
};
|
||||||
config_opts.insert(name.to_string(), opt);
|
config_opts.insert(name.to_string(), opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
ConfigOptNameAndMize {
|
ConfigOptNameAndMize {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
mize: self.clone(),
|
mize: self.clone(),
|
||||||
@@ -842,9 +906,17 @@ impl Mize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(&mut self) -> MizeResult<()> {
|
pub fn run(&mut self) -> MizeResult<()> {
|
||||||
let mut parts = self.parts.lock().unwrap();
|
let part_names = self.part_names();
|
||||||
for part in parts.values_mut() {
|
for part_name in part_names {
|
||||||
part.as_deref_mut().unwrap().run(&mut self.clone())?;
|
let mut part = self.get_part(part_name)?;
|
||||||
|
//println!("running part {part_name}");
|
||||||
|
part.run(&mut self.clone())?;
|
||||||
|
}
|
||||||
|
let mut threads_inner = self.threads.lock().unwrap();
|
||||||
|
for (id, name, handle) in threads_inner.drain(..) {
|
||||||
|
if let Some(handle) = handle {
|
||||||
|
handle.join().unwrap()?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -855,6 +927,7 @@ impl Mize {
|
|||||||
) -> MizeResult<MizePartGuard<T>> {
|
) -> MizeResult<MizePartGuard<T>> {
|
||||||
let mut dyn_guard = self.get_part(name)?;
|
let mut dyn_guard = self.get_part(name)?;
|
||||||
let part = dyn_guard.part.take().unwrap();
|
let part = dyn_guard.part.take().unwrap();
|
||||||
|
drop(dyn_guard);
|
||||||
let concrete_part = part.into_any().downcast::<T>().unwrap();
|
let concrete_part = part.into_any().downcast::<T>().unwrap();
|
||||||
Ok(MizePartGuard {
|
Ok(MizePartGuard {
|
||||||
mize: self.clone(),
|
mize: self.clone(),
|
||||||
|
|||||||
@@ -205,6 +205,13 @@ impl ItemData {
|
|||||||
return Ok(value.into_item_data());
|
return Ok(value.into_item_data());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_toml(&self) -> MizeResult<String> {
|
||||||
|
let mut out_string = String::new();
|
||||||
|
let toml_serializer = toml::Serializer::new(&mut out_string);
|
||||||
|
CborValue::serialize(&self.cbor(), toml_serializer)?;
|
||||||
|
Ok(out_string)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn value_string(self) -> MizeResult<String> {
|
pub fn value_string(self) -> MizeResult<String> {
|
||||||
match self.cbor() {
|
match self.cbor() {
|
||||||
CborValue::Text(string) => Ok(string.to_owned()),
|
CborValue::Text(string) => Ok(string.to_owned()),
|
||||||
@@ -212,6 +219,13 @@ impl ItemData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn value_bool(self) -> MizeResult<bool> {
|
||||||
|
match self.cbor() {
|
||||||
|
CborValue::Bool(val) => Ok(*val),
|
||||||
|
_ => Err(mize_err!("this ItemData has no string value")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn from_string<S: Into<String>>(into_string: S) -> ItemData {
|
pub fn from_string<S: Into<String>>(into_string: S) -> ItemData {
|
||||||
let string = into_string.into();
|
let string = into_string.into();
|
||||||
let data = CborValue::Text(string);
|
let data = CborValue::Text(string);
|
||||||
@@ -286,6 +300,46 @@ impl ItemData {
|
|||||||
.to_owned()
|
.to_owned()
|
||||||
.into_item_data())
|
.into_item_data())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_paths_recursive(&mut self) -> MizeResult<Vec<String>> {
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
let mut path_accu = Vec::new();
|
||||||
|
get_paths(&mut self.0, &mut paths, &mut path_accu, true)?;
|
||||||
|
Ok(paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_paths(&mut self) -> MizeResult<Vec<String>> {
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
let mut path_accu = Vec::new();
|
||||||
|
get_paths(&mut self.0, &mut paths, &mut path_accu, false)?;
|
||||||
|
Ok(paths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_paths(
|
||||||
|
val: &mut CborValue,
|
||||||
|
paths: &mut Vec<String>,
|
||||||
|
path_accu: &mut Vec<String>,
|
||||||
|
recurse: bool,
|
||||||
|
) -> MizeResult<()> {
|
||||||
|
let map = val
|
||||||
|
.as_map_mut()
|
||||||
|
.ok_or(mize_err!("ItemData is not map... when calling get_paths"))?;
|
||||||
|
for (key, val) in map {
|
||||||
|
let text = key
|
||||||
|
.as_text()
|
||||||
|
.ok_or(mize_err!("ItemData::get_paths but key is not of type text"))?
|
||||||
|
.to_owned();
|
||||||
|
if recurse && val.is_map() {
|
||||||
|
let mut path_accu = path_accu.clone();
|
||||||
|
path_accu.push(text);
|
||||||
|
get_paths(val, paths, &mut path_accu, recurse)?;
|
||||||
|
} else {
|
||||||
|
let path = path_accu.join("/") + "/" + text.as_str();
|
||||||
|
paths.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ItemData {
|
impl Default for ItemData {
|
||||||
|
|||||||
@@ -57,41 +57,41 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
// platform specific stuff
|
// platform specific stuff
|
||||||
pub mod platform {
|
pub mod platform {
|
||||||
#[cfg(feature = "wasm-target")]
|
#[cfg(feature = "target-js")]
|
||||||
pub mod wasm;
|
pub mod js;
|
||||||
|
|
||||||
#[cfg(feature = "target-os")]
|
#[cfg(feature = "target-os")]
|
||||||
pub mod os;
|
pub mod os;
|
||||||
|
|
||||||
pub mod any {
|
pub mod any {
|
||||||
//////////// instance_init
|
//////////// instance_init
|
||||||
#[cfg(feature = "wasm-target")]
|
#[cfg(feature = "target-js")]
|
||||||
pub use super::wasm::wasm_instance_init as instance_init;
|
pub use super::js::wasm_instance_init as instance_init;
|
||||||
|
|
||||||
#[cfg(feature = "target-os")]
|
#[cfg(feature = "target-os")]
|
||||||
pub use super::os::os_instance_init as instance_init;
|
pub use super::os::os_instance_init as instance_init;
|
||||||
|
|
||||||
#[cfg(not(any(feature = "target-os", feature = "target-wasm ")))]
|
#[cfg(not(any(feature = "target-os", feature = "target-js")))]
|
||||||
pub use super::super::instance_init;
|
pub use super::super::instance_init;
|
||||||
|
|
||||||
//////////// load_module
|
//////////// load_module
|
||||||
#[cfg(feature = "target-os")]
|
#[cfg(feature = "target-os")]
|
||||||
pub use super::os::load_module;
|
pub use super::os::load_module;
|
||||||
|
|
||||||
#[cfg(feature = "wasm-target")]
|
#[cfg(feature = "target-js")]
|
||||||
pub use super::wasm::load_module;
|
pub use super::js::load_module;
|
||||||
|
|
||||||
#[cfg(not(any(feature = "target-os", feature = "target-wasm ")))]
|
#[cfg(not(any(feature = "target-os", feature = "target-js")))]
|
||||||
pub use super::super::load_module;
|
pub use super::super::load_module;
|
||||||
|
|
||||||
//////////// fetch_module
|
//////////// fetch_module
|
||||||
#[cfg(feature = "target-os")]
|
#[cfg(feature = "target-os")]
|
||||||
pub use super::os::fetch_module;
|
pub use super::os::fetch_module;
|
||||||
|
|
||||||
#[cfg(feature = "wasm-target")]
|
#[cfg(feature = "target-js")]
|
||||||
pub use super::super::fetch_module;
|
pub use super::super::fetch_module;
|
||||||
|
|
||||||
#[cfg(not(any(feature = "target-os", feature = "target-wasm ")))]
|
#[cfg(not(any(feature = "target-os", feature = "target-js")))]
|
||||||
pub use super::super::fetch_module;
|
pub use super::super::fetch_module;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
packages/mize/src/platform/js/deno.json
Normal file
8
packages/mize/src/platform/js/deno.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"tasks": {
|
||||||
|
"dev": "deno run --watch main.ts"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"@std/assert": "jsr:@std/assert@1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,79 +1,72 @@
|
|||||||
use core::convert::From;
|
|
||||||
use core::option::Option;
|
|
||||||
use std::ptr::NonNull;
|
|
||||||
use std::panic;
|
|
||||||
use web_sys::js_sys::{self, eval, Promise};
|
|
||||||
use crate::id::MizeId;
|
use crate::id::MizeId;
|
||||||
use crate::item::{Item, ItemData};
|
use crate::item::{Item, ItemData};
|
||||||
use crate::platform::wasm::js_sys::Function;
|
use crate::platform::js::js_sys::Function;
|
||||||
use web_sys::{WorkerOptions, WorkerType};
|
use core::convert::From;
|
||||||
|
use core::option::Option;
|
||||||
|
use std::panic;
|
||||||
|
use std::ptr::NonNull;
|
||||||
|
use web_sys::js_sys::{self, eval, Promise};
|
||||||
use web_sys::Worker;
|
use web_sys::Worker;
|
||||||
|
use web_sys::{WorkerOptions, WorkerType};
|
||||||
|
|
||||||
use crate::instance::{self, Instance};
|
|
||||||
use crate::error::MizeResult;
|
|
||||||
use crate::{mize_err, Module};
|
|
||||||
use crate::MizeError;
|
|
||||||
use crate::core::item::IntoItemData;
|
use crate::core::item::IntoItemData;
|
||||||
|
use crate::error::MizeResult;
|
||||||
use crate::instance::module::EmptyModule;
|
use crate::instance::module::EmptyModule;
|
||||||
|
use crate::Mize;
|
||||||
|
use crate::MizeError;
|
||||||
|
use crate::{mize_err, Module};
|
||||||
|
|
||||||
// console_log macro
|
// console_log macro
|
||||||
// that can be copied into other files for debugging purposes
|
// that can be copied into other files for debugging purposes
|
||||||
#[cfg(feature = "wasm-target")]
|
#[cfg(feature = "target-js")]
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
#[cfg(feature = "wasm-target")]
|
#[cfg(feature = "target-js")]
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#[wasm_bindgen(js_namespace = console)]
|
#[wasm_bindgen(js_namespace = console)]
|
||||||
fn log(s: &str);
|
fn log(s: &str);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "wasm-target")]
|
#[cfg(feature = "target-js")]
|
||||||
macro_rules! console_log {
|
macro_rules! console_log {
|
||||||
// Note that this is using the `log` function imported above during
|
// Note that this is using the `log` function imported above during
|
||||||
// `bare_bones`
|
// `bare_bones`
|
||||||
($($t:tt)*) => (unsafe { log(&format_args!($($t)*).to_string())})
|
($($t:tt)*) => (unsafe { log(&format_args!($($t)*).to_string())})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "wasm-target"))]
|
#[cfg(not(feature = "target-js"))]
|
||||||
macro_rules! console_log {
|
macro_rules! console_log {
|
||||||
// Note that this is using the `log` function imported above during
|
// Note that this is using the `log` function imported above during
|
||||||
// `bare_bones`
|
// `bare_bones`
|
||||||
($($t:tt)*) => ()
|
($($t:tt)*) => {};
|
||||||
}
|
}
|
||||||
//end of console_log macro
|
//end of console_log macro
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||||
|
|
||||||
|
pub fn wasm_instance_init(mize: &mut Mize) -> MizeResult<()> {
|
||||||
pub fn wasm_instance_init(instance: &mut Instance) -> MizeResult<()> {
|
|
||||||
console_log!("Hello world from wasm_instance_init!!!!!!!!!!");
|
console_log!("Hello world from wasm_instance_init!!!!!!!!!!");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_module(instance: &mut Instance, name: &str, path: Option<String>) -> MizeResult<()> {
|
pub fn load_module(mize: &mut Mize, name: &str, path: Option<String>) -> MizeResult<()> {
|
||||||
console_log!("loading module: {}", name);
|
console_log!("loading module: {}", name);
|
||||||
|
|
||||||
wasm_bindgen_futures::spawn_local(load_module_async(instance.clone(), name.to_owned(), path));
|
wasm_bindgen_futures::spawn_local(load_module_async(mize.clone(), name.to_owned(), path));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_module_async(mut instance: Instance, name: String, path: Option<String>) -> () {
|
async fn load_module_async(mut mize: Mize, name: String, path: Option<String>) -> () {
|
||||||
|
|
||||||
let name = name.as_str();
|
let name = name.as_str();
|
||||||
let module_url = fetch_module(&mut instance, name).unwrap();
|
let module_url = fetch_module(&mut mize, name).unwrap();
|
||||||
let memory = wasm_bindgen::memory();
|
let memory = wasm_bindgen::memory();
|
||||||
|
|
||||||
let mut empty_module: Box<dyn Module + Send + Sync> = Box::new(EmptyModule {});
|
let mut empty_module: Box<dyn Module + Send + Sync> = Box::new(EmptyModule {});
|
||||||
let mut mize = instance.clone();
|
let mut mize = mize.clone();
|
||||||
|
|
||||||
let empty_module_ptr = Box::into_raw(Box::new(empty_module));
|
let empty_module_ptr = Box::into_raw(Box::new(empty_module));
|
||||||
let empty_module_usize = empty_module_ptr as usize;
|
let empty_module_usize = empty_module_ptr as usize;
|
||||||
@@ -81,7 +74,8 @@ async fn load_module_async(mut instance: Instance, name: String, path: Option<St
|
|||||||
|
|
||||||
console_log!("testttttttttttttt in load_module_async...");
|
console_log!("testttttttttttttt in load_module_async...");
|
||||||
|
|
||||||
let function_str = format!(r#"
|
let function_str = format!(
|
||||||
|
r#"
|
||||||
const promise = new Promise((resolve, reject) => {{
|
const promise = new Promise((resolve, reject) => {{
|
||||||
import("{module_url}/mize_module_{name}.js").then( module => {{
|
import("{module_url}/mize_module_{name}.js").then( module => {{
|
||||||
const wasm_bindgen = module.get_wasm_bindgen();
|
const wasm_bindgen = module.get_wasm_bindgen();
|
||||||
@@ -97,18 +91,23 @@ async fn load_module_async(mut instance: Instance, name: String, path: Option<St
|
|||||||
}})
|
}})
|
||||||
}})
|
}})
|
||||||
return promise;
|
return promise;
|
||||||
"#);
|
"#
|
||||||
|
);
|
||||||
|
|
||||||
let function = Function::new_with_args("memory, empty_module_usize, mize_usize", &function_str);
|
let function = Function::new_with_args("memory, empty_module_usize, mize_usize", &function_str);
|
||||||
|
|
||||||
let ret_value: JsValue = match function.call3(&JsValue::null(), &memory, &empty_module_usize.into(), &mize_usize.into()) {
|
let ret_value: JsValue = match function.call3(
|
||||||
|
&JsValue::null(),
|
||||||
|
&memory,
|
||||||
|
&empty_module_usize.into(),
|
||||||
|
&mize_usize.into(),
|
||||||
|
) {
|
||||||
Ok(val) => val,
|
Ok(val) => val,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
console_log!("failed to load the wasm mize module....");
|
console_log!("failed to load the wasm mize module....");
|
||||||
console_log!("{:?}", err);
|
console_log!("{:?}", err);
|
||||||
return;
|
return;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console_log!("got ret_value: {:?}", ret_value);
|
console_log!("got ret_value: {:?}", ret_value);
|
||||||
@@ -119,20 +118,17 @@ async fn load_module_async(mut instance: Instance, name: String, path: Option<St
|
|||||||
console_log!("err awaiting ret_promise");
|
console_log!("err awaiting ret_promise");
|
||||||
console_log!("{:?}", err);
|
console_log!("{:?}", err);
|
||||||
return;
|
return;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut filled_module = unsafe {
|
let mut filled_module = unsafe { Box::from_raw(empty_module_ptr) };
|
||||||
Box::from_raw(empty_module_ptr)
|
|
||||||
};
|
|
||||||
|
|
||||||
console_log!("before....");
|
console_log!("before....");
|
||||||
let a = instance.get("0/config");
|
//let a = mize.get("0/config");
|
||||||
console_log!("after....");
|
console_log!("after....");
|
||||||
|
|
||||||
//filled_module.init(&instance);
|
//filled_module.init(&instance);
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
console_log!("got ret_result: {:?}", ret_result);
|
console_log!("got ret_result: {:?}", ret_result);
|
||||||
let ret_f64 = ret_result.as_f64().unwrap();
|
let ret_f64 = ret_result.as_f64().unwrap();
|
||||||
@@ -148,24 +144,25 @@ async fn load_module_async(mut instance: Instance, name: String, path: Option<St
|
|||||||
get_mize_module_fn(&mut module, instance.clone());
|
get_mize_module_fn(&mut module, instance.clone());
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_module(instance: &mut Instance, name: &str) -> MizeResult<String> {
|
pub fn fetch_module(mize: &mut Mize, name: &str) -> MizeResult<String> {
|
||||||
|
|
||||||
let module_name_with_slashes = name.replace(".", "/");
|
let module_name_with_slashes = name.replace(".", "/");
|
||||||
if let Ok(module_path) = instance.get(format!("self/config/module_dir/{}", module_name_with_slashes))?.value_string() {
|
if let Ok(module_path) = mize
|
||||||
|
.get(format!(
|
||||||
|
"self/config/module_dir/{}",
|
||||||
|
module_name_with_slashes
|
||||||
|
))?
|
||||||
|
.value_string()
|
||||||
|
{
|
||||||
return Ok(module_path);
|
return Ok(module_path);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
let module_url = instance.get("self/config/module_url")?.value_string()?;
|
let module_url = mize.get("self/config/module_url")?.value_string()?;
|
||||||
return Ok(format!("{}/{}", module_url, module_name_with_slashes));
|
return Ok(format!("{}/{}", module_url, module_name_with_slashes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_module(instance: &mut Instance, name: &str) -> MizeResult<js_sys::ArrayBuffer> {
|
async fn download_module(mize: &mut Mize, name: &str) -> MizeResult<js_sys::ArrayBuffer> {
|
||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::{Request, RequestInit, RequestMode, Response};
|
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||||
|
|
||||||
@@ -173,51 +170,57 @@ async fn download_module(instance: &mut Instance, name: &str) -> MizeResult<js_s
|
|||||||
opts.set_method("GET");
|
opts.set_method("GET");
|
||||||
opts.set_mode(RequestMode::Cors);
|
opts.set_mode(RequestMode::Cors);
|
||||||
|
|
||||||
let url = format!("{}/mize_module_{}_bg.wasm", fetch_module(instance, name)?, name);
|
let url = format!("{}/mize_module_{}_bg.wasm", fetch_module(mize, name)?, name);
|
||||||
|
|
||||||
let request = Request::new_with_str_and_init(&url, &opts).unwrap();
|
let request = Request::new_with_str_and_init(&url, &opts).unwrap();
|
||||||
|
|
||||||
let window = web_sys::window().unwrap();
|
let window = web_sys::window().unwrap();
|
||||||
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await.unwrap();
|
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let resp: Response = resp_value.dyn_into().unwrap();
|
let resp: Response = resp_value.dyn_into().unwrap();
|
||||||
|
|
||||||
let wasm_bytes: js_sys::ArrayBuffer = JsFuture::from(resp.array_buffer().unwrap()).await.unwrap().dyn_into().unwrap();
|
let wasm_bytes: js_sys::ArrayBuffer = JsFuture::from(resp.array_buffer().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
Ok(wasm_bytes)
|
Ok(wasm_bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub struct JsInstance {
|
pub struct JsMize {
|
||||||
inner: NonNull<Instance>,
|
inner: NonNull<Mize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub struct JsItemHandle {
|
pub struct JsItem {
|
||||||
instance: NonNull<Instance>,
|
mize: NonNull<Mize>,
|
||||||
id: MizeId,
|
id: MizeId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub async fn new_js_instance(config_json_str: String) -> JsInstance {
|
pub async fn new_js_instance(config_json_str: String) -> JsMize {
|
||||||
|
|
||||||
panic::set_hook(Box::new(console_error_panic_hook::hook));
|
panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||||
|
|
||||||
let config = ItemData::from_json(config_json_str).expect("parsing of json config failed");
|
let config = ItemData::from_json(config_json_str).expect("parsing of json config failed");
|
||||||
|
|
||||||
let mut instance = Instance::empty().expect("Instance::empty() failed");
|
let mut mize = Mize::empty().expect("Mize::empty() failed");
|
||||||
|
|
||||||
instance.set_blocking("0/config", config).expect("Failed to set the config at item 0");
|
mize.set_blocking("0/config", config)
|
||||||
|
.expect("Failed to set the config at item 0");
|
||||||
|
|
||||||
let mut js_instance = JsInstance { inner: NonNull::from(Box::leak(Box::new(instance))) };
|
let mut js_instance = JsMize {
|
||||||
|
inner: NonNull::from(Box::leak(Box::new(mize))),
|
||||||
|
};
|
||||||
|
|
||||||
return js_instance;
|
return js_instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
impl JsInstance {
|
impl JsMize {
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub unsafe fn init(&mut self) -> MizeResult<()> {
|
pub unsafe fn init(&mut self) -> MizeResult<()> {
|
||||||
self.inner.as_mut().init()
|
self.inner.as_mut().init()
|
||||||
@@ -231,43 +234,39 @@ impl JsInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub unsafe fn get_handle(&mut self, id: String) -> MizeResult<JsItemHandle> {
|
pub unsafe fn get_handle(&mut self, id: String) -> MizeResult<JsItem> {
|
||||||
let item = self.inner.as_mut().get(id)?;
|
let item = self.inner.as_mut().get(id)?;
|
||||||
Ok(JsItemHandle { instance: self.inner, id: item.id()})
|
Ok(JsItem {
|
||||||
}
|
mize: self.inner,
|
||||||
|
id: item.id(),
|
||||||
}
|
})
|
||||||
|
|
||||||
impl JsInstance {
|
|
||||||
pub fn inner(&mut self) -> &mut Instance {
|
|
||||||
unsafe {
|
|
||||||
self.inner.as_mut()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl JsMize {
|
||||||
|
pub fn inner(&mut self) -> &mut Mize {
|
||||||
|
unsafe { self.inner.as_mut() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
impl JsItemHandle {
|
impl JsItem {
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub unsafe fn value_string(&mut self) -> MizeResult<String> {
|
pub unsafe fn value_string(&mut self) -> MizeResult<String> {
|
||||||
let item = self.instance.as_mut().get(self.id.clone())?;
|
let item = self.mize.as_mut().get(self.id.clone())?;
|
||||||
let string = item.value_string()?;
|
let string = item.value_string()?;
|
||||||
Ok(string)
|
Ok(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub unsafe fn as_data_full(&mut self) -> MizeResult<JsValue> {
|
pub unsafe fn as_data_full(&mut self) -> MizeResult<JsValue> {
|
||||||
let item = self.instance.as_mut().get(self.id.clone())?;
|
let item = self.mize.as_mut().get(self.id.clone())?;
|
||||||
let data_raw = item.as_data_full()?;
|
let data_raw = item.as_data_full()?;
|
||||||
let jsvalue = serde_wasm_bindgen::to_value(data_raw.cbor())?;
|
let jsvalue = serde_wasm_bindgen::to_value(data_raw.cbor())?;
|
||||||
Ok(jsvalue)
|
Ok(jsvalue)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl From<MizeError> for JsValue {
|
impl From<MizeError> for JsValue {
|
||||||
fn from(value: MizeError) -> Self {
|
fn from(value: MizeError) -> Self {
|
||||||
let string = format!("{:?}", value);
|
let string = format!("{:?}", value);
|
||||||
@@ -58,7 +58,7 @@ async fn connect_async(mut instance: Mize, store_path: PathBuf) -> MizeResult<u6
|
|||||||
let conn_id = instance.new_connection(send_tx)?;
|
let conn_id = instance.new_connection(send_tx)?;
|
||||||
|
|
||||||
let cloned_instance = instance.clone();
|
let cloned_instance = instance.clone();
|
||||||
instance.spawn("incomming", move || {
|
instance.spawn_background("incomming", move || {
|
||||||
let result = unix_incomming(unix_read, cloned_instance, conn_id);
|
let result = unix_incomming(unix_read, cloned_instance, conn_id);
|
||||||
// if unix incomming fails, close the connection
|
// if unix incomming fails, close the connection
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
@@ -68,7 +68,7 @@ async fn connect_async(mut instance: Mize, store_path: PathBuf) -> MizeResult<u6
|
|||||||
});
|
});
|
||||||
|
|
||||||
let outgoing_cloned_instance = instance.clone();
|
let outgoing_cloned_instance = instance.clone();
|
||||||
instance.spawn("outgoing", move || {
|
instance.spawn_background("outgoing", move || {
|
||||||
let result = unix_outgoing(unix_write, send_rx, outgoing_cloned_instance, conn_id);
|
let result = unix_outgoing(unix_write, send_rx, outgoing_cloned_instance, conn_id);
|
||||||
// if writing fails, close this connection
|
// if writing fails, close this connection
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
@@ -108,7 +108,7 @@ async fn unix_listen(listener: UnixListener, mut instance: Mize) -> MizeResult<(
|
|||||||
|
|
||||||
let conn_id = instance.new_connection(send_tx)?;
|
let conn_id = instance.new_connection(send_tx)?;
|
||||||
let cloned_instance = instance.clone();
|
let cloned_instance = instance.clone();
|
||||||
instance.spawn("incomming", move || {
|
instance.spawn_background("incomming", move || {
|
||||||
let result = unix_incomming(unix_read, cloned_instance, conn_id);
|
let result = unix_incomming(unix_read, cloned_instance, conn_id);
|
||||||
// if unix incomming fails, close the connection
|
// if unix incomming fails, close the connection
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
@@ -118,7 +118,7 @@ async fn unix_listen(listener: UnixListener, mut instance: Mize) -> MizeResult<(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let outgoing_cloned_instance = instance.clone();
|
let outgoing_cloned_instance = instance.clone();
|
||||||
instance.spawn("outgoing", move || {
|
instance.spawn_background("outgoing", move || {
|
||||||
let result = unix_outgoing(unix_write, send_rx, outgoing_cloned_instance, conn_id);
|
let result = unix_outgoing(unix_write, send_rx, outgoing_cloned_instance, conn_id);
|
||||||
// if writing fails, close this connection
|
// if writing fails, close this connection
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
|
|||||||
10223
packages/ppc/Cargo.lock
generated
Normal file
10223
packages/ppc/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,33 @@ clap = { version = "4.5.59", optional = true }
|
|||||||
js-sys = { version = "0.3.85", optional = true }
|
js-sys = { version = "0.3.85", optional = true }
|
||||||
wasm-bindgen = { version = "0.2.108", optional = true }
|
wasm-bindgen = { version = "0.2.108", optional = true }
|
||||||
|
|
||||||
|
# server deps
|
||||||
|
axum = { version = "0.8.0", optional = true }
|
||||||
|
axum-extra = { version = "0.10.3", features = ["cookie-private"], optional = true }
|
||||||
|
openidconnect = { version = "4", features = ["reqwest", "rustls-tls"], optional = true }
|
||||||
|
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||||
|
serde_json = { version = "1.0", optional = true }
|
||||||
|
reqwest = { version = "0.11", features = ["rustls-tls"], optional = true }
|
||||||
|
rand = { version = "0.8", optional = true }
|
||||||
|
url = { version = "2", optional = true }
|
||||||
|
time = { version = "0.3", optional = true }
|
||||||
|
tokio = { version = "1.0", features = ["full"], optional = true }
|
||||||
|
dioxus = { version = "0.7.1", optional = true }
|
||||||
|
dioxus-ssr = { version = "0.5", optional = true }
|
||||||
|
html-escape = { version = "0.2.13", optional = true }
|
||||||
|
axum-oidc = { version = "1.0.0-dev-1", optional = true }
|
||||||
|
tracing = { version = "0.1.44", optional = true }
|
||||||
|
tower = { version = "0.5", optional = true }
|
||||||
|
tower-sessions = { version = "0.14.0", optional = true }
|
||||||
|
tower-http = { version = "0.6.8", features = ["trace"] }
|
||||||
|
webbrowser = { version = "1.2.0", optional = true }
|
||||||
|
base64 = { version = "0.22.1", optional = true }
|
||||||
|
dioxus-web = { version = "0.7.3", optional = true }
|
||||||
|
web-sys = { version = "0.3.91", optional = true }
|
||||||
|
spacetimedb-sdk = "2.1.0"
|
||||||
|
flume = "0.12.0"
|
||||||
|
chrono = "0.4.44"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "ppc"
|
name = "ppc"
|
||||||
path = "platform/os/main.rs"
|
path = "platform/os/main.rs"
|
||||||
@@ -20,9 +47,14 @@ path = "lib.rs"
|
|||||||
name = "ppc"
|
name = "ppc"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["target-os"]
|
default = ["target-os", "server", "client"]
|
||||||
target-os = [ "clap", "mize/target-os", "marts/target-os" ]
|
target-os = [ "clap", "mize/target-os", "marts/target-os", "tracing", "webbrowser", "base64", "serde_json", "serde" ]
|
||||||
target-obsidian = [ "js-sys", "wasm-bindgen", "mize/target-wasm", "marts/target-wasm" ]
|
target-obsidian = [ "js-sys", "wasm-bindgen", "mize/target-js", "marts/target-js", "base64", "serde_json", "serde", "openidconnect", "dioxus", "dioxus-web", "web-sys", "web-sys/Document", "dioxus/web" ]
|
||||||
|
server = [ "axum", "axum-extra", "openidconnect", "serde", "serde_json", "reqwest", "rand", "url", "time", "tokio", "dioxus/ssr", "dioxus/server", "dioxus/fullstack", "dioxus/router", "dioxus-ssr", "html-escape", "axum-oidc", "tower", "tower-sessions", "marts/server", "target-os" ]
|
||||||
|
desktop = ["dioxus/desktop", "marts/desktop", "target-os"]
|
||||||
|
client = []
|
||||||
|
webbrowser = ["dep:webbrowser"]
|
||||||
|
|
||||||
|
|
||||||
[package.metadata.wasm-pack.profile.release]
|
[package.metadata.wasm-pack.profile.release]
|
||||||
wasm-opt = false
|
wasm-opt = false
|
||||||
|
|||||||
292
packages/ppc/auth/client.rs
Normal file
292
packages/ppc/auth/client.rs
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
use base64::{
|
||||||
|
Engine,
|
||||||
|
alphabet::STANDARD,
|
||||||
|
engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig},
|
||||||
|
};
|
||||||
|
use mize::{Mize, MizeError, MizePart, MizeResult, item::ItemData, mize_err, mize_part};
|
||||||
|
use openidconnect::{
|
||||||
|
AuthorizationCode, ClientId, CsrfToken, DeviceAuthorizationResponse,
|
||||||
|
EmptyExtraDeviceAuthorizationFields, IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge,
|
||||||
|
PkceCodeVerifier, RedirectUrl, RefreshToken, Scope,
|
||||||
|
core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata},
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
io::{self, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
// console_log macro
|
||||||
|
// that can be copied into other files for debugging purposes
|
||||||
|
#[cfg(feature = "target-obsidian")]
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "target-obsidian")]
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
#[wasm_bindgen(js_namespace = console)]
|
||||||
|
fn log(s: &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "target-obsidian")]
|
||||||
|
macro_rules! console_log {
|
||||||
|
// Note that this is using the `log` function imported above during
|
||||||
|
// `bare_bones`
|
||||||
|
($($t:tt)*) => (unsafe { log(&format_args!($($t)*).to_string())})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "target-os")]
|
||||||
|
use clap::Command;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
#[mize_part]
|
||||||
|
pub struct ClientAuth {
|
||||||
|
mize: Mize,
|
||||||
|
secrets: Option<ClientSecrets>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize)]
|
||||||
|
pub struct ClientSecrets {
|
||||||
|
id_token: String,
|
||||||
|
access_token: String,
|
||||||
|
refresh_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MizePart for ClientAuth {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"client_auth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientAuth {
|
||||||
|
pub fn id_token(&self) -> MizeResult<&str> {
|
||||||
|
if self.secrets.is_none() {
|
||||||
|
return Err(mize_err!("Client not logged in"));
|
||||||
|
}
|
||||||
|
Ok(&self.secrets.as_ref().unwrap().id_token)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn access_token(&self) -> MizeResult<&str> {
|
||||||
|
if self.secrets.is_none() {
|
||||||
|
return Err(mize_err!("Client not logged in"));
|
||||||
|
}
|
||||||
|
Ok(&self.secrets.as_ref().unwrap().access_token)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_token(&self) -> MizeResult<&str> {
|
||||||
|
if self.secrets.is_none() {
|
||||||
|
return Err(mize_err!("Client not logged in"));
|
||||||
|
}
|
||||||
|
Ok(&self.secrets.as_ref().unwrap().refresh_token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client_auth(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
let mize_clone = mize.clone();
|
||||||
|
|
||||||
|
let secrets = match read_secrets(mize) {
|
||||||
|
Ok(secrets) => Some(secrets),
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "target-os")]
|
||||||
|
{
|
||||||
|
let mut cli = mize.get_part_native::<marts::CliPart>("cli")?;
|
||||||
|
cli.subcommand(Command::new("login"), |_, mut mize| {
|
||||||
|
let secrets = mize.spawn_async_blocking("client_auth", auth_flow(mize_clone))?;
|
||||||
|
write_secrets(&mut mize, &secrets)?;
|
||||||
|
let mut auth = mize.get_part_native::<ClientAuth>("client_auth")?;
|
||||||
|
auth.secrets = Some(secrets);
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "target-obsidian")]
|
||||||
|
{}
|
||||||
|
|
||||||
|
mize.register_part(Box::new(ClientAuth {
|
||||||
|
secrets,
|
||||||
|
mize: mize.clone(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_secrets(mize: &mut Mize) -> MizeResult<ClientSecrets> {
|
||||||
|
let mize_dir = mize.get_config("data_dir")?.value_string()?;
|
||||||
|
let secrets_file_path = PathBuf::from(mize_dir).join("client_secrets.json");
|
||||||
|
|
||||||
|
let secrets_string = std::fs::read_to_string(secrets_file_path)?;
|
||||||
|
let secrets: ClientSecrets = serde_json::from_str(&secrets_string)?;
|
||||||
|
Ok(secrets)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_secrets(mize: &mut Mize, secrets: &ClientSecrets) -> MizeResult<()> {
|
||||||
|
let mize_dir = mize.get_config("data_dir")?.value_string()?;
|
||||||
|
let secrets_file_path = PathBuf::from(mize_dir).join("client_secrets.json");
|
||||||
|
|
||||||
|
let secrets_string = serde_json::to_string(secrets)?;
|
||||||
|
std::fs::write(secrets_file_path, secrets_string)?;
|
||||||
|
|
||||||
|
// update the spacetimedb_token in XDG_CONFIG_HOME/spacetime/cli.toml config file if configured to do so
|
||||||
|
if mize
|
||||||
|
.get_config("auth.client.update_spacetime_token")?
|
||||||
|
.value_bool()?
|
||||||
|
{
|
||||||
|
update_spacetimedb_token(&secrets.id_token.as_str())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct Combined<'a> {
|
||||||
|
code: &'a str,
|
||||||
|
state: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
use openidconnect::reqwest::Client as HttpClient;
|
||||||
|
use openidconnect::{AuthUrl, DeviceAuthorizationUrl};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
async fn auth_flow(mut mize: Mize) -> MizeResult<ClientSecrets> {
|
||||||
|
// ===== CONFIG =====
|
||||||
|
let issuer = mize.get_config("auth.client.issuer")?.value_string()?;
|
||||||
|
let client_id = mize.get_config("auth.client.client_id")?.value_string()?;
|
||||||
|
|
||||||
|
let issuer = IssuerUrl::new(issuer)?;
|
||||||
|
let client_id = ClientId::new(client_id);
|
||||||
|
|
||||||
|
let http_client = HttpClient::new();
|
||||||
|
|
||||||
|
// Teach openidconnect-rs about the device authorization url.
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
struct MyExtraProviderMetadata {
|
||||||
|
device_authorization_endpoint: String,
|
||||||
|
}
|
||||||
|
use openidconnect::AdditionalProviderMetadata;
|
||||||
|
use openidconnect::ProviderMetadata;
|
||||||
|
use openidconnect::core::{
|
||||||
|
CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClient, CoreClientAuthMethod,
|
||||||
|
CoreDeviceAuthorizationResponse, CoreGrantType, CoreJsonWebKey, CoreJsonWebKeyType,
|
||||||
|
CoreJsonWebKeyUse, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm,
|
||||||
|
CoreJwsSigningAlgorithm, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType,
|
||||||
|
};
|
||||||
|
impl AdditionalProviderMetadata for MyExtraProviderMetadata {}
|
||||||
|
type MyProviderMetadata = ProviderMetadata<
|
||||||
|
MyExtraProviderMetadata,
|
||||||
|
CoreAuthDisplay,
|
||||||
|
CoreClientAuthMethod,
|
||||||
|
CoreClaimName,
|
||||||
|
CoreClaimType,
|
||||||
|
CoreGrantType,
|
||||||
|
CoreJweContentEncryptionAlgorithm,
|
||||||
|
CoreJweKeyManagementAlgorithm,
|
||||||
|
CoreJsonWebKey,
|
||||||
|
CoreResponseMode,
|
||||||
|
CoreResponseType,
|
||||||
|
CoreSubjectIdentifierType,
|
||||||
|
>;
|
||||||
|
|
||||||
|
// Discover provider metadata
|
||||||
|
let provider_metadata: MyProviderMetadata =
|
||||||
|
MyProviderMetadata::discover_async(issuer, &http_client).await?;
|
||||||
|
|
||||||
|
let device_auth_url = provider_metadata
|
||||||
|
.additional_metadata()
|
||||||
|
.device_authorization_endpoint
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let client = CoreClient::from_provider_metadata(
|
||||||
|
provider_metadata,
|
||||||
|
client_id,
|
||||||
|
None, // public client
|
||||||
|
)
|
||||||
|
.set_device_authorization_url(DeviceAuthorizationUrl::new(device_auth_url)?);
|
||||||
|
|
||||||
|
// ===== DEVICE AUTH REQUEST =====
|
||||||
|
let details: Result<DeviceAuthorizationResponse<EmptyExtraDeviceAuthorizationFields>, _> =
|
||||||
|
client
|
||||||
|
.exchange_device_code()
|
||||||
|
.add_scope(Scope::new("openid".into()))
|
||||||
|
.add_scope(Scope::new("profile".into()))
|
||||||
|
.add_scope(Scope::new("offline_access".into()))
|
||||||
|
.request_async(&http_client)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let details = match details {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(err) => {
|
||||||
|
println!("err: {:?}", err);
|
||||||
|
println!("err: {}", err);
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"If your browser does not open automatically, open this URL in it:\n{}\n",
|
||||||
|
details.verification_uri_complete().unwrap().secret()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"Or enter this PPC login code:\n{}\n",
|
||||||
|
details.user_code().secret()
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(feature = "target-os")]
|
||||||
|
{
|
||||||
|
if let Some(uri_complete) = details.verification_uri_complete() {
|
||||||
|
let _ = webbrowser::open(details.verification_uri_complete().unwrap().secret());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_response = client
|
||||||
|
.exchange_device_access_token(&details)?
|
||||||
|
.request_async(
|
||||||
|
&http_client,
|
||||||
|
async |dur: Duration| {
|
||||||
|
tokio::time::sleep(dur);
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// ===== TOKENS =====
|
||||||
|
let id_token = token_response
|
||||||
|
.extra_fields()
|
||||||
|
.id_token()
|
||||||
|
.ok_or(mize_err!("no id_token"))?;
|
||||||
|
|
||||||
|
let refresh = token_response
|
||||||
|
.refresh_token()
|
||||||
|
.ok_or(mize_err!("no refresh token present"))?;
|
||||||
|
|
||||||
|
Ok(ClientSecrets {
|
||||||
|
id_token: id_token.to_string(),
|
||||||
|
access_token: token_response.access_token().secret().to_string(),
|
||||||
|
refresh_token: refresh.secret().to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_spacetimedb_token(new_token: &str) -> MizeResult<()> {
|
||||||
|
// Resolve config directory
|
||||||
|
let config_dir = match std::env::var("XDG_CONFIG_HOME") {
|
||||||
|
Ok(val) => PathBuf::from(val),
|
||||||
|
Err(_) => {
|
||||||
|
let home = std::env::var("HOME")?;
|
||||||
|
std::path::PathBuf::from(home).join(".config")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_path = config_dir.join("spacetime").join("cli.toml");
|
||||||
|
|
||||||
|
// Read existing file (or create empty document if it doesn't exist)
|
||||||
|
let content = std::fs::read_to_string(&file_path).unwrap_or_default();
|
||||||
|
let mut data = ItemData::from_toml(content.as_str())?;
|
||||||
|
|
||||||
|
// Update the field
|
||||||
|
data.set_path("spacetimedb_token", new_token)?;
|
||||||
|
|
||||||
|
// Write back
|
||||||
|
std::fs::write(&file_path, data.to_toml()?)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
159
packages/ppc/generated/activities_table.rs
Normal file
159
packages/ppc/generated/activities_table.rs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use super::activity_type::Activity;
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
/// Table handle for the table `activities`.
|
||||||
|
///
|
||||||
|
/// Obtain a handle from the [`ActivitiesTableAccess::activities`] method on [`super::RemoteTables`],
|
||||||
|
/// like `ctx.db.activities()`.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.activities().on_insert(...)`.
|
||||||
|
pub struct ActivitiesTableHandle<'ctx> {
|
||||||
|
imp: __sdk::TableHandle<Activity>,
|
||||||
|
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the table `activities`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteTables`].
|
||||||
|
pub trait ActivitiesTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Obtain a [`ActivitiesTableHandle`], which mediates access to the table `activities`.
|
||||||
|
fn activities(&self) -> ActivitiesTableHandle<'_>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActivitiesTableAccess for super::RemoteTables {
|
||||||
|
fn activities(&self) -> ActivitiesTableHandle<'_> {
|
||||||
|
ActivitiesTableHandle {
|
||||||
|
imp: self.imp.get_table::<Activity>("activities"),
|
||||||
|
ctx: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ActivitiesInsertCallbackId(__sdk::CallbackId);
|
||||||
|
pub struct ActivitiesDeleteCallbackId(__sdk::CallbackId);
|
||||||
|
|
||||||
|
impl<'ctx> __sdk::Table for ActivitiesTableHandle<'ctx> {
|
||||||
|
type Row = Activity;
|
||||||
|
type EventContext = super::EventContext;
|
||||||
|
|
||||||
|
fn count(&self) -> u64 {
|
||||||
|
self.imp.count()
|
||||||
|
}
|
||||||
|
fn iter(&self) -> impl Iterator<Item = Activity> + '_ {
|
||||||
|
self.imp.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
type InsertCallbackId = ActivitiesInsertCallbackId;
|
||||||
|
|
||||||
|
fn on_insert(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> ActivitiesInsertCallbackId {
|
||||||
|
ActivitiesInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_insert(&self, callback: ActivitiesInsertCallbackId) {
|
||||||
|
self.imp.remove_on_insert(callback.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteCallbackId = ActivitiesDeleteCallbackId;
|
||||||
|
|
||||||
|
fn on_delete(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> ActivitiesDeleteCallbackId {
|
||||||
|
ActivitiesDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_delete(&self, callback: ActivitiesDeleteCallbackId) {
|
||||||
|
self.imp.remove_on_delete(callback.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ActivitiesUpdateCallbackId(__sdk::CallbackId);
|
||||||
|
|
||||||
|
impl<'ctx> __sdk::TableWithPrimaryKey for ActivitiesTableHandle<'ctx> {
|
||||||
|
type UpdateCallbackId = ActivitiesUpdateCallbackId;
|
||||||
|
|
||||||
|
fn on_update(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||||
|
) -> ActivitiesUpdateCallbackId {
|
||||||
|
ActivitiesUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_update(&self, callback: ActivitiesUpdateCallbackId) {
|
||||||
|
self.imp.remove_on_update(callback.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access to the `id` unique index on the table `activities`,
|
||||||
|
/// which allows point queries on the field of the same name
|
||||||
|
/// via the [`ActivitiesIdUnique::find`] method.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.activities().id().find(...)`.
|
||||||
|
pub struct ActivitiesIdUnique<'ctx> {
|
||||||
|
imp: __sdk::UniqueConstraintHandle<Activity, u64>,
|
||||||
|
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> ActivitiesTableHandle<'ctx> {
|
||||||
|
/// Get a handle on the `id` unique index on the table `activities`.
|
||||||
|
pub fn id(&self) -> ActivitiesIdUnique<'ctx> {
|
||||||
|
ActivitiesIdUnique {
|
||||||
|
imp: self.imp.get_unique_constraint::<u64>("id"),
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> ActivitiesIdUnique<'ctx> {
|
||||||
|
/// Find the subscribed row whose `id` column value is equal to `col_val`,
|
||||||
|
/// if such a row is present in the client cache.
|
||||||
|
pub fn find(&self, col_val: &u64) -> Option<Activity> {
|
||||||
|
self.imp.find(col_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||||
|
let _table = client_cache.get_or_make_table::<Activity>("activities");
|
||||||
|
_table.add_unique_constraint::<u64>("id", |row| &row.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn parse_table_update(
|
||||||
|
raw_updates: __ws::v2::TableUpdate,
|
||||||
|
) -> __sdk::Result<__sdk::TableUpdate<Activity>> {
|
||||||
|
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||||
|
__sdk::InternalError::failed_parse("TableUpdate<Activity>", "TableUpdate")
|
||||||
|
.with_cause(e)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for query builder access to the table `Activity`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||||
|
pub trait activitiesQueryTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Get a query builder for the table `Activity`.
|
||||||
|
fn activities(&self) -> __sdk::__query_builder::Table<Activity>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl activitiesQueryTableAccess for __sdk::QueryTableAccessor {
|
||||||
|
fn activities(&self) -> __sdk::__query_builder::Table<Activity> {
|
||||||
|
__sdk::__query_builder::Table::new("activities")
|
||||||
|
}
|
||||||
|
}
|
||||||
58
packages/ppc/generated/activity_type.rs
Normal file
58
packages/ppc/generated/activity_type.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct Activity {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub task: Option<u64>,
|
||||||
|
pub parents_ids: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for Activity {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Column accessor struct for the table `Activity`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to columns for query building.
|
||||||
|
pub struct ActivityCols {
|
||||||
|
pub id: __sdk::__query_builder::Col<Activity, u64>,
|
||||||
|
pub name: __sdk::__query_builder::Col<Activity, String>,
|
||||||
|
pub task: __sdk::__query_builder::Col<Activity, Option<u64>>,
|
||||||
|
pub parents_ids: __sdk::__query_builder::Col<Activity, Vec<u64>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasCols for Activity {
|
||||||
|
type Cols = ActivityCols;
|
||||||
|
fn cols(table_name: &'static str) -> Self::Cols {
|
||||||
|
ActivityCols {
|
||||||
|
id: __sdk::__query_builder::Col::new(table_name, "id"),
|
||||||
|
name: __sdk::__query_builder::Col::new(table_name, "name"),
|
||||||
|
task: __sdk::__query_builder::Col::new(table_name, "task"),
|
||||||
|
parents_ids: __sdk::__query_builder::Col::new(table_name, "parents_ids"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indexed column accessor struct for the table `Activity`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to indexed columns for query building.
|
||||||
|
pub struct ActivityIxCols {
|
||||||
|
pub id: __sdk::__query_builder::IxCol<Activity, u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasIxCols for Activity {
|
||||||
|
type IxCols = ActivityIxCols;
|
||||||
|
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||||
|
ActivityIxCols {
|
||||||
|
id: __sdk::__query_builder::IxCol::new(table_name, "id"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::CanBeLookupTable for Activity {}
|
||||||
66
packages/ppc/generated/add_activity_reducer.rs
Normal file
66
packages/ppc/generated/add_activity_reducer.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub(super) struct AddActivityArgs {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AddActivityArgs> for super::Reducer {
|
||||||
|
fn from(args: AddActivityArgs) -> Self {
|
||||||
|
Self::AddActivity { name: args.name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for AddActivityArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the reducer `add_activity`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteReducers`].
|
||||||
|
pub trait add_activity {
|
||||||
|
/// Request that the remote module invoke the reducer `add_activity` to run as soon as possible.
|
||||||
|
///
|
||||||
|
/// This method returns immediately, and errors only if we are unable to send the request.
|
||||||
|
/// The reducer will run asynchronously in the future,
|
||||||
|
/// and this method provides no way to listen for its completion status.
|
||||||
|
/// /// Use [`add_activity:add_activity_then`] to run a callback after the reducer completes.
|
||||||
|
fn add_activity(&self, name: String) -> __sdk::Result<()> {
|
||||||
|
self.add_activity_then(name, |_, _| {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request that the remote module invoke the reducer `add_activity` to run as soon as possible,
|
||||||
|
/// registering `callback` to run when we are notified that the reducer completed.
|
||||||
|
///
|
||||||
|
/// This method returns immediately, and errors only if we are unable to send the request.
|
||||||
|
/// The reducer will run asynchronously in the future,
|
||||||
|
/// and its status can be observed with the `callback`.
|
||||||
|
fn add_activity_then(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
|
) -> __sdk::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl add_activity for super::RemoteReducers {
|
||||||
|
fn add_activity_then(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
|
) -> __sdk::Result<()> {
|
||||||
|
self.imp
|
||||||
|
.invoke_reducer_with_callback(AddActivityArgs { name }, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
53
packages/ppc/generated/event_type.rs
Normal file
53
packages/ppc/generated/event_type.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::activity_type::Activity;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct Event {
|
||||||
|
pub start: __sdk::Timestamp,
|
||||||
|
pub end: __sdk::Timestamp,
|
||||||
|
pub activity: Activity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for Event {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Column accessor struct for the table `Event`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to columns for query building.
|
||||||
|
pub struct EventCols {
|
||||||
|
pub start: __sdk::__query_builder::Col<Event, __sdk::Timestamp>,
|
||||||
|
pub end: __sdk::__query_builder::Col<Event, __sdk::Timestamp>,
|
||||||
|
pub activity: __sdk::__query_builder::Col<Event, Activity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasCols for Event {
|
||||||
|
type Cols = EventCols;
|
||||||
|
fn cols(table_name: &'static str) -> Self::Cols {
|
||||||
|
EventCols {
|
||||||
|
start: __sdk::__query_builder::Col::new(table_name, "start"),
|
||||||
|
end: __sdk::__query_builder::Col::new(table_name, "end"),
|
||||||
|
activity: __sdk::__query_builder::Col::new(table_name, "activity"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indexed column accessor struct for the table `Event`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to indexed columns for query building.
|
||||||
|
pub struct EventIxCols {}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasIxCols for Event {
|
||||||
|
type IxCols = EventIxCols;
|
||||||
|
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||||
|
EventIxCols {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::CanBeLookupTable for Event {}
|
||||||
112
packages/ppc/generated/events_table.rs
Normal file
112
packages/ppc/generated/events_table.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use super::activity_type::Activity;
|
||||||
|
use super::event_type::Event;
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
/// Table handle for the table `events`.
|
||||||
|
///
|
||||||
|
/// Obtain a handle from the [`EventsTableAccess::events`] method on [`super::RemoteTables`],
|
||||||
|
/// like `ctx.db.events()`.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.events().on_insert(...)`.
|
||||||
|
pub struct EventsTableHandle<'ctx> {
|
||||||
|
imp: __sdk::TableHandle<Event>,
|
||||||
|
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the table `events`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteTables`].
|
||||||
|
pub trait EventsTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Obtain a [`EventsTableHandle`], which mediates access to the table `events`.
|
||||||
|
fn events(&self) -> EventsTableHandle<'_>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventsTableAccess for super::RemoteTables {
|
||||||
|
fn events(&self) -> EventsTableHandle<'_> {
|
||||||
|
EventsTableHandle {
|
||||||
|
imp: self.imp.get_table::<Event>("events"),
|
||||||
|
ctx: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EventsInsertCallbackId(__sdk::CallbackId);
|
||||||
|
pub struct EventsDeleteCallbackId(__sdk::CallbackId);
|
||||||
|
|
||||||
|
impl<'ctx> __sdk::Table for EventsTableHandle<'ctx> {
|
||||||
|
type Row = Event;
|
||||||
|
type EventContext = super::EventContext;
|
||||||
|
|
||||||
|
fn count(&self) -> u64 {
|
||||||
|
self.imp.count()
|
||||||
|
}
|
||||||
|
fn iter(&self) -> impl Iterator<Item = Event> + '_ {
|
||||||
|
self.imp.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
type InsertCallbackId = EventsInsertCallbackId;
|
||||||
|
|
||||||
|
fn on_insert(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> EventsInsertCallbackId {
|
||||||
|
EventsInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_insert(&self, callback: EventsInsertCallbackId) {
|
||||||
|
self.imp.remove_on_insert(callback.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteCallbackId = EventsDeleteCallbackId;
|
||||||
|
|
||||||
|
fn on_delete(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> EventsDeleteCallbackId {
|
||||||
|
EventsDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_delete(&self, callback: EventsDeleteCallbackId) {
|
||||||
|
self.imp.remove_on_delete(callback.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||||
|
let _table = client_cache.get_or_make_table::<Event>("events");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn parse_table_update(
|
||||||
|
raw_updates: __ws::v2::TableUpdate,
|
||||||
|
) -> __sdk::Result<__sdk::TableUpdate<Event>> {
|
||||||
|
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||||
|
__sdk::InternalError::failed_parse("TableUpdate<Event>", "TableUpdate")
|
||||||
|
.with_cause(e)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for query builder access to the table `Event`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||||
|
pub trait eventsQueryTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Get a query builder for the table `Event`.
|
||||||
|
fn events(&self) -> __sdk::__query_builder::Table<Event>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eventsQueryTableAccess for __sdk::QueryTableAccessor {
|
||||||
|
fn events(&self) -> __sdk::__query_builder::Table<Event> {
|
||||||
|
__sdk::__query_builder::Table::new("events")
|
||||||
|
}
|
||||||
|
}
|
||||||
921
packages/ppc/generated/mod.rs
Normal file
921
packages/ppc/generated/mod.rs
Normal file
@@ -0,0 +1,921 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
// This was generated using spacetimedb cli version 2.1.0 (commit refs/tags/v2.1.0).
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
pub mod activities_table;
|
||||||
|
pub mod activity_type;
|
||||||
|
pub mod add_activity_reducer;
|
||||||
|
pub mod event_type;
|
||||||
|
pub mod events_table;
|
||||||
|
pub mod profile_type;
|
||||||
|
pub mod profiles_table;
|
||||||
|
pub mod remove_activity_reducer;
|
||||||
|
pub mod set_activity_name_reducer;
|
||||||
|
pub mod task_part_table;
|
||||||
|
pub mod task_part_type;
|
||||||
|
pub mod task_type;
|
||||||
|
pub mod tasks_table;
|
||||||
|
pub mod update_activity_reducer;
|
||||||
|
|
||||||
|
pub use activities_table::*;
|
||||||
|
pub use activity_type::Activity;
|
||||||
|
pub use add_activity_reducer::add_activity;
|
||||||
|
pub use event_type::Event;
|
||||||
|
pub use events_table::*;
|
||||||
|
pub use profile_type::Profile;
|
||||||
|
pub use profiles_table::*;
|
||||||
|
pub use remove_activity_reducer::remove_activity;
|
||||||
|
pub use set_activity_name_reducer::set_activity_name;
|
||||||
|
pub use task_part_table::*;
|
||||||
|
pub use task_part_type::TaskPart;
|
||||||
|
pub use task_type::Task;
|
||||||
|
pub use tasks_table::*;
|
||||||
|
pub use update_activity_reducer::update_activity;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
|
||||||
|
/// One of the reducers defined by this module.
|
||||||
|
///
|
||||||
|
/// Contained within a [`__sdk::ReducerEvent`] in [`EventContext`]s for reducer events
|
||||||
|
/// to indicate which reducer caused the event.
|
||||||
|
|
||||||
|
pub enum Reducer {
|
||||||
|
AddActivity { name: String },
|
||||||
|
RemoveActivity { activity: Activity },
|
||||||
|
SetActivityName { id: u64, name: String },
|
||||||
|
UpdateActivity { activity: Activity },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for Reducer {
|
||||||
|
type Module = RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::Reducer for Reducer {
|
||||||
|
fn reducer_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Reducer::AddActivity { .. } => "add_activity",
|
||||||
|
Reducer::RemoveActivity { .. } => "remove_activity",
|
||||||
|
Reducer::SetActivityName { .. } => "set_activity_name",
|
||||||
|
Reducer::UpdateActivity { .. } => "update_activity",
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(clippy::clone_on_copy)]
|
||||||
|
fn args_bsatn(&self) -> Result<Vec<u8>, __sats::bsatn::EncodeError> {
|
||||||
|
match self {
|
||||||
|
Reducer::AddActivity { name } => {
|
||||||
|
__sats::bsatn::to_vec(&add_activity_reducer::AddActivityArgs { name: name.clone() })
|
||||||
|
}
|
||||||
|
Reducer::RemoveActivity { activity } => {
|
||||||
|
__sats::bsatn::to_vec(&remove_activity_reducer::RemoveActivityArgs {
|
||||||
|
activity: activity.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Reducer::SetActivityName { id, name } => {
|
||||||
|
__sats::bsatn::to_vec(&set_activity_name_reducer::SetActivityNameArgs {
|
||||||
|
id: id.clone(),
|
||||||
|
name: name.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Reducer::UpdateActivity { activity } => {
|
||||||
|
__sats::bsatn::to_vec(&update_activity_reducer::UpdateActivityArgs {
|
||||||
|
activity: activity.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct DbUpdate {
|
||||||
|
activities: __sdk::TableUpdate<Activity>,
|
||||||
|
events: __sdk::TableUpdate<Event>,
|
||||||
|
profiles: __sdk::TableUpdate<Profile>,
|
||||||
|
task_part: __sdk::TableUpdate<TaskPart>,
|
||||||
|
tasks: __sdk::TableUpdate<Task>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
|
||||||
|
type Error = __sdk::Error;
|
||||||
|
fn try_from(raw: __ws::v2::TransactionUpdate) -> Result<Self, Self::Error> {
|
||||||
|
let mut db_update = DbUpdate::default();
|
||||||
|
for table_update in __sdk::transaction_update_iter_table_updates(raw) {
|
||||||
|
match &table_update.table_name[..] {
|
||||||
|
"activities" => db_update
|
||||||
|
.activities
|
||||||
|
.append(activities_table::parse_table_update(table_update)?),
|
||||||
|
"events" => db_update
|
||||||
|
.events
|
||||||
|
.append(events_table::parse_table_update(table_update)?),
|
||||||
|
"profiles" => db_update
|
||||||
|
.profiles
|
||||||
|
.append(profiles_table::parse_table_update(table_update)?),
|
||||||
|
"task_part" => db_update
|
||||||
|
.task_part
|
||||||
|
.append(task_part_table::parse_table_update(table_update)?),
|
||||||
|
"tasks" => db_update
|
||||||
|
.tasks
|
||||||
|
.append(tasks_table::parse_table_update(table_update)?),
|
||||||
|
|
||||||
|
unknown => {
|
||||||
|
return Err(__sdk::InternalError::unknown_name(
|
||||||
|
"table",
|
||||||
|
unknown,
|
||||||
|
"DatabaseUpdate",
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(db_update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for DbUpdate {
|
||||||
|
type Module = RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::DbUpdate for DbUpdate {
|
||||||
|
fn apply_to_client_cache(
|
||||||
|
&self,
|
||||||
|
cache: &mut __sdk::ClientCache<RemoteModule>,
|
||||||
|
) -> AppliedDiff<'_> {
|
||||||
|
let mut diff = AppliedDiff::default();
|
||||||
|
|
||||||
|
diff.activities = cache
|
||||||
|
.apply_diff_to_table::<Activity>("activities", &self.activities)
|
||||||
|
.with_updates_by_pk(|row| &row.id);
|
||||||
|
diff.events = cache.apply_diff_to_table::<Event>("events", &self.events);
|
||||||
|
diff.profiles = cache
|
||||||
|
.apply_diff_to_table::<Profile>("profiles", &self.profiles)
|
||||||
|
.with_updates_by_pk(|row| &row.identity);
|
||||||
|
diff.task_part = cache
|
||||||
|
.apply_diff_to_table::<TaskPart>("task_part", &self.task_part)
|
||||||
|
.with_updates_by_pk(|row| &row.identity);
|
||||||
|
diff.tasks = cache
|
||||||
|
.apply_diff_to_table::<Task>("tasks", &self.tasks)
|
||||||
|
.with_updates_by_pk(|row| &row.id);
|
||||||
|
|
||||||
|
diff
|
||||||
|
}
|
||||||
|
fn parse_initial_rows(raw: __ws::v2::QueryRows) -> __sdk::Result<Self> {
|
||||||
|
let mut db_update = DbUpdate::default();
|
||||||
|
for table_rows in raw.tables {
|
||||||
|
match &table_rows.table[..] {
|
||||||
|
"activities" => db_update
|
||||||
|
.activities
|
||||||
|
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
|
"events" => db_update
|
||||||
|
.events
|
||||||
|
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
|
"profiles" => db_update
|
||||||
|
.profiles
|
||||||
|
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
|
"task_part" => db_update
|
||||||
|
.task_part
|
||||||
|
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
|
"tasks" => db_update
|
||||||
|
.tasks
|
||||||
|
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
|
unknown => {
|
||||||
|
return Err(
|
||||||
|
__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(db_update)
|
||||||
|
}
|
||||||
|
fn parse_unsubscribe_rows(raw: __ws::v2::QueryRows) -> __sdk::Result<Self> {
|
||||||
|
let mut db_update = DbUpdate::default();
|
||||||
|
for table_rows in raw.tables {
|
||||||
|
match &table_rows.table[..] {
|
||||||
|
"activities" => db_update
|
||||||
|
.activities
|
||||||
|
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
|
"events" => db_update
|
||||||
|
.events
|
||||||
|
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
|
"profiles" => db_update
|
||||||
|
.profiles
|
||||||
|
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
|
"task_part" => db_update
|
||||||
|
.task_part
|
||||||
|
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
|
"tasks" => db_update
|
||||||
|
.tasks
|
||||||
|
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
|
unknown => {
|
||||||
|
return Err(
|
||||||
|
__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(db_update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct AppliedDiff<'r> {
|
||||||
|
activities: __sdk::TableAppliedDiff<'r, Activity>,
|
||||||
|
events: __sdk::TableAppliedDiff<'r, Event>,
|
||||||
|
profiles: __sdk::TableAppliedDiff<'r, Profile>,
|
||||||
|
task_part: __sdk::TableAppliedDiff<'r, TaskPart>,
|
||||||
|
tasks: __sdk::TableAppliedDiff<'r, Task>,
|
||||||
|
__unused: std::marker::PhantomData<&'r ()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for AppliedDiff<'_> {
|
||||||
|
type Module = RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
|
||||||
|
fn invoke_row_callbacks(
|
||||||
|
&self,
|
||||||
|
event: &EventContext,
|
||||||
|
callbacks: &mut __sdk::DbCallbacks<RemoteModule>,
|
||||||
|
) {
|
||||||
|
callbacks.invoke_table_row_callbacks::<Activity>("activities", &self.activities, event);
|
||||||
|
callbacks.invoke_table_row_callbacks::<Event>("events", &self.events, event);
|
||||||
|
callbacks.invoke_table_row_callbacks::<Profile>("profiles", &self.profiles, event);
|
||||||
|
callbacks.invoke_table_row_callbacks::<TaskPart>("task_part", &self.task_part, event);
|
||||||
|
callbacks.invoke_table_row_callbacks::<Task>("tasks", &self.tasks, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RemoteModule;
|
||||||
|
|
||||||
|
impl __sdk::InModule for RemoteModule {
|
||||||
|
type Module = Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `reducers` field of [`EventContext`] and [`DbConnection`],
|
||||||
|
/// with methods provided by extension traits for each reducer defined by the module.
|
||||||
|
pub struct RemoteReducers {
|
||||||
|
imp: __sdk::DbContextImpl<RemoteModule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for RemoteReducers {
|
||||||
|
type Module = RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `procedures` field of [`DbConnection`] and other [`DbContext`] types,
|
||||||
|
/// with methods provided by extension traits for each procedure defined by the module.
|
||||||
|
pub struct RemoteProcedures {
|
||||||
|
imp: __sdk::DbContextImpl<RemoteModule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for RemoteProcedures {
|
||||||
|
type Module = RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `db` field of [`EventContext`] and [`DbConnection`],
|
||||||
|
/// with methods provided by extension traits for each table defined by the module.
|
||||||
|
pub struct RemoteTables {
|
||||||
|
imp: __sdk::DbContextImpl<RemoteModule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for RemoteTables {
|
||||||
|
type Module = RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A connection to a remote module, including a materialized view of a subset of the database.
|
||||||
|
///
|
||||||
|
/// Connect to a remote module by calling [`DbConnection::builder`]
|
||||||
|
/// and using the [`__sdk::DbConnectionBuilder`] builder-pattern constructor.
|
||||||
|
///
|
||||||
|
/// You must explicitly advance the connection by calling any one of:
|
||||||
|
///
|
||||||
|
/// - [`DbConnection::frame_tick`].
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), doc = "- [`DbConnection::run_threaded`].")]
|
||||||
|
#[cfg_attr(
|
||||||
|
target_arch = "wasm32",
|
||||||
|
doc = "- [`DbConnection::run_background_task`]."
|
||||||
|
)]
|
||||||
|
/// - [`DbConnection::run_async`].
|
||||||
|
/// - [`DbConnection::advance_one_message`].
|
||||||
|
#[cfg_attr(
|
||||||
|
not(target_arch = "wasm32"),
|
||||||
|
doc = "- [`DbConnection::advance_one_message_blocking`]."
|
||||||
|
)]
|
||||||
|
/// - [`DbConnection::advance_one_message_async`].
|
||||||
|
///
|
||||||
|
/// Which of these methods you should call depends on the specific needs of your application,
|
||||||
|
/// but you must call one of them, or else the connection will never progress.
|
||||||
|
pub struct DbConnection {
|
||||||
|
/// Access to tables defined by the module via extension traits implemented for [`RemoteTables`].
|
||||||
|
pub db: RemoteTables,
|
||||||
|
/// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`].
|
||||||
|
pub reducers: RemoteReducers,
|
||||||
|
#[doc(hidden)]
|
||||||
|
|
||||||
|
/// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`].
|
||||||
|
pub procedures: RemoteProcedures,
|
||||||
|
|
||||||
|
imp: __sdk::DbContextImpl<RemoteModule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for DbConnection {
|
||||||
|
type Module = RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::DbContext for DbConnection {
|
||||||
|
type DbView = RemoteTables;
|
||||||
|
type Reducers = RemoteReducers;
|
||||||
|
type Procedures = RemoteProcedures;
|
||||||
|
|
||||||
|
fn db(&self) -> &Self::DbView {
|
||||||
|
&self.db
|
||||||
|
}
|
||||||
|
fn reducers(&self) -> &Self::Reducers {
|
||||||
|
&self.reducers
|
||||||
|
}
|
||||||
|
fn procedures(&self) -> &Self::Procedures {
|
||||||
|
&self.procedures
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_active(&self) -> bool {
|
||||||
|
self.imp.is_active()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disconnect(&self) -> __sdk::Result<()> {
|
||||||
|
self.imp.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>;
|
||||||
|
|
||||||
|
fn subscription_builder(&self) -> Self::SubscriptionBuilder {
|
||||||
|
__sdk::SubscriptionBuilder::new(&self.imp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_identity(&self) -> Option<__sdk::Identity> {
|
||||||
|
self.imp.try_identity()
|
||||||
|
}
|
||||||
|
fn connection_id(&self) -> __sdk::ConnectionId {
|
||||||
|
self.imp.connection_id()
|
||||||
|
}
|
||||||
|
fn try_connection_id(&self) -> Option<__sdk::ConnectionId> {
|
||||||
|
self.imp.try_connection_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DbConnection {
|
||||||
|
/// Builder-pattern constructor for a connection to a remote module.
|
||||||
|
///
|
||||||
|
/// See [`__sdk::DbConnectionBuilder`] for required and optional configuration for the new connection.
|
||||||
|
pub fn builder() -> __sdk::DbConnectionBuilder<RemoteModule> {
|
||||||
|
__sdk::DbConnectionBuilder::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If any WebSocket messages are waiting, process one of them.
|
||||||
|
///
|
||||||
|
/// Returns `true` if a message was processed, or `false` if the queue is empty.
|
||||||
|
/// Callers should invoke this message in a loop until it returns `false`
|
||||||
|
/// or for as much time is available to process messages.
|
||||||
|
///
|
||||||
|
/// Returns an error if the connection is disconnected.
|
||||||
|
/// If the disconnection in question was normal,
|
||||||
|
/// i.e. the result of a call to [`__sdk::DbContext::disconnect`],
|
||||||
|
/// the returned error will be downcastable to [`__sdk::DisconnectedError`].
|
||||||
|
///
|
||||||
|
/// This is a low-level primitive exposed for power users who need significant control over scheduling.
|
||||||
|
/// Most applications should call [`Self::frame_tick`] each frame
|
||||||
|
/// to fully exhaust the queue whenever time is available.
|
||||||
|
pub fn advance_one_message(&self) -> __sdk::Result<bool> {
|
||||||
|
self.imp.advance_one_message()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process one WebSocket message, potentially blocking the current thread until one is received.
|
||||||
|
///
|
||||||
|
/// Returns an error if the connection is disconnected.
|
||||||
|
/// If the disconnection in question was normal,
|
||||||
|
/// i.e. the result of a call to [`__sdk::DbContext::disconnect`],
|
||||||
|
/// the returned error will be downcastable to [`__sdk::DisconnectedError`].
|
||||||
|
///
|
||||||
|
/// This is a low-level primitive exposed for power users who need significant control over scheduling.
|
||||||
|
/// Most applications should call [`Self::run_threaded`] to spawn a thread
|
||||||
|
/// which advances the connection automatically.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn advance_one_message_blocking(&self) -> __sdk::Result<()> {
|
||||||
|
self.imp.advance_one_message_blocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process one WebSocket message, `await`ing until one is received.
|
||||||
|
///
|
||||||
|
/// Returns an error if the connection is disconnected.
|
||||||
|
/// If the disconnection in question was normal,
|
||||||
|
/// i.e. the result of a call to [`__sdk::DbContext::disconnect`],
|
||||||
|
/// the returned error will be downcastable to [`__sdk::DisconnectedError`].
|
||||||
|
///
|
||||||
|
/// This is a low-level primitive exposed for power users who need significant control over scheduling.
|
||||||
|
/// Most applications should call [`Self::run_async`] to run an `async` loop
|
||||||
|
/// which advances the connection when polled.
|
||||||
|
pub async fn advance_one_message_async(&self) -> __sdk::Result<()> {
|
||||||
|
self.imp.advance_one_message_async().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process all WebSocket messages waiting in the queue,
|
||||||
|
/// then return without `await`ing or blocking the current thread.
|
||||||
|
pub fn frame_tick(&self) -> __sdk::Result<()> {
|
||||||
|
self.imp.frame_tick()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn a thread which processes WebSocket messages as they are received.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn run_threaded(&self) -> std::thread::JoinHandle<()> {
|
||||||
|
self.imp.run_threaded()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn a background task which processes WebSocket messages as they are received.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub fn run_background_task(&self) {
|
||||||
|
self.imp.run_background_task()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run an `async` loop which processes WebSocket messages when polled.
|
||||||
|
pub async fn run_async(&self) -> __sdk::Result<()> {
|
||||||
|
self.imp.run_async().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::DbConnection for DbConnection {
|
||||||
|
fn new(imp: __sdk::DbContextImpl<RemoteModule>) -> Self {
|
||||||
|
Self {
|
||||||
|
db: RemoteTables { imp: imp.clone() },
|
||||||
|
reducers: RemoteReducers { imp: imp.clone() },
|
||||||
|
procedures: RemoteProcedures { imp: imp.clone() },
|
||||||
|
imp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handle on a subscribed query.
|
||||||
|
// TODO: Document this better after implementing the new subscription API.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SubscriptionHandle {
|
||||||
|
imp: __sdk::SubscriptionHandleImpl<RemoteModule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for SubscriptionHandle {
|
||||||
|
type Module = RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::SubscriptionHandle for SubscriptionHandle {
|
||||||
|
fn new(imp: __sdk::SubscriptionHandleImpl<RemoteModule>) -> Self {
|
||||||
|
Self { imp }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this subscription has been terminated due to an unsubscribe call or an error.
|
||||||
|
fn is_ended(&self) -> bool {
|
||||||
|
self.imp.is_ended()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this subscription has been applied and has not yet been unsubscribed.
|
||||||
|
fn is_active(&self) -> bool {
|
||||||
|
self.imp.is_active()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unsubscribe from the query controlled by this `SubscriptionHandle`,
|
||||||
|
/// then run `on_end` when its rows are removed from the client cache.
|
||||||
|
fn unsubscribe_then(self, on_end: __sdk::OnEndedCallback<RemoteModule>) -> __sdk::Result<()> {
|
||||||
|
self.imp.unsubscribe_then(Some(on_end))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unsubscribe(self) -> __sdk::Result<()> {
|
||||||
|
self.imp.unsubscribe_then(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alias trait for a [`__sdk::DbContext`] connected to this module,
|
||||||
|
/// with that trait's associated types bounded to this module's concrete types.
|
||||||
|
///
|
||||||
|
/// Users can use this trait as a boundary on definitions which should accept
|
||||||
|
/// either a [`DbConnection`] or an [`EventContext`] and operate on either.
|
||||||
|
pub trait RemoteDbContext:
|
||||||
|
__sdk::DbContext<
|
||||||
|
DbView = RemoteTables,
|
||||||
|
Reducers = RemoteReducers,
|
||||||
|
SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>,
|
||||||
|
>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
impl<
|
||||||
|
Ctx: __sdk::DbContext<
|
||||||
|
DbView = RemoteTables,
|
||||||
|
Reducers = RemoteReducers,
|
||||||
|
SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>,
|
||||||
|
>,
|
||||||
|
> RemoteDbContext for Ctx
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An [`__sdk::DbContext`] augmented with a [`__sdk::Event`],
|
||||||
|
/// passed to [`__sdk::Table::on_insert`], [`__sdk::Table::on_delete`] and [`__sdk::TableWithPrimaryKey::on_update`] callbacks.
|
||||||
|
pub struct EventContext {
|
||||||
|
/// Access to tables defined by the module via extension traits implemented for [`RemoteTables`].
|
||||||
|
pub db: RemoteTables,
|
||||||
|
/// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`].
|
||||||
|
pub reducers: RemoteReducers,
|
||||||
|
/// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`].
|
||||||
|
pub procedures: RemoteProcedures,
|
||||||
|
/// The event which caused these callbacks to run.
|
||||||
|
pub event: __sdk::Event<Reducer>,
|
||||||
|
imp: __sdk::DbContextImpl<RemoteModule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::AbstractEventContext for EventContext {
|
||||||
|
type Event = __sdk::Event<Reducer>;
|
||||||
|
fn event(&self) -> &Self::Event {
|
||||||
|
&self.event
|
||||||
|
}
|
||||||
|
fn new(imp: __sdk::DbContextImpl<RemoteModule>, event: Self::Event) -> Self {
|
||||||
|
Self {
|
||||||
|
db: RemoteTables { imp: imp.clone() },
|
||||||
|
reducers: RemoteReducers { imp: imp.clone() },
|
||||||
|
procedures: RemoteProcedures { imp: imp.clone() },
|
||||||
|
event,
|
||||||
|
imp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for EventContext {
|
||||||
|
type Module = RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::DbContext for EventContext {
|
||||||
|
type DbView = RemoteTables;
|
||||||
|
type Reducers = RemoteReducers;
|
||||||
|
type Procedures = RemoteProcedures;
|
||||||
|
|
||||||
|
fn db(&self) -> &Self::DbView {
|
||||||
|
&self.db
|
||||||
|
}
|
||||||
|
fn reducers(&self) -> &Self::Reducers {
|
||||||
|
&self.reducers
|
||||||
|
}
|
||||||
|
fn procedures(&self) -> &Self::Procedures {
|
||||||
|
&self.procedures
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_active(&self) -> bool {
|
||||||
|
self.imp.is_active()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disconnect(&self) -> __sdk::Result<()> {
|
||||||
|
self.imp.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>;
|
||||||
|
|
||||||
|
fn subscription_builder(&self) -> Self::SubscriptionBuilder {
|
||||||
|
__sdk::SubscriptionBuilder::new(&self.imp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_identity(&self) -> Option<__sdk::Identity> {
|
||||||
|
self.imp.try_identity()
|
||||||
|
}
|
||||||
|
fn connection_id(&self) -> __sdk::ConnectionId {
|
||||||
|
self.imp.connection_id()
|
||||||
|
}
|
||||||
|
fn try_connection_id(&self) -> Option<__sdk::ConnectionId> {
|
||||||
|
self.imp.try_connection_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::EventContext for EventContext {}
|
||||||
|
|
||||||
|
/// An [`__sdk::DbContext`] augmented with a [`__sdk::ReducerEvent`],
|
||||||
|
/// passed to on-reducer callbacks.
|
||||||
|
pub struct ReducerEventContext {
|
||||||
|
/// Access to tables defined by the module via extension traits implemented for [`RemoteTables`].
|
||||||
|
pub db: RemoteTables,
|
||||||
|
/// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`].
|
||||||
|
pub reducers: RemoteReducers,
|
||||||
|
/// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`].
|
||||||
|
pub procedures: RemoteProcedures,
|
||||||
|
/// The event which caused these callbacks to run.
|
||||||
|
pub event: __sdk::ReducerEvent<Reducer>,
|
||||||
|
imp: __sdk::DbContextImpl<RemoteModule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::AbstractEventContext for ReducerEventContext {
|
||||||
|
type Event = __sdk::ReducerEvent<Reducer>;
|
||||||
|
fn event(&self) -> &Self::Event {
|
||||||
|
&self.event
|
||||||
|
}
|
||||||
|
fn new(imp: __sdk::DbContextImpl<RemoteModule>, event: Self::Event) -> Self {
|
||||||
|
Self {
|
||||||
|
db: RemoteTables { imp: imp.clone() },
|
||||||
|
reducers: RemoteReducers { imp: imp.clone() },
|
||||||
|
procedures: RemoteProcedures { imp: imp.clone() },
|
||||||
|
event,
|
||||||
|
imp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for ReducerEventContext {
|
||||||
|
type Module = RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::DbContext for ReducerEventContext {
|
||||||
|
type DbView = RemoteTables;
|
||||||
|
type Reducers = RemoteReducers;
|
||||||
|
type Procedures = RemoteProcedures;
|
||||||
|
|
||||||
|
fn db(&self) -> &Self::DbView {
|
||||||
|
&self.db
|
||||||
|
}
|
||||||
|
fn reducers(&self) -> &Self::Reducers {
|
||||||
|
&self.reducers
|
||||||
|
}
|
||||||
|
fn procedures(&self) -> &Self::Procedures {
|
||||||
|
&self.procedures
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_active(&self) -> bool {
|
||||||
|
self.imp.is_active()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disconnect(&self) -> __sdk::Result<()> {
|
||||||
|
self.imp.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>;
|
||||||
|
|
||||||
|
fn subscription_builder(&self) -> Self::SubscriptionBuilder {
|
||||||
|
__sdk::SubscriptionBuilder::new(&self.imp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_identity(&self) -> Option<__sdk::Identity> {
|
||||||
|
self.imp.try_identity()
|
||||||
|
}
|
||||||
|
fn connection_id(&self) -> __sdk::ConnectionId {
|
||||||
|
self.imp.connection_id()
|
||||||
|
}
|
||||||
|
fn try_connection_id(&self) -> Option<__sdk::ConnectionId> {
|
||||||
|
self.imp.try_connection_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::ReducerEventContext for ReducerEventContext {}
|
||||||
|
|
||||||
|
/// An [`__sdk::DbContext`] passed to procedure callbacks.
|
||||||
|
pub struct ProcedureEventContext {
|
||||||
|
/// Access to tables defined by the module via extension traits implemented for [`RemoteTables`].
|
||||||
|
pub db: RemoteTables,
|
||||||
|
/// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`].
|
||||||
|
pub reducers: RemoteReducers,
|
||||||
|
/// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`].
|
||||||
|
pub procedures: RemoteProcedures,
|
||||||
|
imp: __sdk::DbContextImpl<RemoteModule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::AbstractEventContext for ProcedureEventContext {
|
||||||
|
type Event = ();
|
||||||
|
fn event(&self) -> &Self::Event {
|
||||||
|
&()
|
||||||
|
}
|
||||||
|
fn new(imp: __sdk::DbContextImpl<RemoteModule>, _event: Self::Event) -> Self {
|
||||||
|
Self {
|
||||||
|
db: RemoteTables { imp: imp.clone() },
|
||||||
|
reducers: RemoteReducers { imp: imp.clone() },
|
||||||
|
procedures: RemoteProcedures { imp: imp.clone() },
|
||||||
|
imp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for ProcedureEventContext {
|
||||||
|
type Module = RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::DbContext for ProcedureEventContext {
|
||||||
|
type DbView = RemoteTables;
|
||||||
|
type Reducers = RemoteReducers;
|
||||||
|
type Procedures = RemoteProcedures;
|
||||||
|
|
||||||
|
fn db(&self) -> &Self::DbView {
|
||||||
|
&self.db
|
||||||
|
}
|
||||||
|
fn reducers(&self) -> &Self::Reducers {
|
||||||
|
&self.reducers
|
||||||
|
}
|
||||||
|
fn procedures(&self) -> &Self::Procedures {
|
||||||
|
&self.procedures
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_active(&self) -> bool {
|
||||||
|
self.imp.is_active()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disconnect(&self) -> __sdk::Result<()> {
|
||||||
|
self.imp.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>;
|
||||||
|
|
||||||
|
fn subscription_builder(&self) -> Self::SubscriptionBuilder {
|
||||||
|
__sdk::SubscriptionBuilder::new(&self.imp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_identity(&self) -> Option<__sdk::Identity> {
|
||||||
|
self.imp.try_identity()
|
||||||
|
}
|
||||||
|
fn connection_id(&self) -> __sdk::ConnectionId {
|
||||||
|
self.imp.connection_id()
|
||||||
|
}
|
||||||
|
fn try_connection_id(&self) -> Option<__sdk::ConnectionId> {
|
||||||
|
self.imp.try_connection_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::ProcedureEventContext for ProcedureEventContext {}
|
||||||
|
|
||||||
|
/// An [`__sdk::DbContext`] passed to [`__sdk::SubscriptionBuilder::on_applied`] and [`SubscriptionHandle::unsubscribe_then`] callbacks.
|
||||||
|
pub struct SubscriptionEventContext {
|
||||||
|
/// Access to tables defined by the module via extension traits implemented for [`RemoteTables`].
|
||||||
|
pub db: RemoteTables,
|
||||||
|
/// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`].
|
||||||
|
pub reducers: RemoteReducers,
|
||||||
|
/// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`].
|
||||||
|
pub procedures: RemoteProcedures,
|
||||||
|
imp: __sdk::DbContextImpl<RemoteModule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::AbstractEventContext for SubscriptionEventContext {
|
||||||
|
type Event = ();
|
||||||
|
fn event(&self) -> &Self::Event {
|
||||||
|
&()
|
||||||
|
}
|
||||||
|
fn new(imp: __sdk::DbContextImpl<RemoteModule>, _event: Self::Event) -> Self {
|
||||||
|
Self {
|
||||||
|
db: RemoteTables { imp: imp.clone() },
|
||||||
|
reducers: RemoteReducers { imp: imp.clone() },
|
||||||
|
procedures: RemoteProcedures { imp: imp.clone() },
|
||||||
|
imp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for SubscriptionEventContext {
|
||||||
|
type Module = RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::DbContext for SubscriptionEventContext {
|
||||||
|
type DbView = RemoteTables;
|
||||||
|
type Reducers = RemoteReducers;
|
||||||
|
type Procedures = RemoteProcedures;
|
||||||
|
|
||||||
|
fn db(&self) -> &Self::DbView {
|
||||||
|
&self.db
|
||||||
|
}
|
||||||
|
fn reducers(&self) -> &Self::Reducers {
|
||||||
|
&self.reducers
|
||||||
|
}
|
||||||
|
fn procedures(&self) -> &Self::Procedures {
|
||||||
|
&self.procedures
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_active(&self) -> bool {
|
||||||
|
self.imp.is_active()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disconnect(&self) -> __sdk::Result<()> {
|
||||||
|
self.imp.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>;
|
||||||
|
|
||||||
|
fn subscription_builder(&self) -> Self::SubscriptionBuilder {
|
||||||
|
__sdk::SubscriptionBuilder::new(&self.imp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_identity(&self) -> Option<__sdk::Identity> {
|
||||||
|
self.imp.try_identity()
|
||||||
|
}
|
||||||
|
fn connection_id(&self) -> __sdk::ConnectionId {
|
||||||
|
self.imp.connection_id()
|
||||||
|
}
|
||||||
|
fn try_connection_id(&self) -> Option<__sdk::ConnectionId> {
|
||||||
|
self.imp.try_connection_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::SubscriptionEventContext for SubscriptionEventContext {}
|
||||||
|
|
||||||
|
/// An [`__sdk::DbContext`] augmented with a [`__sdk::Error`],
|
||||||
|
/// passed to [`__sdk::DbConnectionBuilder::on_disconnect`], [`__sdk::DbConnectionBuilder::on_connect_error`] and [`__sdk::SubscriptionBuilder::on_error`] callbacks.
|
||||||
|
pub struct ErrorContext {
|
||||||
|
/// Access to tables defined by the module via extension traits implemented for [`RemoteTables`].
|
||||||
|
pub db: RemoteTables,
|
||||||
|
/// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`].
|
||||||
|
pub reducers: RemoteReducers,
|
||||||
|
/// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`].
|
||||||
|
pub procedures: RemoteProcedures,
|
||||||
|
/// The event which caused these callbacks to run.
|
||||||
|
pub event: Option<__sdk::Error>,
|
||||||
|
imp: __sdk::DbContextImpl<RemoteModule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::AbstractEventContext for ErrorContext {
|
||||||
|
type Event = Option<__sdk::Error>;
|
||||||
|
fn event(&self) -> &Self::Event {
|
||||||
|
&self.event
|
||||||
|
}
|
||||||
|
fn new(imp: __sdk::DbContextImpl<RemoteModule>, event: Self::Event) -> Self {
|
||||||
|
Self {
|
||||||
|
db: RemoteTables { imp: imp.clone() },
|
||||||
|
reducers: RemoteReducers { imp: imp.clone() },
|
||||||
|
procedures: RemoteProcedures { imp: imp.clone() },
|
||||||
|
event,
|
||||||
|
imp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for ErrorContext {
|
||||||
|
type Module = RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::DbContext for ErrorContext {
|
||||||
|
type DbView = RemoteTables;
|
||||||
|
type Reducers = RemoteReducers;
|
||||||
|
type Procedures = RemoteProcedures;
|
||||||
|
|
||||||
|
fn db(&self) -> &Self::DbView {
|
||||||
|
&self.db
|
||||||
|
}
|
||||||
|
fn reducers(&self) -> &Self::Reducers {
|
||||||
|
&self.reducers
|
||||||
|
}
|
||||||
|
fn procedures(&self) -> &Self::Procedures {
|
||||||
|
&self.procedures
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_active(&self) -> bool {
|
||||||
|
self.imp.is_active()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disconnect(&self) -> __sdk::Result<()> {
|
||||||
|
self.imp.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>;
|
||||||
|
|
||||||
|
fn subscription_builder(&self) -> Self::SubscriptionBuilder {
|
||||||
|
__sdk::SubscriptionBuilder::new(&self.imp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_identity(&self) -> Option<__sdk::Identity> {
|
||||||
|
self.imp.try_identity()
|
||||||
|
}
|
||||||
|
fn connection_id(&self) -> __sdk::ConnectionId {
|
||||||
|
self.imp.connection_id()
|
||||||
|
}
|
||||||
|
fn try_connection_id(&self) -> Option<__sdk::ConnectionId> {
|
||||||
|
self.imp.try_connection_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::ErrorContext for ErrorContext {}
|
||||||
|
|
||||||
|
impl __sdk::SpacetimeModule for RemoteModule {
|
||||||
|
type DbConnection = DbConnection;
|
||||||
|
type EventContext = EventContext;
|
||||||
|
type ReducerEventContext = ReducerEventContext;
|
||||||
|
type ProcedureEventContext = ProcedureEventContext;
|
||||||
|
type SubscriptionEventContext = SubscriptionEventContext;
|
||||||
|
type ErrorContext = ErrorContext;
|
||||||
|
type Reducer = Reducer;
|
||||||
|
type DbView = RemoteTables;
|
||||||
|
type Reducers = RemoteReducers;
|
||||||
|
type Procedures = RemoteProcedures;
|
||||||
|
type DbUpdate = DbUpdate;
|
||||||
|
type AppliedDiff<'r> = AppliedDiff<'r>;
|
||||||
|
type SubscriptionHandle = SubscriptionHandle;
|
||||||
|
type QueryBuilder = __sdk::QueryBuilder;
|
||||||
|
|
||||||
|
fn register_tables(client_cache: &mut __sdk::ClientCache<Self>) {
|
||||||
|
activities_table::register_table(client_cache);
|
||||||
|
events_table::register_table(client_cache);
|
||||||
|
profiles_table::register_table(client_cache);
|
||||||
|
task_part_table::register_table(client_cache);
|
||||||
|
tasks_table::register_table(client_cache);
|
||||||
|
}
|
||||||
|
const ALL_TABLE_NAMES: &'static [&'static str] =
|
||||||
|
&["activities", "events", "profiles", "task_part", "tasks"];
|
||||||
|
}
|
||||||
54
packages/ppc/generated/profile_type.rs
Normal file
54
packages/ppc/generated/profile_type.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct Profile {
|
||||||
|
pub identity: __sdk::Identity,
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for Profile {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Column accessor struct for the table `Profile`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to columns for query building.
|
||||||
|
pub struct ProfileCols {
|
||||||
|
pub identity: __sdk::__query_builder::Col<Profile, __sdk::Identity>,
|
||||||
|
pub username: __sdk::__query_builder::Col<Profile, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasCols for Profile {
|
||||||
|
type Cols = ProfileCols;
|
||||||
|
fn cols(table_name: &'static str) -> Self::Cols {
|
||||||
|
ProfileCols {
|
||||||
|
identity: __sdk::__query_builder::Col::new(table_name, "identity"),
|
||||||
|
username: __sdk::__query_builder::Col::new(table_name, "username"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indexed column accessor struct for the table `Profile`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to indexed columns for query building.
|
||||||
|
pub struct ProfileIxCols {
|
||||||
|
pub identity: __sdk::__query_builder::IxCol<Profile, __sdk::Identity>,
|
||||||
|
pub username: __sdk::__query_builder::IxCol<Profile, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasIxCols for Profile {
|
||||||
|
type IxCols = ProfileIxCols;
|
||||||
|
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||||
|
ProfileIxCols {
|
||||||
|
identity: __sdk::__query_builder::IxCol::new(table_name, "identity"),
|
||||||
|
username: __sdk::__query_builder::IxCol::new(table_name, "username"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::CanBeLookupTable for Profile {}
|
||||||
192
packages/ppc/generated/profiles_table.rs
Normal file
192
packages/ppc/generated/profiles_table.rs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use super::profile_type::Profile;
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
/// Table handle for the table `profiles`.
|
||||||
|
///
|
||||||
|
/// Obtain a handle from the [`ProfilesTableAccess::profiles`] method on [`super::RemoteTables`],
|
||||||
|
/// like `ctx.db.profiles()`.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.profiles().on_insert(...)`.
|
||||||
|
pub struct ProfilesTableHandle<'ctx> {
|
||||||
|
imp: __sdk::TableHandle<Profile>,
|
||||||
|
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the table `profiles`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteTables`].
|
||||||
|
pub trait ProfilesTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Obtain a [`ProfilesTableHandle`], which mediates access to the table `profiles`.
|
||||||
|
fn profiles(&self) -> ProfilesTableHandle<'_>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfilesTableAccess for super::RemoteTables {
|
||||||
|
fn profiles(&self) -> ProfilesTableHandle<'_> {
|
||||||
|
ProfilesTableHandle {
|
||||||
|
imp: self.imp.get_table::<Profile>("profiles"),
|
||||||
|
ctx: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProfilesInsertCallbackId(__sdk::CallbackId);
|
||||||
|
pub struct ProfilesDeleteCallbackId(__sdk::CallbackId);
|
||||||
|
|
||||||
|
impl<'ctx> __sdk::Table for ProfilesTableHandle<'ctx> {
|
||||||
|
type Row = Profile;
|
||||||
|
type EventContext = super::EventContext;
|
||||||
|
|
||||||
|
fn count(&self) -> u64 {
|
||||||
|
self.imp.count()
|
||||||
|
}
|
||||||
|
fn iter(&self) -> impl Iterator<Item = Profile> + '_ {
|
||||||
|
self.imp.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
type InsertCallbackId = ProfilesInsertCallbackId;
|
||||||
|
|
||||||
|
fn on_insert(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> ProfilesInsertCallbackId {
|
||||||
|
ProfilesInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_insert(&self, callback: ProfilesInsertCallbackId) {
|
||||||
|
self.imp.remove_on_insert(callback.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteCallbackId = ProfilesDeleteCallbackId;
|
||||||
|
|
||||||
|
fn on_delete(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> ProfilesDeleteCallbackId {
|
||||||
|
ProfilesDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_delete(&self, callback: ProfilesDeleteCallbackId) {
|
||||||
|
self.imp.remove_on_delete(callback.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProfilesUpdateCallbackId(__sdk::CallbackId);
|
||||||
|
|
||||||
|
impl<'ctx> __sdk::TableWithPrimaryKey for ProfilesTableHandle<'ctx> {
|
||||||
|
type UpdateCallbackId = ProfilesUpdateCallbackId;
|
||||||
|
|
||||||
|
fn on_update(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||||
|
) -> ProfilesUpdateCallbackId {
|
||||||
|
ProfilesUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_update(&self, callback: ProfilesUpdateCallbackId) {
|
||||||
|
self.imp.remove_on_update(callback.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access to the `identity` unique index on the table `profiles`,
|
||||||
|
/// which allows point queries on the field of the same name
|
||||||
|
/// via the [`ProfilesIdentityUnique::find`] method.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.profiles().identity().find(...)`.
|
||||||
|
pub struct ProfilesIdentityUnique<'ctx> {
|
||||||
|
imp: __sdk::UniqueConstraintHandle<Profile, __sdk::Identity>,
|
||||||
|
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> ProfilesTableHandle<'ctx> {
|
||||||
|
/// Get a handle on the `identity` unique index on the table `profiles`.
|
||||||
|
pub fn identity(&self) -> ProfilesIdentityUnique<'ctx> {
|
||||||
|
ProfilesIdentityUnique {
|
||||||
|
imp: self
|
||||||
|
.imp
|
||||||
|
.get_unique_constraint::<__sdk::Identity>("identity"),
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> ProfilesIdentityUnique<'ctx> {
|
||||||
|
/// Find the subscribed row whose `identity` column value is equal to `col_val`,
|
||||||
|
/// if such a row is present in the client cache.
|
||||||
|
pub fn find(&self, col_val: &__sdk::Identity) -> Option<Profile> {
|
||||||
|
self.imp.find(col_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access to the `username` unique index on the table `profiles`,
|
||||||
|
/// which allows point queries on the field of the same name
|
||||||
|
/// via the [`ProfilesUsernameUnique::find`] method.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.profiles().username().find(...)`.
|
||||||
|
pub struct ProfilesUsernameUnique<'ctx> {
|
||||||
|
imp: __sdk::UniqueConstraintHandle<Profile, String>,
|
||||||
|
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> ProfilesTableHandle<'ctx> {
|
||||||
|
/// Get a handle on the `username` unique index on the table `profiles`.
|
||||||
|
pub fn username(&self) -> ProfilesUsernameUnique<'ctx> {
|
||||||
|
ProfilesUsernameUnique {
|
||||||
|
imp: self.imp.get_unique_constraint::<String>("username"),
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> ProfilesUsernameUnique<'ctx> {
|
||||||
|
/// Find the subscribed row whose `username` column value is equal to `col_val`,
|
||||||
|
/// if such a row is present in the client cache.
|
||||||
|
pub fn find(&self, col_val: &String) -> Option<Profile> {
|
||||||
|
self.imp.find(col_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||||
|
let _table = client_cache.get_or_make_table::<Profile>("profiles");
|
||||||
|
_table.add_unique_constraint::<__sdk::Identity>("identity", |row| &row.identity);
|
||||||
|
_table.add_unique_constraint::<String>("username", |row| &row.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn parse_table_update(
|
||||||
|
raw_updates: __ws::v2::TableUpdate,
|
||||||
|
) -> __sdk::Result<__sdk::TableUpdate<Profile>> {
|
||||||
|
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||||
|
__sdk::InternalError::failed_parse("TableUpdate<Profile>", "TableUpdate")
|
||||||
|
.with_cause(e)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for query builder access to the table `Profile`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||||
|
pub trait profilesQueryTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Get a query builder for the table `Profile`.
|
||||||
|
fn profiles(&self) -> __sdk::__query_builder::Table<Profile>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl profilesQueryTableAccess for __sdk::QueryTableAccessor {
|
||||||
|
fn profiles(&self) -> __sdk::__query_builder::Table<Profile> {
|
||||||
|
__sdk::__query_builder::Table::new("profiles")
|
||||||
|
}
|
||||||
|
}
|
||||||
70
packages/ppc/generated/remove_activity_reducer.rs
Normal file
70
packages/ppc/generated/remove_activity_reducer.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::activity_type::Activity;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub(super) struct RemoveActivityArgs {
|
||||||
|
pub activity: Activity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RemoveActivityArgs> for super::Reducer {
|
||||||
|
fn from(args: RemoveActivityArgs) -> Self {
|
||||||
|
Self::RemoveActivity {
|
||||||
|
activity: args.activity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for RemoveActivityArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the reducer `remove_activity`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteReducers`].
|
||||||
|
pub trait remove_activity {
|
||||||
|
/// Request that the remote module invoke the reducer `remove_activity` to run as soon as possible.
|
||||||
|
///
|
||||||
|
/// This method returns immediately, and errors only if we are unable to send the request.
|
||||||
|
/// The reducer will run asynchronously in the future,
|
||||||
|
/// and this method provides no way to listen for its completion status.
|
||||||
|
/// /// Use [`remove_activity:remove_activity_then`] to run a callback after the reducer completes.
|
||||||
|
fn remove_activity(&self, activity: Activity) -> __sdk::Result<()> {
|
||||||
|
self.remove_activity_then(activity, |_, _| {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request that the remote module invoke the reducer `remove_activity` to run as soon as possible,
|
||||||
|
/// registering `callback` to run when we are notified that the reducer completed.
|
||||||
|
///
|
||||||
|
/// This method returns immediately, and errors only if we are unable to send the request.
|
||||||
|
/// The reducer will run asynchronously in the future,
|
||||||
|
/// and its status can be observed with the `callback`.
|
||||||
|
fn remove_activity_then(
|
||||||
|
&self,
|
||||||
|
activity: Activity,
|
||||||
|
|
||||||
|
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
|
) -> __sdk::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl remove_activity for super::RemoteReducers {
|
||||||
|
fn remove_activity_then(
|
||||||
|
&self,
|
||||||
|
activity: Activity,
|
||||||
|
|
||||||
|
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
|
) -> __sdk::Result<()> {
|
||||||
|
self.imp
|
||||||
|
.invoke_reducer_with_callback(RemoveActivityArgs { activity }, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
72
packages/ppc/generated/set_activity_name_reducer.rs
Normal file
72
packages/ppc/generated/set_activity_name_reducer.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub(super) struct SetActivityNameArgs {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SetActivityNameArgs> for super::Reducer {
|
||||||
|
fn from(args: SetActivityNameArgs) -> Self {
|
||||||
|
Self::SetActivityName {
|
||||||
|
id: args.id,
|
||||||
|
name: args.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for SetActivityNameArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the reducer `set_activity_name`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteReducers`].
|
||||||
|
pub trait set_activity_name {
|
||||||
|
/// Request that the remote module invoke the reducer `set_activity_name` to run as soon as possible.
|
||||||
|
///
|
||||||
|
/// This method returns immediately, and errors only if we are unable to send the request.
|
||||||
|
/// The reducer will run asynchronously in the future,
|
||||||
|
/// and this method provides no way to listen for its completion status.
|
||||||
|
/// /// Use [`set_activity_name:set_activity_name_then`] to run a callback after the reducer completes.
|
||||||
|
fn set_activity_name(&self, id: u64, name: String) -> __sdk::Result<()> {
|
||||||
|
self.set_activity_name_then(id, name, |_, _| {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request that the remote module invoke the reducer `set_activity_name` to run as soon as possible,
|
||||||
|
/// registering `callback` to run when we are notified that the reducer completed.
|
||||||
|
///
|
||||||
|
/// This method returns immediately, and errors only if we are unable to send the request.
|
||||||
|
/// The reducer will run asynchronously in the future,
|
||||||
|
/// and its status can be observed with the `callback`.
|
||||||
|
fn set_activity_name_then(
|
||||||
|
&self,
|
||||||
|
id: u64,
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
|
) -> __sdk::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl set_activity_name for super::RemoteReducers {
|
||||||
|
fn set_activity_name_then(
|
||||||
|
&self,
|
||||||
|
id: u64,
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
|
) -> __sdk::Result<()> {
|
||||||
|
self.imp
|
||||||
|
.invoke_reducer_with_callback(SetActivityNameArgs { id, name }, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
162
packages/ppc/generated/task_part_table.rs
Normal file
162
packages/ppc/generated/task_part_table.rs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use super::activity_type::Activity;
|
||||||
|
use super::task_part_type::TaskPart;
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
/// Table handle for the table `task_part`.
|
||||||
|
///
|
||||||
|
/// Obtain a handle from the [`TaskPartTableAccess::task_part`] method on [`super::RemoteTables`],
|
||||||
|
/// like `ctx.db.task_part()`.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.task_part().on_insert(...)`.
|
||||||
|
pub struct TaskPartTableHandle<'ctx> {
|
||||||
|
imp: __sdk::TableHandle<TaskPart>,
|
||||||
|
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the table `task_part`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteTables`].
|
||||||
|
pub trait TaskPartTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Obtain a [`TaskPartTableHandle`], which mediates access to the table `task_part`.
|
||||||
|
fn task_part(&self) -> TaskPartTableHandle<'_>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaskPartTableAccess for super::RemoteTables {
|
||||||
|
fn task_part(&self) -> TaskPartTableHandle<'_> {
|
||||||
|
TaskPartTableHandle {
|
||||||
|
imp: self.imp.get_table::<TaskPart>("task_part"),
|
||||||
|
ctx: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TaskPartInsertCallbackId(__sdk::CallbackId);
|
||||||
|
pub struct TaskPartDeleteCallbackId(__sdk::CallbackId);
|
||||||
|
|
||||||
|
impl<'ctx> __sdk::Table for TaskPartTableHandle<'ctx> {
|
||||||
|
type Row = TaskPart;
|
||||||
|
type EventContext = super::EventContext;
|
||||||
|
|
||||||
|
fn count(&self) -> u64 {
|
||||||
|
self.imp.count()
|
||||||
|
}
|
||||||
|
fn iter(&self) -> impl Iterator<Item = TaskPart> + '_ {
|
||||||
|
self.imp.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
type InsertCallbackId = TaskPartInsertCallbackId;
|
||||||
|
|
||||||
|
fn on_insert(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> TaskPartInsertCallbackId {
|
||||||
|
TaskPartInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_insert(&self, callback: TaskPartInsertCallbackId) {
|
||||||
|
self.imp.remove_on_insert(callback.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteCallbackId = TaskPartDeleteCallbackId;
|
||||||
|
|
||||||
|
fn on_delete(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> TaskPartDeleteCallbackId {
|
||||||
|
TaskPartDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_delete(&self, callback: TaskPartDeleteCallbackId) {
|
||||||
|
self.imp.remove_on_delete(callback.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TaskPartUpdateCallbackId(__sdk::CallbackId);
|
||||||
|
|
||||||
|
impl<'ctx> __sdk::TableWithPrimaryKey for TaskPartTableHandle<'ctx> {
|
||||||
|
type UpdateCallbackId = TaskPartUpdateCallbackId;
|
||||||
|
|
||||||
|
fn on_update(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||||
|
) -> TaskPartUpdateCallbackId {
|
||||||
|
TaskPartUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_update(&self, callback: TaskPartUpdateCallbackId) {
|
||||||
|
self.imp.remove_on_update(callback.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access to the `identity` unique index on the table `task_part`,
|
||||||
|
/// which allows point queries on the field of the same name
|
||||||
|
/// via the [`TaskPartIdentityUnique::find`] method.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.task_part().identity().find(...)`.
|
||||||
|
pub struct TaskPartIdentityUnique<'ctx> {
|
||||||
|
imp: __sdk::UniqueConstraintHandle<TaskPart, __sdk::Identity>,
|
||||||
|
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> TaskPartTableHandle<'ctx> {
|
||||||
|
/// Get a handle on the `identity` unique index on the table `task_part`.
|
||||||
|
pub fn identity(&self) -> TaskPartIdentityUnique<'ctx> {
|
||||||
|
TaskPartIdentityUnique {
|
||||||
|
imp: self
|
||||||
|
.imp
|
||||||
|
.get_unique_constraint::<__sdk::Identity>("identity"),
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> TaskPartIdentityUnique<'ctx> {
|
||||||
|
/// Find the subscribed row whose `identity` column value is equal to `col_val`,
|
||||||
|
/// if such a row is present in the client cache.
|
||||||
|
pub fn find(&self, col_val: &__sdk::Identity) -> Option<TaskPart> {
|
||||||
|
self.imp.find(col_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||||
|
let _table = client_cache.get_or_make_table::<TaskPart>("task_part");
|
||||||
|
_table.add_unique_constraint::<__sdk::Identity>("identity", |row| &row.identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn parse_table_update(
|
||||||
|
raw_updates: __ws::v2::TableUpdate,
|
||||||
|
) -> __sdk::Result<__sdk::TableUpdate<TaskPart>> {
|
||||||
|
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||||
|
__sdk::InternalError::failed_parse("TableUpdate<TaskPart>", "TableUpdate")
|
||||||
|
.with_cause(e)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for query builder access to the table `TaskPart`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||||
|
pub trait task_partQueryTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Get a query builder for the table `TaskPart`.
|
||||||
|
fn task_part(&self) -> __sdk::__query_builder::Table<TaskPart>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl task_partQueryTableAccess for __sdk::QueryTableAccessor {
|
||||||
|
fn task_part(&self) -> __sdk::__query_builder::Table<TaskPart> {
|
||||||
|
__sdk::__query_builder::Table::new("task_part")
|
||||||
|
}
|
||||||
|
}
|
||||||
54
packages/ppc/generated/task_part_type.rs
Normal file
54
packages/ppc/generated/task_part_type.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::activity_type::Activity;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct TaskPart {
|
||||||
|
pub identity: __sdk::Identity,
|
||||||
|
pub current_activity: Activity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for TaskPart {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Column accessor struct for the table `TaskPart`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to columns for query building.
|
||||||
|
pub struct TaskPartCols {
|
||||||
|
pub identity: __sdk::__query_builder::Col<TaskPart, __sdk::Identity>,
|
||||||
|
pub current_activity: __sdk::__query_builder::Col<TaskPart, Activity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasCols for TaskPart {
|
||||||
|
type Cols = TaskPartCols;
|
||||||
|
fn cols(table_name: &'static str) -> Self::Cols {
|
||||||
|
TaskPartCols {
|
||||||
|
identity: __sdk::__query_builder::Col::new(table_name, "identity"),
|
||||||
|
current_activity: __sdk::__query_builder::Col::new(table_name, "current_activity"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indexed column accessor struct for the table `TaskPart`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to indexed columns for query building.
|
||||||
|
pub struct TaskPartIxCols {
|
||||||
|
pub identity: __sdk::__query_builder::IxCol<TaskPart, __sdk::Identity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasIxCols for TaskPart {
|
||||||
|
type IxCols = TaskPartIxCols;
|
||||||
|
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||||
|
TaskPartIxCols {
|
||||||
|
identity: __sdk::__query_builder::IxCol::new(table_name, "identity"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::CanBeLookupTable for TaskPart {}
|
||||||
58
packages/ppc/generated/task_type.rs
Normal file
58
packages/ppc/generated/task_type.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct Task {
|
||||||
|
pub id: u64,
|
||||||
|
pub done: bool,
|
||||||
|
pub name: String,
|
||||||
|
pub parent: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for Task {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Column accessor struct for the table `Task`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to columns for query building.
|
||||||
|
pub struct TaskCols {
|
||||||
|
pub id: __sdk::__query_builder::Col<Task, u64>,
|
||||||
|
pub done: __sdk::__query_builder::Col<Task, bool>,
|
||||||
|
pub name: __sdk::__query_builder::Col<Task, String>,
|
||||||
|
pub parent: __sdk::__query_builder::Col<Task, Option<u64>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasCols for Task {
|
||||||
|
type Cols = TaskCols;
|
||||||
|
fn cols(table_name: &'static str) -> Self::Cols {
|
||||||
|
TaskCols {
|
||||||
|
id: __sdk::__query_builder::Col::new(table_name, "id"),
|
||||||
|
done: __sdk::__query_builder::Col::new(table_name, "done"),
|
||||||
|
name: __sdk::__query_builder::Col::new(table_name, "name"),
|
||||||
|
parent: __sdk::__query_builder::Col::new(table_name, "parent"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indexed column accessor struct for the table `Task`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to indexed columns for query building.
|
||||||
|
pub struct TaskIxCols {
|
||||||
|
pub id: __sdk::__query_builder::IxCol<Task, u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasIxCols for Task {
|
||||||
|
type IxCols = TaskIxCols;
|
||||||
|
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||||
|
TaskIxCols {
|
||||||
|
id: __sdk::__query_builder::IxCol::new(table_name, "id"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::CanBeLookupTable for Task {}
|
||||||
159
packages/ppc/generated/tasks_table.rs
Normal file
159
packages/ppc/generated/tasks_table.rs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use super::task_type::Task;
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
/// Table handle for the table `tasks`.
|
||||||
|
///
|
||||||
|
/// Obtain a handle from the [`TasksTableAccess::tasks`] method on [`super::RemoteTables`],
|
||||||
|
/// like `ctx.db.tasks()`.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.tasks().on_insert(...)`.
|
||||||
|
pub struct TasksTableHandle<'ctx> {
|
||||||
|
imp: __sdk::TableHandle<Task>,
|
||||||
|
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the table `tasks`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteTables`].
|
||||||
|
pub trait TasksTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Obtain a [`TasksTableHandle`], which mediates access to the table `tasks`.
|
||||||
|
fn tasks(&self) -> TasksTableHandle<'_>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TasksTableAccess for super::RemoteTables {
|
||||||
|
fn tasks(&self) -> TasksTableHandle<'_> {
|
||||||
|
TasksTableHandle {
|
||||||
|
imp: self.imp.get_table::<Task>("tasks"),
|
||||||
|
ctx: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TasksInsertCallbackId(__sdk::CallbackId);
|
||||||
|
pub struct TasksDeleteCallbackId(__sdk::CallbackId);
|
||||||
|
|
||||||
|
impl<'ctx> __sdk::Table for TasksTableHandle<'ctx> {
|
||||||
|
type Row = Task;
|
||||||
|
type EventContext = super::EventContext;
|
||||||
|
|
||||||
|
fn count(&self) -> u64 {
|
||||||
|
self.imp.count()
|
||||||
|
}
|
||||||
|
fn iter(&self) -> impl Iterator<Item = Task> + '_ {
|
||||||
|
self.imp.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
type InsertCallbackId = TasksInsertCallbackId;
|
||||||
|
|
||||||
|
fn on_insert(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> TasksInsertCallbackId {
|
||||||
|
TasksInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_insert(&self, callback: TasksInsertCallbackId) {
|
||||||
|
self.imp.remove_on_insert(callback.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteCallbackId = TasksDeleteCallbackId;
|
||||||
|
|
||||||
|
fn on_delete(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> TasksDeleteCallbackId {
|
||||||
|
TasksDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_delete(&self, callback: TasksDeleteCallbackId) {
|
||||||
|
self.imp.remove_on_delete(callback.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TasksUpdateCallbackId(__sdk::CallbackId);
|
||||||
|
|
||||||
|
impl<'ctx> __sdk::TableWithPrimaryKey for TasksTableHandle<'ctx> {
|
||||||
|
type UpdateCallbackId = TasksUpdateCallbackId;
|
||||||
|
|
||||||
|
fn on_update(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||||
|
) -> TasksUpdateCallbackId {
|
||||||
|
TasksUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_update(&self, callback: TasksUpdateCallbackId) {
|
||||||
|
self.imp.remove_on_update(callback.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access to the `id` unique index on the table `tasks`,
|
||||||
|
/// which allows point queries on the field of the same name
|
||||||
|
/// via the [`TasksIdUnique::find`] method.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.tasks().id().find(...)`.
|
||||||
|
pub struct TasksIdUnique<'ctx> {
|
||||||
|
imp: __sdk::UniqueConstraintHandle<Task, u64>,
|
||||||
|
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> TasksTableHandle<'ctx> {
|
||||||
|
/// Get a handle on the `id` unique index on the table `tasks`.
|
||||||
|
pub fn id(&self) -> TasksIdUnique<'ctx> {
|
||||||
|
TasksIdUnique {
|
||||||
|
imp: self.imp.get_unique_constraint::<u64>("id"),
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> TasksIdUnique<'ctx> {
|
||||||
|
/// Find the subscribed row whose `id` column value is equal to `col_val`,
|
||||||
|
/// if such a row is present in the client cache.
|
||||||
|
pub fn find(&self, col_val: &u64) -> Option<Task> {
|
||||||
|
self.imp.find(col_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||||
|
let _table = client_cache.get_or_make_table::<Task>("tasks");
|
||||||
|
_table.add_unique_constraint::<u64>("id", |row| &row.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn parse_table_update(
|
||||||
|
raw_updates: __ws::v2::TableUpdate,
|
||||||
|
) -> __sdk::Result<__sdk::TableUpdate<Task>> {
|
||||||
|
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||||
|
__sdk::InternalError::failed_parse("TableUpdate<Task>", "TableUpdate")
|
||||||
|
.with_cause(e)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for query builder access to the table `Task`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||||
|
pub trait tasksQueryTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Get a query builder for the table `Task`.
|
||||||
|
fn tasks(&self) -> __sdk::__query_builder::Table<Task>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tasksQueryTableAccess for __sdk::QueryTableAccessor {
|
||||||
|
fn tasks(&self) -> __sdk::__query_builder::Table<Task> {
|
||||||
|
__sdk::__query_builder::Table::new("tasks")
|
||||||
|
}
|
||||||
|
}
|
||||||
70
packages/ppc/generated/update_activity_reducer.rs
Normal file
70
packages/ppc/generated/update_activity_reducer.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::activity_type::Activity;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub(super) struct UpdateActivityArgs {
|
||||||
|
pub activity: Activity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UpdateActivityArgs> for super::Reducer {
|
||||||
|
fn from(args: UpdateActivityArgs) -> Self {
|
||||||
|
Self::UpdateActivity {
|
||||||
|
activity: args.activity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for UpdateActivityArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the reducer `update_activity`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteReducers`].
|
||||||
|
pub trait update_activity {
|
||||||
|
/// Request that the remote module invoke the reducer `update_activity` to run as soon as possible.
|
||||||
|
///
|
||||||
|
/// This method returns immediately, and errors only if we are unable to send the request.
|
||||||
|
/// The reducer will run asynchronously in the future,
|
||||||
|
/// and this method provides no way to listen for its completion status.
|
||||||
|
/// /// Use [`update_activity:update_activity_then`] to run a callback after the reducer completes.
|
||||||
|
fn update_activity(&self, activity: Activity) -> __sdk::Result<()> {
|
||||||
|
self.update_activity_then(activity, |_, _| {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request that the remote module invoke the reducer `update_activity` to run as soon as possible,
|
||||||
|
/// registering `callback` to run when we are notified that the reducer completed.
|
||||||
|
///
|
||||||
|
/// This method returns immediately, and errors only if we are unable to send the request.
|
||||||
|
/// The reducer will run asynchronously in the future,
|
||||||
|
/// and its status can be observed with the `callback`.
|
||||||
|
fn update_activity_then(
|
||||||
|
&self,
|
||||||
|
activity: Activity,
|
||||||
|
|
||||||
|
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
|
) -> __sdk::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl update_activity for super::RemoteReducers {
|
||||||
|
fn update_activity_then(
|
||||||
|
&self,
|
||||||
|
activity: Activity,
|
||||||
|
|
||||||
|
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
|
) -> __sdk::Result<()> {
|
||||||
|
self.imp
|
||||||
|
.invoke_reducer_with_callback(UpdateActivityArgs { activity }, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,32 @@
|
|||||||
|
#![feature(oneshot_channel)]
|
||||||
|
|
||||||
mod platform {
|
mod platform {
|
||||||
#[cfg(feature = "target-obsidian")]
|
#[cfg(feature = "target-obsidian")]
|
||||||
mod obsidian;
|
pub mod obsidian;
|
||||||
|
|
||||||
|
#[cfg(feature = "target-os")]
|
||||||
|
pub mod os {
|
||||||
|
pub mod server;
|
||||||
|
pub mod ui;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "target-os")]
|
||||||
|
mod website;
|
||||||
|
|
||||||
|
#[cfg(feature = "target-os")]
|
||||||
|
pub use platform::os::server::server;
|
||||||
|
#[cfg(feature = "target-os")]
|
||||||
|
pub use platform::os::ui;
|
||||||
|
|
||||||
|
pub mod auth {
|
||||||
|
pub mod client;
|
||||||
|
}
|
||||||
|
pub use auth::client::client_auth;
|
||||||
|
|
||||||
|
pub mod spacetime;
|
||||||
|
pub mod timetrack;
|
||||||
|
|
||||||
|
// the generated source
|
||||||
|
// right now only from spacetimedb
|
||||||
|
mod generated;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@ppc/obsidian-plugin",
|
"name": "@ppc/obsidian-plugin",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"build_rust": "wasm-pack -vvvvv build -d src/platform/obsidian/rust_dist --target web --no-default-features --features target-obsidian",
|
"build_rust": "wasm-pack -vvvvv build -d platform/obsidian/rust_dist --target web --no-default-features --features target-obsidian",
|
||||||
"build": "deno run -A build_rust && mkdir -p dist && touch dist/.hotreload && cp manifest.json dist && deno -A esbuild.config.mjs production",
|
"build": "deno run -A build_rust && mkdir -p dist && touch dist/.hotreload && cp manifest.json dist && deno -A esbuild.config.mjs production",
|
||||||
"dev": ""
|
"dev": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
export function add(a: number, b: number): number {
|
|
||||||
return a + b;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts
|
|
||||||
if (import.meta.main) {
|
|
||||||
console.log("Add 2 + 3 =", add(2, 3));
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { assertEquals } from "@std/assert";
|
|
||||||
import { add } from "./main.ts";
|
|
||||||
|
|
||||||
Deno.test(function addTest() {
|
|
||||||
assertEquals(add(2, 3), 5);
|
|
||||||
});
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
use mize::Mize;
|
use mize::Mize;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
@@ -17,7 +18,27 @@ macro_rules! console_log {
|
|||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn obsidian_mize_entrypoint() {
|
pub fn obsidian_mize_entrypoint() {
|
||||||
console_log!("hiiiiiiiiiiiii from rust");
|
|
||||||
let mize = Mize::new();
|
let mize = Mize::new();
|
||||||
|
//crate::client_auth(mize)?;
|
||||||
console_log!("Obsidian Mize initialized");
|
console_log!("Obsidian Mize initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn show_main_ui() {
|
||||||
|
let document = web_sys::window()
|
||||||
|
.expect("global window not available")
|
||||||
|
.document()
|
||||||
|
.expect("no document in the global window obj");
|
||||||
|
let root = document.get_element_by_id("ppc-app-root").unwrap();
|
||||||
|
dioxus_web::launch::launch(
|
||||||
|
app,
|
||||||
|
Vec::new(),
|
||||||
|
vec![Box::new(dioxus_web::Config::new().rootelement(root))],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app() -> Element {
|
||||||
|
rsx! {
|
||||||
|
div { "Hello, from dioxus!" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Plugin, TFile } from "obsidian";
|
import { Plugin, ItemView, TFile } from "obsidian";
|
||||||
import wasmBinary from "./rust_dist/ppc_bg.wasm";
|
import wasmBinary from "./rust_dist/ppc_bg.wasm";
|
||||||
import * as wasmNamespace from "./rust_dist/ppc";
|
import * as wasmNamespace from "./rust_dist/ppc";
|
||||||
import { c2vi_obsidian_canvas_patch } from "@ppc/marts";
|
import { c2vi_obsidian_canvas_patch } from "@ppc/marts";
|
||||||
@@ -10,23 +10,29 @@ export default class PPCObsidianPlugin extends Plugin {
|
|||||||
const wasmModule = await import("./rust_dist/ppc.js");
|
const wasmModule = await import("./rust_dist/ppc.js");
|
||||||
await wasmModule.default({ module_or_path: wasmBinary });
|
await wasmModule.default({ module_or_path: wasmBinary });
|
||||||
wasmModule.initSync();
|
wasmModule.initSync();
|
||||||
wasmModule.obsidian_mize_entrypoint();
|
console.log("module: ", wasmModule);
|
||||||
|
|
||||||
const mize = {};
|
// mize filler.... till there is a more proper js lib implemented
|
||||||
mize.get_part_native = (name) => this;
|
this.mize = { parts: { obsidian: this }, misc_stuff: { wasmModule } };
|
||||||
|
window.mize = this.mize;
|
||||||
|
mize.get_part_native = (name) => mize.parts[name];
|
||||||
|
|
||||||
|
wasmModule.obsidian_mize_entrypoint();
|
||||||
|
|
||||||
// c2vi part which patches canvas movement
|
// c2vi part which patches canvas movement
|
||||||
c2vi_obsidian_canvas_patch(mize);
|
c2vi_obsidian_canvas_patch(mize);
|
||||||
|
|
||||||
task_obsidian_otask_parse(mize);
|
task_obsidian_otask_parse(mize);
|
||||||
|
|
||||||
|
this.registerView("ppc-app-view", (leaf) => new PPCAppView(leaf, this));
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "app",
|
id: "app",
|
||||||
name: "Open ppc app main page",
|
name: "Open ppc app main page",
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
const leaf = this.app.workspace.getLeaf(true);
|
const leaf = this.app.workspace.getLeaf(true);
|
||||||
await leaf.setViewState({
|
await leaf.setViewState({
|
||||||
type: "ppc-app",
|
type: "ppc-app-view",
|
||||||
active: true,
|
active: true,
|
||||||
});
|
});
|
||||||
this.app.workspace.revealLeaf(leaf);
|
this.app.workspace.revealLeaf(leaf);
|
||||||
@@ -35,6 +41,33 @@ export default class PPCObsidianPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
this.app.workspace.detachLeavesOfType("ppc-app");
|
this.app.workspace.detachLeavesOfType("ppc-app-view");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PPCAppView extends ItemView {
|
||||||
|
constructor(leaf: WorkspaceLeaf, plugin: PPCObsidianPlugin) {
|
||||||
|
super(leaf);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewType(): string {
|
||||||
|
return "ppc-app-view";
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayText(): string {
|
||||||
|
return "PPC App";
|
||||||
|
}
|
||||||
|
|
||||||
|
override async onOpen() {
|
||||||
|
this.root = document.createElement("div");
|
||||||
|
this.root.id = "ppc-app-root";
|
||||||
|
this.containerEl.children[1].appendChild(this.root);
|
||||||
|
|
||||||
|
this.plugin.mize.misc_stuff.wasmModule.show_main_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
override async onClose() {
|
||||||
|
this.root.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,46 @@ use clap::crate_version;
|
|||||||
use mize::{Mize, MizeResult};
|
use mize::{Mize, MizeResult};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut mize = Mize::new().expect("failed to create mize instance");
|
let mut mize = Mize::new().expect("failed to create mize");
|
||||||
|
|
||||||
#[cfg(feature = "target-os")]
|
#[cfg(feature = "target-os")]
|
||||||
os_main(&mut mize);
|
let result = os_main(&mut mize);
|
||||||
|
|
||||||
mize.run();
|
if let Err(err) = result {
|
||||||
|
println!("EER: {:?}", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "target-os")]
|
#[cfg(feature = "target-os")]
|
||||||
fn os_main(mize: &mut Mize) -> MizeResult<()> {
|
fn os_main(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
use ppc::spacetime::spacetime;
|
||||||
|
|
||||||
marts::cli(mize)?;
|
marts::cli(mize)?;
|
||||||
|
//marts::js(mize)?;
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
{
|
||||||
|
use ppc::timetrack::timetrack;
|
||||||
|
|
||||||
|
ppc::client_auth(mize)?;
|
||||||
|
if let Err(err) = spacetime(mize) {
|
||||||
|
println!("spacetime part failed to initialize: {:?}", err);
|
||||||
|
};
|
||||||
|
timetrack(mize)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
//marts::habitica(mize)?;
|
||||||
|
marts::c2vi(mize)?;
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
ppc::server(mize)?;
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
cli.subcommand(Command::new("gui"), |_, _| {
|
||||||
|
ppc::ui::launch_desktop_app();
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
let mut cli = mize.get_part_native::<marts::CliPart>("cli")?;
|
let mut cli = mize.get_part_native::<marts::CliPart>("cli")?;
|
||||||
|
|
||||||
@@ -24,12 +53,14 @@ fn os_main(mize: &mut Mize) -> MizeResult<()> {
|
|||||||
.version(crate_version!())
|
.version(crate_version!())
|
||||||
.name("ppc")
|
.name("ppc")
|
||||||
.author("ppc")
|
.author("ppc")
|
||||||
.about("the ppc desktop program"))
|
.about("the ppc app"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
cli.subcommand(Command::new("test"), |_| {
|
cli.subcommand(Command::new("test"), |_, _| {
|
||||||
println!("test ppc...");
|
println!("test ppc...");
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
Ok(())
|
|
||||||
|
drop(cli);
|
||||||
|
mize.run()
|
||||||
}
|
}
|
||||||
|
|||||||
201
packages/ppc/platform/os/server.rs
Normal file
201
packages/ppc/platform/os/server.rs
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
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 base64::{
|
||||||
|
Engine,
|
||||||
|
alphabet::STANDARD,
|
||||||
|
engine::{GeneralPurpose, GeneralPurposeConfig},
|
||||||
|
};
|
||||||
|
use clap::Command;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use mize::Mize;
|
||||||
|
use mize::MizeResult;
|
||||||
|
use openidconnect::Scope;
|
||||||
|
use openidconnect::{ClientId, ClientSecret, IssuerUrl};
|
||||||
|
use tower::ServiceBuilder;
|
||||||
|
use tower_sessions::{
|
||||||
|
Expiry, MemoryStore, SessionManagerLayer,
|
||||||
|
cookie::{SameSite, time::Duration},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn server(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
let mut cli = mize.get_part_native::<marts::CliPart>("cli")?;
|
||||||
|
mize.add_name_only_part("ppc.server");
|
||||||
|
|
||||||
|
mize.new_opt("auth.issuer");
|
||||||
|
mize.new_opt("auth.client_id");
|
||||||
|
mize.new_opt("auth.client_secret");
|
||||||
|
mize.new_opt("auth.redirect");
|
||||||
|
mize.new_opt("auth.cookie_key");
|
||||||
|
|
||||||
|
let mut mize = mize.clone();
|
||||||
|
cli.subcommand(Command::new("server"), move |_, _| {
|
||||||
|
// Create tokio runtime for async server
|
||||||
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
|
rt.block_on(async { start_server(&mut mize).await })?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_server(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
dioxus::logger::initialize_default();
|
||||||
|
|
||||||
|
let issuer = mize.get_config("auth.issuer")?.value_string()?;
|
||||||
|
let listen_addr = mize.get_config("server.listen_addr")?.value_string()?;
|
||||||
|
let port = mize.get_config("server.port")?.value_string()?;
|
||||||
|
let issuer = mize.get_config("auth.issuer")?.value_string()?;
|
||||||
|
let client_id = mize.get_config("auth.client_id")?.value_string()?;
|
||||||
|
let client_secret = mize.get_config("auth.client_secret")?.value_string()?;
|
||||||
|
let url = mize.get_config("web.url")?.value_string()?;
|
||||||
|
let redirect_url = format!("{url}/oidc");
|
||||||
|
|
||||||
|
let session_store = MemoryStore::default();
|
||||||
|
let session_layer = SessionManagerLayer::new(session_store)
|
||||||
|
.with_secure(false)
|
||||||
|
.with_same_site(SameSite::Lax)
|
||||||
|
.with_expiry(Expiry::OnInactivity(Duration::seconds(120)));
|
||||||
|
|
||||||
|
let oidc_login_service = ServiceBuilder::new()
|
||||||
|
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
|
||||||
|
dbg!(&e);
|
||||||
|
e.into_response()
|
||||||
|
}))
|
||||||
|
.layer(OidcLoginLayer::<EmptyAdditionalClaims>::new());
|
||||||
|
|
||||||
|
let oidc_client = OidcClient::<EmptyAdditionalClaims>::builder()
|
||||||
|
.with_default_http_client()
|
||||||
|
.with_redirect_url(Uri::from_str(redirect_url.as_str())?)
|
||||||
|
.with_client_id(ClientId::new(client_id))
|
||||||
|
.add_scope(Scope::new("profile".into()))
|
||||||
|
.add_scope(Scope::new("email".into()))
|
||||||
|
// Optional: add untrusted audiences. If the `aud` claim contains any of these audiences, the token is rejected.
|
||||||
|
//.add_untrusted_audience(Audience::new("123456789".to_string()))
|
||||||
|
.with_client_secret(ClientSecret::new(client_secret))
|
||||||
|
.discover(IssuerUrl::new(issuer.into()).expect("Invalid IssuerUrl"))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let oidc_auth_service = ServiceBuilder::new()
|
||||||
|
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
|
||||||
|
dbg!(&e);
|
||||||
|
e.into_response()
|
||||||
|
}))
|
||||||
|
.layer(OidcAuthLayer::new(oidc_client));
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/foo", get(authenticated))
|
||||||
|
.route("/logout", get(logout))
|
||||||
|
.layer(oidc_login_service)
|
||||||
|
.route("/bar", get(maybe_authenticated))
|
||||||
|
.route("/oidc", any(handle_oidc_redirect::<EmptyAdditionalClaims>))
|
||||||
|
.layer(oidc_auth_service)
|
||||||
|
.layer(session_layer)
|
||||||
|
.layer(tower_http::trace::TraceLayer::new_for_http())
|
||||||
|
// not authenticated routes
|
||||||
|
.route("/", get(ppc_main_page))
|
||||||
|
.route("/oidc_display_code_and_state", get(display_code_and_state));
|
||||||
|
|
||||||
|
tracing::info!("Running on http://{listen_addr}:{port}");
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(format!("{listen_addr}:{port}"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
axum::serve(listener, app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authenticated(claims: OidcClaims<EmptyAdditionalClaims>) -> impl IntoResponse {
|
||||||
|
format!("Hello {}", claims.subject().as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ppc_main_page() -> impl IntoResponse {
|
||||||
|
"hi"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[axum::debug_handler]
|
||||||
|
async fn maybe_authenticated(
|
||||||
|
claims: Result<OidcClaims<EmptyAdditionalClaims>, axum_oidc::error::ExtractorError>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if let Ok(claims) = claims {
|
||||||
|
format!(
|
||||||
|
"Hello {}! You are already logged in from another Handler.",
|
||||||
|
claims.subject().as_str()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"Hello anon!".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn logout(logout: OidcRpInitiatedLogout) -> impl IntoResponse {
|
||||||
|
logout.with_post_logout_redirect(Uri::from_static("https://localhost:3000"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/web_callback.rs
|
||||||
|
use axum::{extract::Query, response::Html};
|
||||||
|
use html_escape::encode_text;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct CallbackParams {
|
||||||
|
pub code: Option<String>,
|
||||||
|
pub state: Option<String>,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct Combined<'a> {
|
||||||
|
code: &'a str,
|
||||||
|
state: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn display_code_and_state(Query(params): Query<CallbackParams>) -> Html<String> {
|
||||||
|
// Safe fallbacks
|
||||||
|
let code = params.code.clone().unwrap_or_default();
|
||||||
|
let state = params.state.clone().unwrap_or_default();
|
||||||
|
let error = params.error.clone().unwrap_or_default();
|
||||||
|
|
||||||
|
// Create JSON object and base64 encode it
|
||||||
|
let combined = Combined {
|
||||||
|
code: &code,
|
||||||
|
state: &state,
|
||||||
|
};
|
||||||
|
// serde_json::to_string should succeed for simple strings; fallback to empty JSON on error
|
||||||
|
let json = serde_json::to_string(&combined)
|
||||||
|
.unwrap_or_else(|_| "{\"code\":\"\",\"state\":\"\"}".into());
|
||||||
|
let base64_engine = GeneralPurpose::new(
|
||||||
|
&STANDARD,
|
||||||
|
GeneralPurposeConfig::new().with_encode_padding(false),
|
||||||
|
);
|
||||||
|
let b64 = base64_engine.encode(json);
|
||||||
|
|
||||||
|
let b64_escaped = encode_text(&b64);
|
||||||
|
let error_escaped = encode_text(&error);
|
||||||
|
println!("err: {}", error_escaped);
|
||||||
|
println!("b: {}", b64_escaped);
|
||||||
|
|
||||||
|
let html = match error_escaped.as_ref() {
|
||||||
|
"" => format!("Copy this into your client: {}", b64_escaped),
|
||||||
|
_ => format!("An Error occured: {}", error_escaped),
|
||||||
|
};
|
||||||
|
|
||||||
|
Html(html)
|
||||||
|
}
|
||||||
17
packages/ppc/platform/os/ui/mod.rs
Normal file
17
packages/ppc/platform/os/ui/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Minimal cross-platform UI component
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
pub fn hello_world() -> Element {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
style: "text-align: center; padding: 2rem;",
|
||||||
|
h1 { "Hello from Mize!" }
|
||||||
|
p { "This UI works on Web, Desktop, and Obsidian plugin" }
|
||||||
|
p { "Current platform: {std::env::consts::OS}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn launch_desktop_app() {
|
||||||
|
dioxus::launch(hello_world);
|
||||||
|
}
|
||||||
44
packages/ppc/spacetime.rs
Normal file
44
packages/ppc/spacetime.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use mize::{Mize, MizeError, MizePart, MizeResult, mize_part};
|
||||||
|
|
||||||
|
use crate::{auth::client::ClientAuth, generated::DbConnection};
|
||||||
|
|
||||||
|
#[mize_part]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SpacetimePart {
|
||||||
|
mize: Mize,
|
||||||
|
con: Option<DbConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MizePart for SpacetimePart {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"spacetime"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpacetimePart {
|
||||||
|
pub fn con(&mut self) -> &mut DbConnection {
|
||||||
|
self.con.as_mut().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spacetime(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
let auth = mize.get_part_native::<ClientAuth>("client_auth")?;
|
||||||
|
let db_url = mize.get_config("spacetime.url")?.value_string()?;
|
||||||
|
|
||||||
|
let con = DbConnection::builder()
|
||||||
|
.with_token(Some(auth.id_token()?))
|
||||||
|
.with_database_name("ppc")
|
||||||
|
.with_uri(db_url)
|
||||||
|
//.on_connect(callback)
|
||||||
|
//.on_disconnect(callback)
|
||||||
|
//.on_connect_error(callback)
|
||||||
|
//.with_compression(...)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
con.run_threaded();
|
||||||
|
|
||||||
|
mize.register_part(Box::new(SpacetimePart {
|
||||||
|
mize: mize.clone(),
|
||||||
|
con: Some(con),
|
||||||
|
}))
|
||||||
|
}
|
||||||
13
packages/ppc/spacetimedb/Cargo.toml
Normal file
13
packages/ppc/spacetimedb/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "ppc-spacetime"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
spacetimedb = "2.1.0"
|
||||||
|
log = "0.4"
|
||||||
121
packages/ppc/spacetimedb/src/lib.rs
Normal file
121
packages/ppc/spacetimedb/src/lib.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
use log::*;
|
||||||
|
use spacetimedb::{reducer, table, Identity, ReducerContext, Table, Timestamp};
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
current_activity: Activity,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[table(accessor = activities, public)]
|
||||||
|
pub struct Activity {
|
||||||
|
#[primary_key]
|
||||||
|
#[auto_inc]
|
||||||
|
id: u64,
|
||||||
|
name: String,
|
||||||
|
task: Option<u64>,
|
||||||
|
parents_ids: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[table(accessor = tasks, public)]
|
||||||
|
pub struct Task {
|
||||||
|
#[primary_key]
|
||||||
|
#[auto_inc]
|
||||||
|
id: u64,
|
||||||
|
done: bool,
|
||||||
|
name: String,
|
||||||
|
parent: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[table(accessor = events, public)]
|
||||||
|
pub struct Event {
|
||||||
|
start: Timestamp,
|
||||||
|
end: Timestamp,
|
||||||
|
activity: Activity,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO....
|
||||||
|
|
||||||
|
#[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)?;
|
||||||
|
|
||||||
|
// add user
|
||||||
|
ctx.db.profiles().insert(Profile {
|
||||||
|
identity: ctx.sender(),
|
||||||
|
username: ctx.identity().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_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);
|
||||||
|
}
|
||||||
257
packages/ppc/timetrack/mod.rs
Normal file
257
packages/ppc/timetrack/mod.rs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
use std::{io::Read, sync::oneshot, thread, time::Duration};
|
||||||
|
|
||||||
|
use clap::{ArgMatches, Command};
|
||||||
|
use mize::{Mize, MizeError, MizePart, MizePartGuard, MizeResult, mize_err, mize_part};
|
||||||
|
use spacetimedb_sdk::{
|
||||||
|
DbContext, Error, Event, Identity, Status, SubscriptionHandle, Table, TableWithPrimaryKey,
|
||||||
|
credentials,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
generated::{
|
||||||
|
ActivitiesTableAccess, Activity, DbConnection, activitiesQueryTableAccess, remove_activity,
|
||||||
|
update_activity,
|
||||||
|
},
|
||||||
|
spacetime::SpacetimePart,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[mize_part]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TimeTrack {
|
||||||
|
mize: Mize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn timetrack(mize: &mut Mize) -> MizeResult<()> {
|
||||||
|
let mut cli = mize.get_part_native::<marts::CliPart>("cli")?;
|
||||||
|
|
||||||
|
cli.subcommand(
|
||||||
|
Command::new("listActivities").alias("lac"),
|
||||||
|
command_list_activities,
|
||||||
|
);
|
||||||
|
cli.subcommand(
|
||||||
|
Command::new("editActivities").alias("eac"),
|
||||||
|
command_edit_activities,
|
||||||
|
);
|
||||||
|
|
||||||
|
mize.register_part(Box::new(TimeTrack { mize: mize.clone() }));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MizePart for TimeTrack {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"timetrack"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_list_activities(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
||||||
|
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
||||||
|
let activities = get_activities(&mut mize, spacetime.con())?;
|
||||||
|
for ac in activities.clone() {
|
||||||
|
print!("ac {}: {}", ac.id, ac.name);
|
||||||
|
if !ac.parents_ids.is_empty() {
|
||||||
|
print!(" -");
|
||||||
|
}
|
||||||
|
for child_id in ac.parents_ids {
|
||||||
|
let child_ac = activities.iter().find(|ac| ac.id == child_id).unwrap();
|
||||||
|
print!(" {}", child_ac.name);
|
||||||
|
}
|
||||||
|
print!("\n")
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_edit_activities(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
||||||
|
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
||||||
|
|
||||||
|
// get list of activities
|
||||||
|
let activities = get_activities(&mut mize, spacetime.con())?;
|
||||||
|
|
||||||
|
let con = spacetime.con();
|
||||||
|
|
||||||
|
// write to tmp file
|
||||||
|
let mut tmp_file_content = String::new();
|
||||||
|
for ac in activities.iter() {
|
||||||
|
tmp_file_content.push_str(format!("{} {}", ac.id, ac.name).as_str());
|
||||||
|
if !ac.parents_ids.is_empty() {
|
||||||
|
tmp_file_content.push_str(" -");
|
||||||
|
for parent_id in ac.parents_ids.iter() {
|
||||||
|
let parent_ac = activities.iter().find(|ac| ac.id == *parent_id).unwrap();
|
||||||
|
tmp_file_content.push_str(" ");
|
||||||
|
tmp_file_content.push_str(&parent_ac.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tmp_file_content.push_str("\n");
|
||||||
|
}
|
||||||
|
let tmp_file = std::env::temp_dir().join(format!(
|
||||||
|
"ppc-edit-activities-{}.txt",
|
||||||
|
chrono::Utc::now().timestamp()
|
||||||
|
));
|
||||||
|
std::fs::write(&tmp_file, tmp_file_content)
|
||||||
|
.map_err(|e| mize_err!("Failed to write temp file: {}", e))?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// open $EDITOR blocking
|
||||||
|
std::process::Command::new("nvim")
|
||||||
|
.arg(&tmp_file)
|
||||||
|
.status()
|
||||||
|
.map_err(|e| mize_err!("Failed to run nvim: {}", e))?;
|
||||||
|
|
||||||
|
let new_file_content = std::fs::read_to_string(&tmp_file)
|
||||||
|
.map_err(|e| mize_err!("Failed to read temp file: {}", e))?;
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
|
||||||
|
let old_activities = get_activities(&mut mize, con)?;
|
||||||
|
if let Err(err) =
|
||||||
|
parse_activities_from_text(&mut mize, con, new_file_content, old_activities)
|
||||||
|
{
|
||||||
|
println!("error parsing the activities from the file: {:?}", err);
|
||||||
|
println!("press ENTER to open the file again");
|
||||||
|
|
||||||
|
// wait for user input
|
||||||
|
// so that they can se the err msg, before running nvim again
|
||||||
|
std::io::stdin().read_line(&mut input)?;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete tmpfile
|
||||||
|
let _ = std::fs::remove_file(&tmp_file);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_activities(mize: &mut Mize, con: &mut DbConnection) -> MizeResult<Vec<Activity>> {
|
||||||
|
let (exit_tx, exit_rx) = std::sync::oneshot::channel::<Vec<Activity>>();
|
||||||
|
con.subscription_builder()
|
||||||
|
.on_applied(|ctx| {
|
||||||
|
exit_tx.send(ctx.db.activities().iter().collect()).unwrap();
|
||||||
|
})
|
||||||
|
.add_query(|q| q.from.activities())
|
||||||
|
.subscribe();
|
||||||
|
let activities = exit_rx.recv().unwrap();
|
||||||
|
Ok(activities)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_activities_from_text(
|
||||||
|
mize: &mut Mize,
|
||||||
|
con: &mut DbConnection,
|
||||||
|
text: String,
|
||||||
|
old_activities: Vec<Activity>,
|
||||||
|
) -> MizeResult<()> {
|
||||||
|
let mut activities_to_delete = old_activities.clone();
|
||||||
|
let mut wait_revievers = Vec::new();
|
||||||
|
for line in text.lines() {
|
||||||
|
if line == "" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split
|
||||||
|
let mut parts = line.split(" ");
|
||||||
|
let id: u64 = parts.next().ok_or(mize_err!(".."))?.parse()?;
|
||||||
|
let name = parts.next().ok_or(mize_err!(".."))?.to_string();
|
||||||
|
|
||||||
|
// remove from to-delete list
|
||||||
|
if let Some(pos) = activities_to_delete.iter().position(|ac| ac.id == id) {
|
||||||
|
activities_to_delete.remove(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait rx
|
||||||
|
let (wait_tx, wait_rx) = oneshot::channel::<()>();
|
||||||
|
wait_revievers.push(wait_rx);
|
||||||
|
|
||||||
|
// update or create activity
|
||||||
|
if let Some(old_activity) = old_activities.iter().find(|ac| ac.id == id) {
|
||||||
|
con.reducers.update_activity_then(
|
||||||
|
Activity {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
task: old_activity.task,
|
||||||
|
parents_ids: old_activity.parents_ids.clone(),
|
||||||
|
},
|
||||||
|
move |_, _| {
|
||||||
|
wait_tx.send(());
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
con.reducers.update_activity_then(
|
||||||
|
Activity {
|
||||||
|
id: 0,
|
||||||
|
name,
|
||||||
|
task: None,
|
||||||
|
parents_ids: Vec::new(),
|
||||||
|
},
|
||||||
|
move |_, _| {
|
||||||
|
wait_tx.send(());
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove deleted activities
|
||||||
|
for activity in activities_to_delete {
|
||||||
|
let (wait_tx, wait_rx) = oneshot::channel::<()>();
|
||||||
|
wait_revievers.push(wait_rx);
|
||||||
|
con.reducers.remove_activity_then(activity, move |_, _| {
|
||||||
|
wait_tx.send(());
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for db stuff to finish
|
||||||
|
for wait_rx in wait_revievers {
|
||||||
|
wait_rx.recv().unwrap();
|
||||||
|
}
|
||||||
|
let mut wait_revievers = Vec::new();
|
||||||
|
|
||||||
|
// check for declared parents
|
||||||
|
let new_activities = get_activities(mize, con)?;
|
||||||
|
println!("new_activities: {:?}", new_activities);
|
||||||
|
for line in text.lines() {
|
||||||
|
if line == "" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split
|
||||||
|
let mut parts = line.split(" ");
|
||||||
|
let id: u64 = parts.next().ok_or(mize_err!(".."))?.parse()?;
|
||||||
|
let name = parts.next().ok_or(mize_err!(".."))?.to_string();
|
||||||
|
|
||||||
|
// parse parents
|
||||||
|
let seperator = parts.next();
|
||||||
|
if seperator.is_some() && seperator.unwrap() == "-" {
|
||||||
|
let mut parents_ids = Vec::new();
|
||||||
|
for parent_name in parts {
|
||||||
|
// find id from old_parts by this name
|
||||||
|
let res_from_old = old_activities.iter().find(|ac| ac.name == parent_name);
|
||||||
|
let res_from_new = new_activities.iter().find(|ac| ac.name == parent_name);
|
||||||
|
let parent_activity = res_from_old.or(res_from_new).ok_or(mize_err!(
|
||||||
|
"activity '{name}' declares parent '{parent_name}'"
|
||||||
|
))?;
|
||||||
|
parents_ids.push(parent_activity.id);
|
||||||
|
}
|
||||||
|
let child_activity = new_activities
|
||||||
|
.iter()
|
||||||
|
.find(|ac| ac.name == name)
|
||||||
|
.ok_or(mize_err!(".."))?;
|
||||||
|
let (wait_tx, wait_rx) = oneshot::channel::<()>();
|
||||||
|
wait_revievers.push(wait_rx);
|
||||||
|
con.reducers.update_activity_then(
|
||||||
|
Activity {
|
||||||
|
parents_ids,
|
||||||
|
..child_activity.to_owned()
|
||||||
|
},
|
||||||
|
move |_, _| {
|
||||||
|
wait_tx.send(());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for db stuff to finish
|
||||||
|
for wait_rx in wait_revievers {
|
||||||
|
wait_rx.recv().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
185
packages/ppc/website/mod.rs
Normal file
185
packages/ppc/website/mod.rs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
// PPC Website UI Module
|
||||||
|
// This module contains the user interface components for the PPC website
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
/// Main PPC website component
|
||||||
|
|
||||||
|
pub fn ppc_website() -> Element {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: "min-h-screen bg-gray-100",
|
||||||
|
|
||||||
|
// Header
|
||||||
|
header {
|
||||||
|
class: "bg-blue-600 text-white py-4",
|
||||||
|
div {
|
||||||
|
class: "container mx-auto px-4",
|
||||||
|
h1 {
|
||||||
|
class: "text-2xl font-bold",
|
||||||
|
"People Corner (PPC)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main content
|
||||||
|
main {
|
||||||
|
class: "container mx-auto px-4 py-8",
|
||||||
|
|
||||||
|
// Hero section
|
||||||
|
section {
|
||||||
|
class: "bg-white rounded-lg shadow-md p-6 mb-8",
|
||||||
|
h2 {
|
||||||
|
class: "text-xl font-semibold mb-4 text-blue-600",
|
||||||
|
"Welcome to People Corner"
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
class: "text-gray-700 mb-4",
|
||||||
|
"PPC is a social platform for intentional people who want to get more out of life together."
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
class: "text-gray-700",
|
||||||
|
"We're building a community-driven social media network that brings people together to do cool things, share resources, and support each other."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mission section
|
||||||
|
section {
|
||||||
|
class: "bg-white rounded-lg shadow-md p-6 mb-8",
|
||||||
|
h2 {
|
||||||
|
class: "text-xl font-semibold mb-4 text-blue-600",
|
||||||
|
"Our Mission"
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
class: "list-disc pl-6 space-y-2",
|
||||||
|
li { "Bring people together to get more out of life" }
|
||||||
|
li { "Create meaningful connections beyond superficial social media" }
|
||||||
|
li { "Support intentional living and personal growth" }
|
||||||
|
li { "Build a platform by people, for people" }
|
||||||
|
li { "Foster real-world meetups and collaborations" }
|
||||||
|
li { "Create a social media for diligent, intentional people" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key concepts section
|
||||||
|
section {
|
||||||
|
class: "bg-white rounded-lg shadow-md p-6",
|
||||||
|
h2 {
|
||||||
|
class: "text-xl font-semibold mb-4 text-blue-600",
|
||||||
|
"Key Concepts"
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
class: "list-disc pl-6 space-y-2",
|
||||||
|
li {
|
||||||
|
strong { "Trust Network: " }
|
||||||
|
"A system for building and maintaining trust between members"
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
strong { "Intentional Communities: " }
|
||||||
|
"Both online and physical spaces for like-minded people"
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
strong { "Resource Sharing: " }
|
||||||
|
"Platform for sharing skills, knowledge, and physical resources"
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
strong { "Real Connections: " }
|
||||||
|
"Focus on meaningful relationships over superficial interactions"
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
strong { "Personal Growth: " }
|
||||||
|
"Support for individuals to achieve their life goals"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
footer {
|
||||||
|
class: "bg-blue-600 text-white py-4 mt-8",
|
||||||
|
div {
|
||||||
|
class: "container mx-auto px-4 text-center",
|
||||||
|
p { "© 2024 People Corner. All rights reserved." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// About page component
|
||||||
|
pub fn about_page() -> Element {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: "container mx-auto px-4 py-8",
|
||||||
|
h1 {
|
||||||
|
class: "text-2xl font-bold mb-4 text-blue-600",
|
||||||
|
"About PPC"
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
class: "text-gray-700 mb-4",
|
||||||
|
"People Corner (PPC) is more than just a social platform - it's a movement to bring intentional people together to create meaningful connections and achieve more in life."
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
class: "text-gray-700",
|
||||||
|
"Our vision is to build a world where people support each other, share resources, and grow together through real-world interactions and digital connections."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contact page component
|
||||||
|
pub fn contact_page() -> Element {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: "container mx-auto px-4 py-8",
|
||||||
|
h1 {
|
||||||
|
class: "text-2xl font-bold mb-4 text-blue-600",
|
||||||
|
"Contact Us"
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
class: "text-gray-700 mb-4",
|
||||||
|
"We'd love to hear from you! Whether you have questions, feedback, or want to get involved, please reach out."
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
class: "max-w-md space-y-4",
|
||||||
|
div {
|
||||||
|
label {
|
||||||
|
class: "block text-gray-700 mb-2",
|
||||||
|
"Name"
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
class: "w-full px-3 py-2 border rounded-lg",
|
||||||
|
r#type: "text",
|
||||||
|
placeholder: "Your name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
label {
|
||||||
|
class: "block text-gray-700 mb-2",
|
||||||
|
"Email"
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
class: "w-full px-3 py-2 border rounded-lg",
|
||||||
|
r#type: "email",
|
||||||
|
placeholder: "Your email"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
label {
|
||||||
|
class: "block text-gray-700 mb-2",
|
||||||
|
"Message"
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
class: "w-full px-3 py-2 border rounded-lg h-32",
|
||||||
|
placeholder: "Your message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
class: "bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700",
|
||||||
|
r#type: "submit",
|
||||||
|
"Send Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
test/.gitignore
vendored
Normal file
7
test/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
265
test/AGENTS.md
Normal file
265
test/AGENTS.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
You are an expert [0.7 Dioxus](https://dioxuslabs.com/learn/0.7) assistant. Dioxus 0.7 changes every api in dioxus. Only use this up to date documentation. `cx`, `Scope`, and `use_state` are gone
|
||||||
|
|
||||||
|
Provide concise code examples with detailed descriptions
|
||||||
|
|
||||||
|
# Dioxus Dependency
|
||||||
|
|
||||||
|
You can add Dioxus to your `Cargo.toml` like this:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
dioxus = { version = "0.7.1" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["web", "webview", "server"]
|
||||||
|
web = ["dioxus/web"]
|
||||||
|
webview = ["dioxus/desktop"]
|
||||||
|
server = ["dioxus/server"]
|
||||||
|
```
|
||||||
|
|
||||||
|
# Launching your application
|
||||||
|
|
||||||
|
You need to create a main function that sets up the Dioxus runtime and mounts your root component.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus::launch(App);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn App() -> Element {
|
||||||
|
rsx! { "Hello, Dioxus!" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then serve with `dx serve`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -sSL http://dioxus.dev/install.sh | sh
|
||||||
|
dx serve
|
||||||
|
```
|
||||||
|
|
||||||
|
# UI with RSX
|
||||||
|
|
||||||
|
```rust
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: "container", // Attribute
|
||||||
|
color: "red", // Inline styles
|
||||||
|
width: if condition { "100%" }, // Conditional attributes
|
||||||
|
"Hello, Dioxus!"
|
||||||
|
}
|
||||||
|
// Prefer loops over iterators
|
||||||
|
for i in 0..5 {
|
||||||
|
div { "{i}" } // use elements or components directly in loops
|
||||||
|
}
|
||||||
|
if condition {
|
||||||
|
div { "Condition is true!" } // use elements or components directly in conditionals
|
||||||
|
}
|
||||||
|
|
||||||
|
{children} // Expressions are wrapped in brace
|
||||||
|
{(0..5).map(|i| rsx! { span { "Item {i}" } })} // Iterators must be wrapped in braces
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Assets
|
||||||
|
|
||||||
|
The asset macro can be used to link to local files to use in your project. All links start with `/` and are relative to the root of your project.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
rsx! {
|
||||||
|
img {
|
||||||
|
src: asset!("/assets/image.png"),
|
||||||
|
alt: "An image",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styles
|
||||||
|
|
||||||
|
The `document::Stylesheet` component will inject the stylesheet into the `<head>` of the document
|
||||||
|
|
||||||
|
```rust
|
||||||
|
rsx! {
|
||||||
|
document::Stylesheet {
|
||||||
|
href: asset!("/assets/styles.css"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Components
|
||||||
|
|
||||||
|
Components are the building blocks of apps
|
||||||
|
|
||||||
|
* Component are functions annotated with the `#[component]` macro.
|
||||||
|
* The function name must start with a capital letter or contain an underscore.
|
||||||
|
* A component re-renders only under two conditions:
|
||||||
|
1. Its props change (as determined by `PartialEq`).
|
||||||
|
2. An internal reactive state it depends on is updated.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn Input(mut value: Signal<String>) -> Element {
|
||||||
|
rsx! {
|
||||||
|
input {
|
||||||
|
value,
|
||||||
|
oninput: move |e| {
|
||||||
|
*value.write() = e.value();
|
||||||
|
},
|
||||||
|
onkeydown: move |e| {
|
||||||
|
if e.key() == Key::Enter {
|
||||||
|
value.write().clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each component accepts function arguments (props)
|
||||||
|
|
||||||
|
* Props must be owned values, not references. Use `String` and `Vec<T>` instead of `&str` or `&[T]`.
|
||||||
|
* Props must implement `PartialEq` and `Clone`.
|
||||||
|
* To make props reactive and copy, you can wrap the type in `ReadOnlySignal`. Any reactive state like memos and resources that read `ReadOnlySignal` props will automatically re-run when the prop changes.
|
||||||
|
|
||||||
|
# State
|
||||||
|
|
||||||
|
A signal is a wrapper around a value that automatically tracks where it's read and written. Changing a signal's value causes code that relies on the signal to rerun.
|
||||||
|
|
||||||
|
## Local State
|
||||||
|
|
||||||
|
The `use_signal` hook creates state that is local to a single component. You can call the signal like a function (e.g. `my_signal()`) to clone the value, or use `.read()` to get a reference. `.write()` gets a mutable reference to the value.
|
||||||
|
|
||||||
|
Use `use_memo` to create a memoized value that recalculates when its dependencies change. Memos are useful for expensive calculations that you don't want to repeat unnecessarily.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn Counter() -> Element {
|
||||||
|
let mut count = use_signal(|| 0);
|
||||||
|
let mut doubled = use_memo(move || count() * 2); // doubled will re-run when count changes because it reads the signal
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
h1 { "Count: {count}" } // Counter will re-render when count changes because it reads the signal
|
||||||
|
h2 { "Doubled: {doubled}" }
|
||||||
|
button {
|
||||||
|
onclick: move |_| *count.write() += 1, // Writing to the signal rerenders Counter
|
||||||
|
"Increment"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| count.with_mut(|count| *count += 1), // use with_mut to mutate the signal
|
||||||
|
"Increment with with_mut"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context API
|
||||||
|
|
||||||
|
The Context API allows you to share state down the component tree. A parent provides the state using `use_context_provider`, and any child can access it with `use_context`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn App() -> Element {
|
||||||
|
let mut theme = use_signal(|| "light".to_string());
|
||||||
|
use_context_provider(|| theme); // Provide a type to children
|
||||||
|
rsx! { Child {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Child() -> Element {
|
||||||
|
let theme = use_context::<Signal<String>>(); // Consume the same type
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
"Current theme: {theme}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Async
|
||||||
|
|
||||||
|
For state that depends on an asynchronous operation (like a network request), Dioxus provides a hook called `use_resource`. This hook manages the lifecycle of the async task and provides the result to your component.
|
||||||
|
|
||||||
|
* The `use_resource` hook takes an `async` closure. It re-runs this closure whenever any signals it depends on (reads) are updated
|
||||||
|
* The `Resource` object returned can be in several states when read:
|
||||||
|
1. `None` if the resource is still loading
|
||||||
|
2. `Some(value)` if the resource has successfully loaded
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut dog = use_resource(move || async move {
|
||||||
|
// api request
|
||||||
|
});
|
||||||
|
|
||||||
|
match dog() {
|
||||||
|
Some(dog_info) => rsx! { Dog { dog_info } },
|
||||||
|
None => rsx! { "Loading..." },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Routing
|
||||||
|
|
||||||
|
All possible routes are defined in a single Rust `enum` that derives `Routable`. Each variant represents a route and is annotated with `#[route("/path")]`. Dynamic Segments can capture parts of the URL path as parameters by using `:name` in the route string. These become fields in the enum variant.
|
||||||
|
|
||||||
|
The `Router<Route> {}` component is the entry point that manages rendering the correct component for the current URL.
|
||||||
|
|
||||||
|
You can use the `#[layout(NavBar)]` to create a layout shared between pages and place an `Outlet<Route> {}` inside your layout component. The child routes will be rendered in the outlet.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Routable, Clone, PartialEq)]
|
||||||
|
enum Route {
|
||||||
|
#[layout(NavBar)] // This will use NavBar as the layout for all routes
|
||||||
|
#[route("/")]
|
||||||
|
Home {},
|
||||||
|
#[route("/blog/:id")] // Dynamic segment
|
||||||
|
BlogPost { id: i32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn NavBar() -> Element {
|
||||||
|
rsx! {
|
||||||
|
a { href: "/", "Home" }
|
||||||
|
Outlet<Route> {} // Renders Home or BlogPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn App() -> Element {
|
||||||
|
rsx! { Router::<Route> {} }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
dioxus = { version = "0.7.1", features = ["router"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
# Fullstack
|
||||||
|
|
||||||
|
Fullstack enables server rendering and ipc calls. It uses Cargo features (`server` and a client feature like `web`) to split the code into a server and client binaries.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
dioxus = { version = "0.7.1", features = ["fullstack"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server Functions
|
||||||
|
|
||||||
|
Use the `#[post]` / `#[get]` macros to define an `async` function that will only run on the server. On the server, this macro generates an API endpoint. On the client, it generates a function that makes an HTTP request to that endpoint.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[post("/api/double/:path/&query")]
|
||||||
|
async fn double_server(number: i32, path: String, query: i32) -> Result<i32, ServerFnError> {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
Ok(number * 2)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hydration
|
||||||
|
|
||||||
|
Hydration is the process of making a server-rendered HTML page interactive on the client. The server sends the initial HTML, and then the client-side runs, attaches event listeners, and takes control of future rendering.
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
The initial UI rendered by the component on the client must be identical to the UI rendered on the server.
|
||||||
|
|
||||||
|
* Use the `use_server_future` hook instead of `use_resource`. It runs the future on the server, serializes the result, and sends it to the client, ensuring the client has the data immediately for its first render.
|
||||||
|
* Any code that relies on browser-specific APIs (like accessing `localStorage`) must be run *after* hydration. Place this code inside a `use_effect` hook.
|
||||||
21
test/Cargo.toml
Normal file
21
test/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "test"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Sebastian Moser <smoser@student.tugraz.at>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dioxus = { version = "0.7.1", features = ["router", "fullstack"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["web"]
|
||||||
|
# The feature that are only required for the web = ["dioxus/web"] build target should be optional and only enabled in the web = ["dioxus/web"] feature
|
||||||
|
web = ["dioxus/web"]
|
||||||
|
# The feature that are only required for the desktop = ["dioxus/desktop"] build target should be optional and only enabled in the desktop = ["dioxus/desktop"] feature
|
||||||
|
desktop = ["dioxus/desktop"]
|
||||||
|
# The feature that are only required for the mobile = ["dioxus/mobile"] build target should be optional and only enabled in the mobile = ["dioxus/mobile"] feature
|
||||||
|
mobile = ["dioxus/mobile"]
|
||||||
|
# The feature that are only required for the server = ["dioxus/server"] build target should be optional and only enabled in the server = ["dioxus/server"] feature
|
||||||
|
server = ["dioxus/server"]
|
||||||
21
test/Dioxus.toml
Normal file
21
test/Dioxus.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[application]
|
||||||
|
|
||||||
|
[web.app]
|
||||||
|
|
||||||
|
# HTML title tag content
|
||||||
|
title = "test"
|
||||||
|
|
||||||
|
# include `assets` in web platform
|
||||||
|
[web.resource]
|
||||||
|
|
||||||
|
# Additional CSS style files
|
||||||
|
style = []
|
||||||
|
|
||||||
|
# Additional JavaScript files
|
||||||
|
script = []
|
||||||
|
|
||||||
|
[web.resource.dev]
|
||||||
|
|
||||||
|
# Javascript code file
|
||||||
|
# serve: [dev-server] only
|
||||||
|
script = []
|
||||||
58
test/README.md
Normal file
58
test/README.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Development
|
||||||
|
|
||||||
|
Your new jumpstart project includes basic organization with an organized `assets` folder and a `components` folder.
|
||||||
|
If you chose to develop with the router feature, you will also have a `views` folder.
|
||||||
|
|
||||||
|
```
|
||||||
|
project/
|
||||||
|
├─ assets/ # Any assets that are used by the app should be placed here
|
||||||
|
├─ src/
|
||||||
|
│ ├─ main.rs # The entrypoint for the app. It also defines the routes for the app.
|
||||||
|
│ ├─ components/
|
||||||
|
│ │ ├─ mod.rs # Defines the components module
|
||||||
|
│ │ ├─ hero.rs # The Hero component for use in the home page
|
||||||
|
│ │ ├─ echo.rs # The echo component uses server functions to communicate with the server
|
||||||
|
│ ├─ views/ # The views each route will render in the app.
|
||||||
|
│ │ ├─ mod.rs # Defines the module for the views route and re-exports the components for each route
|
||||||
|
│ │ ├─ blog.rs # The component that will render at the /blog/:id route
|
||||||
|
│ │ ├─ home.rs # The component that will render at the / route
|
||||||
|
├─ Cargo.toml # The Cargo.toml file defines the dependencies and feature flags for your project
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Tailwind (Dioxus 0.7+)
|
||||||
|
|
||||||
|
As of Dioxus 0.7, there no longer is a need to manually install tailwind. Simply `dx serve` and you're good to go!
|
||||||
|
|
||||||
|
Automatic tailwind is supported by checking for a file called `tailwind.css` in your app's manifest directory (next to Cargo.toml). To customize the file, use the dioxus.toml:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[application]
|
||||||
|
tailwind_input = "my.css"
|
||||||
|
tailwind_output = "assets/out.css"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tailwind Manual Install
|
||||||
|
|
||||||
|
To use tailwind plugins or manually customize tailwind, you can can install the Tailwind CLI and use it directly.
|
||||||
|
|
||||||
|
1. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
|
||||||
|
2. Install the Tailwind CSS CLI: https://tailwindcss.com/docs/installation/tailwind-cli
|
||||||
|
3. Run the following command in the root of the project to start the Tailwind CSS compiler:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @tailwindcss/cli -i ./input.css -o ./assets/tailwind.css --watch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Serving Your App
|
||||||
|
|
||||||
|
Run the following command in the root of your project to start developing with the default platform:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dx serve --platform web
|
||||||
|
```
|
||||||
|
|
||||||
|
To run for a different platform, use the `--platform platform` flag. E.g.
|
||||||
|
```bash
|
||||||
|
dx serve --platform desktop
|
||||||
|
```
|
||||||
|
|
||||||
BIN
test/assets/favicon.ico
Normal file
BIN
test/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
20
test/assets/header.svg
Normal file
20
test/assets/header.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 23 KiB |
8
test/assets/styling/blog.css
Normal file
8
test/assets/styling/blog.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#blog {
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blog a {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
34
test/assets/styling/echo.css
Normal file
34
test/assets/styling/echo.css
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#echo {
|
||||||
|
width: 360px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-top: 50px;
|
||||||
|
background-color: #1e222d;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#echo>h4 {
|
||||||
|
margin: 0px 0px 15px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#echo>input {
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px white solid;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #ffffff;
|
||||||
|
transition: border-bottom-color 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
display: block;
|
||||||
|
padding: 0px 0px 5px 0px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#echo>input:focus {
|
||||||
|
border-bottom-color: #6d85c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#echo>p {
|
||||||
|
margin: 20px 0px 0px auto;
|
||||||
|
}
|
||||||
42
test/assets/styling/main.css
Normal file
42
test/assets/styling/main.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
body {
|
||||||
|
background-color: #0f1116;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#links {
|
||||||
|
width: 400px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: x-large;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#links a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin: 10px 0px;
|
||||||
|
border: white 1px solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#links a:hover {
|
||||||
|
background-color: #1f1f1f;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
16
test/assets/styling/navbar.css
Normal file
16
test/assets/styling/navbar.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#navbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navbar a {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-right: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navbar a:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #91a4d2;
|
||||||
|
}
|
||||||
8
test/clippy.toml
Normal file
8
test/clippy.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
await-holding-invalid-types = [
|
||||||
|
"generational_box::GenerationalRef",
|
||||||
|
{ path = "generational_box::GenerationalRef", reason = "Reads should not be held over an await point. This will cause any writes to fail while the await is pending since the read borrow is still active." },
|
||||||
|
"generational_box::GenerationalRefMut",
|
||||||
|
{ path = "generational_box::GenerationalRefMut", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
|
||||||
|
"dioxus_signals::WriteLock",
|
||||||
|
{ path = "dioxus_signals::WriteLock", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
|
||||||
|
]
|
||||||
61
test/src/components/echo.rs
Normal file
61
test/src/components/echo.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
const ECHO_CSS: Asset = asset!("/assets/styling/echo.css");
|
||||||
|
|
||||||
|
/// Echo component that demonstrates fullstack server functions.
|
||||||
|
#[component]
|
||||||
|
pub fn Echo() -> Element {
|
||||||
|
// use_signal is a hook. Hooks in dioxus must be run in a consistent order every time the component is rendered.
|
||||||
|
// That means they can't be run inside other hooks, async blocks, if statements, or loops.
|
||||||
|
//
|
||||||
|
// use_signal is a hook that creates a state for the component. It takes a closure that returns the initial value of the state.
|
||||||
|
// The state is automatically tracked and will rerun any other hooks or components that read it whenever it changes.
|
||||||
|
let mut response = use_signal(|| String::new());
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
document::Link { rel: "stylesheet", href: ECHO_CSS }
|
||||||
|
|
||||||
|
div {
|
||||||
|
id: "echo",
|
||||||
|
h4 { "ServerFn Echo" }
|
||||||
|
input {
|
||||||
|
placeholder: "Type here to echo...",
|
||||||
|
// `oninput` is an event handler that will run when the input changes. It can return either nothing or a future
|
||||||
|
// that will be run when the event runs.
|
||||||
|
oninput: move |event| async move {
|
||||||
|
// When we call the echo_server function from the client, it will fire a request to the server and return
|
||||||
|
// the response. It handles serialization and deserialization of the request and response for us.
|
||||||
|
let data = echo_server(event.value()).await.unwrap();
|
||||||
|
|
||||||
|
// After we have the data from the server, we can set the state of the signal to the new value.
|
||||||
|
// Since we read the `response` signal later in this component, the component will rerun.
|
||||||
|
response.set(data);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signals can be called like a function to clone the current value of the signal
|
||||||
|
if !response().is_empty() {
|
||||||
|
p {
|
||||||
|
"Server echoed: "
|
||||||
|
// Since we read the signal inside this component, the component "subscribes" to the signal. Whenever
|
||||||
|
// the signal changes, the component will rerun.
|
||||||
|
i { "{response}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server functions let us define public APIs on the server that can be called like a normal async function from the client.
|
||||||
|
// Each server function needs to be annotated with the `#[post]`/`#[get]` attributes, accept and return serializable types, and return
|
||||||
|
// a `Result` with the error type [`ServerFnError`].
|
||||||
|
//
|
||||||
|
// When the server function is called from the client, it will just serialize the arguments, call the API, and deserialize the
|
||||||
|
// response.
|
||||||
|
#[post("/api/echo")]
|
||||||
|
async fn echo_server(input: String) -> Result<String> {
|
||||||
|
// The body of server function like this comment are only included on the server. If you have any server-only logic like
|
||||||
|
// database queries, you can put it here. Any imports for the server function should either be imported inside the function
|
||||||
|
// or imported under a `#[cfg(feature = "server")]` block.
|
||||||
|
Ok(input)
|
||||||
|
}
|
||||||
25
test/src/components/hero.rs
Normal file
25
test/src/components/hero.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
const HEADER_SVG: Asset = asset!("/assets/header.svg");
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Hero() -> Element {
|
||||||
|
rsx! {
|
||||||
|
// We can create elements inside the rsx macro with the element name followed by a block of attributes and children.
|
||||||
|
div {
|
||||||
|
// Attributes should be defined in the element before any children
|
||||||
|
id: "hero",
|
||||||
|
// After all attributes are defined, we can define child elements and components
|
||||||
|
img { src: HEADER_SVG, id: "header" }
|
||||||
|
div { id: "links",
|
||||||
|
// The RSX macro also supports text nodes surrounded by quotes
|
||||||
|
a { href: "https://dioxuslabs.com/learn/0.7/", "📚 Learn Dioxus" }
|
||||||
|
a { href: "https://dioxuslabs.com/awesome", "🚀 Awesome Dioxus" }
|
||||||
|
a { href: "https://github.com/dioxus-community/", "📡 Community Libraries" }
|
||||||
|
a { href: "https://github.com/DioxusLabs/sdk", "⚙️ Dioxus Development Kit" }
|
||||||
|
a { href: "https://marketplace.visualstudio.com/items?itemName=DioxusLabs.dioxus", "💫 VSCode Extension" }
|
||||||
|
a { href: "https://discord.gg/XgGxMSkvUM", "👋 Community Discord" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
test/src/components/mod.rs
Normal file
9
test/src/components/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
//! The components module contains all shared components for our app. Components are the building blocks of dioxus apps.
|
||||||
|
//! They can be used to defined common UI elements like buttons, forms, and modals. In this template, we define a Hero
|
||||||
|
//! component and an Echo component for fullstack apps to be used in our app.
|
||||||
|
|
||||||
|
mod hero;
|
||||||
|
pub use hero::Hero;
|
||||||
|
|
||||||
|
mod echo;
|
||||||
|
pub use echo::Echo;
|
||||||
66
test/src/main.rs
Normal file
66
test/src/main.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// The dioxus prelude contains a ton of common items used in dioxus apps. It's a good idea to import wherever you
|
||||||
|
// need dioxus
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
use views::{Blog, Home, Navbar};
|
||||||
|
|
||||||
|
/// Define a components module that contains all shared components for our app.
|
||||||
|
mod components;
|
||||||
|
/// Define a views module that contains the UI for all Layouts and Routes for our app.
|
||||||
|
mod views;
|
||||||
|
|
||||||
|
/// The Route enum is used to define the structure of internal routes in our app. All route enums need to derive
|
||||||
|
/// the [`Routable`] trait, which provides the necessary methods for the router to work.
|
||||||
|
///
|
||||||
|
/// Each variant represents a different URL pattern that can be matched by the router. If that pattern is matched,
|
||||||
|
/// the components for that route will be rendered.
|
||||||
|
#[derive(Debug, Clone, Routable, PartialEq)]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
enum Route {
|
||||||
|
// The layout attribute defines a wrapper for all routes under the layout. Layouts are great for wrapping
|
||||||
|
// many routes with a common UI like a navbar.
|
||||||
|
#[layout(Navbar)]
|
||||||
|
// The route attribute defines the URL pattern that a specific route matches. If that pattern matches the URL,
|
||||||
|
// the component for that route will be rendered. The component name that is rendered defaults to the variant name.
|
||||||
|
#[route("/")]
|
||||||
|
Home {},
|
||||||
|
// The route attribute can include dynamic parameters that implement [`std::str::FromStr`] and [`std::fmt::Display`] with the `:` syntax.
|
||||||
|
// In this case, id will match any integer like `/blog/123` or `/blog/-456`.
|
||||||
|
#[route("/blog/:id")]
|
||||||
|
// Fields of the route variant will be passed to the component as props. In this case, the blog component must accept
|
||||||
|
// an `id` prop of type `i32`.
|
||||||
|
Blog { id: i32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can import assets in dioxus with the `asset!` macro. This macro takes a path to an asset relative to the crate root.
|
||||||
|
// The macro returns an `Asset` type that will display as the path to the asset in the browser or a local path in desktop bundles.
|
||||||
|
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||||
|
// The asset macro also minifies some assets like CSS and JS to make bundled smaller
|
||||||
|
const MAIN_CSS: Asset = asset!("/assets/styling/main.css");
|
||||||
|
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// The `launch` function is the main entry point for a dioxus app. It takes a component and renders it with the platform feature
|
||||||
|
// you have enabled
|
||||||
|
dioxus::launch(App);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// App is the main component of our app. Components are the building blocks of dioxus apps. Each component is a function
|
||||||
|
/// that takes some props and returns an Element. In this case, App takes no props because it is the root of our app.
|
||||||
|
///
|
||||||
|
/// Components should be annotated with `#[component]` to support props, better error messages, and autocomplete
|
||||||
|
#[component]
|
||||||
|
fn App() -> Element {
|
||||||
|
// The `rsx!` macro lets us define HTML inside of rust. It expands to an Element with all of our HTML inside.
|
||||||
|
rsx! {
|
||||||
|
// In addition to element and text (which we will see later), rsx can contain other components. In this case,
|
||||||
|
// we are using the `document::Link` component to add a link to our favicon and main CSS file into the head of our app.
|
||||||
|
document::Link { rel: "icon", href: FAVICON }
|
||||||
|
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
||||||
|
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
|
||||||
|
|
||||||
|
// The router component renders the route enum we defined above. It will handle synchronization of the URL and render
|
||||||
|
// the layouts and components for the active route.
|
||||||
|
Router::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
test/src/views/blog.rs
Normal file
39
test/src/views/blog.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use crate::Route;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
const BLOG_CSS: Asset = asset!("/assets/styling/blog.css");
|
||||||
|
|
||||||
|
/// The Blog page component that will be rendered when the current route is `[Route::Blog]`
|
||||||
|
///
|
||||||
|
/// The component takes a `id` prop of type `i32` from the route enum. Whenever the id changes, the component function will be
|
||||||
|
/// re-run and the rendered HTML will be updated.
|
||||||
|
#[component]
|
||||||
|
pub fn Blog(id: i32) -> Element {
|
||||||
|
rsx! {
|
||||||
|
document::Link { rel: "stylesheet", href: BLOG_CSS }
|
||||||
|
|
||||||
|
div {
|
||||||
|
id: "blog",
|
||||||
|
|
||||||
|
// Content
|
||||||
|
h1 { "This is blog #{id}!" }
|
||||||
|
p { "In blog #{id}, we show how the Dioxus router works and how URL parameters can be passed as props to our route components." }
|
||||||
|
|
||||||
|
// Navigation links
|
||||||
|
// The `Link` component lets us link to other routes inside our app. It takes a `to` prop of type `Route` and
|
||||||
|
// any number of child nodes.
|
||||||
|
Link {
|
||||||
|
// The `to` prop is the route that the link should navigate to. We can use the `Route` enum to link to the
|
||||||
|
// blog page with the id of -1. Since we are using an enum instead of a string, all of the routes will be checked
|
||||||
|
// at compile time to make sure they are valid.
|
||||||
|
to: Route::Blog { id: id - 1 },
|
||||||
|
"Previous"
|
||||||
|
}
|
||||||
|
span { " <---> " }
|
||||||
|
Link {
|
||||||
|
to: Route::Blog { id: id + 1 },
|
||||||
|
"Next"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
test/src/views/home.rs
Normal file
11
test/src/views/home.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
use crate::components::{Echo, Hero};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
/// The Home page component that will be rendered when the current route is `[Route::Home]`
|
||||||
|
#[component]
|
||||||
|
pub fn Home() -> Element {
|
||||||
|
rsx! {
|
||||||
|
Hero {}
|
||||||
|
Echo {}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
test/src/views/mod.rs
Normal file
18
test/src/views/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//! The views module contains the components for all Layouts and Routes for our app. Each layout and route in our [`Route`]
|
||||||
|
//! enum will render one of these components.
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! The [`Home`] and [`Blog`] components will be rendered when the current route is [`Route::Home`] or [`Route::Blog`] respectively.
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! The [`Navbar`] component will be rendered on all pages of our app since every page is under the layout. The layout defines
|
||||||
|
//! a common wrapper around all child routes.
|
||||||
|
|
||||||
|
mod home;
|
||||||
|
pub use home::Home;
|
||||||
|
|
||||||
|
mod blog;
|
||||||
|
pub use blog::Blog;
|
||||||
|
|
||||||
|
mod navbar;
|
||||||
|
pub use navbar::Navbar;
|
||||||
32
test/src/views/navbar.rs
Normal file
32
test/src/views/navbar.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use crate::Route;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css");
|
||||||
|
|
||||||
|
/// The Navbar component that will be rendered on all pages of our app since every page is under the layout.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// This layout component wraps the UI of [Route::Home] and [Route::Blog] in a common navbar. The contents of the Home and Blog
|
||||||
|
/// routes will be rendered under the outlet inside this component
|
||||||
|
#[component]
|
||||||
|
pub fn Navbar() -> Element {
|
||||||
|
rsx! {
|
||||||
|
document::Link { rel: "stylesheet", href: NAVBAR_CSS }
|
||||||
|
|
||||||
|
div {
|
||||||
|
id: "navbar",
|
||||||
|
Link {
|
||||||
|
to: Route::Home {},
|
||||||
|
"Home"
|
||||||
|
}
|
||||||
|
Link {
|
||||||
|
to: Route::Blog { id: 1 },
|
||||||
|
"Blog"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The `Outlet` component is used to render the next component inside the layout. In this case, it will render either
|
||||||
|
// the [`Home`] or [`Blog`] component depending on the current route.
|
||||||
|
Outlet::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
test/tailwind.css
Normal file
1
test/tailwind.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
Reference in New Issue
Block a user