idk. but added AGENTS.md
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ target
|
|||||||
result
|
result
|
||||||
dist
|
dist
|
||||||
rust_dist
|
rust_dist
|
||||||
|
deno_dist
|
||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
|||||||
125
AGENTS.md
Normal file
125
AGENTS.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Mize Codebase Architecture Guide for AI Agents
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
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
|
||||||
|
|
||||||
|
### 1. Core Mize Framework (`packages/mize/`)
|
||||||
|
- **Language**: Rust
|
||||||
|
- **Target platforms**: OS (native), WASM (web), bare metal, JVM
|
||||||
|
- **Key features**:
|
||||||
|
- Strongly typed filesystem abstraction
|
||||||
|
- Module system with dynamic loading
|
||||||
|
- Memory store implementation
|
||||||
|
- Protocol versioning and serialization
|
||||||
|
- Cross-platform support
|
||||||
|
|
||||||
|
**Architecture layers:**
|
||||||
|
- `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
|
||||||
|
|
||||||
|
## Build System
|
||||||
|
|
||||||
|
- **Rust**: Cargo workspace with feature flags for different targets
|
||||||
|
- **TypeScript**: Deno-based build system
|
||||||
|
- **Cross-compilation**: Support for WASM and multiple OS targets
|
||||||
|
|
||||||
|
## Key Technologies
|
||||||
|
|
||||||
|
- **Rust crates**: tokio, serde, wasm-bindgen, tracing, clap
|
||||||
|
- **WASM**: wasm-pack, web-sys
|
||||||
|
- **UI**: tao, wry, slint, Qt
|
||||||
|
- **Scripting**: Deno, rustyscript
|
||||||
|
- **Serialization**: ciborium (CBOR), serde_json, toml
|
||||||
|
|
||||||
|
## Development Patterns
|
||||||
|
|
||||||
|
1. **Feature flags**: Extensive use of Cargo features for platform targeting
|
||||||
|
2. **Cross-platform abstraction**: Core logic separated from platform implementations
|
||||||
|
3. **Module system**: Dynamic loading of functionality
|
||||||
|
4. **Strong typing**: Rust's type system used for filesystem safety
|
||||||
|
|
||||||
|
## Next Steps Discussion
|
||||||
|
|
||||||
|
The codebase appears to be in active development with these key areas for future work:
|
||||||
|
|
||||||
|
1. **Platform completion**: Finish OS, WASM, and other platform implementations
|
||||||
|
2. **Module ecosystem**: Develop more built-in modules
|
||||||
|
3. **Performance optimization**: Memory management and serialization
|
||||||
|
4. **Testing**: Comprehensive test coverage across platforms
|
||||||
|
5. **Documentation**: User-facing documentation and examples
|
||||||
|
6. **Integration**: Better integration with existing tools and systems
|
||||||
|
|
||||||
|
## Working with the Codebase
|
||||||
|
|
||||||
|
**For AI Agents:**
|
||||||
|
- Focus on the `core/` directory for platform-independent logic
|
||||||
|
- Use feature flags appropriately when building/testing
|
||||||
|
- Understand the module system for extending functionality
|
||||||
|
- Pay attention to cross-platform considerations
|
||||||
|
|
||||||
|
**Build targets:**
|
||||||
|
- `target-os`: Native operating system builds
|
||||||
|
- `target-wasm`: WebAssembly builds
|
||||||
|
- Other platform-specific targets as needed
|
||||||
|
|
||||||
|
## Key Files for Understanding
|
||||||
|
|
||||||
|
- `packages/mize/Cargo.toml`: Core dependencies and features
|
||||||
|
- `packages/mize/src/lib.rs`: Main library entry point
|
||||||
|
- `packages/mize/src/core/`: Core platform-independent logic
|
||||||
|
- `packages/mize/src/platform/`: Platform-specific implementations
|
||||||
5130
Cargo.lock
generated
5130
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",
|
||||||
|
"test", "test",
|
||||||
]
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|||||||
2
deno.lock
generated
2
deno.lock
generated
@@ -1,12 +1,14 @@
|
|||||||
{
|
{
|
||||||
"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.25.12",
|
||||||
"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",
|
||||||
|
|||||||
@@ -5,18 +5,24 @@ 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 }
|
||||||
rustyscript = "0.12.3"
|
|
||||||
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"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["target-os"]
|
default = ["target-os"]
|
||||||
target-os = [ "clap", "mize/target-os", "deno_core" ]
|
target-os = [ "clap", "mize/target-os", "deno_core", "rustyscript" ]
|
||||||
target-wasm = [ "mize/target-wasm" ]
|
target-wasm = [ "mize/target-wasm" ]
|
||||||
|
clap = ["dep:clap"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
path = "lib.rs"
|
path = "lib.rs"
|
||||||
|
|||||||
225
packages/marts/c2vi/mod.rs
Normal file
225
packages/marts/c2vi/mod.rs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
use chrono::{Datelike, Timelike, Utc};
|
||||||
|
use clap::{Arg, Command};
|
||||||
|
use rusqlite::{params, Connection, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command as StdCommand;
|
||||||
|
|
||||||
|
// Assuming PPC and other modules are structured like this.
|
||||||
|
// This might need adjustment based on the actual definitions in other files.
|
||||||
|
use crate::habitica::Habitica; // Assuming this exists
|
||||||
|
|
||||||
|
pub struct C2vi {
|
||||||
|
pub db: Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl C2vi {
|
||||||
|
pub fn new(local_storage_path: &str) -> Result<Self> {
|
||||||
|
let db_path = PathBuf::from(local_storage_path).join("data.db");
|
||||||
|
let conn = Connection::open(db_path)?;
|
||||||
|
Ok(C2vi { db: conn })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extend_cli_c2vi(mut cli: Command) -> Command {
|
||||||
|
cli.subcommand(
|
||||||
|
Command::new("bed")
|
||||||
|
.about("Command to run when c2vi goes to bed")
|
||||||
|
.action(async || {
|
||||||
|
// Placeholder for actual PPC context
|
||||||
|
// await save_habitica_dailies(ppc);
|
||||||
|
// await save_habitica_logs(ppc);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
Command::new("clearTodos")
|
||||||
|
.about("Clear all todos in Habitica")
|
||||||
|
.alias("clt")
|
||||||
|
.action(async || {
|
||||||
|
// Placeholder
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
Command::new("listTodos")
|
||||||
|
.about("List all todos in Habitica")
|
||||||
|
.alias("dut")
|
||||||
|
.action(async || {
|
||||||
|
// Placeholder
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
Command::new("buyHealthPotion")
|
||||||
|
.about("Buy a health potion in Habitica")
|
||||||
|
.alias("buyh")
|
||||||
|
.action(async || {
|
||||||
|
// Placeholder
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
Command::new("addActionItems")
|
||||||
|
.about("Add action items from a temp file")
|
||||||
|
.alias("aal")
|
||||||
|
.action(async || {
|
||||||
|
// add_action_items();
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
Command::new("skip")
|
||||||
|
.about("Skip a Habitica todo")
|
||||||
|
.alias("sk")
|
||||||
|
.arg(Arg::new("num").default_value("1"))
|
||||||
|
.action(async || {
|
||||||
|
// let num = matches.value_of("num").unwrap().parse::<i32>().unwrap();
|
||||||
|
// skip_habitica_todo(ppc, num);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
Command::new("dumpLog")
|
||||||
|
.about("Dump the complete Habitica log")
|
||||||
|
.action(async || {
|
||||||
|
// dump_habitica_logs(ppc);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
Command::new("listDailies")
|
||||||
|
.about("List all dailies from the local db")
|
||||||
|
.action(async || {
|
||||||
|
// Placeholder
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
Command::new("printTask")
|
||||||
|
.about("Print a specific todo from Habitica")
|
||||||
|
.arg(Arg::new("id").required(true))
|
||||||
|
.action(async || {
|
||||||
|
// let id = matches.value_of("id").unwrap();
|
||||||
|
// Placeholder
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_date(date: chrono::DateTime<Utc>) -> String {
|
||||||
|
date.format("%Y-%m-%d").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct HabiticaDaily {
|
||||||
|
id: String,
|
||||||
|
text: String,
|
||||||
|
notes: String,
|
||||||
|
priority: Option<String>,
|
||||||
|
frequency: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct HabiticaLog {
|
||||||
|
date: String,
|
||||||
|
data: String, // JSON string
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_tables(conn: &Connection) -> Result<()> {
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS habitica_dailies (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
priority TEXT,
|
||||||
|
frequency TEXT
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS habitica_log (
|
||||||
|
date TEXT PRIMARY KEY,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-progress translation. Many functions will need the PPC context passed in.
|
||||||
|
|
||||||
|
/*
|
||||||
|
async fn save_habitica_dailies(ppc: &PPC) -> Result<()> {
|
||||||
|
println!("######### cli bed cmd #########");
|
||||||
|
setup_tables(&ppc.c2vi.db)?;
|
||||||
|
|
||||||
|
// Assuming ppc.habitica.get_tasks returns a Result
|
||||||
|
let dailies = ppc.habitica.get_tasks("dailys").await?;
|
||||||
|
write_dailies_to_database(&dailies, &ppc.c2vi.db).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_dailies_to_database(dailies: &[HabiticaTask], db: &Connection) -> Result<()> {
|
||||||
|
println!("Syncing {} dailies to database...", dailies.len());
|
||||||
|
let tx = db.transaction()?;
|
||||||
|
for daily in dailies {
|
||||||
|
tx.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![
|
||||||
|
daily.id,
|
||||||
|
daily.text,
|
||||||
|
daily.notes.as_deref().unwrap_or(""),
|
||||||
|
daily.priority.map(|p| p.to_string()),
|
||||||
|
daily.frequency.as_deref().unwrap_or("daily"),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
tx.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_action_items(ppc: &PPC) -> Result<()> {
|
||||||
|
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 = env::temp_dir().join(format!("habitica-tasks-{}.md", Utc::now().timestamp()));
|
||||||
|
fs::write(&tmp_file, initial_content.trim())?;
|
||||||
|
|
||||||
|
StdCommand::new("nvim").arg(&tmp_file).status()?;
|
||||||
|
|
||||||
|
let list_str = fs::read_to_string(&tmp_file)?;
|
||||||
|
fs::remove_file(&tmp_file)?;
|
||||||
|
|
||||||
|
let tasks: Vec<&str> = list_str.lines().map(|t| t.trim()).filter(|t| !t.is_empty()).collect();
|
||||||
|
let mut task_ids = vec![];
|
||||||
|
|
||||||
|
for todo_text in tasks {
|
||||||
|
// Assuming api_request exists and works like this
|
||||||
|
let data = ppc.habitica.api_request("POST", "tasks/user", Some(serde_json::json!({ "text": todo_text, "type": "todo" }))).await?;
|
||||||
|
let task_id = data["id"].as_str().unwrap().to_string();
|
||||||
|
println!("Added to-do: '{}' with id '{}'", todo_text, task_id);
|
||||||
|
task_ids.push(task_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for task_id in task_ids {
|
||||||
|
ppc.habitica.api_request("POST", &format!("tasks/{}/move/to/-1", task_id), None).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -8,8 +8,12 @@ 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<
|
||||||
parsers: Option<Vec<Box<dyn (FnOnce(Mize, Vec<String>) -> MizeResult<()>) + Send + Sync>>>,
|
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<()> {
|
||||||
@@ -37,7 +41,7 @@ impl MizePart for CliPart {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let action = match self.actions.get(sub_cmd) {
|
let action = match self.actions.remove(sub_cmd) {
|
||||||
Some(action) => action,
|
Some(action) => action,
|
||||||
None => {
|
None => {
|
||||||
// external parsers
|
// external parsers
|
||||||
@@ -56,7 +60,7 @@ impl MizePart for CliPart {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
action(matches.subcommand_matches(sub_cmd).unwrap())?;
|
action(matches.subcommand_matches(sub_cmd).unwrap(), mize.clone())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn opts(&self, mize: &mut Mize) {
|
fn opts(&self, mize: &mut Mize) {
|
||||||
@@ -65,14 +69,14 @@ 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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"stub_deno": "./js/stub_deno.ts"
|
"stub_deno": "./js/stub_deno.ts"
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"build": "deno bundle -o ./deno_dist/habitica.js ./habitica.ts"
|
"build": "deno bundle -o ./deno_dist/habitica.js ./habitica.ts && deno -A ./esbuild.config.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
108
packages/marts/habitica.rs
Normal file
108
packages/marts/habitica.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use mize::{mize_err, mize_part, Mize, MizeError, MizePart, MizeResult};
|
||||||
|
use reqwest::{blocking::Client, blocking::RequestBuilder, 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 {}
|
||||||
|
|
||||||
|
impl Habitica {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,9 @@ 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;
|
||||||
|
|||||||
@@ -5,18 +5,10 @@ mod cli;
|
|||||||
#[cfg(feature = "target-os")]
|
#[cfg(feature = "target-os")]
|
||||||
pub use cli::*;
|
pub use cli::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "target-os")]
|
||||||
|
pub mod habitica;
|
||||||
|
#[cfg(feature = "target-os")]
|
||||||
|
pub use habitica::*;
|
||||||
|
|
||||||
pub mod js;
|
pub mod js;
|
||||||
pub use js::*;
|
pub use js::*;
|
||||||
|
|
||||||
#[cfg(feature = "target-os")]
|
|
||||||
pub fn habitica(mize: &mut mize::Mize) -> MizeResult<()> {
|
|
||||||
use deno_core::ascii_str_include;
|
|
||||||
|
|
||||||
js::part_from_file(
|
|
||||||
mize,
|
|
||||||
"habitica",
|
|
||||||
ascii_str_include!("./deno_dist/habitica.js"),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -270,9 +270,23 @@ rec {
|
|||||||
#_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.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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -569,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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,7 +667,59 @@ 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());
|
||||||
|
|
||||||
|
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,
|
||||||
@@ -675,8 +727,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();
|
||||||
@@ -687,6 +737,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()
|
||||||
@@ -695,6 +746,7 @@ impl Mize {
|
|||||||
(_, _) => true,
|
(_, _) => true,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
*/
|
||||||
debug!("thread '{}' stopped", name_to_move);
|
debug!("thread '{}' stopped", name_to_move);
|
||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
@@ -704,6 +756,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")
|
||||||
@@ -747,7 +801,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);
|
||||||
|
|
||||||
@@ -780,7 +834,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;
|
||||||
@@ -849,6 +903,12 @@ impl Mize {
|
|||||||
for part in parts.values_mut() {
|
for part in parts.values_mut() {
|
||||||
part.as_deref_mut().unwrap().run(&mut self.clone())?;
|
part.as_deref_mut().unwrap().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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ fn main() {
|
|||||||
#[cfg(feature = "target-os")]
|
#[cfg(feature = "target-os")]
|
||||||
fn os_main(mize: &mut Mize) -> MizeResult<()> {
|
fn os_main(mize: &mut Mize) -> MizeResult<()> {
|
||||||
marts::cli(mize)?;
|
marts::cli(mize)?;
|
||||||
marts::js(mize)?;
|
//marts::js(mize)?;
|
||||||
marts::habitica(mize)?;
|
marts::habitica(mize)?;
|
||||||
|
|
||||||
let mut cli = mize.get_part_native::<marts::CliPart>("cli")?;
|
let mut cli = mize.get_part_native::<marts::CliPart>("cli")?;
|
||||||
@@ -29,7 +29,7 @@ fn os_main(mize: &mut Mize) -> MizeResult<()> {
|
|||||||
.about("the ppc desktop program"))
|
.about("the ppc desktop program"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
cli.subcommand(Command::new("test"), |_| {
|
cli.subcommand(Command::new("test"), |_, _| {
|
||||||
println!("test ppc...");
|
println!("test ppc...");
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|||||||
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