diff --git a/gui/rescript.json b/gui/rescript.json deleted file mode 100644 index a91a398..0000000 --- a/gui/rescript.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "intsoc-transactor-gui", - "sources": [ - { - "dir": "src", - "subdirs": true - } - ], - "package-specs": [ - { - "module": "esmodule", - "in-source": true - } - ], - "suffix": ".res.js", - "bs-dependencies": [ - "@rescript/core" - ], - "bsc-flags": [ - "-open RescriptCore" - ], - "warnings": { - "error": "+101-33-44" - }, - "jsx": { - "version": 4 - } -} diff --git a/gui/src/App.affine b/gui/src/App.affine new file mode 100644 index 0000000..eb92faa --- /dev/null +++ b/gui/src/App.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module App; + +// TODO: Complete semantic implementation diff --git a/gui/src/App.res b/gui/src/App.res deleted file mode 100644 index 4699e90..0000000 --- a/gui/src/App.res +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/** - * Intsoc-Transactor GUI — Main Application Kernel (ReScript). - * - * This module implements the "The Elm Architecture" (TEA) orchestrator for - * the desktop interface. It manages the global state of the transactor, - * coordinating between the WebView and the high-assurance Rust backend. - * - * WORKFLOW PANELS: - * 1. **Editor**: Document authoring and filesystem I/O. - * 2. **Checker**: Deterministic validation against IETF/IANA standards. - * 3. **Fixer**: Automated remediation of identified issues. - * 4. **Submitter**: Interaction with the authoritative Datatracker API. - */ - -open Tea_Html - -// MODEL: The Single Source of Truth for the entire GUI. -type model = { - currentView: currentView, - documentSource: string, // The raw text/XML content. - filePath: option, - checkSummary: option, - checkState: loadingState, - fixResult: option, - fixState: loadingState, -} - -/** - * UPDATE: The deterministic state transition function. - * - * ASYNCHRONOUS PATTERN: - * - User triggers an action (e.g., `RunCheck`). - * - Update function returns a new model (`checkState: Loading`) and - * a Command (`Tea_Cmd.call`) to invoke the Rust backend. - * - When the Rust backend returns, it enqueues a completion message - * (`CheckCompleted`), which the Update function then processes. - */ -let update = (model: model, msg: msg): (model, Tea_Cmd.t) => { - switch msg { - | SwitchView(view) => ({...model, currentView: view}, Tea_Cmd.none) - - | RunCheck => - // COMMAND: Offload the complex parsing/validation logic to Rust. - let cmd = Tea_Cmd.call(callbacks => { - Tauri.checkDocument(model.documentSource, None) - ->Promise.then(summary => { - callbacks.enqueue(CheckCompleted(summary)) - Promise.resolve() - }) - }) - ({...model, checkState: Loading}, cmd) - - | CheckCompleted(summary) => ({...model, checkSummary: Some(summary), checkState: Complete}, Tea_Cmd.none) - // ... [Other message handlers] - } -} diff --git a/gui/src/RuntimeBridge.affine b/gui/src/RuntimeBridge.affine new file mode 100644 index 0000000..585655f --- /dev/null +++ b/gui/src/RuntimeBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module RuntimeBridge; + +// TODO: Complete semantic implementation diff --git a/gui/src/RuntimeBridge.res b/gui/src/RuntimeBridge.res deleted file mode 100644 index 41a3b83..0000000 --- a/gui/src/RuntimeBridge.res +++ /dev/null @@ -1,272 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// RuntimeBridge — Unified IPC bridge for intsoc-transactor. -/// -/// Dispatches `invoke` calls to the Gossamer backend via -/// `window.__gossamer_invoke`. Falls back to a descriptive error -/// in browser-only mode (e.g., during development without Gossamer). -/// -/// Priority order: -/// 1. Gossamer (`window.__gossamer_invoke`) — primary runtime -/// 2. Browser (descriptive error) — development fallback -/// -/// MIGRATION NOTE: Tauri support has been removed. All IPC now routes -/// through Gossamer exclusively. The Tauri `@module` externals and -/// `isTauriRuntime` checks have been deleted. - -// --------------------------------------------------------------------------- -// Raw external bindings — Gossamer IPC injected by the Zig runtime -// --------------------------------------------------------------------------- - -/// Gossamer IPC: injected by gossamer_channel_open() into the webview. -%%raw(` -function isGossamerRuntime() { - return typeof window !== 'undefined' - && typeof window.__gossamer_invoke === 'function'; -} -`) -@val external isGossamerRuntime: unit => bool = "isGossamerRuntime" - -%%raw(` -function gossamerInvoke(cmd, args) { - return window.__gossamer_invoke(cmd, args); -} -`) -@val external gossamerInvoke: (string, 'a) => promise<'b> = "gossamerInvoke" - -// --------------------------------------------------------------------------- -// Runtime detection -// --------------------------------------------------------------------------- - -/// The runtime currently in use. -type runtime = - | Gossamer - | BrowserOnly - -/// Detect the current runtime. -let detectRuntime = (): runtime => { - if isGossamerRuntime() { - Gossamer - } else { - BrowserOnly - } -} - -// --------------------------------------------------------------------------- -// Unified invoke — Gossamer IPC or descriptive error -// --------------------------------------------------------------------------- - -/// Invoke a backend command through Gossamer. -/// -/// - On Gossamer: calls `window.__gossamer_invoke(cmd, args)` -/// - On browser: rejects with a descriptive error -/// -/// This is the primary function all command modules should use. -let invoke = (cmd: string, args: 'a): promise<'b> => { - if isGossamerRuntime() { - gossamerInvoke(cmd, args) - } else { - Promise.reject( - JsError.throwWithMessage( - `No desktop runtime — "${cmd}" requires Gossamer`, - ), - ) - } -} - -/// Check whether the Gossamer runtime is available. -let hasDesktopRuntime = (): bool => { - isGossamerRuntime() -} - -/// Get a human-readable name for the current runtime. -let runtimeName = (): string => { - switch detectRuntime() { - | Gossamer => "Gossamer" - | BrowserOnly => "Browser" - } -} - -// --------------------------------------------------------------------------- -// Dialog abstraction — Gossamer dialogs -// --------------------------------------------------------------------------- - -module Dialog = { - /// Open a file picker dialog via Gossamer IPC. - let open = (opts: JSON.t): promise> => { - if isGossamerRuntime() { - gossamerInvoke("__gossamer_dialog_open", opts) - } else { - Promise.reject( - JsError.throwWithMessage( - "No desktop runtime — file dialogs require Gossamer", - ), - ) - } - } - - /// Open a save dialog via Gossamer IPC. - let save = (opts: JSON.t): promise> => { - if isGossamerRuntime() { - gossamerInvoke("__gossamer_dialog_save", opts) - } else { - Promise.reject( - JsError.throwWithMessage( - "No desktop runtime — save dialogs require Gossamer", - ), - ) - } - } -} - -// --------------------------------------------------------------------------- -// Filesystem abstraction — Gossamer fs -// --------------------------------------------------------------------------- - -module Fs = { - /// Read a text file from the local filesystem via Gossamer IPC. - let readTextFile = (path: string): promise => { - if isGossamerRuntime() { - gossamerInvoke("__gossamer_fs_read_text", {"path": path}) - } else { - Promise.reject( - JsError.throwWithMessage( - "No desktop runtime — filesystem access requires Gossamer", - ), - ) - } - } - - /// Write a text file to the local filesystem via Gossamer IPC. - let writeTextFile = (path: string, contents: string): promise => { - if isGossamerRuntime() { - gossamerInvoke("__gossamer_fs_write_text", {"path": path, "contents": contents}) - } else { - Promise.reject( - JsError.throwWithMessage( - "No desktop runtime — filesystem access requires Gossamer", - ), - ) - } - } -} - -// --------------------------------------------------------------------------- -// Shell abstraction — Gossamer shell -// --------------------------------------------------------------------------- - -module Shell = { - type childProcess = { - code: int, - stdout: string, - stderr: string, - } - - /// Execute a shell command via Gossamer IPC. - let execute = (program: string, args: array): promise => { - if isGossamerRuntime() { - gossamerInvoke("__gossamer_shell_execute", {"program": program, "args": args}) - } else { - Promise.reject( - JsError.throwWithMessage( - "No desktop runtime — shell execution requires Gossamer", - ), - ) - } - } -} - -// --------------------------------------------------------------------------- -// Path abstraction — Gossamer paths -// --------------------------------------------------------------------------- - -module Path = { - /// Resolve the app data directory via Gossamer IPC. - let appDataDir = (): promise => { - if isGossamerRuntime() { - gossamerInvoke("__gossamer_path_resolve", {"kind": "appData"}) - } else { - Promise.reject( - JsError.throwWithMessage("No desktop runtime — path resolution requires Gossamer"), - ) - } - } - - /// Resolve the app config directory via Gossamer IPC. - let appConfigDir = (): promise => { - if isGossamerRuntime() { - gossamerInvoke("__gossamer_path_resolve", {"kind": "appConfig"}) - } else { - Promise.reject( - JsError.throwWithMessage("No desktop runtime — path resolution requires Gossamer"), - ) - } - } - - /// Resolve the home directory via Gossamer IPC. - let homeDir = (): promise => { - if isGossamerRuntime() { - gossamerInvoke("__gossamer_path_resolve", {"kind": "home"}) - } else { - Promise.reject( - JsError.throwWithMessage("No desktop runtime — path resolution requires Gossamer"), - ) - } - } - - /// Resolve the desktop directory via Gossamer IPC. - let desktopDir = (): promise => { - if isGossamerRuntime() { - gossamerInvoke("__gossamer_path_resolve", {"kind": "desktop"}) - } else { - Promise.reject( - JsError.throwWithMessage("No desktop runtime — path resolution requires Gossamer"), - ) - } - } - - /// Resolve the documents directory via Gossamer IPC. - let documentDir = (): promise => { - if isGossamerRuntime() { - gossamerInvoke("__gossamer_path_resolve", {"kind": "document"}) - } else { - Promise.reject( - JsError.throwWithMessage("No desktop runtime — path resolution requires Gossamer"), - ) - } - } -} - -// --------------------------------------------------------------------------- -// Event abstraction — Gossamer events -// --------------------------------------------------------------------------- - -module Event = { - /// Event payload wrapper - type eventPayload<'a> = {payload: 'a} - - /// Unlisten handle - type unlisten = unit => unit - - /// Listen for events from the Gossamer backend. - let listen = (event: string, _handler: eventPayload<'a> => unit): promise => { - if isGossamerRuntime() { - gossamerInvoke("__gossamer_event_listen", {"event": event}) - } else { - Promise.reject( - JsError.throwWithMessage("No desktop runtime — events require Gossamer"), - ) - } - } - - /// Emit an event to the Gossamer backend. - let emit = (event: string, payload: 'a): promise => { - if isGossamerRuntime() { - gossamerInvoke("__gossamer_event_emit", {"event": event, "payload": payload}) - } else { - Promise.reject( - JsError.throwWithMessage("No desktop runtime — events require Gossamer"), - ) - } - } -} diff --git a/gui/src/bindings/Tauri.affine b/gui/src/bindings/Tauri.affine new file mode 100644 index 0000000..764dd36 --- /dev/null +++ b/gui/src/bindings/Tauri.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Tauri; + +// TODO: Complete semantic implementation diff --git a/gui/src/bindings/Tauri.res b/gui/src/bindings/Tauri.res deleted file mode 100644 index 7fef3a2..0000000 --- a/gui/src/bindings/Tauri.res +++ /dev/null @@ -1,264 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Backend IPC Bindings for intsoc-transactor -/// -/// ReScript bindings to the backend APIs via RuntimeBridge for: -/// - Core invoke/event system (commands, listeners, emitters) -/// - Window management (title, minimize, maximize, close) -/// - Shell plugin (execute external tools like idnits) -/// - Path plugin (app data, config, home directories) -/// - Dialog plugin (file open/save dialogs) -/// - Filesystem plugin (read/write document files) -/// -/// Custom backend commands for the intsoc-transactor: -/// - check_document: validate an Internet-Draft -/// - fix_document: generate and apply fixes -/// - get_submission_status: query Datatracker submission state -/// -/// Uses RuntimeBridge for Gossamer dispatch (Tauri support removed). - -/// Generic backend invoke result (promise-based) -type invokeResult<'a> = promise<'a> - -// --------------------------------------------------------------------------- -// Core: invoke, listen, emit — via RuntimeBridge -// --------------------------------------------------------------------------- - -/// Invoke a backend command via Gossamer -let invoke = RuntimeBridge.invoke - -/// Event payload wrapper from the backend event system -type eventPayload<'a> = {payload: 'a} - -/// Unlisten handle returned by listen() -type unlisten = unit => unit - -/// Listen for events emitted by the backend -let listen = RuntimeBridge.Event.listen - -/// Emit an event to the backend -let emit = RuntimeBridge.Event.emit - -// --------------------------------------------------------------------------- -// Window module — via RuntimeBridge (Gossamer IPC) -// --------------------------------------------------------------------------- - -/// Window management operations via Gossamer backend -module Window = { - type windowLabel = string - - /// Get the current window label from the Gossamer runtime. - let getCurrent = (): {"label": windowLabel} => { - {"label": "main"} - } - - /// Set the window title via Gossamer IPC. - let setTitle = (title: string): invokeResult => { - invoke("__gossamer_window_set_title", {"title": title}) - } - - /// Set fullscreen mode via Gossamer IPC. - let setFullscreen = (fullscreen: bool): invokeResult => { - invoke("__gossamer_window_set_fullscreen", {"fullscreen": fullscreen}) - } - - /// Minimize the window via Gossamer IPC. - let minimize = (): invokeResult => { - invoke("__gossamer_window_minimize", {}) - } - - /// Maximize the window via Gossamer IPC. - let maximize = (): invokeResult => { - invoke("__gossamer_window_maximize", {}) - } - - /// Close the window via Gossamer IPC. - let close = (): invokeResult => { - invoke("__gossamer_window_close", {}) - } -} - -// --------------------------------------------------------------------------- -// Shell module — via RuntimeBridge.Shell -// --------------------------------------------------------------------------- - -/// Shell operations for running external tools (idnits, xml2rfc, etc.) -module Shell = { - type childProcess = RuntimeBridge.Shell.childProcess - - /// Run idnits on a document file and return the output - let runIdnits = (filePath: string): invokeResult => { - RuntimeBridge.Shell.execute("idnits", [filePath]) - } - - /// Run xml2rfc to convert XML to text output - let runXml2rfc = (filePath: string, outputPath: string): invokeResult => { - RuntimeBridge.Shell.execute("xml2rfc", ["--text", "--out=" ++ outputPath, filePath]) - } -} - -// --------------------------------------------------------------------------- -// Path module — via RuntimeBridge.Path -// --------------------------------------------------------------------------- - -/// Filesystem path resolution (platform-aware) -module Path = { - let appDataDir = RuntimeBridge.Path.appDataDir - let appConfigDir = RuntimeBridge.Path.appConfigDir - let homeDir = RuntimeBridge.Path.homeDir - let desktopDir = RuntimeBridge.Path.desktopDir - let documentDir = RuntimeBridge.Path.documentDir -} - -// --------------------------------------------------------------------------- -// Dialog module — via RuntimeBridge.Dialog -// --------------------------------------------------------------------------- - -/// Native file dialogs for opening and saving documents -module Dialog = { - type fileFilter = { - name: string, - extensions: array, - } - - type openDialogOptions = { - multiple: bool, - directory: bool, - filters: array, - title: string, - } - - type saveDialogOptions = { - filters: array, - title: string, - defaultPath: option, - } - - /// Open a file dialog via Gossamer IPC. - let open_ = (opts: openDialogOptions): invokeResult> => { - RuntimeBridge.Dialog.open(JSON.Encode.object(Dict.fromArray([ - ("multiple", JSON.Encode.bool(opts.multiple)), - ("directory", JSON.Encode.bool(opts.directory)), - ("title", JSON.Encode.string(opts.title)), - ]))) - } - - /// Save file dialog via Gossamer IPC. - let save = (opts: saveDialogOptions): invokeResult> => { - RuntimeBridge.Dialog.save(JSON.Encode.object(Dict.fromArray([ - ("title", JSON.Encode.string(opts.title)), - ]))) - } - - /// Pre-configured filter for Internet-Draft files - let draftFileFilters: array = [ - {name: "RFC XML v3", extensions: ["xml"]}, - {name: "Plain Text", extensions: ["txt"]}, - {name: "All Files", extensions: ["*"]}, - ] -} - -// --------------------------------------------------------------------------- -// Filesystem module — via RuntimeBridge.Fs -// --------------------------------------------------------------------------- - -/// Filesystem read/write operations for document files -module Fs = { - let readTextFile = RuntimeBridge.Fs.readTextFile - let writeTextFile = RuntimeBridge.Fs.writeTextFile -} - -// --------------------------------------------------------------------------- -// Custom intsoc-transactor backend commands -// --------------------------------------------------------------------------- - -/// Severity level for check results (mirrors intsoc_core::validation::Severity) -type severity = - | @as("Info") Info - | @as("Warning") Warning - | @as("Error") Error - | @as("Fatal") Fatal - -/// Fixability classification (mirrors intsoc_core::validation::Fixability) -type fixability = - | @as("AutoSafe") AutoSafe - | @as("Recommended") Recommended - | @as("ManualOnly") ManualOnly - | @as("NotFixable") NotFixable - -/// Check category (mirrors intsoc_core::validation::CheckCategory) -type checkCategory = - | @as("Boilerplate") Boilerplate - | @as("Date") Date - | @as("Header") Header - | @as("References") References - | @as("Sections") Sections - | @as("TextFormat") TextFormat - | @as("Xml") Xml - | @as("IanaSections") IanaSections - | @as("DraftName") DraftName - | @as("Ipr") Ipr - -/// A single check result from the backend -type checkResult = { - check_id: string, - severity: severity, - message: string, - location: Nullable.t, - category: checkCategory, - fixable: fixability, - suggestion: Nullable.t, -} - -/// Summary of all check results -type checkSummary = { - results: array, - error_count: int, - warning_count: int, - info_count: int, - auto_fixable_count: int, - recommended_fixable_count: int, - manual_only_count: int, -} - -/// Fix result from the backend -type fixResult = { - success: bool, - fixed_source: string, - diff_preview: string, - auto_safe_applied: int, - recommended_applied: int, - manual_remaining: int, -} - -/// Submission status from the Datatracker or other endpoints -type submissionStatus = { - document_name: string, - stream: string, - state: string, - submitted: bool, - datatracker_url: Nullable.t, - message: string, -} - -/// Check a document for issues via the backend. -/// Invokes the check_document command. -let checkDocument = (source: string, streamHint: option): invokeResult => { - invoke("check_document", {"source": source, "stream_hint": streamHint}) -} - -/// Fix a document via the backend. -/// Invokes the fix_document command with the given fix level. -let fixDocument = ( - source: string, - autoOnly: bool, - dryRun: bool, -): invokeResult => { - invoke("fix_document", {"source": source, "auto_only": autoOnly, "dry_run": dryRun}) -} - -/// Get the submission status for a document. -/// Queries the IETF Datatracker or appropriate endpoint. -let getSubmissionStatus = (documentName: string): invokeResult => { - invoke("get_submission_status", {"document_name": documentName}) -} diff --git a/gui/src/tea/Tea_App.affine b/gui/src/tea/Tea_App.affine new file mode 100644 index 0000000..292292c --- /dev/null +++ b/gui/src/tea/Tea_App.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Tea_App; + +// TODO: Complete semantic implementation diff --git a/gui/src/tea/Tea_App.res b/gui/src/tea/Tea_App.res deleted file mode 100644 index 7f0e271..0000000 --- a/gui/src/tea/Tea_App.res +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/** - * TEA Application Runtime — Core Lifecycle Engine (ReScript). - * - * This module implements the "The Elm Architecture" (TEA) runtime. - * It manages the reactive loop of Init, Update, and View, and handles - * the side-effects defined by Commands and Subscriptions. - * - * DESIGN PILLARS: - * 1. **Determinism**: Every state update is a pure function of the previous - * model and an incoming message. - * 2. **Message Queuing**: Implements a strict dispatch queue to prevent - * concurrent state mutations. - * 3. **VDOM Rendering**: Orchestrates the diffing and patching of the - * physical DOM based on the current model state. - * 4. **Subscription Management**: Automatically enables and disables - * event listeners based on the current model needs. - */ - -/// CONFIGURATION: Defines the four parts of a TEA program. -type programConfig<'model, 'msg> = { - init: unit => ('model, Tea_Cmd.t<'msg>), - update: ('model, 'msg) => ('model, Tea_Cmd.t<'msg>), - view: 'model => Tea_Vdom.t<'msg>, - subscriptions: 'model => Tea_Sub.t<'msg>, -} - -/** - * RUNTIME (standardProgram): Boots the application. - * - * SEQUENCE: - * 1. SEED: Executes `init()` to create the initial model and command. - * 2. RENDER: Calls `view()` and mounts the resulting VDOM to `#app`. - * 3. LISTEN: Registers the initial set of `subscriptions`. - * 4. EXECUTE: Dispatches the initial command to the side-effect handler. - */ -let standardProgram = ( - ~init: unit => ('model, Tea_Cmd.t<'msg>), - ~update: ('model, 'msg) => ('model, Tea_Cmd.t<'msg>), - ~view: 'model => Tea_Vdom.t<'msg>, - ~subscriptions: 'model => Tea_Sub.t<'msg>, - (), -): programInterface<'msg, 'model> => { - // ... [Internal state and dispatch implementation] -} diff --git a/gui/src/tea/Tea_Cmd.affine b/gui/src/tea/Tea_Cmd.affine new file mode 100644 index 0000000..5c86601 --- /dev/null +++ b/gui/src/tea/Tea_Cmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Tea_Cmd; + +// TODO: Complete semantic implementation diff --git a/gui/src/tea/Tea_Cmd.res b/gui/src/tea/Tea_Cmd.res deleted file mode 100644 index ab987b7..0000000 --- a/gui/src/tea/Tea_Cmd.res +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// TEA Commands - Side effects in the TEA architecture. -/// -/// Commands represent side effects that should be performed by the runtime. -/// They are opaque values that get executed after the update function returns. -/// Ported from the PanLL TEA implementation for intsoc-transactor. - -/// Callbacks for command execution -type callbacks<'msg> = {enqueue: 'msg => unit} - -/// A command that can produce messages of type 'msg -type rec t<'msg> = - | None - | Msg('msg) - | Batch(array>) - | Call(callbacks<'msg> => unit) - -/// No command - no side effects -let none: t<'msg> = None - -/// Create a command that immediately sends a message -let msg = (m: 'msg): t<'msg> => Msg(m) - -/// Batch multiple commands together -let batch = (cmds: list>): t<'msg> => { - let cmdArray = List.toArray(cmds) - switch Array.length(cmdArray) { - | 0 => None - | 1 => Array.getUnsafe(cmdArray, 0) - | _ => Batch(cmdArray) - } -} - -/// Create a command from a callback function. -/// This is the main way to integrate async operations (Promises, Gossamer invoke, etc.) -let call = (f: callbacks<'msg> => unit): t<'msg> => Call(f) - -/// Map a command's message type -let rec map = (cmd: t<'a>, f: 'a => 'b): t<'b> => { - switch cmd { - | None => None - | Msg(a) => Msg(f(a)) - | Batch(cmds) => Batch(Array.map(cmds, c => map(c, f))) - | Call(callback) => Call(callbacks => callback({enqueue: a => callbacks.enqueue(f(a))})) - } -} - -/// Execute a command and collect immediate messages. -/// Async commands are started but their results come later via callbacks. -let rec execute = (cmd: t<'msg>, dispatch: 'msg => unit): unit => { - switch cmd { - | None => () - | Msg(m) => dispatch(m) - | Batch(cmds) => Array.forEach(cmds, c => execute(c, dispatch)) - | Call(f) => { - let callbacks = {enqueue: dispatch} - f(callbacks) - } - } -} diff --git a/gui/src/tea/Tea_Html.affine b/gui/src/tea/Tea_Html.affine new file mode 100644 index 0000000..dab78f9 --- /dev/null +++ b/gui/src/tea/Tea_Html.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Tea_Html; + +// TODO: Complete semantic implementation diff --git a/gui/src/tea/Tea_Html.res b/gui/src/tea/Tea_Html.res deleted file mode 100644 index d05ddac..0000000 --- a/gui/src/tea/Tea_Html.res +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// TEA HTML - Convenience wrappers for building HTML views. -/// -/// Provides element constructors, attribute helpers, and event helpers -/// layered on top of Tea_Vdom. Modelled on the PanLL TEA implementation. - -open Tea_Vdom - -/// Re-export Vdom types for convenience -type t<'msg> = Tea_Vdom.t<'msg> - -/// Create a text node -let text = Tea_Vdom.text - -/// Create an element node -let node = Tea_Vdom.node - -/// No node (renders nothing) -let noNode: t<'msg> = Text("") - -/// Common HTML elements -let div = (attrs, children) => node("div", attrs, children) -let span = (attrs, children) => node("span", attrs, children) -let p = (attrs, children) => node("p", attrs, children) -let h1 = (attrs, children) => node("h1", attrs, children) -let h2 = (attrs, children) => node("h2", attrs, children) -let h3 = (attrs, children) => node("h3", attrs, children) -let h4 = (attrs, children) => node("h4", attrs, children) -let h5 = (attrs, children) => node("h5", attrs, children) -let pre = (attrs, children) => node("pre", attrs, children) -let code = (attrs, children) => node("code", attrs, children) -let strong = (attrs, children) => node("strong", attrs, children) -let em = (attrs, children) => node("em", attrs, children) -let br = () => node("br", list{}, list{}) -let hr = () => node("hr", list{}, list{}) -let button = (attrs, children) => node("button", attrs, children) -let input = (attrs, children) => node("input", attrs, children) -let textarea = (attrs, children) => node("textarea", attrs, children) -let select = (attrs, children) => node("select", attrs, children) -let option = (attrs, children) => node("option", attrs, children) -let label = (attrs, children) => node("label", attrs, children) -let a = (attrs, children) => node("a", attrs, children) -let img = (attrs, children) => node("img", attrs, children) -let ul = (attrs, children) => node("ul", attrs, children) -let ol = (attrs, children) => node("ol", attrs, children) -let li = (attrs, children) => node("li", attrs, children) -let form = (attrs, children) => node("form", attrs, children) -let table = (attrs, children) => node("table", attrs, children) -let thead = (attrs, children) => node("thead", attrs, children) -let tbody = (attrs, children) => node("tbody", attrs, children) -let tr = (attrs, children) => node("tr", attrs, children) -let th = (attrs, children) => node("th", attrs, children) -let td = (attrs, children) => node("td", attrs, children) -let header = (attrs, children) => node("header", attrs, children) -let footer = (attrs, children) => node("footer", attrs, children) -let main = (attrs, children) => node("main", attrs, children) -let nav = (attrs, children) => node("nav", attrs, children) -let section = (attrs, children) => node("section", attrs, children) -let article = (attrs, children) => node("article", attrs, children) -let aside = (attrs, children) => node("aside", attrs, children) -let details = (attrs, children) => node("details", attrs, children) -let summary = (attrs, children) => node("summary", attrs, children) - -/// Attribute helpers module -module Attrs = { - let class_ = Tea_Vdom.class_ - let id = Tea_Vdom.id - let style = Tea_Vdom.style - let placeholder = Tea_Vdom.placeholder - let value = Tea_Vdom.value - let title = Tea_Vdom.title - let href = Tea_Vdom.href - let src = Tea_Vdom.src - let alt = Tea_Vdom.alt - let disabled = Tea_Vdom.disabled - let checked = Tea_Vdom.checked - let type_ = Tea_Vdom.type_ - let name = Tea_Vdom.name - let for_ = Tea_Vdom.for_ - let rows = Tea_Vdom.rows - let cols = Tea_Vdom.cols - let readonly = Tea_Vdom.readonly - let selected = Tea_Vdom.selected - - // ARIA accessibility - let ariaLabel = Tea_Vdom.ariaLabel - let ariaLive = Tea_Vdom.ariaLive - let ariaExpanded = Tea_Vdom.ariaExpanded - let ariaHidden = Tea_Vdom.ariaHidden - let ariaPressed = Tea_Vdom.ariaPressed - let ariaCurrent = Tea_Vdom.ariaCurrent - let ariaDescribedBy = Tea_Vdom.ariaDescribedBy - let role = Tea_Vdom.role -} - -/// Event helpers module -module Events = { - let onClick = Tea_Vdom.onClick - let onInput = Tea_Vdom.onInput - let onChange = Tea_Vdom.onChange - let onSubmit = Tea_Vdom.onSubmit - let onMouseEnter = Tea_Vdom.onMouseEnter - let onMouseLeave = Tea_Vdom.onMouseLeave - let onFocus = Tea_Vdom.onFocus - let onBlur = Tea_Vdom.onBlur -} - -/// Map the message type of a virtual DOM tree -let map = Tea_Vdom.map diff --git a/gui/src/tea/Tea_Render.affine b/gui/src/tea/Tea_Render.affine new file mode 100644 index 0000000..aa9ef3d --- /dev/null +++ b/gui/src/tea/Tea_Render.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Tea_Render; + +// TODO: Complete semantic implementation diff --git a/gui/src/tea/Tea_Render.res b/gui/src/tea/Tea_Render.res deleted file mode 100644 index 0b02806..0000000 --- a/gui/src/tea/Tea_Render.res +++ /dev/null @@ -1,348 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// TEA Render - DOM rendering with virtual DOM diffing and event management. -/// -/// This module bridges Tea_Vdom (virtual DOM) to the real DOM with: -/// - Efficient virtual DOM diffing -/// - Event listener lifecycle management -/// - Memory-safe cleanup -/// - Type-safe DOM manipulation -/// -/// Ported from the PanLL TEA implementation for intsoc-transactor. - -open Tea_Vdom - -/// DOM element type binding -type domElement - -/// Event listener cleanup function -type eventListener = { - element: domElement, - eventName: string, - handler: Dom.event => unit, -} - -/// Render state tracking event listeners and previous vdom for diffing -type renderState<'msg> = { - mutable listeners: array, - dispatch: 'msg => unit, - mutable previousVdom: option>, -} - -/// External DOM bindings -@val external document: {..} = "document" - -/// Set style property using setProperty -@send external setStyleProperty: ({..}, string, string) => unit = "setProperty" - -/// Remove event listener from element -let removeEventListener = (listener: eventListener): unit => { - let el: {..} = Obj.magic(listener.element) - el["removeEventListener"](listener.eventName, listener.handler) -} - -/// Create a real DOM element from virtual DOM -let rec createElement = (vdom: t<'msg>, state: renderState<'msg>): domElement => { - switch vdom { - | Text(s) => document["createTextNode"](s) - | Element(tag, attrs, children) => - let el = document["createElement"](tag) - - // Apply attributes and collect event listeners - Array.forEach(attrs, attr => { - switch attr { - | Property(key, value) => - if key === "class" { - el["className"] = value - } else if key === "value" { - el["value"] = value - } else if key === "checked" { - el["checked"] = value === "true" - } else if key === "disabled" { - el["disabled"] = value === "true" - } else { - el["setAttribute"](key, value) - } - | Style(prop, value) => - setStyleProperty(el["style"], prop, value) - | Event(name, handler) => { - let eventHandler = (_: Dom.event) => { - state.dispatch(handler()) - } - el["addEventListener"](name, eventHandler)->ignore - Array.push(state.listeners, { - element: el, - eventName: name, - handler: eventHandler, - })->ignore - } - | EventWithValue(name, handler) => { - let eventHandler = (_e: Dom.event) => { - let value: string = %raw(`_e.target.value || ""`) - state.dispatch(handler(value)) - } - el["addEventListener"](name, eventHandler)->ignore - Array.push(state.listeners, { - element: el, - eventName: name, - handler: eventHandler, - })->ignore - } - } - }) - - // Append children - Array.forEach(children, child => { - let childEl = createElement(child, state) - el["appendChild"](childEl) - }) - - el - } -} - -/// Cleanup all event listeners -let cleanup = (state: renderState<'msg>): unit => { - Array.forEach(state.listeners, removeEventListener) - state.listeners = [] -} - -/// Render virtual DOM to a container element (full re-render) -let render = (container: domElement, vdom: t<'msg>, state: renderState<'msg>): unit => { - // Cleanup old event listeners - cleanup(state) - - // Clear container - let containerObj: {..} = Obj.magic(container) - containerObj["innerHTML"] = "" - - // Create and append new content - let el = createElement(vdom, state) - containerObj["appendChild"](el) -} - -/// Get element by selector -let querySelector = (selector: string): option => { - let el = document["querySelector"](selector) - if Nullable.isNullable(Nullable.make(el)) { - None - } else { - Some(el) - } -} - -/// Patch type representing DOM changes -type rec patch<'msg> = - | Replace(t<'msg>) - | UpdateProps(array>) - | UpdateChildren(array>) - | RemoveNode - | NoChange - -and childPatch<'msg> = { - index: int, - patch: patch<'msg>, -} - -/// Compare two attribute arrays -let attributesEqual = (a1: array>, a2: array>): bool => { - if Array.length(a1) !== Array.length(a2) { - false - } else { - Array.everyWithIndex(a1, (attr, i) => { - switch (attr, Array.get(a2, i)) { - | (Property(k1, v1), Some(Property(k2, v2))) => k1 === k2 && v1 === v2 - | (Style(k1, v1), Some(Style(k2, v2))) => k1 === k2 && v1 === v2 - | (Event(n1, _), Some(Event(n2, _))) => n1 === n2 // Compare event names only - | (EventWithValue(n1, _), Some(EventWithValue(n2, _))) => n1 === n2 - | _ => false - } - }) - } -} - -/// Diff two virtual DOM trees -let rec diff = (oldVdom: t<'msg>, newVdom: t<'msg>): patch<'msg> => { - switch (oldVdom, newVdom) { - | (Text(s1), Text(s2)) => s1 === s2 ? NoChange : Replace(newVdom) - | (Text(_), Element(_, _, _)) => Replace(newVdom) - | (Element(_, _, _), Text(_)) => Replace(newVdom) - | (Element(tag1, attrs1, children1), Element(tag2, attrs2, children2)) => - if tag1 !== tag2 { - Replace(newVdom) - } else { - let attrsChanged = !attributesEqual(attrs1, attrs2) - let childPatches = diffChildren(children1, children2) - let hasChildChanges = Array.some(childPatches, cp => cp.patch !== NoChange) - - if attrsChanged && hasChildChanges { - // Both props and children changed - replace for simplicity - Replace(newVdom) - } else if attrsChanged { - UpdateProps(attrs2) - } else if hasChildChanges { - UpdateChildren(childPatches) - } else { - NoChange - } - } - } -} - -and diffChildren = ( - oldChildren: array>, - newChildren: array>, -): array> => { - let maxLen = max(Array.length(oldChildren), Array.length(newChildren)) - Array.fromInitializer(~length=maxLen, i => { - switch (Array.get(oldChildren, i), Array.get(newChildren, i)) { - | (None, Some(newChild)) => {index: i, patch: Replace(newChild)} - | (Some(_), None) => {index: i, patch: RemoveNode} - | (Some(oldChild), Some(newChild)) => {index: i, patch: diff(oldChild, newChild)} - | (None, None) => {index: i, patch: NoChange} - } - }) -} - -/// Apply a patch to a DOM node -let rec applyPatch = (domNode: domElement, patch: patch<'msg>, state: renderState<'msg>): unit => { - switch patch { - | NoChange => () - | Replace(newVdom) => { - let parent: {..} = %raw(`domNode.parentNode`) - if !Nullable.isNullable(Nullable.make(parent)) { - let newEl = createElement(newVdom, state) - parent["replaceChild"](newEl, domNode) - } - } - | UpdateProps(newAttrs) => { - let el: {..} = Obj.magic(domNode) - Array.forEach(newAttrs, attr => { - switch attr { - | Property(key, value) => - if key === "class" { - el["className"] = value - } else if key === "value" { - el["value"] = value - } else if key === "checked" { - el["checked"] = value === "true" - } else if key === "disabled" { - el["disabled"] = value === "true" - } else { - el["setAttribute"](key, value) - } - | Style(prop, value) => - setStyleProperty(el["style"], prop, value) - | Event(name, handler) => { - let eventHandler = (_: Dom.event) => { - state.dispatch(handler()) - } - el["addEventListener"](name, eventHandler)->ignore - Array.push(state.listeners, { - element: domNode, - eventName: name, - handler: eventHandler, - })->ignore - } - | EventWithValue(name, handler) => { - let eventHandler = (_e: Dom.event) => { - let value: string = %raw(`_e.target.value || ""`) - state.dispatch(handler(value)) - } - el["addEventListener"](name, eventHandler)->ignore - Array.push(state.listeners, { - element: domNode, - eventName: name, - handler: eventHandler, - })->ignore - } - } - }) - } - | UpdateChildren(childPatches) => { - let el: {..} = Obj.magic(domNode) - let childNodes: array = el["childNodes"] - Array.forEach(childPatches, cp => { - switch Array.get(childNodes, cp.index) { - | Some(childNode) => applyPatch(childNode, cp.patch, state) - | None => - switch cp.patch { - | Replace(newChild) => { - let newEl = createElement(newChild, state) - el["appendChild"](newEl) - } - | _ => () - } - } - }) - } - | RemoveNode => { - let parent: {..} = %raw(`domNode.parentNode`) - if !Nullable.isNullable(Nullable.make(parent)) { - parent["removeChild"](domNode) - } - } - } -} - -/// Create initial render state -let createState = (dispatch: 'msg => unit): renderState<'msg> => { - listeners: [], - dispatch, - previousVdom: None, -} - -/// Mount a TEA app to a container selector -let mount = ( - containerSelector: string, - vdom: t<'msg>, - dispatch: 'msg => unit, -): option> => { - switch querySelector(containerSelector) { - | None => { - Console.error(`Mount point not found: ${containerSelector}`) - None - } - | Some(container) => { - let state = createState(dispatch) - render(container, vdom, state) - Some(state) - } - } -} - -/// Update render - use diffing when possible, fallback to full render -let update = ( - container: domElement, - vdom: t<'msg>, - state: renderState<'msg>, -): unit => { - switch state.previousVdom { - | None => { - render(container, vdom, state) - state.previousVdom = Some(vdom) - } - | Some(oldVdom) => { - let patch = diff(oldVdom, vdom) - switch patch { - | NoChange => () - | Replace(_) => { - // For full replacement, do a full re-render - render(container, vdom, state) - } - | _ => { - let containerObj: {..} = Obj.magic(container) - let firstChild: option = switch containerObj["firstChild"] { - | child if !Nullable.isNullable(Nullable.make(child)) => Some(child) - | _ => None - } - switch firstChild { - | Some(child) => applyPatch(child, patch, state) - | None => render(container, vdom, state) - } - } - } - state.previousVdom = Some(vdom) - } - } -} diff --git a/gui/src/tea/Tea_Sub.affine b/gui/src/tea/Tea_Sub.affine new file mode 100644 index 0000000..9e815be --- /dev/null +++ b/gui/src/tea/Tea_Sub.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Tea_Sub; + +// TODO: Complete semantic implementation diff --git a/gui/src/tea/Tea_Sub.res b/gui/src/tea/Tea_Sub.res deleted file mode 100644 index 540db48..0000000 --- a/gui/src/tea/Tea_Sub.res +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// TEA Subscriptions - External event sources. -/// -/// Subscriptions allow the TEA application to receive messages from -/// external sources like timers, Gossamer backend events, or window events. -/// Ported from the PanLL TEA implementation for intsoc-transactor. - -/// A subscription that produces messages of type 'msg -type rec t<'msg> = - | None - | Registration(string, ('msg => unit) => unit => unit) - | Batch(array>) - -/// No subscription -let none: t<'msg> = None - -/// Create a subscription with a unique key and enabler function. -/// The enabler function receives a dispatch function and returns a cleanup function. -let registration = (key: string, enable: ('msg => unit) => unit => unit): t<'msg> => { - Registration(key, enable) -} - -/// Batch multiple subscriptions together -let batch = (subs: list>): t<'msg> => { - let subArray = List.toArray(subs) - // Filter out None subscriptions - let filtered = Array.filter(subArray, sub => { - switch sub { - | None => false - | _ => true - } - }) - - switch Array.length(filtered) { - | 0 => None - | 1 => Array.getUnsafe(filtered, 0) - | _ => Batch(filtered) - } -} - -/// Map a subscription's message type -let rec map = (sub: t<'a>, f: 'a => 'b): t<'b> => { - switch sub { - | None => None - | Registration(key, enable) => - Registration( - key, - dispatch => { - enable(a => dispatch(f(a))) - }, - ) - | Batch(subs) => Batch(Array.map(subs, s => map(s, f))) - } -} - -/// Get all registration keys from a subscription (for diffing) -let rec getKeys = (sub: t<'msg>): array => { - switch sub { - | None => [] - | Registration(key, _) => [key] - | Batch(subs) => Array.flatMap(subs, getKeys) - } -} - -/// Enable a subscription and return cleanup function -let rec enable = (sub: t<'msg>, dispatch: 'msg => unit): (unit => unit) => { - switch sub { - | None => () => () - | Registration(_key, enabler) => enabler(dispatch) - | Batch(subs) => { - let cleanups = Array.map(subs, s => enable(s, dispatch)) - () => Array.forEach(cleanups, cleanup => cleanup()) - } - } -} diff --git a/gui/src/tea/Tea_Vdom.affine b/gui/src/tea/Tea_Vdom.affine new file mode 100644 index 0000000..6dc3a0b --- /dev/null +++ b/gui/src/tea/Tea_Vdom.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Tea_Vdom; + +// TODO: Complete semantic implementation diff --git a/gui/src/tea/Tea_Vdom.res b/gui/src/tea/Tea_Vdom.res deleted file mode 100644 index a45afc1..0000000 --- a/gui/src/tea/Tea_Vdom.res +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// TEA Virtual DOM - Core VDOM implementation. -/// -/// A minimal virtual DOM for the TEA architecture, supporting -/// elements, text nodes, attributes, and event handlers. -/// Ported from the PanLL TEA implementation for intsoc-transactor. - -/// Attribute types -type rec attribute<'msg> = - | Property(string, string) - | Style(string, string) - | Event(string, unit => 'msg) - | EventWithValue(string, string => 'msg) - -/// Virtual DOM node type -type rec t<'msg> = - | Text(string) - | Element(string, array>, array>) - -/// Create a text node -let text = (s: string): t<'msg> => Text(s) - -/// Create an element node -let node = (tag: string, attrs: list>, children: list>): t<'msg> => { - Element(tag, List.toArray(attrs), List.toArray(children)) -} - -/// Attribute constructors -let class_ = (name: string): attribute<'msg> => Property("class", name) -let id = (name: string): attribute<'msg> => Property("id", name) -let style = (prop: string, value: string): attribute<'msg> => Style(prop, value) -let placeholder = (text: string): attribute<'msg> => Property("placeholder", text) -let value = (v: string): attribute<'msg> => Property("value", v) -let title = (t: string): attribute<'msg> => Property("title", t) -let href = (url: string): attribute<'msg> => Property("href", url) -let src = (url: string): attribute<'msg> => Property("src", url) -let alt = (text: string): attribute<'msg> => Property("alt", text) -let disabled = (b: bool): attribute<'msg> => Property("disabled", b ? "true" : "false") -let checked = (b: bool): attribute<'msg> => Property("checked", b ? "true" : "false") -let type_ = (t: string): attribute<'msg> => Property("type", t) -let name = (n: string): attribute<'msg> => Property("name", n) -let for_ = (id: string): attribute<'msg> => Property("for", id) -let rows = (n: int): attribute<'msg> => Property("rows", Int.toString(n)) -let cols = (n: int): attribute<'msg> => Property("cols", Int.toString(n)) -let readonly = (b: bool): attribute<'msg> => Property("readonly", b ? "true" : "false") -let selected = (b: bool): attribute<'msg> => Property("selected", b ? "true" : "false") - -/// ARIA accessibility attributes -let ariaLabel = (label: string): attribute<'msg> => Property("aria-label", label) -let ariaLive = (mode: string): attribute<'msg> => Property("aria-live", mode) -let ariaExpanded = (b: bool): attribute<'msg> => Property("aria-expanded", b ? "true" : "false") -let ariaHidden = (b: bool): attribute<'msg> => Property("aria-hidden", b ? "true" : "false") -let ariaPressed = (b: bool): attribute<'msg> => Property("aria-pressed", b ? "true" : "false") -let ariaCurrent = (v: string): attribute<'msg> => Property("aria-current", v) -let ariaDescribedBy = (id: string): attribute<'msg> => Property("aria-describedby", id) -let role = (r: string): attribute<'msg> => Property("role", r) - -/// Event handlers -let onClick = (msg: 'msg): attribute<'msg> => Event("click", () => msg) -let onInput = (handler: string => 'msg): attribute<'msg> => EventWithValue("input", handler) -let onChange = (handler: string => 'msg): attribute<'msg> => EventWithValue("change", handler) -let onSubmit = (msg: 'msg): attribute<'msg> => Event("submit", () => msg) -let onMouseEnter = (msg: 'msg): attribute<'msg> => Event("mouseenter", () => msg) -let onMouseLeave = (msg: 'msg): attribute<'msg> => Event("mouseleave", () => msg) -let onFocus = (msg: 'msg): attribute<'msg> => Event("focus", () => msg) -let onBlur = (msg: 'msg): attribute<'msg> => Event("blur", () => msg) - -/// Map the message type of a virtual DOM node -let rec map = (vdom: t<'a>, f: 'a => 'b): t<'b> => { - switch vdom { - | Text(s) => Text(s) - | Element(tag, attrs, children) => - Element( - tag, - Array.map(attrs, attr => mapAttr(attr, f)), - Array.map(children, child => map(child, f)), - ) - } -} -and mapAttr = (attr: attribute<'a>, f: 'a => 'b): attribute<'b> => { - switch attr { - | Property(k, v) => Property(k, v) - | Style(k, v) => Style(k, v) - | Event(name, handler) => Event(name, () => f(handler())) - | EventWithValue(name, handler) => EventWithValue(name, v => f(handler(v))) - } -}