diff --git a/Cargo.lock b/Cargo.lock
index a6c704308..b6f3a5668 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4185,10 +4185,11 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.70"
+version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
+checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
dependencies = [
+ "once_cell",
"wasm-bindgen",
]
@@ -10068,9 +10069,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
-version = "0.2.93"
+version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
+checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
dependencies = [
"cfg-if 1.0.0",
"once_cell",
@@ -10079,13 +10080,12 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
-version = "0.2.93"
+version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
+checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
dependencies = [
"bumpalo",
"log",
- "once_cell",
"proc-macro2",
"quote",
"syn 2.0.90",
@@ -10106,9 +10106,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.93"
+version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
+checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -10116,9 +10116,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.93"
+version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
+checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
dependencies = [
"proc-macro2",
"quote",
@@ -10129,9 +10129,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.93"
+version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
+checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
[[package]]
name = "wasm-streams"
@@ -10208,9 +10208,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.70"
+version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
+checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
dependencies = [
"js-sys",
"wasm-bindgen",
diff --git a/Cargo.toml b/Cargo.toml
index d370985c4..5cfe978f7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,20 +2,24 @@
resolver = '2'
members = [
'./packages/app-lib',
- './apps/app-playground',
+ './apps/app-playground',
'./apps/app',
'./apps/labrinth',
- './apps/daedalus_client',
- './packages/daedalus',
+ './apps/daedalus_client',
+ './packages/daedalus',
+]
+
+exclude = [
+ './packages/tartaros'
]
# Optimize for speed and reduce size on release builds
[profile.release]
-panic = "abort" # Strip expensive panic clean-up logic
+panic = "abort" # Strip expensive panic clean-up logic
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
-lto = true # Enables link to optimizations
-opt-level = "s" # Optimize for binary size
-strip = true # Remove debug symbols
+lto = true # Enables link to optimizations
+opt-level = "s" # Optimize for binary size
+strip = true # Remove debug symbols
[profile.dev.package.sqlx-macros]
opt-level = 3
diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json
index 3f7fe8ed2..c8afc4ba9 100644
--- a/apps/app-frontend/package.json
+++ b/apps/app-frontend/package.json
@@ -13,6 +13,7 @@
},
"dependencies": {
"@modrinth/assets": "workspace:*",
+ "@pyro/tartaros": "workspace:*",
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@sentry/vue": "^8.27.0",
diff --git a/apps/app-frontend/tsconfig.app.json b/apps/app-frontend/tsconfig.app.json
index 000aadc29..f7200093a 100644
--- a/apps/app-frontend/tsconfig.app.json
+++ b/apps/app-frontend/tsconfig.app.json
@@ -18,7 +18,8 @@
"paths": {
"@/*": ["./src/*"]
- }
+ },
+ "types": ["vite/client"]
},
"include": ["src"]
}
diff --git a/apps/frontend/nuxt.config.ts b/apps/frontend/nuxt.config.ts
index 6f9418832..4c5263741 100644
--- a/apps/frontend/nuxt.config.ts
+++ b/apps/frontend/nuxt.config.ts
@@ -1,6 +1,7 @@
import { promises as fs } from "fs";
import { pathToFileURL } from "node:url";
import svgLoader from "vite-svg-loader";
+import wasm from "vite-plugin-wasm";
import { resolve, basename, relative } from "pathe";
import { defineNuxtConfig } from "nuxt/config";
import { $fetch } from "ofetch";
@@ -93,6 +94,7 @@ export default defineNuxtConfig({
dedupe: ["vue"],
},
plugins: [
+ wasm(),
svgLoader({
svgoConfig: {
plugins: [
@@ -357,6 +359,7 @@ export default defineNuxtConfig({
compilerOptions: {
moduleResolution: "bundler",
allowImportingTsExtensions: true,
+ types: ["vite/client"],
},
},
},
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index 52faf1324..4158857a8 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -29,6 +29,8 @@
"sass": "^1.58.0",
"tailwindcss": "^3.4.4",
"typescript": "^5.4.5",
+ "vite": "^5.4.6",
+ "vite-plugin-wasm": "3.3.0",
"vite-svg-loader": "^5.1.0",
"vue-tsc": "^2.0.24"
},
@@ -39,6 +41,7 @@
"@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
+ "@pyro/tartaros": "workspace:*",
"@pinia/nuxt": "^0.5.1",
"@vintl/vintl": "^4.4.1",
"@vueuse/core": "^11.1.0",
diff --git a/apps/frontend/src/components/ui/servers/PanelTerminal.vue b/apps/frontend/src/components/ui/servers/PanelTerminal.vue
index 29dfb8c0c..16197dccd 100644
--- a/apps/frontend/src/components/ui/servers/PanelTerminal.vue
+++ b/apps/frontend/src/components/ui/servers/PanelTerminal.vue
@@ -2,41 +2,11 @@
-
-
-
+
-
-
-
+
+
-
-
-
-
-
diff --git a/apps/frontend/src/components/ui/servers/ServerStats.vue b/apps/frontend/src/components/ui/servers/ServerStats.vue
index 85547442a..36977f284 100644
--- a/apps/frontend/src/components/ui/servers/ServerStats.vue
+++ b/apps/frontend/src/components/ui/servers/ServerStats.vue
@@ -159,7 +159,6 @@ const metrics = ref([
]);
const updateMetrics = () => {
- console.log(props.data.current.ram_usage_bytes);
metrics.value = metrics.value.map((metric, index) => {
if (userPreferences.value.ramAsNumber && index === 1) {
return {
@@ -317,6 +316,7 @@ onUnmounted(() => {
0% {
opacity: 0;
}
+
100% {
opacity: 1;
}
diff --git a/apps/frontend/src/pages/servers/manage/[id].vue b/apps/frontend/src/pages/servers/manage/[id].vue
index 1f888474b..95c8de691 100644
--- a/apps/frontend/src/pages/servers/manage/[id].vue
+++ b/apps/frontend/src/pages/servers/manage/[id].vue
@@ -360,7 +360,6 @@ const error = ref
(null);
const isConnected = ref(false);
const isWSAuthIncorrect = ref(false);
const pyroConsole = usePyroConsole();
-console.log("||||||||||||||||||||||| console", pyroConsole.output);
const cpuData = ref([]);
const ramData = ref([]);
const isActioning = ref(false);
@@ -673,7 +672,6 @@ const updatePowerState = (
state: ServerState,
details?: { oom_killed?: boolean; exit_code?: number },
) => {
- console.log("Power state:", state, details);
serverPowerState.value = state;
if (state === "crashed") {
@@ -896,6 +894,7 @@ definePageMeta({
opacity: 0;
transform: translateX(1rem);
}
+
100% {
opacity: 1;
transform: none;
diff --git a/apps/frontend/src/pages/servers/manage/[id]/index.vue b/apps/frontend/src/pages/servers/manage/[id]/index.vue
index 37d687c66..b427414a6 100644
--- a/apps/frontend/src/pages/servers/manage/[id]/index.vue
+++ b/apps/frontend/src/pages/servers/manage/[id]/index.vue
@@ -80,7 +80,7 @@
-
+
-
+
(null);
const mcError = ref(null);
+const searchQuery = ref("");
+
+const onSearch = (val: string) => {
+ searchQuery.value = val;
+};
const inspectError = async () => {
const log = await props.server.fs?.downloadFile("logs/latest.log");
@@ -578,7 +588,6 @@ const commandTree: any = {
xp: null,
};
-const fullScreen = ref(false);
const commandInput = ref("");
const suggestions = ref([]);
const selectedSuggestionIndex = ref(0);
diff --git a/apps/frontend/src/store/console.ts b/apps/frontend/src/store/console.ts
index 8b901f323..db510bd54 100644
--- a/apps/frontend/src/store/console.ts
+++ b/apps/frontend/src/store/console.ts
@@ -23,6 +23,8 @@ export const usePyroConsole = createGlobalState(() => {
*/
const output: Ref = ref([]);
+ const listeners = ref void>>({});
+
/**
* Adds a new output line to the console output
* Automatically removes the oldest line if max output is exceeded
@@ -50,6 +52,25 @@ export const usePyroConsole = createGlobalState(() => {
if (output.value.length > maxLines) {
output.value.splice(0, output.value.length - maxLines);
}
+
+ for (const line of lines) {
+ for (const listener of Object.values(listeners.value)) {
+ listener(line);
+ }
+ }
+ };
+
+ const addListener = (listener: (line: string) => void): string => {
+ for (const line of output.value) {
+ listener(line);
+ }
+ const id = Math.random().toString(36).substring(7);
+ listeners.value[id] = listener;
+ return id;
+ };
+
+ const removeListener = (id: string): void => {
+ delete listeners.value[id];
};
/**
@@ -64,5 +85,7 @@ export const usePyroConsole = createGlobalState(() => {
addLine,
addLines,
clear,
+ addListener,
+ removeListener,
};
});
diff --git a/apps/frontend/src/workers/console.ts b/apps/frontend/src/workers/console.ts
new file mode 100644
index 000000000..3bee8e09c
--- /dev/null
+++ b/apps/frontend/src/workers/console.ts
@@ -0,0 +1,100 @@
+let pyroConsole: typeof import("@pyro/tartaros").PyroConsole.prototype | null = null;
+let ctx: OffscreenCanvasRenderingContext2D | null = null;
+const mod = import("@pyro/tartaros");
+
+const _log = console.log;
+console.log = (...args: any[]) => {
+ _log.apply(console, ["[tartaros] [worker]", ...args]);
+};
+
+let resolveLoad: () => void;
+const loadPromise: Promise = new Promise((resolve) => {
+ resolveLoad = resolve;
+});
+
+onmessage = async (e) => {
+ if (e.data.type !== "init") {
+ await loadPromise;
+ }
+ try {
+ switch (e.data.type) {
+ case "init": {
+ const { canvas } = e.data;
+ ctx = canvas.getContext("2d") as OffscreenCanvasRenderingContext2D;
+ const { PyroConsole } = await mod;
+ pyroConsole = new PyroConsole(canvas);
+ pyroConsole.init();
+ postMessage({ type: "init", height: ctx.canvas.height });
+ resolveLoad();
+ break;
+ }
+
+ case "line": {
+ if (!pyroConsole) {
+ throw new Error("PyroConsole not initialized");
+ }
+ const { text } = e.data;
+ pyroConsole.add_line(text);
+ break;
+ }
+
+ case "destroy": {
+ pyroConsole?.destroy();
+ pyroConsole?.free();
+ break;
+ }
+
+ case "resize": {
+ if (!ctx) {
+ throw new Error("Canvas not initialized");
+ }
+
+ const { width } = e.data;
+ ctx.canvas.width = width;
+ pyroConsole?.redraw();
+ postMessage({ type: "resize", width });
+ break;
+ }
+
+ case "clear": {
+ pyroConsole?.clear();
+ break;
+ }
+
+ case "mousedown": {
+ if (!pyroConsole) return;
+ const { x, y, clientWidth, clientHeight } = e.data;
+ pyroConsole.mouse_down(x, y, clientWidth, clientHeight);
+ break;
+ }
+
+ case "mouseup": {
+ if (!pyroConsole) return;
+ pyroConsole.mouse_up();
+ break;
+ }
+
+ case "mousemove": {
+ if (!pyroConsole) return;
+ const { y, clientHeight } = e.data;
+ pyroConsole.mouse_move(y, clientHeight);
+ break;
+ }
+
+ case "wheel": {
+ if (!pyroConsole) return;
+ const { deltaY } = e.data;
+ pyroConsole.wheel(deltaY);
+ break;
+ }
+
+ case "search": {
+ const { query } = e.data;
+ pyroConsole?.search(query);
+ break;
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ }
+};
diff --git a/package.json b/package.json
index b2c7ca962..c8a3a7c5a 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,8 @@
"private": true,
"scripts": {
"ui:intl:extract": "pnpm run --filter=@modrinth/ui intl:extract",
- "web:dev": "turbo run dev --filter=@modrinth/frontend",
- "web:build": "turbo run build --filter=@modrinth/frontend",
+ "web:dev": "turbo run dev --filter=@modrinth/frontend --filter=@pyro/tartaros",
+ "web:build": "turbo run build --filter=@modrinth/frontend --filter=@pyro/tartaros",
"web:intl:extract": "pnpm run --filter=@modrinth/frontend intl:extract",
"app:dev": "turbo run dev --filter=@modrinth/app",
"docs:dev": "turbo run dev --filter=@modrinth/docs",
@@ -13,6 +13,7 @@
"app:intl:extract": "pnpm run --filter=@modrinth/app-frontend intl:extract",
"pages:build": "NITRO_PRESET=cloudflare-pages pnpm --filter frontend run build",
"build": "turbo run build --continue",
+ "tartaros:build": "turbo run build --filter=@pyro/tartaros",
"lint": "turbo run lint --continue",
"test": "turbo run test --continue",
"fix": "turbo run fix --continue",
diff --git a/packages/tartaros/.gitignore b/packages/tartaros/.gitignore
new file mode 100644
index 000000000..4e301317e
--- /dev/null
+++ b/packages/tartaros/.gitignore
@@ -0,0 +1,6 @@
+/target
+**/*.rs.bk
+Cargo.lock
+bin/
+pkg/
+wasm-pack.log
diff --git a/packages/tartaros/Cargo.toml b/packages/tartaros/Cargo.toml
new file mode 100644
index 000000000..b055c43cb
--- /dev/null
+++ b/packages/tartaros/Cargo.toml
@@ -0,0 +1,43 @@
+[package]
+name = "pyro-console"
+version = "0.1.0"
+authors = ["not-nullptr "]
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+default = ["console_error_panic_hook"]
+
+[dependencies]
+wasm-bindgen = "0.2.84"
+console_error_panic_hook = { version = "0.1.7", optional = true }
+web-sys = { version = "0.3.76", features = [
+ "HtmlCanvasElement",
+ "CanvasRenderingContext2d",
+ 'Document',
+ 'Element',
+ 'HtmlElement',
+ 'Node',
+ 'Window',
+ 'EventTarget',
+ 'WheelEvent',
+ 'Performance',
+ 'TextMetrics',
+ 'DomRect',
+ 'OffscreenCanvas',
+ 'OffscreenCanvasRenderingContext2d',
+ 'DedicatedWorkerGlobalScope',
+] }
+console_log = "1.0.0"
+log = "0.4.22"
+rust-fuzzy-search = "0.1.1"
+fuzzy-search = "0.1.0"
+
+[dev-dependencies]
+wasm-bindgen-test = "0.3.34"
+
+[profile.release]
+# Tell `rustc` to optimize for small code size.
+opt-level = "s"
diff --git a/packages/tartaros/package.json b/packages/tartaros/package.json
new file mode 100644
index 000000000..ba363ecc8
--- /dev/null
+++ b/packages/tartaros/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@pyro/tartaros",
+ "private": true,
+ "main": "./pkg/index.js",
+ "types": "./pkg/index.d.ts",
+ "packageManager": "pnpm@9.4.0",
+ "scripts": {
+ "build": "wasm-pack build --out-name index"
+ },
+ "devDependencies": {
+ "wasm-pack": "*"
+ }
+}
diff --git a/packages/tartaros/src/ansi.rs b/packages/tartaros/src/ansi.rs
new file mode 100644
index 000000000..1ca0eda45
--- /dev/null
+++ b/packages/tartaros/src/ansi.rs
@@ -0,0 +1,149 @@
+#[derive(Debug, PartialEq, Clone)]
+pub enum AnsiControl {
+ Black,
+ Red,
+ Green,
+ Yellow,
+ Blue,
+ Magenta,
+ Cyan,
+ White,
+}
+
+impl AnsiControl {
+ pub fn to_color(&self) -> String {
+ match self {
+ AnsiControl::Black => "black".to_string(),
+ AnsiControl::Red => "red".to_string(),
+ AnsiControl::Green => "green".to_string(),
+ AnsiControl::Yellow => "orange".to_string(),
+ AnsiControl::Blue => "blue".to_string(),
+ AnsiControl::Magenta => "magenta".to_string(),
+ AnsiControl::Cyan => "cyan".to_string(),
+ AnsiControl::White => "gray".to_string(),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum AnsiCommand {
+ ModifyStyle(AnsiControl),
+ RenderText(String),
+ Reset,
+}
+
+pub struct AnsiParser {
+ input: String,
+}
+
+impl AnsiParser {
+ pub fn new(input: String) -> Self {
+ AnsiParser { input }
+ }
+
+ pub fn parse(&self) -> Vec {
+ let mut commands = Vec::new();
+ let chars = self.input.chars().collect::>();
+ let mut i = 0;
+
+ let mut current_text = String::new();
+
+ while i < chars.len() {
+ match chars[i] {
+ '\x1b' => {
+ // read until 'm', which is the end of the escape sequence
+ let start = i.clone();
+ while i < chars.len() && chars[i] != 'm' {
+ i += 1;
+ }
+ let escape_sequence = &chars[start..i];
+ i += 1;
+ if escape_sequence.len() < 3 {
+ continue;
+ }
+ if !current_text.is_empty() {
+ AnsiParser::run_render_text(&mut commands, &mut current_text);
+ }
+ let escape_sequence = &escape_sequence[2..];
+ let control_characters = escape_sequence
+ .iter()
+ .collect::()
+ .split(";")
+ .filter_map(|x| x.parse::().ok())
+ .collect::>();
+
+ for control_character in control_characters {
+ commands.push(AnsiParser::get_ansi_command(control_character))
+ }
+ }
+ _ => {
+ while i < chars.len() && chars[i] != '\x1b' {
+ current_text.push(chars[i]);
+ i += 1;
+ }
+ }
+ }
+ }
+
+ if !current_text.is_empty() {
+ AnsiParser::run_render_text(&mut commands, &mut current_text);
+ }
+
+ commands
+ }
+
+ fn get_ansi_command(control_character: u8) -> AnsiCommand {
+ match control_character {
+ 30 => AnsiCommand::ModifyStyle(AnsiControl::Black),
+ 31 => AnsiCommand::ModifyStyle(AnsiControl::Red),
+ 32 => AnsiCommand::ModifyStyle(AnsiControl::Green),
+ 33 => AnsiCommand::ModifyStyle(AnsiControl::Yellow),
+ 34 => AnsiCommand::ModifyStyle(AnsiControl::Blue),
+ 35 => AnsiCommand::ModifyStyle(AnsiControl::Magenta),
+ 36 => AnsiCommand::ModifyStyle(AnsiControl::Cyan),
+ 37 => AnsiCommand::ModifyStyle(AnsiControl::White),
+ 0 => AnsiCommand::Reset,
+ _ => AnsiCommand::Reset,
+ }
+ }
+
+ fn run_render_text(commands: &mut Vec, current_text: &mut String) {
+ let mut text = String::new();
+ for c in current_text.chars() {
+ if c == ' ' {
+ if !text.is_empty() {
+ commands.push(AnsiCommand::RenderText(text.clone()));
+ text.clear();
+ }
+ commands.push(AnsiCommand::RenderText(" ".to_string()));
+ } else {
+ text.push(c);
+ }
+ }
+ if !text.is_empty() {
+ commands.push(AnsiCommand::RenderText(text.clone()));
+ }
+ current_text.clear();
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn properly_parse_ansi_string() {
+ let input = "\x1b[31mhello\x1b[0m \x1b[34mworld\x1b[0m".to_string();
+ let parser = AnsiParser::new(input);
+ let commands = parser.parse();
+ println!("{:?}", commands);
+ assert_eq!(commands.len(), 7);
+ assert_eq!(commands[0], AnsiCommand::ModifyStyle(AnsiControl::Red));
+ assert_eq!(commands[1], AnsiCommand::RenderText("hello".to_string()));
+ assert_eq!(commands[2], AnsiCommand::Reset);
+ assert_eq!(commands[3], AnsiCommand::RenderText(" ".to_string()));
+ assert_eq!(commands[4], AnsiCommand::ModifyStyle(AnsiControl::Blue));
+ assert_eq!(commands[5], AnsiCommand::RenderText("world".to_string()));
+ assert_eq!(commands[6], AnsiCommand::Reset);
+ }
+}
diff --git a/packages/tartaros/src/lib.rs b/packages/tartaros/src/lib.rs
new file mode 100644
index 000000000..50de09f14
--- /dev/null
+++ b/packages/tartaros/src/lib.rs
@@ -0,0 +1,425 @@
+/**
+IMPORTANT INFO FOR FUTURE CONTRIBUTORS
+
+Each call to any function on `ctx` is a call to a JS function. This is very slow.
+You need to minimize this -- see `measure.rs` for details.
+*/
+mod ansi;
+mod line;
+mod measure;
+mod utils;
+
+use std::{cell::RefCell, cmp, rc::Rc};
+
+use ansi::AnsiCommand;
+use line::LineManager;
+use log::{info, Level};
+use measure::TextMeasureCache;
+use utils::{
+ cancel_animation_frame, request_animation_frame, set_panic_hook, worker,
+};
+use wasm_bindgen::prelude::*;
+use web_sys::{
+ DedicatedWorkerGlobalScope, OffscreenCanvas,
+ OffscreenCanvasRenderingContext2d,
+};
+
+const FONT_SIZE: usize = 18;
+const LINE_HEIGHT: usize = FONT_SIZE + (FONT_SIZE / 2);
+const LINES_VISIBLE: usize = 18;
+const BOTTOM_MARGIN: usize = 20;
+const CANVAS_HEIGHT: usize = (LINE_HEIGHT * (LINES_VISIBLE)) + BOTTOM_MARGIN;
+const LINE_OFFSET: isize = 0;
+const GAP_LINES: usize = 3;
+const SCROLL_BAR_WIDTH: usize = 8;
+const MIN_SCROLL_BAR_HEIGHT: usize = 16;
+
+#[wasm_bindgen]
+#[derive(Clone)]
+pub struct PyroConsoleState {
+ animation_frame: i32,
+ canvas: OffscreenCanvas,
+ ctx: OffscreenCanvasRenderingContext2d,
+ current_fill_style: String,
+ last_size: (u32, u32),
+ line_manager: LineManager,
+ measure_cache: Rc>,
+ offset: u64,
+ last_frame_time: f64,
+ framerates: Vec,
+ fps: u16,
+ scroll_bar_y_offset: f64,
+ worker_scope: DedicatedWorkerGlobalScope,
+ query: String,
+}
+
+#[wasm_bindgen]
+pub struct PyroConsole {
+ state: Rc>,
+}
+
+#[wasm_bindgen]
+impl PyroConsole {
+ #[wasm_bindgen(constructor)]
+ pub fn new(canvas: OffscreenCanvas) -> PyroConsole {
+ set_panic_hook();
+ _ = console_log::init_with_level(Level::Debug);
+ let ctx = canvas
+ .get_context("2d")
+ .expect("Failed to get canvas ctx")
+ .expect("Failed to get canvas ctx")
+ .dyn_into::()
+ .unwrap();
+ let measure_cache =
+ Rc::new(RefCell::new(TextMeasureCache::new(ctx.clone())));
+ PyroConsole {
+ state: Rc::new(RefCell::new(PyroConsoleState {
+ animation_frame: 0,
+ canvas: canvas.clone(),
+ ctx: ctx.clone(),
+ current_fill_style: "black".to_owned(),
+ last_size: (0, 0),
+ line_manager: LineManager::new(
+ &measure_cache,
+ canvas.width() as f64,
+ ),
+ offset: 0,
+ last_frame_time: 0.0,
+ framerates: Vec::new(),
+ fps: 360,
+ scroll_bar_y_offset: -1.0,
+ worker_scope: worker(),
+ measure_cache,
+ query: String::new(),
+ })),
+ }
+ }
+
+ pub fn init(&mut self) {
+ let mut _state = &self.state;
+
+ let closure = {
+ let state = _state.clone();
+ Closure::::new(move || {
+ let mut state = state.borrow_mut();
+ let new_fps = state.framerates.iter().sum::()
+ / state.framerates.len() as f64;
+ state.fps = cmp::min(new_fps.round() as u16, 360) as u16;
+ state.framerates.clear();
+ })
+ };
+
+ _state
+ .borrow()
+ .worker_scope
+ .set_interval_with_callback_and_timeout_and_arguments_0(
+ closure.as_ref().unchecked_ref(),
+ 1000,
+ )
+ .expect("failed to set interval for fps");
+
+ closure.forget();
+
+ self.state.borrow().canvas.set_height(CANVAS_HEIGHT as u32);
+
+ let f = Rc::new(RefCell::new(None));
+ let g = f.clone();
+
+ let closure = {
+ let state = self.state.clone();
+ Closure::::new(move || {
+ let mut state = state.borrow_mut();
+ if (state.offset as usize)
+ >= state.line_manager.len_linebreaks()
+ {
+ state.offset = 0;
+ }
+ let performance = state
+ .worker_scope
+ .performance()
+ .expect("failed to get performance");
+ PyroConsole::check_for_resize(&mut state);
+ PyroConsole::draw_console(&mut state);
+ let now = performance.now();
+ let delta = now - state.last_frame_time;
+ state.last_frame_time = now;
+ state.framerates.push(1000.0 / delta);
+ state.animation_frame =
+ request_animation_frame(f.borrow().as_ref().unwrap());
+ })
+ };
+
+ *g.borrow_mut() = Some(closure);
+ request_animation_frame(g.borrow().as_ref().unwrap());
+ }
+
+ fn draw_console(state: &mut PyroConsoleState) {
+ let len = state.line_manager.len_linebreaks();
+ let max = cmp::min(len, state.offset as usize + LINES_VISIBLE);
+ let range = cmp::min(state.offset as usize, max)..max;
+ let lines = &state.line_manager.get_lines(range);
+ let (width, height) =
+ (state.canvas.width() as f64, state.canvas.height() as f64);
+ state.ctx.set_fill_style_str("white");
+ state
+ .ctx
+ .set_font(format!("{}px monospace", FONT_SIZE).as_str());
+ state.ctx.fill_rect(0.0, 0.0, width, height);
+ state.ctx.set_fill_style_str("black");
+ state.current_fill_style = "black".to_owned();
+ // search through raw_lines for query
+ for (i, line) in lines.iter().enumerate() {
+ let mut style = "black".to_owned();
+ let mut x = 0.0;
+
+ for command in line.commands.iter() {
+ match command {
+ AnsiCommand::RenderText(text) => {
+ if style != state.current_fill_style {
+ state.ctx.set_fill_style_str(style.as_str());
+ state.current_fill_style = style.clone();
+ }
+ state
+ .ctx
+ .fill_text(
+ text.as_str(),
+ (LINE_HEIGHT / 2) as f64 + x as f64,
+ (i as f64 + 1.0) * LINE_HEIGHT as f64
+ + LINE_OFFSET as f64,
+ )
+ .expect("failed to draw");
+ // x += state.ctx.measure_text(text.as_str()).unwrap().width();
+ // x += state.char_width * text.len() as f64;
+ x += state
+ .measure_cache
+ .borrow_mut()
+ .measure(FONT_SIZE, text.as_str());
+ }
+ AnsiCommand::ModifyStyle(control) => {
+ style = control.to_color();
+ }
+ AnsiCommand::Reset => {
+ style = "black".to_owned();
+ }
+ }
+ }
+ }
+
+ state.ctx.set_font("12px monospace");
+ state.ctx.set_fill_style_str("rgba(0, 0, 0, 0.5)");
+
+ // ctx.fill_text(format!("FPS: {:.2}", fps).as_str(), width - 100.0, 20.0)
+ // .expect("failed to draw");
+ let str = format!(
+ "FPS: {:.2} | Lines: {} ({})",
+ state.fps,
+ state.line_manager.len_linebreaks(),
+ state.line_manager.len_no_linebreaks()
+ );
+ // let text_width = state.ctx.measure_text(str.as_str()).unwrap().width();
+ // let text_width = state.char_width * str.len() as f64;
+ let text_width =
+ state.measure_cache.borrow_mut().measure(12, str.as_str());
+ state
+ .ctx
+ .fill_text(
+ str.as_str(),
+ width - text_width - 8.0 - SCROLL_BAR_WIDTH as f64,
+ 15.0,
+ )
+ .expect("failed to draw");
+
+ // scroll bar
+
+ let total_lines =
+ cmp::max(len as u64, LINES_VISIBLE as u64) - LINES_VISIBLE as u64;
+
+ state.ctx.set_fill_style_str("rgba(0, 0, 0, 0.15)");
+ state.ctx.fill_rect(
+ width - SCROLL_BAR_WIDTH as f64,
+ 0.0,
+ SCROLL_BAR_WIDTH as f64,
+ height,
+ );
+
+ let total_height = (LINE_HEIGHT as f64) * total_lines as f64;
+ let scroll_bar_height = if total_lines <= LINES_VISIBLE as u64 {
+ height as u64
+ } else {
+ cmp::max(
+ (height / total_height * height).round() as u64,
+ MIN_SCROLL_BAR_HEIGHT as u64,
+ )
+ };
+ let scroll_bar_y = (height - scroll_bar_height as f64)
+ * state.offset as f64
+ / total_lines as f64;
+ state.ctx.fill_rect(
+ width - SCROLL_BAR_WIDTH as f64,
+ scroll_bar_y as f64,
+ SCROLL_BAR_WIDTH as f64,
+ scroll_bar_height as f64,
+ );
+ }
+
+ pub fn add_line(&mut self, line: &str) {
+ // self.state.borrow_mut().line_manager.add_line(line, state);
+ let mut state = self.state.borrow_mut();
+ let is_at_bottom = state.offset + LINES_VISIBLE as u64
+ >= (state.line_manager.len_linebreaks() + GAP_LINES as usize)
+ as u64;
+ state.line_manager.add_line(line);
+ if is_at_bottom {
+ state.offset = (state.line_manager.len_linebreaks() + GAP_LINES)
+ .saturating_sub(LINES_VISIBLE)
+ as u64;
+ }
+ }
+
+ pub fn destroy(&self) {
+ cancel_animation_frame(self.state.borrow().animation_frame);
+ }
+
+ fn calculate_offset(y: f64, height: f64, total_lines: u64) -> u64 {
+ let total_height = LINE_HEIGHT as f64 * total_lines as f64; // total scrollable content height
+ let scroll_ratio = height / total_height; // proportion of the visible area to the total content height
+ let scroll_bar_height =
+ f64::max(height * scroll_ratio, MIN_SCROLL_BAR_HEIGHT as f64); // actual scrollbar height
+ let scroll_range = height - scroll_bar_height; // the scrollable range of the scrollbar
+ let offset = ((y / scroll_range).clamp(0.0, 1.0)) * total_lines as f64;
+ (offset.round() as u64).clamp(0, total_lines)
+ }
+
+ pub fn clear(&mut self) {
+ let mut state = self.state.borrow_mut();
+ state.offset = 0;
+ state.line_manager.clear();
+ }
+
+ pub fn redraw(&mut self) {
+ let mut state = self.state.borrow_mut();
+ // state.lines = PyroConsole::calculate_line_breaks(&state);
+ PyroConsole::check_for_resize(&mut state);
+ PyroConsole::draw_console(&mut state);
+ }
+
+ pub fn check_for_resize(state: &mut PyroConsoleState) {
+ let size = (state.canvas.width(), state.canvas.height());
+ if size != state.last_size {
+ state.last_size = size;
+ let is_hooked_on_last_line = state.offset as usize + LINES_VISIBLE
+ >= state.line_manager.len_linebreaks() + GAP_LINES;
+ state.line_manager.on_resize(size.0 as f64);
+ if is_hooked_on_last_line {
+ state.offset = (state.line_manager.len_linebreaks() + GAP_LINES)
+ .saturating_sub(LINES_VISIBLE as usize)
+ as u64;
+ }
+ }
+ }
+
+ pub fn get_scroll_px(&self) -> f64 {
+ let state = self.state.borrow();
+ (state.offset as u64 * LINE_HEIGHT as u64) as f64
+ }
+
+ pub fn get_content_height(&self) -> u32 {
+ // get the content height, where the content height is equal to get_scroll_px() when scrolled to the bottom
+ let mut state = self.state.borrow_mut();
+ let total_lines = cmp::max(
+ state.line_manager.len_linebreaks() as u64,
+ LINES_VISIBLE as u64,
+ ) - LINES_VISIBLE as u64;
+ let total_height =
+ (LINE_HEIGHT as f64) * (total_lines + GAP_LINES as u64) as f64;
+ total_height as u32
+ }
+
+ pub fn mouse_down(
+ &mut self,
+ x: f64,
+ y: f64,
+ client_width: u32,
+ client_height: u32,
+ ) {
+ let mut state = self.state.borrow_mut();
+ let total_lines =
+ (state.line_manager.len_linebreaks() - LINES_VISIBLE) as u64;
+ let height = state.canvas.height() as f64;
+ let total_height = (LINE_HEIGHT as f64) * total_lines as f64;
+ let scroll_bar_height = cmp::max(
+ (height / total_height * height).round() as u64,
+ MIN_SCROLL_BAR_HEIGHT as u64,
+ );
+ let scroll_bar_y = (height - scroll_bar_height as f64)
+ * state.offset as f64
+ / total_lines as f64;
+
+ // if the mouse isn't inside the scroll bar, return
+ // scrollbar width should be accounted for
+ if x < client_width as f64 - SCROLL_BAR_WIDTH as f64 {
+ state.scroll_bar_y_offset = -1.0;
+ return;
+ }
+ if y < scroll_bar_y || y > scroll_bar_y + scroll_bar_height as f64 {
+ state.scroll_bar_y_offset = (scroll_bar_height as f64) / 2.0;
+ state.offset = PyroConsole::calculate_offset(
+ y - scroll_bar_height as f64 / 2.0,
+ client_height as f64,
+ (state.line_manager.len_linebreaks() - LINES_VISIBLE
+ + GAP_LINES) as u64,
+ );
+ } else {
+ state.scroll_bar_y_offset = y - scroll_bar_y;
+ }
+ }
+
+ pub fn mouse_move(&mut self, y: f64, client_height: u32) {
+ let mut state = self.state.borrow_mut();
+ if state.scroll_bar_y_offset < 0.0 {
+ return;
+ }
+ let y = y - state.scroll_bar_y_offset;
+
+ let offset = PyroConsole::calculate_offset(
+ y as f64,
+ client_height as f64,
+ (state.line_manager.len_linebreaks() - LINES_VISIBLE + GAP_LINES)
+ as u64,
+ );
+ state.offset = offset;
+ }
+
+ pub fn mouse_up(&mut self) {
+ let mut state = self.state.borrow_mut();
+ state.scroll_bar_y_offset = -1.0;
+ }
+
+ pub fn wheel(&mut self, delta_y: f64) {
+ let mut state = self.state.borrow_mut();
+ if delta_y > 0.0 {
+ if state.offset == u64::MAX
+ || state.offset as usize + LINES_VISIBLE
+ >= state.line_manager.len_linebreaks() + GAP_LINES
+ {
+ return;
+ }
+ state.offset += 1;
+ } else {
+ if state.offset == 0 {
+ return;
+ }
+ state.offset -= 1;
+ }
+ }
+
+ pub fn search(&mut self, query: &str) {
+ let mut state = self.state.borrow_mut();
+ if query.is_empty() {
+ state.line_manager.clear_search();
+ } else {
+ state.line_manager.search(query);
+ state.offset = 0;
+ }
+ }
+}
diff --git a/packages/tartaros/src/line.rs b/packages/tartaros/src/line.rs
new file mode 100644
index 000000000..01e9e5935
--- /dev/null
+++ b/packages/tartaros/src/line.rs
@@ -0,0 +1,255 @@
+use fuzzy_search::{automata::LevenshteinAutomata, symspell::SymSpell};
+use log::info;
+
+use crate::{
+ ansi::{AnsiCommand, AnsiParser},
+ measure::TextMeasureCache,
+ FONT_SIZE,
+};
+use std::{cell::RefCell, rc::Rc};
+
+#[derive(Debug, Clone)]
+pub struct ConsoleLine {
+ pub commands: Vec,
+ pub unbroken_line_index: usize,
+}
+
+impl ConsoleLine {
+ pub fn new(commands: Vec, unbroken_line_index: usize) -> Self {
+ Self {
+ commands,
+ unbroken_line_index,
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct LineManager {
+ unbroken_lines: Vec>,
+ measure_cache: Rc>,
+ canvas_width: f64,
+ cached_lines: Option>,
+ search_results: Option>,
+}
+
+impl LineManager {
+ pub fn new(
+ measure_cache: &Rc>,
+ canvas_width: f64,
+ ) -> Self {
+ Self {
+ unbroken_lines: Vec::new(),
+ cached_lines: None,
+ measure_cache: measure_cache.clone(),
+ canvas_width,
+ search_results: None,
+ }
+ }
+
+ pub fn add_line(&mut self, line: &str) -> usize {
+ if self.unbroken_lines.len() > 1000 {
+ self.remove_at(0);
+ }
+
+ let parser = AnsiParser::new(line.to_owned());
+ let result = parser.parse();
+ self.unbroken_lines.push(result.clone());
+ if let Some(cache) = &mut self.cached_lines {
+ let index = self.unbroken_lines.len() - 1;
+ let new_broken_lines = Self::calculate_line_breaks(
+ result.clone(),
+ &self.measure_cache,
+ self.canvas_width,
+ );
+ for broken_line in new_broken_lines {
+ cache.push(ConsoleLine::new(broken_line, index));
+ }
+ } else {
+ self.invalidate_cache();
+ }
+
+ Self::calculate_line_breaks(
+ result.clone(),
+ &self.measure_cache,
+ self.canvas_width,
+ )
+ .len()
+ }
+
+ pub fn invalidate_cache(&mut self) {
+ self.cached_lines = None;
+ }
+
+ pub fn calculate_line_breaks(
+ line: Vec,
+ measure_cache: &Rc>,
+ canvas_width: f64,
+ ) -> Vec> {
+ let mut lines: Vec> = Vec::new();
+ let mut x = 0.0;
+ let mut current_line = Vec::new();
+ let mut last_was_space = false;
+ for command in line.iter() {
+ match command {
+ AnsiCommand::RenderText(text) => {
+ let split = text.split(' ').collect::>();
+ for word in split.iter() {
+ let word = if word.is_empty() && !last_was_space {
+ last_was_space = true;
+ " "
+ } else {
+ last_was_space = false;
+ *word
+ };
+ let word = (*word).to_owned();
+ let word = word.as_str();
+ let width =
+ measure_cache.borrow_mut().measure(FONT_SIZE, word);
+ if x + width > canvas_width {
+ lines.push(current_line.clone());
+ let mut styles = Vec::new();
+ for style in current_line.iter().rev() {
+ match style {
+ AnsiCommand::RenderText(_) => continue,
+ _ => styles.push(style.clone()),
+ }
+ }
+ current_line.clear();
+ for style in styles.iter().rev() {
+ current_line.push(style.clone());
+ }
+ x = 0.0;
+ }
+ current_line
+ .push(AnsiCommand::RenderText(word.to_owned()));
+ x += width;
+ }
+ }
+ _ => {
+ current_line.push(command.clone());
+ }
+ }
+ }
+
+ if current_line.len() > 0 {
+ lines.push(current_line);
+ }
+
+ lines
+ }
+
+ pub fn len_no_linebreaks(&self) -> usize {
+ self.unbroken_lines.len()
+ }
+
+ pub fn len_linebreaks(&mut self) -> usize {
+ if self.cached_lines.is_none() {
+ self.recalculate_cache();
+ }
+ if self.search_results.is_some() {
+ return self.search_results.as_ref().unwrap().len();
+ }
+ self.cached_lines.as_ref().unwrap().len()
+ }
+
+ pub fn get_lines(
+ &mut self,
+ range: std::ops::Range,
+ ) -> Vec {
+ if self.cached_lines.is_none() {
+ self.recalculate_cache();
+ }
+ if self.search_results.is_some() {
+ let search_results = self.search_results.as_ref().unwrap();
+ return search_results
+ .iter()
+ .filter_map(|index| {
+ if range.contains(index) {
+ Some(
+ self.cached_lines.as_ref().unwrap()[*index].clone(),
+ )
+ } else {
+ None
+ }
+ })
+ .collect();
+ }
+ self.cached_lines.as_ref().unwrap()[range].to_vec()
+ }
+
+ fn recalculate_cache(&mut self) {
+ self.cached_lines = Some(
+ self.unbroken_lines
+ .iter()
+ .enumerate()
+ .flat_map(|(index, line)| {
+ Self::calculate_line_breaks(
+ line.clone(),
+ &self.measure_cache,
+ self.canvas_width,
+ )
+ .into_iter()
+ .map(move |broken_line| {
+ ConsoleLine::new(broken_line, index)
+ })
+ })
+ .collect(),
+ );
+ }
+
+ pub fn clear(&mut self) {
+ self.unbroken_lines.clear();
+ self.cached_lines = None;
+ }
+
+ pub fn on_resize(&mut self, width: f64) {
+ self.canvas_width = width;
+ self.invalidate_cache();
+ }
+
+ pub fn remove_at(&mut self, raw_index: usize) {}
+
+ pub fn search(&mut self, query: &str) {
+ let len = self.len_linebreaks();
+ let all_lines = self.get_lines(0..len);
+
+ let choices = all_lines
+ .iter()
+ .enumerate()
+ .map(|(i, line)| {
+ (
+ i,
+ line.commands
+ .iter()
+ .filter_map(|command| match command {
+ AnsiCommand::RenderText(text) => Some(text),
+ _ => None,
+ })
+ .map(String::as_str)
+ .collect::>()
+ .join(""),
+ )
+ })
+ .filter(|(_, line)| {
+ line.to_lowercase().contains(&query.to_lowercase())
+ })
+ .map(|(i, _)| i)
+ .collect::>();
+
+ self.search_results = Some(choices);
+
+ // scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
+ // let top_indices = scores
+ // .iter()
+ // .filter(|(_, score)| *score > 0.0)
+ // .take(20)
+ // .map(|(index, _)| *index)
+ // .collect::>();
+
+ // self.search_results = Some(top_indices.clone());
+ }
+
+ pub fn clear_search(&mut self) {
+ self.search_results = None;
+ }
+}
diff --git a/packages/tartaros/src/measure.rs b/packages/tartaros/src/measure.rs
new file mode 100644
index 000000000..3690ecde1
--- /dev/null
+++ b/packages/tartaros/src/measure.rs
@@ -0,0 +1,41 @@
+use std::collections::HashMap;
+
+use web_sys::OffscreenCanvasRenderingContext2d;
+
+/**
+ A cache for measuring text width.
+
+ This brings huge performance improvements because of the overhead when calling JS functions.
+*/
+#[derive(Debug, Clone)]
+pub struct TextMeasureCache {
+ // font_size -> width
+ cache: HashMap,
+ ctx: OffscreenCanvasRenderingContext2d,
+}
+
+impl TextMeasureCache {
+ pub fn new(ctx: OffscreenCanvasRenderingContext2d) -> Self {
+ TextMeasureCache {
+ cache: HashMap::new(),
+ ctx,
+ }
+ }
+
+ pub fn measure(&mut self, font_size: usize, text: &str) -> f64 {
+ let font_size = font_size as u32;
+ if let Some(width) = self.cache.get(&font_size) {
+ return *width * text.len() as f64;
+ }
+
+ let char_width = self.measure_char(font_size, "a");
+ let width = char_width * text.len() as f64;
+ self.cache.insert(font_size, char_width);
+ width
+ }
+
+ fn measure_char(&self, font_size: u32, text: &str) -> f64 {
+ self.ctx.set_font(&format!("{}px monospace", font_size));
+ self.ctx.measure_text(text).unwrap().width()
+ }
+}
diff --git a/packages/tartaros/src/utils.rs b/packages/tartaros/src/utils.rs
new file mode 100644
index 000000000..e99c2b080
--- /dev/null
+++ b/packages/tartaros/src/utils.rs
@@ -0,0 +1,38 @@
+use wasm_bindgen::{
+ prelude::{wasm_bindgen, Closure},
+ JsCast,
+};
+use web_sys::{js_sys::global, DedicatedWorkerGlobalScope};
+
+#[wasm_bindgen]
+extern "C" {
+ #[wasm_bindgen(js_namespace = console)]
+ pub fn log(s: &str);
+}
+
+pub fn set_panic_hook() {
+ // When the `console_error_panic_hook` feature is enabled, we can call the
+ // `set_panic_hook` function at least once during initialization, and then
+ // we will get better error messages if our code ever panics.
+ //
+ // For more details see
+ // https://github.com/rustwasm/console_error_panic_hook#readme
+ #[cfg(feature = "console_error_panic_hook")]
+ console_error_panic_hook::set_once();
+}
+
+pub fn worker() -> DedicatedWorkerGlobalScope {
+ global().dyn_into().expect("global scope is a worker")
+}
+
+pub fn request_animation_frame(f: &Closure) -> i32 {
+ worker()
+ .request_animation_frame(f.as_ref().unchecked_ref())
+ .expect("should register `requestAnimationFrame` OK")
+}
+
+pub fn cancel_animation_frame(h: i32) {
+ worker()
+ .cancel_animation_frame(h)
+ .expect("should clear `requestAnimationFrame` OK")
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 708fb7d99..0ce980f8d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -50,6 +50,9 @@ importers:
'@modrinth/utils':
specifier: workspace:*
version: link:../../packages/utils
+ '@pyro/tartaros':
+ specifier: workspace:*
+ version: link:../../packages/tartaros
'@sentry/vue':
specifier: ^8.27.0
version: 8.27.0(vue@3.5.13(typescript@5.5.4))
@@ -209,6 +212,9 @@ importers:
'@pinia/nuxt':
specifier: ^0.5.1
version: 0.5.1(magicast@0.3.5)(rollup@4.28.1)(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4))
+ '@pyro/tartaros':
+ specifier: workspace:*
+ version: link:../../packages/tartaros
'@vintl/vintl':
specifier: ^4.4.1
version: 4.4.1(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4))
@@ -324,6 +330,12 @@ importers:
typescript:
specifier: ^5.4.5
version: 5.5.4
+ vite:
+ specifier: ^5.4.6
+ version: 5.4.11(@types/node@20.14.11)(sass@1.77.6)(terser@5.31.6)
+ vite-plugin-wasm:
+ specifier: 3.3.0
+ version: 3.3.0(vite@5.4.11(@types/node@20.14.11)(sass@1.77.6)(terser@5.31.6))
vite-svg-loader:
specifier: ^5.1.0
version: 5.1.0(vue@3.5.13(typescript@5.5.4))
@@ -376,6 +388,12 @@ importers:
specifier: ^5.5.3
version: 5.5.4
+ packages/tartaros:
+ devDependencies:
+ wasm-pack:
+ specifier: '*'
+ version: 0.13.1
+
packages/tsconfig:
devDependencies:
'@vue/tsconfig':
@@ -2917,6 +2935,9 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
+ axios@0.26.1:
+ resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
+
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
@@ -2961,6 +2982,10 @@ packages:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
+ binary-install@1.1.0:
+ resolution: {integrity: sha512-rkwNGW+3aQVSZoD0/o3mfPN6Yxh3Id0R/xzTVBVVpGNlVz8EGwusksxRlbk/A5iKTZt9zkMn3qIqmAt3vpfbzg==}
+ engines: {node: '>=10'}
+
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
@@ -4080,6 +4105,15 @@ packages:
'@nuxt/kit':
optional: true
+ follow-redirects@1.15.9:
+ resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+
for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@@ -7001,6 +7035,11 @@ packages:
peerDependencies:
vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0
+ vite-plugin-wasm@3.3.0:
+ resolution: {integrity: sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==}
+ peerDependencies:
+ vite: ^2 || ^3 || ^4 || ^5
+
vite-svg-loader@5.1.0:
resolution: {integrity: sha512-M/wqwtOEjgb956/+m5ZrYT/Iq6Hax0OakWbokj8+9PXOnB7b/4AxESHieEtnNEy7ZpjsjYW1/5nK8fATQMmRxw==}
peerDependencies:
@@ -7281,6 +7320,10 @@ packages:
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+ wasm-pack@0.13.1:
+ resolution: {integrity: sha512-P9exD4YkjpDbw68xUhF3MDm/CC/3eTmmthyG5bHJ56kalxOTewOunxTke4SyF8MTXV6jUtNjXggPgrGmMtczGg==}
+ hasBin: true
+
watchpack@2.4.2:
resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==}
engines: {node: '>=10.13.0'}
@@ -10571,6 +10614,12 @@ snapshots:
dependencies:
possible-typed-array-names: 1.0.0
+ axios@0.26.1:
+ dependencies:
+ follow-redirects: 1.15.9
+ transitivePeerDependencies:
+ - debug
+
axobject-query@4.1.0: {}
b4a@1.6.6: {}
@@ -10617,6 +10666,14 @@ snapshots:
binary-extensions@2.3.0: {}
+ binary-install@1.1.0:
+ dependencies:
+ axios: 0.26.1
+ rimraf: 3.0.2
+ tar: 6.2.1
+ transitivePeerDependencies:
+ - debug
+
bindings@1.5.0:
dependencies:
file-uri-to-path: 1.0.0
@@ -12002,6 +12059,8 @@ snapshots:
optionalDependencies:
'@nuxt/kit': 3.14.1592(magicast@0.3.5)
+ follow-redirects@1.15.9: {}
+
for-each@0.3.3:
dependencies:
is-callable: 1.2.7
@@ -15708,6 +15767,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ vite-plugin-wasm@3.3.0(vite@5.4.11(@types/node@20.14.11)(sass@1.77.6)(terser@5.31.6)):
+ dependencies:
+ vite: 5.4.11(@types/node@20.14.11)(sass@1.77.6)(terser@5.31.6)
+
vite-svg-loader@5.1.0(vue@3.5.13(typescript@5.5.4)):
dependencies:
svgo: 3.3.2
@@ -15963,6 +16026,12 @@ snapshots:
w3c-keyname@2.2.8: {}
+ wasm-pack@0.13.1:
+ dependencies:
+ binary-install: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
watchpack@2.4.2:
dependencies:
glob-to-regexp: 0.4.1
diff --git a/turbo.json b/turbo.json
index 71cd9ce3b..f139ca633 100644
--- a/turbo.json
+++ b/turbo.json
@@ -27,6 +27,7 @@
"env": ["SQLX_OFFLINE"]
},
"dev": {
+ "dependsOn": ["@pyro/tartaros#build"],
"cache": false,
"persistent": true,
"inputs": ["$TURBO_DEFAULT$", ".env*"],
@@ -37,6 +38,10 @@
},
"fix": {
"cache": false
+ },
+ "@pyro/tartaros#build": {
+ "inputs": ["packages/tartaros/**"],
+ "outputs": ["packages/tartaros/pkg/**"]
}
}
}