commit 214c52f556e42fb65f1f44129ef8263d1d5758dd Author: stan44 Date: Sun Mar 1 20:52:56 2026 -0600 v1.4 push. diff --git a/.github/workflows/reliability-matrix.yml b/.github/workflows/reliability-matrix.yml new file mode 100644 index 0000000..4d73746 --- /dev/null +++ b/.github/workflows/reliability-matrix.yml @@ -0,0 +1,58 @@ +name: reliability-matrix + +on: + workflow_dispatch: + push: + branches: [ main ] + pull_request: + +jobs: + matrix: + name: ${{ matrix.os }} / .NET tests + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Restore + run: dotnet restore DevTool.csproj + + - name: Build + run: dotnet build DevTool.csproj -c Release --no-restore + + - name: Test + run: dotnet test tests/DevTool.Tests/DevTool.Tests.csproj -c Release --no-build --logger "trx;LogFileName=test-results.trx" + + - name: Generate matrix summary + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path artifacts | Out-Null + @" + { + "os": "${{ matrix.os }}", + "commit": "${{ github.sha }}", + "workflow": "${{ github.workflow }}", + "runId": "${{ github.run_id }}", + "runNumber": "${{ github.run_number }}", + "status": "passed" + } + "@ | Set-Content artifacts/reliability-${{ matrix.os }}.json -Encoding UTF8 + + - name: Upload test artifacts + uses: actions/upload-artifact@v4 + with: + name: reliability-${{ matrix.os }} + path: | + **/test-results.trx + artifacts/reliability-${{ matrix.os }}.json + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d148d21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +bin/ +obj/ +__pycache__/ +.cache/ +.vscode/ +.idea/ +.vs/ +.git/ +.pip +.tmp +.venv +.dotnet_home +.nuget +publish-test/ + diff --git a/DevTool.csproj b/DevTool.csproj new file mode 100644 index 0000000..037a0c2 --- /dev/null +++ b/DevTool.csproj @@ -0,0 +1,44 @@ + + + + Exe + net10.0 + enable + enable + sdt + Sdt + false + + + + + + + + + + + + + + + + + + + + + scripts\%(Filename)%(Extension) + scripts\%(Filename)%(Extension) + PreserveNewest + PreserveNewest + + + scripts\%(Filename)%(Extension) + scripts\%(Filename)%(Extension) + PreserveNewest + PreserveNewest + + + + diff --git a/DevTool.slnx b/DevTool.slnx new file mode 100644 index 0000000..2d7eab5 --- /dev/null +++ b/DevTool.slnx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..c7f2ccc --- /dev/null +++ b/Program.cs @@ -0,0 +1,329 @@ +using Sdt.Config; +using Sdt.Core; +using Sdt.Tui; +using Spectre.Console; + +try +{ + var cliArgs = Environment.GetCommandLineArgs().Skip(1).ToArray(); + if (TryGetWorkspaceCommand(cliArgs, out var workspaceCommand)) + return RunWorkspaceCommand(cliArgs, workspaceCommand); + + if (TryGetHeadlessCommand(cliArgs, out var headlessKind)) + { + var exit = await RunHeadlessAsync(cliArgs, headlessKind); + return exit; + } + + // ── Workspace + project discovery ──────────────────────────────────────── + var workspaceResult = WorkspaceLoader.FindAndLoad(); + var projectResult = ConfigLoader.FindAndLoad(); + var forceInit = cliArgs.Any(a => string.Equals(a, "init", StringComparison.OrdinalIgnoreCase) || + string.Equals(a, "--init", StringComparison.OrdinalIgnoreCase)); + + if (forceInit) + { + var scan = ConfigBootstrapper.Scan(Directory.GetCurrentDirectory()); + var generated = ConfigBootstrapper.BuildDefaultConfig(scan); + var path = ConfigBootstrapper.WriteDefaultConfig(scan.ProjectRoot, generated, overwrite: false); + AnsiConsole.MarkupLine(Theme.Ok($"Initialized config at {path}")); + projectResult = ConfigLoader.FindAndLoad(scan.ProjectRoot); + } + + if (projectResult is null) + { + AnsiConsole.MarkupLine($"[bold {Theme.Red}]SDT:[/] [{Theme.Amber}]No devtool.json found[/] in current directory or any parent."); + var bootstrap = AnsiConsole.Confirm( + $"[{Theme.Amber}]Generate a default devtool.json for this project now?[/]", + defaultValue: true); + if (!bootstrap) + { + AnsiConsole.MarkupLine(Theme.Faint("Create a devtool.json in your project root to get started.")); + return 1; + } + + var scan = ConfigBootstrapper.Scan(Directory.GetCurrentDirectory()); + AnsiConsole.MarkupLine(Theme.Faint($"Detected project root: {scan.ProjectRoot}")); + if (scan.ToolFamilies.Count > 0) + AnsiConsole.MarkupLine(Theme.Faint($"Detected tool families: {string.Join(", ", scan.ToolFamilies)}")); + + var generated = ConfigBootstrapper.BuildDefaultConfig(scan); + var preview = ConfigBootstrapper.ToJson(generated); + AnsiConsole.Write(new Panel(Markup.Escape(preview)).Header("Generated devtool.json preview").BorderStyle(Theme.DimStyle)); + + var confirmWrite = AnsiConsole.Confirm( + $"[{Theme.Amber}]Write generated devtool.json to {scan.ProjectRoot}?[/]", + defaultValue: true); + if (!confirmWrite) + return 1; + + var path = ConfigBootstrapper.WriteDefaultConfig(scan.ProjectRoot, generated, overwrite: false); + AnsiConsole.MarkupLine(Theme.Ok($"Created {path}")); + projectResult = ConfigLoader.FindAndLoad(scan.ProjectRoot); + if (projectResult is null) + { + AnsiConsole.MarkupLine(Theme.Fail("Generated config could not be reloaded.")); + return 1; + } + } + + // ── Main run loop (handles workspace project switching) ──────────────── + var currentLoaded = projectResult; + var (workspace, workspaceRoot) = workspaceResult.HasValue + ? (workspaceResult.Value.Config, workspaceResult.Value.WorkspaceRoot) + : ((WorkspaceConfig?)null, (string?)null); + string? pendingWorkflowId = null; + + while (true) + { + var app = new App( + currentLoaded.Config, + currentLoaded.ProjectRoot, + currentLoaded.Warnings, + workspace, + workspaceRoot, + pendingWorkflowId); + pendingWorkflowId = null; + + var result = await app.RunAsync(); + + if (result.Reason == AppExitReason.Quit) + break; + + if (result.Reason == AppExitReason.SwitchProject && result.NewProjectRoot is not null) + { + LoadedProjectConfig? loaded; + try + { + loaded = ConfigLoader.FindAndLoad(result.NewProjectRoot); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine(Theme.Fail(ex.Message)); + AnsiConsole.MarkupLine(Theme.Faint("Press any key to stay on current project...")); + Console.ReadKey(intercept: true); + continue; + } + if (loaded is null) + { + AnsiConsole.MarkupLine(Theme.Fail($"No devtool.json found at: {result.NewProjectRoot}")); + AnsiConsole.MarkupLine(Theme.Faint("Press any key to stay on current project...")); + Console.ReadKey(intercept: true); + continue; + } + + currentLoaded = loaded; + pendingWorkflowId = result.RunWorkflowId; + } + } + + return 0; +} +catch (Exception ex) +{ + var message = ex.Message; + var isExpectedMigrationError = + ex is InvalidOperationException && + message.Contains("Legacy targets-only config detected", StringComparison.OrdinalIgnoreCase); + + AnsiConsole.MarkupLine(Theme.Fail($"Fatal: {message}")); + if (isExpectedMigrationError) + { + var configPath = ConfigLoader.FindConfigPath(); + if (!string.IsNullOrWhiteSpace(configPath)) + { + var migrate = AnsiConsole.Confirm( + $"[{Theme.Amber}]Apply automatic migration now (creates backup + converts targets -> workflows)?[/]", + defaultValue: true); + if (migrate) + { + var result = ConfigLoader.ApplyLegacyTargetMigration(configPath, createBackup: true); + if (result.Success) + { + AnsiConsole.MarkupLine(Theme.Ok("Migration applied successfully.")); + if (!string.IsNullOrWhiteSpace(result.BackupPath)) + AnsiConsole.MarkupLine(Theme.Faint($"Backup: {result.BackupPath}")); + AnsiConsole.MarkupLine(Theme.Faint("Run sdt.exe again in strict mode.")); + } + else + { + AnsiConsole.MarkupLine(Theme.Fail($"Migration failed: {result.Message}")); + } + } + } + } + + if (!isExpectedMigrationError) + AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything); + + return 1; +} + +static bool TryGetHeadlessCommand(IReadOnlyList cliArgs, out string kind) +{ + kind = ""; + if (cliArgs.Count == 0) + return false; + + if (string.Equals(cliArgs[0], "run", StringComparison.OrdinalIgnoreCase)) + { + kind = "run"; + return true; + } + + if (string.Equals(cliArgs[0], "debug", StringComparison.OrdinalIgnoreCase)) + { + kind = "debug"; + return true; + } + + return false; +} + +static bool TryGetWorkspaceCommand(IReadOnlyList cliArgs, out string command) +{ + command = ""; + if (cliArgs.Count < 2) + return false; + + if (!string.Equals(cliArgs[0], "workspace", StringComparison.OrdinalIgnoreCase)) + return false; + + if (string.Equals(cliArgs[1], "scan", StringComparison.OrdinalIgnoreCase)) + { + command = "scan"; + return true; + } + + return false; +} + +static int RunWorkspaceCommand(IReadOnlyList cliArgs, string command) +{ + if (!string.Equals(command, "scan", StringComparison.OrdinalIgnoreCase)) + return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed); + + var options = ParseOptions(cliArgs.Skip(2).ToArray(), out _); + var startDir = options.TryGetValue("--project-root", out var root) && !string.IsNullOrWhiteSpace(root) + ? root + : Directory.GetCurrentDirectory(); + var asJson = options.ContainsKey("--json"); + + var workspaceLoaded = WorkspaceLoader.FindAndLoad(startDir); + if (workspaceLoaded is null) + { + var payload = new { success = false, message = "No workspace could be discovered." }; + Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(payload)); + return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed); + } + + var (workspace, workspaceRoot) = workspaceLoaded.Value; + var currentRoot = ConfigLoader.FindAndLoad(startDir)?.ProjectRoot ?? startDir; + var scan = WorkspaceLoader.ScanInventory(workspaceRoot, currentRoot, workspace); + if (asJson) + { + Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(scan, new System.Text.Json.JsonSerializerOptions + { + PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, + WriteIndented = true + }.WithEnumStrings())); + } + else + { + Console.WriteLine($"Workspace root: {scan.WorkspaceRoot}"); + Console.WriteLine($"Known projects: {scan.KnownProjects.Count}"); + Console.WriteLine($"Candidates: {scan.Candidates.Count}"); + foreach (var item in scan.Candidates) + Console.WriteLine($" - {item.DisplayName} [{item.PrimaryKind}] {item.RootPath}"); + } + + return 0; +} + +static async Task RunHeadlessAsync(IReadOnlyList cliArgs, string kind) +{ + var options = ParseOptions(cliArgs.Skip(1).ToArray(), out var positional); + var json = options.ContainsKey("--json"); + var startDir = options.TryGetValue("--project-root", out var projectRootOpt) + ? projectRootOpt + : Directory.GetCurrentDirectory(); + var envProfile = options.TryGetValue("--env-profile", out var profile) ? profile : null; + var nonInteractive = options.ContainsKey("--non-interactive") || RuntimePolicy.IsNonInteractive(); + + var loaded = ConfigLoader.FindAndLoad(startDir); + if (loaded is null) + { + Console.WriteLine("{\"success\":false,\"message\":\"No devtool.json found for headless command.\"}"); + return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed); + } + + var service = new HeadlessExecutionService(); + if (kind == "run") + { + if (positional.Count == 0) + { + Console.WriteLine("{\"success\":false,\"message\":\"Missing workflow id. Usage: sdt run [--json] [--project-root ] [--env-profile ] [--non-interactive]\"}"); + return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed); + } + + return await service.RunWorkflowAsync( + loaded, + new HeadlessRunRequest( + WorkflowId: positional[0], + ProjectRoot: loaded.ProjectRoot, + EnvProfile: envProfile, + NonInteractive: nonInteractive, + JsonOutput: json)); + } + + if (positional.Count == 0) + { + Console.WriteLine("{\"success\":false,\"message\":\"Missing debug profile id. Usage: sdt debug [--json] [--project-root ] [--env-profile ] [--non-interactive]\"}"); + return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed); + } + + return await service.RunDebugAsync( + loaded, + new HeadlessDebugRequest( + ProfileId: positional[0], + ProjectRoot: loaded.ProjectRoot, + EnvProfile: envProfile, + NonInteractive: nonInteractive, + JsonOutput: json)); +} + +static Dictionary ParseOptions(string[] args, out List positional) +{ + var options = new Dictionary(StringComparer.OrdinalIgnoreCase); + positional = []; + for (var i = 0; i < args.Length; i++) + { + var token = args[i]; + if (!token.StartsWith("-", StringComparison.Ordinal)) + { + positional.Add(token); + continue; + } + + if (i + 1 < args.Length && !args[i + 1].StartsWith("-", StringComparison.Ordinal)) + { + options[token] = args[i + 1]; + i++; + } + else + { + options[token] = null; + } + } + + return options; +} + +static class JsonOptionsExtensions +{ + public static System.Text.Json.JsonSerializerOptions WithEnumStrings(this System.Text.Json.JsonSerializerOptions options) + { + options.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); + return options; + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..766749b --- /dev/null +++ b/README.md @@ -0,0 +1,267 @@ +# SDT (Stan's Dev Tools) + +Cross-platform terminal orchestrator for project workflows, toolchain checks, and prerequisite gating. + +## Current State + +- Standalone `.NET` TUI app (`net10.0`) +- Domain-separated source projects under `src/`: + - `DevTool.Engine` (workflow/config/orchestration services) + - `DevTool.Runtime` (process/command execution primitives) + - `DevTool.Host.Tui` (terminal UI host surface) +- Workflow-first config model in `devtool.json` +- Strict-by-default legacy migration (`targets`-only configs fail unless compat mode is enabled) +- Python-first diagnostics/build script layer under `scripts/` +- Fail-fast execution with install prompt gating for missing prerequisites +- Debug profiles with attach metadata and diagnostics bundle generation +- Workspace-first project switching with support for external project paths +- Workspace-level defaults layering via `sdt-defaults.json` (ancestor defaults merged, project config wins) +- Project status tracking is maintained in `ROADMAP.md` +- Core run-event stream (`RunEvent`) shared by workflow + debug execution (TUI consumes it; GUI-ready) +- Run events are persisted to JSONL at `.sdt/events/` for external tooling/GUI consumers +- Run events now include versioned contract fields: `run_event_version`, `run_id`, `project_root`, `env_profile`, `timestamp_utc`, `event_type` +- TUI includes `SYSTEM -> View run events` to inspect persisted JSONL event logs +- `SYSTEM -> Run config doctor` can apply common autofixes (missing working dirs, legacy migration) +- `SYSTEM -> Keybinding help` provides normalized cross-platform shortcut guidance +- `SYSTEM -> Run history` supports rerun from prior execution context +- First-run projects are prompted to run a setup wizard (doctor + autofix + optional toolchain setup) +- Toolchain management now includes toolchain doctor + auto-fix flow with installer prompts and post-install verification +- Env profiles (`envProfiles`) support deterministic inheritance (`dev`/`ci`/`release`) and runtime profile selection from `SYSTEM -> Select env profile` +- Diagnostics bundles include managed secret redaction policy (env-key pattern redaction + output token redaction) +- Workspace quick actions/favorites can run workflows across projects (auto switch-and-run) +- Quick-action pinning is supported from workflow run results and events viewer +- Bootstrap detects additional project stacks (`go`, `maven`, `gradle`) and sets `project.type` (`dotnet`, `node`, `python`, `rust`, `go`, `java`, `tauri`, `polyglot`, `generic`) +- Headless execution mode is available for workflow/debug automation with JSON output +- Terminal capability fallback modes supported via `NO_COLOR`/`SDT_NO_COLOR` and `SDT_NO_UNICODE` + +## Run + +```powershell +dotnet run --project DevTool.csproj +``` + +Run from any subdirectory inside a project; SDT walks up to find `devtool.json`. + +If `devtool.json` is missing, SDT now offers to scan the repo and generate a default config. + +Explicit bootstrap command: + +```powershell +dotnet run --project DevTool.csproj -- init +``` + +Headless workflow/debug commands: + +```powershell +sdt run --json [--project-root ] [--env-profile ] [--non-interactive] +sdt debug --json [--project-root ] [--env-profile ] [--non-interactive] +``` + +Workspace inventory scan (GUI/TUI shared discovery contract): + +```powershell +sdt workspace scan --json [--project-root ] +``` + +`SDT_NONINTERACTIVE=1` globally enables non-interactive behavior for install prompts. + +Bootstrap detects common stacks (`dotnet`, `npm/node`, `python`, `cargo/tauri`, `go`, `maven/gradle`, `git`, `docker`) and generates: + +- default workflows +- toolchain/tooling defaults +- debug profiles + diagnostics defaults + +## Config Model + +SDT supports both: + +- `workflows` (preferred) +- `targets` (legacy; compat mode only) + +### Legacy Migration Mode (v1.2) + +- Default: strict mode +- Behavior: `targets`-only config fails early with migration instructions +- Preview file: SDT writes `devtool.generated.workflows.json` for migration help +- Temporary rollback: set `SDT_LEGACY_MODE=compat` + +Permanent fix (recommended): + +1. Open `devtool.generated.workflows.json` +2. Copy its `workflows` into `devtool.json` +3. Remove or empty legacy `targets` +4. Run `sdt.exe` again in strict mode + +### Workflow shape (preferred) + +```json +{ + "id": "build", + "label": "Build", + "description": "Build project", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "dotnet-build", + "label": "dotnet build", + "action": "dotnet-build", + "actionArgs": [], + "workingDir": ".", + "requires": [ + { "tool": "dotnet", "installPolicy": "Prompt" } + ] + } + ] +} +``` + +### Extra sections + +- `tooling.tools[].preferredInstallCommands`: preferred install commands per tool +- `tooling.tools[].executables`: explicit executable candidates for non-standard PATH setups +- `project.rootHints`: files/folders that identify project root +- `env`: session-level environment variable editor values +- `debug.profiles[]`: run/attach debug profiles +- `debug.diagnostics`: diagnostics bundle policy (`.sdt/debug` by default) + - secure default: allowlist-only environment capture + - set `includeAllEnv=true` to opt into full environment capture + +### Workspace Defaults Layering + +If SDT finds `sdt-defaults.json` in the project directory tree (current project root or an ancestor), it merges it into the effective config before runtime: + +- base layer: `sdt-defaults.json` +- override layer: project `devtool.json` (project values win) + +Merge behavior: + +- objects merge recursively +- arrays/scalars are replaced when project provides the property + +This is useful for shared defaults like toolchains, diagnostics policies, and baseline env definitions across multiple projects in one workspace. + +## Execution Behavior + +For each workflow step: + +1. Resolve dependencies (topological order) +2. Probe required tools +3. If missing, show install commands and prompt (`Prompt` policy) +4. On decline/install failure/step failure, stop immediately +5. Render step summary table with exit code + elapsed time +6. On workflow/debug failure, generate diagnostics bundle when enabled + +Installer command precedence: + +1. `tooling.tools[].preferredInstallCommands` +2. `scripts/diag.py install-plan` +3. built-in C# fallback templates (used automatically if script planning fails) + +When a tool probe fails, SDT now prints probe diagnostics (including command resolution source/path) in run output before prompting for installs. + +Headless exit code contract: + +- `0` success +- `10` missing prerequisite +- `11` install failed +- `12` command failed +- `13` validation/config error +- `14` user-declined / non-interactive prompt refusal + +## Scripts + +See [scripts/README.md](/e:/stansshit/csharp/DevTool-master/scripts/README.md). + +Primary Python entrypoints: + +- `scripts/diag.py` +- `scripts/build.py` +- `scripts/dotnet-min.py` +- `scripts/pip-min.py` +- `scripts/publish-*.py` + +## Workspace Support + +- Uses `sdt-workspace.json` when present +- If missing, can auto-discover nearby projects containing `devtool.json` +- Workspace screen can add external project roots (absolute paths supported) +- `projects[].disabled`, `projects[].tags`, and `projects[].toolFamilies` are supported +- Hybrid inventory model discovers marker-only candidates (`.slnx/.sln/.csproj`) without silently mutating workspace config +- TUI workspace screen supports: + - `Add candidate` + - `Add + initialize devtool.json` + - `Ignore for now` (session-only) +- Inventory snapshot is cached at `.sdt/workspace-inventory.json` for GUI-readiness + +## GUI Direction + +- Planned GUI stack for current phase: **Tauri-first** +- Avalonia is deferred for re-evaluation after first GUI milestone ships on top of the same headless contracts +- GUI workspace scaffold: [TauriShell README](/e:/stansshit/csharp/DevTool-master/src/DevTool.Host.Gui/TauriShell/README.md) +- First command bridge shipped in scaffold: `sdt workspace scan --json` +- Second command bridge shipped in scaffold: `sdt run --json` with live stream panel +- GUI will consume: + - `sdt workspace scan --json` inventory payload + - `run/debug --json` summaries + - persisted run events from `.sdt/events/*.jsonl` + +## Dev Shell Bootstrap + +Python-first cross-shell dev environment bootstrap: + +```powershell +# PowerShell +. ./scripts/dev-shell.ps1 + +# cmd +scripts\dev-shell.cmd +``` + +```bash +# bash/zsh +source ./scripts/dev-shell.sh +``` + +Underlying implementation is `scripts/dev_shell.py`: + +- `python scripts/dev_shell.py export --shell pwsh --json` +- `python scripts/dev_shell.py doctor` + +## Legacy PowerShell Compatibility + +Legacy `.ps1` scripts remain for migration compatibility only. New functionality is implemented in Python scripts first. `script-common.ps1` is legacy-only. + +Legacy runtime behavior in v1.2: + +- strict mode rejects `targets`-only configs by default +- compat mode (`SDT_LEGACY_MODE=compat`) temporarily allows legacy execution +- TUI `SYSTEM` includes `Migrate legacy targets -> workflows` to apply migration in place (with backup) +- Python reroute is authoritative for legacy `pwsh -File ...ps1` targets +- `.ps1` fallback is opt-in only: set `SDT_PWSH_LEGACY_FALLBACK=1` for temporary compatibility + +Deprecation target: + +- v1.x: compatibility only (no new behavior guarantees) +- v2.0: remove legacy `.ps1` scripts from default SDT workflows and docs + +## Testing + +Run unit/integration tests: + +```powershell +dotnet test tests/DevTool.Tests/DevTool.Tests.csproj +``` + +Run Python script smoke checks: + +```powershell +python -m py_compile scripts/*.py +``` + +## Reliability Matrix + +- CI matrix workflow: [reliability-matrix.yml](/e:/stansshit/csharp/DevTool-master/.github/workflows/reliability-matrix.yml) +- Runbook: [reliability-matrix-runbook.md](/e:/stansshit/csharp/DevTool-master/docs/reliability-matrix-runbook.md) +- Results log: [reliability-matrix-results.md](/e:/stansshit/csharp/DevTool-master/docs/reliability-matrix-results.md) +- Milestone status (Windows/Linux shipped, macOS delegated): [matrix-status.md](/e:/stansshit/csharp/DevTool-master/docs/matrix-status.md) diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..3a43403 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,102 @@ +# SDT Roadmap / Kanban + +## Done (v1.2 Stabilization) + +- [x] Python-first runtime for diagnostics/build wrappers +- [x] Config bootstrap generator for missing `devtool.json` +- [x] Workflow-first execution + dual schema support +- [x] Strict legacy mode default (`targets`-only blocked) +- [x] Compatibility escape hatch (`SDT_LEGACY_MODE=compat`) +- [x] Auto-generated migration preview (`devtool.generated.workflows.json`) +- [x] In-app migration action: `Migrate legacy targets -> workflows` +- [x] Centralized requirement inference (`RequirementResolver`) +- [x] Installer planning precedence + fallback resilience +- [x] Windows resolver hardening (`%VAR%` PATH expansion) +- [x] Resolver tracing surfaced in probe details +- [x] Secure diagnostics default (allowlist-only env capture) +- [x] Workspace external project add flow +- [x] Shared run event stream (`RunEvent`) across workflow + debug execution +- [x] TUI event rendering layer wired on top of core run events (GUI-readiness slice) +- [x] Persist run-event stream to JSONL for external GUI/client consumption (`.sdt/events/*.jsonl`) +- [x] TUI events viewer for persisted run-event logs (`SYSTEM -> View run events`) +- [x] Config doctor (`SYSTEM -> Run config doctor`) +- [x] Doctor autofix actions (create missing working dirs + invoke legacy migration) +- [x] Rich probe diagnostics panel in workflow failure summary +- [x] Enhanced Tauri fallback guidance (Windows/macOS/Linux package manager aware) +- [x] First-run setup wizard + completion state marker (`.sdt/setup-state.json`) +- [x] Toolchain hardening: probe diagnostics + toolchain doctor + auto-fix missing tools +- [x] Workspace-level defaults file layering (`sdt-defaults.json`) above per-project `devtool.json` +- [x] Add dedicated TUI "Events" viewer for last run +- [x] Add doctor autofix actions for common issues (missing dirs, legacy schema migration) +- [x] Env profiles (`dev`, `ci`, `release`) with deterministic merge order and runtime selection +- [x] Managed secrets redaction policy for diagnostics bundles (env + output patterns) +- [x] Setup wizard for first run (bootstrap + tool fixes) +- [x] Improve setup wizard to handle more edge cases and missing tools for projects +- [x] Add support for more project types +- [x] Add support for more tool types +- [x] Add support for more environment types +- [x] Add support for more environment variables +- [x] Favorites/quick actions across projects +- [x] Legacy PowerShell fallback made opt-in (`SDT_PWSH_LEGACY_FALLBACK=1`), Python reroute remains default +- [x] Bootstrap now resolves dotnet working directory from nearest `.slnx/.sln/.csproj` in multi-project roots +- [x] Bootstrap filters node/npm detection to runnable package scripts (avoids dependency-only `package.json` false positives) +- [x] Action layer now skips non-applicable stacks (`npm`/`cargo`/`tauri`/`dotnet`) instead of hard-failing +- [x] `publish-output.py` now auto-skips non-detected sidecar/web/gateway/tauri stacks in generic repos + +## In Progress (next focus) + +- [ ] Execute full OS matrix verification on Windows/Linux/macOS runners +- [ ] Native GUI shell over headless core services (Tauri-first in v1.x; Avalonia re-evaluate later) +- [x] Create dedicated `src/DevTool.Host.Gui/TauriShell` scaffold to keep GUI work isolated from TUI/core +- [x] Bootstrap first Tauri command bridge: `sdt workspace scan --json` +- [x] Structural refactor to domain-separated projects under `src/` (`DevTool.Engine`, `DevTool.Runtime`, `DevTool.Host.Tui`) +- [x] Add second Tauri bridge command: `sdt run --json` with live stream panel +- [ ] Remove legacy PowerShell wrappers in v2 +- [x] Add workspace project inventory model (all `.slnx/.sln/.csproj`) for GUI/TUI multi-project selector + +## Next Sprint (v1.3 UX Foundation) + +- [x] Define stable event contract version (`run_event_version`) and publish schema docs +- [x] Add `--json`/headless execution mode for workflow/debug runs (machine-readable progress + result) +- [x] Add TUI command palette (`Ctrl+K`) for quick action/run/switch project +- [x] Add unified failure card: `What failed`, `Why`, `Exact fix command`, `Retry action` +- [x] Add non-interactive install mode for CI/headless (`SDT_NONINTERACTIVE=1`) +- [x] Add per-run execution context block in TUI (`project`, `envProfile`, `cwd`, `tool resolution source`) +- [x] Add cross-platform keybinding help screen (`?`) and normalize shortcuts across Windows/macOS/Linux terminals +- [x] Add terminal capability fallback modes (no color/no unicode/no emoji safe rendering) +- [x] Add workspace quick-action pinning from run results and events viewer +- [x] Add run history browser with re-run from prior execution context + +## v1.3 Reliability Matrix + +- [x] Add project-type matrix coverage tests (`dotnet`, `node/npm`, `tauri/cargo`) +- [x] Add deterministic headless stop-reason/exit-code tests +- [ ] Execute full OS matrix verification on Windows/Linux/macOS runners +- [x] Publish reliability matrix runbook + results artifact in docs + - Blocked in current local workspace: no `.git` repo context and no `gh` CLI/auth; execute from Git-connected checkout. + - v1.4 policy: ship Windows/Linux, macOS delegated with checklist in `docs/matrix-status.md`. + +## Cross-Platform UX Principles + +- [x] One mental model for all surfaces: `Plan -> Probe -> Prompt -> Execute -> Diagnose` +- [x] Every failure must include a copy-pastable remediation command +- [x] No blocking hidden prompts from package managers (always explicit/non-interactive flags where possible) +- [x] Keep TUI and future GUI on the same core events/state, not duplicated execution logic +- [x] Preserve deterministic behavior across shell/env differences via explicit working directory + env profile display +- [ ] Remove legacy PowerShell wrappers in v2 + +## Current Milestone Status + +- v1.1 expansion items shipped partially (debug profiles + diagnostics + workspace add external) +- Robustness Sprint v1.2: **complete** +- v1.3 workstreams A/B/C: **complete** +- v1.3 workstream D: **in progress** (matrix test suite implemented; cross-OS execution pending) +- v1.3 UX foundation backlog items: **complete** +- Remaining gaps for broader AIO vision are GUI shell, cross-OS matrix run completion, and advanced orchestration scale. + +## GUI Decision Record + +- [x] GUI runtime direction locked for current phase: **Tauri-first now** +- [x] Keep headless core + JSON/event contracts as the shared backend authority +- [x] Defer Avalonia shell implementation to a re-evaluation checkpoint after Tauri shell proves workflow/debug/event parity +- [ ] Avalonia re-evaluation gate criteria documented and reviewed after first Tauri shell milestone diff --git a/devtool.json b/devtool.json new file mode 100644 index 0000000..3920575 --- /dev/null +++ b/devtool.json @@ -0,0 +1,487 @@ +{ + "name": "DevTool-master", + "version": "0.1.0", + "targets": [], + "workflows": [ + { + "id": "sidecar", + "label": "Publish Sidecar", + "description": "Publish sidecar service", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "sidecar:run", + "label": "python scripts/publish-sidecar.py", + "command": "python", + "args": [ + "scripts/publish-sidecar.py" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "web", + "label": "Build Web UI", + "description": "Build frontend assets", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "web:run", + "label": "python scripts/publish-app.py --target web", + "command": "python", + "args": [ + "scripts/publish-app.py", + "--target", + "web" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "tauri", + "label": "Build Tauri Desktop App", + "description": "Build desktop binary", + "group": "Build", + "dependsOn": [ + "sidecar" + ], + "steps": [ + { + "id": "tauri:run", + "label": "python scripts/publish-app.py --target tauri --tauri-bundles none", + "command": "python", + "args": [ + "scripts/publish-app.py", + "--target", + "tauri", + "--tauri-bundles", + "none" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "webgateway", + "label": "Publish WebGateway", + "description": "Publish ASP.NET gateway", + "group": "Build", + "dependsOn": [ + "web" + ], + "steps": [ + { + "id": "webgateway:run", + "label": "python scripts/publish-webgateway.py", + "command": "python", + "args": [ + "scripts/publish-webgateway.py" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "sync-output", + "label": "Sync Output", + "description": "Sync newest artifacts to output", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "sync-output:run", + "label": "python scripts/sync-output.py", + "command": "python", + "args": [ + "scripts/sync-output.py" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "stage-output", + "label": "Stage Output Bundle", + "description": "Publish and stage distributable output", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "stage-output:run", + "label": "python scripts/publish-output.py", + "command": "python", + "args": [ + "scripts/publish-output.py" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "run-gateway-dev", + "label": "Run WebGateway Server (Dev)", + "description": "Run gateway in development mode", + "group": "Dev", + "dependsOn": [], + "steps": [ + { + "id": "run-gateway-dev:run", + "label": "python scripts/run-webgateway.py --mode Dev", + "command": "python", + "args": [ + "scripts/run-webgateway.py", + "--mode", + "Dev" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "build", + "label": "Build", + "description": "Build detected project stacks", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "dotnet-build", + "label": "dotnet build", + "command": null, + "args": [], + "workingDir": ".", + "action": "dotnet-build", + "actionArgs": [], + "requires": [] + }, + { + "id": "npm-build", + "label": "npm run build", + "command": null, + "args": [], + "workingDir": ".", + "action": "npm-build", + "actionArgs": [], + "requires": [] + } + ] + }, + { + "id": "deps-refresh", + "label": "Refresh Dependencies", + "description": "Restore/install dependency stacks", + "group": "Deps", + "dependsOn": [], + "steps": [ + { + "id": "dotnet-restore", + "label": "dotnet restore", + "command": null, + "args": [], + "workingDir": ".", + "action": "dotnet-restore", + "actionArgs": [], + "requires": [] + }, + { + "id": "npm-ci", + "label": "npm ci", + "command": null, + "args": [], + "workingDir": ".", + "action": "npm-ci", + "actionArgs": [], + "requires": [] + } + ] + }, + { + "id": "test", + "label": "Run Tests", + "description": "Run detected test stacks", + "group": "Test", + "dependsOn": [], + "steps": [ + { + "id": "dotnet-test", + "label": "dotnet test", + "command": null, + "args": [], + "workingDir": ".", + "action": "dotnet-test", + "actionArgs": [], + "requires": [] + }, + { + "id": "npm-test", + "label": "npm test", + "command": null, + "args": [], + "workingDir": ".", + "action": "npm-test", + "actionArgs": [], + "requires": [] + }, + { + "id": "python-pytest", + "label": "python -m pytest", + "command": null, + "args": [], + "workingDir": ".", + "action": "python-pytest", + "actionArgs": [], + "requires": [] + } + ] + } + ], + "env": [ + { + "key": "SDT_LOG_LEVEL", + "description": "CLI log verbosity", + "default": "information", + "options": [ + "trace", + "debug", + "information", + "warning", + "error", + "critical" + ] + }, + { + "key": "SDT_ENV_PROFILE", + "description": "Active SDT runtime environment profile", + "default": "dev", + "options": [ + "dev", + "ci", + "release" + ] + } + ], + "envProfiles": { + "active": "dev", + "profiles": [ + { + "id": "dev", + "description": "Local development defaults", + "inherits": [], + "values": { + "SDT_ENV_PROFILE": "dev", + "SDT_LOG_LEVEL": "information" + } + }, + { + "id": "ci", + "description": "Continuous integration defaults", + "inherits": [ + "dev" + ], + "values": { + "SDT_ENV_PROFILE": "ci", + "CI": "true", + "SDT_LOG_LEVEL": "warning" + } + }, + { + "id": "release", + "description": "Release build defaults", + "inherits": [ + "dev" + ], + "values": { + "SDT_ENV_PROFILE": "release", + "SDT_LOG_LEVEL": "warning" + } + } + ] + }, + "toolchains": { + "python": { + "executable": "python", + "windowsExecutable": "py", + "launcherVersion": null, + "venvDir": ".venv", + "profiles": [], + "pipScript": null + }, + "node": { + "packageManager": "npm", + "workingDir": "." + } + }, + "tooling": { + "tools": [ + { + "tool": "dotnet", + "preferredInstallCommands": [], + "executables": [] + }, + { + "tool": "node", + "preferredInstallCommands": [], + "executables": [] + }, + { + "tool": "npm", + "preferredInstallCommands": [], + "executables": [] + }, + { + "tool": "python", + "preferredInstallCommands": [], + "executables": [] + } + ] + }, + "project": { + "type": "polyglot", + "rootHints": [ + "*.sln", + "package.json", + "scripts" + ], + "artifacts": [ + "bin", + "obj", + ".sdt/debug" + ] + }, + "debug": { + "profiles": [ + { + "id": "dotnet-run", + "label": "Run .NET app", + "type": "dotnet", + "command": "dotnet", + "args": [ + "run" + ], + "workingDir": ".", + "env": {}, + "requires": [ + { + "tool": "dotnet", + "installPolicy": "Prompt" + } + ], + "attach": { + "kind": "manual", + "port": null, + "processName": null, + "note": "Attach your IDE debugger to the running dotnet process." + } + }, + { + "id": "npm-dev", + "label": "Run npm dev server", + "type": "node", + "command": "npm", + "args": [ + "run", + "dev" + ], + "workingDir": ".", + "env": {}, + "requires": [ + { + "tool": "node", + "installPolicy": "Prompt" + }, + { + "tool": "npm", + "installPolicy": "Prompt" + } + ], + "attach": null + } + ], + "diagnostics": { + "enabled": true, + "outputDir": ".sdt/debug", + "includeAllEnv": false, + "captureEnvKeys": [ + "SDT_LOG_LEVEL", + "DOTNET_CLI_HOME", + "NUGET_PACKAGES", + "PIP_CACHE_DIR", + "NVM_HOME", + "NVM_SYMLINK" + ], + "redactSensitive": true, + "sensitiveKeyPatterns": [ + "TOKEN", + "SECRET", + "PASSWORD", + "PWD", + "CREDENTIAL", + "API_KEY", + "ACCESS_KEY", + "PRIVATE_KEY" + ], + "redactionAllowKeys": [], + "bundleOnFailure": true + } + } +} diff --git a/devtool.json.bak-20260301-182536 b/devtool.json.bak-20260301-182536 new file mode 100644 index 0000000..55201bc --- /dev/null +++ b/devtool.json.bak-20260301-182536 @@ -0,0 +1,477 @@ +{ + "name": "DevTool-master", + "version": "0.1.0", + "targets": [], + "workflows": [ + { + "id": "sidecar", + "label": "Publish Sidecar", + "description": "Publish sidecar service", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "sidecar:run", + "label": "python scripts/publish-sidecar.py", + "command": "python", + "args": [ + "scripts/publish-sidecar.py" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "web", + "label": "Build Web UI", + "description": "Build frontend assets", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "web:run", + "label": "python scripts/publish-app.py --target web", + "command": "python", + "args": [ + "scripts/publish-app.py", + "--target", + "web" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "tauri", + "label": "Build Tauri Desktop App", + "description": "Build desktop binary", + "group": "Build", + "dependsOn": [ + "sidecar" + ], + "steps": [ + { + "id": "tauri:run", + "label": "python scripts/publish-app.py --target tauri --tauri-bundles none", + "command": "python", + "args": [ + "scripts/publish-app.py", + "--target", + "tauri", + "--tauri-bundles", + "none" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "webgateway", + "label": "Publish WebGateway", + "description": "Publish ASP.NET gateway", + "group": "Build", + "dependsOn": [ + "web" + ], + "steps": [ + { + "id": "webgateway:run", + "label": "python scripts/publish-webgateway.py", + "command": "python", + "args": [ + "scripts/publish-webgateway.py" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "sync-output", + "label": "Sync Output", + "description": "Sync newest artifacts to output", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "sync-output:run", + "label": "python scripts/sync-output.py", + "command": "python", + "args": [ + "scripts/sync-output.py" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "stage-output", + "label": "Stage Output Bundle", + "description": "Publish and stage distributable output", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "stage-output:run", + "label": "python scripts/publish-output.py", + "command": "python", + "args": [ + "scripts/publish-output.py" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "run-gateway-dev", + "label": "Run WebGateway Server (Dev)", + "description": "Run gateway in development mode", + "group": "Dev", + "dependsOn": [], + "steps": [ + { + "id": "run-gateway-dev:run", + "label": "python scripts/run-webgateway.py --mode Dev", + "command": "python", + "args": [ + "scripts/run-webgateway.py", + "--mode", + "Dev" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "build", + "label": "Build", + "description": "Build detected project stacks", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "dotnet-build", + "label": "dotnet build", + "command": null, + "args": [], + "workingDir": ".", + "action": "dotnet-build", + "actionArgs": [], + "requires": [] + }, + { + "id": "npm-build", + "label": "npm run build", + "command": null, + "args": [], + "workingDir": ".", + "action": "npm-build", + "actionArgs": [], + "requires": [] + } + ] + }, + { + "id": "deps-refresh", + "label": "Refresh Dependencies", + "description": "Restore/install dependency stacks", + "group": "Deps", + "dependsOn": [], + "steps": [ + { + "id": "dotnet-restore", + "label": "dotnet restore", + "command": null, + "args": [], + "workingDir": ".", + "action": "dotnet-restore", + "actionArgs": [], + "requires": [] + }, + { + "id": "npm-ci", + "label": "npm ci", + "command": null, + "args": [], + "workingDir": ".", + "action": "npm-ci", + "actionArgs": [], + "requires": [] + } + ] + }, + { + "id": "test", + "label": "Run Tests", + "description": "Run detected test stacks", + "group": "Test", + "dependsOn": [], + "steps": [ + { + "id": "dotnet-test", + "label": "dotnet test", + "command": null, + "args": [], + "workingDir": ".", + "action": "dotnet-test", + "actionArgs": [], + "requires": [] + }, + { + "id": "npm-test", + "label": "npm test", + "command": null, + "args": [], + "workingDir": ".", + "action": "npm-test", + "actionArgs": [], + "requires": [] + }, + { + "id": "python-pytest", + "label": "python -m pytest", + "command": null, + "args": [], + "workingDir": ".", + "action": "python-pytest", + "actionArgs": [], + "requires": [] + } + ] + } + ], + "env": [ + { + "key": "SDT_LOG_LEVEL", + "description": "CLI log verbosity", + "default": "information", + "options": [ + "trace", + "debug", + "information", + "warning", + "error", + "critical" + ] + } + ], + "envProfiles": { + "active": "dev", + "profiles": [ + { + "id": "dev", + "description": "Local development defaults", + "inherits": [], + "values": { + "SDT_ENV_PROFILE": "dev", + "SDT_LOG_LEVEL": "information" + } + }, + { + "id": "ci", + "description": "Continuous integration defaults", + "inherits": [ + "dev" + ], + "values": { + "SDT_ENV_PROFILE": "ci", + "CI": "true", + "SDT_LOG_LEVEL": "warning" + } + }, + { + "id": "release", + "description": "Release build defaults", + "inherits": [ + "dev" + ], + "values": { + "SDT_ENV_PROFILE": "release", + "SDT_LOG_LEVEL": "warning" + } + } + ] + }, + "toolchains": { + "python": { + "executable": "python", + "windowsExecutable": "py", + "launcherVersion": null, + "venvDir": ".venv", + "profiles": [], + "pipScript": null + }, + "node": { + "packageManager": "npm", + "workingDir": "." + } + }, + "tooling": { + "tools": [ + { + "tool": "dotnet", + "preferredInstallCommands": [], + "executables": [] + }, + { + "tool": "node", + "preferredInstallCommands": [], + "executables": [] + }, + { + "tool": "npm", + "preferredInstallCommands": [], + "executables": [] + }, + { + "tool": "python", + "preferredInstallCommands": [], + "executables": [] + } + ] + }, + "project": { + "type": "polyglot", + "rootHints": [ + "*.sln", + "package.json", + "scripts" + ], + "artifacts": [ + "bin", + "obj", + ".sdt/debug" + ] + }, + "debug": { + "profiles": [ + { + "id": "dotnet-run", + "label": "Run .NET app", + "type": "dotnet", + "command": "dotnet", + "args": [ + "run" + ], + "workingDir": ".", + "env": {}, + "requires": [ + { + "tool": "dotnet", + "installPolicy": "Prompt" + } + ], + "attach": { + "kind": "manual", + "port": null, + "processName": null, + "note": "Attach your IDE debugger to the running dotnet process." + } + }, + { + "id": "npm-dev", + "label": "Run npm dev server", + "type": "node", + "command": "npm", + "args": [ + "run", + "dev" + ], + "workingDir": ".", + "env": {}, + "requires": [ + { + "tool": "node", + "installPolicy": "Prompt" + }, + { + "tool": "npm", + "installPolicy": "Prompt" + } + ], + "attach": null + } + ], + "diagnostics": { + "enabled": true, + "outputDir": ".sdt/debug", + "includeAllEnv": false, + "captureEnvKeys": [ + "SDT_LOG_LEVEL", + "DOTNET_CLI_HOME", + "NUGET_PACKAGES", + "PIP_CACHE_DIR", + "NVM_HOME", + "NVM_SYMLINK" + ], + "redactSensitive": true, + "sensitiveKeyPatterns": [ + "TOKEN", + "SECRET", + "PASSWORD", + "PWD", + "CREDENTIAL", + "API_KEY", + "ACCESS_KEY", + "PRIVATE_KEY" + ], + "redactionAllowKeys": [], + "bundleOnFailure": true + } + } +} diff --git a/docs/matrix-status.md b/docs/matrix-status.md new file mode 100644 index 0000000..904e425 --- /dev/null +++ b/docs/matrix-status.md @@ -0,0 +1,33 @@ +# SDT Matrix Status (v1.4) + +## Scope + +- Required for this milestone: Windows, Linux +- Delegated/blocked: macOS + +## Current Status + +- Windows: local verification expected by maintainer +- Linux: local/native verification expected by maintainer +- macOS: delegated to collaborator (pending) + +## macOS Delegation Checklist + +1. Build and run SDT from a clean checkout. +2. Execute representative workflows for: + - dotnet + - node/npm + - cargo/tauri (if available) +3. Capture and submit artifacts: + - `.sdt/events/*.jsonl` + - `.sdt/debug/*/summary.json` + - `.sdt/debug/*/tools.json` + - pass/fail table by workflow +4. Record any path, resolver, or install-plan differences. + +## Artifact Format + +- `events`: run timeline and lifecycle transitions +- `summary`: stop reason / exit code contract validation +- `tools`: probe and resolver trace validation +- `results`: markdown table with workflow, status, and notes diff --git a/docs/reliability-matrix-results.md b/docs/reliability-matrix-results.md new file mode 100644 index 0000000..5b47adb --- /dev/null +++ b/docs/reliability-matrix-results.md @@ -0,0 +1,19 @@ +# SDT Reliability Matrix Results + +## Latest Baseline + +- Status: pending first full CI matrix run +- Workflow: `.github/workflows/reliability-matrix.yml` +- Last updated: 2026-03-01 + +## Results Log + +| Date (UTC) | Commit | Run ID | Windows | Linux | macOS | Notes | +|---|---|---|---|---|---|---| +| 2026-03-01 | pending | pending | pending | pending | pending | Initial v1.3 matrix workflow added; awaiting first run. | + +## Notes + +- A result row should only be marked `pass` when both tests and artifact generation succeed for that OS. +- Any flaky behavior should be recorded in Notes with a link/reference to the failing artifact. + diff --git a/docs/reliability-matrix-runbook.md b/docs/reliability-matrix-runbook.md new file mode 100644 index 0000000..fb11d8f --- /dev/null +++ b/docs/reliability-matrix-runbook.md @@ -0,0 +1,69 @@ +# SDT Reliability Matrix Runbook + +## Purpose +Run and track SDT reliability verification across: + +- Windows +- Linux +- macOS + +with deterministic pass/fail outcomes for core orchestration flows. + +## Matrix Scope + +### Project-Type Coverage +- `dotnet` +- `node/npm` +- `tauri/cargo` + +### Validation Categories +- workflow planning/execution determinism +- prerequisite probe/install gating behavior +- versioned event JSON contract validity +- diagnostics bundle generation on failure +- deterministic stop reason and exit-code mapping + +## Execution Paths + +### CI (Preferred) +Use the GitHub Actions workflow: + +- `.github/workflows/reliability-matrix.yml` + +Triggers: +- pull requests +- pushes to `main` +- manual `workflow_dispatch` + +Expected artifacts per OS: +- `test-results.trx` +- `reliability-.json` + +### Local Spot Checks +Run: + +```powershell +dotnet build DevTool.csproj -c Release +dotnet test tests/DevTool.Tests/DevTool.Tests.csproj -c Release +``` + +## Result Recording +After each CI matrix run: + +1. Collect uploaded artifacts for all OS jobs. +2. Update `docs/reliability-matrix-results.md` with: + - commit SHA + - run id + - per-OS status + - notes on failures/flakes +3. If any OS fails: + - open/attach issue with failing artifact references + - do not mark matrix item complete in `ROADMAP.md` + +## Completion Criteria +Matrix verification is considered complete when: + +1. A full CI run passes on all three OS runners. +2. Results are logged in `docs/reliability-matrix-results.md`. +3. `ROADMAP.md` matrix checkboxes are updated accordingly. + diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 0000000..4304b8a --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,26 @@ +{ + "name": "DevTool-master", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/@tauri-apps/api": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/tauri-plugin-mic-recorder-api": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tauri-plugin-mic-recorder-api/-/tauri-plugin-mic-recorder-api-2.0.0.tgz", + "integrity": "sha512-04wqYCX4WIlYd6KUY7aS3+W4B5RtnSoVczaQCBSXKpQkEx9XdaaBN05XCee2unxGva0btSXBItFqQSdosnS4jQ==", + "license": "MIT", + "dependencies": { + "@tauri-apps/api": ">=2.0.0-beta.6" + } + } + } +} diff --git a/node_modules/.sdt-deps.sha256 b/node_modules/.sdt-deps.sha256 new file mode 100644 index 0000000..ed053a1 --- /dev/null +++ b/node_modules/.sdt-deps.sha256 @@ -0,0 +1 @@ +9241cd740a6b6aa9ccbad93df22cafe05a801f777e6113df8118981c3a438e58 \ No newline at end of file diff --git a/node_modules/@tauri-apps/api/CHANGELOG.md b/node_modules/@tauri-apps/api/CHANGELOG.md new file mode 100644 index 0000000..5af6867 --- /dev/null +++ b/node_modules/@tauri-apps/api/CHANGELOG.md @@ -0,0 +1,1041 @@ +# Changelog + +## \[2.10.1] + +### Bug Fixes + +- Re-release of `2.10.0` with working assets. + +## \[2.10.0] + +### Dependencies + +- [`517b81e97`](https://www.github.com/tauri-apps/tauri/commit/517b81e97005d087ca5fc4538d954982fce0f4ac) ([#14876](https://www.github.com/tauri-apps/tauri/pull/14876)) Upgraded to `tauri@2.10` + +## \[2.9.1] + +### Bug Fixes + +- [`ad1dec2e2`](https://www.github.com/tauri-apps/tauri/commit/ad1dec2e2488fe5c0a004b69f1bd290dfc593bf8) ([#14464](https://www.github.com/tauri-apps/tauri/pull/14464) by [@funnydino](https://www.github.com/tauri-apps/tauri/../../funnydino)) Fix `addPluginListener` fallback added in https://github.com/tauri-apps/tauri/pull/14132 didn't work properly + +## \[2.9.0] + +### New Features + +- [`f5851ee00`](https://www.github.com/tauri-apps/tauri/commit/f5851ee00d6d1f4d560a220ca5a728fedd525092) ([#14089](https://www.github.com/tauri-apps/tauri/pull/14089)) Adds the `scrollBarStyle` option to the Webview and WebviewBuilder constructors. +- [`3397fd9bf`](https://www.github.com/tauri-apps/tauri/commit/3397fd9bfe5f6b1337110149f6c34731b8a44bb3) ([#14133](https://www.github.com/tauri-apps/tauri/pull/14133)) Added `app > onBackButtonPress` for Android back button handling. + +### Enhancements + +- [`59089723f`](https://www.github.com/tauri-apps/tauri/commit/59089723fc20d66f3f305f2008adeb279bf87462) ([#14091](https://www.github.com/tauri-apps/tauri/pull/14091)) Added a config to set a data_directory relative to the app-specific data dir in JavaScript and `tauri.conf.json`. + +### Bug Fixes + +- [`08bda64c2`](https://www.github.com/tauri-apps/tauri/commit/08bda64c25008bd45c5b58d06ff14649081a2f5d) ([#14132](https://www.github.com/tauri-apps/tauri/pull/14132)) Fix `core > addPluginListener` failing on command permission check. + +## \[2.8.0] + +### New Features + +- [`68874c68c`](https://www.github.com/tauri-apps/tauri/commit/68874c68c566638b4c21a3aa67844d1bdaeb6dab) ([#13564](https://www.github.com/tauri-apps/tauri/pull/13564) by [@robertrpf](https://www.github.com/tauri-apps/tauri/../../robertrpf)) Add window focusable attribute and set_focusable API. +- [`5110a762e`](https://www.github.com/tauri-apps/tauri/commit/5110a762e9db978a28a15400bf76e3c864da2a86) ([#13830](https://www.github.com/tauri-apps/tauri/pull/13830) by [@Sky-walkerX](https://www.github.com/tauri-apps/tauri/../../Sky-walkerX)) Added `Window::setSimpleFullscreen`. + +### Enhancements + +- [`5ba1c3faa`](https://www.github.com/tauri-apps/tauri/commit/5ba1c3faa468073512bdb5035a01f7f99720fcf0) ([#13722](https://www.github.com/tauri-apps/tauri/pull/13722) by [@s00d](https://www.github.com/tauri-apps/tauri/../../s00d)) Added icon (icon and nativeIcon) support for Submenu: + + - In the Rust API (`tauri`), you can now set an icon for submenus via the builder and dedicated methods. + - In the JS/TS API (`@tauri-apps/api`), `SubmenuOptions` now has an `icon` field, and the `Submenu` class provides `setIcon` and `setNativeIcon` methods. + - Usage examples are added to the documentation and demo app. + + This is a backwards-compatible feature. Submenus can now display icons just like regular menu items. + +## \[2.7.0] + +### New Features + +- [`232265c70`](https://www.github.com/tauri-apps/tauri/commit/232265c70e1c213bbb3f84b5541ddc07d330fce1) ([#13209](https://www.github.com/tauri-apps/tauri/pull/13209) by [@kandrelczyk](https://www.github.com/tauri-apps/tauri/../../kandrelczyk)) Added `getBundleType` to the app module. + +### Enhancements + +- [`96391467e`](https://www.github.com/tauri-apps/tauri/commit/96391467e967c1e3c6475ce75166c58a326116a3) ([#13783](https://www.github.com/tauri-apps/tauri/pull/13783) by [@JosephBrooksbank](https://www.github.com/tauri-apps/tauri/../../JosephBrooksbank)) Allow events emitted with `emit` to be handled correctly by `listen` callbacks when in a mocked environment + +### Bug Fixes + +- [`152d971bc`](https://www.github.com/tauri-apps/tauri/commit/152d971bcd6c1fdc5716f7d5417dd4df5ce7479f) ([#13744](https://www.github.com/tauri-apps/tauri/pull/13744) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Expose `unregisterCallback`, `runCallback`, `callbacks` in `mockIPC` +- [`b821796ad`](https://www.github.com/tauri-apps/tauri/commit/b821796add33cca4de72f48882684af487936d02) ([#13810](https://www.github.com/tauri-apps/tauri/pull/13810) by [@asdolo](https://www.github.com/tauri-apps/tauri/../../asdolo)) Add missing `trafficLightPosition` TypeScript type definition + +## \[2.6.0] + +### New Features + +- [`50ebddaa2`](https://www.github.com/tauri-apps/tauri/commit/50ebddaa2d83033a393a176ba07ef28352b98210) ([#13319](https://www.github.com/tauri-apps/tauri/pull/13319) by [@kingsword09](https://www.github.com/tauri-apps/tauri/../../kingsword09)) Expose the `setAutoResize` API for webviews in `@tauri-apps/api`. +- [`267368fd4`](https://www.github.com/tauri-apps/tauri/commit/267368fd4f83e0a71dfb1b72a66d56592a2066bc) ([#13276](https://www.github.com/tauri-apps/tauri/pull/13276) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `Monitor.workArea` field. + +### Bug Fixes + +- [`23b9da75b`](https://www.github.com/tauri-apps/tauri/commit/23b9da75b91379cca9520bc53b10fdf39ebae241) ([#13324](https://www.github.com/tauri-apps/tauri/pull/13324) by [@kingsword09](https://www.github.com/tauri-apps/tauri/../../kingsword09)) Fixed path joining behavior where `path.join('', 'a')` incorrectly returns "/a" instead of "a". +- [`b985eaf0a`](https://www.github.com/tauri-apps/tauri/commit/b985eaf0a231ea570e36d686c665cddbc76ab4f6) ([#13306](https://www.github.com/tauri-apps/tauri/pull/13306) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Immediately unregister event listener when the unlisten function is called. + +### What's Changed + +- [`b5c549d18`](https://www.github.com/tauri-apps/tauri/commit/b5c549d1898ecdb712822c02dc665cc6771fbd07) ([#13325](https://www.github.com/tauri-apps/tauri/pull/13325) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) `transformCallback` now registers the callbacks inside `window.__TAURI_INTERNALS__.callbacks` instead of directly on `window['_{id}']` + +## \[2.5.0] + +### New Features + +- [`66e6325f4`](https://www.github.com/tauri-apps/tauri/commit/66e6325f43efa49ec2165c45afec911a1a14ecfb) ([#13136](https://www.github.com/tauri-apps/tauri/pull/13136)) Allow passing the callback as the parameter of constructor of `Channel` so you can use it like this `new Channel((message) => console.log(message))` +- [`ea36294cb`](https://www.github.com/tauri-apps/tauri/commit/ea36294cbca98f7725c91d1464fd92e77c89698a) ([#13208](https://www.github.com/tauri-apps/tauri/pull/13208)) Added `disableInputAccessoryView: bool` config for iOS. +- [`c1cd0a2dd`](https://www.github.com/tauri-apps/tauri/commit/c1cd0a2ddb5bc3e99451cbe399b5fc9f0035f571) ([#13090](https://www.github.com/tauri-apps/tauri/pull/13090)) macOS/iOS: add option to disable or enable link previews when building a webview (the webkit api has it enabled by default) + + - `WindowOptions::allowLinkPreview` + - `WebviewOptions::allowLinkPreview` +- [`b072e2b29`](https://www.github.com/tauri-apps/tauri/commit/b072e2b2967640ae4fa1af466ae878c156551edd) ([#9687](https://www.github.com/tauri-apps/tauri/pull/9687)) Add `preventOverflow` config option to prevent the window from overflowing the monitor size on creation +- [`dd4f13ce4`](https://www.github.com/tauri-apps/tauri/commit/dd4f13ce4b3cd89cde2fa3f18a063c272f215621) ([#13185](https://www.github.com/tauri-apps/tauri/pull/13185)) Added `app.setDockVisibility` for macOS. + +### Enhancements + +- [`b8f86669a`](https://www.github.com/tauri-apps/tauri/commit/b8f86669ab05f7dbdd15839a20999e63dc43bda6) ([#13145](https://www.github.com/tauri-apps/tauri/pull/13145)) `core.isTauri` now leverages `globalThis` instead of `window` in order to be used in unit tests. + +### Bug Fixes + +- [`66e6325f4`](https://www.github.com/tauri-apps/tauri/commit/66e6325f43efa49ec2165c45afec911a1a14ecfb) ([#13136](https://www.github.com/tauri-apps/tauri/pull/13136)) Fix `Channel`'s callback attached to `window` never cleaned up + +## \[2.4.1] + +### Enhancements + +- [`dd1372833`](https://www.github.com/tauri-apps/tauri/commit/dd137283341ce0e6aabfd158d07d77c6feeb920e) ([#13066](https://www.github.com/tauri-apps/tauri/pull/13066) by [@ahaoboy](https://www.github.com/tauri-apps/tauri/../../ahaoboy)) Add a generic to `emit` and `emitTo` functions for the `payload` instead of the previously used type (`unknown`). + +## \[2.4.0] + +### New Features + +- [`d8059bad3`](https://www.github.com/tauri-apps/tauri/commit/d8059bad3cc922dc369c39ca1cfa49aaec31322e) ([#12900](https://www.github.com/tauri-apps/tauri/pull/12900) by [@Simon-Laux](https://www.github.com/tauri-apps/tauri/../../Simon-Laux)) add `AppHandle.fetch_data_store_identifiers` and `AppHandle.remove_data_store` (macOS and iOS only) +- [`20c190691`](https://www.github.com/tauri-apps/tauri/commit/20c19069125c89b2d45a2127278c9ffc2df35fc2) ([#12821](https://www.github.com/tauri-apps/tauri/pull/12821) by [@Simon-Laux](https://www.github.com/tauri-apps/tauri/../../Simon-Laux)) Added `WindowOptions::javascriptDisabled` and `WebviewOptions::javascriptDisabled`. +- [`060de5bbd`](https://www.github.com/tauri-apps/tauri/commit/060de5bbdddca384e3965a8938d89840f27c581d) ([#12837](https://www.github.com/tauri-apps/tauri/pull/12837) by [@niladrix719](https://www.github.com/tauri-apps/tauri/../../niladrix719)) Added `getIdentifier()` function to get the application identifier configured in tauri.conf.json +- [`be2e6b85f`](https://www.github.com/tauri-apps/tauri/commit/be2e6b85fed226732b4a98f68cc5d72b4f8f5a13) ([#12944](https://www.github.com/tauri-apps/tauri/pull/12944) by [@Simon-Laux](https://www.github.com/tauri-apps/tauri/../../Simon-Laux)) Added `Window#isAlwaysOnTop` and `WebviewWindow#isAlwaysOnTop` methods. +- [`bcdd51025`](https://www.github.com/tauri-apps/tauri/commit/bcdd510254ebe37827e22a5ffeb944321361e97c) ([#13012](https://www.github.com/tauri-apps/tauri/pull/13012) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) The `path` basename and extname APIs now accept Android content URIs, such as the paths returned by the dialog plugin. + +### Bug Fixes + +- [`3a74dc8f3`](https://www.github.com/tauri-apps/tauri/commit/3a74dc8f3421112b1d0a32b6a432606b1f33cc25) ([#12935](https://www.github.com/tauri-apps/tauri/pull/12935) by [@tk103331](https://www.github.com/tauri-apps/tauri/../../tk103331)) Fix `Webview.close` always fail with command not found + +## \[2.3.0] + +### Enhancements + +- [`a2d36b8c3`](https://www.github.com/tauri-apps/tauri/commit/a2d36b8c34a8dcfc6736797ca5cd4665faf75e7e) ([#12181](https://www.github.com/tauri-apps/tauri/pull/12181) by [@bastiankistner](https://www.github.com/tauri-apps/tauri/../../bastiankistner)) Add an option to change the default background throttling policy (currently for WebKit only). + +## \[2.2.0] + +### New Features + +- [`020ea0556`](https://www.github.com/tauri-apps/tauri/commit/020ea05561348dcd6d2a7df358f8a5190f661ba2) ([#11661](https://www.github.com/tauri-apps/tauri/pull/11661) by [@ahqsoftwares](https://www.github.com/tauri-apps/tauri/../../ahqsoftwares)) Add badging APIs: + + - `Window/WebviewWindow::set_badge_count` for Linux, macOS and IOS. + - `Window/WebviewWindow::set_overlay_icon` for Windows Only. + - `Window/WebviewWindow::set_badge_label`for macOS Only. +- [`fc30b20be`](https://www.github.com/tauri-apps/tauri/commit/fc30b20bea125f647db00ca824663f8e1da4d61f) ([#11726](https://www.github.com/tauri-apps/tauri/pull/11726) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `TrayIcon.setShowMenuOnLeftClick` method and deprecate `TrayIcon.setMenuOnLeftClick` to match the Rust API. +- [`fc30b20be`](https://www.github.com/tauri-apps/tauri/commit/fc30b20bea125f647db00ca824663f8e1da4d61f) ([#11726](https://www.github.com/tauri-apps/tauri/pull/11726) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `TrayIconOptions.showMenuOnLeftClick` field and deprecate `TrayIconOptions.menuOnLeftClick` to match the Rust API. + +### Enhancements + +- [`fc30b20be`](https://www.github.com/tauri-apps/tauri/commit/fc30b20bea125f647db00ca824663f8e1da4d61f) ([#11726](https://www.github.com/tauri-apps/tauri/pull/11726) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add support for `TrayIconOptions.menuOnLeftClick` option and `TrayIcon.setMenuOnLeftClick` on Windows. + +### Bug Fixes + +- [`a16796a55`](https://www.github.com/tauri-apps/tauri/commit/a16796a55592cf5be80043edfbb630dd2e32efab) ([#12069](https://www.github.com/tauri-apps/tauri/pull/12069) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix `Channel` never calls `onmessage` in some cases +- [`12a48d1e2`](https://www.github.com/tauri-apps/tauri/commit/12a48d1e26a83c3915eaa0687b196fbc8f2d457a) ([#11741](https://www.github.com/tauri-apps/tauri/pull/11741) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix error when calling `PredefinedMenuItem.new` to create an `About` menu item that uses an `Image` instance for the about icon. +- [`12a48d1e2`](https://www.github.com/tauri-apps/tauri/commit/12a48d1e26a83c3915eaa0687b196fbc8f2d457a) ([#11741](https://www.github.com/tauri-apps/tauri/pull/11741) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix error when calling `IconMenuItem.new` using an `Image` instance for the icon. +- [`b63262cd4`](https://www.github.com/tauri-apps/tauri/commit/b63262cd4d6a3667ca1664607a0a5444ad79fe0e) ([#11724](https://www.github.com/tauri-apps/tauri/pull/11724) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Removed the generic in the type of the callback function argument in `mockIPC` which prevented its proper use in tests using TypeScript. +- [`a6e84f7d2`](https://www.github.com/tauri-apps/tauri/commit/a6e84f7d2c1d5fdc65901fce683502be3f47833f) ([#11835](https://www.github.com/tauri-apps/tauri/pull/11835) by [@ilittlebig](https://www.github.com/tauri-apps/tauri/../../ilittlebig)) Fix error where using `isAbsolute` would return `Command not found`. + +## \[2.1.1] + +### Bug Fixes + +- [`7f81f0523`](https://www.github.com/tauri-apps/tauri/commit/7f81f052365675721312aafba297a7b67fb872d2) Fix regression in `toLogical` and `toPhysical` for position types in `dpi` module returning incorrect `y` value. +- [`e8a50f6d7`](https://www.github.com/tauri-apps/tauri/commit/e8a50f6d760fad4529e7abb400302a1b487f11dd) ([#11645](https://www.github.com/tauri-apps/tauri/pull/11645)) Fix integer values of `BasDirectory.Home` and `BaseDirectory.Font` regression which broke path APIs in JS. + +## \[2.1.0] + +### New Features + +- [`5c4b83084`](https://www.github.com/tauri-apps/tauri/commit/5c4b830843ab085f8ff9db9e08d832223b027e4e) ([#11191](https://www.github.com/tauri-apps/tauri/pull/11191) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Improved support for `dpi` module types to allow these types to be used without manual conversions with `invoke`: + + - Added `SERIALIZE_TO_IPC_FN` const in `core` module which can be used to implement custom IPC serialization for types passed to `invoke`. + - Added `Size` and `Position` classes in `dpi` module. + - Implementd `SERIALIZE_TO_IPC_FN` method on `PhysicalSize`, `PhysicalPosition`, `LogicalSize` and `LogicalPosition` to convert it into a valid IPC-compatible value that can be deserialized correctly on the Rust side into its equivalent struct. +- [`4d545ab3c`](https://www.github.com/tauri-apps/tauri/commit/4d545ab3ca228c8a21b966b709f84a0da2864479) ([#11486](https://www.github.com/tauri-apps/tauri/pull/11486) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Added `Webview::setBackgroundColor`, `WebviewWindow::setBackgroundColor` APIs to set the window background color dynamically + and a `backgroundColor` window option to set the background color on window creation. +- [`cbc095ec5`](https://www.github.com/tauri-apps/tauri/commit/cbc095ec5fe7de29b5c9265576d4e071ec159c1c) ([#11451](https://www.github.com/tauri-apps/tauri/pull/11451) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `app > windows > devtools` config option and when creating the webview from JS, to enable or disable devtools for a specific webview. +- [`2a75c64b5`](https://www.github.com/tauri-apps/tauri/commit/2a75c64b5431284e7340e8743d4ea56a62c75466) ([#11469](https://www.github.com/tauri-apps/tauri/pull/11469) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Added `windowClassname` option, when constructing a `Webview` or `WebviewWindow`, to specify the name of the window class on Windows. + +### Bug Fixes + +- [`54cbf59b5`](https://www.github.com/tauri-apps/tauri/commit/54cbf59b5a572570a47237a3b5e6505f2a9e5d5d) ([#11441](https://www.github.com/tauri-apps/tauri/pull/11441) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix submenu created as a menu item instead of a submenu when created by using an object in the `items` field in the options object passed to `Menu.new` or `Submenu.new`. + +## \[2.0.3] + +### Bug Fixes + +- [`fbb45c674`](https://www.github.com/tauri-apps/tauri/commit/fbb45c674ca92fbbe04f1a8360e5f2e477dd4297) ([#11423](https://www.github.com/tauri-apps/tauri/pull/11423) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fixes `addPluginListener` not working. + +### What's Changed + +- [`2e88633ba`](https://www.github.com/tauri-apps/tauri/commit/2e88633ba4da8fc289c6d8a29c36f3327f9b576e) ([#11369](https://www.github.com/tauri-apps/tauri/pull/11369) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Remove references to no longer used `__TAURI_INTERNALS__.metadata.windows` and `__TAURI_INTERNALS__.metadata.webviews`. + +## \[2.0.2] + +### What's Changed + +- [`e968b3d25`](https://www.github.com/tauri-apps/tauri/commit/e968b3d2527b8edf7653e6cf7284dc4a8889b5fe) ([#11219](https://www.github.com/tauri-apps/tauri/pull/11219) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Actually publish package with the latest tag. + +## \[2.0.1] + +### What's Changed + +- [`be683e2ac`](https://www.github.com/tauri-apps/tauri/commit/be683e2ac36df9c51a5c050d9d500247bd019090) ([#11199](https://www.github.com/tauri-apps/tauri/pull/11199) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Publish package with the latest NPM tag. + +## \[2.0.0] + +### What's Changed + +- [`637285790`](https://www.github.com/tauri-apps/tauri/commit/6372857905ae9c0aedb7f482ddf6cf9f9836c9f2) Promote to v2 stable! + +## \[2.0.0-rc.6] + +### New Features + +- [`9014a3f17`](https://www.github.com/tauri-apps/tauri/commit/9014a3f1765ca406ea5c3e5224267a79c52cd53d) ([#11066](https://www.github.com/tauri-apps/tauri/pull/11066) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `WebviewWindow.clearAllBrowsingData` and `Webview.clearAllBrowsingData` to clear the webview browsing data. +- [`95df53a2e`](https://www.github.com/tauri-apps/tauri/commit/95df53a2ed96873cd35a4b14a5e312d07e4e3004) ([#11143](https://www.github.com/tauri-apps/tauri/pull/11143) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Add the ability to set theme dynamically using `Window.setTheme` or `setTheme` function from the `app` module +- [`d9d2502b4`](https://www.github.com/tauri-apps/tauri/commit/d9d2502b41e39efde679e30c8955006e2ba9ea64) ([#11140](https://www.github.com/tauri-apps/tauri/pull/11140) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `Webview.hide` and `Webview.show` methods. +- [`de7414aab`](https://www.github.com/tauri-apps/tauri/commit/de7414aab935e45540594ea930eb60bae4dbc979) ([#11154](https://www.github.com/tauri-apps/tauri/pull/11154) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `Window::setEnabled` and `Window::isEnabled` methods + +### Bug Fixes + +- [`948772a65`](https://www.github.com/tauri-apps/tauri/commit/948772a657eb3caf20843628abac9109e3b67d41) ([#11114](https://www.github.com/tauri-apps/tauri/pull/11114) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Change the `button_state` tray event field to camelCase `buttonState`. + +### Breaking Changes + +- [`0b4495996`](https://www.github.com/tauri-apps/tauri/commit/0b4495996d3131a5ee80fbb2c71a28203e491ee7) ([#11121](https://www.github.com/tauri-apps/tauri/pull/11121) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Simplified emitted tray event JS value and updated `TrayIconEvent` type definition to match it. + +## \[2.0.0-rc.5] + +### New Features + +- [`ddf69157b`](https://www.github.com/tauri-apps/tauri/commit/ddf69157b54249f3321ca72db6703812019f1ab9) ([#11031](https://www.github.com/tauri-apps/tauri/pull/11031) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `toPhysical` method on `LogicalPositon` and `LogicalSize` classes. + +## \[2.0.0-rc.4] + +### Enhancements + +- [`f81929e25`](https://www.github.com/tauri-apps/tauri/commit/f81929e25104aa1091e464bd012c80649dedf9e5) ([#10799](https://www.github.com/tauri-apps/tauri/pull/10799) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Added `PermissionState`, `checkPermissions` and `requestPermissions` base APIs to the core module, designed for plugin authors to extend. + +### Bug Fixes + +- [`fbe76a955`](https://www.github.com/tauri-apps/tauri/commit/fbe76a955a63af9fb33f66d5f747caf858cf179b) ([#10797](https://www.github.com/tauri-apps/tauri/pull/10797) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Uint8Arrays and ArrayBuffers are now properly serialized as an array of numbers. + +## \[2.0.0-rc.3] + +### What's Changed + +- [`f4d5241b3`](https://www.github.com/tauri-apps/tauri/commit/f4d5241b377d0f7a1b58100ee19f7843384634ac) ([#10731](https://www.github.com/tauri-apps/tauri/pull/10731) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Update documentation icon path. + +## \[2.0.0-rc.2] + +### Bug Fixes + +- [`c689521a7`](https://www.github.com/tauri-apps/tauri/commit/c689521a7674b6562b5dfd4f5cacd12138d99d85) ([#10681](https://www.github.com/tauri-apps/tauri/pull/10681) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix tslib path in dist. + +## \[2.0.0-rc.1] + +### Breaking Changes + +- [`b6dca99ff`](https://www.github.com/tauri-apps/tauri/commit/b6dca99fff73816a39380b288c299b47b493cfdb) ([#10630](https://www.github.com/tauri-apps/tauri/pull/10630) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Changed `WebviewWindow.getAll`, `WebviewWindow.getByLabel`, `getAllWebviewWindows`, + `Window.getAll`, `Window.getByLabel`, `getAllWindows`, + `Webview.getAll`, `Webview.getByLabel`, `getAllWebviews` + to be async so their return value are synchronized with the state from the Rust side, + meaning new and destroyed windows are reflected. + +## \[2.0.0-rc.0] + +### Changes + +- Promoted to RC! + +## \[2.0.0-beta.16] + +### New Features + +- [`da25f7353`](https://www.github.com/tauri-apps/tauri/commit/da25f7353070477ba969851e974379d7666d6806) ([#10242](https://www.github.com/tauri-apps/tauri/pull/10242) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add APIs to enable setting window size constraints separately: + + - Added `WindowSizeConstraints` interface in `window` and `webviewWindow` modules. + - Added `Window.setSizeConstraints` and `WebviewWindow.setSizeConstraints` + +### Bug Fixes + +- [`3c17fb64f`](https://www.github.com/tauri-apps/tauri/commit/3c17fb64fd822597d5cc16ee7e7b3f9e1023637b) ([#10277](https://www.github.com/tauri-apps/tauri/pull/10277) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix `Webview.reparent` pointing to `set_webview_focus` instead of `reparent` Rust API +- [`da25f7353`](https://www.github.com/tauri-apps/tauri/commit/da25f7353070477ba969851e974379d7666d6806) ([#10242](https://www.github.com/tauri-apps/tauri/pull/10242) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Apply `minWidth`, `minHieght`, `maxWidth` and `maxHeight` constraints separately, which fixes a long standing bug where these constraints were never applied unless width and height were constrained together. + +## \[2.0.0-beta.15] + +### New Features + +- [`7bc6a2a1d`](https://www.github.com/tauri-apps/tauri/commit/7bc6a2a1d6d2c5406d91cac94d33bce76443c28f) ([#9788](https://www.github.com/tauri-apps/tauri/pull/9788) by [@pewsheen](https://www.github.com/tauri-apps/tauri/../../pewsheen)) Add a new method to set title bar style dynamically on macOS. + +### Enhancements + +- [`080b6e127`](https://www.github.com/tauri-apps/tauri/commit/080b6e12720b89d839c686d7067cc94d276ed7e4) ([#10246](https://www.github.com/tauri-apps/tauri/pull/10246) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Use `EventName` on `Window`, `Webview` and `WebviewWindow`'s `once` so you can get auto complete for tauri's built-in events + +### Bug Fixes + +- [`080b6e127`](https://www.github.com/tauri-apps/tauri/commit/080b6e12720b89d839c686d7067cc94d276ed7e4) ([#10246](https://www.github.com/tauri-apps/tauri/pull/10246) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix `once` doesn't detached after one callback if event handler throws + +### Breaking Changes + +- [`261c9f942`](https://www.github.com/tauri-apps/tauri/commit/261c9f942de9a598b5c6cc504de6bddd1306113b) ([#10170](https://www.github.com/tauri-apps/tauri/pull/10170) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Renamed drag and drop events in `TauriEvent` enum to better convey when they are triggered: + + - `TauriEvent.DRAG` -> `TauriEvent.DRAG_ENTER` + - `TauriEvent.DROP_OVER` -> `TauriEvent.DRAG_OVER` + - `TauriEvent.DROP` -> `TauriEvent.DRAG_DROP` + - `TauriEvent.DROP_CANCELLED` -> `TauriEvent::DRAG_LEAVE` + + Also the `type` field values in `Window/Webview/WebviewWindow.onDropEvent` and `DragDropEvent` have changed: + + - `dragged` -> `enter` + - `dragOver` -> `over` + - `dropped` -> `drop` + - `cancelled` -> `leave` +- [`2b1ceb40d`](https://www.github.com/tauri-apps/tauri/commit/2b1ceb40d345aef42dd79438fa69ca7989ee0194) ([#10229](https://www.github.com/tauri-apps/tauri/pull/10229) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Renamed the JS `getCurrent` and `getAll` functions to a clearer name to avoid ambiguity: + + - `getCurrent` in `window` module has been renamed to `getCurrentWindow` + - `getCurrent` in `webview` module has been renamed to `getCurrentWebview` + - `getCurrent` in `webviewWindow` module has been renamed to `getCurrentWebviewWindow` + - `getAll` in `window` module has been renamed to `getAllWindows` + - `getAll` in `webview` module has been renamed to `getAllWebviews` + - `getAll` in `webviewWindow` module has been renamed to `getAllWebviewWindows` + +## \[2.0.0-beta.14] + +### New Features + +- [`148f04887`](https://www.github.com/tauri-apps/tauri/commit/148f048871caee21498b236c058b8890f2b66cc7) ([#9979](https://www.github.com/tauri-apps/tauri/pull/9979)) Add `defaultWindowIcon` to the JS `app` module to retrieve the default window icon in JS. + +### Bug Fixes + +- [`c98f385cb`](https://www.github.com/tauri-apps/tauri/commit/c98f385cb5da4d72968df24b1fc0b58212d59653) ([#10044](https://www.github.com/tauri-apps/tauri/pull/10044)) Export `mocks` module in `@tauri-apps/api` npm package. + +## \[2.0.0-beta.13] + +### Breaking Changes + +- [`c4410daa8`](https://www.github.com/tauri-apps/tauri/commit/c4410daa85616340e911c8243fdaa69e6906fd49)([#9777](https://www.github.com/tauri-apps/tauri/pull/9777)) This release contains breaking changes to the tray event structure because of newly added events: + + - Changed `TrayIconEvent` to be an enum instead of a struct. + - Added `MouseButtonState` and `MouseButton` enums. + - Removed `ClickType` enum and replaced it with `MouseButton` enum. + - Added `MouseButtonState` enum. + +## \[2.0.0-beta.12] + +### New Features + +- [`ec0e092ec`](https://www.github.com/tauri-apps/tauri/commit/ec0e092ecd23b547c756c7476f23a0d95be6db80)([#9770](https://www.github.com/tauri-apps/tauri/pull/9770)) Add `monitorFromPoint` function in `window` module to get the monitor from a given point. + +## \[2.0.0-beta.11] + +### Bug Fixes + +- [`aa080696e`](https://www.github.com/tauri-apps/tauri/commit/aa080696e0952abff416dd9088d519eaf2587a3a)([#9618](https://www.github.com/tauri-apps/tauri/pull/9618)) Fix `isTauri` incorrect return type. + +## \[2.0.0-beta.10] + +### New Features + +- [`477bb8cd4`](https://www.github.com/tauri-apps/tauri/commit/477bb8cd4ea88ade3f6c1f268ad1701a68150161)([#9297](https://www.github.com/tauri-apps/tauri/pull/9297)) Add `cursorPosition` function in `window` module to get the current cursor position. + +## \[2.0.0-beta.9] + +### New Features + +- [`70c51371e`](https://www.github.com/tauri-apps/tauri/commit/70c51371e01184223312de3dba8030394a5a9406)([#9539](https://www.github.com/tauri-apps/tauri/pull/9539)) Add `isTauri` function in `core` module to check whether running inside tauri or not. + +### Bug Fixes + +- [`be7eab209`](https://www.github.com/tauri-apps/tauri/commit/be7eab209c60c45e140f7bcb4bab1037d62d4c03)([#9486](https://www.github.com/tauri-apps/tauri/pull/9486)) Set the `exports > types` package.json field. +- [`cf615e8e4`](https://www.github.com/tauri-apps/tauri/commit/cf615e8e4d5008ee1ac3f77e530ba26fb91e8977)([#9463](https://www.github.com/tauri-apps/tauri/pull/9463)) Fixes a bug when processing channel messages out of order. +- [`35b25f7e5`](https://www.github.com/tauri-apps/tauri/commit/35b25f7e5c0fe03af4ed3582e22a626863f035f0)([#9530](https://www.github.com/tauri-apps/tauri/pull/9530)) Do not use JS optional chaining to prevent script errors on older webviews such as macOS 10.14. + +## \[2.0.0-beta.8] + +### New Features + +- [`58a7a552d`](https://www.github.com/tauri-apps/tauri/commit/58a7a552d739b77b71d61af11c53f7f2dc7a6e7e)([#9378](https://www.github.com/tauri-apps/tauri/pull/9378)) Added the `set_zoom` function to the webview API. +- [`58a7a552d`](https://www.github.com/tauri-apps/tauri/commit/58a7a552d739b77b71d61af11c53f7f2dc7a6e7e)([#9378](https://www.github.com/tauri-apps/tauri/pull/9378)) Add `zoom_hotkeys_enabled` to enable browser native zoom controls on creating webviews. + +### Bug Fixes + +- [`48a7a78f8`](https://www.github.com/tauri-apps/tauri/commit/48a7a78f8094d08e5e403e88050391642d29151b)([#9376](https://www.github.com/tauri-apps/tauri/pull/9376)) Fix `Window/Webview/WebviewWindow.setSize`, `Window/Webview/WebviewWindow.setPostion`, `Window/WebviewWindow.setMinSize`, `Window/WebviewWindow.setMaxSize`, `Window/WebviewWindow.setCursorPosition` and `Menu/Submenu.popup` methods failing with invalid args. + +## \[2.0.0-beta.7] + +### Bug Fixes + +- [`c33f6e6cf`](https://www.github.com/tauri-apps/tauri/commit/c33f6e6cf35a0d34b5598875a2e5b642a01c8b38)([#9211](https://www.github.com/tauri-apps/tauri/pull/9211)) Re-added the `TauriEvent.WINDOW_CREATED` (`tauri://window-created`) event. + +### Breaking Changes + +- [`06833f4fa`](https://www.github.com/tauri-apps/tauri/commit/06833f4fa8e63ecc55fe3fc874a9e397e77a5709)([#9100](https://www.github.com/tauri-apps/tauri/pull/9100)) Rename `FileDrop` to `DragDrop` on structs, enums and enum variants. Also renamed `file_drop` to `drag_drop` on fields and function names. + +## \[2.0.0-beta.6] + +### New Features + +- [`acdd76833`](https://www.github.com/tauri-apps/tauri/commit/acdd76833db6d81f4012418133d0042220de100b)([#9155](https://www.github.com/tauri-apps/tauri/pull/9155)) Add `TrayIcon.getById` and `TrayIcon.removeById` static methods. + +### Enhancements + +- [`ea0242db4`](https://www.github.com/tauri-apps/tauri/commit/ea0242db4aa6c127d2bb4a2e275000ba47c9e68c)([#9179](https://www.github.com/tauri-apps/tauri/pull/9179)) The `Image` constructor is now public (for internal use only). + +### Bug Fixes + +- [`379cc2b35`](https://www.github.com/tauri-apps/tauri/commit/379cc2b3547395474d4b66b4222679cf4538428d)([#9165](https://www.github.com/tauri-apps/tauri/pull/9165)) Fix `basename(path, 'ext')` JS API when removing all occurances of `ext` where it should only remove the last one. + +### Breaking Changes + +- [`ea0242db4`](https://www.github.com/tauri-apps/tauri/commit/ea0242db4aa6c127d2bb4a2e275000ba47c9e68c)([#9179](https://www.github.com/tauri-apps/tauri/pull/9179)) `Image::rgba()` now returns `Promise`. +- [`ea0242db4`](https://www.github.com/tauri-apps/tauri/commit/ea0242db4aa6c127d2bb4a2e275000ba47c9e68c)([#9179](https://www.github.com/tauri-apps/tauri/pull/9179)) Removed `width` and `height` methods on the JS `Image` class, use `size` instead. + +## \[2.0.0-beta.5] + +### Breaking Changes + +- [`db0a24a97`](https://www.github.com/tauri-apps/tauri/commit/db0a24a973191752aeecfbd556faa254b0f17e79)([#9132](https://www.github.com/tauri-apps/tauri/pull/9132)) Remove the `Image.fromPngBytes` and `Image.fromIcoBytes` APIs. Use `Image.fromBytes` instead. + +## \[2.0.0-beta.4] + +### New Features + +- [`d1e77acd8`](https://www.github.com/tauri-apps/tauri/commit/d1e77acd8dfdf554b90b542513a58a2de1ef2360)([#9011](https://www.github.com/tauri-apps/tauri/pull/9011)) Add a new `Image` type in Rust and JS. + +### Enhancements + +- [`e62ca4ee9`](https://www.github.com/tauri-apps/tauri/commit/e62ca4ee95f4308a6ad128d0f100c85634e28223)([#9070](https://www.github.com/tauri-apps/tauri/pull/9070)) Added a mechanism to preserve channel message order. + +## \[2.0.0-beta.3] + +### New Features + +- [`fdcaf935`](https://www.github.com/tauri-apps/tauri/commit/fdcaf935fa75ecfa2806939c4faad4fe9e880386)([#8939](https://www.github.com/tauri-apps/tauri/pull/8939)) Added the `reparent` function to the webview API. + +## \[2.0.0-beta.2] + +### Breaking Changes + +- [`361ec37f`](https://www.github.com/tauri-apps/tauri/commit/361ec37fd4a5caa5b6630b9563ef079f53c6c336)([#8932](https://www.github.com/tauri-apps/tauri/pull/8932)) Removed the `unityUri` option from the progress bar state, no longer required. + +## \[2.0.0-beta.1] + +### New Features + +- [`16e550ec`](https://www.github.com/tauri-apps/tauri/commit/16e550ec1503765158cdc3bb2a20e70ec710e981)([#8844](https://www.github.com/tauri-apps/tauri/pull/8844)) Add a new `webviewWindow` module that exports `WebviewWindow` class and related methods such as `getCurrent` and `getAll`. +- [`16e550ec`](https://www.github.com/tauri-apps/tauri/commit/16e550ec1503765158cdc3bb2a20e70ec710e981)([#8844](https://www.github.com/tauri-apps/tauri/pull/8844)) Add `Window.onFileDropEvent` method. + +### Breaking Changes + +- [`16e550ec`](https://www.github.com/tauri-apps/tauri/commit/16e550ec1503765158cdc3bb2a20e70ec710e981)([#8844](https://www.github.com/tauri-apps/tauri/pull/8844)) Renamed the following enum variants of `TauriEvent` enum: + + - `TauriEvent.WEBVIEW_FILE_DROP` -> `TauriEvent.FILE_DROP` + - `TauriEvent.WEBVIEW_FILE_DROP_HOVER` -> `TauriEvent.FILE_DROP_HOVER` + - `TauriEvent.WEBVIEW_FILE_DROP_CANCELLED` -> `TauriEvent.FILE_DROP_CANCELLED` +- [`16e550ec`](https://www.github.com/tauri-apps/tauri/commit/16e550ec1503765158cdc3bb2a20e70ec710e981)([#8844](https://www.github.com/tauri-apps/tauri/pull/8844)) Move `WebviewWindow` class from `webview` module to a new `webviewWindow` module. + +## \[2.0.0-beta.0] + +### New Features + +- [`74a2a603`](https://www.github.com/tauri-apps/tauri/commit/74a2a6036a5e57462f161d728cbd8a6f121028ca)([#8661](https://www.github.com/tauri-apps/tauri/pull/8661)) Implement access control list for IPC usage. +- [`a093682d`](https://www.github.com/tauri-apps/tauri/commit/a093682d2df7169b024bb4f736c7f1fd2ea8b327)([#8621](https://www.github.com/tauri-apps/tauri/pull/8621)) Added `emitTo` api to `event` module which is equivalent to the rust `emit_to` method. Also added `emitTo` method on `Window`, `Webivew` and `WebviewWindow` classes. +- [`a2fc3a63`](https://www.github.com/tauri-apps/tauri/commit/a2fc3a63579ca739646d696870cbecbb3a169d33)([#8657](https://www.github.com/tauri-apps/tauri/pull/8657)) Add `visibleOnAllWorkspaces` option when creating the window in JS and `Window.setVisibleOnAllWorkspaces` method. +- [`7f033f6d`](https://www.github.com/tauri-apps/tauri/commit/7f033f6dcd54c69a4193765a5c1584755ba92c61)([#8537](https://www.github.com/tauri-apps/tauri/pull/8537)) Add `Window.startResizeDragging`. +- [`9eaeb5a8`](https://www.github.com/tauri-apps/tauri/commit/9eaeb5a8cd95ae24b5e66205bdc2763cb7f965ce)([#8622](https://www.github.com/tauri-apps/tauri/pull/8622)) Add `parent` option when creating a window. +- [`af610232`](https://www.github.com/tauri-apps/tauri/commit/af6102327376884364b2075b468bdf08ee0d02aa)([#8710](https://www.github.com/tauri-apps/tauri/pull/8710)) Added `Window::destroy` to force close a window. +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) Added support to multiwebview via the new `window` and `webview` modules. + +### Breaking Changes + +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) Removed event callback's `windowLabel`. +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) The event target is now an object so you can target either a window or a webview. +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) Moved webview-specific APIs from the `Window` class to the `Webview` class. +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) Renamed `TauriEvent.WINDOW_FILE_DROP` to `TauriEvent.WEBVIEW_FILE_DROP`, `TauriEvent.WINDOW_FILE_DROP_HOVER` to `TauriEvent.WEBVIEW_FILE_DROP_HOVER` and `TauriEvent.WINDOW_FILE_DROP_CANCELLED` to `TauriEvent.WEBVIEW_FILE_DROP_CANCELLED`. +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) Added back the `WebviewWindow` API that exposes functionality of a window that hosts a single webview. The dedicated `Window` and `Webview` types are exposed for multiwebview features. +- [`af610232`](https://www.github.com/tauri-apps/tauri/commit/af6102327376884364b2075b468bdf08ee0d02aa)([#8710](https://www.github.com/tauri-apps/tauri/pull/8710)) `Window::close` now triggers a close requested event instead of forcing the window to be closed. + +## \[2.0.0-alpha.14] + +- [`97e33412`](https://www.github.com/tauri-apps/tauri/commit/97e334129956159bbd60e1c531b6acd3bc6139a6)([#8534](https://www.github.com/tauri-apps/tauri/pull/8534)) `mockIPC` and `mockWindows` no longer crash if `window.__TAURI_INTERNALS__` is undefined. + +## \[2.0.0-alpha.13] + +### New Features + +- [`428ea652`](https://www.github.com/tauri-apps/tauri/commit/428ea6524c70545be33aac96d7c22b21f25caa4c)([#8370](https://www.github.com/tauri-apps/tauri/pull/8370)) Exposed `Resource` class which should be extended for Rust-backed resources created through `tauri::Manager::resources_table`. + +### Bug Fixes + +- [`ef21b681`](https://www.github.com/tauri-apps/tauri/commit/ef21b681e237a80592c9118b9c023c1d57231bac)([#8391](https://www.github.com/tauri-apps/tauri/pull/8391)) Fix a regression where typescript could not find types when using `"moduleResolution": "node"` +- [`46451aee`](https://www.github.com/tauri-apps/tauri/commit/46451aee1318f63a6cd861a12b63929b38c64eb6)([#8268](https://www.github.com/tauri-apps/tauri/pull/8268)) Add top-level `main`, `module` and `types` fields in `package.json` to be compliant with typescripts's `"moduleResolution": "node"` + +### Breaking Changes + +- [`c2ad4d28`](https://www.github.com/tauri-apps/tauri/commit/c2ad4d28c481b2d7ed643458db56210cd44a2e0c)([#8273](https://www.github.com/tauri-apps/tauri/pull/8273)) Changed former `tauri` module from `primitives` to `core`. + +## \[2.0.0-alpha.12] + +### New Features + +- [`f93148ea`](https://www.github.com/tauri-apps/tauri/commit/f93148eac05a1428e038bd9351a8149b2464ff4c)([#7709](https://www.github.com/tauri-apps/tauri/pull/7709)) Add `tray` and `menu` modules to create and manage tray icons and menus from Javascript. + +### Enhancements + +- [`b7add750`](https://www.github.com/tauri-apps/tauri/commit/b7add750ef9f32d959de613ab35063ff240281c2)([#8204](https://www.github.com/tauri-apps/tauri/pull/8204)) Added `position` field to the `FileDropEvent` payload. + +## \[2.0.0-alpha.11] + +### Bug Fixes + +- [`822bf15d`](https://www.github.com/tauri-apps/tauri/commit/822bf15d6b258556b689ca55ac2ac224897e913a)([#8130](https://www.github.com/tauri-apps/tauri/pull/8130)) Fix tslib missing in the distributed api package. + +## \[2.0.0-alpha.10] + +### Enhancements + +- [`c6c59cf2`](https://www.github.com/tauri-apps/tauri/commit/c6c59cf2373258b626b00a26f4de4331765dd487) Pull changes from Tauri 1.5 release. + +### Bug Fixes + +- [`287066b2`](https://www.github.com/tauri-apps/tauri/commit/287066b279f503dd09bfd43d5da37d1f471451fb)([#8071](https://www.github.com/tauri-apps/tauri/pull/8071)) No longer crashing in tests without mocks when `clearMocks` is defined in `afterEach` hook. + +## \[2.0.0-alpha.9] + +### New Features + +- [`c1ec0f15`](https://www.github.com/tauri-apps/tauri/commit/c1ec0f155118527361dd5645d920becbc8afd569)([#7933](https://www.github.com/tauri-apps/tauri/pull/7933)) Added `setAlwaysOnBottom` function on `Window` and the `alwaysOnBottom` option when creating a window. +- [`fb10b879`](https://www.github.com/tauri-apps/tauri/commit/fb10b87970a43320ef4d14564f45e7579b774eaf)([#8039](https://www.github.com/tauri-apps/tauri/pull/8039)) Add the `app` module back. +- [`ed32257d`](https://www.github.com/tauri-apps/tauri/commit/ed32257d044f90b5eb15053efd1667125def2d2b)([#7794](https://www.github.com/tauri-apps/tauri/pull/7794)) On Windows, add `Effect.Tabbed`,`Effect.TabbedDark` and `Effect.TabbedLight` effects. +- [`c9a9246c`](https://www.github.com/tauri-apps/tauri/commit/c9a9246c37bdf190661355c8ee406dac6c427344)([#8007](https://www.github.com/tauri-apps/tauri/pull/8007)) Add the `window` module back. +- [`c085adda`](https://www.github.com/tauri-apps/tauri/commit/c085addab58ba851398373c6fd13f9cb026d71e8)([#8009](https://www.github.com/tauri-apps/tauri/pull/8009)) Added the `setProgressBar` API on the `Window` class. + +### What's Changed + +- [`5c0eeb40`](https://www.github.com/tauri-apps/tauri/commit/5c0eeb40c1003583290ff3aebfa02e2b5f5b9c41)([#7638](https://www.github.com/tauri-apps/tauri/pull/7638)) Updated minimum Node.js version to 18. + +### Breaking Changes + +- [`a63e71f9`](https://www.github.com/tauri-apps/tauri/commit/a63e71f9799e9bbc82521d2f17b5238fbf690e89)([#7942](https://www.github.com/tauri-apps/tauri/pull/7942)) Changed `tauri` module to `primitives` and removed the undocumented `invoke` export from the root module. + +## \[2.0.0-alpha.8] + +### Breaking Changes + +- [`d5074af5`](https://www.github.com/tauri-apps/tauri/commit/d5074af562b2b5cb6c5711442097c4058af32db6)([#7801](https://www.github.com/tauri-apps/tauri/pull/7801)) The custom protocol on Android now uses the `http` scheme instead of `https`. + +## \[2.0.0-alpha.7] + +### Breaking Changes + +- [`4cb51a2d`](https://www.github.com/tauri-apps/tauri/commit/4cb51a2d56cfcae0749062c79ede5236bd8c02c2)([#7779](https://www.github.com/tauri-apps/tauri/pull/7779)) The custom protocol on Windows now uses the `http` scheme instead of `https`. + +## \[2.0.0-alpha.6] + +### New Features + +- [`4af5c5a8`](https://www.github.com/tauri-apps/tauri/commit/4af5c5a8293263c16f8a65e8d232f2de52f41701)([#7170](https://www.github.com/tauri-apps/tauri/pull/7170)) Change the IPC call to align with the new format for the custom protocol based API. + +## \[2.0.0-alpha.5] + +### New Features + +- [`e0f0dce2`](https://www.github.com/tauri-apps/tauri/commit/e0f0dce220730e2822fc202463aedf0166145de7)([#6442](https://www.github.com/tauri-apps/tauri/pull/6442)) Added the `windowEffects` option when creating a window and `setWindowEffects` method to change it at runtime. + +### Enhancements + +- [`9e3a18e0`](https://www.github.com/tauri-apps/tauri/commit/9e3a18e04672edad15d0ec654bd8632544871967)([#7132](https://www.github.com/tauri-apps/tauri/pull/7132)) Expose the window target option on event APIs. +- [`6d3f3138`](https://www.github.com/tauri-apps/tauri/commit/6d3f3138b9e2f41cda712c7d9caba0f0e65dfd3c)([#7160](https://www.github.com/tauri-apps/tauri/pull/7160)) Changed `sep` and `delimiter` from `path` module into functions to fix import in frameworks like `next.js` +- [`4652c446`](https://www.github.com/tauri-apps/tauri/commit/4652c446b361a801252bcf45e9da39813bf85482)([#7144](https://www.github.com/tauri-apps/tauri/pull/7144)) Add `tempDir` function to `path` module + +## \[2.0.0-alpha.4] + +- [`0ab5f40d`](https://www.github.com/tauri-apps/tauri/commit/0ab5f40d3a4207f20e4440587b41c4e78f91d233)([#6813](https://www.github.com/tauri-apps/tauri/pull/6813)) Add channel API for sending data across the IPC. +- [`3245d14b`](https://www.github.com/tauri-apps/tauri/commit/3245d14b9eb256a5c5675c7030bac7082855df47)([#6895](https://www.github.com/tauri-apps/tauri/pull/6895)) Moved the `app` feature to its own plugin in the plugins-workspace repository. +- [`09376af5`](https://www.github.com/tauri-apps/tauri/commit/09376af59424cc27803fa2820d2ac0d4cdc90a6d)([#6704](https://www.github.com/tauri-apps/tauri/pull/6704)) Moved the `cli` feature to its own plugin in the plugins-workspace repository. +- [`2d5378bf`](https://www.github.com/tauri-apps/tauri/commit/2d5378bfc1ba817ee2f331b41738a90e5997e5e8)([#6717](https://www.github.com/tauri-apps/tauri/pull/6717)) Moved the dialog APIs to its own plugin in the plugins-workspace repository. +- [`39f1b04f`](https://www.github.com/tauri-apps/tauri/commit/39f1b04f7be4966488484829cd54c8ce72a04200)([#6943](https://www.github.com/tauri-apps/tauri/pull/6943)) Moved the `event` JS APIs to a plugin. +- [`fc4d687e`](https://www.github.com/tauri-apps/tauri/commit/fc4d687ef0ef2ea069ed73c40916da733b5dcb8f)([#6716](https://www.github.com/tauri-apps/tauri/pull/6716)) Moved the file system APIs to its own plugin in the plugins-workspace repository. +- [`f78a3783`](https://www.github.com/tauri-apps/tauri/commit/f78a378344bbec48533641661d865920a8f46f8f)([#6742](https://www.github.com/tauri-apps/tauri/pull/6742)) Moved the `http` feature to its own plugin in the plugins-workspace repository. +- [`29ce9ce2`](https://www.github.com/tauri-apps/tauri/commit/29ce9ce2ce7dfb260d556d5cffd075e8fe06660c)([#6902](https://www.github.com/tauri-apps/tauri/pull/6902)) Moved the `os` feature to its own plugin in the plugins-workspace repository. +- [`60cf9ed2`](https://www.github.com/tauri-apps/tauri/commit/60cf9ed2fcd7be4df41e86cf18735efe9b6cb254)([#6905](https://www.github.com/tauri-apps/tauri/pull/6905)) Moved the `process` feature to its own plugin in the plugins-workspace repository. +- [`96639ca2`](https://www.github.com/tauri-apps/tauri/commit/96639ca239c9e4f75142fc07868ac46822111cff)([#6749](https://www.github.com/tauri-apps/tauri/pull/6749)) Moved the `shell` functionality to its own plugin in the plugins-workspace repository. +- [`b072daa3`](https://www.github.com/tauri-apps/tauri/commit/b072daa3bd3e38b808466666619ddb885052c5b2)([#6919](https://www.github.com/tauri-apps/tauri/pull/6919)) Moved the `updater` feature to its own plugin in the plugins-workspace repository. +- [`cebd7526`](https://www.github.com/tauri-apps/tauri/commit/cebd75261ac71b98976314a450cb292eeeec1515)([#6728](https://www.github.com/tauri-apps/tauri/pull/6728)) Moved the `clipboard` feature to its own plugin in the plugins-workspace repository. +- [`3f17ee82`](https://www.github.com/tauri-apps/tauri/commit/3f17ee82f6ff21108806edb7b00500b8512b8dc7)([#6737](https://www.github.com/tauri-apps/tauri/pull/6737)) Moved the `global-shortcut` feature to its own plugin in the plugins-workspace repository. +- [`9a79dc08`](https://www.github.com/tauri-apps/tauri/commit/9a79dc085870e0c1a5df13481ff271b8c6cc3b78)([#6947](https://www.github.com/tauri-apps/tauri/pull/6947)) Moved the `window` JS APIs to its own plugin in the plugins-workspace repository. + +## \[2.0.0-alpha.3] + +- Overload the dialog `open` function to have better TS result types. + - [1eacd51d](https://www.github.com/tauri-apps/tauri/commit/1eacd51d185ba69a3c3cb2cc93c792e2d5929843) overloaded the open function for convenient type inference ([#5619](https://www.github.com/tauri-apps/tauri/pull/5619)) on 2023-04-07 + +## \[2.0.0-alpha.2] + +- Added `raw` encoding option to read stdout and stderr raw bytes. + - [f992e7f5](https://www.github.com/tauri-apps/tauri/commit/f992e7f58bf975c654a3daf36780b31a32bac064) chore(changes): readd change file on 2023-04-03 +- Removed shell's `Command` constructor and added the `Command.create` static function instead. + - [509d4678](https://www.github.com/tauri-apps/tauri/commit/509d4678b12816c1dd08a9a5efa71ba556d91c27) Support sending raw byte data to the "data" event for child command's stdout and stderr ([#5789](https://www.github.com/tauri-apps/tauri/pull/5789)) on 2023-03-31 + +## \[2.0.0-alpha.1] + +- Added the `shadow` option when creating a window and `setShadow` function. + - [a81750d7](https://www.github.com/tauri-apps/tauri/commit/a81750d779bc72f0fdb7de90b7fbddfd8049b328) feat(core): add shadow APIs ([#6206](https://www.github.com/tauri-apps/tauri/pull/6206)) on 2023-02-08 + +## \[2.0.0-alpha.0] + +- First mobile alpha release! + - [fa3a1098](https://www.github.com/tauri-apps/tauri/commit/fa3a10988a03aed1b66fb17d893b1a9adb90f7cd) feat(ci): prepare 2.0.0-alpha.0 ([#5786](https://www.github.com/tauri-apps/tauri/pull/5786)) on 2022-12-08 + +## \[1.5.3] + +### Bug Fixes + +- [`1c582a94`](https://www.github.com/tauri-apps/tauri/commit/1c582a942e345a066b65620e4db9f688ec142bb9)([#8392](https://www.github.com/tauri-apps/tauri/pull/8392)) Fix a regression where typescript could not find types when using `"moduleResolution": "node"` + +## \[1.5.2] + +### Bug Fixes + +- [`50462702`](https://www.github.com/tauri-apps/tauri/commit/504627027303ef5a0e855aab2abea64c6964223b)([#8267](https://www.github.com/tauri-apps/tauri/pull/8267)) Add top-level `main`, `module` and `types` fields in `package.json` to be compliant with typescripts's `"moduleResolution": "node"` +- [`14544e4b`](https://www.github.com/tauri-apps/tauri/commit/14544e4b87269c06c89fed3647d80f492e0a1d34)([#8219](https://www.github.com/tauri-apps/tauri/pull/8219)) Avoid crashing in `clearMocks` + +## \[1.5.1] + +### New Features + +- [`2b0212af`](https://www.github.com/tauri-apps/tauri/commit/2b0212af49c386e52bb2357381813d6d435ec4af)([#7961](https://www.github.com/tauri-apps/tauri/pull/7961)) Add `mockConvertFileSrc` in `mocks` module, to mock `convertFileSrc` function. + +## \[1.5.0] + +### New Features + +- [`6c408b73`](https://www.github.com/tauri-apps/tauri/commit/6c408b736c7aa2a0a91f0a40d45a2b7a7dedfe78)([#7269](https://www.github.com/tauri-apps/tauri/pull/7269)) Add option to specify notification sound. + +### Enhancements + +- [`58d6b899`](https://www.github.com/tauri-apps/tauri/commit/58d6b899e21d37bb42810890d289deb57f2273bd)([#7636](https://www.github.com/tauri-apps/tauri/pull/7636)) Add `append` option to `FsOptions` in the `fs` JS module, used in `writeTextFile` and `writeBinaryFile`, to be able to append to existing files instead of overwriting it. + +### Bug Fixes + +- [`2eab1505`](https://www.github.com/tauri-apps/tauri/commit/2eab1505632ff71431d4c31c49b5afc78fa5b9dd)([#7394](https://www.github.com/tauri-apps/tauri/pull/7394)) Fix `Body.form` static not reading and sending entries of type `Blob` (including subclasses such as `File`) + +## \[1.4.0] + +### New Features + +- [`359058ce`](https://www.github.com/tauri-apps/tauri/commit/359058cecca44a9c30b65140c44a8bb3a6dd3be8)([#5939](https://www.github.com/tauri-apps/tauri/pull/5939)) Add `locale` function in the `os` module to get the system locale. +- [`c4d6fb4b`](https://www.github.com/tauri-apps/tauri/commit/c4d6fb4b1ea8acf02707a9fe5dcab47c1c5bae7b)([#2353](https://www.github.com/tauri-apps/tauri/pull/2353)) Added the `maximizable`, `minimizable` and `closable` fields on `WindowOptions`. +- [`c4d6fb4b`](https://www.github.com/tauri-apps/tauri/commit/c4d6fb4b1ea8acf02707a9fe5dcab47c1c5bae7b)([#2353](https://www.github.com/tauri-apps/tauri/pull/2353)) Added the `setMaximizable`, `setMinimizable`, `setClosable`, `isMaximizable`, `isMinimizable` and `isClosable` methods. +- [`000104bc`](https://www.github.com/tauri-apps/tauri/commit/000104bc3bc0c9ff3d20558ab9cf2080f126e9e0)([#6472](https://www.github.com/tauri-apps/tauri/pull/6472)) Add `WebviewWindow.is_focused` and `WebviewWindow.getFocusedWindow` getters. + +## \[1.3.0] + +- Return correct type for `event.payload` in `onResized` and `onMoved` window event handlers. + - [0b46637e](https://www.github.com/tauri-apps/tauri/commit/0b46637ebaba54403afa32a1cb466f09df2db999) fix(api): construct correct object for onResized and onMoved, closes [#6507](https://www.github.com/tauri-apps/tauri/pull/6507) ([#6509](https://www.github.com/tauri-apps/tauri/pull/6509)) on 2023-04-03 +- Added the `WindowOptions::contentProtected` option and `WebviewWindow#setContentProtected` to change it at runtime. + - [4ab5545b](https://www.github.com/tauri-apps/tauri/commit/4ab5545b7a831c549f3c65e74de487ede3ab7ce5) feat: add content protection api, closes [#5132](https://www.github.com/tauri-apps/tauri/pull/5132) ([#5513](https://www.github.com/tauri-apps/tauri/pull/5513)) on 2022-12-13 +- Allow setting the text of the dialog buttons. + - [00e1efaa](https://www.github.com/tauri-apps/tauri/commit/00e1efaa9b33876d41dd360624b69971e70d3856) feat: customize button texts of message dialog ([#4383](https://www.github.com/tauri-apps/tauri/pull/4383)) on 2022-12-28 +- Add `is_minimized()` window method. + - [62144ef3](https://www.github.com/tauri-apps/tauri/commit/62144ef3be63b237869e511826edfb938e2c7174) feat: add is_minimized (fix [#3878](https://www.github.com/tauri-apps/tauri/pull/3878)) ([#5618](https://www.github.com/tauri-apps/tauri/pull/5618)) on 2022-12-13 +- Add `title` getter on window. + - [233e43b0](https://www.github.com/tauri-apps/tauri/commit/233e43b0c34fada1ca025378533a0b76931a6540) feat: add `title` getter on window, closes [#5023](https://www.github.com/tauri-apps/tauri/pull/5023) ([#5515](https://www.github.com/tauri-apps/tauri/pull/5515)) on 2022-12-13 + +## \[1.2.0] + +- Added the `acceptFirstMouse` window option. + - [95f467ad](https://www.github.com/tauri-apps/tauri/commit/95f467add51448319983c54e2f382c7c09fb72d6) feat(core): add window `accept_first_mouse` option, closes [#5347](https://www.github.com/tauri-apps/tauri/pull/5347) ([#5374](https://www.github.com/tauri-apps/tauri/pull/5374)) on 2022-10-17 +- Fix incorrect return type on `fs/exists` + - [ca3cd8b3](https://www.github.com/tauri-apps/tauri/commit/ca3cd8b3d11beb9b6102da40b7d27f6dbe6cd2d0) fix(api): fs/exists return type previously set to void when it should be boolean ([#5252](https://www.github.com/tauri-apps/tauri/pull/5252)) on 2022-09-29 +- Initialize `Monitor` instances with the correct classes for `position` and `size` fields instead of plain object. + - [6f41a271](https://www.github.com/tauri-apps/tauri/commit/6f41a2712445ac41a5ed84bbcd40af3b76c8b1d8) fix(api.js): fix `Monitor` initialization, closes [#4672](https://www.github.com/tauri-apps/tauri/pull/4672) ([#5314](https://www.github.com/tauri-apps/tauri/pull/5314)) on 2022-09-30 +- **Breaking change:** Node.js v12 is no longer supported. + - [1129f4f5](https://www.github.com/tauri-apps/tauri/commit/1129f4f575dd02f746abe8e66472c88c8f9fe63d) refactor: simplify api.js bundling ([#4277](https://www.github.com/tauri-apps/tauri/pull/4277)) on 2022-10-04 +- Add new app-specific `BaseDirectory` enum variants `AppConfig`, `AppData`, `AppLocalData`, `AppCache` and `AppLog` along with equivalent functions in `path` module and deprecated ambiguous variants `Log` and `App` along with their equivalent functions in `path` module. + - [5d89905e](https://www.github.com/tauri-apps/tauri/commit/5d89905e39ce0e6eaaec50a693679335449edb32) feat(api): add app-specific directory APIs, closes [#5263](https://www.github.com/tauri-apps/tauri/pull/5263) ([#5272](https://www.github.com/tauri-apps/tauri/pull/5272)) on 2022-09-28 +- Fix `dialog.save` return type + - [8357ce5b](https://www.github.com/tauri-apps/tauri/commit/8357ce5b2efdd6f92c7944822542e48ba0e303ce) Fix dialog.save return type ([#5373](https://www.github.com/tauri-apps/tauri/pull/5373)) on 2022-10-08 +- Added support to `FormData` on the `Body.form` function. + - [aa119f28](https://www.github.com/tauri-apps/tauri/commit/aa119f28364f8ffbc64c6bcdfc77483613076a20) feat(api): add FormData support on Body.form, closes [#5545](https://www.github.com/tauri-apps/tauri/pull/5545) ([#5546](https://www.github.com/tauri-apps/tauri/pull/5546)) on 2022-11-04 +- Added `show` and `hide` methods on the `app` module. + - [39bf895b](https://www.github.com/tauri-apps/tauri/commit/39bf895b73ec6b53f5758815396ba85dda6b9c67) feat(macOS): Add application `show` and `hide` methods ([#3689](https://www.github.com/tauri-apps/tauri/pull/3689)) on 2022-10-03 +- Added `tabbingIdentifier` window option for macOS. + - [4137ab44](https://www.github.com/tauri-apps/tauri/commit/4137ab44a81d739556cbc7583485887e78952bf1) feat(macos): add `tabbing_identifier` option, closes [#2804](https://www.github.com/tauri-apps/tauri/pull/2804), [#3912](https://www.github.com/tauri-apps/tauri/pull/3912) ([#5399](https://www.github.com/tauri-apps/tauri/pull/5399)) on 2022-10-19 +- Added `tabbing_identifier` to the window builder on macOS. + - [4137ab44](https://www.github.com/tauri-apps/tauri/commit/4137ab44a81d739556cbc7583485887e78952bf1) feat(macos): add `tabbing_identifier` option, closes [#2804](https://www.github.com/tauri-apps/tauri/pull/2804), [#3912](https://www.github.com/tauri-apps/tauri/pull/3912) ([#5399](https://www.github.com/tauri-apps/tauri/pull/5399)) on 2022-10-19 +- Added the `user_agent` option when creating a window. + - [a6c94119](https://www.github.com/tauri-apps/tauri/commit/a6c94119d8545d509723b147c273ca5edfe3729f) feat(core): expose user_agent to window config ([#5317](https://www.github.com/tauri-apps/tauri/pull/5317)) on 2022-10-02 + +## \[1.1.0] + +- Update `mockIPC()` handler signature to allow async handler functions. + - [4fa968dc](https://www.github.com/tauri-apps/tauri/commit/4fa968dc0e74b5206bfcd54e704d180c16b67b08) fix(api): add async `mockIPC()` handler signature ([#5056](https://www.github.com/tauri-apps/tauri/pull/5056)) on 2022-08-26 +- Improve shell's `Command`, `Command.stdout` and `Command.stderr` events with new `once`, `off`, `listenerCount`, `prependListener`, `prependOnceListener` and `removeAllListeners` functions. + - [aa9f1243](https://www.github.com/tauri-apps/tauri/commit/aa9f1243e6c1629972a82e469f20c8399741740e) Improved EventEmitter for tauri api shell ([#4697](https://www.github.com/tauri-apps/tauri/pull/4697)) on 2022-07-26 +- Added the `encoding` option to the `Command` options. + - [d8cf9f9f](https://www.github.com/tauri-apps/tauri/commit/d8cf9f9fcd617ac24fa418952fd4a32c08804f5c) Command support for specified character encoding, closes [#4644](https://www.github.com/tauri-apps/tauri/pull/4644) ([#4772](https://www.github.com/tauri-apps/tauri/pull/4772)) on 2022-07-28 +- Add `exists` function to the fs module. + - [3c62dbc9](https://www.github.com/tauri-apps/tauri/commit/3c62dbc902c904d35a7472ce72a969084c95fbbe) feat(api): Add `exists` function to the fs module. ([#5060](https://www.github.com/tauri-apps/tauri/pull/5060)) on 2022-09-15 + +## \[1.0.2] + +- Added helper functions to listen to updater and window events. + - [b02fc90f](https://www.github.com/tauri-apps/tauri/commit/b02fc90f450ff9e9d8a35ee55dc1beced4957869) feat(api): add abstractions to updater and window event listeners ([#4569](https://www.github.com/tauri-apps/tauri/pull/4569)) on 2022-07-05 +- Add support to `ArrayBuffer` in `Body.bytes` and `writeBinaryFile`. + - [92aca55a](https://www.github.com/tauri-apps/tauri/commit/92aca55a6f1f899d5c0c3a6aae9ac9cb0a7e9a86) feat(api): add support to ArrayBuffer ([#4579](https://www.github.com/tauri-apps/tauri/pull/4579)) on 2022-07-05 +- Use `toString()` on message/confirm/ask dialogs title and message values. + - [b8cd2a79](https://www.github.com/tauri-apps/tauri/commit/b8cd2a7993cd2aa5b71b30c545b3307245d254bf) feat(api): call `toString()` on dialog title and message, closes [#4583](https://www.github.com/tauri-apps/tauri/pull/4583) ([#4588](https://www.github.com/tauri-apps/tauri/pull/4588)) on 2022-07-04 +- Remove the `type-fest` dependency, changing the OS types to the specific enum instead of allowing any string. + - [d5e910eb](https://www.github.com/tauri-apps/tauri/commit/d5e910ebcc6c8d7f055ab0691286722b140ffcd4) chore(api): remove `type-fest` ([#4605](https://www.github.com/tauri-apps/tauri/pull/4605)) on 2022-07-06 + +## \[1.0.1] + +- Fixes the `writeBinaryFile` sending an empty file contents when only the first argument is passed. + - [ea43cf52](https://www.github.com/tauri-apps/tauri/commit/ea43cf52db8541d20a6397ef3ecd40f0f2bd6113) fix(api): `writeBinaryFile` sends an empty contents with only one arg ([#4368](https://www.github.com/tauri-apps/tauri/pull/4368)) on 2022-06-16 + +## \[1.0.0] + +- Allow choosing multiple folders in `dialog.open`. + - [4e51dce6](https://www.github.com/tauri-apps/tauri/commit/4e51dce6ca21c7664de779bc78a04be1051371f7) fix: dialog open supports multiple dirs, fixes [#4091](https://www.github.com/tauri-apps/tauri/pull/4091) ([#4354](https://www.github.com/tauri-apps/tauri/pull/4354)) on 2022-06-15 +- Upgrade to `stable`! + - [f4bb30cc](https://www.github.com/tauri-apps/tauri/commit/f4bb30cc73d6ba9b9ef19ef004dc5e8e6bb901d3) feat(covector): prepare for v1 ([#4351](https://www.github.com/tauri-apps/tauri/pull/4351)) on 2022-06-15 + +## \[1.0.0-rc.7] + +- Fix `FilePart` usage in `http.Body.form` by renaming the `value` property to `file`. + - [55f89d5f](https://www.github.com/tauri-apps/tauri/commit/55f89d5f9d429252ad3fd557b1d6233b256495e0) fix(api): Rename FormPart `value` to `file` to match docs and endpoint ([#4307](https://www.github.com/tauri-apps/tauri/pull/4307)) on 2022-06-09 +- Fixes a memory leak in the command system. + - [f72cace3](https://www.github.com/tauri-apps/tauri/commit/f72cace36821dc675a6d25268ae85a21bdbd6296) fix: never remove ipc callback & mem never be released ([#4274](https://www.github.com/tauri-apps/tauri/pull/4274)) on 2022-06-05 +- The notification's `isPermissionGranted` function now returns `boolean` instead of `boolean | null`. The response is never `null` because we won't check the permission for now, always returning `true` instead. + - [f482b094](https://www.github.com/tauri-apps/tauri/commit/f482b0942276e9402ab3725957535039bacb4fef) fix: remove notification permission prompt ([#4302](https://www.github.com/tauri-apps/tauri/pull/4302)) on 2022-06-09 +- Added the `resolveResource` API to the path module. + - [7bba8db8](https://www.github.com/tauri-apps/tauri/commit/7bba8db83ead92e9bd9c4be7863742e71ac47513) feat(api): add `resolveResource` API to the path module ([#4234](https://www.github.com/tauri-apps/tauri/pull/4234)) on 2022-05-29 +- Renamed `writeFile` to `writeTextFile` but kept the original function for backwards compatibility. + - [3f998ca2](https://www.github.com/tauri-apps/tauri/commit/3f998ca29445a349489078a74dd068e157a4d68e) feat(api): add `writeTextFile` and `(path, contents, options)` overload ([#4228](https://www.github.com/tauri-apps/tauri/pull/4228)) on 2022-05-29 +- Added `(path, contents[, options])` overload to the `writeTextFile` and `writeBinaryFile` APIs. + - [3f998ca2](https://www.github.com/tauri-apps/tauri/commit/3f998ca29445a349489078a74dd068e157a4d68e) feat(api): add `writeTextFile` and `(path, contents, options)` overload ([#4228](https://www.github.com/tauri-apps/tauri/pull/4228)) on 2022-05-29 + +## \[1.0.0-rc.6] + +- Expose option to set the dialog type. + - [f46175d5](https://www.github.com/tauri-apps/tauri/commit/f46175d5d46fa3eae66ad2415a0eb1efb7d31da2) feat(core): expose option to set dialog type, closes [#4183](https://www.github.com/tauri-apps/tauri/pull/4183) ([#4187](https://www.github.com/tauri-apps/tauri/pull/4187)) on 2022-05-21 +- Expose `title` option in the message dialog API. + - [ae99f991](https://www.github.com/tauri-apps/tauri/commit/ae99f991674d77c322a2240d10ed4b78ed2f4d4b) feat(core): expose message dialog's title option, ref [#4183](https://www.github.com/tauri-apps/tauri/pull/4183) ([#4186](https://www.github.com/tauri-apps/tauri/pull/4186)) on 2022-05-21 + +## \[1.0.0-rc.5] + +- Fixes the type of `http > connectTimeout`. + - [f3c5ca89](https://www.github.com/tauri-apps/tauri/commit/f3c5ca89e79d429183c4e15a9e7cebada2b493a0) fix(core): http api `connect_timeout` deserialization, closes [#4004](https://www.github.com/tauri-apps/tauri/pull/4004) ([#4006](https://www.github.com/tauri-apps/tauri/pull/4006)) on 2022-04-29 + +## \[1.0.0-rc.4] + +- Encode the file path in the `convertFileSrc` function. + - [42e8d9cf](https://www.github.com/tauri-apps/tauri/commit/42e8d9cf925089e9ad591198ee04b0cc0a0eed48) fix(api): encode file path in `convertFileSrc` function, closes [#3841](https://www.github.com/tauri-apps/tauri/pull/3841) ([#3846](https://www.github.com/tauri-apps/tauri/pull/3846)) on 2022-04-02 +- Added `theme` getter to `WebviewWindow`. + - [4cebcf6d](https://www.github.com/tauri-apps/tauri/commit/4cebcf6da7cad1953e0f01b426afac3b5ef1f81e) feat: expose theme APIs, closes [#3903](https://www.github.com/tauri-apps/tauri/pull/3903) ([#3937](https://www.github.com/tauri-apps/tauri/pull/3937)) on 2022-04-21 +- Added `theme` field to `WindowOptions`. + - [4cebcf6d](https://www.github.com/tauri-apps/tauri/commit/4cebcf6da7cad1953e0f01b426afac3b5ef1f81e) feat: expose theme APIs, closes [#3903](https://www.github.com/tauri-apps/tauri/pull/3903) ([#3937](https://www.github.com/tauri-apps/tauri/pull/3937)) on 2022-04-21 +- Added the `setCursorGrab`, `setCursorVisible`, `setCursorIcon` and `setCursorPosition` methods to the `WebviewWindow` class. + - [c54ddfe9](https://www.github.com/tauri-apps/tauri/commit/c54ddfe9338e7eb90b4d5b02dfde687d432d5bc1) feat: expose window cursor APIs, closes [#3888](https://www.github.com/tauri-apps/tauri/pull/3888) [#3890](https://www.github.com/tauri-apps/tauri/pull/3890) ([#3935](https://www.github.com/tauri-apps/tauri/pull/3935)) on 2022-04-21 +- **Breaking change:** The process Command API stdio lines now includes the trailing `\r`. + - [b5622882](https://www.github.com/tauri-apps/tauri/commit/b5622882cf3748e1e5a90915f415c0cd922aaaf8) fix(cli): exit on non-compilation Cargo errors, closes [#3930](https://www.github.com/tauri-apps/tauri/pull/3930) ([#3942](https://www.github.com/tauri-apps/tauri/pull/3942)) on 2022-04-22 +- Added the `tauri://theme-changed` event. + - [4cebcf6d](https://www.github.com/tauri-apps/tauri/commit/4cebcf6da7cad1953e0f01b426afac3b5ef1f81e) feat: expose theme APIs, closes [#3903](https://www.github.com/tauri-apps/tauri/pull/3903) ([#3937](https://www.github.com/tauri-apps/tauri/pull/3937)) on 2022-04-21 + +## \[1.0.0-rc.3] + +- Properly define the `appWindow` type. + - [1deeb03e](https://www.github.com/tauri-apps/tauri/commit/1deeb03ef6c7cbea8cf585864424a3d66f184a02) fix(api.js): appWindow shown as type `any`, fixes [#3747](https://www.github.com/tauri-apps/tauri/pull/3747) ([#3772](https://www.github.com/tauri-apps/tauri/pull/3772)) on 2022-03-24 +- Added `Temp` to the `BaseDirectory` enum. + - [266156a0](https://www.github.com/tauri-apps/tauri/commit/266156a0b08150b21140dd552c8bc252fe413cdd) feat(core): add `BaseDirectory::Temp` and `$TEMP` variable ([#3763](https://www.github.com/tauri-apps/tauri/pull/3763)) on 2022-03-24 + +## \[1.0.0-rc.2] + +- Do not crash if `__TAURI_METADATA__` is not set, log an error instead. + - [9cb1059a](https://www.github.com/tauri-apps/tauri/commit/9cb1059aa3f81521ccc6da655243acfe0327cd98) fix(api): do not throw an exception if **TAURI_METADATA** is not set, fixes [#3554](https://www.github.com/tauri-apps/tauri/pull/3554) ([#3572](https://www.github.com/tauri-apps/tauri/pull/3572)) on 2022-03-03 +- Reimplement endpoint to read file as string for performance. + - [834ccc51](https://www.github.com/tauri-apps/tauri/commit/834ccc51539401d36a7dfa1c0982623c9c446a4c) feat(core): reimplement `readTextFile` for performance ([#3631](https://www.github.com/tauri-apps/tauri/pull/3631)) on 2022-03-07 +- Fixes a regression on the `unlisten` command. + - [76c791bd](https://www.github.com/tauri-apps/tauri/commit/76c791bd2b836d2055410e37e71716172a3f81ef) fix(core): regression on the unlisten function ([#3623](https://www.github.com/tauri-apps/tauri/pull/3623)) on 2022-03-06 + +## \[1.0.0-rc.1] + +- Provide functions to mock IPC calls during testing and static site generation. + - [7e04c072](https://www.github.com/tauri-apps/tauri/commit/7e04c072c4ee2278c648f44575c6c4710ac047f3) feat: add mock functions for testing and SSG ([#3437](https://www.github.com/tauri-apps/tauri/pull/3437)) on 2022-02-14 + - [6f5ed2e6](https://www.github.com/tauri-apps/tauri/commit/6f5ed2e69cb7ffa0d5c8eb5a744fbf94ed6010d4) fix: change file on 2022-02-14 + +## \[1.0.0-rc.0] + +- Add `fileDropEnabled` property to `WindowOptions` so you can now disable it when creating windows from js. + + - [1bfc32a3](https://www.github.com/tauri-apps/tauri/commit/1bfc32a3b2f31b962ce8a5c611b60cb008360923) fix(api.js): add `fileDropEnabled` to `WindowOptions`, closes [#2968](https://www.github.com/tauri-apps/tauri/pull/2968) ([#2989](https://www.github.com/tauri-apps/tauri/pull/2989)) on 2021-12-09 + +- Add `logDir` function to the `path` module to access the suggested log directory. + Add `BaseDirectory.Log` to the `fs` module. + + - [acbb3ae7](https://www.github.com/tauri-apps/tauri/commit/acbb3ae7bb0165846b9456aea103269f027fc548) feat: add Log directory ([#2736](https://www.github.com/tauri-apps/tauri/pull/2736)) on 2021-10-16 + - [62c7a8ad](https://www.github.com/tauri-apps/tauri/commit/62c7a8ad30fd3031b8679960590e5ef3eef8e4da) chore(covector): prepare for `rc` release ([#3376](https://www.github.com/tauri-apps/tauri/pull/3376)) on 2022-02-10 + +- Expose `ask`, `message` and `confirm` APIs on the dialog module. + + - [e98c1af4](https://www.github.com/tauri-apps/tauri/commit/e98c1af44279a5ff6c8a6f0a506ecc219c9f77af) feat(core): expose message dialog APIs, fix window.confirm, implement HasRawWindowHandle for Window, closes [#2535](https://www.github.com/tauri-apps/tauri/pull/2535) ([#2700](https://www.github.com/tauri-apps/tauri/pull/2700)) on 2021-10-02 + +- Event `emit` now automatically serialize non-string types. + + - [06000996](https://www.github.com/tauri-apps/tauri/commit/060009969627890fa9018e2f1105bad13299394c) feat(api): support unknown types for event emit payload, closes [#2929](https://www.github.com/tauri-apps/tauri/pull/2929) ([#2964](https://www.github.com/tauri-apps/tauri/pull/2964)) on 2022-01-07 + +- Fix `http.fetch` throwing error if the response is successful but the body is empty. + + - [50c63900](https://www.github.com/tauri-apps/tauri/commit/50c63900c7313064037e2ceb798a6432fcd1bcda) fix(api.js): fix `http.fetch` throwing error if response body is empty, closes [#2831](https://www.github.com/tauri-apps/tauri/pull/2831) ([#3008](https://www.github.com/tauri-apps/tauri/pull/3008)) on 2021-12-09 + +- Add `title` option to file open/save dialogs. + + - [e1d6a6e6](https://www.github.com/tauri-apps/tauri/commit/e1d6a6e6445637723e2331ca799a662e720e15a8) Create api-file-dialog-title.md ([#3235](https://www.github.com/tauri-apps/tauri/pull/3235)) on 2022-01-16 + - [62c7a8ad](https://www.github.com/tauri-apps/tauri/commit/62c7a8ad30fd3031b8679960590e5ef3eef8e4da) chore(covector): prepare for `rc` release ([#3376](https://www.github.com/tauri-apps/tauri/pull/3376)) on 2022-02-10 + +- Fix `os.platform` returning `macos` and `windows` instead of `darwin` and `win32`. + + - [3924c3d8](https://www.github.com/tauri-apps/tauri/commit/3924c3d85365df30b376a1ec6c2d933460d66af0) fix(api.js): fix `os.platform` return on macos and windows, closes [#2698](https://www.github.com/tauri-apps/tauri/pull/2698) ([#2699](https://www.github.com/tauri-apps/tauri/pull/2699)) on 2021-10-02 + +- The `formatCallback` helper function now returns a number instead of a string. + + - [a48b8b18](https://www.github.com/tauri-apps/tauri/commit/a48b8b18d428bcc404d489daa690bbefe1f57311) feat(core): validate callbacks and event names \[TRI-038] \[TRI-020] ([#21](https://www.github.com/tauri-apps/tauri/pull/21)) on 2022-01-09 + +- Added `rawHeaders` to `http > Response`. + + - [b7a2345b](https://www.github.com/tauri-apps/tauri/commit/b7a2345b06ca0306988b4ba3d3deadd449e65af9) feat(core): add raw headers to HTTP API, closes [#2695](https://www.github.com/tauri-apps/tauri/pull/2695) ([#3053](https://www.github.com/tauri-apps/tauri/pull/3053)) on 2022-01-07 + +- Removed the `currentDir` API from the `path` module. + + - [a08509c6](https://www.github.com/tauri-apps/tauri/commit/a08509c641f43695e25944a2dd47697b18cd83e2) fix(api): remove `currentDir` API from the `path` module on 2022-02-04 + +- Remove `.ts` files on the published package. + + - [0f321ac0](https://www.github.com/tauri-apps/tauri/commit/0f321ac08d56412edd5bc9d166201fbc95d887d8) fix(api): do not ship TS files, closes [#2598](https://www.github.com/tauri-apps/tauri/pull/2598) ([#2645](https://www.github.com/tauri-apps/tauri/pull/2645)) on 2021-09-23 + +- **Breaking change:** Replaces all usages of `number[]` with `Uint8Array` to be closer aligned with the wider JS ecosystem. + + - [9b19a805](https://www.github.com/tauri-apps/tauri/commit/9b19a805aa8efa64b22f2dfef193a144b8e0cee3) fix(api.js) Replace `number[]`with `Uint8Array`. fixes [#3306](https://www.github.com/tauri-apps/tauri/pull/3306) ([#3305](https://www.github.com/tauri-apps/tauri/pull/3305)) on 2022-02-05 + +- `WindowManager` methods `innerPosition` `outerPosition` now correctly return instance of `PhysicalPosition`. + `WindowManager` methods `innerSize` `outerSize` now correctly return instance of `PhysicalSize`. + + - [cc8b1468](https://www.github.com/tauri-apps/tauri/commit/cc8b1468c821df53ceb771061c919409a9c80978) Fix(api): Window size and position returning wrong class (fix: [#2599](https://www.github.com/tauri-apps/tauri/pull/2599)) ([#2621](https://www.github.com/tauri-apps/tauri/pull/2621)) on 2021-09-22 + +- Change the `event` field of the `Event` interface to type `EventName` instead of `string`. + + - [b5d9bcb4](https://www.github.com/tauri-apps/tauri/commit/b5d9bcb402380abc86ae1fa1a77c629af2275f9d) Consistent event name usage ([#3228](https://www.github.com/tauri-apps/tauri/pull/3228)) on 2022-01-15 + - [62c7a8ad](https://www.github.com/tauri-apps/tauri/commit/62c7a8ad30fd3031b8679960590e5ef3eef8e4da) chore(covector): prepare for `rc` release ([#3376](https://www.github.com/tauri-apps/tauri/pull/3376)) on 2022-02-10 + +- Now `resolve()`, `join()` and `normalize()` from the `path` module, won't throw errors if the path doesn't exist, which matches NodeJS behavior. + + - [fe381a0b](https://www.github.com/tauri-apps/tauri/commit/fe381a0bde86ebf4014007f6e21af4c1a9e58cef) fix: `join` no longer cares if path doesn't exist, closes [#2499](https://www.github.com/tauri-apps/tauri/pull/2499) ([#2548](https://www.github.com/tauri-apps/tauri/pull/2548)) on 2021-09-21 + +- Fixes the dialog `defaultPath` usage on Linux. + + - [2212bd5d](https://www.github.com/tauri-apps/tauri/commit/2212bd5d75146f5a2df27cc2157a057642f626da) fix: dialog default path on Linux, closes [#3091](https://www.github.com/tauri-apps/tauri/pull/3091) ([#3123](https://www.github.com/tauri-apps/tauri/pull/3123)) on 2021-12-27 + +- Fixes `window.label` property returning null instead of the actual label. + + - [f5109e0c](https://www.github.com/tauri-apps/tauri/commit/f5109e0c962e3d25404995194968bade1be33b16) fix(api): window label null instead of actual value, closes [#3295](https://www.github.com/tauri-apps/tauri/pull/3295) ([#3332](https://www.github.com/tauri-apps/tauri/pull/3332)) on 2022-02-04 + +- Remove the `BaseDirectory::Current` enum variant for security reasons. + + - [696dca58](https://www.github.com/tauri-apps/tauri/commit/696dca58a9f8ee127a1cf857eb848e09f5845d18) refactor(core): remove `BaseDirectory::Current` variant on 2022-01-26 + +- Change `WindowLabel` type to `string`. + + - [f68603ae](https://www.github.com/tauri-apps/tauri/commit/f68603aee4e16500dff9e385b217f5dd8b1b39e8) chore(docs): simplify event system documentation on 2021-09-27 + +- When building Universal macOS Binaries through the virtual target `universal-apple-darwin`: + +- Expect a universal binary to be created by the user + +- Ensure that binary is bundled and accessed correctly at runtime + +- [3035e458](https://www.github.com/tauri-apps/tauri/commit/3035e4581c161ec7f0bd6d9b42e9015cf1dd1d77) Remove target triple from sidecar bin paths, closes [#3355](https://www.github.com/tauri-apps/tauri/pull/3355) ([#3356](https://www.github.com/tauri-apps/tauri/pull/3356)) on 2022-02-07 + +## \[1.0.0-beta.8] + +- Revert target back to ES5. + - [657c7dac](https://www.github.com/tauri-apps/tauri/commit/657c7dac734661956b87d021ff531ba530dd92a3) fix(api): revert ES2021 target on 2021-08-23 + +## \[1.0.0-beta.7] + +- Fix missing asset protocol path.Now the protocol is `https://asset.localhost/path/to/file` on Windows. Linux and macOS + is still `asset://path/to/file`. + - [994b5325](https://www.github.com/tauri-apps/tauri/commit/994b5325dd385f564b37fe1530c5d798dc925fff) fix: missing asset protocol path ([#2484](https://www.github.com/tauri-apps/tauri/pull/2484)) on 2021-08-23 + +## \[1.0.0-beta.6] + +- `bundle` now exports `clipboard` module so you can `import { clipboard } from "@tauri-apps/api"`. + - [4f88c3fb](https://www.github.com/tauri-apps/tauri/commit/4f88c3fb94286f3daafb906e3513c9210ecfa76b) fix(api.js): `bundle` now exports `clipboard` mod, closes [#2243](https://www.github.com/tauri-apps/tauri/pull/2243) ([#2244](https://www.github.com/tauri-apps/tauri/pull/2244)) on 2021-07-19 +- Fix double window creation + - [9fbcc024](https://www.github.com/tauri-apps/tauri/commit/9fbcc024542d87f71afd364acdcf2302cf82912c) fix(api.js): fix double window creation, closes [#2284](https://www.github.com/tauri-apps/tauri/pull/2284) ([#2285](https://www.github.com/tauri-apps/tauri/pull/2285)) on 2021-07-23 +- Add `os` module which exports `EOL`, `platform()`, `version()`, `type()`, `arch()`, `tempdir()` + - [05e679a6](https://www.github.com/tauri-apps/tauri/commit/05e679a6d2aca5642c780052bcf1384c49a462de) feat(api.js): add `os` module ([#2299](https://www.github.com/tauri-apps/tauri/pull/2299)) on 2021-07-28 +- - Add new nodejs-inspired functions which are `join`, `resolve`, `normalize`, `dirname`, `basename` and `extname`. +- Add `sep` and `delimiter` constants. +- Removed `resolvePath` API, use `resolve` instead. +- [05b9d81e](https://www.github.com/tauri-apps/tauri/commit/05b9d81ee6bcc920defca76cff00178b301fffe8) feat(api.js): add nodejs-inspired functions in `path` module ([#2310](https://www.github.com/tauri-apps/tauri/pull/2310)) on 2021-08-02 +- Change target to ES2021. + - [97bc52ee](https://www.github.com/tauri-apps/tauri/commit/97bc52ee03dec0b67cc1cced23305a4c53e9eb62) Tooling: \[API] Changed target in tsconfig to es6 ([#2362](https://www.github.com/tauri-apps/tauri/pull/2362)) on 2021-08-09 +- Add `toggleMaximize()` function to the `WebviewWindow` class. + - [1a510066](https://www.github.com/tauri-apps/tauri/commit/1a510066732d5f61c88c0ceed1c5f5cc559faf7d) fix(core): `data-tauri-drag-region` didn't respect resizable, closes [#2314](https://www.github.com/tauri-apps/tauri/pull/2314) ([#2316](https://www.github.com/tauri-apps/tauri/pull/2316)) on 2021-08-02 +- Fix `@ts-expect` error usage + - [dd52e738](https://www.github.com/tauri-apps/tauri/commit/dd52e738f1fd323bd8d185d6e650f412eb031200) fix(api.js): fix `@ts-expect-error` usage, closes [#2249](https://www.github.com/tauri-apps/tauri/pull/2249) ([#2250](https://www.github.com/tauri-apps/tauri/pull/2250)) on 2021-07-20 +- Fixes file drop events being swapped (`file-drop-hover` on drop and `file-drop` on hover). + - [c2b0fe1c](https://www.github.com/tauri-apps/tauri/commit/c2b0fe1ce58e54dbcfdb63162ad17d7e6d8774d9) fix(core): fix wrong file drop events ([#2300](https://www.github.com/tauri-apps/tauri/pull/2300)) on 2021-07-31 +- Fixes the global bundle UMD code. + - [268450b1](https://www.github.com/tauri-apps/tauri/commit/268450b1329a4b55f2043890c565a8563f890c3a) fix(api): global bundle broken code, closes [#2289](https://www.github.com/tauri-apps/tauri/pull/2289) ([#2297](https://www.github.com/tauri-apps/tauri/pull/2297)) on 2021-07-26 +- - Fixes monitor api not working. +- Fixes window.print() not working on macOS. +- [0f63f5e7](https://www.github.com/tauri-apps/tauri/commit/0f63f5e757873f1787a1ae07ca531340d0d45ec3) fix(api): Fix monitor functions, closes [#2294](https://www.github.com/tauri-apps/tauri/pull/2294) ([#2301](https://www.github.com/tauri-apps/tauri/pull/2301)) on 2021-07-29 +- Improve `EventName` type using `type-fest`'s `LiteralUnion`. + - [8e480297](https://www.github.com/tauri-apps/tauri/commit/8e48029790857b38988da4d291aa7458f51bb265) feat(api): improve `EventName` type definition ([#2379](https://www.github.com/tauri-apps/tauri/pull/2379)) on 2021-08-10 +- Update protocol url path with wry 0.12.1 on Windows. + - [88382fe1](https://www.github.com/tauri-apps/tauri/commit/88382fe147ebcb3f59308cc529e5562a04970876) chore(api): update protocol url path with wry 0.12.1 on Windows ([#2409](https://www.github.com/tauri-apps/tauri/pull/2409)) on 2021-08-13 + +## \[1.0.0-beta.5] + +- Adds `convertFileSrc` helper to the `tauri` module, simplifying the process of using file paths as webview source (`img`, `video`, etc). + - [51a5cfe4](https://www.github.com/tauri-apps/tauri/commit/51a5cfe4b5e9890fb6f639c9c929657fd747a595) feat(api): add `convertFileSrc` helper ([#2138](https://www.github.com/tauri-apps/tauri/pull/2138)) on 2021-07-02 +- You can now use `emit`, `listen` and `once` using the `appWindow` exported by the window module. + - [5d7626f8](https://www.github.com/tauri-apps/tauri/commit/5d7626f89781a6ebccceb9ab3b2e8335aa7a0392) feat(api): WindowManager extends WebviewWindowHandle, add events docs ([#2146](https://www.github.com/tauri-apps/tauri/pull/2146)) on 2021-07-03 +- Allow manipulating a spawned window directly using `WebviewWindow`, which now extends `WindowManager`. + - [d69b1cf6](https://www.github.com/tauri-apps/tauri/commit/d69b1cf6d7c13297073073d753e30fe1a22a09cb) feat(api): allow managing windows created on JS ([#2154](https://www.github.com/tauri-apps/tauri/pull/2154)) on 2021-07-05 + +## \[1.0.0-beta.4] + +- Add asset custom protocol to access local file system. + - [ee60e424](https://www.github.com/tauri-apps/tauri/commit/ee60e424221559d3d725716b0003c5566ef2b5cd) feat: asset custom protocol to access local file system ([#2104](https://www.github.com/tauri-apps/tauri/pull/2104)) on 2021-06-28 + +## \[1.0.0-beta.3] + +- Export `Response` and `ResponseType` as value instead of type. + - [394b6e05](https://www.github.com/tauri-apps/tauri/commit/394b6e0572e7a0a92e103e462a7f603f7d569319) fix(api): http `ResponseType` export type error ([#2065](https://www.github.com/tauri-apps/tauri/pull/2065)) on 2021-06-24 + +## \[1.0.0-beta.2] + +- Export `BaseDirectory` in `path` module + - [277f5ca5](https://www.github.com/tauri-apps/tauri/commit/277f5ca5a8ae227bbdccee1ad52bdd88b4a5b11b) feat(api): export `BaseDirectory` in `path` module ([#1885](https://www.github.com/tauri-apps/tauri/pull/1885)) on 2021-05-30 +- Use `export type` to export TS types, enums and interfaces. + - [9a662d26](https://www.github.com/tauri-apps/tauri/commit/9a662d2601b01d712c6bd205f8db1b674f56dfa7) fix: Monitor if --isolatedModules is enabled ([#1825](https://www.github.com/tauri-apps/tauri/pull/1825)) on 2021-05-13 + - [612cd8ec](https://www.github.com/tauri-apps/tauri/commit/612cd8ecb8e02954f3696b9e138cbc7d2c228fad) feat(api): finalize `export type` usage ([#1847](https://www.github.com/tauri-apps/tauri/pull/1847)) on 2021-05-17 +- Adds `focus?: boolean` to the WindowOptions interface. + - [5f351622](https://www.github.com/tauri-apps/tauri/commit/5f351622c7812ad1bb56ddb37364ccaa4124c24b) feat(core): add focus API to the WindowBuilder and WindowOptions, [#1737](https://www.github.com/tauri-apps/tauri/pull/1737) on 2021-05-30 +- Adds `isDecorated` getter on the window API. + - [f58a2114](https://www.github.com/tauri-apps/tauri/commit/f58a2114fbfd5307c349f05c88f2e08fd8baa8aa) feat(core): add `is_decorated` Window getter on 2021-05-30 +- Adds `isResizable` getter on the window API. + - [1e8af280](https://www.github.com/tauri-apps/tauri/commit/1e8af280c27f381828d6209722b10e889082fa00) feat(core): add `is_resizable` Window getter on 2021-05-30 +- Adds `isVisible` getter on the window API. + - [36506c96](https://www.github.com/tauri-apps/tauri/commit/36506c967de82bc7ff453d11e6104ecf66d7a588) feat(core): add `is_visible` API on 2021-05-30 +- Adds `requestUserAttention` API to the `window` module. + - [7dcca6e9](https://www.github.com/tauri-apps/tauri/commit/7dcca6e9281182b11ad3d4a79871f09b30b9b419) feat(core): add `request_user_attention` API, closes [#2023](https://www.github.com/tauri-apps/tauri/pull/2023) ([#2026](https://www.github.com/tauri-apps/tauri/pull/2026)) on 2021-06-20 +- Adds `setFocus` to the window API. + - [bb6992f8](https://www.github.com/tauri-apps/tauri/commit/bb6992f888196ca7c87bb2fe74ad2bd8bf393e05) feat(core): add `set_focus` window API, fixes [#1737](https://www.github.com/tauri-apps/tauri/pull/1737) on 2021-05-30 +- Adds `setSkipTaskbar` to the window API. + - [e06aa277](https://www.github.com/tauri-apps/tauri/commit/e06aa277384450cfef617c0e57b0d5d403bb1e7f) feat(core): add `set_skip_taskbar` API on 2021-05-30 +- Adds `skipTaskbar?: boolean` to the WindowOptions interface. + - [5525b03a](https://www.github.com/tauri-apps/tauri/commit/5525b03a78a2232c650043fbd9894ce1553cad41) feat(core): add `skip_taskbar` API to the WindowBuilder/WindowOptions on 2021-05-30 +- Adds `center?: boolean` to `WindowOptions` and `center()` API to the `appWindow`. + - [5cba6eb4](https://www.github.com/tauri-apps/tauri/commit/5cba6eb4d28d53f06855d60d4d0eae6b95233ccf) feat(core): add window `center` API, closes [#1822](https://www.github.com/tauri-apps/tauri/pull/1822) ([#1954](https://www.github.com/tauri-apps/tauri/pull/1954)) on 2021-06-05 +- Adds `clipboard` APIs (write and read text). + - [285bf64b](https://www.github.com/tauri-apps/tauri/commit/285bf64bf9569efb2df904c69c6df405ff0d62e2) feat(core): add clipboard writeText and readText APIs ([#2035](https://www.github.com/tauri-apps/tauri/pull/2035)) on 2021-06-21 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- The `http` APIs now resolve the returned promise when the API call finishes with an error status code. + - [47f75584](https://www.github.com/tauri-apps/tauri/commit/47f7558417cc654bdb1d018127e8900bc4eac622) fix(core): resolve HTTP API on non-ok status code, fix binary response, closes [#2046](https://www.github.com/tauri-apps/tauri/pull/2046) ([#2053](https://www.github.com/tauri-apps/tauri/pull/2053)) on 2021-06-23 +- Improve RPC security by requiring a numeric code to invoke commands. The codes are generated by the Rust side and injected into the app's code using a closure, so external scripts can't access the backend. This change doesn't protect `withGlobalTauri` (`window.__TAURI__`) usage. + - [160fb052](https://www.github.com/tauri-apps/tauri/commit/160fb0529fd31d755574ae30fbdf01fa221a2acb) feat(core): improve RPC security, closes [#814](https://www.github.com/tauri-apps/tauri/pull/814) ([#2047](https://www.github.com/tauri-apps/tauri/pull/2047)) on 2021-06-22 +- Mark the `WebviewWindow` constructor as public. + - [4aeb936e](https://www.github.com/tauri-apps/tauri/commit/4aeb936e9b60b895d383597dc698ee5d638436f9) fix(api): `WebviewWindow` constructor is public ([#1888](https://www.github.com/tauri-apps/tauri/pull/1888)) on 2021-05-21 +- Validate arguments on the window `setLocation`, `setSize`, `setMinSize` and `setMaxSize` API. + - [7616e6cc](https://www.github.com/tauri-apps/tauri/commit/7616e6cc7bcd49f688b0d00fdc33c94b7b93713d) feat(api): validate window API `size` and `location` arguments ([#1846](https://www.github.com/tauri-apps/tauri/pull/1846)) on 2021-05-17 + +## \[1.0.0-beta.1] + +- Adds `package.json` to the `exports` object. + - [ab1ea96](https://www.github.com/tauri-apps/tauri/commit/ab1ea964786e1781c922582b059c555b6072f1a0) chore(api): add `package.json` to the `exports` field ([#1807](https://www.github.com/tauri-apps/tauri/pull/1807)) on 2021-05-12 + +## \[1.0.0-beta.0] + +- CommonJS chunks are now properly exported with `.cjs` extension + - [ddcd923](https://www.github.com/tauri-apps/tauri/commit/ddcd9233bd6f499aa7f22484d6c151b01778bc1b) fix(api): export commonjs chunks with `.cjs` extension, fix [#1625](https://www.github.com/tauri-apps/tauri/pull/1625) ([#1627](https://www.github.com/tauri-apps/tauri/pull/1627)) on 2021-04-26 +- Adds `transparent?: boolean` to the `WindowOptions` interface. + - [08c1c5c](https://www.github.com/tauri-apps/tauri/commit/08c1c5ca5c0ebe17ea98689a5fe3b7e47a98e955) fix(api): missing `transparent` flag on `WindowOptions` ([#1764](https://www.github.com/tauri-apps/tauri/pull/1764)) on 2021-05-10 +- Adds `options` argument to the shell command API (`env` and `cwd` configuration). + - [721e98f](https://www.github.com/tauri-apps/tauri/commit/721e98f175567b360c86f30565ab1b9d08e7cf85) feat(core): add env, cwd to the command API, closes [#1634](https://www.github.com/tauri-apps/tauri/pull/1634) ([#1635](https://www.github.com/tauri-apps/tauri/pull/1635)) on 2021-04-28 +- Adds `startDragging` API on the window module. + - [c31f097](https://www.github.com/tauri-apps/tauri/commit/c31f0978c535f794fffb75a121e69a323e70b06e) refactor: update to wry 0.9 ([#1630](https://www.github.com/tauri-apps/tauri/pull/1630)) on 2021-04-28 +- Move `exit` and `relaunch` APIs from `app` to `process` module. + - [b0bb796](https://www.github.com/tauri-apps/tauri/commit/b0bb796a42e2560233aea47ce6ced54ac238eb53) refactor: rename `command` mod to `process`, move restart_application ([#1667](https://www.github.com/tauri-apps/tauri/pull/1667)) on 2021-04-30 +- The window management API was refactored: removed `setX`, `setY`, `setWidth`, `setHeight` APIs, renamed `resize` to `setSize` and the size and position APIs now allow defining both logical and physical values. + - [6bfac86](https://www.github.com/tauri-apps/tauri/commit/6bfac866a703f1499a64237fb29b2625703f4e22) refactor(core): add window getters, physical & logical sizes/positions ([#1723](https://www.github.com/tauri-apps/tauri/pull/1723)) on 2021-05-05 +- Adds window getters. + - [6bfac86](https://www.github.com/tauri-apps/tauri/commit/6bfac866a703f1499a64237fb29b2625703f4e22) refactor(core): add window getters, physical & logical sizes/positions ([#1723](https://www.github.com/tauri-apps/tauri/pull/1723)) on 2021-05-05 + +## \[1.0.0-beta-rc.3] + +- Fixes distribution of the `@tauri-apps/api` package for older bundlers. + - [7f998d0](https://www.github.com/tauri-apps/tauri/commit/7f998d08e3ab8823c99190fa283bdfa2c4f2749b) fix(api): distribution ([#1582](https://www.github.com/tauri-apps/tauri/pull/1582)) on 2021-04-22 +- Update minimum Node.js version to v12.13.0 + - [1f089fb](https://www.github.com/tauri-apps/tauri/commit/1f089fb4f964c673dcab5784bdf1da2833487a7c) chore: update minimum nodejs version to 12.13.0 ([#1562](https://www.github.com/tauri-apps/tauri/pull/1562)) on 2021-04-21 + +## \[1.0.0-beta-rc.2] + +- TS was wrongly re-exporting the module. + - [fcb3b48](https://www.github.com/tauri-apps/tauri/commit/fcb3b4857efa17d2a3717f32457e88b24520cc9b) fix: [#1512](https://www.github.com/tauri-apps/tauri/pull/1512) ([#1517](https://www.github.com/tauri-apps/tauri/pull/1517)) on 2021-04-19 + - [ae14a3f](https://www.github.com/tauri-apps/tauri/commit/ae14a3ff51a742b6ab6f76bbfc21f385310f1dc6) fix: [#1517](https://www.github.com/tauri-apps/tauri/pull/1517) had the wrong package reference in the changefile ([#1538](https://www.github.com/tauri-apps/tauri/pull/1538)) on 2021-04-19 + +## \[1.0.0-beta-rc.1] + +- Missing the `files` property in the package.json which mean that the `dist` directory was not published and used. + - [b2569a7](https://www.github.com/tauri-apps/tauri/commit/b2569a729a3caa88bdba62abc31f0665e1323aaa) fix(js-api): dist ([#1498](https://www.github.com/tauri-apps/tauri/pull/1498)) on 2021-04-15 + +## \[1.0.0-beta-rc.0] + +- Add current working directory to the path api module. + - [52c2baf](https://www.github.com/tauri-apps/tauri/commit/52c2baf940773cf7c51647fb6f20d0f7df126115) feat: add current working directory to path api module ([#1375](https://www.github.com/tauri-apps/tauri/pull/1375)) on 2021-03-23 + - [a6def70](https://www.github.com/tauri-apps/tauri/commit/a6def7066eec19c889b0f14cc1e475bf209a332e) Refactor(tauri): move tauri-api and tauri-updater to tauri ([#1455](https://www.github.com/tauri-apps/tauri/pull/1455)) on 2021-04-11 +- The shell process spawning API was rewritten and now includes stream access. + - [3713066](https://www.github.com/tauri-apps/tauri/commit/3713066e451bd30d0cc6f57bb437f08276f4c4ad) refactor(core): rewrite shell execute API, closes [#1229](https://www.github.com/tauri-apps/tauri/pull/1229) ([#1408](https://www.github.com/tauri-apps/tauri/pull/1408)) on 2021-03-31 +- The file dialog API now uses [rfd](https://github.com/PolyMeilex/rfd). The filter option is now an array of `{ name: string, extensions: string[] }`. + - [2326bcd](https://www.github.com/tauri-apps/tauri/commit/2326bcd399411f7f0eabdb7ade910be473adadae) refactor(core): use `nfd` for file dialogs, closes [#1251](https://www.github.com/tauri-apps/tauri/pull/1251) ([#1257](https://www.github.com/tauri-apps/tauri/pull/1257)) on 2021-02-18 + - [a6def70](https://www.github.com/tauri-apps/tauri/commit/a6def7066eec19c889b0f14cc1e475bf209a332e) Refactor(tauri): move tauri-api and tauri-updater to tauri ([#1455](https://www.github.com/tauri-apps/tauri/pull/1455)) on 2021-04-11 +- The HTTP API was improved with client caching and better payload and response types. + - [a7bc472](https://www.github.com/tauri-apps/tauri/commit/a7bc472e994730071f960d09a12ac85296a080ae) refactor(core): improve HTTP API, closes [#1098](https://www.github.com/tauri-apps/tauri/pull/1098) ([#1237](https://www.github.com/tauri-apps/tauri/pull/1237)) on 2021-02-15 + - [a6def70](https://www.github.com/tauri-apps/tauri/commit/a6def7066eec19c889b0f14cc1e475bf209a332e) Refactor(tauri): move tauri-api and tauri-updater to tauri ([#1455](https://www.github.com/tauri-apps/tauri/pull/1455)) on 2021-04-11 +- Update all code files to have our license header. + - [bf82136](https://www.github.com/tauri-apps/tauri/commit/bf8213646689175f8a158b956911f3a43e360690) feat(license): SPDX Headers ([#1449](https://www.github.com/tauri-apps/tauri/pull/1449)) on 2021-04-11 + - [a6def70](https://www.github.com/tauri-apps/tauri/commit/a6def7066eec19c889b0f14cc1e475bf209a332e) Refactor(tauri): move tauri-api and tauri-updater to tauri ([#1455](https://www.github.com/tauri-apps/tauri/pull/1455)) on 2021-04-11 + - [aea6145](https://www.github.com/tauri-apps/tauri/commit/aea614587bddab930d552512b54e18624fbf573e) refactor(repo): add /tooling folder ([#1457](https://www.github.com/tauri-apps/tauri/pull/1457)) on 2021-04-12 +- Use secure RNG on callback function names. + - [c8992bb](https://www.github.com/tauri-apps/tauri/commit/c8992bb0bfb8eaeae8ebed444719f9c9372d39d4) refactor(api): use secure RNG, closes [#1356](https://www.github.com/tauri-apps/tauri/pull/1356) ([#1398](https://www.github.com/tauri-apps/tauri/pull/1398)) on 2021-03-30 +- The invoke function can now be called with the cmd as the first parameter and the args as the second. + - [427d170](https://www.github.com/tauri-apps/tauri/commit/427d170930ab711fd0ca82f7a73b524d6fdc222f) feat(api/invoke): separate cmd arg ([#1321](https://www.github.com/tauri-apps/tauri/pull/1321)) on 2021-03-04 +- Adds a global shortcut API. + - [855effa](https://www.github.com/tauri-apps/tauri/commit/855effadd9ebfb6bc1a3555ac7fc733f6f766b7a) feat(core): globalShortcut API ([#1232](https://www.github.com/tauri-apps/tauri/pull/1232)) on 2021-02-14 + - [a6def70](https://www.github.com/tauri-apps/tauri/commit/a6def7066eec19c889b0f14cc1e475bf209a332e) Refactor(tauri): move tauri-api and tauri-updater to tauri ([#1455](https://www.github.com/tauri-apps/tauri/pull/1455)) on 2021-04-11 +- Added window management and window creation APIs. + - [a3d6dff](https://www.github.com/tauri-apps/tauri/commit/a3d6dff2163c7a45842253edd81dbc62248dc65d) feat(core): window API ([#1225](https://www.github.com/tauri-apps/tauri/pull/1225)) on 2021-02-13 + - [641374b](https://www.github.com/tauri-apps/tauri/commit/641374b15343518cd835bd5ada811941c65dcf2e) feat(core): window creation at runtime ([#1249](https://www.github.com/tauri-apps/tauri/pull/1249)) on 2021-02-17 diff --git a/node_modules/@tauri-apps/api/LICENSE_APACHE-2.0 b/node_modules/@tauri-apps/api/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/node_modules/@tauri-apps/api/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/node_modules/@tauri-apps/api/LICENSE_MIT b/node_modules/@tauri-apps/api/LICENSE_MIT new file mode 100644 index 0000000..b08530d --- /dev/null +++ b/node_modules/@tauri-apps/api/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/@tauri-apps/api/README.md b/node_modules/@tauri-apps/api/README.md new file mode 100644 index 0000000..d74ab83 --- /dev/null +++ b/node_modules/@tauri-apps/api/README.md @@ -0,0 +1,52 @@ +# @tauri-apps/api + + + +[![status](https://img.shields.io/badge/status-stable-blue.svg)](https://github.com/tauri-apps/tauri/tree/dev) +[![License](https://img.shields.io/badge/License-MIT%20or%20Apache%202-green.svg)](https://opencollective.com/tauri) +[![lint js](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/lint-js.yml?label=lint%20js&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/lint-js.yml) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield) +[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/SpmNs4S) +[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app) +[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation) +[![support](https://img.shields.io/badge/sponsor-Open%20Collective-blue.svg)](https://opencollective.com/tauri) + +| Component | Version | +| --------------- | ----------------------------------------------------- | +| @tauri-apps/api | ![](https://img.shields.io/npm/v/@tauri-apps/api.svg) | + +## About Tauri + +Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily. + +Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task. + +## This module + +This is a typescript library that creates `cjs` and `esm` JavaScript endpoints for you to import into your Frontend framework so that the Webview can call and listen to backend activity. We also ship the pure typescript, because for some frameworks this is more optimal. It uses the message passing of webviews to their hosts. + +To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document. + +## Installation + +The preferred method is to install this module locally as a dependency: + +``` +$ pnpm add @tauri-apps/api +$ yarn add @tauri-apps/api +$ npm add @tauri-apps/api +``` + +## Semver + +**tauri** is following [Semantic Versioning 2.0](https://semver.org/). + +## Licenses + +Code: (c) 2019 - 2021 - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. + +Logo: CC-BY-NC-ND + +- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum) diff --git a/node_modules/@tauri-apps/api/app.cjs b/node_modules/@tauri-apps/api/app.cjs new file mode 100644 index 0000000..12df98f --- /dev/null +++ b/node_modules/@tauri-apps/api/app.cjs @@ -0,0 +1,235 @@ +'use strict'; + +var core = require('./core.cjs'); +var image = require('./image.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * Bundle type of the current application. + */ +exports.BundleType = void 0; +(function (BundleType) { + /** Windows NSIS */ + BundleType["Nsis"] = "nsis"; + /** Windows MSI */ + BundleType["Msi"] = "msi"; + /** Linux Debian package */ + BundleType["Deb"] = "deb"; + /** Linux RPM */ + BundleType["Rpm"] = "rpm"; + /** Linux AppImage */ + BundleType["AppImage"] = "appimage"; + /** macOS app bundle */ + BundleType["App"] = "app"; +})(exports.BundleType || (exports.BundleType = {})); +/** + * Application metadata and related APIs. + * + * @module + */ +/** + * Gets the application version. + * @example + * ```typescript + * import { getVersion } from '@tauri-apps/api/app'; + * const appVersion = await getVersion(); + * ``` + * + * @since 1.0.0 + */ +async function getVersion() { + return core.invoke('plugin:app|version'); +} +/** + * Gets the application name. + * @example + * ```typescript + * import { getName } from '@tauri-apps/api/app'; + * const appName = await getName(); + * ``` + * + * @since 1.0.0 + */ +async function getName() { + return core.invoke('plugin:app|name'); +} +/** + * Gets the Tauri framework version used by this application. + * + * @example + * ```typescript + * import { getTauriVersion } from '@tauri-apps/api/app'; + * const tauriVersion = await getTauriVersion(); + * ``` + * + * @since 1.0.0 + */ +async function getTauriVersion() { + return core.invoke('plugin:app|tauri_version'); +} +/** + * Gets the application identifier. + * @example + * ```typescript + * import { getIdentifier } from '@tauri-apps/api/app'; + * const identifier = await getIdentifier(); + * ``` + * + * @returns The application identifier as configured in `tauri.conf.json`. + * + * @since 2.4.0 + */ +async function getIdentifier() { + return core.invoke('plugin:app|identifier'); +} +/** + * Shows the application on macOS. This function does not automatically + * focus any specific app window. + * + * @example + * ```typescript + * import { show } from '@tauri-apps/api/app'; + * await show(); + * ``` + * + * @since 1.2.0 + */ +async function show() { + return core.invoke('plugin:app|app_show'); +} +/** + * Hides the application on macOS. + * + * @example + * ```typescript + * import { hide } from '@tauri-apps/api/app'; + * await hide(); + * ``` + * + * @since 1.2.0 + */ +async function hide() { + return core.invoke('plugin:app|app_hide'); +} +/** + * Fetches the data store identifiers on macOS and iOS. + * + * See https://developer.apple.com/documentation/webkit/wkwebsitedatastore for more information. + * + * @example + * ```typescript + * import { fetchDataStoreIdentifiers } from '@tauri-apps/api/app'; + * const ids = await fetchDataStoreIdentifiers(); + * ``` + * + * @since 2.4.0 + */ +async function fetchDataStoreIdentifiers() { + return core.invoke('plugin:app|fetch_data_store_identifiers'); +} +/** + * Removes the data store with the given identifier. + * + * Note that any webview using this data store should be closed before running this API. + * + * See https://developer.apple.com/documentation/webkit/wkwebsitedatastore for more information. + * + * @example + * ```typescript + * import { fetchDataStoreIdentifiers, removeDataStore } from '@tauri-apps/api/app'; + * for (const id of (await fetchDataStoreIdentifiers())) { + * await removeDataStore(id); + * } + * ``` + * + * @since 2.4.0 + */ +async function removeDataStore(uuid) { + return core.invoke('plugin:app|remove_data_store', { uuid }); +} +/** + * Gets the default window icon. + * + * @example + * ```typescript + * import { defaultWindowIcon } from '@tauri-apps/api/app'; + * const icon = await defaultWindowIcon(); + * ``` + * + * @since 2.0.0 + */ +async function defaultWindowIcon() { + return core.invoke('plugin:app|default_window_icon').then((rid) => rid ? new image.Image(rid) : null); +} +/** + * Sets the application's theme. Pass in `null` or `undefined` to follow + * the system theme. + * + * @example + * ```typescript + * import { setTheme } from '@tauri-apps/api/app'; + * await setTheme('dark'); + * ``` + * + * #### Platform-specific + * + * - **iOS / Android:** Unsupported. + * + * @since 2.0.0 + */ +async function setTheme(theme) { + return core.invoke('plugin:app|set_app_theme', { theme }); +} +/** + * Sets the dock visibility for the application on macOS. + * + * @param visible - Whether the dock should be visible or not. + * + * @example + * ```typescript + * import { setDockVisibility } from '@tauri-apps/api/app'; + * await setDockVisibility(false); + * ``` + * + * @since 2.5.0 + */ +async function setDockVisibility(visible) { + return core.invoke('plugin:app|set_dock_visibility', { visible }); +} +/** + * Gets the application bundle type. + * + * @example + * ```typescript + * import { getBundleType } from '@tauri-apps/api/app'; + * const type = await getBundleType(); + * ``` + * + * @since 2.5.0 + */ +async function getBundleType() { + return core.invoke('plugin:app|bundle_type'); +} +/** + * Listens to the backButton event on Android. + * @param handler + */ +async function onBackButtonPress(handler) { + return core.addPluginListener('app', 'back-button', handler); +} + +exports.defaultWindowIcon = defaultWindowIcon; +exports.fetchDataStoreIdentifiers = fetchDataStoreIdentifiers; +exports.getBundleType = getBundleType; +exports.getIdentifier = getIdentifier; +exports.getName = getName; +exports.getTauriVersion = getTauriVersion; +exports.getVersion = getVersion; +exports.hide = hide; +exports.onBackButtonPress = onBackButtonPress; +exports.removeDataStore = removeDataStore; +exports.setDockVisibility = setDockVisibility; +exports.setTheme = setTheme; +exports.show = show; diff --git a/node_modules/@tauri-apps/api/app.d.ts b/node_modules/@tauri-apps/api/app.d.ts new file mode 100644 index 0000000..d535727 --- /dev/null +++ b/node_modules/@tauri-apps/api/app.d.ts @@ -0,0 +1,220 @@ +import { PluginListener } from './core'; +import { Image } from './image'; +import { Theme } from './window'; +/** + * Identifier type used for data stores on macOS and iOS. + * + * Represents a 128-bit identifier, commonly expressed as a 16-byte UUID. + */ +export type DataStoreIdentifier = [ + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number +]; +/** + * Bundle type of the current application. + */ +export declare enum BundleType { + /** Windows NSIS */ + Nsis = "nsis", + /** Windows MSI */ + Msi = "msi", + /** Linux Debian package */ + Deb = "deb", + /** Linux RPM */ + Rpm = "rpm", + /** Linux AppImage */ + AppImage = "appimage", + /** macOS app bundle */ + App = "app" +} +/** + * Application metadata and related APIs. + * + * @module + */ +/** + * Gets the application version. + * @example + * ```typescript + * import { getVersion } from '@tauri-apps/api/app'; + * const appVersion = await getVersion(); + * ``` + * + * @since 1.0.0 + */ +declare function getVersion(): Promise; +/** + * Gets the application name. + * @example + * ```typescript + * import { getName } from '@tauri-apps/api/app'; + * const appName = await getName(); + * ``` + * + * @since 1.0.0 + */ +declare function getName(): Promise; +/** + * Gets the Tauri framework version used by this application. + * + * @example + * ```typescript + * import { getTauriVersion } from '@tauri-apps/api/app'; + * const tauriVersion = await getTauriVersion(); + * ``` + * + * @since 1.0.0 + */ +declare function getTauriVersion(): Promise; +/** + * Gets the application identifier. + * @example + * ```typescript + * import { getIdentifier } from '@tauri-apps/api/app'; + * const identifier = await getIdentifier(); + * ``` + * + * @returns The application identifier as configured in `tauri.conf.json`. + * + * @since 2.4.0 + */ +declare function getIdentifier(): Promise; +/** + * Shows the application on macOS. This function does not automatically + * focus any specific app window. + * + * @example + * ```typescript + * import { show } from '@tauri-apps/api/app'; + * await show(); + * ``` + * + * @since 1.2.0 + */ +declare function show(): Promise; +/** + * Hides the application on macOS. + * + * @example + * ```typescript + * import { hide } from '@tauri-apps/api/app'; + * await hide(); + * ``` + * + * @since 1.2.0 + */ +declare function hide(): Promise; +/** + * Fetches the data store identifiers on macOS and iOS. + * + * See https://developer.apple.com/documentation/webkit/wkwebsitedatastore for more information. + * + * @example + * ```typescript + * import { fetchDataStoreIdentifiers } from '@tauri-apps/api/app'; + * const ids = await fetchDataStoreIdentifiers(); + * ``` + * + * @since 2.4.0 + */ +declare function fetchDataStoreIdentifiers(): Promise; +/** + * Removes the data store with the given identifier. + * + * Note that any webview using this data store should be closed before running this API. + * + * See https://developer.apple.com/documentation/webkit/wkwebsitedatastore for more information. + * + * @example + * ```typescript + * import { fetchDataStoreIdentifiers, removeDataStore } from '@tauri-apps/api/app'; + * for (const id of (await fetchDataStoreIdentifiers())) { + * await removeDataStore(id); + * } + * ``` + * + * @since 2.4.0 + */ +declare function removeDataStore(uuid: DataStoreIdentifier): Promise; +/** + * Gets the default window icon. + * + * @example + * ```typescript + * import { defaultWindowIcon } from '@tauri-apps/api/app'; + * const icon = await defaultWindowIcon(); + * ``` + * + * @since 2.0.0 + */ +declare function defaultWindowIcon(): Promise; +/** + * Sets the application's theme. Pass in `null` or `undefined` to follow + * the system theme. + * + * @example + * ```typescript + * import { setTheme } from '@tauri-apps/api/app'; + * await setTheme('dark'); + * ``` + * + * #### Platform-specific + * + * - **iOS / Android:** Unsupported. + * + * @since 2.0.0 + */ +declare function setTheme(theme?: Theme | null): Promise; +/** + * Sets the dock visibility for the application on macOS. + * + * @param visible - Whether the dock should be visible or not. + * + * @example + * ```typescript + * import { setDockVisibility } from '@tauri-apps/api/app'; + * await setDockVisibility(false); + * ``` + * + * @since 2.5.0 + */ +declare function setDockVisibility(visible: boolean): Promise; +/** + * Gets the application bundle type. + * + * @example + * ```typescript + * import { getBundleType } from '@tauri-apps/api/app'; + * const type = await getBundleType(); + * ``` + * + * @since 2.5.0 + */ +declare function getBundleType(): Promise; +/** + * Payload for the onBackButtonPress event. + */ +type OnBackButtonPressPayload = { + /** Whether the webview canGoBack property is true. */ + canGoBack: boolean; +}; +/** + * Listens to the backButton event on Android. + * @param handler + */ +declare function onBackButtonPress(handler: (payload: OnBackButtonPressPayload) => void): Promise; +export { getName, getVersion, getTauriVersion, getIdentifier, show, hide, defaultWindowIcon, setTheme, fetchDataStoreIdentifiers, removeDataStore, setDockVisibility, getBundleType, type OnBackButtonPressPayload, onBackButtonPress }; diff --git a/node_modules/@tauri-apps/api/app.js b/node_modules/@tauri-apps/api/app.js new file mode 100644 index 0000000..c1f2228 --- /dev/null +++ b/node_modules/@tauri-apps/api/app.js @@ -0,0 +1,221 @@ +import { invoke, addPluginListener } from './core.js'; +import { Image } from './image.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * Bundle type of the current application. + */ +var BundleType; +(function (BundleType) { + /** Windows NSIS */ + BundleType["Nsis"] = "nsis"; + /** Windows MSI */ + BundleType["Msi"] = "msi"; + /** Linux Debian package */ + BundleType["Deb"] = "deb"; + /** Linux RPM */ + BundleType["Rpm"] = "rpm"; + /** Linux AppImage */ + BundleType["AppImage"] = "appimage"; + /** macOS app bundle */ + BundleType["App"] = "app"; +})(BundleType || (BundleType = {})); +/** + * Application metadata and related APIs. + * + * @module + */ +/** + * Gets the application version. + * @example + * ```typescript + * import { getVersion } from '@tauri-apps/api/app'; + * const appVersion = await getVersion(); + * ``` + * + * @since 1.0.0 + */ +async function getVersion() { + return invoke('plugin:app|version'); +} +/** + * Gets the application name. + * @example + * ```typescript + * import { getName } from '@tauri-apps/api/app'; + * const appName = await getName(); + * ``` + * + * @since 1.0.0 + */ +async function getName() { + return invoke('plugin:app|name'); +} +/** + * Gets the Tauri framework version used by this application. + * + * @example + * ```typescript + * import { getTauriVersion } from '@tauri-apps/api/app'; + * const tauriVersion = await getTauriVersion(); + * ``` + * + * @since 1.0.0 + */ +async function getTauriVersion() { + return invoke('plugin:app|tauri_version'); +} +/** + * Gets the application identifier. + * @example + * ```typescript + * import { getIdentifier } from '@tauri-apps/api/app'; + * const identifier = await getIdentifier(); + * ``` + * + * @returns The application identifier as configured in `tauri.conf.json`. + * + * @since 2.4.0 + */ +async function getIdentifier() { + return invoke('plugin:app|identifier'); +} +/** + * Shows the application on macOS. This function does not automatically + * focus any specific app window. + * + * @example + * ```typescript + * import { show } from '@tauri-apps/api/app'; + * await show(); + * ``` + * + * @since 1.2.0 + */ +async function show() { + return invoke('plugin:app|app_show'); +} +/** + * Hides the application on macOS. + * + * @example + * ```typescript + * import { hide } from '@tauri-apps/api/app'; + * await hide(); + * ``` + * + * @since 1.2.0 + */ +async function hide() { + return invoke('plugin:app|app_hide'); +} +/** + * Fetches the data store identifiers on macOS and iOS. + * + * See https://developer.apple.com/documentation/webkit/wkwebsitedatastore for more information. + * + * @example + * ```typescript + * import { fetchDataStoreIdentifiers } from '@tauri-apps/api/app'; + * const ids = await fetchDataStoreIdentifiers(); + * ``` + * + * @since 2.4.0 + */ +async function fetchDataStoreIdentifiers() { + return invoke('plugin:app|fetch_data_store_identifiers'); +} +/** + * Removes the data store with the given identifier. + * + * Note that any webview using this data store should be closed before running this API. + * + * See https://developer.apple.com/documentation/webkit/wkwebsitedatastore for more information. + * + * @example + * ```typescript + * import { fetchDataStoreIdentifiers, removeDataStore } from '@tauri-apps/api/app'; + * for (const id of (await fetchDataStoreIdentifiers())) { + * await removeDataStore(id); + * } + * ``` + * + * @since 2.4.0 + */ +async function removeDataStore(uuid) { + return invoke('plugin:app|remove_data_store', { uuid }); +} +/** + * Gets the default window icon. + * + * @example + * ```typescript + * import { defaultWindowIcon } from '@tauri-apps/api/app'; + * const icon = await defaultWindowIcon(); + * ``` + * + * @since 2.0.0 + */ +async function defaultWindowIcon() { + return invoke('plugin:app|default_window_icon').then((rid) => rid ? new Image(rid) : null); +} +/** + * Sets the application's theme. Pass in `null` or `undefined` to follow + * the system theme. + * + * @example + * ```typescript + * import { setTheme } from '@tauri-apps/api/app'; + * await setTheme('dark'); + * ``` + * + * #### Platform-specific + * + * - **iOS / Android:** Unsupported. + * + * @since 2.0.0 + */ +async function setTheme(theme) { + return invoke('plugin:app|set_app_theme', { theme }); +} +/** + * Sets the dock visibility for the application on macOS. + * + * @param visible - Whether the dock should be visible or not. + * + * @example + * ```typescript + * import { setDockVisibility } from '@tauri-apps/api/app'; + * await setDockVisibility(false); + * ``` + * + * @since 2.5.0 + */ +async function setDockVisibility(visible) { + return invoke('plugin:app|set_dock_visibility', { visible }); +} +/** + * Gets the application bundle type. + * + * @example + * ```typescript + * import { getBundleType } from '@tauri-apps/api/app'; + * const type = await getBundleType(); + * ``` + * + * @since 2.5.0 + */ +async function getBundleType() { + return invoke('plugin:app|bundle_type'); +} +/** + * Listens to the backButton event on Android. + * @param handler + */ +async function onBackButtonPress(handler) { + return addPluginListener('app', 'back-button', handler); +} + +export { BundleType, defaultWindowIcon, fetchDataStoreIdentifiers, getBundleType, getIdentifier, getName, getTauriVersion, getVersion, hide, onBackButtonPress, removeDataStore, setDockVisibility, setTheme, show }; diff --git a/node_modules/@tauri-apps/api/core.cjs b/node_modules/@tauri-apps/api/core.cjs new file mode 100644 index 0000000..8046d4e --- /dev/null +++ b/node_modules/@tauri-apps/api/core.cjs @@ -0,0 +1,295 @@ +'use strict'; + +var tslib_es6 = require('./external/tslib/tslib.es6.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +var _Channel_onmessage, _Channel_nextMessageIndex, _Channel_pendingMessages, _Channel_messageEndIndex, _Resource_rid; +/** + * Invoke your custom commands. + * + * This package is also accessible with `window.__TAURI__.core` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * @module + */ +/** + * A key to be used to implement a special function + * on your types that define how your type should be serialized + * when passing across the IPC. + * @example + * Given a type in Rust that looks like this + * ```rs + * #[derive(serde::Serialize, serde::Deserialize) + * enum UserId { + * String(String), + * Number(u32), + * } + * ``` + * `UserId::String("id")` would be serialized into `{ String: "id" }` + * and so we need to pass the same structure back to Rust + * ```ts + * import { SERIALIZE_TO_IPC_FN } from "@tauri-apps/api/core" + * + * class UserIdString { + * id + * constructor(id) { + * this.id = id + * } + * + * [SERIALIZE_TO_IPC_FN]() { + * return { String: this.id } + * } + * } + * + * class UserIdNumber { + * id + * constructor(id) { + * this.id = id + * } + * + * [SERIALIZE_TO_IPC_FN]() { + * return { Number: this.id } + * } + * } + * + * type UserId = UserIdString | UserIdNumber + * ``` + * + */ +// if this value changes, make sure to update it in: +// 1. ipc.js +// 2. process-ipc-message-fn.js +const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__'; +/** + * Stores the callback in a known location, and returns an identifier that can be passed to the backend. + * The backend uses the identifier to `eval()` the callback. + * + * @return An unique identifier associated with the callback function. + * + * @since 1.0.0 + */ +function transformCallback( +// TODO: Make this not optional in v3 +callback, once = false) { + return window.__TAURI_INTERNALS__.transformCallback(callback, once); +} +class Channel { + constructor(onmessage) { + _Channel_onmessage.set(this, void 0); + // the index is used as a mechanism to preserve message order + _Channel_nextMessageIndex.set(this, 0); + _Channel_pendingMessages.set(this, []); + _Channel_messageEndIndex.set(this, void 0); + tslib_es6.__classPrivateFieldSet(this, _Channel_onmessage, onmessage || (() => { }), "f"); + this.id = transformCallback((rawMessage) => { + const index = rawMessage.index; + if ('end' in rawMessage) { + if (index == tslib_es6.__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")) { + this.cleanupCallback(); + } + else { + tslib_es6.__classPrivateFieldSet(this, _Channel_messageEndIndex, index, "f"); + } + return; + } + const message = rawMessage.message; + // Process the message if we're at the right order + if (index == tslib_es6.__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")) { + tslib_es6.__classPrivateFieldGet(this, _Channel_onmessage, "f").call(this, message); + tslib_es6.__classPrivateFieldSet(this, _Channel_nextMessageIndex, tslib_es6.__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") + 1, "f"); + // process pending messages + while (tslib_es6.__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") in tslib_es6.__classPrivateFieldGet(this, _Channel_pendingMessages, "f")) { + const message = tslib_es6.__classPrivateFieldGet(this, _Channel_pendingMessages, "f")[tslib_es6.__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")]; + tslib_es6.__classPrivateFieldGet(this, _Channel_onmessage, "f").call(this, message); + // eslint-disable-next-line @typescript-eslint/no-array-delete + delete tslib_es6.__classPrivateFieldGet(this, _Channel_pendingMessages, "f")[tslib_es6.__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")]; + tslib_es6.__classPrivateFieldSet(this, _Channel_nextMessageIndex, tslib_es6.__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") + 1, "f"); + } + if (tslib_es6.__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") === tslib_es6.__classPrivateFieldGet(this, _Channel_messageEndIndex, "f")) { + this.cleanupCallback(); + } + } + // Queue the message if we're not + else { + // eslint-disable-next-line security/detect-object-injection + tslib_es6.__classPrivateFieldGet(this, _Channel_pendingMessages, "f")[index] = message; + } + }); + } + cleanupCallback() { + window.__TAURI_INTERNALS__.unregisterCallback(this.id); + } + set onmessage(handler) { + tslib_es6.__classPrivateFieldSet(this, _Channel_onmessage, handler, "f"); + } + get onmessage() { + return tslib_es6.__classPrivateFieldGet(this, _Channel_onmessage, "f"); + } + [(_Channel_onmessage = new WeakMap(), _Channel_nextMessageIndex = new WeakMap(), _Channel_pendingMessages = new WeakMap(), _Channel_messageEndIndex = new WeakMap(), SERIALIZE_TO_IPC_FN)]() { + return `__CHANNEL__:${this.id}`; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN](); + } +} +class PluginListener { + constructor(plugin, event, channelId) { + this.plugin = plugin; + this.event = event; + this.channelId = channelId; + } + async unregister() { + return invoke(`plugin:${this.plugin}|remove_listener`, { + event: this.event, + channelId: this.channelId + }); + } +} +/** + * Adds a listener to a plugin event. + * + * @returns The listener object to stop listening to the events. + * + * @since 2.0.0 + */ +async function addPluginListener(plugin, event, cb) { + const handler = new Channel(cb); + try { + await invoke(`plugin:${plugin}|register_listener`, { + event, + handler + }); + return new PluginListener(plugin, event, handler.id); + } + catch { + // TODO(v3): remove this fallback + // note: we must try with camelCase here for backwards compatibility + await invoke(`plugin:${plugin}|registerListener`, { event, handler }); + return new PluginListener(plugin, event, handler.id); + } +} +/** + * Get permission state for a plugin. + * + * This should be used by plugin authors to wrap their actual implementation. + */ +async function checkPermissions(plugin) { + return invoke(`plugin:${plugin}|check_permissions`); +} +/** + * Request permissions. + * + * This should be used by plugin authors to wrap their actual implementation. + */ +async function requestPermissions(plugin) { + return invoke(`plugin:${plugin}|request_permissions`); +} +/** + * Sends a message to the backend. + * @example + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * await invoke('login', { user: 'tauri', password: 'poiwe3h4r5ip3yrhtew9ty' }); + * ``` + * + * @param cmd The command name. + * @param args The optional arguments to pass to the command. + * @param options The request options. + * @return A promise resolving or rejecting to the backend response. + * + * @since 1.0.0 + */ +async function invoke(cmd, args = {}, options) { + return window.__TAURI_INTERNALS__.invoke(cmd, args, options); +} +/** + * Convert a device file path to an URL that can be loaded by the webview. + * Note that `asset:` and `http://asset.localhost` must be added to [`app.security.csp`](https://v2.tauri.app/reference/config/#csp-1) in `tauri.conf.json`. + * Example CSP value: `"csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost"` to use the asset protocol on image sources. + * + * Additionally, `"enable" : "true"` must be added to [`app.security.assetProtocol`](https://v2.tauri.app/reference/config/#assetprotocolconfig) + * in `tauri.conf.json` and its access scope must be defined on the `scope` array on the same `assetProtocol` object. + * + * @param filePath The file path. + * @param protocol The protocol to use. Defaults to `asset`. You only need to set this when using a custom protocol. + * @example + * ```typescript + * import { appDataDir, join } from '@tauri-apps/api/path'; + * import { convertFileSrc } from '@tauri-apps/api/core'; + * const appDataDirPath = await appDataDir(); + * const filePath = await join(appDataDirPath, 'assets/video.mp4'); + * const assetUrl = convertFileSrc(filePath); + * + * const video = document.getElementById('my-video'); + * const source = document.createElement('source'); + * source.type = 'video/mp4'; + * source.src = assetUrl; + * video.appendChild(source); + * video.load(); + * ``` + * + * @return the URL that can be used as source on the webview. + * + * @since 1.0.0 + */ +function convertFileSrc(filePath, protocol = 'asset') { + return window.__TAURI_INTERNALS__.convertFileSrc(filePath, protocol); +} +/** + * A rust-backed resource stored through `tauri::Manager::resources_table` API. + * + * The resource lives in the main process and does not exist + * in the Javascript world, and thus will not be cleaned up automatically + * except on application exit. If you want to clean it up early, call {@linkcode Resource.close} + * + * @example + * ```typescript + * import { Resource, invoke } from '@tauri-apps/api/core'; + * export class DatabaseHandle extends Resource { + * static async open(path: string): Promise { + * const rid: number = await invoke('open_db', { path }); + * return new DatabaseHandle(rid); + * } + * + * async execute(sql: string): Promise { + * await invoke('execute_sql', { rid: this.rid, sql }); + * } + * } + * ``` + */ +class Resource { + get rid() { + return tslib_es6.__classPrivateFieldGet(this, _Resource_rid, "f"); + } + constructor(rid) { + _Resource_rid.set(this, void 0); + tslib_es6.__classPrivateFieldSet(this, _Resource_rid, rid, "f"); + } + /** + * Destroys and cleans up this resource from memory. + * **You should not call any method on this object anymore and should drop any reference to it.** + */ + async close() { + return invoke('plugin:resources|close', { + rid: this.rid + }); + } +} +_Resource_rid = new WeakMap(); +function isTauri() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + return !!(globalThis || window).isTauri; +} + +exports.Channel = Channel; +exports.PluginListener = PluginListener; +exports.Resource = Resource; +exports.SERIALIZE_TO_IPC_FN = SERIALIZE_TO_IPC_FN; +exports.addPluginListener = addPluginListener; +exports.checkPermissions = checkPermissions; +exports.convertFileSrc = convertFileSrc; +exports.invoke = invoke; +exports.isTauri = isTauri; +exports.requestPermissions = requestPermissions; +exports.transformCallback = transformCallback; diff --git a/node_modules/@tauri-apps/api/core.d.ts b/node_modules/@tauri-apps/api/core.d.ts new file mode 100644 index 0000000..7e126b9 --- /dev/null +++ b/node_modules/@tauri-apps/api/core.d.ts @@ -0,0 +1,193 @@ +/** + * Invoke your custom commands. + * + * This package is also accessible with `window.__TAURI__.core` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * @module + */ +/** + * A key to be used to implement a special function + * on your types that define how your type should be serialized + * when passing across the IPC. + * @example + * Given a type in Rust that looks like this + * ```rs + * #[derive(serde::Serialize, serde::Deserialize) + * enum UserId { + * String(String), + * Number(u32), + * } + * ``` + * `UserId::String("id")` would be serialized into `{ String: "id" }` + * and so we need to pass the same structure back to Rust + * ```ts + * import { SERIALIZE_TO_IPC_FN } from "@tauri-apps/api/core" + * + * class UserIdString { + * id + * constructor(id) { + * this.id = id + * } + * + * [SERIALIZE_TO_IPC_FN]() { + * return { String: this.id } + * } + * } + * + * class UserIdNumber { + * id + * constructor(id) { + * this.id = id + * } + * + * [SERIALIZE_TO_IPC_FN]() { + * return { Number: this.id } + * } + * } + * + * type UserId = UserIdString | UserIdNumber + * ``` + * + */ +export declare const SERIALIZE_TO_IPC_FN = "__TAURI_TO_IPC_KEY__"; +/** + * Stores the callback in a known location, and returns an identifier that can be passed to the backend. + * The backend uses the identifier to `eval()` the callback. + * + * @return An unique identifier associated with the callback function. + * + * @since 1.0.0 + */ +declare function transformCallback(callback?: (response: T) => void, once?: boolean): number; +declare class Channel { + #private; + /** The callback id returned from {@linkcode transformCallback} */ + id: number; + constructor(onmessage?: (response: T) => void); + private cleanupCallback; + set onmessage(handler: (response: T) => void); + get onmessage(): (response: T) => void; + [SERIALIZE_TO_IPC_FN](): string; + toJSON(): string; +} +declare class PluginListener { + plugin: string; + event: string; + channelId: number; + constructor(plugin: string, event: string, channelId: number); + unregister(): Promise; +} +/** + * Adds a listener to a plugin event. + * + * @returns The listener object to stop listening to the events. + * + * @since 2.0.0 + */ +declare function addPluginListener(plugin: string, event: string, cb: (payload: T) => void): Promise; +type PermissionState = 'granted' | 'denied' | 'prompt' | 'prompt-with-rationale'; +/** + * Get permission state for a plugin. + * + * This should be used by plugin authors to wrap their actual implementation. + */ +declare function checkPermissions(plugin: string): Promise; +/** + * Request permissions. + * + * This should be used by plugin authors to wrap their actual implementation. + */ +declare function requestPermissions(plugin: string): Promise; +/** + * Command arguments. + * + * @since 1.0.0 + */ +type InvokeArgs = Record | number[] | ArrayBuffer | Uint8Array; +/** + * @since 2.0.0 + */ +interface InvokeOptions { + headers: HeadersInit; +} +/** + * Sends a message to the backend. + * @example + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * await invoke('login', { user: 'tauri', password: 'poiwe3h4r5ip3yrhtew9ty' }); + * ``` + * + * @param cmd The command name. + * @param args The optional arguments to pass to the command. + * @param options The request options. + * @return A promise resolving or rejecting to the backend response. + * + * @since 1.0.0 + */ +declare function invoke(cmd: string, args?: InvokeArgs, options?: InvokeOptions): Promise; +/** + * Convert a device file path to an URL that can be loaded by the webview. + * Note that `asset:` and `http://asset.localhost` must be added to [`app.security.csp`](https://v2.tauri.app/reference/config/#csp-1) in `tauri.conf.json`. + * Example CSP value: `"csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost"` to use the asset protocol on image sources. + * + * Additionally, `"enable" : "true"` must be added to [`app.security.assetProtocol`](https://v2.tauri.app/reference/config/#assetprotocolconfig) + * in `tauri.conf.json` and its access scope must be defined on the `scope` array on the same `assetProtocol` object. + * + * @param filePath The file path. + * @param protocol The protocol to use. Defaults to `asset`. You only need to set this when using a custom protocol. + * @example + * ```typescript + * import { appDataDir, join } from '@tauri-apps/api/path'; + * import { convertFileSrc } from '@tauri-apps/api/core'; + * const appDataDirPath = await appDataDir(); + * const filePath = await join(appDataDirPath, 'assets/video.mp4'); + * const assetUrl = convertFileSrc(filePath); + * + * const video = document.getElementById('my-video'); + * const source = document.createElement('source'); + * source.type = 'video/mp4'; + * source.src = assetUrl; + * video.appendChild(source); + * video.load(); + * ``` + * + * @return the URL that can be used as source on the webview. + * + * @since 1.0.0 + */ +declare function convertFileSrc(filePath: string, protocol?: string): string; +/** + * A rust-backed resource stored through `tauri::Manager::resources_table` API. + * + * The resource lives in the main process and does not exist + * in the Javascript world, and thus will not be cleaned up automatically + * except on application exit. If you want to clean it up early, call {@linkcode Resource.close} + * + * @example + * ```typescript + * import { Resource, invoke } from '@tauri-apps/api/core'; + * export class DatabaseHandle extends Resource { + * static async open(path: string): Promise { + * const rid: number = await invoke('open_db', { path }); + * return new DatabaseHandle(rid); + * } + * + * async execute(sql: string): Promise { + * await invoke('execute_sql', { rid: this.rid, sql }); + * } + * } + * ``` + */ +export declare class Resource { + #private; + get rid(): number; + constructor(rid: number); + /** + * Destroys and cleans up this resource from memory. + * **You should not call any method on this object anymore and should drop any reference to it.** + */ + close(): Promise; +} +declare function isTauri(): boolean; +export type { InvokeArgs, InvokeOptions }; +export { transformCallback, Channel, PluginListener, addPluginListener, PermissionState, checkPermissions, requestPermissions, invoke, convertFileSrc, isTauri }; diff --git a/node_modules/@tauri-apps/api/core.js b/node_modules/@tauri-apps/api/core.js new file mode 100644 index 0000000..ad03e61 --- /dev/null +++ b/node_modules/@tauri-apps/api/core.js @@ -0,0 +1,283 @@ +import { __classPrivateFieldSet, __classPrivateFieldGet } from './external/tslib/tslib.es6.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +var _Channel_onmessage, _Channel_nextMessageIndex, _Channel_pendingMessages, _Channel_messageEndIndex, _Resource_rid; +/** + * Invoke your custom commands. + * + * This package is also accessible with `window.__TAURI__.core` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * @module + */ +/** + * A key to be used to implement a special function + * on your types that define how your type should be serialized + * when passing across the IPC. + * @example + * Given a type in Rust that looks like this + * ```rs + * #[derive(serde::Serialize, serde::Deserialize) + * enum UserId { + * String(String), + * Number(u32), + * } + * ``` + * `UserId::String("id")` would be serialized into `{ String: "id" }` + * and so we need to pass the same structure back to Rust + * ```ts + * import { SERIALIZE_TO_IPC_FN } from "@tauri-apps/api/core" + * + * class UserIdString { + * id + * constructor(id) { + * this.id = id + * } + * + * [SERIALIZE_TO_IPC_FN]() { + * return { String: this.id } + * } + * } + * + * class UserIdNumber { + * id + * constructor(id) { + * this.id = id + * } + * + * [SERIALIZE_TO_IPC_FN]() { + * return { Number: this.id } + * } + * } + * + * type UserId = UserIdString | UserIdNumber + * ``` + * + */ +// if this value changes, make sure to update it in: +// 1. ipc.js +// 2. process-ipc-message-fn.js +const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__'; +/** + * Stores the callback in a known location, and returns an identifier that can be passed to the backend. + * The backend uses the identifier to `eval()` the callback. + * + * @return An unique identifier associated with the callback function. + * + * @since 1.0.0 + */ +function transformCallback( +// TODO: Make this not optional in v3 +callback, once = false) { + return window.__TAURI_INTERNALS__.transformCallback(callback, once); +} +class Channel { + constructor(onmessage) { + _Channel_onmessage.set(this, void 0); + // the index is used as a mechanism to preserve message order + _Channel_nextMessageIndex.set(this, 0); + _Channel_pendingMessages.set(this, []); + _Channel_messageEndIndex.set(this, void 0); + __classPrivateFieldSet(this, _Channel_onmessage, onmessage || (() => { }), "f"); + this.id = transformCallback((rawMessage) => { + const index = rawMessage.index; + if ('end' in rawMessage) { + if (index == __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")) { + this.cleanupCallback(); + } + else { + __classPrivateFieldSet(this, _Channel_messageEndIndex, index, "f"); + } + return; + } + const message = rawMessage.message; + // Process the message if we're at the right order + if (index == __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")) { + __classPrivateFieldGet(this, _Channel_onmessage, "f").call(this, message); + __classPrivateFieldSet(this, _Channel_nextMessageIndex, __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") + 1, "f"); + // process pending messages + while (__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") in __classPrivateFieldGet(this, _Channel_pendingMessages, "f")) { + const message = __classPrivateFieldGet(this, _Channel_pendingMessages, "f")[__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")]; + __classPrivateFieldGet(this, _Channel_onmessage, "f").call(this, message); + // eslint-disable-next-line @typescript-eslint/no-array-delete + delete __classPrivateFieldGet(this, _Channel_pendingMessages, "f")[__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")]; + __classPrivateFieldSet(this, _Channel_nextMessageIndex, __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") + 1, "f"); + } + if (__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") === __classPrivateFieldGet(this, _Channel_messageEndIndex, "f")) { + this.cleanupCallback(); + } + } + // Queue the message if we're not + else { + // eslint-disable-next-line security/detect-object-injection + __classPrivateFieldGet(this, _Channel_pendingMessages, "f")[index] = message; + } + }); + } + cleanupCallback() { + window.__TAURI_INTERNALS__.unregisterCallback(this.id); + } + set onmessage(handler) { + __classPrivateFieldSet(this, _Channel_onmessage, handler, "f"); + } + get onmessage() { + return __classPrivateFieldGet(this, _Channel_onmessage, "f"); + } + [(_Channel_onmessage = new WeakMap(), _Channel_nextMessageIndex = new WeakMap(), _Channel_pendingMessages = new WeakMap(), _Channel_messageEndIndex = new WeakMap(), SERIALIZE_TO_IPC_FN)]() { + return `__CHANNEL__:${this.id}`; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN](); + } +} +class PluginListener { + constructor(plugin, event, channelId) { + this.plugin = plugin; + this.event = event; + this.channelId = channelId; + } + async unregister() { + return invoke(`plugin:${this.plugin}|remove_listener`, { + event: this.event, + channelId: this.channelId + }); + } +} +/** + * Adds a listener to a plugin event. + * + * @returns The listener object to stop listening to the events. + * + * @since 2.0.0 + */ +async function addPluginListener(plugin, event, cb) { + const handler = new Channel(cb); + try { + await invoke(`plugin:${plugin}|register_listener`, { + event, + handler + }); + return new PluginListener(plugin, event, handler.id); + } + catch { + // TODO(v3): remove this fallback + // note: we must try with camelCase here for backwards compatibility + await invoke(`plugin:${plugin}|registerListener`, { event, handler }); + return new PluginListener(plugin, event, handler.id); + } +} +/** + * Get permission state for a plugin. + * + * This should be used by plugin authors to wrap their actual implementation. + */ +async function checkPermissions(plugin) { + return invoke(`plugin:${plugin}|check_permissions`); +} +/** + * Request permissions. + * + * This should be used by plugin authors to wrap their actual implementation. + */ +async function requestPermissions(plugin) { + return invoke(`plugin:${plugin}|request_permissions`); +} +/** + * Sends a message to the backend. + * @example + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * await invoke('login', { user: 'tauri', password: 'poiwe3h4r5ip3yrhtew9ty' }); + * ``` + * + * @param cmd The command name. + * @param args The optional arguments to pass to the command. + * @param options The request options. + * @return A promise resolving or rejecting to the backend response. + * + * @since 1.0.0 + */ +async function invoke(cmd, args = {}, options) { + return window.__TAURI_INTERNALS__.invoke(cmd, args, options); +} +/** + * Convert a device file path to an URL that can be loaded by the webview. + * Note that `asset:` and `http://asset.localhost` must be added to [`app.security.csp`](https://v2.tauri.app/reference/config/#csp-1) in `tauri.conf.json`. + * Example CSP value: `"csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost"` to use the asset protocol on image sources. + * + * Additionally, `"enable" : "true"` must be added to [`app.security.assetProtocol`](https://v2.tauri.app/reference/config/#assetprotocolconfig) + * in `tauri.conf.json` and its access scope must be defined on the `scope` array on the same `assetProtocol` object. + * + * @param filePath The file path. + * @param protocol The protocol to use. Defaults to `asset`. You only need to set this when using a custom protocol. + * @example + * ```typescript + * import { appDataDir, join } from '@tauri-apps/api/path'; + * import { convertFileSrc } from '@tauri-apps/api/core'; + * const appDataDirPath = await appDataDir(); + * const filePath = await join(appDataDirPath, 'assets/video.mp4'); + * const assetUrl = convertFileSrc(filePath); + * + * const video = document.getElementById('my-video'); + * const source = document.createElement('source'); + * source.type = 'video/mp4'; + * source.src = assetUrl; + * video.appendChild(source); + * video.load(); + * ``` + * + * @return the URL that can be used as source on the webview. + * + * @since 1.0.0 + */ +function convertFileSrc(filePath, protocol = 'asset') { + return window.__TAURI_INTERNALS__.convertFileSrc(filePath, protocol); +} +/** + * A rust-backed resource stored through `tauri::Manager::resources_table` API. + * + * The resource lives in the main process and does not exist + * in the Javascript world, and thus will not be cleaned up automatically + * except on application exit. If you want to clean it up early, call {@linkcode Resource.close} + * + * @example + * ```typescript + * import { Resource, invoke } from '@tauri-apps/api/core'; + * export class DatabaseHandle extends Resource { + * static async open(path: string): Promise { + * const rid: number = await invoke('open_db', { path }); + * return new DatabaseHandle(rid); + * } + * + * async execute(sql: string): Promise { + * await invoke('execute_sql', { rid: this.rid, sql }); + * } + * } + * ``` + */ +class Resource { + get rid() { + return __classPrivateFieldGet(this, _Resource_rid, "f"); + } + constructor(rid) { + _Resource_rid.set(this, void 0); + __classPrivateFieldSet(this, _Resource_rid, rid, "f"); + } + /** + * Destroys and cleans up this resource from memory. + * **You should not call any method on this object anymore and should drop any reference to it.** + */ + async close() { + return invoke('plugin:resources|close', { + rid: this.rid + }); + } +} +_Resource_rid = new WeakMap(); +function isTauri() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + return !!(globalThis || window).isTauri; +} + +export { Channel, PluginListener, Resource, SERIALIZE_TO_IPC_FN, addPluginListener, checkPermissions, convertFileSrc, invoke, isTauri, requestPermissions, transformCallback }; diff --git a/node_modules/@tauri-apps/api/dpi.cjs b/node_modules/@tauri-apps/api/dpi.cjs new file mode 100644 index 0000000..972617f --- /dev/null +++ b/node_modules/@tauri-apps/api/dpi.cjs @@ -0,0 +1,347 @@ +'use strict'; + +var core = require('./core.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * A size represented in logical pixels. + * Logical pixels are scaled according to the window's DPI scale. + * Most browser APIs (i.e. `MouseEvent`'s `clientX`) will return logical pixels. + * + * For logical-pixel-based position, see {@linkcode LogicalPosition}. + * + * @since 2.0.0 + */ +class LogicalSize { + constructor(...args) { + this.type = 'Logical'; + if (args.length === 1) { + if ('Logical' in args[0]) { + this.width = args[0].Logical.width; + this.height = args[0].Logical.height; + } + else { + this.width = args[0].width; + this.height = args[0].height; + } + } + else { + this.width = args[0]; + this.height = args[1]; + } + } + /** + * Converts the logical size to a physical one. + * @example + * ```typescript + * import { LogicalSize } from '@tauri-apps/api/dpi'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const size = new LogicalSize(400, 500); + * const physical = size.toPhysical(factor); + * ``` + * + * @since 2.0.0 + */ + toPhysical(scaleFactor) { + return new PhysicalSize(this.width * scaleFactor, this.height * scaleFactor); + } + [core.SERIALIZE_TO_IPC_FN]() { + return { + width: this.width, + height: this.height + }; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[core.SERIALIZE_TO_IPC_FN](); + } +} +/** + * A size represented in physical pixels. + * + * Physical pixels represent actual screen pixels, and are DPI-independent. + * For high-DPI windows, this means that any point in the window on the screen + * will have a different position in logical pixels {@linkcode LogicalSize}. + * + * For physical-pixel-based position, see {@linkcode PhysicalPosition}. + * + * @since 2.0.0 + */ +class PhysicalSize { + constructor(...args) { + this.type = 'Physical'; + if (args.length === 1) { + if ('Physical' in args[0]) { + this.width = args[0].Physical.width; + this.height = args[0].Physical.height; + } + else { + this.width = args[0].width; + this.height = args[0].height; + } + } + else { + this.width = args[0]; + this.height = args[1]; + } + } + /** + * Converts the physical size to a logical one. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const size = await appWindow.innerSize(); // PhysicalSize + * const logical = size.toLogical(factor); + * ``` + */ + toLogical(scaleFactor) { + return new LogicalSize(this.width / scaleFactor, this.height / scaleFactor); + } + [core.SERIALIZE_TO_IPC_FN]() { + return { + width: this.width, + height: this.height + }; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[core.SERIALIZE_TO_IPC_FN](); + } +} +/** + * A size represented either in physical or in logical pixels. + * + * This type is basically a union type of {@linkcode LogicalSize} and {@linkcode PhysicalSize} + * but comes in handy when using `tauri::Size` in Rust as an argument to a command, as this class + * automatically serializes into a valid format so it can be deserialized correctly into `tauri::Size` + * + * So instead of + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalSize, PhysicalSize } from '@tauri-apps/api/dpi'; + * + * const size: LogicalSize | PhysicalSize = someFunction(); // where someFunction returns either LogicalSize or PhysicalSize + * const validSize = size instanceof LogicalSize + * ? { Logical: { width: size.width, height: size.height } } + * : { Physical: { width: size.width, height: size.height } } + * await invoke("do_something_with_size", { size: validSize }); + * ``` + * + * You can just use {@linkcode Size} + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalSize, PhysicalSize, Size } from '@tauri-apps/api/dpi'; + * + * const size: LogicalSize | PhysicalSize = someFunction(); // where someFunction returns either LogicalSize or PhysicalSize + * const validSize = new Size(size); + * await invoke("do_something_with_size", { size: validSize }); + * ``` + * + * @since 2.1.0 + */ +class Size { + constructor(size) { + this.size = size; + } + toLogical(scaleFactor) { + return this.size instanceof LogicalSize + ? this.size + : this.size.toLogical(scaleFactor); + } + toPhysical(scaleFactor) { + return this.size instanceof PhysicalSize + ? this.size + : this.size.toPhysical(scaleFactor); + } + [core.SERIALIZE_TO_IPC_FN]() { + return { + [`${this.size.type}`]: { + width: this.size.width, + height: this.size.height + } + }; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[core.SERIALIZE_TO_IPC_FN](); + } +} +/** + * A position represented in logical pixels. + * For an explanation of what logical pixels are, see description of {@linkcode LogicalSize}. + * + * @since 2.0.0 + */ +class LogicalPosition { + constructor(...args) { + this.type = 'Logical'; + if (args.length === 1) { + if ('Logical' in args[0]) { + this.x = args[0].Logical.x; + this.y = args[0].Logical.y; + } + else { + this.x = args[0].x; + this.y = args[0].y; + } + } + else { + this.x = args[0]; + this.y = args[1]; + } + } + /** + * Converts the logical position to a physical one. + * @example + * ```typescript + * import { LogicalPosition } from '@tauri-apps/api/dpi'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const position = new LogicalPosition(400, 500); + * const physical = position.toPhysical(factor); + * ``` + * + * @since 2.0.0 + */ + toPhysical(scaleFactor) { + return new PhysicalPosition(this.x * scaleFactor, this.y * scaleFactor); + } + [core.SERIALIZE_TO_IPC_FN]() { + return { + x: this.x, + y: this.y + }; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[core.SERIALIZE_TO_IPC_FN](); + } +} +/** + * A position represented in physical pixels. + * + * For an explanation of what physical pixels are, see description of {@linkcode PhysicalSize}. + * + * @since 2.0.0 + */ +class PhysicalPosition { + constructor(...args) { + this.type = 'Physical'; + if (args.length === 1) { + if ('Physical' in args[0]) { + this.x = args[0].Physical.x; + this.y = args[0].Physical.y; + } + else { + this.x = args[0].x; + this.y = args[0].y; + } + } + else { + this.x = args[0]; + this.y = args[1]; + } + } + /** + * Converts the physical position to a logical one. + * @example + * ```typescript + * import { PhysicalPosition } from '@tauri-apps/api/dpi'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const position = new PhysicalPosition(400, 500); + * const physical = position.toLogical(factor); + * ``` + * + * @since 2.0.0 + */ + toLogical(scaleFactor) { + return new LogicalPosition(this.x / scaleFactor, this.y / scaleFactor); + } + [core.SERIALIZE_TO_IPC_FN]() { + return { + x: this.x, + y: this.y + }; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[core.SERIALIZE_TO_IPC_FN](); + } +} +/** + * A position represented either in physical or in logical pixels. + * + * This type is basically a union type of {@linkcode LogicalSize} and {@linkcode PhysicalSize} + * but comes in handy when using `tauri::Position` in Rust as an argument to a command, as this class + * automatically serializes into a valid format so it can be deserialized correctly into `tauri::Position` + * + * So instead of + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalPosition, PhysicalPosition } from '@tauri-apps/api/dpi'; + * + * const position: LogicalPosition | PhysicalPosition = someFunction(); // where someFunction returns either LogicalPosition or PhysicalPosition + * const validPosition = position instanceof LogicalPosition + * ? { Logical: { x: position.x, y: position.y } } + * : { Physical: { x: position.x, y: position.y } } + * await invoke("do_something_with_position", { position: validPosition }); + * ``` + * + * You can just use {@linkcode Position} + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalPosition, PhysicalPosition, Position } from '@tauri-apps/api/dpi'; + * + * const position: LogicalPosition | PhysicalPosition = someFunction(); // where someFunction returns either LogicalPosition or PhysicalPosition + * const validPosition = new Position(position); + * await invoke("do_something_with_position", { position: validPosition }); + * ``` + * + * @since 2.1.0 + */ +class Position { + constructor(position) { + this.position = position; + } + toLogical(scaleFactor) { + return this.position instanceof LogicalPosition + ? this.position + : this.position.toLogical(scaleFactor); + } + toPhysical(scaleFactor) { + return this.position instanceof PhysicalPosition + ? this.position + : this.position.toPhysical(scaleFactor); + } + [core.SERIALIZE_TO_IPC_FN]() { + return { + [`${this.position.type}`]: { + x: this.position.x, + y: this.position.y + } + }; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[core.SERIALIZE_TO_IPC_FN](); + } +} + +exports.LogicalPosition = LogicalPosition; +exports.LogicalSize = LogicalSize; +exports.PhysicalPosition = PhysicalPosition; +exports.PhysicalSize = PhysicalSize; +exports.Position = Position; +exports.Size = Size; diff --git a/node_modules/@tauri-apps/api/dpi.d.ts b/node_modules/@tauri-apps/api/dpi.d.ts new file mode 100644 index 0000000..7e6c1da --- /dev/null +++ b/node_modules/@tauri-apps/api/dpi.d.ts @@ -0,0 +1,289 @@ +import { SERIALIZE_TO_IPC_FN } from './core'; +/** + * A size represented in logical pixels. + * Logical pixels are scaled according to the window's DPI scale. + * Most browser APIs (i.e. `MouseEvent`'s `clientX`) will return logical pixels. + * + * For logical-pixel-based position, see {@linkcode LogicalPosition}. + * + * @since 2.0.0 + */ +declare class LogicalSize { + readonly type = "Logical"; + width: number; + height: number; + constructor(width: number, height: number); + constructor(object: { + Logical: { + width: number; + height: number; + }; + }); + constructor(object: { + width: number; + height: number; + }); + /** + * Converts the logical size to a physical one. + * @example + * ```typescript + * import { LogicalSize } from '@tauri-apps/api/dpi'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const size = new LogicalSize(400, 500); + * const physical = size.toPhysical(factor); + * ``` + * + * @since 2.0.0 + */ + toPhysical(scaleFactor: number): PhysicalSize; + [SERIALIZE_TO_IPC_FN](): { + width: number; + height: number; + }; + toJSON(): { + width: number; + height: number; + }; +} +/** + * A size represented in physical pixels. + * + * Physical pixels represent actual screen pixels, and are DPI-independent. + * For high-DPI windows, this means that any point in the window on the screen + * will have a different position in logical pixels {@linkcode LogicalSize}. + * + * For physical-pixel-based position, see {@linkcode PhysicalPosition}. + * + * @since 2.0.0 + */ +declare class PhysicalSize { + readonly type = "Physical"; + width: number; + height: number; + constructor(width: number, height: number); + constructor(object: { + Physical: { + width: number; + height: number; + }; + }); + constructor(object: { + width: number; + height: number; + }); + /** + * Converts the physical size to a logical one. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const size = await appWindow.innerSize(); // PhysicalSize + * const logical = size.toLogical(factor); + * ``` + */ + toLogical(scaleFactor: number): LogicalSize; + [SERIALIZE_TO_IPC_FN](): { + width: number; + height: number; + }; + toJSON(): { + width: number; + height: number; + }; +} +/** + * A size represented either in physical or in logical pixels. + * + * This type is basically a union type of {@linkcode LogicalSize} and {@linkcode PhysicalSize} + * but comes in handy when using `tauri::Size` in Rust as an argument to a command, as this class + * automatically serializes into a valid format so it can be deserialized correctly into `tauri::Size` + * + * So instead of + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalSize, PhysicalSize } from '@tauri-apps/api/dpi'; + * + * const size: LogicalSize | PhysicalSize = someFunction(); // where someFunction returns either LogicalSize or PhysicalSize + * const validSize = size instanceof LogicalSize + * ? { Logical: { width: size.width, height: size.height } } + * : { Physical: { width: size.width, height: size.height } } + * await invoke("do_something_with_size", { size: validSize }); + * ``` + * + * You can just use {@linkcode Size} + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalSize, PhysicalSize, Size } from '@tauri-apps/api/dpi'; + * + * const size: LogicalSize | PhysicalSize = someFunction(); // where someFunction returns either LogicalSize or PhysicalSize + * const validSize = new Size(size); + * await invoke("do_something_with_size", { size: validSize }); + * ``` + * + * @since 2.1.0 + */ +declare class Size { + size: LogicalSize | PhysicalSize; + constructor(size: LogicalSize | PhysicalSize); + toLogical(scaleFactor: number): LogicalSize; + toPhysical(scaleFactor: number): PhysicalSize; + [SERIALIZE_TO_IPC_FN](): { + [x: string]: { + width: number; + height: number; + }; + }; + toJSON(): { + [x: string]: { + width: number; + height: number; + }; + }; +} +/** + * A position represented in logical pixels. + * For an explanation of what logical pixels are, see description of {@linkcode LogicalSize}. + * + * @since 2.0.0 + */ +declare class LogicalPosition { + readonly type = "Logical"; + x: number; + y: number; + constructor(x: number, y: number); + constructor(object: { + Logical: { + x: number; + y: number; + }; + }); + constructor(object: { + x: number; + y: number; + }); + /** + * Converts the logical position to a physical one. + * @example + * ```typescript + * import { LogicalPosition } from '@tauri-apps/api/dpi'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const position = new LogicalPosition(400, 500); + * const physical = position.toPhysical(factor); + * ``` + * + * @since 2.0.0 + */ + toPhysical(scaleFactor: number): PhysicalPosition; + [SERIALIZE_TO_IPC_FN](): { + x: number; + y: number; + }; + toJSON(): { + x: number; + y: number; + }; +} +/** + * A position represented in physical pixels. + * + * For an explanation of what physical pixels are, see description of {@linkcode PhysicalSize}. + * + * @since 2.0.0 + */ +declare class PhysicalPosition { + readonly type = "Physical"; + x: number; + y: number; + constructor(x: number, y: number); + constructor(object: { + Physical: { + x: number; + y: number; + }; + }); + constructor(object: { + x: number; + y: number; + }); + /** + * Converts the physical position to a logical one. + * @example + * ```typescript + * import { PhysicalPosition } from '@tauri-apps/api/dpi'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const position = new PhysicalPosition(400, 500); + * const physical = position.toLogical(factor); + * ``` + * + * @since 2.0.0 + */ + toLogical(scaleFactor: number): LogicalPosition; + [SERIALIZE_TO_IPC_FN](): { + x: number; + y: number; + }; + toJSON(): { + x: number; + y: number; + }; +} +/** + * A position represented either in physical or in logical pixels. + * + * This type is basically a union type of {@linkcode LogicalSize} and {@linkcode PhysicalSize} + * but comes in handy when using `tauri::Position` in Rust as an argument to a command, as this class + * automatically serializes into a valid format so it can be deserialized correctly into `tauri::Position` + * + * So instead of + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalPosition, PhysicalPosition } from '@tauri-apps/api/dpi'; + * + * const position: LogicalPosition | PhysicalPosition = someFunction(); // where someFunction returns either LogicalPosition or PhysicalPosition + * const validPosition = position instanceof LogicalPosition + * ? { Logical: { x: position.x, y: position.y } } + * : { Physical: { x: position.x, y: position.y } } + * await invoke("do_something_with_position", { position: validPosition }); + * ``` + * + * You can just use {@linkcode Position} + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalPosition, PhysicalPosition, Position } from '@tauri-apps/api/dpi'; + * + * const position: LogicalPosition | PhysicalPosition = someFunction(); // where someFunction returns either LogicalPosition or PhysicalPosition + * const validPosition = new Position(position); + * await invoke("do_something_with_position", { position: validPosition }); + * ``` + * + * @since 2.1.0 + */ +declare class Position { + position: LogicalPosition | PhysicalPosition; + constructor(position: LogicalPosition | PhysicalPosition); + toLogical(scaleFactor: number): LogicalPosition; + toPhysical(scaleFactor: number): PhysicalPosition; + [SERIALIZE_TO_IPC_FN](): { + [x: string]: { + x: number; + y: number; + }; + }; + toJSON(): { + [x: string]: { + x: number; + y: number; + }; + }; +} +export { LogicalPosition, LogicalSize, Size, PhysicalPosition, PhysicalSize, Position }; diff --git a/node_modules/@tauri-apps/api/dpi.js b/node_modules/@tauri-apps/api/dpi.js new file mode 100644 index 0000000..f7fb485 --- /dev/null +++ b/node_modules/@tauri-apps/api/dpi.js @@ -0,0 +1,340 @@ +import { SERIALIZE_TO_IPC_FN } from './core.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * A size represented in logical pixels. + * Logical pixels are scaled according to the window's DPI scale. + * Most browser APIs (i.e. `MouseEvent`'s `clientX`) will return logical pixels. + * + * For logical-pixel-based position, see {@linkcode LogicalPosition}. + * + * @since 2.0.0 + */ +class LogicalSize { + constructor(...args) { + this.type = 'Logical'; + if (args.length === 1) { + if ('Logical' in args[0]) { + this.width = args[0].Logical.width; + this.height = args[0].Logical.height; + } + else { + this.width = args[0].width; + this.height = args[0].height; + } + } + else { + this.width = args[0]; + this.height = args[1]; + } + } + /** + * Converts the logical size to a physical one. + * @example + * ```typescript + * import { LogicalSize } from '@tauri-apps/api/dpi'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const size = new LogicalSize(400, 500); + * const physical = size.toPhysical(factor); + * ``` + * + * @since 2.0.0 + */ + toPhysical(scaleFactor) { + return new PhysicalSize(this.width * scaleFactor, this.height * scaleFactor); + } + [SERIALIZE_TO_IPC_FN]() { + return { + width: this.width, + height: this.height + }; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN](); + } +} +/** + * A size represented in physical pixels. + * + * Physical pixels represent actual screen pixels, and are DPI-independent. + * For high-DPI windows, this means that any point in the window on the screen + * will have a different position in logical pixels {@linkcode LogicalSize}. + * + * For physical-pixel-based position, see {@linkcode PhysicalPosition}. + * + * @since 2.0.0 + */ +class PhysicalSize { + constructor(...args) { + this.type = 'Physical'; + if (args.length === 1) { + if ('Physical' in args[0]) { + this.width = args[0].Physical.width; + this.height = args[0].Physical.height; + } + else { + this.width = args[0].width; + this.height = args[0].height; + } + } + else { + this.width = args[0]; + this.height = args[1]; + } + } + /** + * Converts the physical size to a logical one. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const size = await appWindow.innerSize(); // PhysicalSize + * const logical = size.toLogical(factor); + * ``` + */ + toLogical(scaleFactor) { + return new LogicalSize(this.width / scaleFactor, this.height / scaleFactor); + } + [SERIALIZE_TO_IPC_FN]() { + return { + width: this.width, + height: this.height + }; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN](); + } +} +/** + * A size represented either in physical or in logical pixels. + * + * This type is basically a union type of {@linkcode LogicalSize} and {@linkcode PhysicalSize} + * but comes in handy when using `tauri::Size` in Rust as an argument to a command, as this class + * automatically serializes into a valid format so it can be deserialized correctly into `tauri::Size` + * + * So instead of + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalSize, PhysicalSize } from '@tauri-apps/api/dpi'; + * + * const size: LogicalSize | PhysicalSize = someFunction(); // where someFunction returns either LogicalSize or PhysicalSize + * const validSize = size instanceof LogicalSize + * ? { Logical: { width: size.width, height: size.height } } + * : { Physical: { width: size.width, height: size.height } } + * await invoke("do_something_with_size", { size: validSize }); + * ``` + * + * You can just use {@linkcode Size} + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalSize, PhysicalSize, Size } from '@tauri-apps/api/dpi'; + * + * const size: LogicalSize | PhysicalSize = someFunction(); // where someFunction returns either LogicalSize or PhysicalSize + * const validSize = new Size(size); + * await invoke("do_something_with_size", { size: validSize }); + * ``` + * + * @since 2.1.0 + */ +class Size { + constructor(size) { + this.size = size; + } + toLogical(scaleFactor) { + return this.size instanceof LogicalSize + ? this.size + : this.size.toLogical(scaleFactor); + } + toPhysical(scaleFactor) { + return this.size instanceof PhysicalSize + ? this.size + : this.size.toPhysical(scaleFactor); + } + [SERIALIZE_TO_IPC_FN]() { + return { + [`${this.size.type}`]: { + width: this.size.width, + height: this.size.height + } + }; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN](); + } +} +/** + * A position represented in logical pixels. + * For an explanation of what logical pixels are, see description of {@linkcode LogicalSize}. + * + * @since 2.0.0 + */ +class LogicalPosition { + constructor(...args) { + this.type = 'Logical'; + if (args.length === 1) { + if ('Logical' in args[0]) { + this.x = args[0].Logical.x; + this.y = args[0].Logical.y; + } + else { + this.x = args[0].x; + this.y = args[0].y; + } + } + else { + this.x = args[0]; + this.y = args[1]; + } + } + /** + * Converts the logical position to a physical one. + * @example + * ```typescript + * import { LogicalPosition } from '@tauri-apps/api/dpi'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const position = new LogicalPosition(400, 500); + * const physical = position.toPhysical(factor); + * ``` + * + * @since 2.0.0 + */ + toPhysical(scaleFactor) { + return new PhysicalPosition(this.x * scaleFactor, this.y * scaleFactor); + } + [SERIALIZE_TO_IPC_FN]() { + return { + x: this.x, + y: this.y + }; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN](); + } +} +/** + * A position represented in physical pixels. + * + * For an explanation of what physical pixels are, see description of {@linkcode PhysicalSize}. + * + * @since 2.0.0 + */ +class PhysicalPosition { + constructor(...args) { + this.type = 'Physical'; + if (args.length === 1) { + if ('Physical' in args[0]) { + this.x = args[0].Physical.x; + this.y = args[0].Physical.y; + } + else { + this.x = args[0].x; + this.y = args[0].y; + } + } + else { + this.x = args[0]; + this.y = args[1]; + } + } + /** + * Converts the physical position to a logical one. + * @example + * ```typescript + * import { PhysicalPosition } from '@tauri-apps/api/dpi'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const position = new PhysicalPosition(400, 500); + * const physical = position.toLogical(factor); + * ``` + * + * @since 2.0.0 + */ + toLogical(scaleFactor) { + return new LogicalPosition(this.x / scaleFactor, this.y / scaleFactor); + } + [SERIALIZE_TO_IPC_FN]() { + return { + x: this.x, + y: this.y + }; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN](); + } +} +/** + * A position represented either in physical or in logical pixels. + * + * This type is basically a union type of {@linkcode LogicalSize} and {@linkcode PhysicalSize} + * but comes in handy when using `tauri::Position` in Rust as an argument to a command, as this class + * automatically serializes into a valid format so it can be deserialized correctly into `tauri::Position` + * + * So instead of + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalPosition, PhysicalPosition } from '@tauri-apps/api/dpi'; + * + * const position: LogicalPosition | PhysicalPosition = someFunction(); // where someFunction returns either LogicalPosition or PhysicalPosition + * const validPosition = position instanceof LogicalPosition + * ? { Logical: { x: position.x, y: position.y } } + * : { Physical: { x: position.x, y: position.y } } + * await invoke("do_something_with_position", { position: validPosition }); + * ``` + * + * You can just use {@linkcode Position} + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalPosition, PhysicalPosition, Position } from '@tauri-apps/api/dpi'; + * + * const position: LogicalPosition | PhysicalPosition = someFunction(); // where someFunction returns either LogicalPosition or PhysicalPosition + * const validPosition = new Position(position); + * await invoke("do_something_with_position", { position: validPosition }); + * ``` + * + * @since 2.1.0 + */ +class Position { + constructor(position) { + this.position = position; + } + toLogical(scaleFactor) { + return this.position instanceof LogicalPosition + ? this.position + : this.position.toLogical(scaleFactor); + } + toPhysical(scaleFactor) { + return this.position instanceof PhysicalPosition + ? this.position + : this.position.toPhysical(scaleFactor); + } + [SERIALIZE_TO_IPC_FN]() { + return { + [`${this.position.type}`]: { + x: this.position.x, + y: this.position.y + } + }; + } + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN](); + } +} + +export { LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size }; diff --git a/node_modules/@tauri-apps/api/event.cjs b/node_modules/@tauri-apps/api/event.cjs new file mode 100644 index 0000000..33ee012 --- /dev/null +++ b/node_modules/@tauri-apps/api/event.cjs @@ -0,0 +1,163 @@ +'use strict'; + +var core = require('./core.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * The event system allows you to emit events to the backend and listen to events from it. + * + * This package is also accessible with `window.__TAURI__.event` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * @module + */ +/** + * @since 1.1.0 + */ +exports.TauriEvent = void 0; +(function (TauriEvent) { + TauriEvent["WINDOW_RESIZED"] = "tauri://resize"; + TauriEvent["WINDOW_MOVED"] = "tauri://move"; + TauriEvent["WINDOW_CLOSE_REQUESTED"] = "tauri://close-requested"; + TauriEvent["WINDOW_DESTROYED"] = "tauri://destroyed"; + TauriEvent["WINDOW_FOCUS"] = "tauri://focus"; + TauriEvent["WINDOW_BLUR"] = "tauri://blur"; + TauriEvent["WINDOW_SCALE_FACTOR_CHANGED"] = "tauri://scale-change"; + TauriEvent["WINDOW_THEME_CHANGED"] = "tauri://theme-changed"; + TauriEvent["WINDOW_CREATED"] = "tauri://window-created"; + TauriEvent["WEBVIEW_CREATED"] = "tauri://webview-created"; + TauriEvent["DRAG_ENTER"] = "tauri://drag-enter"; + TauriEvent["DRAG_OVER"] = "tauri://drag-over"; + TauriEvent["DRAG_DROP"] = "tauri://drag-drop"; + TauriEvent["DRAG_LEAVE"] = "tauri://drag-leave"; +})(exports.TauriEvent || (exports.TauriEvent = {})); +/** + * Unregister the event listener associated with the given name and id. + * + * @ignore + * @param event The event name + * @param eventId Event identifier + * @returns + */ +async function _unlisten(event, eventId) { + window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(event, eventId); + await core.invoke('plugin:event|unlisten', { + event, + eventId + }); +} +/** + * Listen to an emitted event to any {@link EventTarget|target}. + * + * @example + * ```typescript + * import { listen } from '@tauri-apps/api/event'; + * const unlisten = await listen('error', (event) => { + * console.log(`Got error, payload: ${event.payload}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler callback. + * @param options Event listening options. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.0 + */ +async function listen(event, handler, options) { + var _a; + const target = typeof (options === null || options === void 0 ? void 0 : options.target) === 'string' + ? { kind: 'AnyLabel', label: options.target } + : ((_a = options === null || options === void 0 ? void 0 : options.target) !== null && _a !== void 0 ? _a : { kind: 'Any' }); + return core.invoke('plugin:event|listen', { + event, + target, + handler: core.transformCallback(handler) + }).then((eventId) => { + return async () => _unlisten(event, eventId); + }); +} +/** + * Listens once to an emitted event to any {@link EventTarget|target}. + * + * @example + * ```typescript + * import { once } from '@tauri-apps/api/event'; + * interface LoadedPayload { + * loggedIn: boolean, + * token: string + * } + * const unlisten = await once('loaded', (event) => { + * console.log(`App is loaded, loggedIn: ${event.payload.loggedIn}, token: ${event.payload.token}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler callback. + * @param options Event listening options. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.0 + */ +async function once(event, handler, options) { + return listen(event, (eventData) => { + void _unlisten(event, eventData.id); + handler(eventData); + }, options); +} +/** + * Emits an event to all {@link EventTarget|targets}. + * + * @example + * ```typescript + * import { emit } from '@tauri-apps/api/event'; + * await emit('frontend-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + * + * @since 1.0.0 + */ +async function emit(event, payload) { + await core.invoke('plugin:event|emit', { + event, + payload + }); +} +/** + * Emits an event to all {@link EventTarget|targets} matching the given target. + * + * @example + * ```typescript + * import { emitTo } from '@tauri-apps/api/event'; + * await emitTo('main', 'frontend-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object. + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + * + * @since 2.0.0 + */ +async function emitTo(target, event, payload) { + const eventTarget = typeof target === 'string' ? { kind: 'AnyLabel', label: target } : target; + await core.invoke('plugin:event|emit_to', { + target: eventTarget, + event, + payload + }); +} + +exports.emit = emit; +exports.emitTo = emitTo; +exports.listen = listen; +exports.once = once; diff --git a/node_modules/@tauri-apps/api/event.d.ts b/node_modules/@tauri-apps/api/event.d.ts new file mode 100644 index 0000000..c110f7b --- /dev/null +++ b/node_modules/@tauri-apps/api/event.d.ts @@ -0,0 +1,145 @@ +declare global { + interface Window { + __TAURI_EVENT_PLUGIN_INTERNALS__: { + unregisterListener: (event: string, eventId: number) => void; + }; + } +} +type EventTarget = { + kind: 'Any'; +} | { + kind: 'AnyLabel'; + label: string; +} | { + kind: 'App'; +} | { + kind: 'Window'; + label: string; +} | { + kind: 'Webview'; + label: string; +} | { + kind: 'WebviewWindow'; + label: string; +}; +interface Event { + /** Event name */ + event: EventName; + /** Event identifier used to unlisten */ + id: number; + /** Event payload */ + payload: T; +} +type EventCallback = (event: Event) => void; +type UnlistenFn = () => void; +type EventName = `${TauriEvent}` | (string & Record); +interface Options { + /** + * The event target to listen to, defaults to `{ kind: 'Any' }`, see {@link EventTarget}. + * + * If a string is provided, {@link EventTarget.AnyLabel} is used. + */ + target?: string | EventTarget; +} +/** + * @since 1.1.0 + */ +declare enum TauriEvent { + WINDOW_RESIZED = "tauri://resize", + WINDOW_MOVED = "tauri://move", + WINDOW_CLOSE_REQUESTED = "tauri://close-requested", + WINDOW_DESTROYED = "tauri://destroyed", + WINDOW_FOCUS = "tauri://focus", + WINDOW_BLUR = "tauri://blur", + WINDOW_SCALE_FACTOR_CHANGED = "tauri://scale-change", + WINDOW_THEME_CHANGED = "tauri://theme-changed", + WINDOW_CREATED = "tauri://window-created", + WEBVIEW_CREATED = "tauri://webview-created", + DRAG_ENTER = "tauri://drag-enter", + DRAG_OVER = "tauri://drag-over", + DRAG_DROP = "tauri://drag-drop", + DRAG_LEAVE = "tauri://drag-leave" +} +/** + * Listen to an emitted event to any {@link EventTarget|target}. + * + * @example + * ```typescript + * import { listen } from '@tauri-apps/api/event'; + * const unlisten = await listen('error', (event) => { + * console.log(`Got error, payload: ${event.payload}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler callback. + * @param options Event listening options. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.0 + */ +declare function listen(event: EventName, handler: EventCallback, options?: Options): Promise; +/** + * Listens once to an emitted event to any {@link EventTarget|target}. + * + * @example + * ```typescript + * import { once } from '@tauri-apps/api/event'; + * interface LoadedPayload { + * loggedIn: boolean, + * token: string + * } + * const unlisten = await once('loaded', (event) => { + * console.log(`App is loaded, loggedIn: ${event.payload.loggedIn}, token: ${event.payload.token}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler callback. + * @param options Event listening options. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.0 + */ +declare function once(event: EventName, handler: EventCallback, options?: Options): Promise; +/** + * Emits an event to all {@link EventTarget|targets}. + * + * @example + * ```typescript + * import { emit } from '@tauri-apps/api/event'; + * await emit('frontend-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + * + * @since 1.0.0 + */ +declare function emit(event: string, payload?: T): Promise; +/** + * Emits an event to all {@link EventTarget|targets} matching the given target. + * + * @example + * ```typescript + * import { emitTo } from '@tauri-apps/api/event'; + * await emitTo('main', 'frontend-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object. + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + * + * @since 2.0.0 + */ +declare function emitTo(target: EventTarget | string, event: string, payload?: T): Promise; +export type { Event, EventTarget, EventCallback, UnlistenFn, EventName, Options }; +export { listen, once, emit, emitTo, TauriEvent }; diff --git a/node_modules/@tauri-apps/api/event.js b/node_modules/@tauri-apps/api/event.js new file mode 100644 index 0000000..5ef61bc --- /dev/null +++ b/node_modules/@tauri-apps/api/event.js @@ -0,0 +1,158 @@ +import { invoke, transformCallback } from './core.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * The event system allows you to emit events to the backend and listen to events from it. + * + * This package is also accessible with `window.__TAURI__.event` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * @module + */ +/** + * @since 1.1.0 + */ +var TauriEvent; +(function (TauriEvent) { + TauriEvent["WINDOW_RESIZED"] = "tauri://resize"; + TauriEvent["WINDOW_MOVED"] = "tauri://move"; + TauriEvent["WINDOW_CLOSE_REQUESTED"] = "tauri://close-requested"; + TauriEvent["WINDOW_DESTROYED"] = "tauri://destroyed"; + TauriEvent["WINDOW_FOCUS"] = "tauri://focus"; + TauriEvent["WINDOW_BLUR"] = "tauri://blur"; + TauriEvent["WINDOW_SCALE_FACTOR_CHANGED"] = "tauri://scale-change"; + TauriEvent["WINDOW_THEME_CHANGED"] = "tauri://theme-changed"; + TauriEvent["WINDOW_CREATED"] = "tauri://window-created"; + TauriEvent["WEBVIEW_CREATED"] = "tauri://webview-created"; + TauriEvent["DRAG_ENTER"] = "tauri://drag-enter"; + TauriEvent["DRAG_OVER"] = "tauri://drag-over"; + TauriEvent["DRAG_DROP"] = "tauri://drag-drop"; + TauriEvent["DRAG_LEAVE"] = "tauri://drag-leave"; +})(TauriEvent || (TauriEvent = {})); +/** + * Unregister the event listener associated with the given name and id. + * + * @ignore + * @param event The event name + * @param eventId Event identifier + * @returns + */ +async function _unlisten(event, eventId) { + window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(event, eventId); + await invoke('plugin:event|unlisten', { + event, + eventId + }); +} +/** + * Listen to an emitted event to any {@link EventTarget|target}. + * + * @example + * ```typescript + * import { listen } from '@tauri-apps/api/event'; + * const unlisten = await listen('error', (event) => { + * console.log(`Got error, payload: ${event.payload}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler callback. + * @param options Event listening options. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.0 + */ +async function listen(event, handler, options) { + var _a; + const target = typeof (options === null || options === void 0 ? void 0 : options.target) === 'string' + ? { kind: 'AnyLabel', label: options.target } + : ((_a = options === null || options === void 0 ? void 0 : options.target) !== null && _a !== void 0 ? _a : { kind: 'Any' }); + return invoke('plugin:event|listen', { + event, + target, + handler: transformCallback(handler) + }).then((eventId) => { + return async () => _unlisten(event, eventId); + }); +} +/** + * Listens once to an emitted event to any {@link EventTarget|target}. + * + * @example + * ```typescript + * import { once } from '@tauri-apps/api/event'; + * interface LoadedPayload { + * loggedIn: boolean, + * token: string + * } + * const unlisten = await once('loaded', (event) => { + * console.log(`App is loaded, loggedIn: ${event.payload.loggedIn}, token: ${event.payload.token}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler callback. + * @param options Event listening options. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.0 + */ +async function once(event, handler, options) { + return listen(event, (eventData) => { + void _unlisten(event, eventData.id); + handler(eventData); + }, options); +} +/** + * Emits an event to all {@link EventTarget|targets}. + * + * @example + * ```typescript + * import { emit } from '@tauri-apps/api/event'; + * await emit('frontend-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + * + * @since 1.0.0 + */ +async function emit(event, payload) { + await invoke('plugin:event|emit', { + event, + payload + }); +} +/** + * Emits an event to all {@link EventTarget|targets} matching the given target. + * + * @example + * ```typescript + * import { emitTo } from '@tauri-apps/api/event'; + * await emitTo('main', 'frontend-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object. + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + * + * @since 2.0.0 + */ +async function emitTo(target, event, payload) { + const eventTarget = typeof target === 'string' ? { kind: 'AnyLabel', label: target } : target; + await invoke('plugin:event|emit_to', { + target: eventTarget, + event, + payload + }); +} + +export { TauriEvent, emit, emitTo, listen, once }; diff --git a/node_modules/@tauri-apps/api/external/tslib/tslib.es6.cjs b/node_modules/@tauri-apps/api/external/tslib/tslib.es6.cjs new file mode 100644 index 0000000..a15899d --- /dev/null +++ b/node_modules/@tauri-apps/api/external/tslib/tslib.es6.cjs @@ -0,0 +1,39 @@ +'use strict'; + +/****************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ +/* global Reflect, Promise, SuppressedError, Symbol, Iterator */ + + +function __classPrivateFieldGet(receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +} + +function __classPrivateFieldSet(receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; +} + +typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { + var e = new Error(message); + return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; +}; + +exports.__classPrivateFieldGet = __classPrivateFieldGet; +exports.__classPrivateFieldSet = __classPrivateFieldSet; diff --git a/node_modules/@tauri-apps/api/external/tslib/tslib.es6.js b/node_modules/@tauri-apps/api/external/tslib/tslib.es6.js new file mode 100644 index 0000000..e42559f --- /dev/null +++ b/node_modules/@tauri-apps/api/external/tslib/tslib.es6.js @@ -0,0 +1,36 @@ +/****************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ +/* global Reflect, Promise, SuppressedError, Symbol, Iterator */ + + +function __classPrivateFieldGet(receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +} + +function __classPrivateFieldSet(receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; +} + +typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { + var e = new Error(message); + return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; +}; + +export { __classPrivateFieldGet, __classPrivateFieldSet }; diff --git a/node_modules/@tauri-apps/api/image.cjs b/node_modules/@tauri-apps/api/image.cjs new file mode 100644 index 0000000..160bfc1 --- /dev/null +++ b/node_modules/@tauri-apps/api/image.cjs @@ -0,0 +1,88 @@ +'use strict'; + +var core = require('./core.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** An RGBA Image in row-major order from top to bottom. */ +class Image extends core.Resource { + /** + * Creates an Image from a resource ID. For internal use only. + * + * @ignore + */ + constructor(rid) { + super(rid); + } + /** Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height. */ + static async new(rgba, width, height) { + return core.invoke('plugin:image|new', { + rgba: transformImage(rgba), + width, + height + }).then((rid) => new Image(rid)); + } + /** + * Creates a new image using the provided bytes by inferring the file format. + * If the format is known, prefer [@link Image.fromPngBytes] or [@link Image.fromIcoBytes]. + * + * Only `ico` and `png` are supported (based on activated feature flag). + * + * Note that you need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + */ + static async fromBytes(bytes) { + return core.invoke('plugin:image|from_bytes', { + bytes: transformImage(bytes) + }).then((rid) => new Image(rid)); + } + /** + * Creates a new image using the provided path. + * + * Only `ico` and `png` are supported (based on activated feature flag). + * + * Note that you need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + */ + static async fromPath(path) { + return core.invoke('plugin:image|from_path', { path }).then((rid) => new Image(rid)); + } + /** Returns the RGBA data for this image, in row-major order from top to bottom. */ + async rgba() { + return core.invoke('plugin:image|rgba', { + rid: this.rid + }).then((buffer) => new Uint8Array(buffer)); + } + /** Returns the size of this image. */ + async size() { + return core.invoke('plugin:image|size', { rid: this.rid }); + } +} +/** + * Transforms image from various types into a type acceptable by Rust. + * + * See [tauri::image::JsImage](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html) for more information. + * Note the API signature is not stable and might change. + */ +function transformImage(image) { + const ret = image == null + ? null + : typeof image === 'string' + ? image + : image instanceof Image + ? image.rid + : image; + return ret; +} + +exports.Image = Image; +exports.transformImage = transformImage; diff --git a/node_modules/@tauri-apps/api/image.d.ts b/node_modules/@tauri-apps/api/image.d.ts new file mode 100644 index 0000000..82f31c4 --- /dev/null +++ b/node_modules/@tauri-apps/api/image.d.ts @@ -0,0 +1,57 @@ +import { Resource } from './core'; +import { NativeIcon } from './menu/iconMenuItem'; +export interface ImageSize { + width: number; + height: number; +} +/** A type that represents an icon that can be used in menu items. */ +export type MenuIcon = NativeIcon | string | Image | Uint8Array | ArrayBuffer | number[]; +/** An RGBA Image in row-major order from top to bottom. */ +export declare class Image extends Resource { + /** + * Creates an Image from a resource ID. For internal use only. + * + * @ignore + */ + constructor(rid: number); + /** Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height. */ + static new(rgba: number[] | Uint8Array | ArrayBuffer, width: number, height: number): Promise; + /** + * Creates a new image using the provided bytes by inferring the file format. + * If the format is known, prefer [@link Image.fromPngBytes] or [@link Image.fromIcoBytes]. + * + * Only `ico` and `png` are supported (based on activated feature flag). + * + * Note that you need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + */ + static fromBytes(bytes: number[] | Uint8Array | ArrayBuffer): Promise; + /** + * Creates a new image using the provided path. + * + * Only `ico` and `png` are supported (based on activated feature flag). + * + * Note that you need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + */ + static fromPath(path: string): Promise; + /** Returns the RGBA data for this image, in row-major order from top to bottom. */ + rgba(): Promise; + /** Returns the size of this image. */ + size(): Promise; +} +/** + * Transforms image from various types into a type acceptable by Rust. + * + * See [tauri::image::JsImage](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html) for more information. + * Note the API signature is not stable and might change. + */ +export declare function transformImage(image: string | Image | Uint8Array | ArrayBuffer | number[] | null): T; diff --git a/node_modules/@tauri-apps/api/image.js b/node_modules/@tauri-apps/api/image.js new file mode 100644 index 0000000..66e7e84 --- /dev/null +++ b/node_modules/@tauri-apps/api/image.js @@ -0,0 +1,85 @@ +import { Resource, invoke } from './core.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** An RGBA Image in row-major order from top to bottom. */ +class Image extends Resource { + /** + * Creates an Image from a resource ID. For internal use only. + * + * @ignore + */ + constructor(rid) { + super(rid); + } + /** Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height. */ + static async new(rgba, width, height) { + return invoke('plugin:image|new', { + rgba: transformImage(rgba), + width, + height + }).then((rid) => new Image(rid)); + } + /** + * Creates a new image using the provided bytes by inferring the file format. + * If the format is known, prefer [@link Image.fromPngBytes] or [@link Image.fromIcoBytes]. + * + * Only `ico` and `png` are supported (based on activated feature flag). + * + * Note that you need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + */ + static async fromBytes(bytes) { + return invoke('plugin:image|from_bytes', { + bytes: transformImage(bytes) + }).then((rid) => new Image(rid)); + } + /** + * Creates a new image using the provided path. + * + * Only `ico` and `png` are supported (based on activated feature flag). + * + * Note that you need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + */ + static async fromPath(path) { + return invoke('plugin:image|from_path', { path }).then((rid) => new Image(rid)); + } + /** Returns the RGBA data for this image, in row-major order from top to bottom. */ + async rgba() { + return invoke('plugin:image|rgba', { + rid: this.rid + }).then((buffer) => new Uint8Array(buffer)); + } + /** Returns the size of this image. */ + async size() { + return invoke('plugin:image|size', { rid: this.rid }); + } +} +/** + * Transforms image from various types into a type acceptable by Rust. + * + * See [tauri::image::JsImage](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html) for more information. + * Note the API signature is not stable and might change. + */ +function transformImage(image) { + const ret = image == null + ? null + : typeof image === 'string' + ? image + : image instanceof Image + ? image.rid + : image; + return ret; +} + +export { Image, transformImage }; diff --git a/node_modules/@tauri-apps/api/index.cjs b/node_modules/@tauri-apps/api/index.cjs new file mode 100644 index 0000000..05c2266 --- /dev/null +++ b/node_modules/@tauri-apps/api/index.cjs @@ -0,0 +1,29 @@ +'use strict'; + +var app = require('./app.cjs'); +var core = require('./core.cjs'); +var dpi = require('./dpi.cjs'); +var event = require('./event.cjs'); +var image = require('./image.cjs'); +var menu = require('./menu.cjs'); +var mocks = require('./mocks.cjs'); +var path = require('./path.cjs'); +var tray = require('./tray.cjs'); +var webview = require('./webview.cjs'); +var webviewWindow = require('./webviewWindow.cjs'); +var window = require('./window.cjs'); + + + +exports.app = app; +exports.core = core; +exports.dpi = dpi; +exports.event = event; +exports.image = image; +exports.menu = menu; +exports.mocks = mocks; +exports.path = path; +exports.tray = tray; +exports.webview = webview; +exports.webviewWindow = webviewWindow; +exports.window = window; diff --git a/node_modules/@tauri-apps/api/index.d.ts b/node_modules/@tauri-apps/api/index.d.ts new file mode 100644 index 0000000..6111f0e --- /dev/null +++ b/node_modules/@tauri-apps/api/index.d.ts @@ -0,0 +1,33 @@ +/** + * The Tauri API allows you to interface with the backend layer. + * + * This module exposes all other modules as an object where the key is the module name, and the value is the module exports. + * @example + * ```typescript + * import { event, window, path } from '@tauri-apps/api' + * ``` + * + * ### Vanilla JS API + * + * The above import syntax is for JavaScript/TypeScript with a bundler. If you're using vanilla JavaScript, you can use the global `window.__TAURI__` object instead. It requires `app.withGlobalTauri` configuration option enabled. + * + * @example + * ```js + * const { event, window: tauriWindow, path } = window.__TAURI__; + * ``` + * + * @module + */ +import * as app from './app'; +import * as core from './core'; +import * as dpi from './dpi'; +import * as event from './event'; +import * as image from './image'; +import * as menu from './menu'; +import * as mocks from './mocks'; +import * as path from './path'; +import * as tray from './tray'; +import * as webview from './webview'; +import * as webviewWindow from './webviewWindow'; +import * as window from './window'; +export { app, core, dpi, event, image, menu, mocks, path, tray, webview, webviewWindow, window }; diff --git a/node_modules/@tauri-apps/api/index.js b/node_modules/@tauri-apps/api/index.js new file mode 100644 index 0000000..8518a55 --- /dev/null +++ b/node_modules/@tauri-apps/api/index.js @@ -0,0 +1,24 @@ +import * as app from './app.js'; +export { app }; +import * as core from './core.js'; +export { core }; +import * as dpi from './dpi.js'; +export { dpi }; +import * as event from './event.js'; +export { event }; +import * as image from './image.js'; +export { image }; +import * as menu from './menu.js'; +export { menu }; +import * as mocks from './mocks.js'; +export { mocks }; +import * as path from './path.js'; +export { path }; +import * as tray from './tray.js'; +export { tray }; +import * as webview from './webview.js'; +export { webview }; +import * as webviewWindow from './webviewWindow.js'; +export { webviewWindow }; +import * as window from './window.js'; +export { window }; diff --git a/node_modules/@tauri-apps/api/menu.cjs b/node_modules/@tauri-apps/api/menu.cjs new file mode 100644 index 0000000..13ae499 --- /dev/null +++ b/node_modules/@tauri-apps/api/menu.cjs @@ -0,0 +1,30 @@ +'use strict'; + +var submenu = require('./menu/submenu.cjs'); +var menuItem = require('./menu/menuItem.cjs'); +var menu = require('./menu/menu.cjs'); +var checkMenuItem = require('./menu/checkMenuItem.cjs'); +var iconMenuItem = require('./menu/iconMenuItem.cjs'); +var predefinedMenuItem = require('./menu/predefinedMenuItem.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * Menu types and utilities. + * + * This package is also accessible with `window.__TAURI__.menu` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * @module + */ + +exports.Submenu = submenu.Submenu; +exports.itemFromKind = submenu.itemFromKind; +exports.MenuItem = menuItem.MenuItem; +exports.Menu = menu.Menu; +exports.CheckMenuItem = checkMenuItem.CheckMenuItem; +exports.IconMenuItem = iconMenuItem.IconMenuItem; +Object.defineProperty(exports, "NativeIcon", { + enumerable: true, + get: function () { return iconMenuItem.NativeIcon; } +}); +exports.PredefinedMenuItem = predefinedMenuItem.PredefinedMenuItem; diff --git a/node_modules/@tauri-apps/api/menu.d.ts b/node_modules/@tauri-apps/api/menu.d.ts new file mode 100644 index 0000000..0aacba4 --- /dev/null +++ b/node_modules/@tauri-apps/api/menu.d.ts @@ -0,0 +1,12 @@ +export * from './menu/submenu'; +export * from './menu/menuItem'; +export * from './menu/menu'; +export * from './menu/checkMenuItem'; +export * from './menu/iconMenuItem'; +export * from './menu/predefinedMenuItem'; +/** + * Menu types and utilities. + * + * This package is also accessible with `window.__TAURI__.menu` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * @module + */ diff --git a/node_modules/@tauri-apps/api/menu.js b/node_modules/@tauri-apps/api/menu.js new file mode 100644 index 0000000..8d1248c --- /dev/null +++ b/node_modules/@tauri-apps/api/menu.js @@ -0,0 +1,16 @@ +export { Submenu, itemFromKind } from './menu/submenu.js'; +export { MenuItem } from './menu/menuItem.js'; +export { Menu } from './menu/menu.js'; +export { CheckMenuItem } from './menu/checkMenuItem.js'; +export { IconMenuItem, NativeIcon } from './menu/iconMenuItem.js'; +export { PredefinedMenuItem } from './menu/predefinedMenuItem.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * Menu types and utilities. + * + * This package is also accessible with `window.__TAURI__.menu` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * @module + */ diff --git a/node_modules/@tauri-apps/api/menu/base.cjs b/node_modules/@tauri-apps/api/menu/base.cjs new file mode 100644 index 0000000..17608e3 --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/base.cjs @@ -0,0 +1,100 @@ +'use strict'; + +var tslib_es6 = require('../external/tslib/tslib.es6.cjs'); +var core = require('../core.cjs'); +var image = require('../image.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +var _MenuItemBase_id, _MenuItemBase_kind; +function injectChannel(i) { + var _a; + if ('items' in i) { + i.items = (_a = i.items) === null || _a === void 0 ? void 0 : _a.map((item) => 'rid' in item ? item : injectChannel(item)); + } + else if ('action' in i && i.action) { + const handler = new core.Channel(); + handler.onmessage = i.action; + delete i.action; + return { ...i, handler }; + } + return i; +} +async function newMenu(kind, opts) { + const handler = new core.Channel(); + if (opts && typeof opts === 'object') { + if ('action' in opts && opts.action) { + handler.onmessage = opts.action; + delete opts.action; + } + // about predefined menu item icon + if ('item' in opts + && opts.item + && typeof opts.item === 'object' + && 'About' in opts.item + && opts.item.About + && typeof opts.item.About === 'object' + && 'icon' in opts.item.About + && opts.item.About.icon) { + opts.item.About.icon = image.transformImage(opts.item.About.icon); + } + // icon menu item icon + if ('icon' in opts && opts.icon) { + opts.icon = image.transformImage(opts.icon); + } + // submenu items + if ('items' in opts && opts.items) { + function prepareItem(i) { + var _a; + if ('rid' in i) { + return [i.rid, i.kind]; + } + if ('item' in i && typeof i.item === 'object' && ((_a = i.item.About) === null || _a === void 0 ? void 0 : _a.icon)) { + i.item.About.icon = image.transformImage(i.item.About.icon); + } + if ('icon' in i && i.icon) { + i.icon = image.transformImage(i.icon); + } + if ('items' in i && i.items) { + // @ts-expect-error the `prepareItem` return doesn't exactly match + // this is fine, because the difference is in `[number, string]` variant + i.items = i.items.map(prepareItem); + } + return injectChannel(i); + } + // @ts-expect-error the `prepareItem` return doesn't exactly match + // this is fine, because the difference is in `[number, string]` variant + opts.items = opts.items.map(prepareItem); + } + } + return core.invoke('plugin:menu|new', { + kind, + options: opts, + handler + }); +} +class MenuItemBase extends core.Resource { + /** The id of this item. */ + get id() { + return tslib_es6.__classPrivateFieldGet(this, _MenuItemBase_id, "f"); + } + /** @ignore */ + get kind() { + return tslib_es6.__classPrivateFieldGet(this, _MenuItemBase_kind, "f"); + } + /** @ignore */ + constructor(rid, id, kind) { + super(rid); + /** @ignore */ + _MenuItemBase_id.set(this, void 0); + /** @ignore */ + _MenuItemBase_kind.set(this, void 0); + tslib_es6.__classPrivateFieldSet(this, _MenuItemBase_id, id, "f"); + tslib_es6.__classPrivateFieldSet(this, _MenuItemBase_kind, kind, "f"); + } +} +_MenuItemBase_id = new WeakMap(), _MenuItemBase_kind = new WeakMap(); + +exports.MenuItemBase = MenuItemBase; +exports.newMenu = newMenu; diff --git a/node_modules/@tauri-apps/api/menu/base.d.ts b/node_modules/@tauri-apps/api/menu/base.d.ts new file mode 100644 index 0000000..ce3a985 --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/base.d.ts @@ -0,0 +1,18 @@ +import { Resource } from '../core'; +import { CheckMenuItemOptions } from './checkMenuItem'; +import { IconMenuItemOptions } from './iconMenuItem'; +import { MenuOptions } from './menu'; +import { MenuItemOptions } from './menuItem'; +import { PredefinedMenuItemOptions } from './predefinedMenuItem'; +import { SubmenuOptions } from './submenu'; +export type ItemKind = 'MenuItem' | 'Predefined' | 'Check' | 'Icon' | 'Submenu' | 'Menu'; +export declare function newMenu(kind: ItemKind, opts?: MenuOptions | MenuItemOptions | SubmenuOptions | PredefinedMenuItemOptions | CheckMenuItemOptions | IconMenuItemOptions): Promise<[number, string]>; +export declare class MenuItemBase extends Resource { + #private; + /** The id of this item. */ + get id(): string; + /** @ignore */ + get kind(): string; + /** @ignore */ + protected constructor(rid: number, id: string, kind: ItemKind); +} diff --git a/node_modules/@tauri-apps/api/menu/base.js b/node_modules/@tauri-apps/api/menu/base.js new file mode 100644 index 0000000..d59637a --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/base.js @@ -0,0 +1,97 @@ +import { __classPrivateFieldGet, __classPrivateFieldSet } from '../external/tslib/tslib.es6.js'; +import { Resource, Channel, invoke } from '../core.js'; +import { transformImage } from '../image.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +var _MenuItemBase_id, _MenuItemBase_kind; +function injectChannel(i) { + var _a; + if ('items' in i) { + i.items = (_a = i.items) === null || _a === void 0 ? void 0 : _a.map((item) => 'rid' in item ? item : injectChannel(item)); + } + else if ('action' in i && i.action) { + const handler = new Channel(); + handler.onmessage = i.action; + delete i.action; + return { ...i, handler }; + } + return i; +} +async function newMenu(kind, opts) { + const handler = new Channel(); + if (opts && typeof opts === 'object') { + if ('action' in opts && opts.action) { + handler.onmessage = opts.action; + delete opts.action; + } + // about predefined menu item icon + if ('item' in opts + && opts.item + && typeof opts.item === 'object' + && 'About' in opts.item + && opts.item.About + && typeof opts.item.About === 'object' + && 'icon' in opts.item.About + && opts.item.About.icon) { + opts.item.About.icon = transformImage(opts.item.About.icon); + } + // icon menu item icon + if ('icon' in opts && opts.icon) { + opts.icon = transformImage(opts.icon); + } + // submenu items + if ('items' in opts && opts.items) { + function prepareItem(i) { + var _a; + if ('rid' in i) { + return [i.rid, i.kind]; + } + if ('item' in i && typeof i.item === 'object' && ((_a = i.item.About) === null || _a === void 0 ? void 0 : _a.icon)) { + i.item.About.icon = transformImage(i.item.About.icon); + } + if ('icon' in i && i.icon) { + i.icon = transformImage(i.icon); + } + if ('items' in i && i.items) { + // @ts-expect-error the `prepareItem` return doesn't exactly match + // this is fine, because the difference is in `[number, string]` variant + i.items = i.items.map(prepareItem); + } + return injectChannel(i); + } + // @ts-expect-error the `prepareItem` return doesn't exactly match + // this is fine, because the difference is in `[number, string]` variant + opts.items = opts.items.map(prepareItem); + } + } + return invoke('plugin:menu|new', { + kind, + options: opts, + handler + }); +} +class MenuItemBase extends Resource { + /** The id of this item. */ + get id() { + return __classPrivateFieldGet(this, _MenuItemBase_id, "f"); + } + /** @ignore */ + get kind() { + return __classPrivateFieldGet(this, _MenuItemBase_kind, "f"); + } + /** @ignore */ + constructor(rid, id, kind) { + super(rid); + /** @ignore */ + _MenuItemBase_id.set(this, void 0); + /** @ignore */ + _MenuItemBase_kind.set(this, void 0); + __classPrivateFieldSet(this, _MenuItemBase_id, id, "f"); + __classPrivateFieldSet(this, _MenuItemBase_kind, kind, "f"); + } +} +_MenuItemBase_id = new WeakMap(), _MenuItemBase_kind = new WeakMap(); + +export { MenuItemBase, newMenu }; diff --git a/node_modules/@tauri-apps/api/menu/checkMenuItem.cjs b/node_modules/@tauri-apps/api/menu/checkMenuItem.cjs new file mode 100644 index 0000000..469b53f --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/checkMenuItem.cjs @@ -0,0 +1,68 @@ +'use strict'; + +var base = require('./base.cjs'); +var core = require('../core.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * A check menu item inside a {@linkcode Menu} or {@linkcode Submenu} + * and usually contains a text and a check mark or a similar toggle + * that corresponds to a checked and unchecked states. + */ +class CheckMenuItem extends base.MenuItemBase { + /** @ignore */ + constructor(rid, id) { + super(rid, id, 'Check'); + } + /** Create a new check menu item. */ + static async new(opts) { + return base.newMenu('Check', opts).then(([rid, id]) => new CheckMenuItem(rid, id)); + } + /** Returns the text of this check menu item. */ + async text() { + return core.invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }); + } + /** Sets the text for this check menu item. */ + async setText(text) { + return core.invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }); + } + /** Returns whether this check menu item is enabled or not. */ + async isEnabled() { + return core.invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind }); + } + /** Sets whether this check menu item is enabled or not. */ + async setEnabled(enabled) { + return core.invoke('plugin:menu|set_enabled', { + rid: this.rid, + kind: this.kind, + enabled + }); + } + /** Sets the accelerator for this check menu item. */ + async setAccelerator(accelerator) { + return core.invoke('plugin:menu|set_accelerator', { + rid: this.rid, + kind: this.kind, + accelerator + }); + } + /** Returns whether this check menu item is checked or not. */ + async isChecked() { + return core.invoke('plugin:menu|is_checked', { rid: this.rid }); + } + /** Sets whether this check menu item is checked or not. */ + async setChecked(checked) { + return core.invoke('plugin:menu|set_checked', { + rid: this.rid, + checked + }); + } +} + +exports.CheckMenuItem = CheckMenuItem; diff --git a/node_modules/@tauri-apps/api/menu/checkMenuItem.d.ts b/node_modules/@tauri-apps/api/menu/checkMenuItem.d.ts new file mode 100644 index 0000000..ec4aa4b --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/checkMenuItem.d.ts @@ -0,0 +1,32 @@ +import { MenuItemBase } from './base'; +import { type MenuItemOptions } from '../menu'; +/** Options for creating a new check menu item. */ +export interface CheckMenuItemOptions extends MenuItemOptions { + /** Whether the new check menu item is enabled or not. */ + checked?: boolean; +} +/** + * A check menu item inside a {@linkcode Menu} or {@linkcode Submenu} + * and usually contains a text and a check mark or a similar toggle + * that corresponds to a checked and unchecked states. + */ +export declare class CheckMenuItem extends MenuItemBase { + /** @ignore */ + protected constructor(rid: number, id: string); + /** Create a new check menu item. */ + static new(opts: CheckMenuItemOptions): Promise; + /** Returns the text of this check menu item. */ + text(): Promise; + /** Sets the text for this check menu item. */ + setText(text: string): Promise; + /** Returns whether this check menu item is enabled or not. */ + isEnabled(): Promise; + /** Sets whether this check menu item is enabled or not. */ + setEnabled(enabled: boolean): Promise; + /** Sets the accelerator for this check menu item. */ + setAccelerator(accelerator: string | null): Promise; + /** Returns whether this check menu item is checked or not. */ + isChecked(): Promise; + /** Sets whether this check menu item is checked or not. */ + setChecked(checked: boolean): Promise; +} diff --git a/node_modules/@tauri-apps/api/menu/checkMenuItem.js b/node_modules/@tauri-apps/api/menu/checkMenuItem.js new file mode 100644 index 0000000..84c4774 --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/checkMenuItem.js @@ -0,0 +1,66 @@ +import { MenuItemBase, newMenu } from './base.js'; +import { invoke } from '../core.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * A check menu item inside a {@linkcode Menu} or {@linkcode Submenu} + * and usually contains a text and a check mark or a similar toggle + * that corresponds to a checked and unchecked states. + */ +class CheckMenuItem extends MenuItemBase { + /** @ignore */ + constructor(rid, id) { + super(rid, id, 'Check'); + } + /** Create a new check menu item. */ + static async new(opts) { + return newMenu('Check', opts).then(([rid, id]) => new CheckMenuItem(rid, id)); + } + /** Returns the text of this check menu item. */ + async text() { + return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }); + } + /** Sets the text for this check menu item. */ + async setText(text) { + return invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }); + } + /** Returns whether this check menu item is enabled or not. */ + async isEnabled() { + return invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind }); + } + /** Sets whether this check menu item is enabled or not. */ + async setEnabled(enabled) { + return invoke('plugin:menu|set_enabled', { + rid: this.rid, + kind: this.kind, + enabled + }); + } + /** Sets the accelerator for this check menu item. */ + async setAccelerator(accelerator) { + return invoke('plugin:menu|set_accelerator', { + rid: this.rid, + kind: this.kind, + accelerator + }); + } + /** Returns whether this check menu item is checked or not. */ + async isChecked() { + return invoke('plugin:menu|is_checked', { rid: this.rid }); + } + /** Sets whether this check menu item is checked or not. */ + async setChecked(checked) { + return invoke('plugin:menu|set_checked', { + rid: this.rid, + checked + }); + } +} + +export { CheckMenuItem }; diff --git a/node_modules/@tauri-apps/api/menu/iconMenuItem.cjs b/node_modules/@tauri-apps/api/menu/iconMenuItem.cjs new file mode 100644 index 0000000..5a6a34b --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/iconMenuItem.cjs @@ -0,0 +1,187 @@ +'use strict'; + +var base = require('./base.cjs'); +var core = require('../core.cjs'); +var image = require('../image.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * A native Icon to be used for the menu item + * + * #### Platform-specific: + * + * - **Windows / Linux**: Unsupported. + */ +exports.NativeIcon = void 0; +(function (NativeIcon) { + /** An add item template image. */ + NativeIcon["Add"] = "Add"; + /** Advanced preferences toolbar icon for the preferences window. */ + NativeIcon["Advanced"] = "Advanced"; + /** A Bluetooth template image. */ + NativeIcon["Bluetooth"] = "Bluetooth"; + /** Bookmarks image suitable for a template. */ + NativeIcon["Bookmarks"] = "Bookmarks"; + /** A caution image. */ + NativeIcon["Caution"] = "Caution"; + /** A color panel toolbar icon. */ + NativeIcon["ColorPanel"] = "ColorPanel"; + /** A column view mode template image. */ + NativeIcon["ColumnView"] = "ColumnView"; + /** A computer icon. */ + NativeIcon["Computer"] = "Computer"; + /** An enter full-screen mode template image. */ + NativeIcon["EnterFullScreen"] = "EnterFullScreen"; + /** Permissions for all users. */ + NativeIcon["Everyone"] = "Everyone"; + /** An exit full-screen mode template image. */ + NativeIcon["ExitFullScreen"] = "ExitFullScreen"; + /** A cover flow view mode template image. */ + NativeIcon["FlowView"] = "FlowView"; + /** A folder image. */ + NativeIcon["Folder"] = "Folder"; + /** A burnable folder icon. */ + NativeIcon["FolderBurnable"] = "FolderBurnable"; + /** A smart folder icon. */ + NativeIcon["FolderSmart"] = "FolderSmart"; + /** A link template image. */ + NativeIcon["FollowLinkFreestanding"] = "FollowLinkFreestanding"; + /** A font panel toolbar icon. */ + NativeIcon["FontPanel"] = "FontPanel"; + /** A `go back` template image. */ + NativeIcon["GoLeft"] = "GoLeft"; + /** A `go forward` template image. */ + NativeIcon["GoRight"] = "GoRight"; + /** Home image suitable for a template. */ + NativeIcon["Home"] = "Home"; + /** An iChat Theater template image. */ + NativeIcon["IChatTheater"] = "IChatTheater"; + /** An icon view mode template image. */ + NativeIcon["IconView"] = "IconView"; + /** An information toolbar icon. */ + NativeIcon["Info"] = "Info"; + /** A template image used to denote invalid data. */ + NativeIcon["InvalidDataFreestanding"] = "InvalidDataFreestanding"; + /** A generic left-facing triangle template image. */ + NativeIcon["LeftFacingTriangle"] = "LeftFacingTriangle"; + /** A list view mode template image. */ + NativeIcon["ListView"] = "ListView"; + /** A locked padlock template image. */ + NativeIcon["LockLocked"] = "LockLocked"; + /** An unlocked padlock template image. */ + NativeIcon["LockUnlocked"] = "LockUnlocked"; + /** A horizontal dash, for use in menus. */ + NativeIcon["MenuMixedState"] = "MenuMixedState"; + /** A check mark template image, for use in menus. */ + NativeIcon["MenuOnState"] = "MenuOnState"; + /** A MobileMe icon. */ + NativeIcon["MobileMe"] = "MobileMe"; + /** A drag image for multiple items. */ + NativeIcon["MultipleDocuments"] = "MultipleDocuments"; + /** A network icon. */ + NativeIcon["Network"] = "Network"; + /** A path button template image. */ + NativeIcon["Path"] = "Path"; + /** General preferences toolbar icon for the preferences window. */ + NativeIcon["PreferencesGeneral"] = "PreferencesGeneral"; + /** A Quick Look template image. */ + NativeIcon["QuickLook"] = "QuickLook"; + /** A refresh template image. */ + NativeIcon["RefreshFreestanding"] = "RefreshFreestanding"; + /** A refresh template image. */ + NativeIcon["Refresh"] = "Refresh"; + /** A remove item template image. */ + NativeIcon["Remove"] = "Remove"; + /** A reveal contents template image. */ + NativeIcon["RevealFreestanding"] = "RevealFreestanding"; + /** A generic right-facing triangle template image. */ + NativeIcon["RightFacingTriangle"] = "RightFacingTriangle"; + /** A share view template image. */ + NativeIcon["Share"] = "Share"; + /** A slideshow template image. */ + NativeIcon["Slideshow"] = "Slideshow"; + /** A badge for a `smart` item. */ + NativeIcon["SmartBadge"] = "SmartBadge"; + /** Small green indicator, similar to iChat's available image. */ + NativeIcon["StatusAvailable"] = "StatusAvailable"; + /** Small clear indicator. */ + NativeIcon["StatusNone"] = "StatusNone"; + /** Small yellow indicator, similar to iChat's idle image. */ + NativeIcon["StatusPartiallyAvailable"] = "StatusPartiallyAvailable"; + /** Small red indicator, similar to iChat's unavailable image. */ + NativeIcon["StatusUnavailable"] = "StatusUnavailable"; + /** A stop progress template image. */ + NativeIcon["StopProgressFreestanding"] = "StopProgressFreestanding"; + /** A stop progress button template image. */ + NativeIcon["StopProgress"] = "StopProgress"; + /** An image of the empty trash can. */ + NativeIcon["TrashEmpty"] = "TrashEmpty"; + /** An image of the full trash can. */ + NativeIcon["TrashFull"] = "TrashFull"; + /** Permissions for a single user. */ + NativeIcon["User"] = "User"; + /** User account toolbar icon for the preferences window. */ + NativeIcon["UserAccounts"] = "UserAccounts"; + /** Permissions for a group of users. */ + NativeIcon["UserGroup"] = "UserGroup"; + /** Permissions for guests. */ + NativeIcon["UserGuest"] = "UserGuest"; +})(exports.NativeIcon || (exports.NativeIcon = {})); +/** + * An icon menu item inside a {@linkcode Menu} or {@linkcode Submenu} + * and usually contains an icon and a text. + */ +class IconMenuItem extends base.MenuItemBase { + /** @ignore */ + constructor(rid, id) { + super(rid, id, 'Icon'); + } + /** Create a new icon menu item. */ + static async new(opts) { + return base.newMenu('Icon', opts).then(([rid, id]) => new IconMenuItem(rid, id)); + } + /** Returns the text of this icon menu item. */ + async text() { + return core.invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }); + } + /** Sets the text for this icon menu item. */ + async setText(text) { + return core.invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }); + } + /** Returns whether this icon menu item is enabled or not. */ + async isEnabled() { + return core.invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind }); + } + /** Sets whether this icon menu item is enabled or not. */ + async setEnabled(enabled) { + return core.invoke('plugin:menu|set_enabled', { + rid: this.rid, + kind: this.kind, + enabled + }); + } + /** Sets the accelerator for this icon menu item. */ + async setAccelerator(accelerator) { + return core.invoke('plugin:menu|set_accelerator', { + rid: this.rid, + kind: this.kind, + accelerator + }); + } + /** Sets an icon for this icon menu item */ + async setIcon(icon) { + return core.invoke('plugin:menu|set_icon', { + rid: this.rid, + kind: this.kind, + icon: image.transformImage(icon) + }); + } +} + +exports.IconMenuItem = IconMenuItem; diff --git a/node_modules/@tauri-apps/api/menu/iconMenuItem.d.ts b/node_modules/@tauri-apps/api/menu/iconMenuItem.d.ts new file mode 100644 index 0000000..10ae6b9 --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/iconMenuItem.d.ts @@ -0,0 +1,160 @@ +import { MenuItemBase } from './base'; +import { type MenuItemOptions } from '../menu'; +import { MenuIcon } from '../image'; +/** + * A native Icon to be used for the menu item + * + * #### Platform-specific: + * + * - **Windows / Linux**: Unsupported. + */ +export declare enum NativeIcon { + /** An add item template image. */ + Add = "Add", + /** Advanced preferences toolbar icon for the preferences window. */ + Advanced = "Advanced", + /** A Bluetooth template image. */ + Bluetooth = "Bluetooth", + /** Bookmarks image suitable for a template. */ + Bookmarks = "Bookmarks", + /** A caution image. */ + Caution = "Caution", + /** A color panel toolbar icon. */ + ColorPanel = "ColorPanel", + /** A column view mode template image. */ + ColumnView = "ColumnView", + /** A computer icon. */ + Computer = "Computer", + /** An enter full-screen mode template image. */ + EnterFullScreen = "EnterFullScreen", + /** Permissions for all users. */ + Everyone = "Everyone", + /** An exit full-screen mode template image. */ + ExitFullScreen = "ExitFullScreen", + /** A cover flow view mode template image. */ + FlowView = "FlowView", + /** A folder image. */ + Folder = "Folder", + /** A burnable folder icon. */ + FolderBurnable = "FolderBurnable", + /** A smart folder icon. */ + FolderSmart = "FolderSmart", + /** A link template image. */ + FollowLinkFreestanding = "FollowLinkFreestanding", + /** A font panel toolbar icon. */ + FontPanel = "FontPanel", + /** A `go back` template image. */ + GoLeft = "GoLeft", + /** A `go forward` template image. */ + GoRight = "GoRight", + /** Home image suitable for a template. */ + Home = "Home", + /** An iChat Theater template image. */ + IChatTheater = "IChatTheater", + /** An icon view mode template image. */ + IconView = "IconView", + /** An information toolbar icon. */ + Info = "Info", + /** A template image used to denote invalid data. */ + InvalidDataFreestanding = "InvalidDataFreestanding", + /** A generic left-facing triangle template image. */ + LeftFacingTriangle = "LeftFacingTriangle", + /** A list view mode template image. */ + ListView = "ListView", + /** A locked padlock template image. */ + LockLocked = "LockLocked", + /** An unlocked padlock template image. */ + LockUnlocked = "LockUnlocked", + /** A horizontal dash, for use in menus. */ + MenuMixedState = "MenuMixedState", + /** A check mark template image, for use in menus. */ + MenuOnState = "MenuOnState", + /** A MobileMe icon. */ + MobileMe = "MobileMe", + /** A drag image for multiple items. */ + MultipleDocuments = "MultipleDocuments", + /** A network icon. */ + Network = "Network", + /** A path button template image. */ + Path = "Path", + /** General preferences toolbar icon for the preferences window. */ + PreferencesGeneral = "PreferencesGeneral", + /** A Quick Look template image. */ + QuickLook = "QuickLook", + /** A refresh template image. */ + RefreshFreestanding = "RefreshFreestanding", + /** A refresh template image. */ + Refresh = "Refresh", + /** A remove item template image. */ + Remove = "Remove", + /** A reveal contents template image. */ + RevealFreestanding = "RevealFreestanding", + /** A generic right-facing triangle template image. */ + RightFacingTriangle = "RightFacingTriangle", + /** A share view template image. */ + Share = "Share", + /** A slideshow template image. */ + Slideshow = "Slideshow", + /** A badge for a `smart` item. */ + SmartBadge = "SmartBadge", + /** Small green indicator, similar to iChat's available image. */ + StatusAvailable = "StatusAvailable", + /** Small clear indicator. */ + StatusNone = "StatusNone", + /** Small yellow indicator, similar to iChat's idle image. */ + StatusPartiallyAvailable = "StatusPartiallyAvailable", + /** Small red indicator, similar to iChat's unavailable image. */ + StatusUnavailable = "StatusUnavailable", + /** A stop progress template image. */ + StopProgressFreestanding = "StopProgressFreestanding", + /** A stop progress button template image. */ + StopProgress = "StopProgress", + /** An image of the empty trash can. */ + TrashEmpty = "TrashEmpty", + /** An image of the full trash can. */ + TrashFull = "TrashFull", + /** Permissions for a single user. */ + User = "User", + /** User account toolbar icon for the preferences window. */ + UserAccounts = "UserAccounts", + /** Permissions for a group of users. */ + UserGroup = "UserGroup", + /** Permissions for guests. */ + UserGuest = "UserGuest" +} +/** Options for creating a new icon menu item. */ +export interface IconMenuItemOptions extends MenuItemOptions { + /** + * Icon to be used for the new icon menu item. + * + * Note that you may need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + */ + icon?: MenuIcon; +} +/** + * An icon menu item inside a {@linkcode Menu} or {@linkcode Submenu} + * and usually contains an icon and a text. + */ +export declare class IconMenuItem extends MenuItemBase { + /** @ignore */ + protected constructor(rid: number, id: string); + /** Create a new icon menu item. */ + static new(opts: IconMenuItemOptions): Promise; + /** Returns the text of this icon menu item. */ + text(): Promise; + /** Sets the text for this icon menu item. */ + setText(text: string): Promise; + /** Returns whether this icon menu item is enabled or not. */ + isEnabled(): Promise; + /** Sets whether this icon menu item is enabled or not. */ + setEnabled(enabled: boolean): Promise; + /** Sets the accelerator for this icon menu item. */ + setAccelerator(accelerator: string | null): Promise; + /** Sets an icon for this icon menu item */ + setIcon(icon: MenuIcon | null): Promise; +} diff --git a/node_modules/@tauri-apps/api/menu/iconMenuItem.js b/node_modules/@tauri-apps/api/menu/iconMenuItem.js new file mode 100644 index 0000000..6719bb0 --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/iconMenuItem.js @@ -0,0 +1,185 @@ +import { MenuItemBase, newMenu } from './base.js'; +import { invoke } from '../core.js'; +import { transformImage } from '../image.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * A native Icon to be used for the menu item + * + * #### Platform-specific: + * + * - **Windows / Linux**: Unsupported. + */ +var NativeIcon; +(function (NativeIcon) { + /** An add item template image. */ + NativeIcon["Add"] = "Add"; + /** Advanced preferences toolbar icon for the preferences window. */ + NativeIcon["Advanced"] = "Advanced"; + /** A Bluetooth template image. */ + NativeIcon["Bluetooth"] = "Bluetooth"; + /** Bookmarks image suitable for a template. */ + NativeIcon["Bookmarks"] = "Bookmarks"; + /** A caution image. */ + NativeIcon["Caution"] = "Caution"; + /** A color panel toolbar icon. */ + NativeIcon["ColorPanel"] = "ColorPanel"; + /** A column view mode template image. */ + NativeIcon["ColumnView"] = "ColumnView"; + /** A computer icon. */ + NativeIcon["Computer"] = "Computer"; + /** An enter full-screen mode template image. */ + NativeIcon["EnterFullScreen"] = "EnterFullScreen"; + /** Permissions for all users. */ + NativeIcon["Everyone"] = "Everyone"; + /** An exit full-screen mode template image. */ + NativeIcon["ExitFullScreen"] = "ExitFullScreen"; + /** A cover flow view mode template image. */ + NativeIcon["FlowView"] = "FlowView"; + /** A folder image. */ + NativeIcon["Folder"] = "Folder"; + /** A burnable folder icon. */ + NativeIcon["FolderBurnable"] = "FolderBurnable"; + /** A smart folder icon. */ + NativeIcon["FolderSmart"] = "FolderSmart"; + /** A link template image. */ + NativeIcon["FollowLinkFreestanding"] = "FollowLinkFreestanding"; + /** A font panel toolbar icon. */ + NativeIcon["FontPanel"] = "FontPanel"; + /** A `go back` template image. */ + NativeIcon["GoLeft"] = "GoLeft"; + /** A `go forward` template image. */ + NativeIcon["GoRight"] = "GoRight"; + /** Home image suitable for a template. */ + NativeIcon["Home"] = "Home"; + /** An iChat Theater template image. */ + NativeIcon["IChatTheater"] = "IChatTheater"; + /** An icon view mode template image. */ + NativeIcon["IconView"] = "IconView"; + /** An information toolbar icon. */ + NativeIcon["Info"] = "Info"; + /** A template image used to denote invalid data. */ + NativeIcon["InvalidDataFreestanding"] = "InvalidDataFreestanding"; + /** A generic left-facing triangle template image. */ + NativeIcon["LeftFacingTriangle"] = "LeftFacingTriangle"; + /** A list view mode template image. */ + NativeIcon["ListView"] = "ListView"; + /** A locked padlock template image. */ + NativeIcon["LockLocked"] = "LockLocked"; + /** An unlocked padlock template image. */ + NativeIcon["LockUnlocked"] = "LockUnlocked"; + /** A horizontal dash, for use in menus. */ + NativeIcon["MenuMixedState"] = "MenuMixedState"; + /** A check mark template image, for use in menus. */ + NativeIcon["MenuOnState"] = "MenuOnState"; + /** A MobileMe icon. */ + NativeIcon["MobileMe"] = "MobileMe"; + /** A drag image for multiple items. */ + NativeIcon["MultipleDocuments"] = "MultipleDocuments"; + /** A network icon. */ + NativeIcon["Network"] = "Network"; + /** A path button template image. */ + NativeIcon["Path"] = "Path"; + /** General preferences toolbar icon for the preferences window. */ + NativeIcon["PreferencesGeneral"] = "PreferencesGeneral"; + /** A Quick Look template image. */ + NativeIcon["QuickLook"] = "QuickLook"; + /** A refresh template image. */ + NativeIcon["RefreshFreestanding"] = "RefreshFreestanding"; + /** A refresh template image. */ + NativeIcon["Refresh"] = "Refresh"; + /** A remove item template image. */ + NativeIcon["Remove"] = "Remove"; + /** A reveal contents template image. */ + NativeIcon["RevealFreestanding"] = "RevealFreestanding"; + /** A generic right-facing triangle template image. */ + NativeIcon["RightFacingTriangle"] = "RightFacingTriangle"; + /** A share view template image. */ + NativeIcon["Share"] = "Share"; + /** A slideshow template image. */ + NativeIcon["Slideshow"] = "Slideshow"; + /** A badge for a `smart` item. */ + NativeIcon["SmartBadge"] = "SmartBadge"; + /** Small green indicator, similar to iChat's available image. */ + NativeIcon["StatusAvailable"] = "StatusAvailable"; + /** Small clear indicator. */ + NativeIcon["StatusNone"] = "StatusNone"; + /** Small yellow indicator, similar to iChat's idle image. */ + NativeIcon["StatusPartiallyAvailable"] = "StatusPartiallyAvailable"; + /** Small red indicator, similar to iChat's unavailable image. */ + NativeIcon["StatusUnavailable"] = "StatusUnavailable"; + /** A stop progress template image. */ + NativeIcon["StopProgressFreestanding"] = "StopProgressFreestanding"; + /** A stop progress button template image. */ + NativeIcon["StopProgress"] = "StopProgress"; + /** An image of the empty trash can. */ + NativeIcon["TrashEmpty"] = "TrashEmpty"; + /** An image of the full trash can. */ + NativeIcon["TrashFull"] = "TrashFull"; + /** Permissions for a single user. */ + NativeIcon["User"] = "User"; + /** User account toolbar icon for the preferences window. */ + NativeIcon["UserAccounts"] = "UserAccounts"; + /** Permissions for a group of users. */ + NativeIcon["UserGroup"] = "UserGroup"; + /** Permissions for guests. */ + NativeIcon["UserGuest"] = "UserGuest"; +})(NativeIcon || (NativeIcon = {})); +/** + * An icon menu item inside a {@linkcode Menu} or {@linkcode Submenu} + * and usually contains an icon and a text. + */ +class IconMenuItem extends MenuItemBase { + /** @ignore */ + constructor(rid, id) { + super(rid, id, 'Icon'); + } + /** Create a new icon menu item. */ + static async new(opts) { + return newMenu('Icon', opts).then(([rid, id]) => new IconMenuItem(rid, id)); + } + /** Returns the text of this icon menu item. */ + async text() { + return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }); + } + /** Sets the text for this icon menu item. */ + async setText(text) { + return invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }); + } + /** Returns whether this icon menu item is enabled or not. */ + async isEnabled() { + return invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind }); + } + /** Sets whether this icon menu item is enabled or not. */ + async setEnabled(enabled) { + return invoke('plugin:menu|set_enabled', { + rid: this.rid, + kind: this.kind, + enabled + }); + } + /** Sets the accelerator for this icon menu item. */ + async setAccelerator(accelerator) { + return invoke('plugin:menu|set_accelerator', { + rid: this.rid, + kind: this.kind, + accelerator + }); + } + /** Sets an icon for this icon menu item */ + async setIcon(icon) { + return invoke('plugin:menu|set_icon', { + rid: this.rid, + kind: this.kind, + icon: transformImage(icon) + }); + } +} + +export { IconMenuItem, NativeIcon }; diff --git a/node_modules/@tauri-apps/api/menu/menu.cjs b/node_modules/@tauri-apps/api/menu/menu.cjs new file mode 100644 index 0000000..f7eb3b6 --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/menu.cjs @@ -0,0 +1,148 @@ +'use strict'; + +var submenu = require('./submenu.cjs'); +var dpi = require('../dpi.cjs'); +var core = require('../core.cjs'); +var base = require('./base.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** A type that is either a menu bar on the window + * on Windows and Linux or as a global menu in the menubar on macOS. + * + * #### Platform-specific: + * + * - **macOS**: if using {@linkcode Menu} for the global menubar, it can only contain {@linkcode Submenu}s. + */ +class Menu extends base.MenuItemBase { + /** @ignore */ + constructor(rid, id) { + super(rid, id, 'Menu'); + } + /** Create a new menu. */ + static async new(opts) { + return base.newMenu('Menu', opts).then(([rid, id]) => new Menu(rid, id)); + } + /** Create a default menu. */ + static async default() { + return core.invoke('plugin:menu|create_default').then(([rid, id]) => new Menu(rid, id)); + } + /** + * Add a menu item to the end of this menu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async append(items) { + return core.invoke('plugin:menu|append', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => 'rid' in i ? [i.rid, i.kind] : i) + }); + } + /** + * Add a menu item to the beginning of this menu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async prepend(items) { + return core.invoke('plugin:menu|prepend', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => 'rid' in i ? [i.rid, i.kind] : i) + }); + } + /** + * Add a menu item to the specified position in this menu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async insert(items, position) { + return core.invoke('plugin:menu|insert', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => 'rid' in i ? [i.rid, i.kind] : i), + position + }); + } + /** Remove a menu item from this menu. */ + async remove(item) { + return core.invoke('plugin:menu|remove', { + rid: this.rid, + kind: this.kind, + item: [item.rid, item.kind] + }); + } + /** Remove a menu item from this menu at the specified position. */ + async removeAt(position) { + return core.invoke('plugin:menu|remove_at', { + rid: this.rid, + kind: this.kind, + position + }).then(submenu.itemFromKind); + } + /** Returns a list of menu items that has been added to this menu. */ + async items() { + return core.invoke('plugin:menu|items', { + rid: this.rid, + kind: this.kind + }).then((i) => i.map(submenu.itemFromKind)); + } + /** Retrieves the menu item matching the given identifier. */ + async get(id) { + return core.invoke('plugin:menu|get', { + rid: this.rid, + kind: this.kind, + id + }).then((r) => (r ? submenu.itemFromKind(r) : null)); + } + /** + * Popup this menu as a context menu on the specified window. + * + * @param at If a position is provided, it is relative to the window's top-left corner. + * If there isn't one provided, the menu will pop up at the current location of the mouse. + */ + async popup(at, window) { + var _a; + return core.invoke('plugin:menu|popup', { + rid: this.rid, + kind: this.kind, + window: (_a = window === null || window === void 0 ? void 0 : window.label) !== null && _a !== void 0 ? _a : null, + at: at instanceof dpi.Position ? at : at ? new dpi.Position(at) : null + }); + } + /** + * Sets the app-wide menu and returns the previous one. + * + * If a window was not created with an explicit menu or had one set explicitly, + * this menu will be assigned to it. + */ + async setAsAppMenu() { + return core.invoke('plugin:menu|set_as_app_menu', { + rid: this.rid + }).then((r) => (r ? new Menu(r[0], r[1]) : null)); + } + /** + * Sets the window menu and returns the previous one. + * + * #### Platform-specific: + * + * - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + * window, if you need to set it, use {@linkcode Menu.setAsAppMenu} instead. + */ + async setAsWindowMenu(window) { + var _a; + return core.invoke('plugin:menu|set_as_window_menu', { + rid: this.rid, + window: (_a = window === null || window === void 0 ? void 0 : window.label) !== null && _a !== void 0 ? _a : null + }).then((r) => (r ? new Menu(r[0], r[1]) : null)); + } +} + +exports.Menu = Menu; diff --git a/node_modules/@tauri-apps/api/menu/menu.d.ts b/node_modules/@tauri-apps/api/menu/menu.d.ts new file mode 100644 index 0000000..194673c --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/menu.d.ts @@ -0,0 +1,86 @@ +import { MenuItemOptions, SubmenuOptions, IconMenuItemOptions, PredefinedMenuItemOptions, CheckMenuItemOptions } from '../menu'; +import { MenuItem } from './menuItem'; +import { CheckMenuItem } from './checkMenuItem'; +import { IconMenuItem } from './iconMenuItem'; +import { PredefinedMenuItem } from './predefinedMenuItem'; +import { Submenu } from './submenu'; +import { type LogicalPosition, PhysicalPosition, Position } from '../dpi'; +import { type Window } from '../window'; +import { MenuItemBase } from './base'; +/** Options for creating a new menu. */ +export interface MenuOptions { + /** Specify an id to use for the new menu. */ + id?: string; + /** List of items to add to the new menu. */ + items?: Array; +} +/** A type that is either a menu bar on the window + * on Windows and Linux or as a global menu in the menubar on macOS. + * + * #### Platform-specific: + * + * - **macOS**: if using {@linkcode Menu} for the global menubar, it can only contain {@linkcode Submenu}s. + */ +export declare class Menu extends MenuItemBase { + /** @ignore */ + protected constructor(rid: number, id: string); + /** Create a new menu. */ + static new(opts?: MenuOptions): Promise; + /** Create a default menu. */ + static default(): Promise; + /** + * Add a menu item to the end of this menu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + append(items: T | T[]): Promise; + /** + * Add a menu item to the beginning of this menu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + prepend(items: T | T[]): Promise; + /** + * Add a menu item to the specified position in this menu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + insert(items: T | T[], position: number): Promise; + /** Remove a menu item from this menu. */ + remove(item: Submenu | MenuItem | PredefinedMenuItem | CheckMenuItem | IconMenuItem): Promise; + /** Remove a menu item from this menu at the specified position. */ + removeAt(position: number): Promise; + /** Returns a list of menu items that has been added to this menu. */ + items(): Promise>; + /** Retrieves the menu item matching the given identifier. */ + get(id: string): Promise; + /** + * Popup this menu as a context menu on the specified window. + * + * @param at If a position is provided, it is relative to the window's top-left corner. + * If there isn't one provided, the menu will pop up at the current location of the mouse. + */ + popup(at?: PhysicalPosition | LogicalPosition | Position, window?: Window): Promise; + /** + * Sets the app-wide menu and returns the previous one. + * + * If a window was not created with an explicit menu or had one set explicitly, + * this menu will be assigned to it. + */ + setAsAppMenu(): Promise; + /** + * Sets the window menu and returns the previous one. + * + * #### Platform-specific: + * + * - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + * window, if you need to set it, use {@linkcode Menu.setAsAppMenu} instead. + */ + setAsWindowMenu(window?: Window): Promise; +} diff --git a/node_modules/@tauri-apps/api/menu/menu.js b/node_modules/@tauri-apps/api/menu/menu.js new file mode 100644 index 0000000..9153283 --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/menu.js @@ -0,0 +1,146 @@ +import { itemFromKind } from './submenu.js'; +import { Position } from '../dpi.js'; +import { invoke } from '../core.js'; +import { MenuItemBase, newMenu } from './base.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** A type that is either a menu bar on the window + * on Windows and Linux or as a global menu in the menubar on macOS. + * + * #### Platform-specific: + * + * - **macOS**: if using {@linkcode Menu} for the global menubar, it can only contain {@linkcode Submenu}s. + */ +class Menu extends MenuItemBase { + /** @ignore */ + constructor(rid, id) { + super(rid, id, 'Menu'); + } + /** Create a new menu. */ + static async new(opts) { + return newMenu('Menu', opts).then(([rid, id]) => new Menu(rid, id)); + } + /** Create a default menu. */ + static async default() { + return invoke('plugin:menu|create_default').then(([rid, id]) => new Menu(rid, id)); + } + /** + * Add a menu item to the end of this menu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async append(items) { + return invoke('plugin:menu|append', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => 'rid' in i ? [i.rid, i.kind] : i) + }); + } + /** + * Add a menu item to the beginning of this menu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async prepend(items) { + return invoke('plugin:menu|prepend', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => 'rid' in i ? [i.rid, i.kind] : i) + }); + } + /** + * Add a menu item to the specified position in this menu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async insert(items, position) { + return invoke('plugin:menu|insert', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => 'rid' in i ? [i.rid, i.kind] : i), + position + }); + } + /** Remove a menu item from this menu. */ + async remove(item) { + return invoke('plugin:menu|remove', { + rid: this.rid, + kind: this.kind, + item: [item.rid, item.kind] + }); + } + /** Remove a menu item from this menu at the specified position. */ + async removeAt(position) { + return invoke('plugin:menu|remove_at', { + rid: this.rid, + kind: this.kind, + position + }).then(itemFromKind); + } + /** Returns a list of menu items that has been added to this menu. */ + async items() { + return invoke('plugin:menu|items', { + rid: this.rid, + kind: this.kind + }).then((i) => i.map(itemFromKind)); + } + /** Retrieves the menu item matching the given identifier. */ + async get(id) { + return invoke('plugin:menu|get', { + rid: this.rid, + kind: this.kind, + id + }).then((r) => (r ? itemFromKind(r) : null)); + } + /** + * Popup this menu as a context menu on the specified window. + * + * @param at If a position is provided, it is relative to the window's top-left corner. + * If there isn't one provided, the menu will pop up at the current location of the mouse. + */ + async popup(at, window) { + var _a; + return invoke('plugin:menu|popup', { + rid: this.rid, + kind: this.kind, + window: (_a = window === null || window === void 0 ? void 0 : window.label) !== null && _a !== void 0 ? _a : null, + at: at instanceof Position ? at : at ? new Position(at) : null + }); + } + /** + * Sets the app-wide menu and returns the previous one. + * + * If a window was not created with an explicit menu or had one set explicitly, + * this menu will be assigned to it. + */ + async setAsAppMenu() { + return invoke('plugin:menu|set_as_app_menu', { + rid: this.rid + }).then((r) => (r ? new Menu(r[0], r[1]) : null)); + } + /** + * Sets the window menu and returns the previous one. + * + * #### Platform-specific: + * + * - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + * window, if you need to set it, use {@linkcode Menu.setAsAppMenu} instead. + */ + async setAsWindowMenu(window) { + var _a; + return invoke('plugin:menu|set_as_window_menu', { + rid: this.rid, + window: (_a = window === null || window === void 0 ? void 0 : window.label) !== null && _a !== void 0 ? _a : null + }).then((r) => (r ? new Menu(r[0], r[1]) : null)); + } +} + +export { Menu }; diff --git a/node_modules/@tauri-apps/api/menu/menuItem.cjs b/node_modules/@tauri-apps/api/menu/menuItem.cjs new file mode 100644 index 0000000..c16526f --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/menuItem.cjs @@ -0,0 +1,53 @@ +'use strict'; + +var base = require('./base.cjs'); +var core = require('../core.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** A menu item inside a {@linkcode Menu} or {@linkcode Submenu} and contains only text. */ +class MenuItem extends base.MenuItemBase { + /** @ignore */ + constructor(rid, id) { + super(rid, id, 'MenuItem'); + } + /** Create a new menu item. */ + static async new(opts) { + return base.newMenu('MenuItem', opts).then(([rid, id]) => new MenuItem(rid, id)); + } + /** Returns the text of this menu item. */ + async text() { + return core.invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }); + } + /** Sets the text for this menu item. */ + async setText(text) { + return core.invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }); + } + /** Returns whether this menu item is enabled or not. */ + async isEnabled() { + return core.invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind }); + } + /** Sets whether this menu item is enabled or not. */ + async setEnabled(enabled) { + return core.invoke('plugin:menu|set_enabled', { + rid: this.rid, + kind: this.kind, + enabled + }); + } + /** Sets the accelerator for this menu item. */ + async setAccelerator(accelerator) { + return core.invoke('plugin:menu|set_accelerator', { + rid: this.rid, + kind: this.kind, + accelerator + }); + } +} + +exports.MenuItem = MenuItem; diff --git a/node_modules/@tauri-apps/api/menu/menuItem.d.ts b/node_modules/@tauri-apps/api/menu/menuItem.d.ts new file mode 100644 index 0000000..b57093a --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/menuItem.d.ts @@ -0,0 +1,31 @@ +import { MenuItemBase } from './base'; +/** Options for creating a new menu item. */ +export interface MenuItemOptions { + /** Specify an id to use for the new menu item. */ + id?: string; + /** The text of the new menu item. */ + text: string; + /** Whether the new menu item is enabled or not. */ + enabled?: boolean; + /** Specify an accelerator for the new menu item. */ + accelerator?: string; + /** Specify a handler to be called when this menu item is activated. */ + action?: (id: string) => void; +} +/** A menu item inside a {@linkcode Menu} or {@linkcode Submenu} and contains only text. */ +export declare class MenuItem extends MenuItemBase { + /** @ignore */ + protected constructor(rid: number, id: string); + /** Create a new menu item. */ + static new(opts: MenuItemOptions): Promise; + /** Returns the text of this menu item. */ + text(): Promise; + /** Sets the text for this menu item. */ + setText(text: string): Promise; + /** Returns whether this menu item is enabled or not. */ + isEnabled(): Promise; + /** Sets whether this menu item is enabled or not. */ + setEnabled(enabled: boolean): Promise; + /** Sets the accelerator for this menu item. */ + setAccelerator(accelerator: string | null): Promise; +} diff --git a/node_modules/@tauri-apps/api/menu/menuItem.js b/node_modules/@tauri-apps/api/menu/menuItem.js new file mode 100644 index 0000000..098101c --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/menuItem.js @@ -0,0 +1,51 @@ +import { MenuItemBase, newMenu } from './base.js'; +import { invoke } from '../core.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** A menu item inside a {@linkcode Menu} or {@linkcode Submenu} and contains only text. */ +class MenuItem extends MenuItemBase { + /** @ignore */ + constructor(rid, id) { + super(rid, id, 'MenuItem'); + } + /** Create a new menu item. */ + static async new(opts) { + return newMenu('MenuItem', opts).then(([rid, id]) => new MenuItem(rid, id)); + } + /** Returns the text of this menu item. */ + async text() { + return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }); + } + /** Sets the text for this menu item. */ + async setText(text) { + return invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }); + } + /** Returns whether this menu item is enabled or not. */ + async isEnabled() { + return invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind }); + } + /** Sets whether this menu item is enabled or not. */ + async setEnabled(enabled) { + return invoke('plugin:menu|set_enabled', { + rid: this.rid, + kind: this.kind, + enabled + }); + } + /** Sets the accelerator for this menu item. */ + async setAccelerator(accelerator) { + return invoke('plugin:menu|set_accelerator', { + rid: this.rid, + kind: this.kind, + accelerator + }); + } +} + +export { MenuItem }; diff --git a/node_modules/@tauri-apps/api/menu/predefinedMenuItem.cjs b/node_modules/@tauri-apps/api/menu/predefinedMenuItem.cjs new file mode 100644 index 0000000..a693073 --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/predefinedMenuItem.cjs @@ -0,0 +1,33 @@ +'use strict'; + +var base = require('./base.cjs'); +var core = require('../core.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** A predefined (native) menu item which has a predefined behavior by the OS or by tauri. */ +class PredefinedMenuItem extends base.MenuItemBase { + /** @ignore */ + constructor(rid, id) { + super(rid, id, 'Predefined'); + } + /** Create a new predefined menu item. */ + static async new(opts) { + return base.newMenu('Predefined', opts).then(([rid, id]) => new PredefinedMenuItem(rid, id)); + } + /** Returns the text of this predefined menu item. */ + async text() { + return core.invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }); + } + /** Sets the text for this predefined menu item. */ + async setText(text) { + return core.invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }); + } +} + +exports.PredefinedMenuItem = PredefinedMenuItem; diff --git a/node_modules/@tauri-apps/api/menu/predefinedMenuItem.d.ts b/node_modules/@tauri-apps/api/menu/predefinedMenuItem.d.ts new file mode 100644 index 0000000..b41a7cd --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/predefinedMenuItem.d.ts @@ -0,0 +1,95 @@ +import { MenuItemBase } from './base'; +import { Image } from '../image'; +/** A metadata for the about predefined menu item. */ +export interface AboutMetadata { + /** Sets the application name. */ + name?: string; + /** The application version. */ + version?: string; + /** + * The short version, e.g. "1.0". + * + * #### Platform-specific + * + * - **Windows / Linux:** Appended to the end of `version` in parentheses. + */ + shortVersion?: string; + /** + * The authors of the application. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + */ + authors?: string[]; + /** + * Application comments. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + */ + comments?: string; + /** The copyright of the application. */ + copyright?: string; + /** + * The license of the application. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + */ + license?: string; + /** + * The application website. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + */ + website?: string; + /** + * The website label. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + */ + websiteLabel?: string; + /** + * The credits. + * + * #### Platform-specific + * + * - **Windows / Linux:** Unsupported. + */ + credits?: string; + /** + * The application icon. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. + */ + icon?: string | Uint8Array | ArrayBuffer | number[] | Image; +} +/** Options for creating a new predefined menu item. */ +export interface PredefinedMenuItemOptions { + /** The text of the new predefined menu item. */ + text?: string; + /** The predefined item type */ + item: 'Separator' | 'Copy' | 'Cut' | 'Paste' | 'SelectAll' | 'Undo' | 'Redo' | 'Minimize' | 'Maximize' | 'Fullscreen' | 'Hide' | 'HideOthers' | 'ShowAll' | 'CloseWindow' | 'Quit' | 'Services' | { + About: AboutMetadata | null; + }; +} +/** A predefined (native) menu item which has a predefined behavior by the OS or by tauri. */ +export declare class PredefinedMenuItem extends MenuItemBase { + /** @ignore */ + protected constructor(rid: number, id: string); + /** Create a new predefined menu item. */ + static new(opts?: PredefinedMenuItemOptions): Promise; + /** Returns the text of this predefined menu item. */ + text(): Promise; + /** Sets the text for this predefined menu item. */ + setText(text: string): Promise; +} diff --git a/node_modules/@tauri-apps/api/menu/predefinedMenuItem.js b/node_modules/@tauri-apps/api/menu/predefinedMenuItem.js new file mode 100644 index 0000000..2e86254 --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/predefinedMenuItem.js @@ -0,0 +1,31 @@ +import { MenuItemBase, newMenu } from './base.js'; +import { invoke } from '../core.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** A predefined (native) menu item which has a predefined behavior by the OS or by tauri. */ +class PredefinedMenuItem extends MenuItemBase { + /** @ignore */ + constructor(rid, id) { + super(rid, id, 'Predefined'); + } + /** Create a new predefined menu item. */ + static async new(opts) { + return newMenu('Predefined', opts).then(([rid, id]) => new PredefinedMenuItem(rid, id)); + } + /** Returns the text of this predefined menu item. */ + async text() { + return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }); + } + /** Sets the text for this predefined menu item. */ + async setText(text) { + return invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }); + } +} + +export { PredefinedMenuItem }; diff --git a/node_modules/@tauri-apps/api/menu/submenu.cjs b/node_modules/@tauri-apps/api/menu/submenu.cjs new file mode 100644 index 0000000..aab25a2 --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/submenu.cjs @@ -0,0 +1,203 @@ +'use strict'; + +var menuItem = require('./menuItem.cjs'); +var checkMenuItem = require('./checkMenuItem.cjs'); +var iconMenuItem = require('./iconMenuItem.cjs'); +var predefinedMenuItem = require('./predefinedMenuItem.cjs'); +var core = require('../core.cjs'); +var base = require('./base.cjs'); +var dpi = require('../dpi.cjs'); +var image = require('../image.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** @ignore */ +function itemFromKind([rid, id, kind]) { + /* eslint-disable @typescript-eslint/no-unsafe-return */ + switch (kind) { + case 'Submenu': + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new Submenu(rid, id); + case 'Predefined': + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new predefinedMenuItem.PredefinedMenuItem(rid, id); + case 'Check': + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new checkMenuItem.CheckMenuItem(rid, id); + case 'Icon': + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new iconMenuItem.IconMenuItem(rid, id); + case 'MenuItem': + default: + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new menuItem.MenuItem(rid, id); + } + /* eslint-enable @typescript-eslint/no-unsafe-return */ +} +/** A type that is a submenu inside a {@linkcode Menu} or {@linkcode Submenu}. */ +class Submenu extends base.MenuItemBase { + /** @ignore */ + constructor(rid, id) { + super(rid, id, 'Submenu'); + } + /** Create a new submenu. */ + static async new(opts) { + return base.newMenu('Submenu', opts).then(([rid, id]) => new Submenu(rid, id)); + } + /** Returns the text of this submenu. */ + async text() { + return core.invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }); + } + /** Sets the text for this submenu. */ + async setText(text) { + return core.invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }); + } + /** Returns whether this submenu is enabled or not. */ + async isEnabled() { + return core.invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind }); + } + /** Sets whether this submenu is enabled or not. */ + async setEnabled(enabled) { + return core.invoke('plugin:menu|set_enabled', { + rid: this.rid, + kind: this.kind, + enabled + }); + } + /** + * Add a menu item to the end of this submenu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async append(items) { + return core.invoke('plugin:menu|append', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => 'rid' in i ? [i.rid, i.kind] : i) + }); + } + /** + * Add a menu item to the beginning of this submenu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async prepend(items) { + return core.invoke('plugin:menu|prepend', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => 'rid' in i ? [i.rid, i.kind] : i) + }); + } + /** + * Add a menu item to the specified position in this submenu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async insert(items, position) { + return core.invoke('plugin:menu|insert', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => 'rid' in i ? [i.rid, i.kind] : i), + position + }); + } + /** Remove a menu item from this submenu. */ + async remove(item) { + return core.invoke('plugin:menu|remove', { + rid: this.rid, + kind: this.kind, + item: [item.rid, item.kind] + }); + } + /** Remove a menu item from this submenu at the specified position. */ + async removeAt(position) { + return core.invoke('plugin:menu|remove_at', { + rid: this.rid, + kind: this.kind, + position + }).then(itemFromKind); + } + /** Returns a list of menu items that has been added to this submenu. */ + async items() { + return core.invoke('plugin:menu|items', { + rid: this.rid, + kind: this.kind + }).then((i) => i.map(itemFromKind)); + } + /** Retrieves the menu item matching the given identifier. */ + async get(id) { + return core.invoke('plugin:menu|get', { + rid: this.rid, + kind: this.kind, + id + }).then((r) => (r ? itemFromKind(r) : null)); + } + /** + * Popup this submenu as a context menu on the specified window. + * + * If the position, is provided, it is relative to the window's top-left corner. + */ + async popup(at, window) { + var _a; + return core.invoke('plugin:menu|popup', { + rid: this.rid, + kind: this.kind, + window: (_a = window === null || window === void 0 ? void 0 : window.label) !== null && _a !== void 0 ? _a : null, + at: at instanceof dpi.Position ? at : at ? new dpi.Position(at) : null + }); + } + /** + * Set this submenu as the Window menu for the application on macOS. + * + * This will cause macOS to automatically add window-switching items and + * certain other items to the menu. + * + * #### Platform-specific: + * + * - **Windows / Linux**: Unsupported. + */ + async setAsWindowsMenuForNSApp() { + return core.invoke('plugin:menu|set_as_windows_menu_for_nsapp', { + rid: this.rid + }); + } + /** + * Set this submenu as the Help menu for the application on macOS. + * + * This will cause macOS to automatically add a search box to the menu. + * + * If no menu is set as the Help menu, macOS will automatically use any menu + * which has a title matching the localized word "Help". + * + * #### Platform-specific: + * + * - **Windows / Linux**: Unsupported. + */ + async setAsHelpMenuForNSApp() { + return core.invoke('plugin:menu|set_as_help_menu_for_nsapp', { + rid: this.rid + }); + } + /** Sets an icon for this submenu */ + async setIcon(icon) { + return core.invoke('plugin:menu|set_icon', { + rid: this.rid, + kind: this.kind, + icon: image.transformImage(icon) + }); + } +} + +exports.Submenu = Submenu; +exports.itemFromKind = itemFromKind; diff --git a/node_modules/@tauri-apps/api/menu/submenu.d.ts b/node_modules/@tauri-apps/api/menu/submenu.d.ts new file mode 100644 index 0000000..6e5ee8b --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/submenu.d.ts @@ -0,0 +1,97 @@ +import { IconMenuItemOptions, PredefinedMenuItemOptions, CheckMenuItemOptions } from '../menu'; +import { MenuItem, type MenuItemOptions } from './menuItem'; +import { CheckMenuItem } from './checkMenuItem'; +import { IconMenuItem } from './iconMenuItem'; +import { PredefinedMenuItem } from './predefinedMenuItem'; +import { type LogicalPosition, PhysicalPosition, type Window } from '../window'; +import { type ItemKind, MenuItemBase } from './base'; +import { type MenuOptions } from './menu'; +import { MenuIcon } from '../image'; +/** @ignore */ +export declare function itemFromKind([rid, id, kind]: [number, string, ItemKind]): Submenu | MenuItem | PredefinedMenuItem | CheckMenuItem | IconMenuItem; +export type SubmenuOptions = (Omit & MenuOptions) & { + /** + * Icon to be used for the submenu. + * Note: you may need the `image-ico` or `image-png` Cargo features to use this API. + */ + icon?: MenuIcon; +}; +/** A type that is a submenu inside a {@linkcode Menu} or {@linkcode Submenu}. */ +export declare class Submenu extends MenuItemBase { + /** @ignore */ + protected constructor(rid: number, id: string); + /** Create a new submenu. */ + static new(opts: SubmenuOptions): Promise; + /** Returns the text of this submenu. */ + text(): Promise; + /** Sets the text for this submenu. */ + setText(text: string): Promise; + /** Returns whether this submenu is enabled or not. */ + isEnabled(): Promise; + /** Sets whether this submenu is enabled or not. */ + setEnabled(enabled: boolean): Promise; + /** + * Add a menu item to the end of this submenu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + append(items: T | T[]): Promise; + /** + * Add a menu item to the beginning of this submenu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + prepend(items: T | T[]): Promise; + /** + * Add a menu item to the specified position in this submenu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + insert(items: T | T[], position: number): Promise; + /** Remove a menu item from this submenu. */ + remove(item: Submenu | MenuItem | PredefinedMenuItem | CheckMenuItem | IconMenuItem): Promise; + /** Remove a menu item from this submenu at the specified position. */ + removeAt(position: number): Promise; + /** Returns a list of menu items that has been added to this submenu. */ + items(): Promise>; + /** Retrieves the menu item matching the given identifier. */ + get(id: string): Promise; + /** + * Popup this submenu as a context menu on the specified window. + * + * If the position, is provided, it is relative to the window's top-left corner. + */ + popup(at?: PhysicalPosition | LogicalPosition, window?: Window): Promise; + /** + * Set this submenu as the Window menu for the application on macOS. + * + * This will cause macOS to automatically add window-switching items and + * certain other items to the menu. + * + * #### Platform-specific: + * + * - **Windows / Linux**: Unsupported. + */ + setAsWindowsMenuForNSApp(): Promise; + /** + * Set this submenu as the Help menu for the application on macOS. + * + * This will cause macOS to automatically add a search box to the menu. + * + * If no menu is set as the Help menu, macOS will automatically use any menu + * which has a title matching the localized word "Help". + * + * #### Platform-specific: + * + * - **Windows / Linux**: Unsupported. + */ + setAsHelpMenuForNSApp(): Promise; + /** Sets an icon for this submenu */ + setIcon(icon: MenuIcon | null): Promise; +} diff --git a/node_modules/@tauri-apps/api/menu/submenu.js b/node_modules/@tauri-apps/api/menu/submenu.js new file mode 100644 index 0000000..83facef --- /dev/null +++ b/node_modules/@tauri-apps/api/menu/submenu.js @@ -0,0 +1,200 @@ +import { MenuItem } from './menuItem.js'; +import { CheckMenuItem } from './checkMenuItem.js'; +import { IconMenuItem } from './iconMenuItem.js'; +import { PredefinedMenuItem } from './predefinedMenuItem.js'; +import { invoke } from '../core.js'; +import { MenuItemBase, newMenu } from './base.js'; +import { Position } from '../dpi.js'; +import { transformImage } from '../image.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** @ignore */ +function itemFromKind([rid, id, kind]) { + /* eslint-disable @typescript-eslint/no-unsafe-return */ + switch (kind) { + case 'Submenu': + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new Submenu(rid, id); + case 'Predefined': + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new PredefinedMenuItem(rid, id); + case 'Check': + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new CheckMenuItem(rid, id); + case 'Icon': + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new IconMenuItem(rid, id); + case 'MenuItem': + default: + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new MenuItem(rid, id); + } + /* eslint-enable @typescript-eslint/no-unsafe-return */ +} +/** A type that is a submenu inside a {@linkcode Menu} or {@linkcode Submenu}. */ +class Submenu extends MenuItemBase { + /** @ignore */ + constructor(rid, id) { + super(rid, id, 'Submenu'); + } + /** Create a new submenu. */ + static async new(opts) { + return newMenu('Submenu', opts).then(([rid, id]) => new Submenu(rid, id)); + } + /** Returns the text of this submenu. */ + async text() { + return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }); + } + /** Sets the text for this submenu. */ + async setText(text) { + return invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }); + } + /** Returns whether this submenu is enabled or not. */ + async isEnabled() { + return invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind }); + } + /** Sets whether this submenu is enabled or not. */ + async setEnabled(enabled) { + return invoke('plugin:menu|set_enabled', { + rid: this.rid, + kind: this.kind, + enabled + }); + } + /** + * Add a menu item to the end of this submenu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async append(items) { + return invoke('plugin:menu|append', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => 'rid' in i ? [i.rid, i.kind] : i) + }); + } + /** + * Add a menu item to the beginning of this submenu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async prepend(items) { + return invoke('plugin:menu|prepend', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => 'rid' in i ? [i.rid, i.kind] : i) + }); + } + /** + * Add a menu item to the specified position in this submenu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async insert(items, position) { + return invoke('plugin:menu|insert', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => 'rid' in i ? [i.rid, i.kind] : i), + position + }); + } + /** Remove a menu item from this submenu. */ + async remove(item) { + return invoke('plugin:menu|remove', { + rid: this.rid, + kind: this.kind, + item: [item.rid, item.kind] + }); + } + /** Remove a menu item from this submenu at the specified position. */ + async removeAt(position) { + return invoke('plugin:menu|remove_at', { + rid: this.rid, + kind: this.kind, + position + }).then(itemFromKind); + } + /** Returns a list of menu items that has been added to this submenu. */ + async items() { + return invoke('plugin:menu|items', { + rid: this.rid, + kind: this.kind + }).then((i) => i.map(itemFromKind)); + } + /** Retrieves the menu item matching the given identifier. */ + async get(id) { + return invoke('plugin:menu|get', { + rid: this.rid, + kind: this.kind, + id + }).then((r) => (r ? itemFromKind(r) : null)); + } + /** + * Popup this submenu as a context menu on the specified window. + * + * If the position, is provided, it is relative to the window's top-left corner. + */ + async popup(at, window) { + var _a; + return invoke('plugin:menu|popup', { + rid: this.rid, + kind: this.kind, + window: (_a = window === null || window === void 0 ? void 0 : window.label) !== null && _a !== void 0 ? _a : null, + at: at instanceof Position ? at : at ? new Position(at) : null + }); + } + /** + * Set this submenu as the Window menu for the application on macOS. + * + * This will cause macOS to automatically add window-switching items and + * certain other items to the menu. + * + * #### Platform-specific: + * + * - **Windows / Linux**: Unsupported. + */ + async setAsWindowsMenuForNSApp() { + return invoke('plugin:menu|set_as_windows_menu_for_nsapp', { + rid: this.rid + }); + } + /** + * Set this submenu as the Help menu for the application on macOS. + * + * This will cause macOS to automatically add a search box to the menu. + * + * If no menu is set as the Help menu, macOS will automatically use any menu + * which has a title matching the localized word "Help". + * + * #### Platform-specific: + * + * - **Windows / Linux**: Unsupported. + */ + async setAsHelpMenuForNSApp() { + return invoke('plugin:menu|set_as_help_menu_for_nsapp', { + rid: this.rid + }); + } + /** Sets an icon for this submenu */ + async setIcon(icon) { + return invoke('plugin:menu|set_icon', { + rid: this.rid, + kind: this.kind, + icon: transformImage(icon) + }); + } +} + +export { Submenu, itemFromKind }; diff --git a/node_modules/@tauri-apps/api/mocks.cjs b/node_modules/@tauri-apps/api/mocks.cjs new file mode 100644 index 0000000..96d9e3b --- /dev/null +++ b/node_modules/@tauri-apps/api/mocks.cjs @@ -0,0 +1,297 @@ +'use strict'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +function mockInternals() { + var _a, _b; + window.__TAURI_INTERNALS__ = (_a = window.__TAURI_INTERNALS__) !== null && _a !== void 0 ? _a : {}; + window.__TAURI_EVENT_PLUGIN_INTERNALS__ = + (_b = window.__TAURI_EVENT_PLUGIN_INTERNALS__) !== null && _b !== void 0 ? _b : {}; +} +/** + * Intercepts all IPC requests with the given mock handler. + * + * This function can be used when testing tauri frontend applications or when running the frontend in a Node.js context during static site generation. + * + * # Examples + * + * Testing setup using Vitest: + * ```ts + * import { mockIPC, clearMocks } from "@tauri-apps/api/mocks" + * import { invoke } from "@tauri-apps/api/core" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked command", () => { + * mockIPC((cmd, payload) => { + * switch (cmd) { + * case "add": + * return (payload.a as number) + (payload.b as number); + * default: + * break; + * } + * }); + * + * expect(invoke('add', { a: 12, b: 15 })).resolves.toBe(27); + * }) + * ``` + * + * The callback function can also return a Promise: + * ```js + * import { mockIPC, clearMocks } from "@tauri-apps/api/mocks" + * import { invoke } from "@tauri-apps/api/core" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked command", () => { + * mockIPC((cmd, payload) => { + * if(cmd === "get_data") { + * return fetch("https://example.com/data.json") + * .then((response) => response.json()) + * } + * }); + * + * expect(invoke('get_data')).resolves.toBe({ foo: 'bar' }); + * }) + * ``` + * + * `listen` can also be mocked with direct calls to the `emit` function. This functionality is opt-in via the `shouldMockEvents` option: + * ```js + * import { mockIPC, clearMocks } from "@tauri-apps/api/mocks" + * import { emit, listen } from "@tauri-apps/api/event" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked event", () => { + * mockIPC(() => {}, { shouldMockEvents: true }); // enable event mocking + * + * const eventHandler = vi.fn(); + * listen('test-event', eventHandler); // typically in component setup or similar + * + * emit('test-event', { foo: 'bar' }); + * expect(eventHandler).toHaveBeenCalledWith({ + * event: 'test-event', + * payload: { foo: 'bar' } + * }); + * }) + * ``` + * `emitTo` is currently **not** supported by this mock implementation. + * + * @since 1.0.0 + */ +function mockIPC(cb, options) { + mockInternals(); + function isEventPluginInvoke(cmd) { + return cmd.startsWith('plugin:event|'); + } + function handleEventPlugin(cmd, args) { + switch (cmd) { + case 'plugin:event|listen': + return handleListen(args); + case 'plugin:event|emit': + return handleEmit(args); + case 'plugin:event|unlisten': + return handleRemoveListener(args); + } + } + const listeners = new Map(); + function handleListen(args) { + if (!listeners.has(args.event)) { + listeners.set(args.event, []); + } + listeners.get(args.event).push(args.handler); + return args.handler; + } + function handleEmit(args) { + const eventListeners = listeners.get(args.event) || []; + for (const handler of eventListeners) { + runCallback(handler, args); + } + return null; + } + function handleRemoveListener(args) { + const eventListeners = listeners.get(args.event); + if (eventListeners) { + const index = eventListeners.indexOf(args.id); + if (index !== -1) { + eventListeners.splice(index, 1); + } + } + } + // eslint-disable-next-line @typescript-eslint/require-await + async function invoke(cmd, args, _options) { + if ((options === null || options === void 0 ? void 0 : options.shouldMockEvents) && isEventPluginInvoke(cmd)) { + return handleEventPlugin(cmd, args); + } + return cb(cmd, args); + } + const callbacks = new Map(); + function registerCallback(callback, once = false) { + const identifier = window.crypto.getRandomValues(new Uint32Array(1))[0]; + callbacks.set(identifier, (data) => { + if (once) { + unregisterCallback(identifier); + } + return callback && callback(data); + }); + return identifier; + } + function unregisterCallback(id) { + callbacks.delete(id); + } + function runCallback(id, data) { + const callback = callbacks.get(id); + if (callback) { + callback(data); + } + else { + // eslint-disable-next-line no-console + console.warn(`[TAURI] Couldn't find callback id ${id}. This might happen when the app is reloaded while Rust is running an asynchronous operation.`); + } + } + function unregisterListener(event, id) { + unregisterCallback(id); + } + window.__TAURI_INTERNALS__.invoke = invoke; + window.__TAURI_INTERNALS__.transformCallback = registerCallback; + window.__TAURI_INTERNALS__.unregisterCallback = unregisterCallback; + window.__TAURI_INTERNALS__.runCallback = runCallback; + window.__TAURI_INTERNALS__.callbacks = callbacks; + window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener = + unregisterListener; +} +/** + * Mocks one or many window labels. + * In non-tauri context it is required to call this function *before* using the `@tauri-apps/api/window` module. + * + * This function only mocks the *presence* of windows, + * window properties (e.g. width and height) can be mocked like regular IPC calls using the `mockIPC` function. + * + * # Examples + * + * ```js + * import { mockWindows } from "@tauri-apps/api/mocks"; + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * + * mockWindows("main", "second", "third"); + * + * const win = getCurrentWindow(); + * + * win.label // "main" + * ``` + * + * ```js + * import { mockWindows } from "@tauri-apps/api/mocks"; + * + * mockWindows("main", "second", "third"); + * + * mockIPC((cmd, args) => { + * if (cmd === "plugin:event|emit") { + * console.log('emit event', args?.event, args?.payload); + * } + * }); + * + * const { emit } = await import("@tauri-apps/api/event"); + * await emit('loaded'); // this will cause the mocked IPC handler to log to the console. + * ``` + * + * @param current Label of window this JavaScript context is running in. + * + * @since 1.0.0 + */ +function mockWindows(current, ..._additionalWindows) { + mockInternals(); + window.__TAURI_INTERNALS__.metadata = { + currentWindow: { label: current }, + currentWebview: { windowLabel: current, label: current } + }; +} +/** + * Mock `convertFileSrc` function + * + * + * @example + * ```js + * import { mockConvertFileSrc } from "@tauri-apps/api/mocks"; + * import { convertFileSrc } from "@tauri-apps/api/core"; + * + * mockConvertFileSrc("windows") + * + * const url = convertFileSrc("C:\\Users\\user\\file.txt") + * ``` + * + * @param osName The operating system to mock, can be one of linux, macos, or windows + * + * @since 1.6.0 + */ +function mockConvertFileSrc(osName) { + mockInternals(); + window.__TAURI_INTERNALS__.convertFileSrc = function (filePath, protocol = 'asset') { + const path = encodeURIComponent(filePath); + return osName === 'windows' + ? `http://${protocol}.localhost/${path}` + : `${protocol}://localhost/${path}`; + }; +} +/** + * Clears mocked functions/data injected by the other functions in this module. + * When using a test runner that doesn't provide a fresh window object for each test, calling this function will reset tauri specific properties. + * + * # Example + * + * ```js + * import { mockWindows, clearMocks } from "@tauri-apps/api/mocks" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked windows", () => { + * mockWindows("main", "second", "third"); + * + * expect(window.__TAURI_INTERNALS__).toHaveProperty("metadata") + * }) + * + * test("no mocked windows", () => { + * expect(window.__TAURI_INTERNALS__).not.toHaveProperty("metadata") + * }) + * ``` + * + * @since 1.0.0 + */ +function clearMocks() { + if (typeof window.__TAURI_INTERNALS__ !== 'object') { + return; + } + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.invoke; + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.transformCallback; + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.unregisterCallback; + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.runCallback; + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.callbacks; + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.convertFileSrc; + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.metadata; + if (typeof window.__TAURI_EVENT_PLUGIN_INTERNALS__ !== 'object') { + return; + } + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener; +} + +exports.clearMocks = clearMocks; +exports.mockConvertFileSrc = mockConvertFileSrc; +exports.mockIPC = mockIPC; +exports.mockWindows = mockWindows; diff --git a/node_modules/@tauri-apps/api/mocks.d.ts b/node_modules/@tauri-apps/api/mocks.d.ts new file mode 100644 index 0000000..8eeb818 --- /dev/null +++ b/node_modules/@tauri-apps/api/mocks.d.ts @@ -0,0 +1,177 @@ +import type { InvokeArgs } from './core'; +/** + * Options for `mockIPC`. + * + * # Options + * `shouldMockEvents`: If true, the `listen` and `emit` functions will be mocked, allowing you to test event handling without a real backend. + * **This will consume any events emitted with the `plugin:event` prefix.** + * + * @since 2.7.0 + */ +export interface MockIPCOptions { + shouldMockEvents?: boolean; +} +/** + * Intercepts all IPC requests with the given mock handler. + * + * This function can be used when testing tauri frontend applications or when running the frontend in a Node.js context during static site generation. + * + * # Examples + * + * Testing setup using Vitest: + * ```ts + * import { mockIPC, clearMocks } from "@tauri-apps/api/mocks" + * import { invoke } from "@tauri-apps/api/core" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked command", () => { + * mockIPC((cmd, payload) => { + * switch (cmd) { + * case "add": + * return (payload.a as number) + (payload.b as number); + * default: + * break; + * } + * }); + * + * expect(invoke('add', { a: 12, b: 15 })).resolves.toBe(27); + * }) + * ``` + * + * The callback function can also return a Promise: + * ```js + * import { mockIPC, clearMocks } from "@tauri-apps/api/mocks" + * import { invoke } from "@tauri-apps/api/core" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked command", () => { + * mockIPC((cmd, payload) => { + * if(cmd === "get_data") { + * return fetch("https://example.com/data.json") + * .then((response) => response.json()) + * } + * }); + * + * expect(invoke('get_data')).resolves.toBe({ foo: 'bar' }); + * }) + * ``` + * + * `listen` can also be mocked with direct calls to the `emit` function. This functionality is opt-in via the `shouldMockEvents` option: + * ```js + * import { mockIPC, clearMocks } from "@tauri-apps/api/mocks" + * import { emit, listen } from "@tauri-apps/api/event" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked event", () => { + * mockIPC(() => {}, { shouldMockEvents: true }); // enable event mocking + * + * const eventHandler = vi.fn(); + * listen('test-event', eventHandler); // typically in component setup or similar + * + * emit('test-event', { foo: 'bar' }); + * expect(eventHandler).toHaveBeenCalledWith({ + * event: 'test-event', + * payload: { foo: 'bar' } + * }); + * }) + * ``` + * `emitTo` is currently **not** supported by this mock implementation. + * + * @since 1.0.0 + */ +export declare function mockIPC(cb: (cmd: string, payload?: InvokeArgs) => unknown, options?: MockIPCOptions): void; +/** + * Mocks one or many window labels. + * In non-tauri context it is required to call this function *before* using the `@tauri-apps/api/window` module. + * + * This function only mocks the *presence* of windows, + * window properties (e.g. width and height) can be mocked like regular IPC calls using the `mockIPC` function. + * + * # Examples + * + * ```js + * import { mockWindows } from "@tauri-apps/api/mocks"; + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * + * mockWindows("main", "second", "third"); + * + * const win = getCurrentWindow(); + * + * win.label // "main" + * ``` + * + * ```js + * import { mockWindows } from "@tauri-apps/api/mocks"; + * + * mockWindows("main", "second", "third"); + * + * mockIPC((cmd, args) => { + * if (cmd === "plugin:event|emit") { + * console.log('emit event', args?.event, args?.payload); + * } + * }); + * + * const { emit } = await import("@tauri-apps/api/event"); + * await emit('loaded'); // this will cause the mocked IPC handler to log to the console. + * ``` + * + * @param current Label of window this JavaScript context is running in. + * + * @since 1.0.0 + */ +export declare function mockWindows(current: string, ..._additionalWindows: string[]): void; +/** + * Mock `convertFileSrc` function + * + * + * @example + * ```js + * import { mockConvertFileSrc } from "@tauri-apps/api/mocks"; + * import { convertFileSrc } from "@tauri-apps/api/core"; + * + * mockConvertFileSrc("windows") + * + * const url = convertFileSrc("C:\\Users\\user\\file.txt") + * ``` + * + * @param osName The operating system to mock, can be one of linux, macos, or windows + * + * @since 1.6.0 + */ +export declare function mockConvertFileSrc(osName: string): void; +/** + * Clears mocked functions/data injected by the other functions in this module. + * When using a test runner that doesn't provide a fresh window object for each test, calling this function will reset tauri specific properties. + * + * # Example + * + * ```js + * import { mockWindows, clearMocks } from "@tauri-apps/api/mocks" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked windows", () => { + * mockWindows("main", "second", "third"); + * + * expect(window.__TAURI_INTERNALS__).toHaveProperty("metadata") + * }) + * + * test("no mocked windows", () => { + * expect(window.__TAURI_INTERNALS__).not.toHaveProperty("metadata") + * }) + * ``` + * + * @since 1.0.0 + */ +export declare function clearMocks(): void; diff --git a/node_modules/@tauri-apps/api/mocks.js b/node_modules/@tauri-apps/api/mocks.js new file mode 100644 index 0000000..a08c0b2 --- /dev/null +++ b/node_modules/@tauri-apps/api/mocks.js @@ -0,0 +1,292 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +function mockInternals() { + var _a, _b; + window.__TAURI_INTERNALS__ = (_a = window.__TAURI_INTERNALS__) !== null && _a !== void 0 ? _a : {}; + window.__TAURI_EVENT_PLUGIN_INTERNALS__ = + (_b = window.__TAURI_EVENT_PLUGIN_INTERNALS__) !== null && _b !== void 0 ? _b : {}; +} +/** + * Intercepts all IPC requests with the given mock handler. + * + * This function can be used when testing tauri frontend applications or when running the frontend in a Node.js context during static site generation. + * + * # Examples + * + * Testing setup using Vitest: + * ```ts + * import { mockIPC, clearMocks } from "@tauri-apps/api/mocks" + * import { invoke } from "@tauri-apps/api/core" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked command", () => { + * mockIPC((cmd, payload) => { + * switch (cmd) { + * case "add": + * return (payload.a as number) + (payload.b as number); + * default: + * break; + * } + * }); + * + * expect(invoke('add', { a: 12, b: 15 })).resolves.toBe(27); + * }) + * ``` + * + * The callback function can also return a Promise: + * ```js + * import { mockIPC, clearMocks } from "@tauri-apps/api/mocks" + * import { invoke } from "@tauri-apps/api/core" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked command", () => { + * mockIPC((cmd, payload) => { + * if(cmd === "get_data") { + * return fetch("https://example.com/data.json") + * .then((response) => response.json()) + * } + * }); + * + * expect(invoke('get_data')).resolves.toBe({ foo: 'bar' }); + * }) + * ``` + * + * `listen` can also be mocked with direct calls to the `emit` function. This functionality is opt-in via the `shouldMockEvents` option: + * ```js + * import { mockIPC, clearMocks } from "@tauri-apps/api/mocks" + * import { emit, listen } from "@tauri-apps/api/event" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked event", () => { + * mockIPC(() => {}, { shouldMockEvents: true }); // enable event mocking + * + * const eventHandler = vi.fn(); + * listen('test-event', eventHandler); // typically in component setup or similar + * + * emit('test-event', { foo: 'bar' }); + * expect(eventHandler).toHaveBeenCalledWith({ + * event: 'test-event', + * payload: { foo: 'bar' } + * }); + * }) + * ``` + * `emitTo` is currently **not** supported by this mock implementation. + * + * @since 1.0.0 + */ +function mockIPC(cb, options) { + mockInternals(); + function isEventPluginInvoke(cmd) { + return cmd.startsWith('plugin:event|'); + } + function handleEventPlugin(cmd, args) { + switch (cmd) { + case 'plugin:event|listen': + return handleListen(args); + case 'plugin:event|emit': + return handleEmit(args); + case 'plugin:event|unlisten': + return handleRemoveListener(args); + } + } + const listeners = new Map(); + function handleListen(args) { + if (!listeners.has(args.event)) { + listeners.set(args.event, []); + } + listeners.get(args.event).push(args.handler); + return args.handler; + } + function handleEmit(args) { + const eventListeners = listeners.get(args.event) || []; + for (const handler of eventListeners) { + runCallback(handler, args); + } + return null; + } + function handleRemoveListener(args) { + const eventListeners = listeners.get(args.event); + if (eventListeners) { + const index = eventListeners.indexOf(args.id); + if (index !== -1) { + eventListeners.splice(index, 1); + } + } + } + // eslint-disable-next-line @typescript-eslint/require-await + async function invoke(cmd, args, _options) { + if ((options === null || options === void 0 ? void 0 : options.shouldMockEvents) && isEventPluginInvoke(cmd)) { + return handleEventPlugin(cmd, args); + } + return cb(cmd, args); + } + const callbacks = new Map(); + function registerCallback(callback, once = false) { + const identifier = window.crypto.getRandomValues(new Uint32Array(1))[0]; + callbacks.set(identifier, (data) => { + if (once) { + unregisterCallback(identifier); + } + return callback && callback(data); + }); + return identifier; + } + function unregisterCallback(id) { + callbacks.delete(id); + } + function runCallback(id, data) { + const callback = callbacks.get(id); + if (callback) { + callback(data); + } + else { + // eslint-disable-next-line no-console + console.warn(`[TAURI] Couldn't find callback id ${id}. This might happen when the app is reloaded while Rust is running an asynchronous operation.`); + } + } + function unregisterListener(event, id) { + unregisterCallback(id); + } + window.__TAURI_INTERNALS__.invoke = invoke; + window.__TAURI_INTERNALS__.transformCallback = registerCallback; + window.__TAURI_INTERNALS__.unregisterCallback = unregisterCallback; + window.__TAURI_INTERNALS__.runCallback = runCallback; + window.__TAURI_INTERNALS__.callbacks = callbacks; + window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener = + unregisterListener; +} +/** + * Mocks one or many window labels. + * In non-tauri context it is required to call this function *before* using the `@tauri-apps/api/window` module. + * + * This function only mocks the *presence* of windows, + * window properties (e.g. width and height) can be mocked like regular IPC calls using the `mockIPC` function. + * + * # Examples + * + * ```js + * import { mockWindows } from "@tauri-apps/api/mocks"; + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * + * mockWindows("main", "second", "third"); + * + * const win = getCurrentWindow(); + * + * win.label // "main" + * ``` + * + * ```js + * import { mockWindows } from "@tauri-apps/api/mocks"; + * + * mockWindows("main", "second", "third"); + * + * mockIPC((cmd, args) => { + * if (cmd === "plugin:event|emit") { + * console.log('emit event', args?.event, args?.payload); + * } + * }); + * + * const { emit } = await import("@tauri-apps/api/event"); + * await emit('loaded'); // this will cause the mocked IPC handler to log to the console. + * ``` + * + * @param current Label of window this JavaScript context is running in. + * + * @since 1.0.0 + */ +function mockWindows(current, ..._additionalWindows) { + mockInternals(); + window.__TAURI_INTERNALS__.metadata = { + currentWindow: { label: current }, + currentWebview: { windowLabel: current, label: current } + }; +} +/** + * Mock `convertFileSrc` function + * + * + * @example + * ```js + * import { mockConvertFileSrc } from "@tauri-apps/api/mocks"; + * import { convertFileSrc } from "@tauri-apps/api/core"; + * + * mockConvertFileSrc("windows") + * + * const url = convertFileSrc("C:\\Users\\user\\file.txt") + * ``` + * + * @param osName The operating system to mock, can be one of linux, macos, or windows + * + * @since 1.6.0 + */ +function mockConvertFileSrc(osName) { + mockInternals(); + window.__TAURI_INTERNALS__.convertFileSrc = function (filePath, protocol = 'asset') { + const path = encodeURIComponent(filePath); + return osName === 'windows' + ? `http://${protocol}.localhost/${path}` + : `${protocol}://localhost/${path}`; + }; +} +/** + * Clears mocked functions/data injected by the other functions in this module. + * When using a test runner that doesn't provide a fresh window object for each test, calling this function will reset tauri specific properties. + * + * # Example + * + * ```js + * import { mockWindows, clearMocks } from "@tauri-apps/api/mocks" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked windows", () => { + * mockWindows("main", "second", "third"); + * + * expect(window.__TAURI_INTERNALS__).toHaveProperty("metadata") + * }) + * + * test("no mocked windows", () => { + * expect(window.__TAURI_INTERNALS__).not.toHaveProperty("metadata") + * }) + * ``` + * + * @since 1.0.0 + */ +function clearMocks() { + if (typeof window.__TAURI_INTERNALS__ !== 'object') { + return; + } + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.invoke; + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.transformCallback; + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.unregisterCallback; + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.runCallback; + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.callbacks; + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.convertFileSrc; + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.metadata; + if (typeof window.__TAURI_EVENT_PLUGIN_INTERNALS__ !== 'object') { + return; + } + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener; +} + +export { clearMocks, mockConvertFileSrc, mockIPC, mockWindows }; diff --git a/node_modules/@tauri-apps/api/package.json b/node_modules/@tauri-apps/api/package.json new file mode 100644 index 0000000..f5ac01a --- /dev/null +++ b/node_modules/@tauri-apps/api/package.json @@ -0,0 +1,63 @@ +{ + "name": "@tauri-apps/api", + "version": "2.10.1", + "description": "Tauri API definitions", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "homepage": "https://github.com/tauri-apps/tauri#readme", + "type": "module", + "main": "./index.cjs", + "module": "./index.js", + "types": "./index.d.ts", + "exports": { + ".": { + "import": "./index.js", + "require": "./index.cjs", + "types": "./index.d.ts" + }, + "./*": { + "import": "./*.js", + "require": "./*.cjs", + "types": "./*.d.ts" + }, + "./package.json": "./package.json" + }, + "devDependencies": { + "@eslint/js": "^9.29.0", + "@rollup/plugin-terser": "0.4.4", + "@rollup/plugin-typescript": "12.3.0", + "@types/eslint": "^9.6.1", + "@types/node": "^24.0.0", + "eslint": "^9.29.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-security": "3.0.1", + "fast-glob": "3.3.3", + "globals": "^17.0.0", + "rollup": "4.57.0", + "tslib": "^2.8.1", + "typescript": "^5.8.3", + "typescript-eslint": "^8.34.1" + }, + "scripts": { + "build": "rollup -c --configPlugin typescript", + "build:debug": "rollup -c --configPlugin typescript", + "npm-pack": "pnpm build && cd ./dist && npm pack", + "npm-publish": "pnpm build && cd ./dist && pnpm publish --access public --loglevel silly --no-git-checks", + "ts:check": "tsc --noEmit", + "eslint:check": "eslint src/**/*.ts", + "eslint:fix": "eslint src/**/*.ts --fix" + } +} \ No newline at end of file diff --git a/node_modules/@tauri-apps/api/path.cjs b/node_modules/@tauri-apps/api/path.cjs new file mode 100644 index 0000000..15f16c2 --- /dev/null +++ b/node_modules/@tauri-apps/api/path.cjs @@ -0,0 +1,753 @@ +'use strict'; + +var core = require('./core.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * The path module provides utilities for working with file and directory paths. + * + * This package is also accessible with `window.__TAURI__.path` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * It is recommended to allowlist only the APIs you use for optimal bundle size and security. + * @module + */ +/** + * @since 2.0.0 + */ +exports.BaseDirectory = void 0; +(function (BaseDirectory) { + /** + * @see {@link audioDir} for more information. + */ + BaseDirectory[BaseDirectory["Audio"] = 1] = "Audio"; + /** + * @see {@link cacheDir} for more information. + */ + BaseDirectory[BaseDirectory["Cache"] = 2] = "Cache"; + /** + * @see {@link configDir} for more information. + */ + BaseDirectory[BaseDirectory["Config"] = 3] = "Config"; + /** + * @see {@link dataDir} for more information. + */ + BaseDirectory[BaseDirectory["Data"] = 4] = "Data"; + /** + * @see {@link localDataDir} for more information. + */ + BaseDirectory[BaseDirectory["LocalData"] = 5] = "LocalData"; + /** + * @see {@link documentDir} for more information. + */ + BaseDirectory[BaseDirectory["Document"] = 6] = "Document"; + /** + * @see {@link downloadDir} for more information. + */ + BaseDirectory[BaseDirectory["Download"] = 7] = "Download"; + /** + * @see {@link pictureDir} for more information. + */ + BaseDirectory[BaseDirectory["Picture"] = 8] = "Picture"; + /** + * @see {@link publicDir} for more information. + */ + BaseDirectory[BaseDirectory["Public"] = 9] = "Public"; + /** + * @see {@link videoDir} for more information. + */ + BaseDirectory[BaseDirectory["Video"] = 10] = "Video"; + /** + * @see {@link resourceDir} for more information. + */ + BaseDirectory[BaseDirectory["Resource"] = 11] = "Resource"; + /** + * @see {@link tempDir} for more information. + */ + BaseDirectory[BaseDirectory["Temp"] = 12] = "Temp"; + /** + * @see {@link appConfigDir} for more information. + */ + BaseDirectory[BaseDirectory["AppConfig"] = 13] = "AppConfig"; + /** + * @see {@link appDataDir} for more information. + */ + BaseDirectory[BaseDirectory["AppData"] = 14] = "AppData"; + /** + * @see {@link appLocalDataDir} for more information. + */ + BaseDirectory[BaseDirectory["AppLocalData"] = 15] = "AppLocalData"; + /** + * @see {@link appCacheDir} for more information. + */ + BaseDirectory[BaseDirectory["AppCache"] = 16] = "AppCache"; + /** + * @see {@link appLogDir} for more information. + */ + BaseDirectory[BaseDirectory["AppLog"] = 17] = "AppLog"; + /** + * @see {@link desktopDir} for more information. + */ + BaseDirectory[BaseDirectory["Desktop"] = 18] = "Desktop"; + /** + * @see {@link executableDir} for more information. + */ + BaseDirectory[BaseDirectory["Executable"] = 19] = "Executable"; + /** + * @see {@link fontDir} for more information. + */ + BaseDirectory[BaseDirectory["Font"] = 20] = "Font"; + /** + * @see {@link homeDir} for more information. + */ + BaseDirectory[BaseDirectory["Home"] = 21] = "Home"; + /** + * @see {@link runtimeDir} for more information. + */ + BaseDirectory[BaseDirectory["Runtime"] = 22] = "Runtime"; + /** + * @see {@link templateDir} for more information. + */ + BaseDirectory[BaseDirectory["Template"] = 23] = "Template"; +})(exports.BaseDirectory || (exports.BaseDirectory = {})); +/** + * Returns the path to the suggested directory for your app's config files. + * Resolves to `${configDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appConfigDir } from '@tauri-apps/api/path'; + * const appConfigDirPath = await appConfigDir(); + * ``` + * + * @since 1.2.0 + */ +async function appConfigDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.AppConfig + }); +} +/** + * Returns the path to the suggested directory for your app's data files. + * Resolves to `${dataDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * ``` + * + * @since 1.2.0 + */ +async function appDataDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.AppData + }); +} +/** + * Returns the path to the suggested directory for your app's local data files. + * Resolves to `${localDataDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appLocalDataDir } from '@tauri-apps/api/path'; + * const appLocalDataDirPath = await appLocalDataDir(); + * ``` + * + * @since 1.2.0 + */ +async function appLocalDataDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.AppLocalData + }); +} +/** + * Returns the path to the suggested directory for your app's cache files. + * Resolves to `${cacheDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appCacheDir } from '@tauri-apps/api/path'; + * const appCacheDirPath = await appCacheDir(); + * ``` + * + * @since 1.2.0 + */ +async function appCacheDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.AppCache + }); +} +/** + * Returns the path to the user's audio directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_MUSIC_DIR`. + * - **macOS:** Resolves to `$HOME/Music`. + * - **Windows:** Resolves to `{FOLDERID_Music}`. + * @example + * ```typescript + * import { audioDir } from '@tauri-apps/api/path'; + * const audioDirPath = await audioDir(); + * ``` + * + * @since 1.0.0 + */ +async function audioDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Audio + }); +} +/** + * Returns the path to the user's cache directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_CACHE_HOME` or `$HOME/.cache`. + * - **macOS:** Resolves to `$HOME/Library/Caches`. + * - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + * @example + * ```typescript + * import { cacheDir } from '@tauri-apps/api/path'; + * const cacheDirPath = await cacheDir(); + * ``` + * + * @since 1.0.0 + */ +async function cacheDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Cache + }); +} +/** + * Returns the path to the user's config directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_CONFIG_HOME` or `$HOME/.config`. + * - **macOS:** Resolves to `$HOME/Library/Application Support`. + * - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + * @example + * ```typescript + * import { configDir } from '@tauri-apps/api/path'; + * const configDirPath = await configDir(); + * ``` + * + * @since 1.0.0 + */ +async function configDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Config + }); +} +/** + * Returns the path to the user's data directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + * - **macOS:** Resolves to `$HOME/Library/Application Support`. + * - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + * @example + * ```typescript + * import { dataDir } from '@tauri-apps/api/path'; + * const dataDirPath = await dataDir(); + * ``` + * + * @since 1.0.0 + */ +async function dataDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Data + }); +} +/** + * Returns the path to the user's desktop directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DESKTOP_DIR`. + * - **macOS:** Resolves to `$HOME/Desktop`. + * - **Windows:** Resolves to `{FOLDERID_Desktop}`. + * @example + * ```typescript + * import { desktopDir } from '@tauri-apps/api/path'; + * const desktopPath = await desktopDir(); + * ``` + * + * @since 1.0.0 + */ +async function desktopDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Desktop + }); +} +/** + * Returns the path to the user's document directory. + * @example + * ```typescript + * import { documentDir } from '@tauri-apps/api/path'; + * const documentDirPath = await documentDir(); + * ``` + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOCUMENTS_DIR`. + * - **macOS:** Resolves to `$HOME/Documents`. + * - **Windows:** Resolves to `{FOLDERID_Documents}`. + * + * @since 1.0.0 + */ +async function documentDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Document + }); +} +/** + * Returns the path to the user's download directory. + * + * #### Platform-specific + * + * - **Linux**: Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOWNLOAD_DIR`. + * - **macOS**: Resolves to `$HOME/Downloads`. + * - **Windows**: Resolves to `{FOLDERID_Downloads}`. + * @example + * ```typescript + * import { downloadDir } from '@tauri-apps/api/path'; + * const downloadDirPath = await downloadDir(); + * ``` + * + * @since 1.0.0 + */ +async function downloadDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Download + }); +} +/** + * Returns the path to the user's executable directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_BIN_HOME/../bin` or `$XDG_DATA_HOME/../bin` or `$HOME/.local/bin`. + * - **macOS:** Not supported. + * - **Windows:** Not supported. + * @example + * ```typescript + * import { executableDir } from '@tauri-apps/api/path'; + * const executableDirPath = await executableDir(); + * ``` + * + * @since 1.0.0 + */ +async function executableDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Executable + }); +} +/** + * Returns the path to the user's font directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_DATA_HOME/fonts` or `$HOME/.local/share/fonts`. + * - **macOS:** Resolves to `$HOME/Library/Fonts`. + * - **Windows:** Not supported. + * @example + * ```typescript + * import { fontDir } from '@tauri-apps/api/path'; + * const fontDirPath = await fontDir(); + * ``` + * + * @since 1.0.0 + */ +async function fontDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Font + }); +} +/** + * Returns the path to the user's home directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$HOME`. + * - **macOS:** Resolves to `$HOME`. + * - **Windows:** Resolves to `{FOLDERID_Profile}`. + * @example + * ```typescript + * import { homeDir } from '@tauri-apps/api/path'; + * const homeDirPath = await homeDir(); + * ``` + * + * @since 1.0.0 + */ +async function homeDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Home + }); +} +/** + * Returns the path to the user's local data directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + * - **macOS:** Resolves to `$HOME/Library/Application Support`. + * - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + * @example + * ```typescript + * import { localDataDir } from '@tauri-apps/api/path'; + * const localDataDirPath = await localDataDir(); + * ``` + * + * @since 1.0.0 + */ +async function localDataDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.LocalData + }); +} +/** + * Returns the path to the user's picture directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PICTURES_DIR`. + * - **macOS:** Resolves to `$HOME/Pictures`. + * - **Windows:** Resolves to `{FOLDERID_Pictures}`. + * @example + * ```typescript + * import { pictureDir } from '@tauri-apps/api/path'; + * const pictureDirPath = await pictureDir(); + * ``` + * + * @since 1.0.0 + */ +async function pictureDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Picture + }); +} +/** + * Returns the path to the user's public directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PUBLICSHARE_DIR`. + * - **macOS:** Resolves to `$HOME/Public`. + * - **Windows:** Resolves to `{FOLDERID_Public}`. + * @example + * ```typescript + * import { publicDir } from '@tauri-apps/api/path'; + * const publicDirPath = await publicDir(); + * ``` + * + * @since 1.0.0 + */ +async function publicDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Public + }); +} +/** + * Returns the path to the application's resource directory. + * To resolve a resource path, see {@linkcode resolveResource}. + * + * ## Platform-specific + * + * Although we provide the exact path where this function resolves to, + * this is not a contract and things might change in the future + * + * - **Windows:** Resolves to the directory that contains the main executable. + * - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to + * the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`. + * If not running in an AppImage, the path is `/usr/lib/${exe_name}`. + * When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`. + * - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app). + * - **iOS:** Resolves to `${exe_dir}/assets`. + * - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path, + * we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/), + * + * @example + * ```typescript + * import { resourceDir } from '@tauri-apps/api/path'; + * const resourceDirPath = await resourceDir(); + * ``` + * + * @since 1.0.0 + */ +async function resourceDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Resource + }); +} +/** + * Resolve the path to a resource file. + * @example + * ```typescript + * import { resolveResource } from '@tauri-apps/api/path'; + * const resourcePath = await resolveResource('script.sh'); + * ``` + * + * @param resourcePath The path to the resource. + * Must follow the same syntax as defined in `tauri.conf.json > bundle > resources`, i.e. keeping subfolders and parent dir components (`../`). + * @returns The full path to the resource. + * + * @since 1.0.0 + */ +async function resolveResource(resourcePath) { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Resource, + path: resourcePath + }); +} +/** + * Returns the path to the user's runtime directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_RUNTIME_DIR`. + * - **macOS:** Not supported. + * - **Windows:** Not supported. + * @example + * ```typescript + * import { runtimeDir } from '@tauri-apps/api/path'; + * const runtimeDirPath = await runtimeDir(); + * ``` + * + * @since 1.0.0 + */ +async function runtimeDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Runtime + }); +} +/** + * Returns the path to the user's template directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_TEMPLATES_DIR`. + * - **macOS:** Not supported. + * - **Windows:** Resolves to `{FOLDERID_Templates}`. + * @example + * ```typescript + * import { templateDir } from '@tauri-apps/api/path'; + * const templateDirPath = await templateDir(); + * ``` + * + * @since 1.0.0 + */ +async function templateDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Template + }); +} +/** + * Returns the path to the user's video directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_VIDEOS_DIR`. + * - **macOS:** Resolves to `$HOME/Movies`. + * - **Windows:** Resolves to `{FOLDERID_Videos}`. + * @example + * ```typescript + * import { videoDir } from '@tauri-apps/api/path'; + * const videoDirPath = await videoDir(); + * ``` + * + * @since 1.0.0 + */ +async function videoDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Video + }); +} +/** + * Returns the path to the suggested directory for your app's log files. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `${configDir}/${bundleIdentifier}/logs`. + * - **macOS:** Resolves to `${homeDir}/Library/Logs/{bundleIdentifier}` + * - **Windows:** Resolves to `${configDir}/${bundleIdentifier}/logs`. + * @example + * ```typescript + * import { appLogDir } from '@tauri-apps/api/path'; + * const appLogDirPath = await appLogDir(); + * ``` + * + * @since 1.2.0 + */ +async function appLogDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.AppLog + }); +} +/** + * Returns a temporary directory. + * @example + * ```typescript + * import { tempDir } from '@tauri-apps/api/path'; + * const temp = await tempDir(); + * ``` + * + * @since 2.0.0 + */ +async function tempDir() { + return core.invoke('plugin:path|resolve_directory', { + directory: exports.BaseDirectory.Temp + }); +} +/** + * Returns the platform-specific path segment separator: + * - `\` on Windows + * - `/` on POSIX + * + * @since 2.0.0 + */ +function sep() { + return window.__TAURI_INTERNALS__.plugins.path.sep; +} +/** + * Returns the platform-specific path segment delimiter: + * - `;` on Windows + * - `:` on POSIX + * + * @since 2.0.0 + */ +function delimiter() { + return window.__TAURI_INTERNALS__.plugins.path.delimiter; +} +/** + * Resolves a sequence of `paths` or `path` segments into an absolute path. + * @example + * ```typescript + * import { resolve, appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * const path = await resolve(appDataDirPath, '..', 'users', 'tauri', 'avatar.png'); + * ``` + * + * @since 1.0.0 + */ +async function resolve(...paths) { + return core.invoke('plugin:path|resolve', { paths }); +} +/** + * Normalizes the given `path`, resolving `'..'` and `'.'` segments and resolve symbolic links. + * @example + * ```typescript + * import { normalize, appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * const path = await normalize(`${appDataDirPath}/../users/tauri/avatar.png`); + * ``` + * + * @since 1.0.0 + */ +async function normalize(path) { + return core.invoke('plugin:path|normalize', { path }); +} +/** + * Joins all given `path` segments together using the platform-specific separator as a delimiter, then normalizes the resulting path. + * @example + * ```typescript + * import { join, appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * const path = await join(appDataDirPath, 'users', 'tauri', 'avatar.png'); + * ``` + * + * @since 1.0.0 + */ +async function join(...paths) { + return core.invoke('plugin:path|join', { paths }); +} +/** + * Returns the parent directory of a given `path`. Trailing directory separators are ignored. + * @example + * ```typescript + * import { dirname } from '@tauri-apps/api/path'; + * const dir = await dirname('/path/to/somedir/'); + * assert(dir === '/path/to'); + * ``` + * + * @since 1.0.0 + */ +async function dirname(path) { + return core.invoke('plugin:path|dirname', { path }); +} +/** + * Returns the extension of the `path`. + * @example + * ```typescript + * import { extname } from '@tauri-apps/api/path'; + * const ext = await extname('/path/to/file.html'); + * assert(ext === 'html'); + * ``` + * + * @since 1.0.0 + */ +async function extname(path) { + return core.invoke('plugin:path|extname', { path }); +} +/** + * Returns the last portion of a `path`. Trailing directory separators are ignored. + * @example + * ```typescript + * import { basename } from '@tauri-apps/api/path'; + * const base = await basename('path/to/app.conf'); + * assert(base === 'app.conf'); + * ``` + * @param ext An optional file extension to be removed from the returned path. + * + * @since 1.0.0 + */ +async function basename(path, ext) { + return core.invoke('plugin:path|basename', { path, ext }); +} +/** + * Returns whether the path is absolute or not. + * @example + * ```typescript + * import { isAbsolute } from '@tauri-apps/api/path'; + * assert(await isAbsolute('/home/tauri')); + * ``` + * + * @since 1.0.0 + */ +async function isAbsolute(path) { + return core.invoke('plugin:path|is_absolute', { path }); +} + +exports.appCacheDir = appCacheDir; +exports.appConfigDir = appConfigDir; +exports.appDataDir = appDataDir; +exports.appLocalDataDir = appLocalDataDir; +exports.appLogDir = appLogDir; +exports.audioDir = audioDir; +exports.basename = basename; +exports.cacheDir = cacheDir; +exports.configDir = configDir; +exports.dataDir = dataDir; +exports.delimiter = delimiter; +exports.desktopDir = desktopDir; +exports.dirname = dirname; +exports.documentDir = documentDir; +exports.downloadDir = downloadDir; +exports.executableDir = executableDir; +exports.extname = extname; +exports.fontDir = fontDir; +exports.homeDir = homeDir; +exports.isAbsolute = isAbsolute; +exports.join = join; +exports.localDataDir = localDataDir; +exports.normalize = normalize; +exports.pictureDir = pictureDir; +exports.publicDir = publicDir; +exports.resolve = resolve; +exports.resolveResource = resolveResource; +exports.resourceDir = resourceDir; +exports.runtimeDir = runtimeDir; +exports.sep = sep; +exports.tempDir = tempDir; +exports.templateDir = templateDir; +exports.videoDir = videoDir; diff --git a/node_modules/@tauri-apps/api/path.d.ts b/node_modules/@tauri-apps/api/path.d.ts new file mode 100644 index 0000000..1808f68 --- /dev/null +++ b/node_modules/@tauri-apps/api/path.d.ts @@ -0,0 +1,589 @@ +/** + * @since 2.0.0 + */ +declare enum BaseDirectory { + /** + * @see {@link audioDir} for more information. + */ + Audio = 1, + /** + * @see {@link cacheDir} for more information. + */ + Cache = 2, + /** + * @see {@link configDir} for more information. + */ + Config = 3, + /** + * @see {@link dataDir} for more information. + */ + Data = 4, + /** + * @see {@link localDataDir} for more information. + */ + LocalData = 5, + /** + * @see {@link documentDir} for more information. + */ + Document = 6, + /** + * @see {@link downloadDir} for more information. + */ + Download = 7, + /** + * @see {@link pictureDir} for more information. + */ + Picture = 8, + /** + * @see {@link publicDir} for more information. + */ + Public = 9, + /** + * @see {@link videoDir} for more information. + */ + Video = 10, + /** + * @see {@link resourceDir} for more information. + */ + Resource = 11, + /** + * @see {@link tempDir} for more information. + */ + Temp = 12, + /** + * @see {@link appConfigDir} for more information. + */ + AppConfig = 13, + /** + * @see {@link appDataDir} for more information. + */ + AppData = 14, + /** + * @see {@link appLocalDataDir} for more information. + */ + AppLocalData = 15, + /** + * @see {@link appCacheDir} for more information. + */ + AppCache = 16, + /** + * @see {@link appLogDir} for more information. + */ + AppLog = 17, + /** + * @see {@link desktopDir} for more information. + */ + Desktop = 18, + /** + * @see {@link executableDir} for more information. + */ + Executable = 19, + /** + * @see {@link fontDir} for more information. + */ + Font = 20, + /** + * @see {@link homeDir} for more information. + */ + Home = 21, + /** + * @see {@link runtimeDir} for more information. + */ + Runtime = 22, + /** + * @see {@link templateDir} for more information. + */ + Template = 23 +} +/** + * Returns the path to the suggested directory for your app's config files. + * Resolves to `${configDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appConfigDir } from '@tauri-apps/api/path'; + * const appConfigDirPath = await appConfigDir(); + * ``` + * + * @since 1.2.0 + */ +declare function appConfigDir(): Promise; +/** + * Returns the path to the suggested directory for your app's data files. + * Resolves to `${dataDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * ``` + * + * @since 1.2.0 + */ +declare function appDataDir(): Promise; +/** + * Returns the path to the suggested directory for your app's local data files. + * Resolves to `${localDataDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appLocalDataDir } from '@tauri-apps/api/path'; + * const appLocalDataDirPath = await appLocalDataDir(); + * ``` + * + * @since 1.2.0 + */ +declare function appLocalDataDir(): Promise; +/** + * Returns the path to the suggested directory for your app's cache files. + * Resolves to `${cacheDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appCacheDir } from '@tauri-apps/api/path'; + * const appCacheDirPath = await appCacheDir(); + * ``` + * + * @since 1.2.0 + */ +declare function appCacheDir(): Promise; +/** + * Returns the path to the user's audio directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_MUSIC_DIR`. + * - **macOS:** Resolves to `$HOME/Music`. + * - **Windows:** Resolves to `{FOLDERID_Music}`. + * @example + * ```typescript + * import { audioDir } from '@tauri-apps/api/path'; + * const audioDirPath = await audioDir(); + * ``` + * + * @since 1.0.0 + */ +declare function audioDir(): Promise; +/** + * Returns the path to the user's cache directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_CACHE_HOME` or `$HOME/.cache`. + * - **macOS:** Resolves to `$HOME/Library/Caches`. + * - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + * @example + * ```typescript + * import { cacheDir } from '@tauri-apps/api/path'; + * const cacheDirPath = await cacheDir(); + * ``` + * + * @since 1.0.0 + */ +declare function cacheDir(): Promise; +/** + * Returns the path to the user's config directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_CONFIG_HOME` or `$HOME/.config`. + * - **macOS:** Resolves to `$HOME/Library/Application Support`. + * - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + * @example + * ```typescript + * import { configDir } from '@tauri-apps/api/path'; + * const configDirPath = await configDir(); + * ``` + * + * @since 1.0.0 + */ +declare function configDir(): Promise; +/** + * Returns the path to the user's data directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + * - **macOS:** Resolves to `$HOME/Library/Application Support`. + * - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + * @example + * ```typescript + * import { dataDir } from '@tauri-apps/api/path'; + * const dataDirPath = await dataDir(); + * ``` + * + * @since 1.0.0 + */ +declare function dataDir(): Promise; +/** + * Returns the path to the user's desktop directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DESKTOP_DIR`. + * - **macOS:** Resolves to `$HOME/Desktop`. + * - **Windows:** Resolves to `{FOLDERID_Desktop}`. + * @example + * ```typescript + * import { desktopDir } from '@tauri-apps/api/path'; + * const desktopPath = await desktopDir(); + * ``` + * + * @since 1.0.0 + */ +declare function desktopDir(): Promise; +/** + * Returns the path to the user's document directory. + * @example + * ```typescript + * import { documentDir } from '@tauri-apps/api/path'; + * const documentDirPath = await documentDir(); + * ``` + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOCUMENTS_DIR`. + * - **macOS:** Resolves to `$HOME/Documents`. + * - **Windows:** Resolves to `{FOLDERID_Documents}`. + * + * @since 1.0.0 + */ +declare function documentDir(): Promise; +/** + * Returns the path to the user's download directory. + * + * #### Platform-specific + * + * - **Linux**: Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOWNLOAD_DIR`. + * - **macOS**: Resolves to `$HOME/Downloads`. + * - **Windows**: Resolves to `{FOLDERID_Downloads}`. + * @example + * ```typescript + * import { downloadDir } from '@tauri-apps/api/path'; + * const downloadDirPath = await downloadDir(); + * ``` + * + * @since 1.0.0 + */ +declare function downloadDir(): Promise; +/** + * Returns the path to the user's executable directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_BIN_HOME/../bin` or `$XDG_DATA_HOME/../bin` or `$HOME/.local/bin`. + * - **macOS:** Not supported. + * - **Windows:** Not supported. + * @example + * ```typescript + * import { executableDir } from '@tauri-apps/api/path'; + * const executableDirPath = await executableDir(); + * ``` + * + * @since 1.0.0 + */ +declare function executableDir(): Promise; +/** + * Returns the path to the user's font directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_DATA_HOME/fonts` or `$HOME/.local/share/fonts`. + * - **macOS:** Resolves to `$HOME/Library/Fonts`. + * - **Windows:** Not supported. + * @example + * ```typescript + * import { fontDir } from '@tauri-apps/api/path'; + * const fontDirPath = await fontDir(); + * ``` + * + * @since 1.0.0 + */ +declare function fontDir(): Promise; +/** + * Returns the path to the user's home directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$HOME`. + * - **macOS:** Resolves to `$HOME`. + * - **Windows:** Resolves to `{FOLDERID_Profile}`. + * @example + * ```typescript + * import { homeDir } from '@tauri-apps/api/path'; + * const homeDirPath = await homeDir(); + * ``` + * + * @since 1.0.0 + */ +declare function homeDir(): Promise; +/** + * Returns the path to the user's local data directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + * - **macOS:** Resolves to `$HOME/Library/Application Support`. + * - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + * @example + * ```typescript + * import { localDataDir } from '@tauri-apps/api/path'; + * const localDataDirPath = await localDataDir(); + * ``` + * + * @since 1.0.0 + */ +declare function localDataDir(): Promise; +/** + * Returns the path to the user's picture directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PICTURES_DIR`. + * - **macOS:** Resolves to `$HOME/Pictures`. + * - **Windows:** Resolves to `{FOLDERID_Pictures}`. + * @example + * ```typescript + * import { pictureDir } from '@tauri-apps/api/path'; + * const pictureDirPath = await pictureDir(); + * ``` + * + * @since 1.0.0 + */ +declare function pictureDir(): Promise; +/** + * Returns the path to the user's public directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PUBLICSHARE_DIR`. + * - **macOS:** Resolves to `$HOME/Public`. + * - **Windows:** Resolves to `{FOLDERID_Public}`. + * @example + * ```typescript + * import { publicDir } from '@tauri-apps/api/path'; + * const publicDirPath = await publicDir(); + * ``` + * + * @since 1.0.0 + */ +declare function publicDir(): Promise; +/** + * Returns the path to the application's resource directory. + * To resolve a resource path, see {@linkcode resolveResource}. + * + * ## Platform-specific + * + * Although we provide the exact path where this function resolves to, + * this is not a contract and things might change in the future + * + * - **Windows:** Resolves to the directory that contains the main executable. + * - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to + * the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`. + * If not running in an AppImage, the path is `/usr/lib/${exe_name}`. + * When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`. + * - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app). + * - **iOS:** Resolves to `${exe_dir}/assets`. + * - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path, + * we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/), + * + * @example + * ```typescript + * import { resourceDir } from '@tauri-apps/api/path'; + * const resourceDirPath = await resourceDir(); + * ``` + * + * @since 1.0.0 + */ +declare function resourceDir(): Promise; +/** + * Resolve the path to a resource file. + * @example + * ```typescript + * import { resolveResource } from '@tauri-apps/api/path'; + * const resourcePath = await resolveResource('script.sh'); + * ``` + * + * @param resourcePath The path to the resource. + * Must follow the same syntax as defined in `tauri.conf.json > bundle > resources`, i.e. keeping subfolders and parent dir components (`../`). + * @returns The full path to the resource. + * + * @since 1.0.0 + */ +declare function resolveResource(resourcePath: string): Promise; +/** + * Returns the path to the user's runtime directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_RUNTIME_DIR`. + * - **macOS:** Not supported. + * - **Windows:** Not supported. + * @example + * ```typescript + * import { runtimeDir } from '@tauri-apps/api/path'; + * const runtimeDirPath = await runtimeDir(); + * ``` + * + * @since 1.0.0 + */ +declare function runtimeDir(): Promise; +/** + * Returns the path to the user's template directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_TEMPLATES_DIR`. + * - **macOS:** Not supported. + * - **Windows:** Resolves to `{FOLDERID_Templates}`. + * @example + * ```typescript + * import { templateDir } from '@tauri-apps/api/path'; + * const templateDirPath = await templateDir(); + * ``` + * + * @since 1.0.0 + */ +declare function templateDir(): Promise; +/** + * Returns the path to the user's video directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_VIDEOS_DIR`. + * - **macOS:** Resolves to `$HOME/Movies`. + * - **Windows:** Resolves to `{FOLDERID_Videos}`. + * @example + * ```typescript + * import { videoDir } from '@tauri-apps/api/path'; + * const videoDirPath = await videoDir(); + * ``` + * + * @since 1.0.0 + */ +declare function videoDir(): Promise; +/** + * Returns the path to the suggested directory for your app's log files. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `${configDir}/${bundleIdentifier}/logs`. + * - **macOS:** Resolves to `${homeDir}/Library/Logs/{bundleIdentifier}` + * - **Windows:** Resolves to `${configDir}/${bundleIdentifier}/logs`. + * @example + * ```typescript + * import { appLogDir } from '@tauri-apps/api/path'; + * const appLogDirPath = await appLogDir(); + * ``` + * + * @since 1.2.0 + */ +declare function appLogDir(): Promise; +/** + * Returns a temporary directory. + * @example + * ```typescript + * import { tempDir } from '@tauri-apps/api/path'; + * const temp = await tempDir(); + * ``` + * + * @since 2.0.0 + */ +declare function tempDir(): Promise; +/** + * Returns the platform-specific path segment separator: + * - `\` on Windows + * - `/` on POSIX + * + * @since 2.0.0 + */ +declare function sep(): string; +/** + * Returns the platform-specific path segment delimiter: + * - `;` on Windows + * - `:` on POSIX + * + * @since 2.0.0 + */ +declare function delimiter(): string; +/** + * Resolves a sequence of `paths` or `path` segments into an absolute path. + * @example + * ```typescript + * import { resolve, appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * const path = await resolve(appDataDirPath, '..', 'users', 'tauri', 'avatar.png'); + * ``` + * + * @since 1.0.0 + */ +declare function resolve(...paths: string[]): Promise; +/** + * Normalizes the given `path`, resolving `'..'` and `'.'` segments and resolve symbolic links. + * @example + * ```typescript + * import { normalize, appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * const path = await normalize(`${appDataDirPath}/../users/tauri/avatar.png`); + * ``` + * + * @since 1.0.0 + */ +declare function normalize(path: string): Promise; +/** + * Joins all given `path` segments together using the platform-specific separator as a delimiter, then normalizes the resulting path. + * @example + * ```typescript + * import { join, appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * const path = await join(appDataDirPath, 'users', 'tauri', 'avatar.png'); + * ``` + * + * @since 1.0.0 + */ +declare function join(...paths: string[]): Promise; +/** + * Returns the parent directory of a given `path`. Trailing directory separators are ignored. + * @example + * ```typescript + * import { dirname } from '@tauri-apps/api/path'; + * const dir = await dirname('/path/to/somedir/'); + * assert(dir === '/path/to'); + * ``` + * + * @since 1.0.0 + */ +declare function dirname(path: string): Promise; +/** + * Returns the extension of the `path`. + * @example + * ```typescript + * import { extname } from '@tauri-apps/api/path'; + * const ext = await extname('/path/to/file.html'); + * assert(ext === 'html'); + * ``` + * + * @since 1.0.0 + */ +declare function extname(path: string): Promise; +/** + * Returns the last portion of a `path`. Trailing directory separators are ignored. + * @example + * ```typescript + * import { basename } from '@tauri-apps/api/path'; + * const base = await basename('path/to/app.conf'); + * assert(base === 'app.conf'); + * ``` + * @param ext An optional file extension to be removed from the returned path. + * + * @since 1.0.0 + */ +declare function basename(path: string, ext?: string): Promise; +/** + * Returns whether the path is absolute or not. + * @example + * ```typescript + * import { isAbsolute } from '@tauri-apps/api/path'; + * assert(await isAbsolute('/home/tauri')); + * ``` + * + * @since 1.0.0 + */ +declare function isAbsolute(path: string): Promise; +export { BaseDirectory, appConfigDir, appDataDir, appLocalDataDir, appCacheDir, appLogDir, audioDir, cacheDir, configDir, dataDir, desktopDir, documentDir, downloadDir, executableDir, fontDir, homeDir, localDataDir, pictureDir, publicDir, resourceDir, resolveResource, runtimeDir, templateDir, videoDir, sep, delimiter, resolve, normalize, join, dirname, extname, basename, isAbsolute, tempDir }; diff --git a/node_modules/@tauri-apps/api/path.js b/node_modules/@tauri-apps/api/path.js new file mode 100644 index 0000000..76d0ae6 --- /dev/null +++ b/node_modules/@tauri-apps/api/path.js @@ -0,0 +1,719 @@ +import { invoke } from './core.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * The path module provides utilities for working with file and directory paths. + * + * This package is also accessible with `window.__TAURI__.path` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * It is recommended to allowlist only the APIs you use for optimal bundle size and security. + * @module + */ +/** + * @since 2.0.0 + */ +var BaseDirectory; +(function (BaseDirectory) { + /** + * @see {@link audioDir} for more information. + */ + BaseDirectory[BaseDirectory["Audio"] = 1] = "Audio"; + /** + * @see {@link cacheDir} for more information. + */ + BaseDirectory[BaseDirectory["Cache"] = 2] = "Cache"; + /** + * @see {@link configDir} for more information. + */ + BaseDirectory[BaseDirectory["Config"] = 3] = "Config"; + /** + * @see {@link dataDir} for more information. + */ + BaseDirectory[BaseDirectory["Data"] = 4] = "Data"; + /** + * @see {@link localDataDir} for more information. + */ + BaseDirectory[BaseDirectory["LocalData"] = 5] = "LocalData"; + /** + * @see {@link documentDir} for more information. + */ + BaseDirectory[BaseDirectory["Document"] = 6] = "Document"; + /** + * @see {@link downloadDir} for more information. + */ + BaseDirectory[BaseDirectory["Download"] = 7] = "Download"; + /** + * @see {@link pictureDir} for more information. + */ + BaseDirectory[BaseDirectory["Picture"] = 8] = "Picture"; + /** + * @see {@link publicDir} for more information. + */ + BaseDirectory[BaseDirectory["Public"] = 9] = "Public"; + /** + * @see {@link videoDir} for more information. + */ + BaseDirectory[BaseDirectory["Video"] = 10] = "Video"; + /** + * @see {@link resourceDir} for more information. + */ + BaseDirectory[BaseDirectory["Resource"] = 11] = "Resource"; + /** + * @see {@link tempDir} for more information. + */ + BaseDirectory[BaseDirectory["Temp"] = 12] = "Temp"; + /** + * @see {@link appConfigDir} for more information. + */ + BaseDirectory[BaseDirectory["AppConfig"] = 13] = "AppConfig"; + /** + * @see {@link appDataDir} for more information. + */ + BaseDirectory[BaseDirectory["AppData"] = 14] = "AppData"; + /** + * @see {@link appLocalDataDir} for more information. + */ + BaseDirectory[BaseDirectory["AppLocalData"] = 15] = "AppLocalData"; + /** + * @see {@link appCacheDir} for more information. + */ + BaseDirectory[BaseDirectory["AppCache"] = 16] = "AppCache"; + /** + * @see {@link appLogDir} for more information. + */ + BaseDirectory[BaseDirectory["AppLog"] = 17] = "AppLog"; + /** + * @see {@link desktopDir} for more information. + */ + BaseDirectory[BaseDirectory["Desktop"] = 18] = "Desktop"; + /** + * @see {@link executableDir} for more information. + */ + BaseDirectory[BaseDirectory["Executable"] = 19] = "Executable"; + /** + * @see {@link fontDir} for more information. + */ + BaseDirectory[BaseDirectory["Font"] = 20] = "Font"; + /** + * @see {@link homeDir} for more information. + */ + BaseDirectory[BaseDirectory["Home"] = 21] = "Home"; + /** + * @see {@link runtimeDir} for more information. + */ + BaseDirectory[BaseDirectory["Runtime"] = 22] = "Runtime"; + /** + * @see {@link templateDir} for more information. + */ + BaseDirectory[BaseDirectory["Template"] = 23] = "Template"; +})(BaseDirectory || (BaseDirectory = {})); +/** + * Returns the path to the suggested directory for your app's config files. + * Resolves to `${configDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appConfigDir } from '@tauri-apps/api/path'; + * const appConfigDirPath = await appConfigDir(); + * ``` + * + * @since 1.2.0 + */ +async function appConfigDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.AppConfig + }); +} +/** + * Returns the path to the suggested directory for your app's data files. + * Resolves to `${dataDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * ``` + * + * @since 1.2.0 + */ +async function appDataDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.AppData + }); +} +/** + * Returns the path to the suggested directory for your app's local data files. + * Resolves to `${localDataDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appLocalDataDir } from '@tauri-apps/api/path'; + * const appLocalDataDirPath = await appLocalDataDir(); + * ``` + * + * @since 1.2.0 + */ +async function appLocalDataDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.AppLocalData + }); +} +/** + * Returns the path to the suggested directory for your app's cache files. + * Resolves to `${cacheDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appCacheDir } from '@tauri-apps/api/path'; + * const appCacheDirPath = await appCacheDir(); + * ``` + * + * @since 1.2.0 + */ +async function appCacheDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.AppCache + }); +} +/** + * Returns the path to the user's audio directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_MUSIC_DIR`. + * - **macOS:** Resolves to `$HOME/Music`. + * - **Windows:** Resolves to `{FOLDERID_Music}`. + * @example + * ```typescript + * import { audioDir } from '@tauri-apps/api/path'; + * const audioDirPath = await audioDir(); + * ``` + * + * @since 1.0.0 + */ +async function audioDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Audio + }); +} +/** + * Returns the path to the user's cache directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_CACHE_HOME` or `$HOME/.cache`. + * - **macOS:** Resolves to `$HOME/Library/Caches`. + * - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + * @example + * ```typescript + * import { cacheDir } from '@tauri-apps/api/path'; + * const cacheDirPath = await cacheDir(); + * ``` + * + * @since 1.0.0 + */ +async function cacheDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Cache + }); +} +/** + * Returns the path to the user's config directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_CONFIG_HOME` or `$HOME/.config`. + * - **macOS:** Resolves to `$HOME/Library/Application Support`. + * - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + * @example + * ```typescript + * import { configDir } from '@tauri-apps/api/path'; + * const configDirPath = await configDir(); + * ``` + * + * @since 1.0.0 + */ +async function configDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Config + }); +} +/** + * Returns the path to the user's data directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + * - **macOS:** Resolves to `$HOME/Library/Application Support`. + * - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + * @example + * ```typescript + * import { dataDir } from '@tauri-apps/api/path'; + * const dataDirPath = await dataDir(); + * ``` + * + * @since 1.0.0 + */ +async function dataDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Data + }); +} +/** + * Returns the path to the user's desktop directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DESKTOP_DIR`. + * - **macOS:** Resolves to `$HOME/Desktop`. + * - **Windows:** Resolves to `{FOLDERID_Desktop}`. + * @example + * ```typescript + * import { desktopDir } from '@tauri-apps/api/path'; + * const desktopPath = await desktopDir(); + * ``` + * + * @since 1.0.0 + */ +async function desktopDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Desktop + }); +} +/** + * Returns the path to the user's document directory. + * @example + * ```typescript + * import { documentDir } from '@tauri-apps/api/path'; + * const documentDirPath = await documentDir(); + * ``` + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOCUMENTS_DIR`. + * - **macOS:** Resolves to `$HOME/Documents`. + * - **Windows:** Resolves to `{FOLDERID_Documents}`. + * + * @since 1.0.0 + */ +async function documentDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Document + }); +} +/** + * Returns the path to the user's download directory. + * + * #### Platform-specific + * + * - **Linux**: Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOWNLOAD_DIR`. + * - **macOS**: Resolves to `$HOME/Downloads`. + * - **Windows**: Resolves to `{FOLDERID_Downloads}`. + * @example + * ```typescript + * import { downloadDir } from '@tauri-apps/api/path'; + * const downloadDirPath = await downloadDir(); + * ``` + * + * @since 1.0.0 + */ +async function downloadDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Download + }); +} +/** + * Returns the path to the user's executable directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_BIN_HOME/../bin` or `$XDG_DATA_HOME/../bin` or `$HOME/.local/bin`. + * - **macOS:** Not supported. + * - **Windows:** Not supported. + * @example + * ```typescript + * import { executableDir } from '@tauri-apps/api/path'; + * const executableDirPath = await executableDir(); + * ``` + * + * @since 1.0.0 + */ +async function executableDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Executable + }); +} +/** + * Returns the path to the user's font directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_DATA_HOME/fonts` or `$HOME/.local/share/fonts`. + * - **macOS:** Resolves to `$HOME/Library/Fonts`. + * - **Windows:** Not supported. + * @example + * ```typescript + * import { fontDir } from '@tauri-apps/api/path'; + * const fontDirPath = await fontDir(); + * ``` + * + * @since 1.0.0 + */ +async function fontDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Font + }); +} +/** + * Returns the path to the user's home directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$HOME`. + * - **macOS:** Resolves to `$HOME`. + * - **Windows:** Resolves to `{FOLDERID_Profile}`. + * @example + * ```typescript + * import { homeDir } from '@tauri-apps/api/path'; + * const homeDirPath = await homeDir(); + * ``` + * + * @since 1.0.0 + */ +async function homeDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Home + }); +} +/** + * Returns the path to the user's local data directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + * - **macOS:** Resolves to `$HOME/Library/Application Support`. + * - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + * @example + * ```typescript + * import { localDataDir } from '@tauri-apps/api/path'; + * const localDataDirPath = await localDataDir(); + * ``` + * + * @since 1.0.0 + */ +async function localDataDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.LocalData + }); +} +/** + * Returns the path to the user's picture directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PICTURES_DIR`. + * - **macOS:** Resolves to `$HOME/Pictures`. + * - **Windows:** Resolves to `{FOLDERID_Pictures}`. + * @example + * ```typescript + * import { pictureDir } from '@tauri-apps/api/path'; + * const pictureDirPath = await pictureDir(); + * ``` + * + * @since 1.0.0 + */ +async function pictureDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Picture + }); +} +/** + * Returns the path to the user's public directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PUBLICSHARE_DIR`. + * - **macOS:** Resolves to `$HOME/Public`. + * - **Windows:** Resolves to `{FOLDERID_Public}`. + * @example + * ```typescript + * import { publicDir } from '@tauri-apps/api/path'; + * const publicDirPath = await publicDir(); + * ``` + * + * @since 1.0.0 + */ +async function publicDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Public + }); +} +/** + * Returns the path to the application's resource directory. + * To resolve a resource path, see {@linkcode resolveResource}. + * + * ## Platform-specific + * + * Although we provide the exact path where this function resolves to, + * this is not a contract and things might change in the future + * + * - **Windows:** Resolves to the directory that contains the main executable. + * - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to + * the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`. + * If not running in an AppImage, the path is `/usr/lib/${exe_name}`. + * When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`. + * - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app). + * - **iOS:** Resolves to `${exe_dir}/assets`. + * - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path, + * we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/), + * + * @example + * ```typescript + * import { resourceDir } from '@tauri-apps/api/path'; + * const resourceDirPath = await resourceDir(); + * ``` + * + * @since 1.0.0 + */ +async function resourceDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Resource + }); +} +/** + * Resolve the path to a resource file. + * @example + * ```typescript + * import { resolveResource } from '@tauri-apps/api/path'; + * const resourcePath = await resolveResource('script.sh'); + * ``` + * + * @param resourcePath The path to the resource. + * Must follow the same syntax as defined in `tauri.conf.json > bundle > resources`, i.e. keeping subfolders and parent dir components (`../`). + * @returns The full path to the resource. + * + * @since 1.0.0 + */ +async function resolveResource(resourcePath) { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Resource, + path: resourcePath + }); +} +/** + * Returns the path to the user's runtime directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_RUNTIME_DIR`. + * - **macOS:** Not supported. + * - **Windows:** Not supported. + * @example + * ```typescript + * import { runtimeDir } from '@tauri-apps/api/path'; + * const runtimeDirPath = await runtimeDir(); + * ``` + * + * @since 1.0.0 + */ +async function runtimeDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Runtime + }); +} +/** + * Returns the path to the user's template directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_TEMPLATES_DIR`. + * - **macOS:** Not supported. + * - **Windows:** Resolves to `{FOLDERID_Templates}`. + * @example + * ```typescript + * import { templateDir } from '@tauri-apps/api/path'; + * const templateDirPath = await templateDir(); + * ``` + * + * @since 1.0.0 + */ +async function templateDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Template + }); +} +/** + * Returns the path to the user's video directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_VIDEOS_DIR`. + * - **macOS:** Resolves to `$HOME/Movies`. + * - **Windows:** Resolves to `{FOLDERID_Videos}`. + * @example + * ```typescript + * import { videoDir } from '@tauri-apps/api/path'; + * const videoDirPath = await videoDir(); + * ``` + * + * @since 1.0.0 + */ +async function videoDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Video + }); +} +/** + * Returns the path to the suggested directory for your app's log files. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `${configDir}/${bundleIdentifier}/logs`. + * - **macOS:** Resolves to `${homeDir}/Library/Logs/{bundleIdentifier}` + * - **Windows:** Resolves to `${configDir}/${bundleIdentifier}/logs`. + * @example + * ```typescript + * import { appLogDir } from '@tauri-apps/api/path'; + * const appLogDirPath = await appLogDir(); + * ``` + * + * @since 1.2.0 + */ +async function appLogDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.AppLog + }); +} +/** + * Returns a temporary directory. + * @example + * ```typescript + * import { tempDir } from '@tauri-apps/api/path'; + * const temp = await tempDir(); + * ``` + * + * @since 2.0.0 + */ +async function tempDir() { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Temp + }); +} +/** + * Returns the platform-specific path segment separator: + * - `\` on Windows + * - `/` on POSIX + * + * @since 2.0.0 + */ +function sep() { + return window.__TAURI_INTERNALS__.plugins.path.sep; +} +/** + * Returns the platform-specific path segment delimiter: + * - `;` on Windows + * - `:` on POSIX + * + * @since 2.0.0 + */ +function delimiter() { + return window.__TAURI_INTERNALS__.plugins.path.delimiter; +} +/** + * Resolves a sequence of `paths` or `path` segments into an absolute path. + * @example + * ```typescript + * import { resolve, appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * const path = await resolve(appDataDirPath, '..', 'users', 'tauri', 'avatar.png'); + * ``` + * + * @since 1.0.0 + */ +async function resolve(...paths) { + return invoke('plugin:path|resolve', { paths }); +} +/** + * Normalizes the given `path`, resolving `'..'` and `'.'` segments and resolve symbolic links. + * @example + * ```typescript + * import { normalize, appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * const path = await normalize(`${appDataDirPath}/../users/tauri/avatar.png`); + * ``` + * + * @since 1.0.0 + */ +async function normalize(path) { + return invoke('plugin:path|normalize', { path }); +} +/** + * Joins all given `path` segments together using the platform-specific separator as a delimiter, then normalizes the resulting path. + * @example + * ```typescript + * import { join, appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * const path = await join(appDataDirPath, 'users', 'tauri', 'avatar.png'); + * ``` + * + * @since 1.0.0 + */ +async function join(...paths) { + return invoke('plugin:path|join', { paths }); +} +/** + * Returns the parent directory of a given `path`. Trailing directory separators are ignored. + * @example + * ```typescript + * import { dirname } from '@tauri-apps/api/path'; + * const dir = await dirname('/path/to/somedir/'); + * assert(dir === '/path/to'); + * ``` + * + * @since 1.0.0 + */ +async function dirname(path) { + return invoke('plugin:path|dirname', { path }); +} +/** + * Returns the extension of the `path`. + * @example + * ```typescript + * import { extname } from '@tauri-apps/api/path'; + * const ext = await extname('/path/to/file.html'); + * assert(ext === 'html'); + * ``` + * + * @since 1.0.0 + */ +async function extname(path) { + return invoke('plugin:path|extname', { path }); +} +/** + * Returns the last portion of a `path`. Trailing directory separators are ignored. + * @example + * ```typescript + * import { basename } from '@tauri-apps/api/path'; + * const base = await basename('path/to/app.conf'); + * assert(base === 'app.conf'); + * ``` + * @param ext An optional file extension to be removed from the returned path. + * + * @since 1.0.0 + */ +async function basename(path, ext) { + return invoke('plugin:path|basename', { path, ext }); +} +/** + * Returns whether the path is absolute or not. + * @example + * ```typescript + * import { isAbsolute } from '@tauri-apps/api/path'; + * assert(await isAbsolute('/home/tauri')); + * ``` + * + * @since 1.0.0 + */ +async function isAbsolute(path) { + return invoke('plugin:path|is_absolute', { path }); +} + +export { BaseDirectory, appCacheDir, appConfigDir, appDataDir, appLocalDataDir, appLogDir, audioDir, basename, cacheDir, configDir, dataDir, delimiter, desktopDir, dirname, documentDir, downloadDir, executableDir, extname, fontDir, homeDir, isAbsolute, join, localDataDir, normalize, pictureDir, publicDir, resolve, resolveResource, resourceDir, runtimeDir, sep, tempDir, templateDir, videoDir }; diff --git a/node_modules/@tauri-apps/api/tray.cjs b/node_modules/@tauri-apps/api/tray.cjs new file mode 100644 index 0000000..e6d8817 --- /dev/null +++ b/node_modules/@tauri-apps/api/tray.cjs @@ -0,0 +1,188 @@ +'use strict'; + +var core = require('./core.cjs'); +var image = require('./image.cjs'); +var dpi = require('./dpi.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * Tray icon class and associated methods. This type constructor is private, + * instead, you should use the static method {@linkcode TrayIcon.new}. + * + * #### Warning + * + * Unlike Rust, javascript does not have any way to run cleanup code + * when an object is being removed by garbage collection, but this tray icon + * will be cleaned up when the tauri app exists, however if you want to cleanup + * this object early, you need to call {@linkcode TrayIcon.close}. + * + * @example + * ```ts + * import { TrayIcon } from '@tauri-apps/api/tray'; + * const tray = await TrayIcon.new({ tooltip: 'awesome tray tooltip' }); + * tray.set_tooltip('new tooltip'); + * ``` + */ +class TrayIcon extends core.Resource { + constructor(rid, id) { + super(rid); + this.id = id; + } + /** Gets a tray icon using the provided id. */ + static async getById(id) { + return core.invoke('plugin:tray|get_by_id', { id }).then((rid) => rid ? new TrayIcon(rid, id) : null); + } + /** + * Removes a tray icon using the provided id from tauri's internal state. + * + * Note that this may cause the tray icon to disappear + * if it wasn't cloned somewhere else or referenced by JS. + */ + static async removeById(id) { + return core.invoke('plugin:tray|remove_by_id', { id }); + } + /** + * Creates a new {@linkcode TrayIcon} + * + * #### Platform-specific: + * + * - **Linux:** Sometimes the icon won't be visible unless a menu is set. + * Setting an empty {@linkcode Menu} is enough. + */ + static async new(options) { + if (options === null || options === void 0 ? void 0 : options.menu) { + // @ts-expect-error we only need the rid and kind + options.menu = [options.menu.rid, options.menu.kind]; + } + if (options === null || options === void 0 ? void 0 : options.icon) { + options.icon = image.transformImage(options.icon); + } + const handler = new core.Channel(); + if (options === null || options === void 0 ? void 0 : options.action) { + const action = options.action; + handler.onmessage = (e) => action(mapEvent(e)); + delete options.action; + } + return core.invoke('plugin:tray|new', { + options: options !== null && options !== void 0 ? options : {}, + handler + }).then(([rid, id]) => new TrayIcon(rid, id)); + } + /** + * Sets a new tray icon. If `null` is provided, it will remove the icon. + * + * Note that you may need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + */ + async setIcon(icon) { + let trayIcon = null; + if (icon) { + trayIcon = image.transformImage(icon); + } + return core.invoke('plugin:tray|set_icon', { rid: this.rid, icon: trayIcon }); + } + /** + * Sets a new tray menu. + * + * #### Platform-specific: + * + * - **Linux**: once a menu is set it cannot be removed so `null` has no effect + */ + async setMenu(menu) { + if (menu) { + // @ts-expect-error we only need the rid and kind + menu = [menu.rid, menu.kind]; + } + return core.invoke('plugin:tray|set_menu', { rid: this.rid, menu }); + } + /** + * Sets the tooltip for this tray icon. + * + * #### Platform-specific: + * + * - **Linux:** Unsupported + */ + async setTooltip(tooltip) { + return core.invoke('plugin:tray|set_tooltip', { rid: this.rid, tooltip }); + } + /** + * Sets the tooltip for this tray icon. + * + * #### Platform-specific: + * + * - **Linux:** The title will not be shown unless there is an icon + * as well. The title is useful for numerical and other frequently + * updated information. In general, it shouldn't be shown unless a + * user requests it as it can take up a significant amount of space + * on the user's panel. This may not be shown in all visualizations. + * - **Windows:** Unsupported + */ + async setTitle(title) { + return core.invoke('plugin:tray|set_title', { rid: this.rid, title }); + } + /** Show or hide this tray icon. */ + async setVisible(visible) { + return core.invoke('plugin:tray|set_visible', { rid: this.rid, visible }); + } + /** + * Sets the tray icon temp dir path. **Linux only**. + * + * On Linux, we need to write the icon to the disk and usually it will + * be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + */ + async setTempDirPath(path) { + return core.invoke('plugin:tray|set_temp_dir_path', { rid: this.rid, path }); + } + /** Sets the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only** */ + async setIconAsTemplate(asTemplate) { + return core.invoke('plugin:tray|set_icon_as_template', { + rid: this.rid, + asTemplate + }); + } + /** + * Disable or enable showing the tray menu on left click. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. + * + * @deprecated use {@linkcode TrayIcon.setShowMenuOnLeftClick} instead. + */ + async setMenuOnLeftClick(onLeft) { + return core.invoke('plugin:tray|set_show_menu_on_left_click', { + rid: this.rid, + onLeft + }); + } + /** + * Disable or enable showing the tray menu on left click. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. + * + * @since 2.2.0 + */ + async setShowMenuOnLeftClick(onLeft) { + return core.invoke('plugin:tray|set_show_menu_on_left_click', { + rid: this.rid, + onLeft + }); + } +} +function mapEvent(e) { + const out = e; + out.position = new dpi.PhysicalPosition(e.position); + out.rect.position = new dpi.PhysicalPosition(e.rect.position); + out.rect.size = new dpi.PhysicalSize(e.rect.size); + return out; +} + +exports.TrayIcon = TrayIcon; diff --git a/node_modules/@tauri-apps/api/tray.d.ts b/node_modules/@tauri-apps/api/tray.d.ts new file mode 100644 index 0000000..d3ee8ed --- /dev/null +++ b/node_modules/@tauri-apps/api/tray.d.ts @@ -0,0 +1,219 @@ +import type { Menu, Submenu } from './menu'; +import { Resource } from './core'; +import { Image } from './image'; +import { PhysicalPosition, PhysicalSize } from './dpi'; +export type MouseButtonState = 'Up' | 'Down'; +export type MouseButton = 'Left' | 'Right' | 'Middle'; +export type TrayIconEventType = 'Click' | 'DoubleClick' | 'Enter' | 'Move' | 'Leave'; +export type TrayIconEventBase = { + /** The tray icon event type */ + type: T; + /** Id of the tray icon which triggered this event. */ + id: string; + /** Physical position of the click the triggered this event. */ + position: PhysicalPosition; + /** Position and size of the tray icon. */ + rect: { + position: PhysicalPosition; + size: PhysicalSize; + }; +}; +export type TrayIconClickEvent = { + /** Mouse button that triggered this event. */ + button: MouseButton; + /** Mouse button state when this event was triggered. */ + buttonState: MouseButtonState; +}; +/** + * Describes a tray icon event. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. The event is not emitted even though the icon is shown, + * the icon will still show a context menu on right click. + */ +export type TrayIconEvent = (TrayIconEventBase<'Click'> & TrayIconClickEvent) | (TrayIconEventBase<'DoubleClick'> & Omit) | TrayIconEventBase<'Enter'> | TrayIconEventBase<'Move'> | TrayIconEventBase<'Leave'>; +/** + * Tray icon types and utilities. + * + * This package is also accessible with `window.__TAURI__.tray` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * @module + */ +/** {@link TrayIcon.new|`TrayIcon`} creation options */ +export interface TrayIconOptions { + /** The tray icon id. If undefined, a random one will be assigned */ + id?: string; + /** The tray icon menu */ + menu?: Menu | Submenu; + /** + * The tray icon which could be icon bytes or path to the icon file. + * + * Note that you may need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + */ + icon?: string | Uint8Array | ArrayBuffer | number[] | Image; + /** The tray icon tooltip */ + tooltip?: string; + /** + * The tray title + * + * #### Platform-specific + * + * - **Linux:** The title will not be shown unless there is an icon + * as well. The title is useful for numerical and other frequently + * updated information. In general, it shouldn't be shown unless a + * user requests it as it can take up a significant amount of space + * on the user's panel. This may not be shown in all visualizations. + * - **Windows:** Unsupported. + */ + title?: string; + /** + * The tray icon temp dir path. **Linux only**. + * + * On Linux, we need to write the icon to the disk and usually it will + * be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + */ + tempDirPath?: string; + /** + * Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. + */ + iconAsTemplate?: boolean; + /** + * Whether to show the tray menu on left click or not, default is `true`. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. + * + * @deprecated use {@linkcode TrayIconOptions.showMenuOnLeftClick} instead. + */ + menuOnLeftClick?: boolean; + /** + * Whether to show the tray menu on left click or not, default is `true`. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. + * + * @since 2.2.0 + */ + showMenuOnLeftClick?: boolean; + /** A handler for an event on the tray icon. */ + action?: (event: TrayIconEvent) => void; +} +/** + * Tray icon class and associated methods. This type constructor is private, + * instead, you should use the static method {@linkcode TrayIcon.new}. + * + * #### Warning + * + * Unlike Rust, javascript does not have any way to run cleanup code + * when an object is being removed by garbage collection, but this tray icon + * will be cleaned up when the tauri app exists, however if you want to cleanup + * this object early, you need to call {@linkcode TrayIcon.close}. + * + * @example + * ```ts + * import { TrayIcon } from '@tauri-apps/api/tray'; + * const tray = await TrayIcon.new({ tooltip: 'awesome tray tooltip' }); + * tray.set_tooltip('new tooltip'); + * ``` + */ +export declare class TrayIcon extends Resource { + /** The id associated with this tray icon. */ + id: string; + private constructor(); + /** Gets a tray icon using the provided id. */ + static getById(id: string): Promise; + /** + * Removes a tray icon using the provided id from tauri's internal state. + * + * Note that this may cause the tray icon to disappear + * if it wasn't cloned somewhere else or referenced by JS. + */ + static removeById(id: string): Promise; + /** + * Creates a new {@linkcode TrayIcon} + * + * #### Platform-specific: + * + * - **Linux:** Sometimes the icon won't be visible unless a menu is set. + * Setting an empty {@linkcode Menu} is enough. + */ + static new(options?: TrayIconOptions): Promise; + /** + * Sets a new tray icon. If `null` is provided, it will remove the icon. + * + * Note that you may need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + */ + setIcon(icon: string | Image | Uint8Array | ArrayBuffer | number[] | null): Promise; + /** + * Sets a new tray menu. + * + * #### Platform-specific: + * + * - **Linux**: once a menu is set it cannot be removed so `null` has no effect + */ + setMenu(menu: Menu | Submenu | null): Promise; + /** + * Sets the tooltip for this tray icon. + * + * #### Platform-specific: + * + * - **Linux:** Unsupported + */ + setTooltip(tooltip: string | null): Promise; + /** + * Sets the tooltip for this tray icon. + * + * #### Platform-specific: + * + * - **Linux:** The title will not be shown unless there is an icon + * as well. The title is useful for numerical and other frequently + * updated information. In general, it shouldn't be shown unless a + * user requests it as it can take up a significant amount of space + * on the user's panel. This may not be shown in all visualizations. + * - **Windows:** Unsupported + */ + setTitle(title: string | null): Promise; + /** Show or hide this tray icon. */ + setVisible(visible: boolean): Promise; + /** + * Sets the tray icon temp dir path. **Linux only**. + * + * On Linux, we need to write the icon to the disk and usually it will + * be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + */ + setTempDirPath(path: string | null): Promise; + /** Sets the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only** */ + setIconAsTemplate(asTemplate: boolean): Promise; + /** + * Disable or enable showing the tray menu on left click. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. + * + * @deprecated use {@linkcode TrayIcon.setShowMenuOnLeftClick} instead. + */ + setMenuOnLeftClick(onLeft: boolean): Promise; + /** + * Disable or enable showing the tray menu on left click. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. + * + * @since 2.2.0 + */ + setShowMenuOnLeftClick(onLeft: boolean): Promise; +} diff --git a/node_modules/@tauri-apps/api/tray.js b/node_modules/@tauri-apps/api/tray.js new file mode 100644 index 0000000..dceda43 --- /dev/null +++ b/node_modules/@tauri-apps/api/tray.js @@ -0,0 +1,186 @@ +import { Resource, invoke, Channel } from './core.js'; +import { transformImage } from './image.js'; +import { PhysicalPosition, PhysicalSize } from './dpi.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * Tray icon class and associated methods. This type constructor is private, + * instead, you should use the static method {@linkcode TrayIcon.new}. + * + * #### Warning + * + * Unlike Rust, javascript does not have any way to run cleanup code + * when an object is being removed by garbage collection, but this tray icon + * will be cleaned up when the tauri app exists, however if you want to cleanup + * this object early, you need to call {@linkcode TrayIcon.close}. + * + * @example + * ```ts + * import { TrayIcon } from '@tauri-apps/api/tray'; + * const tray = await TrayIcon.new({ tooltip: 'awesome tray tooltip' }); + * tray.set_tooltip('new tooltip'); + * ``` + */ +class TrayIcon extends Resource { + constructor(rid, id) { + super(rid); + this.id = id; + } + /** Gets a tray icon using the provided id. */ + static async getById(id) { + return invoke('plugin:tray|get_by_id', { id }).then((rid) => rid ? new TrayIcon(rid, id) : null); + } + /** + * Removes a tray icon using the provided id from tauri's internal state. + * + * Note that this may cause the tray icon to disappear + * if it wasn't cloned somewhere else or referenced by JS. + */ + static async removeById(id) { + return invoke('plugin:tray|remove_by_id', { id }); + } + /** + * Creates a new {@linkcode TrayIcon} + * + * #### Platform-specific: + * + * - **Linux:** Sometimes the icon won't be visible unless a menu is set. + * Setting an empty {@linkcode Menu} is enough. + */ + static async new(options) { + if (options === null || options === void 0 ? void 0 : options.menu) { + // @ts-expect-error we only need the rid and kind + options.menu = [options.menu.rid, options.menu.kind]; + } + if (options === null || options === void 0 ? void 0 : options.icon) { + options.icon = transformImage(options.icon); + } + const handler = new Channel(); + if (options === null || options === void 0 ? void 0 : options.action) { + const action = options.action; + handler.onmessage = (e) => action(mapEvent(e)); + delete options.action; + } + return invoke('plugin:tray|new', { + options: options !== null && options !== void 0 ? options : {}, + handler + }).then(([rid, id]) => new TrayIcon(rid, id)); + } + /** + * Sets a new tray icon. If `null` is provided, it will remove the icon. + * + * Note that you may need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + */ + async setIcon(icon) { + let trayIcon = null; + if (icon) { + trayIcon = transformImage(icon); + } + return invoke('plugin:tray|set_icon', { rid: this.rid, icon: trayIcon }); + } + /** + * Sets a new tray menu. + * + * #### Platform-specific: + * + * - **Linux**: once a menu is set it cannot be removed so `null` has no effect + */ + async setMenu(menu) { + if (menu) { + // @ts-expect-error we only need the rid and kind + menu = [menu.rid, menu.kind]; + } + return invoke('plugin:tray|set_menu', { rid: this.rid, menu }); + } + /** + * Sets the tooltip for this tray icon. + * + * #### Platform-specific: + * + * - **Linux:** Unsupported + */ + async setTooltip(tooltip) { + return invoke('plugin:tray|set_tooltip', { rid: this.rid, tooltip }); + } + /** + * Sets the tooltip for this tray icon. + * + * #### Platform-specific: + * + * - **Linux:** The title will not be shown unless there is an icon + * as well. The title is useful for numerical and other frequently + * updated information. In general, it shouldn't be shown unless a + * user requests it as it can take up a significant amount of space + * on the user's panel. This may not be shown in all visualizations. + * - **Windows:** Unsupported + */ + async setTitle(title) { + return invoke('plugin:tray|set_title', { rid: this.rid, title }); + } + /** Show or hide this tray icon. */ + async setVisible(visible) { + return invoke('plugin:tray|set_visible', { rid: this.rid, visible }); + } + /** + * Sets the tray icon temp dir path. **Linux only**. + * + * On Linux, we need to write the icon to the disk and usually it will + * be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + */ + async setTempDirPath(path) { + return invoke('plugin:tray|set_temp_dir_path', { rid: this.rid, path }); + } + /** Sets the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only** */ + async setIconAsTemplate(asTemplate) { + return invoke('plugin:tray|set_icon_as_template', { + rid: this.rid, + asTemplate + }); + } + /** + * Disable or enable showing the tray menu on left click. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. + * + * @deprecated use {@linkcode TrayIcon.setShowMenuOnLeftClick} instead. + */ + async setMenuOnLeftClick(onLeft) { + return invoke('plugin:tray|set_show_menu_on_left_click', { + rid: this.rid, + onLeft + }); + } + /** + * Disable or enable showing the tray menu on left click. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. + * + * @since 2.2.0 + */ + async setShowMenuOnLeftClick(onLeft) { + return invoke('plugin:tray|set_show_menu_on_left_click', { + rid: this.rid, + onLeft + }); + } +} +function mapEvent(e) { + const out = e; + out.position = new PhysicalPosition(e.position); + out.rect.position = new PhysicalPosition(e.rect.position); + out.rect.size = new PhysicalSize(e.rect.size); + return out; +} + +export { TrayIcon }; diff --git a/node_modules/@tauri-apps/api/webview.cjs b/node_modules/@tauri-apps/api/webview.cjs new file mode 100644 index 0000000..6feee33 --- /dev/null +++ b/node_modules/@tauri-apps/api/webview.cjs @@ -0,0 +1,594 @@ +'use strict'; + +var dpi = require('./dpi.cjs'); +var event = require('./event.cjs'); +var core = require('./core.cjs'); +var window$1 = require('./window.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * Provides APIs to create webviews, communicate with other webviews and manipulate the current webview. + * + * #### Webview events + * + * Events can be listened to using {@link Webview.listen}: + * ```typescript + * import { getCurrentWebview } from "@tauri-apps/api/webview"; + * getCurrentWebview().listen("my-webview-event", ({ event, payload }) => { }); + * ``` + * + * @module + */ +/** + * Get an instance of `Webview` for the current webview. + * + * @since 2.0.0 + */ +function getCurrentWebview() { + return new Webview(window$1.getCurrentWindow(), window.__TAURI_INTERNALS__.metadata.currentWebview.label, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }); +} +/** + * Gets a list of instances of `Webview` for all available webviews. + * + * @since 2.0.0 + */ +async function getAllWebviews() { + return core.invoke('plugin:webview|get_all_webviews').then((webviews) => webviews.map((w) => new Webview(new window$1.Window(w.windowLabel, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }), w.label, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }))); +} +/** @ignore */ +// events that are emitted right here instead of by the created webview +const localTauriEvents = ['tauri://created', 'tauri://error']; +/** + * Create new webview or get a handle to an existing one. + * + * Webviews are identified by a *label* a unique identifier that can be used to reference it later. + * It may only contain alphanumeric characters `a-zA-Z` plus the following special characters `-`, `/`, `:` and `_`. + * + * @example + * ```typescript + * import { Window } from "@tauri-apps/api/window" + * import { Webview } from "@tauri-apps/api/webview" + * + * const appWindow = new Window('uniqueLabel'); + * + * appWindow.once('tauri://created', async function () { + * // `new Webview` Should be called after the window is successfully created, + * // or webview may not be attached to the window since window is not created yet. + * + * // loading embedded asset: + * const webview = new Webview(appWindow, 'theUniqueLabel', { + * url: 'path/to/page.html', + * + * // create a webview with specific logical position and size + * x: 0, + * y: 0, + * width: 800, + * height: 600, + * }); + * // alternatively, load a remote URL: + * const webview = new Webview(appWindow, 'theUniqueLabel', { + * url: 'https://github.com/tauri-apps/tauri', + * + * // create a webview with specific logical position and size + * x: 0, + * y: 0, + * width: 800, + * height: 600, + * }); + * + * webview.once('tauri://created', function () { + * // webview successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview + * }); + * + * + * // emit an event to the backend + * await webview.emit("some-event", "data"); + * // listen to an event from the backend + * const unlisten = await webview.listen("event-name", e => { }); + * unlisten(); + * }); + * ``` + * + * @since 2.0.0 + */ +class Webview { + /** + * Creates a new Webview. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window' + * import { Webview } from '@tauri-apps/api/webview' + * const appWindow = new Window('my-label') + * + * appWindow.once('tauri://created', async function() { + * const webview = new Webview(appWindow, 'my-label', { + * url: 'https://github.com/tauri-apps/tauri', + * + * // create a webview with specific logical position and size + * x: 0, + * y: 0, + * width: 800, + * height: 600, + * }); + * + * webview.once('tauri://created', function () { + * // webview successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview + * }); + * }); + * ``` + * + * @param window the window to add this webview to. + * @param label The unique webview label. Must be alphanumeric: `a-zA-Z-/:_`. + * @returns The {@link Webview} instance to communicate with the webview. + */ + constructor(window, label, options) { + this.window = window; + this.label = label; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.listeners = Object.create(null); + // @ts-expect-error `skip` is not a public API so it is not defined in WebviewOptions + if (!(options === null || options === void 0 ? void 0 : options.skip)) { + core.invoke('plugin:webview|create_webview', { + windowLabel: window.label, + options: { + ...options, + label + } + }) + .then(async () => this.emit('tauri://created')) + .catch(async (e) => this.emit('tauri://error', e)); + } + } + /** + * Gets the Webview for the webview associated with the given label. + * @example + * ```typescript + * import { Webview } from '@tauri-apps/api/webview'; + * const mainWebview = Webview.getByLabel('main'); + * ``` + * + * @param label The webview label. + * @returns The Webview instance to communicate with the webview or null if the webview doesn't exist. + */ + static async getByLabel(label) { + var _a; + return (_a = (await getAllWebviews()).find((w) => w.label === label)) !== null && _a !== void 0 ? _a : null; + } + /** + * Get an instance of `Webview` for the current webview. + */ + static getCurrent() { + return getCurrentWebview(); + } + /** + * Gets a list of instances of `Webview` for all available webviews. + */ + static async getAll() { + return getAllWebviews(); + } + /** + * Listen to an emitted event on this webview. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const unlisten = await getCurrentWebview().listen('state-changed', (event) => { + * console.log(`Got error: ${payload}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async listen(event$1, handler) { + if (this._handleTauriEvent(event$1, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event$1]; + listeners.splice(listeners.indexOf(handler), 1); + }; + } + return event.listen(event$1, handler, { + target: { kind: 'Webview', label: this.label } + }); + } + /** + * Listen to an emitted event on this webview only once. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const unlisten = await getCurrent().once('initialized', (event) => { + * console.log(`Webview initialized!`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async once(event$1, handler) { + if (this._handleTauriEvent(event$1, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event$1]; + listeners.splice(listeners.indexOf(handler), 1); + }; + } + return event.once(event$1, handler, { + target: { kind: 'Webview', label: this.label } + }); + } + /** + * Emits an event to all {@link EventTarget|targets}. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().emit('webview-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + async emit(event$1, payload) { + if (localTauriEvents.includes(event$1)) { + // eslint-disable-next-line + for (const handler of this.listeners[event$1] || []) { + handler({ + event: event$1, + id: -1, + payload + }); + } + return; + } + return event.emit(event$1, payload); + } + /** + * Emits an event to all {@link EventTarget|targets} matching the given target. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().emitTo('main', 'webview-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object. + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + async emitTo(target, event$1, payload) { + if (localTauriEvents.includes(event$1)) { + // eslint-disable-next-line + for (const handler of this.listeners[event$1] || []) { + handler({ + event: event$1, + id: -1, + payload + }); + } + return; + } + return event.emitTo(target, event$1, payload); + } + /** @ignore */ + _handleTauriEvent(event, handler) { + if (localTauriEvents.includes(event)) { + if (!(event in this.listeners)) { + // eslint-disable-next-line security/detect-object-injection + this.listeners[event] = [handler]; + } + else { + // eslint-disable-next-line security/detect-object-injection + this.listeners[event].push(handler); + } + return true; + } + return false; + } + // Getters + /** + * The position of the top-left hand corner of the webview's client area relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const position = await getCurrentWebview().position(); + * ``` + * + * @returns The webview's position. + */ + async position() { + return core.invoke('plugin:webview|webview_position', { + label: this.label + }).then((p) => new dpi.PhysicalPosition(p)); + } + /** + * The physical size of the webview's client area. + * The client area is the content of the webview, excluding the title bar and borders. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const size = await getCurrentWebview().size(); + * ``` + * + * @returns The webview's size. + */ + async size() { + return core.invoke('plugin:webview|webview_size', { + label: this.label + }).then((s) => new dpi.PhysicalSize(s)); + } + // Setters + /** + * Closes the webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().close(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async close() { + return core.invoke('plugin:webview|webview_close', { + label: this.label + }); + } + /** + * Resizes the webview. + * @example + * ```typescript + * import { getCurrent, LogicalSize } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical size. + * @returns A promise indicating the success or failure of the operation. + */ + async setSize(size) { + return core.invoke('plugin:webview|set_webview_size', { + label: this.label, + value: size instanceof dpi.Size ? size : new dpi.Size(size) + }); + } + /** + * Sets the webview position. + * @example + * ```typescript + * import { getCurrent, LogicalPosition } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setPosition(new LogicalPosition(600, 500)); + * ``` + * + * @param position The new position, in logical or physical pixels. + * @returns A promise indicating the success or failure of the operation. + */ + async setPosition(position) { + return core.invoke('plugin:webview|set_webview_position', { + label: this.label, + value: position instanceof dpi.Position ? position : new dpi.Position(position) + }); + } + /** + * Bring the webview to front and focus. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setFocus(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setFocus() { + return core.invoke('plugin:webview|set_webview_focus', { + label: this.label + }); + } + /** + * Sets whether the webview should automatically grow and shrink its size and position when the parent window resizes. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setAutoResize(true); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setAutoResize(autoResize) { + return core.invoke('plugin:webview|set_webview_auto_resize', { + label: this.label, + value: autoResize + }); + } + /** + * Hide the webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().hide(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async hide() { + return core.invoke('plugin:webview|webview_hide', { + label: this.label + }); + } + /** + * Show the webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().show(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async show() { + return core.invoke('plugin:webview|webview_show', { + label: this.label + }); + } + /** + * Set webview zoom level. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setZoom(1.5); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setZoom(scaleFactor) { + return core.invoke('plugin:webview|set_webview_zoom', { + label: this.label, + value: scaleFactor + }); + } + /** + * Moves this webview to the given label. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().reparent('other-window'); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async reparent(window) { + return core.invoke('plugin:webview|reparent', { + label: this.label, + window: typeof window === 'string' ? window : window.label + }); + } + /** + * Clears all browsing data for this webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().clearAllBrowsingData(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async clearAllBrowsingData() { + return core.invoke('plugin:webview|clear_all_browsing_data'); + } + /** + * Specify the webview background color. + * + * #### Platfrom-specific: + * + * - **macOS / iOS**: Not implemented. + * - **Windows**: + * - On Windows 7, transparency is not supported and the alpha value will be ignored. + * - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.1.0 + */ + async setBackgroundColor(color) { + return core.invoke('plugin:webview|set_webview_background_color', { color }); + } + // Listeners + /** + * Listen to a file drop event. + * The listener is triggered when the user hovers the selected files on the webview, + * drops the files or cancels the operation. + * + * @example + * ```typescript + * import { getCurrentWebview } from "@tauri-apps/api/webview"; + * const unlisten = await getCurrentWebview().onDragDropEvent((event) => { + * if (event.payload.type === 'over') { + * console.log('User hovering', event.payload.position); + * } else if (event.payload.type === 'drop') { + * console.log('User dropped', event.payload.paths); + * } else { + * console.log('File drop cancelled'); + * } + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * When the debugger panel is open, the drop position of this event may be inaccurate due to a known limitation. + * To retrieve the correct drop position, please detach the debugger. + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onDragDropEvent(handler) { + const unlistenDragEnter = await this.listen(event.TauriEvent.DRAG_ENTER, (event) => { + handler({ + ...event, + payload: { + type: 'enter', + paths: event.payload.paths, + position: new dpi.PhysicalPosition(event.payload.position) + } + }); + }); + const unlistenDragOver = await this.listen(event.TauriEvent.DRAG_OVER, (event) => { + handler({ + ...event, + payload: { + type: 'over', + position: new dpi.PhysicalPosition(event.payload.position) + } + }); + }); + const unlistenDragDrop = await this.listen(event.TauriEvent.DRAG_DROP, (event) => { + handler({ + ...event, + payload: { + type: 'drop', + paths: event.payload.paths, + position: new dpi.PhysicalPosition(event.payload.position) + } + }); + }); + const unlistenDragLeave = await this.listen(event.TauriEvent.DRAG_LEAVE, (event) => { + handler({ ...event, payload: { type: 'leave' } }); + }); + return () => { + unlistenDragEnter(); + unlistenDragDrop(); + unlistenDragOver(); + unlistenDragLeave(); + }; + } +} + +exports.Webview = Webview; +exports.getAllWebviews = getAllWebviews; +exports.getCurrentWebview = getCurrentWebview; diff --git a/node_modules/@tauri-apps/api/webview.d.ts b/node_modules/@tauri-apps/api/webview.d.ts new file mode 100644 index 0000000..c3048c1 --- /dev/null +++ b/node_modules/@tauri-apps/api/webview.d.ts @@ -0,0 +1,612 @@ +/** + * Provides APIs to create webviews, communicate with other webviews and manipulate the current webview. + * + * #### Webview events + * + * Events can be listened to using {@link Webview.listen}: + * ```typescript + * import { getCurrentWebview } from "@tauri-apps/api/webview"; + * getCurrentWebview().listen("my-webview-event", ({ event, payload }) => { }); + * ``` + * + * @module + */ +import { PhysicalPosition, PhysicalSize } from './dpi'; +import type { LogicalPosition, LogicalSize } from './dpi'; +import { Position, Size } from './dpi'; +import type { EventName, EventCallback, UnlistenFn } from './event'; +import { type EventTarget } from './event'; +import { BackgroundThrottlingPolicy, ScrollBarStyle, Color, Window } from './window'; +import { WebviewWindow } from './webviewWindow'; +/** The drag and drop event types. */ +type DragDropEvent = { + type: 'enter'; + paths: string[]; + position: PhysicalPosition; +} | { + type: 'over'; + position: PhysicalPosition; +} | { + type: 'drop'; + paths: string[]; + position: PhysicalPosition; +} | { + type: 'leave'; +}; +/** + * Get an instance of `Webview` for the current webview. + * + * @since 2.0.0 + */ +declare function getCurrentWebview(): Webview; +/** + * Gets a list of instances of `Webview` for all available webviews. + * + * @since 2.0.0 + */ +declare function getAllWebviews(): Promise; +/** @ignore */ +export type WebviewLabel = string; +/** + * Create new webview or get a handle to an existing one. + * + * Webviews are identified by a *label* a unique identifier that can be used to reference it later. + * It may only contain alphanumeric characters `a-zA-Z` plus the following special characters `-`, `/`, `:` and `_`. + * + * @example + * ```typescript + * import { Window } from "@tauri-apps/api/window" + * import { Webview } from "@tauri-apps/api/webview" + * + * const appWindow = new Window('uniqueLabel'); + * + * appWindow.once('tauri://created', async function () { + * // `new Webview` Should be called after the window is successfully created, + * // or webview may not be attached to the window since window is not created yet. + * + * // loading embedded asset: + * const webview = new Webview(appWindow, 'theUniqueLabel', { + * url: 'path/to/page.html', + * + * // create a webview with specific logical position and size + * x: 0, + * y: 0, + * width: 800, + * height: 600, + * }); + * // alternatively, load a remote URL: + * const webview = new Webview(appWindow, 'theUniqueLabel', { + * url: 'https://github.com/tauri-apps/tauri', + * + * // create a webview with specific logical position and size + * x: 0, + * y: 0, + * width: 800, + * height: 600, + * }); + * + * webview.once('tauri://created', function () { + * // webview successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview + * }); + * + * + * // emit an event to the backend + * await webview.emit("some-event", "data"); + * // listen to an event from the backend + * const unlisten = await webview.listen("event-name", e => { }); + * unlisten(); + * }); + * ``` + * + * @since 2.0.0 + */ +declare class Webview { + /** The webview label. It is a unique identifier for the webview, can be used to reference it later. */ + label: WebviewLabel; + /** The window hosting this webview. */ + window: Window; + /** Local event listeners. */ + listeners: Record>>; + /** + * Creates a new Webview. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window' + * import { Webview } from '@tauri-apps/api/webview' + * const appWindow = new Window('my-label') + * + * appWindow.once('tauri://created', async function() { + * const webview = new Webview(appWindow, 'my-label', { + * url: 'https://github.com/tauri-apps/tauri', + * + * // create a webview with specific logical position and size + * x: 0, + * y: 0, + * width: 800, + * height: 600, + * }); + * + * webview.once('tauri://created', function () { + * // webview successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview + * }); + * }); + * ``` + * + * @param window the window to add this webview to. + * @param label The unique webview label. Must be alphanumeric: `a-zA-Z-/:_`. + * @returns The {@link Webview} instance to communicate with the webview. + */ + constructor(window: Window, label: WebviewLabel, options: WebviewOptions); + /** + * Gets the Webview for the webview associated with the given label. + * @example + * ```typescript + * import { Webview } from '@tauri-apps/api/webview'; + * const mainWebview = Webview.getByLabel('main'); + * ``` + * + * @param label The webview label. + * @returns The Webview instance to communicate with the webview or null if the webview doesn't exist. + */ + static getByLabel(label: string): Promise; + /** + * Get an instance of `Webview` for the current webview. + */ + static getCurrent(): Webview; + /** + * Gets a list of instances of `Webview` for all available webviews. + */ + static getAll(): Promise; + /** + * Listen to an emitted event on this webview. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const unlisten = await getCurrentWebview().listen('state-changed', (event) => { + * console.log(`Got error: ${payload}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + listen(event: EventName, handler: EventCallback): Promise; + /** + * Listen to an emitted event on this webview only once. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const unlisten = await getCurrent().once('initialized', (event) => { + * console.log(`Webview initialized!`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + once(event: EventName, handler: EventCallback): Promise; + /** + * Emits an event to all {@link EventTarget|targets}. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().emit('webview-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + emit(event: string, payload?: T): Promise; + /** + * Emits an event to all {@link EventTarget|targets} matching the given target. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().emitTo('main', 'webview-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object. + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + emitTo(target: string | EventTarget, event: string, payload?: T): Promise; + /** @ignore */ + _handleTauriEvent(event: string, handler: EventCallback): boolean; + /** + * The position of the top-left hand corner of the webview's client area relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const position = await getCurrentWebview().position(); + * ``` + * + * @returns The webview's position. + */ + position(): Promise; + /** + * The physical size of the webview's client area. + * The client area is the content of the webview, excluding the title bar and borders. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const size = await getCurrentWebview().size(); + * ``` + * + * @returns The webview's size. + */ + size(): Promise; + /** + * Closes the webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().close(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + close(): Promise; + /** + * Resizes the webview. + * @example + * ```typescript + * import { getCurrent, LogicalSize } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical size. + * @returns A promise indicating the success or failure of the operation. + */ + setSize(size: LogicalSize | PhysicalSize | Size): Promise; + /** + * Sets the webview position. + * @example + * ```typescript + * import { getCurrent, LogicalPosition } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setPosition(new LogicalPosition(600, 500)); + * ``` + * + * @param position The new position, in logical or physical pixels. + * @returns A promise indicating the success or failure of the operation. + */ + setPosition(position: LogicalPosition | PhysicalPosition | Position): Promise; + /** + * Bring the webview to front and focus. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setFocus(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + setFocus(): Promise; + /** + * Sets whether the webview should automatically grow and shrink its size and position when the parent window resizes. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setAutoResize(true); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + setAutoResize(autoResize: boolean): Promise; + /** + * Hide the webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().hide(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + hide(): Promise; + /** + * Show the webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().show(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + show(): Promise; + /** + * Set webview zoom level. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setZoom(1.5); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + setZoom(scaleFactor: number): Promise; + /** + * Moves this webview to the given label. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().reparent('other-window'); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + reparent(window: Window | WebviewWindow | string): Promise; + /** + * Clears all browsing data for this webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().clearAllBrowsingData(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + clearAllBrowsingData(): Promise; + /** + * Specify the webview background color. + * + * #### Platfrom-specific: + * + * - **macOS / iOS**: Not implemented. + * - **Windows**: + * - On Windows 7, transparency is not supported and the alpha value will be ignored. + * - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.1.0 + */ + setBackgroundColor(color: Color | null): Promise; + /** + * Listen to a file drop event. + * The listener is triggered when the user hovers the selected files on the webview, + * drops the files or cancels the operation. + * + * @example + * ```typescript + * import { getCurrentWebview } from "@tauri-apps/api/webview"; + * const unlisten = await getCurrentWebview().onDragDropEvent((event) => { + * if (event.payload.type === 'over') { + * console.log('User hovering', event.payload.position); + * } else if (event.payload.type === 'drop') { + * console.log('User dropped', event.payload.paths); + * } else { + * console.log('File drop cancelled'); + * } + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * When the debugger panel is open, the drop position of this event may be inaccurate due to a known limitation. + * To retrieve the correct drop position, please detach the debugger. + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + onDragDropEvent(handler: EventCallback): Promise; +} +/** + * Configuration for the webview to create. + * + * @since 2.0.0 + */ +interface WebviewOptions { + /** + * Remote URL or local file path to open. + * + * - URL such as `https://github.com/tauri-apps` is opened directly on a Tauri webview. + * - data: URL such as `data:text/html,...` is only supported with the `webview-data-url` Cargo feature for the `tauri` dependency. + * - local file path or route such as `/path/to/page.html` or `/users` is appended to the application URL (the devServer URL on development, or `tauri://localhost/` and `https://tauri.localhost/` on production). + */ + url?: string; + /** The initial vertical position in logical pixels. */ + x: number; + /** The initial horizontal position in logical pixels. */ + y: number; + /** The initial width in logical pixels. */ + width: number; + /** The initial height in logical pixels. */ + height: number; + /** + * Whether the webview is transparent or not. + * Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri.conf.json > app > macOSPrivateApi`. + * WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`. + */ + transparent?: boolean; + /** + * Whether the webview should have focus or not + * + * @since 2.1.0 + */ + focus?: boolean; + /** + * Whether the drag and drop is enabled or not on the webview. By default it is enabled. + * + * Disabling it is required to use HTML5 drag and drop on the frontend on Windows. + */ + dragDropEnabled?: boolean; + /** + * Whether clicking an inactive webview also clicks through to the webview on macOS. + */ + acceptFirstMouse?: boolean; + /** + * The user agent for the webview. + */ + userAgent?: string; + /** + * Whether or not the webview should be launched in incognito mode. + * + * #### Platform-specific + * + * - **Android:** Unsupported. + */ + incognito?: boolean; + /** + * The proxy URL for the WebView for all network requests. + * + * Must be either a `http://` or a `socks5://` URL. + * + * #### Platform-specific + * + * - **macOS**: Requires the `macos-proxy` feature flag and only compiles for macOS 14+. + * */ + proxyUrl?: string; + /** + * Whether page zooming by hotkeys is enabled + * + * #### Platform-specific: + * + * - **Windows**: Controls WebView2's [`IsZoomControlEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2settings?view=webview2-winrt-1.0.2420.47#iszoomcontrolenabled) setting. + * - **MacOS / Linux**: Injects a polyfill that zooms in and out with `ctrl/command` + `-/=`, + * 20% in each step, ranging from 20% to 1000%. Requires `webview:allow-set-webview-zoom` permission + * + * - **Android / iOS**: Unsupported. + */ + zoomHotkeysEnabled?: boolean; + /** + * Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`. + * + * #### Note + * + * Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux. + * + * #### Warning + * + * Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access them. + * + * @since 2.1.0 + */ + useHttpsScheme?: boolean; + /** + * Whether web inspector, which is usually called browser devtools, is enabled or not. Enabled by default. + * + * This API works in **debug** builds, but requires `devtools` feature flag to enable it in **release** builds. + * + * #### Platform-specific + * + * - macOS: This will call private functions on **macOS**. + * - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android. + * - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window. + * + * @since 2.1.0 + */ + devtools?: boolean; + /** + * Set the window and webview background color. + * + * #### Platform-specific: + * + * - **macOS / iOS**: Not implemented. + * - **Windows**: + * - On Windows 7, alpha channel is ignored. + * - On Windows 8 and newer, if alpha channel is not `0`, it will be ignored. + * + * @since 2.1.0 + */ + backgroundColor?: Color; + /** Change the default background throttling behaviour. + * + * By default, browsers use a suspend policy that will throttle timers and even unload + * the whole tab (view) to free resources after roughly 5 minutes when a view became + * minimized or hidden. This will pause all tasks until the documents visibility state + * changes back from hidden to visible by bringing the view back to the foreground. + * + * ## Platform-specific + * + * - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice. + * - **iOS**: Supported since version 17.0+. + * - **macOS**: Supported since version 14.0+. + * + * see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + * + * @since 2.3.0 + */ + backgroundThrottling?: BackgroundThrottlingPolicy; + /** + * Whether we should disable JavaScript code execution on the webview or not. + */ + javascriptDisabled?: boolean; + /** + * on macOS and iOS there is a link preview on long pressing links, this is enabled by default. + * see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview + */ + allowLinkPreview?: boolean; + /** + * Allows disabling the input accessory view on iOS. + * + * The accessory view is the view that appears above the keyboard when a text input element is focused. + * It usually displays a view with "Done", "Next" buttons. + */ + disableInputAccessoryView?: boolean; + /** + * Set a custom path for the webview's data directory (localStorage, cache, etc.) **relative to [`appDataDir()`]/${label}**. + * For security reasons, paths outside of that location can only be configured on the Rust side. + * + * #### Platform-specific: + * + * - **Windows**: WebViews with different values for settings like `additionalBrowserArgs`, `browserExtensionsEnabled` or `scrollBarStyle` must have different data directories. + * - **macOS / iOS**: Unsupported, use `dataStoreIdentifier` instead. + * - **Android**: Unsupported. + * + * @since 2.9.0 + */ + dataDirectory?: string; + /** + * Initialize the WebView with a custom data store identifier. This can be seen as a replacement for `dataDirectory` which is unavailable in WKWebView. + * See https://developer.apple.com/documentation/webkit/wkwebsitedatastore/init(foridentifier:)?language=objc + * + * The array must contain 16 u8 numbers. + * + * #### Platform-specific: + * + * - **macOS / iOS**: Available on macOS >= 14 and iOS >= 17 + * - **Windows / Linux / Android**: Unsupported. + * + * @since 2.9.0 + */ + dataStoreIdentifier?: number[]; + /** + * Specifies the native scrollbar style to use with the webview. + * CSS styles that modify the scrollbar are applied on top of the native appearance configured here. + * + * Defaults to `default`, which is the browser default. + * + * ## Platform-specific + * + * - **Windows**: + * - `fluentOverlay` requires WebView2 Runtime version 125.0.2535.41 or higher, and does nothing + * on older versions. + * - This option must be given the same value for all webviews. + * - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation. + */ + scrollBarStyle?: ScrollBarStyle; +} +export { Webview, getCurrentWebview, getAllWebviews }; +export type { DragDropEvent, WebviewOptions, Color }; diff --git a/node_modules/@tauri-apps/api/webview.js b/node_modules/@tauri-apps/api/webview.js new file mode 100644 index 0000000..703a556 --- /dev/null +++ b/node_modules/@tauri-apps/api/webview.js @@ -0,0 +1,590 @@ +import { PhysicalPosition, PhysicalSize, Size, Position } from './dpi.js'; +import { listen, once, emit, emitTo, TauriEvent } from './event.js'; +import { invoke } from './core.js'; +import { Window, getCurrentWindow } from './window.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * Provides APIs to create webviews, communicate with other webviews and manipulate the current webview. + * + * #### Webview events + * + * Events can be listened to using {@link Webview.listen}: + * ```typescript + * import { getCurrentWebview } from "@tauri-apps/api/webview"; + * getCurrentWebview().listen("my-webview-event", ({ event, payload }) => { }); + * ``` + * + * @module + */ +/** + * Get an instance of `Webview` for the current webview. + * + * @since 2.0.0 + */ +function getCurrentWebview() { + return new Webview(getCurrentWindow(), window.__TAURI_INTERNALS__.metadata.currentWebview.label, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }); +} +/** + * Gets a list of instances of `Webview` for all available webviews. + * + * @since 2.0.0 + */ +async function getAllWebviews() { + return invoke('plugin:webview|get_all_webviews').then((webviews) => webviews.map((w) => new Webview(new Window(w.windowLabel, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }), w.label, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }))); +} +/** @ignore */ +// events that are emitted right here instead of by the created webview +const localTauriEvents = ['tauri://created', 'tauri://error']; +/** + * Create new webview or get a handle to an existing one. + * + * Webviews are identified by a *label* a unique identifier that can be used to reference it later. + * It may only contain alphanumeric characters `a-zA-Z` plus the following special characters `-`, `/`, `:` and `_`. + * + * @example + * ```typescript + * import { Window } from "@tauri-apps/api/window" + * import { Webview } from "@tauri-apps/api/webview" + * + * const appWindow = new Window('uniqueLabel'); + * + * appWindow.once('tauri://created', async function () { + * // `new Webview` Should be called after the window is successfully created, + * // or webview may not be attached to the window since window is not created yet. + * + * // loading embedded asset: + * const webview = new Webview(appWindow, 'theUniqueLabel', { + * url: 'path/to/page.html', + * + * // create a webview with specific logical position and size + * x: 0, + * y: 0, + * width: 800, + * height: 600, + * }); + * // alternatively, load a remote URL: + * const webview = new Webview(appWindow, 'theUniqueLabel', { + * url: 'https://github.com/tauri-apps/tauri', + * + * // create a webview with specific logical position and size + * x: 0, + * y: 0, + * width: 800, + * height: 600, + * }); + * + * webview.once('tauri://created', function () { + * // webview successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview + * }); + * + * + * // emit an event to the backend + * await webview.emit("some-event", "data"); + * // listen to an event from the backend + * const unlisten = await webview.listen("event-name", e => { }); + * unlisten(); + * }); + * ``` + * + * @since 2.0.0 + */ +class Webview { + /** + * Creates a new Webview. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window' + * import { Webview } from '@tauri-apps/api/webview' + * const appWindow = new Window('my-label') + * + * appWindow.once('tauri://created', async function() { + * const webview = new Webview(appWindow, 'my-label', { + * url: 'https://github.com/tauri-apps/tauri', + * + * // create a webview with specific logical position and size + * x: 0, + * y: 0, + * width: 800, + * height: 600, + * }); + * + * webview.once('tauri://created', function () { + * // webview successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview + * }); + * }); + * ``` + * + * @param window the window to add this webview to. + * @param label The unique webview label. Must be alphanumeric: `a-zA-Z-/:_`. + * @returns The {@link Webview} instance to communicate with the webview. + */ + constructor(window, label, options) { + this.window = window; + this.label = label; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.listeners = Object.create(null); + // @ts-expect-error `skip` is not a public API so it is not defined in WebviewOptions + if (!(options === null || options === void 0 ? void 0 : options.skip)) { + invoke('plugin:webview|create_webview', { + windowLabel: window.label, + options: { + ...options, + label + } + }) + .then(async () => this.emit('tauri://created')) + .catch(async (e) => this.emit('tauri://error', e)); + } + } + /** + * Gets the Webview for the webview associated with the given label. + * @example + * ```typescript + * import { Webview } from '@tauri-apps/api/webview'; + * const mainWebview = Webview.getByLabel('main'); + * ``` + * + * @param label The webview label. + * @returns The Webview instance to communicate with the webview or null if the webview doesn't exist. + */ + static async getByLabel(label) { + var _a; + return (_a = (await getAllWebviews()).find((w) => w.label === label)) !== null && _a !== void 0 ? _a : null; + } + /** + * Get an instance of `Webview` for the current webview. + */ + static getCurrent() { + return getCurrentWebview(); + } + /** + * Gets a list of instances of `Webview` for all available webviews. + */ + static async getAll() { + return getAllWebviews(); + } + /** + * Listen to an emitted event on this webview. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const unlisten = await getCurrentWebview().listen('state-changed', (event) => { + * console.log(`Got error: ${payload}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async listen(event, handler) { + if (this._handleTauriEvent(event, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event]; + listeners.splice(listeners.indexOf(handler), 1); + }; + } + return listen(event, handler, { + target: { kind: 'Webview', label: this.label } + }); + } + /** + * Listen to an emitted event on this webview only once. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const unlisten = await getCurrent().once('initialized', (event) => { + * console.log(`Webview initialized!`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async once(event, handler) { + if (this._handleTauriEvent(event, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event]; + listeners.splice(listeners.indexOf(handler), 1); + }; + } + return once(event, handler, { + target: { kind: 'Webview', label: this.label } + }); + } + /** + * Emits an event to all {@link EventTarget|targets}. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().emit('webview-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + async emit(event, payload) { + if (localTauriEvents.includes(event)) { + // eslint-disable-next-line + for (const handler of this.listeners[event] || []) { + handler({ + event, + id: -1, + payload + }); + } + return; + } + return emit(event, payload); + } + /** + * Emits an event to all {@link EventTarget|targets} matching the given target. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().emitTo('main', 'webview-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object. + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + async emitTo(target, event, payload) { + if (localTauriEvents.includes(event)) { + // eslint-disable-next-line + for (const handler of this.listeners[event] || []) { + handler({ + event, + id: -1, + payload + }); + } + return; + } + return emitTo(target, event, payload); + } + /** @ignore */ + _handleTauriEvent(event, handler) { + if (localTauriEvents.includes(event)) { + if (!(event in this.listeners)) { + // eslint-disable-next-line security/detect-object-injection + this.listeners[event] = [handler]; + } + else { + // eslint-disable-next-line security/detect-object-injection + this.listeners[event].push(handler); + } + return true; + } + return false; + } + // Getters + /** + * The position of the top-left hand corner of the webview's client area relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const position = await getCurrentWebview().position(); + * ``` + * + * @returns The webview's position. + */ + async position() { + return invoke('plugin:webview|webview_position', { + label: this.label + }).then((p) => new PhysicalPosition(p)); + } + /** + * The physical size of the webview's client area. + * The client area is the content of the webview, excluding the title bar and borders. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const size = await getCurrentWebview().size(); + * ``` + * + * @returns The webview's size. + */ + async size() { + return invoke('plugin:webview|webview_size', { + label: this.label + }).then((s) => new PhysicalSize(s)); + } + // Setters + /** + * Closes the webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().close(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async close() { + return invoke('plugin:webview|webview_close', { + label: this.label + }); + } + /** + * Resizes the webview. + * @example + * ```typescript + * import { getCurrent, LogicalSize } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical size. + * @returns A promise indicating the success or failure of the operation. + */ + async setSize(size) { + return invoke('plugin:webview|set_webview_size', { + label: this.label, + value: size instanceof Size ? size : new Size(size) + }); + } + /** + * Sets the webview position. + * @example + * ```typescript + * import { getCurrent, LogicalPosition } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setPosition(new LogicalPosition(600, 500)); + * ``` + * + * @param position The new position, in logical or physical pixels. + * @returns A promise indicating the success or failure of the operation. + */ + async setPosition(position) { + return invoke('plugin:webview|set_webview_position', { + label: this.label, + value: position instanceof Position ? position : new Position(position) + }); + } + /** + * Bring the webview to front and focus. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setFocus(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setFocus() { + return invoke('plugin:webview|set_webview_focus', { + label: this.label + }); + } + /** + * Sets whether the webview should automatically grow and shrink its size and position when the parent window resizes. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setAutoResize(true); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setAutoResize(autoResize) { + return invoke('plugin:webview|set_webview_auto_resize', { + label: this.label, + value: autoResize + }); + } + /** + * Hide the webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().hide(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async hide() { + return invoke('plugin:webview|webview_hide', { + label: this.label + }); + } + /** + * Show the webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().show(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async show() { + return invoke('plugin:webview|webview_show', { + label: this.label + }); + } + /** + * Set webview zoom level. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setZoom(1.5); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setZoom(scaleFactor) { + return invoke('plugin:webview|set_webview_zoom', { + label: this.label, + value: scaleFactor + }); + } + /** + * Moves this webview to the given label. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().reparent('other-window'); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async reparent(window) { + return invoke('plugin:webview|reparent', { + label: this.label, + window: typeof window === 'string' ? window : window.label + }); + } + /** + * Clears all browsing data for this webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().clearAllBrowsingData(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async clearAllBrowsingData() { + return invoke('plugin:webview|clear_all_browsing_data'); + } + /** + * Specify the webview background color. + * + * #### Platfrom-specific: + * + * - **macOS / iOS**: Not implemented. + * - **Windows**: + * - On Windows 7, transparency is not supported and the alpha value will be ignored. + * - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.1.0 + */ + async setBackgroundColor(color) { + return invoke('plugin:webview|set_webview_background_color', { color }); + } + // Listeners + /** + * Listen to a file drop event. + * The listener is triggered when the user hovers the selected files on the webview, + * drops the files or cancels the operation. + * + * @example + * ```typescript + * import { getCurrentWebview } from "@tauri-apps/api/webview"; + * const unlisten = await getCurrentWebview().onDragDropEvent((event) => { + * if (event.payload.type === 'over') { + * console.log('User hovering', event.payload.position); + * } else if (event.payload.type === 'drop') { + * console.log('User dropped', event.payload.paths); + * } else { + * console.log('File drop cancelled'); + * } + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * When the debugger panel is open, the drop position of this event may be inaccurate due to a known limitation. + * To retrieve the correct drop position, please detach the debugger. + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onDragDropEvent(handler) { + const unlistenDragEnter = await this.listen(TauriEvent.DRAG_ENTER, (event) => { + handler({ + ...event, + payload: { + type: 'enter', + paths: event.payload.paths, + position: new PhysicalPosition(event.payload.position) + } + }); + }); + const unlistenDragOver = await this.listen(TauriEvent.DRAG_OVER, (event) => { + handler({ + ...event, + payload: { + type: 'over', + position: new PhysicalPosition(event.payload.position) + } + }); + }); + const unlistenDragDrop = await this.listen(TauriEvent.DRAG_DROP, (event) => { + handler({ + ...event, + payload: { + type: 'drop', + paths: event.payload.paths, + position: new PhysicalPosition(event.payload.position) + } + }); + }); + const unlistenDragLeave = await this.listen(TauriEvent.DRAG_LEAVE, (event) => { + handler({ ...event, payload: { type: 'leave' } }); + }); + return () => { + unlistenDragEnter(); + unlistenDragDrop(); + unlistenDragOver(); + unlistenDragLeave(); + }; + } +} + +export { Webview, getAllWebviews, getCurrentWebview }; diff --git a/node_modules/@tauri-apps/api/webviewWindow.cjs b/node_modules/@tauri-apps/api/webviewWindow.cjs new file mode 100644 index 0000000..3740633 --- /dev/null +++ b/node_modules/@tauri-apps/api/webviewWindow.cjs @@ -0,0 +1,211 @@ +'use strict'; + +var webview = require('./webview.cjs'); +var window = require('./window.cjs'); +var event = require('./event.cjs'); +var core = require('./core.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * Get an instance of `Webview` for the current webview window. + * + * @since 2.0.0 + */ +function getCurrentWebviewWindow() { + const webview$1 = webview.getCurrentWebview(); + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + return new WebviewWindow(webview$1.label, { skip: true }); +} +/** + * Gets a list of instances of `Webview` for all available webview windows. + * + * @since 2.0.0 + */ +async function getAllWebviewWindows() { + return core.invoke('plugin:window|get_all_windows').then((windows) => windows.map((w) => new WebviewWindow(w, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }))); +} +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +class WebviewWindow { + /** + * Creates a new {@link Window} hosting a {@link Webview}. + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow' + * const webview = new WebviewWindow('my-label', { + * url: 'https://github.com/tauri-apps/tauri' + * }); + * webview.once('tauri://created', function () { + * // webview successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview + * }); + * ``` + * + * @param label The unique webview label. Must be alphanumeric: `a-zA-Z-/:_`. + * @returns The {@link WebviewWindow} instance to communicate with the window and webview. + */ + constructor(label, options = {}) { + var _a; + this.label = label; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.listeners = Object.create(null); + // @ts-expect-error `skip` is not a public API so it is not defined in WebviewOptions + if (!(options === null || options === void 0 ? void 0 : options.skip)) { + core.invoke('plugin:webview|create_webview_window', { + options: { + ...options, + parent: typeof options.parent === 'string' + ? options.parent + : (_a = options.parent) === null || _a === void 0 ? void 0 : _a.label, + label + } + }) + .then(async () => this.emit('tauri://created')) + .catch(async (e) => this.emit('tauri://error', e)); + } + } + /** + * Gets the Webview for the webview associated with the given label. + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * const mainWebview = WebviewWindow.getByLabel('main'); + * ``` + * + * @param label The webview label. + * @returns The Webview instance to communicate with the webview or null if the webview doesn't exist. + */ + static async getByLabel(label) { + var _a; + const webview = (_a = (await getAllWebviewWindows()).find((w) => w.label === label)) !== null && _a !== void 0 ? _a : null; + if (webview) { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + return new WebviewWindow(webview.label, { skip: true }); + } + return null; + } + /** + * Get an instance of `Webview` for the current webview. + */ + static getCurrent() { + return getCurrentWebviewWindow(); + } + /** + * Gets a list of instances of `Webview` for all available webviews. + */ + static async getAll() { + return getAllWebviewWindows(); + } + /** + * Listen to an emitted event on this webview window. + * + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * const unlisten = await WebviewWindow.getCurrent().listen('state-changed', (event) => { + * console.log(`Got error: ${payload}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async listen(event$1, handler) { + if (this._handleTauriEvent(event$1, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event$1]; + listeners.splice(listeners.indexOf(handler), 1); + }; + } + return event.listen(event$1, handler, { + target: { kind: 'WebviewWindow', label: this.label } + }); + } + /** + * Listen to an emitted event on this webview window only once. + * + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * const unlisten = await WebviewWindow.getCurrent().once('initialized', (event) => { + * console.log(`Webview initialized!`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async once(event$1, handler) { + if (this._handleTauriEvent(event$1, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event$1]; + listeners.splice(listeners.indexOf(handler), 1); + }; + } + return event.once(event$1, handler, { + target: { kind: 'WebviewWindow', label: this.label } + }); + } + /** + * Set the window and webview background color. + * + * #### Platform-specific: + * + * - **Android / iOS:** Unsupported for the window layer. + * - **macOS / iOS**: Not implemented for the webview layer. + * - **Windows**: + * - alpha channel is ignored for the window layer. + * - On Windows 7, alpha channel is ignored for the webview layer. + * - On Windows 8 and newer, if alpha channel is not `0`, it will be ignored. + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.1.0 + */ + async setBackgroundColor(color) { + return core.invoke('plugin:window|set_background_color', { color }).then(() => { + return core.invoke('plugin:webview|set_webview_background_color', { color }); + }); + } +} +// Order matters, we use window APIs by default +applyMixins(WebviewWindow, [window.Window, webview.Webview]); +/** Extends a base class by other specified classes, without overriding existing properties */ +function applyMixins(baseClass, extendedClasses) { + (Array.isArray(extendedClasses) + ? extendedClasses + : [extendedClasses]).forEach((extendedClass) => { + Object.getOwnPropertyNames(extendedClass.prototype).forEach((name) => { + var _a; + if (typeof baseClass.prototype === 'object' + && baseClass.prototype + && name in baseClass.prototype) + return; + Object.defineProperty(baseClass.prototype, name, + // eslint-disable-next-line + (_a = Object.getOwnPropertyDescriptor(extendedClass.prototype, name)) !== null && _a !== void 0 ? _a : Object.create(null)); + }); + }); +} + +exports.WebviewWindow = WebviewWindow; +exports.getAllWebviewWindows = getAllWebviewWindows; +exports.getCurrentWebviewWindow = getCurrentWebviewWindow; diff --git a/node_modules/@tauri-apps/api/webviewWindow.d.ts b/node_modules/@tauri-apps/api/webviewWindow.d.ts new file mode 100644 index 0000000..f5ae32c --- /dev/null +++ b/node_modules/@tauri-apps/api/webviewWindow.d.ts @@ -0,0 +1,123 @@ +import { Webview, WebviewLabel, WebviewOptions } from './webview'; +import type { WindowOptions } from './window'; +import { Window } from './window'; +import type { EventName, EventCallback, UnlistenFn } from './event'; +import type { Color, DragDropEvent } from './webview'; +/** + * Get an instance of `Webview` for the current webview window. + * + * @since 2.0.0 + */ +declare function getCurrentWebviewWindow(): WebviewWindow; +/** + * Gets a list of instances of `Webview` for all available webview windows. + * + * @since 2.0.0 + */ +declare function getAllWebviewWindows(): Promise; +interface WebviewWindow extends Webview, Window { +} +declare class WebviewWindow { + label: string; + /** Local event listeners. */ + listeners: Record>>; + /** + * Creates a new {@link Window} hosting a {@link Webview}. + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow' + * const webview = new WebviewWindow('my-label', { + * url: 'https://github.com/tauri-apps/tauri' + * }); + * webview.once('tauri://created', function () { + * // webview successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview + * }); + * ``` + * + * @param label The unique webview label. Must be alphanumeric: `a-zA-Z-/:_`. + * @returns The {@link WebviewWindow} instance to communicate with the window and webview. + */ + constructor(label: WebviewLabel, options?: Omit & WindowOptions); + /** + * Gets the Webview for the webview associated with the given label. + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * const mainWebview = WebviewWindow.getByLabel('main'); + * ``` + * + * @param label The webview label. + * @returns The Webview instance to communicate with the webview or null if the webview doesn't exist. + */ + static getByLabel(label: string): Promise; + /** + * Get an instance of `Webview` for the current webview. + */ + static getCurrent(): WebviewWindow; + /** + * Gets a list of instances of `Webview` for all available webviews. + */ + static getAll(): Promise; + /** + * Listen to an emitted event on this webview window. + * + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * const unlisten = await WebviewWindow.getCurrent().listen('state-changed', (event) => { + * console.log(`Got error: ${payload}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + listen(event: EventName, handler: EventCallback): Promise; + /** + * Listen to an emitted event on this webview window only once. + * + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * const unlisten = await WebviewWindow.getCurrent().once('initialized', (event) => { + * console.log(`Webview initialized!`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + once(event: EventName, handler: EventCallback): Promise; + /** + * Set the window and webview background color. + * + * #### Platform-specific: + * + * - **Android / iOS:** Unsupported for the window layer. + * - **macOS / iOS**: Not implemented for the webview layer. + * - **Windows**: + * - alpha channel is ignored for the window layer. + * - On Windows 7, alpha channel is ignored for the webview layer. + * - On Windows 8 and newer, if alpha channel is not `0`, it will be ignored. + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.1.0 + */ + setBackgroundColor(color: Color): Promise; +} +export { WebviewWindow, getCurrentWebviewWindow, getAllWebviewWindows }; +export type { DragDropEvent, Color }; diff --git a/node_modules/@tauri-apps/api/webviewWindow.js b/node_modules/@tauri-apps/api/webviewWindow.js new file mode 100644 index 0000000..38a6a4c --- /dev/null +++ b/node_modules/@tauri-apps/api/webviewWindow.js @@ -0,0 +1,207 @@ +import { getCurrentWebview, Webview } from './webview.js'; +import { Window } from './window.js'; +import { listen, once } from './event.js'; +import { invoke } from './core.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * Get an instance of `Webview` for the current webview window. + * + * @since 2.0.0 + */ +function getCurrentWebviewWindow() { + const webview = getCurrentWebview(); + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + return new WebviewWindow(webview.label, { skip: true }); +} +/** + * Gets a list of instances of `Webview` for all available webview windows. + * + * @since 2.0.0 + */ +async function getAllWebviewWindows() { + return invoke('plugin:window|get_all_windows').then((windows) => windows.map((w) => new WebviewWindow(w, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }))); +} +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +class WebviewWindow { + /** + * Creates a new {@link Window} hosting a {@link Webview}. + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow' + * const webview = new WebviewWindow('my-label', { + * url: 'https://github.com/tauri-apps/tauri' + * }); + * webview.once('tauri://created', function () { + * // webview successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview + * }); + * ``` + * + * @param label The unique webview label. Must be alphanumeric: `a-zA-Z-/:_`. + * @returns The {@link WebviewWindow} instance to communicate with the window and webview. + */ + constructor(label, options = {}) { + var _a; + this.label = label; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.listeners = Object.create(null); + // @ts-expect-error `skip` is not a public API so it is not defined in WebviewOptions + if (!(options === null || options === void 0 ? void 0 : options.skip)) { + invoke('plugin:webview|create_webview_window', { + options: { + ...options, + parent: typeof options.parent === 'string' + ? options.parent + : (_a = options.parent) === null || _a === void 0 ? void 0 : _a.label, + label + } + }) + .then(async () => this.emit('tauri://created')) + .catch(async (e) => this.emit('tauri://error', e)); + } + } + /** + * Gets the Webview for the webview associated with the given label. + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * const mainWebview = WebviewWindow.getByLabel('main'); + * ``` + * + * @param label The webview label. + * @returns The Webview instance to communicate with the webview or null if the webview doesn't exist. + */ + static async getByLabel(label) { + var _a; + const webview = (_a = (await getAllWebviewWindows()).find((w) => w.label === label)) !== null && _a !== void 0 ? _a : null; + if (webview) { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + return new WebviewWindow(webview.label, { skip: true }); + } + return null; + } + /** + * Get an instance of `Webview` for the current webview. + */ + static getCurrent() { + return getCurrentWebviewWindow(); + } + /** + * Gets a list of instances of `Webview` for all available webviews. + */ + static async getAll() { + return getAllWebviewWindows(); + } + /** + * Listen to an emitted event on this webview window. + * + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * const unlisten = await WebviewWindow.getCurrent().listen('state-changed', (event) => { + * console.log(`Got error: ${payload}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async listen(event, handler) { + if (this._handleTauriEvent(event, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event]; + listeners.splice(listeners.indexOf(handler), 1); + }; + } + return listen(event, handler, { + target: { kind: 'WebviewWindow', label: this.label } + }); + } + /** + * Listen to an emitted event on this webview window only once. + * + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * const unlisten = await WebviewWindow.getCurrent().once('initialized', (event) => { + * console.log(`Webview initialized!`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async once(event, handler) { + if (this._handleTauriEvent(event, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event]; + listeners.splice(listeners.indexOf(handler), 1); + }; + } + return once(event, handler, { + target: { kind: 'WebviewWindow', label: this.label } + }); + } + /** + * Set the window and webview background color. + * + * #### Platform-specific: + * + * - **Android / iOS:** Unsupported for the window layer. + * - **macOS / iOS**: Not implemented for the webview layer. + * - **Windows**: + * - alpha channel is ignored for the window layer. + * - On Windows 7, alpha channel is ignored for the webview layer. + * - On Windows 8 and newer, if alpha channel is not `0`, it will be ignored. + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.1.0 + */ + async setBackgroundColor(color) { + return invoke('plugin:window|set_background_color', { color }).then(() => { + return invoke('plugin:webview|set_webview_background_color', { color }); + }); + } +} +// Order matters, we use window APIs by default +applyMixins(WebviewWindow, [Window, Webview]); +/** Extends a base class by other specified classes, without overriding existing properties */ +function applyMixins(baseClass, extendedClasses) { + (Array.isArray(extendedClasses) + ? extendedClasses + : [extendedClasses]).forEach((extendedClass) => { + Object.getOwnPropertyNames(extendedClass.prototype).forEach((name) => { + var _a; + if (typeof baseClass.prototype === 'object' + && baseClass.prototype + && name in baseClass.prototype) + return; + Object.defineProperty(baseClass.prototype, name, + // eslint-disable-next-line + (_a = Object.getOwnPropertyDescriptor(extendedClass.prototype, name)) !== null && _a !== void 0 ? _a : Object.create(null)); + }); + }); +} + +export { WebviewWindow, getAllWebviewWindows, getCurrentWebviewWindow }; diff --git a/node_modules/@tauri-apps/api/window.cjs b/node_modules/@tauri-apps/api/window.cjs new file mode 100644 index 0000000..d49d1c6 --- /dev/null +++ b/node_modules/@tauri-apps/api/window.cjs @@ -0,0 +1,2051 @@ +'use strict'; + +var dpi = require('./dpi.cjs'); +var event = require('./event.cjs'); +var core = require('./core.cjs'); +var image = require('./image.cjs'); + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * Provides APIs to create windows, communicate with other windows and manipulate the current window. + * + * #### Window events + * + * Events can be listened to using {@link Window.listen}: + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * getCurrentWindow().listen("my-window-event", ({ event, payload }) => { }); + * ``` + * + * @module + */ +/** + * Attention type to request on a window. + * + * @since 1.0.0 + */ +exports.UserAttentionType = void 0; +(function (UserAttentionType) { + /** + * #### Platform-specific + * - **macOS:** Bounces the dock icon until the application is in focus. + * - **Windows:** Flashes both the window and the taskbar button until the application is in focus. + */ + UserAttentionType[UserAttentionType["Critical"] = 1] = "Critical"; + /** + * #### Platform-specific + * - **macOS:** Bounces the dock icon once. + * - **Windows:** Flashes the taskbar button until the application is in focus. + */ + UserAttentionType[UserAttentionType["Informational"] = 2] = "Informational"; +})(exports.UserAttentionType || (exports.UserAttentionType = {})); +class CloseRequestedEvent { + constructor(event) { + this._preventDefault = false; + this.event = event.event; + this.id = event.id; + } + preventDefault() { + this._preventDefault = true; + } + isPreventDefault() { + return this._preventDefault; + } +} +exports.ProgressBarStatus = void 0; +(function (ProgressBarStatus) { + /** + * Hide progress bar. + */ + ProgressBarStatus["None"] = "none"; + /** + * Normal state. + */ + ProgressBarStatus["Normal"] = "normal"; + /** + * Indeterminate state. **Treated as Normal on Linux and macOS** + */ + ProgressBarStatus["Indeterminate"] = "indeterminate"; + /** + * Paused state. **Treated as Normal on Linux** + */ + ProgressBarStatus["Paused"] = "paused"; + /** + * Error state. **Treated as Normal on linux** + */ + ProgressBarStatus["Error"] = "error"; +})(exports.ProgressBarStatus || (exports.ProgressBarStatus = {})); +/** + * Get an instance of `Window` for the current window. + * + * @since 1.0.0 + */ +function getCurrentWindow() { + return new Window(window.__TAURI_INTERNALS__.metadata.currentWindow.label, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }); +} +/** + * Gets a list of instances of `Window` for all available windows. + * + * @since 1.0.0 + */ +async function getAllWindows() { + return core.invoke('plugin:window|get_all_windows').then((windows) => windows.map((w) => new Window(w, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }))); +} +/** @ignore */ +// events that are emitted right here instead of by the created window +const localTauriEvents = ['tauri://created', 'tauri://error']; +/** + * Create new window or get a handle to an existing one. + * + * Windows are identified by a *label* a unique identifier that can be used to reference it later. + * It may only contain alphanumeric characters `a-zA-Z` plus the following special characters `-`, `/`, `:` and `_`. + * + * @example + * ```typescript + * import { Window } from "@tauri-apps/api/window" + * + * const appWindow = new Window('theUniqueLabel'); + * + * appWindow.once('tauri://created', function () { + * // window successfully created + * }); + * appWindow.once('tauri://error', function (e) { + * // an error happened creating the window + * }); + * + * // emit an event to the backend + * await appWindow.emit("some-event", "data"); + * // listen to an event from the backend + * const unlisten = await appWindow.listen("event-name", e => {}); + * unlisten(); + * ``` + * + * @since 2.0.0 + */ +class Window { + /** + * Creates a new Window. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window'; + * const appWindow = new Window('my-label'); + * appWindow.once('tauri://created', function () { + * // window successfully created + * }); + * appWindow.once('tauri://error', function (e) { + * // an error happened creating the window + * }); + * ``` + * + * @param label The unique window label. Must be alphanumeric: `a-zA-Z-/:_`. + * @returns The {@link Window} instance to communicate with the window. + */ + constructor(label, options = {}) { + var _a; + this.label = label; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.listeners = Object.create(null); + // @ts-expect-error `skip` is not a public API so it is not defined in WindowOptions + if (!(options === null || options === void 0 ? void 0 : options.skip)) { + core.invoke('plugin:window|create', { + options: { + ...options, + parent: typeof options.parent === 'string' + ? options.parent + : (_a = options.parent) === null || _a === void 0 ? void 0 : _a.label, + label + } + }) + .then(async () => this.emit('tauri://created')) + .catch(async (e) => this.emit('tauri://error', e)); + } + } + /** + * Gets the Window associated with the given label. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window'; + * const mainWindow = Window.getByLabel('main'); + * ``` + * + * @param label The window label. + * @returns The Window instance to communicate with the window or null if the window doesn't exist. + */ + static async getByLabel(label) { + var _a; + return (_a = (await getAllWindows()).find((w) => w.label === label)) !== null && _a !== void 0 ? _a : null; + } + /** + * Get an instance of `Window` for the current window. + */ + static getCurrent() { + return getCurrentWindow(); + } + /** + * Gets a list of instances of `Window` for all available windows. + */ + static async getAll() { + return getAllWindows(); + } + /** + * Gets the focused window. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window'; + * const focusedWindow = Window.getFocusedWindow(); + * ``` + * + * @returns The Window instance or `undefined` if there is not any focused window. + */ + static async getFocusedWindow() { + for (const w of await getAllWindows()) { + if (await w.isFocused()) { + return w; + } + } + return null; + } + /** + * Listen to an emitted event on this window. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const unlisten = await getCurrentWindow().listen('state-changed', (event) => { + * console.log(`Got error: ${payload}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async listen(event$1, handler) { + if (this._handleTauriEvent(event$1, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event$1]; + listeners.splice(listeners.indexOf(handler), 1); + }; + } + return event.listen(event$1, handler, { + target: { kind: 'Window', label: this.label } + }); + } + /** + * Listen to an emitted event on this window only once. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const unlisten = await getCurrentWindow().once('initialized', (event) => { + * console.log(`Window initialized!`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async once(event$1, handler) { + if (this._handleTauriEvent(event$1, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event$1]; + listeners.splice(listeners.indexOf(handler), 1); + }; + } + return event.once(event$1, handler, { + target: { kind: 'Window', label: this.label } + }); + } + /** + * Emits an event to all {@link EventTarget|targets}. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().emit('window-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + async emit(event$1, payload) { + if (localTauriEvents.includes(event$1)) { + // eslint-disable-next-line + for (const handler of this.listeners[event$1] || []) { + handler({ + event: event$1, + id: -1, + payload + }); + } + return; + } + return event.emit(event$1, payload); + } + /** + * Emits an event to all {@link EventTarget|targets} matching the given target. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().emit('main', 'window-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object. + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + async emitTo(target, event$1, payload) { + if (localTauriEvents.includes(event$1)) { + // eslint-disable-next-line security/detect-object-injection + for (const handler of this.listeners[event$1] || []) { + handler({ + event: event$1, + id: -1, + payload + }); + } + return; + } + return event.emitTo(target, event$1, payload); + } + /** @ignore */ + _handleTauriEvent(event, handler) { + if (localTauriEvents.includes(event)) { + if (!(event in this.listeners)) { + // eslint-disable-next-line + this.listeners[event] = [handler]; + } + else { + // eslint-disable-next-line + this.listeners[event].push(handler); + } + return true; + } + return false; + } + // Getters + /** + * The scale factor that can be used to map physical pixels to logical pixels. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const factor = await getCurrentWindow().scaleFactor(); + * ``` + * + * @returns The window's monitor scale factor. + */ + async scaleFactor() { + return core.invoke('plugin:window|scale_factor', { + label: this.label + }); + } + /** + * The position of the top-left hand corner of the window's client area relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const position = await getCurrentWindow().innerPosition(); + * ``` + * + * @returns The window's inner position. + */ + async innerPosition() { + return core.invoke('plugin:window|inner_position', { + label: this.label + }).then((p) => new dpi.PhysicalPosition(p)); + } + /** + * The position of the top-left hand corner of the window relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const position = await getCurrentWindow().outerPosition(); + * ``` + * + * @returns The window's outer position. + */ + async outerPosition() { + return core.invoke('plugin:window|outer_position', { + label: this.label + }).then((p) => new dpi.PhysicalPosition(p)); + } + /** + * The physical size of the window's client area. + * The client area is the content of the window, excluding the title bar and borders. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const size = await getCurrentWindow().innerSize(); + * ``` + * + * @returns The window's inner size. + */ + async innerSize() { + return core.invoke('plugin:window|inner_size', { + label: this.label + }).then((s) => new dpi.PhysicalSize(s)); + } + /** + * The physical size of the entire window. + * These dimensions include the title bar and borders. If you don't want that (and you usually don't), use inner_size instead. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const size = await getCurrentWindow().outerSize(); + * ``` + * + * @returns The window's outer size. + */ + async outerSize() { + return core.invoke('plugin:window|outer_size', { + label: this.label + }).then((s) => new dpi.PhysicalSize(s)); + } + /** + * Gets the window's current fullscreen state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const fullscreen = await getCurrentWindow().isFullscreen(); + * ``` + * + * @returns Whether the window is in fullscreen mode or not. + */ + async isFullscreen() { + return core.invoke('plugin:window|is_fullscreen', { + label: this.label + }); + } + /** + * Gets the window's current minimized state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const minimized = await getCurrentWindow().isMinimized(); + * ``` + */ + async isMinimized() { + return core.invoke('plugin:window|is_minimized', { + label: this.label + }); + } + /** + * Gets the window's current maximized state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const maximized = await getCurrentWindow().isMaximized(); + * ``` + * + * @returns Whether the window is maximized or not. + */ + async isMaximized() { + return core.invoke('plugin:window|is_maximized', { + label: this.label + }); + } + /** + * Gets the window's current focus state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const focused = await getCurrentWindow().isFocused(); + * ``` + * + * @returns Whether the window is focused or not. + */ + async isFocused() { + return core.invoke('plugin:window|is_focused', { + label: this.label + }); + } + /** + * Gets the window's current decorated state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const decorated = await getCurrentWindow().isDecorated(); + * ``` + * + * @returns Whether the window is decorated or not. + */ + async isDecorated() { + return core.invoke('plugin:window|is_decorated', { + label: this.label + }); + } + /** + * Gets the window's current resizable state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const resizable = await getCurrentWindow().isResizable(); + * ``` + * + * @returns Whether the window is resizable or not. + */ + async isResizable() { + return core.invoke('plugin:window|is_resizable', { + label: this.label + }); + } + /** + * Gets the window's native maximize button state. + * + * #### Platform-specific + * + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const maximizable = await getCurrentWindow().isMaximizable(); + * ``` + * + * @returns Whether the window's native maximize button is enabled or not. + */ + async isMaximizable() { + return core.invoke('plugin:window|is_maximizable', { + label: this.label + }); + } + /** + * Gets the window's native minimize button state. + * + * #### Platform-specific + * + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const minimizable = await getCurrentWindow().isMinimizable(); + * ``` + * + * @returns Whether the window's native minimize button is enabled or not. + */ + async isMinimizable() { + return core.invoke('plugin:window|is_minimizable', { + label: this.label + }); + } + /** + * Gets the window's native close button state. + * + * #### Platform-specific + * + * - **iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const closable = await getCurrentWindow().isClosable(); + * ``` + * + * @returns Whether the window's native close button is enabled or not. + */ + async isClosable() { + return core.invoke('plugin:window|is_closable', { + label: this.label + }); + } + /** + * Gets the window's current visible state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const visible = await getCurrentWindow().isVisible(); + * ``` + * + * @returns Whether the window is visible or not. + */ + async isVisible() { + return core.invoke('plugin:window|is_visible', { + label: this.label + }); + } + /** + * Gets the window's current title. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const title = await getCurrentWindow().title(); + * ``` + */ + async title() { + return core.invoke('plugin:window|title', { + label: this.label + }); + } + /** + * Gets the window's current theme. + * + * #### Platform-specific + * + * - **macOS:** Theme was introduced on macOS 10.14. Returns `light` on macOS 10.13 and below. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const theme = await getCurrentWindow().theme(); + * ``` + * + * @returns The window theme. + */ + async theme() { + return core.invoke('plugin:window|theme', { + label: this.label + }); + } + /** + * Whether the window is configured to be always on top of other windows or not. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const alwaysOnTop = await getCurrentWindow().isAlwaysOnTop(); + * ``` + * + * @returns Whether the window is visible or not. + */ + async isAlwaysOnTop() { + return core.invoke('plugin:window|is_always_on_top', { + label: this.label + }); + } + // Setters + /** + * Centers the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().center(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async center() { + return core.invoke('plugin:window|center', { + label: this.label + }); + } + /** + * Requests user attention to the window, this has no effect if the application + * is already focused. How requesting for user attention manifests is platform dependent, + * see `UserAttentionType` for details. + * + * Providing `null` will unset the request for user attention. Unsetting the request for + * user attention might not be done automatically by the WM when the window receives input. + * + * #### Platform-specific + * + * - **macOS:** `null` has no effect. + * - **Linux:** Urgency levels have the same effect. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().requestUserAttention(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async requestUserAttention(requestType) { + let requestType_ = null; + if (requestType) { + if (requestType === exports.UserAttentionType.Critical) { + requestType_ = { type: 'Critical' }; + } + else { + requestType_ = { type: 'Informational' }; + } + } + return core.invoke('plugin:window|request_user_attention', { + label: this.label, + value: requestType_ + }); + } + /** + * Updates the window resizable flag. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setResizable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setResizable(resizable) { + return core.invoke('plugin:window|set_resizable', { + label: this.label, + value: resizable + }); + } + /** + * Enable or disable the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setEnabled(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ + async setEnabled(enabled) { + return core.invoke('plugin:window|set_enabled', { + label: this.label, + value: enabled + }); + } + /** + * Whether the window is enabled or disabled. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setEnabled(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ + async isEnabled() { + return core.invoke('plugin:window|is_enabled', { + label: this.label + }); + } + /** + * Sets whether the window's native maximize button is enabled or not. + * If resizable is set to false, this setting is ignored. + * + * #### Platform-specific + * + * - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode. + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMaximizable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setMaximizable(maximizable) { + return core.invoke('plugin:window|set_maximizable', { + label: this.label, + value: maximizable + }); + } + /** + * Sets whether the window's native minimize button is enabled or not. + * + * #### Platform-specific + * + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMinimizable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setMinimizable(minimizable) { + return core.invoke('plugin:window|set_minimizable', { + label: this.label, + value: minimizable + }); + } + /** + * Sets whether the window's native close button is enabled or not. + * + * #### Platform-specific + * + * - **Linux:** GTK+ will do its best to convince the window manager not to show a close button. Depending on the system, this function may not have any effect when called on a window that is already visible + * - **iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setClosable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setClosable(closable) { + return core.invoke('plugin:window|set_closable', { + label: this.label, + value: closable + }); + } + /** + * Sets the window title. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setTitle('Tauri'); + * ``` + * + * @param title The new title + * @returns A promise indicating the success or failure of the operation. + */ + async setTitle(title) { + return core.invoke('plugin:window|set_title', { + label: this.label, + value: title + }); + } + /** + * Maximizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().maximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async maximize() { + return core.invoke('plugin:window|maximize', { + label: this.label + }); + } + /** + * Unmaximizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().unmaximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async unmaximize() { + return core.invoke('plugin:window|unmaximize', { + label: this.label + }); + } + /** + * Toggles the window maximized state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().toggleMaximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async toggleMaximize() { + return core.invoke('plugin:window|toggle_maximize', { + label: this.label + }); + } + /** + * Minimizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().minimize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async minimize() { + return core.invoke('plugin:window|minimize', { + label: this.label + }); + } + /** + * Unminimizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().unminimize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async unminimize() { + return core.invoke('plugin:window|unminimize', { + label: this.label + }); + } + /** + * Sets the window visibility to true. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().show(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async show() { + return core.invoke('plugin:window|show', { + label: this.label + }); + } + /** + * Sets the window visibility to false. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().hide(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async hide() { + return core.invoke('plugin:window|hide', { + label: this.label + }); + } + /** + * Closes the window. + * + * Note this emits a closeRequested event so you can intercept it. To force window close, use {@link Window.destroy}. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().close(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async close() { + return core.invoke('plugin:window|close', { + label: this.label + }); + } + /** + * Destroys the window. Behaves like {@link Window.close} but forces the window close instead of emitting a closeRequested event. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().destroy(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async destroy() { + return core.invoke('plugin:window|destroy', { + label: this.label + }); + } + /** + * Whether the window should have borders and bars. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setDecorations(false); + * ``` + * + * @param decorations Whether the window should have borders and bars. + * @returns A promise indicating the success or failure of the operation. + */ + async setDecorations(decorations) { + return core.invoke('plugin:window|set_decorations', { + label: this.label, + value: decorations + }); + } + /** + * Whether or not the window should have shadow. + * + * #### Platform-specific + * + * - **Windows:** + * - `false` has no effect on decorated window, shadows are always ON. + * - `true` will make undecorated window have a 1px white border, + * and on Windows 11, it will have a rounded corners. + * - **Linux:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setShadow(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setShadow(enable) { + return core.invoke('plugin:window|set_shadow', { + label: this.label, + value: enable + }); + } + /** + * Set window effects. + */ + async setEffects(effects) { + return core.invoke('plugin:window|set_effects', { + label: this.label, + value: effects + }); + } + /** + * Clear any applied effects if possible. + */ + async clearEffects() { + return core.invoke('plugin:window|set_effects', { + label: this.label, + value: null + }); + } + /** + * Whether the window should always be on top of other windows. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setAlwaysOnTop(true); + * ``` + * + * @param alwaysOnTop Whether the window should always be on top of other windows or not. + * @returns A promise indicating the success or failure of the operation. + */ + async setAlwaysOnTop(alwaysOnTop) { + return core.invoke('plugin:window|set_always_on_top', { + label: this.label, + value: alwaysOnTop + }); + } + /** + * Whether the window should always be below other windows. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setAlwaysOnBottom(true); + * ``` + * + * @param alwaysOnBottom Whether the window should always be below other windows or not. + * @returns A promise indicating the success or failure of the operation. + */ + async setAlwaysOnBottom(alwaysOnBottom) { + return core.invoke('plugin:window|set_always_on_bottom', { + label: this.label, + value: alwaysOnBottom + }); + } + /** + * Prevents the window contents from being captured by other apps. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setContentProtected(true); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setContentProtected(protected_) { + return core.invoke('plugin:window|set_content_protected', { + label: this.label, + value: protected_ + }); + } + /** + * Resizes the window with a new inner size. + * @example + * ```typescript + * import { getCurrentWindow, LogicalSize } from '@tauri-apps/api/window'; + * await getCurrentWindow().setSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size. + * @returns A promise indicating the success or failure of the operation. + */ + async setSize(size) { + return core.invoke('plugin:window|set_size', { + label: this.label, + value: size instanceof dpi.Size ? size : new dpi.Size(size) + }); + } + /** + * Sets the window minimum inner size. If the `size` argument is not provided, the constraint is unset. + * @example + * ```typescript + * import { getCurrentWindow, PhysicalSize } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMinSize(new PhysicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size, or `null` to unset the constraint. + * @returns A promise indicating the success or failure of the operation. + */ + async setMinSize(size) { + return core.invoke('plugin:window|set_min_size', { + label: this.label, + value: size instanceof dpi.Size ? size : size ? new dpi.Size(size) : null + }); + } + /** + * Sets the window maximum inner size. If the `size` argument is undefined, the constraint is unset. + * @example + * ```typescript + * import { getCurrentWindow, LogicalSize } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMaxSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size, or `null` to unset the constraint. + * @returns A promise indicating the success or failure of the operation. + */ + async setMaxSize(size) { + return core.invoke('plugin:window|set_max_size', { + label: this.label, + value: size instanceof dpi.Size ? size : size ? new dpi.Size(size) : null + }); + } + /** + * Sets the window inner size constraints. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setSizeConstraints({ minWidth: 300 }); + * ``` + * + * @param constraints The logical or physical inner size, or `null` to unset the constraint. + * @returns A promise indicating the success or failure of the operation. + */ + async setSizeConstraints(constraints) { + function logical(pixel) { + return pixel ? { Logical: pixel } : null; + } + return core.invoke('plugin:window|set_size_constraints', { + label: this.label, + value: { + minWidth: logical(constraints === null || constraints === void 0 ? void 0 : constraints.minWidth), + minHeight: logical(constraints === null || constraints === void 0 ? void 0 : constraints.minHeight), + maxWidth: logical(constraints === null || constraints === void 0 ? void 0 : constraints.maxWidth), + maxHeight: logical(constraints === null || constraints === void 0 ? void 0 : constraints.maxHeight) + } + }); + } + /** + * Sets the window outer position. + * @example + * ```typescript + * import { getCurrentWindow, LogicalPosition } from '@tauri-apps/api/window'; + * await getCurrentWindow().setPosition(new LogicalPosition(600, 500)); + * ``` + * + * @param position The new position, in logical or physical pixels. + * @returns A promise indicating the success or failure of the operation. + */ + async setPosition(position) { + return core.invoke('plugin:window|set_position', { + label: this.label, + value: position instanceof dpi.Position ? position : new dpi.Position(position) + }); + } + /** + * Sets the window fullscreen state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setFullscreen(true); + * ``` + * + * @param fullscreen Whether the window should go to fullscreen or not. + * @returns A promise indicating the success or failure of the operation. + */ + async setFullscreen(fullscreen) { + return core.invoke('plugin:window|set_fullscreen', { + label: this.label, + value: fullscreen + }); + } + /** + * On macOS, Toggles a fullscreen mode that doesn’t require a new macOS space. Returns a boolean indicating whether the transition was successful (this won’t work if the window was already in the native fullscreen). + * This is how fullscreen used to work on macOS in versions before Lion. And allows the user to have a fullscreen window without using another space or taking control over the entire monitor. + * + * On other platforms, this is the same as {@link Window.setFullscreen}. + * + * @param fullscreen Whether the window should go to simple fullscreen or not. + * @returns A promise indicating the success or failure of the operation. + */ + async setSimpleFullscreen(fullscreen) { + return core.invoke('plugin:window|set_simple_fullscreen', { + label: this.label, + value: fullscreen + }); + } + /** + * Bring the window to front and focus. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setFocus(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setFocus() { + return core.invoke('plugin:window|set_focus', { + label: this.label + }); + } + /** + * Sets whether the window can be focused. + * + * #### Platform-specific + * + * - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. + * In this case, you might consider calling {@link Window.setFocus} but it will move the window to the back i.e. at the bottom in terms of z-order. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setFocusable(true); + * ``` + * + * @param focusable Whether the window can be focused. + * @returns A promise indicating the success or failure of the operation. + */ + async setFocusable(focusable) { + return core.invoke('plugin:window|set_focusable', { + label: this.label, + value: focusable + }); + } + /** + * Sets the window icon. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setIcon('/tauri/awesome.png'); + * ``` + * + * Note that you may need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + * + * @param icon Icon bytes or path to the icon file. + * @returns A promise indicating the success or failure of the operation. + */ + async setIcon(icon) { + return core.invoke('plugin:window|set_icon', { + label: this.label, + value: image.transformImage(icon) + }); + } + /** + * Whether the window icon should be hidden from the taskbar or not. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setSkipTaskbar(true); + * ``` + * + * @param skip true to hide window icon, false to show it. + * @returns A promise indicating the success or failure of the operation. + */ + async setSkipTaskbar(skip) { + return core.invoke('plugin:window|set_skip_taskbar', { + label: this.label, + value: skip + }); + } + /** + * Grabs the cursor, preventing it from leaving the window. + * + * There's no guarantee that the cursor will be hidden. You should + * hide it by yourself if you want so. + * + * #### Platform-specific + * + * - **Linux:** Unsupported. + * - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorGrab(true); + * ``` + * + * @param grab `true` to grab the cursor icon, `false` to release it. + * @returns A promise indicating the success or failure of the operation. + */ + async setCursorGrab(grab) { + return core.invoke('plugin:window|set_cursor_grab', { + label: this.label, + value: grab + }); + } + /** + * Modifies the cursor's visibility. + * + * #### Platform-specific + * + * - **Windows:** The cursor is only hidden within the confines of the window. + * - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor is + * outside of the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorVisible(false); + * ``` + * + * @param visible If `false`, this will hide the cursor. If `true`, this will show the cursor. + * @returns A promise indicating the success or failure of the operation. + */ + async setCursorVisible(visible) { + return core.invoke('plugin:window|set_cursor_visible', { + label: this.label, + value: visible + }); + } + /** + * Modifies the cursor icon of the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorIcon('help'); + * ``` + * + * @param icon The new cursor icon. + * @returns A promise indicating the success or failure of the operation. + */ + async setCursorIcon(icon) { + return core.invoke('plugin:window|set_cursor_icon', { + label: this.label, + value: icon + }); + } + /** + * Sets the window background color. + * + * #### Platform-specific: + * + * - **Windows:** alpha channel is ignored. + * - **iOS / Android:** Unsupported. + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.1.0 + */ + async setBackgroundColor(color) { + return core.invoke('plugin:window|set_background_color', { color }); + } + /** + * Changes the position of the cursor in window coordinates. + * @example + * ```typescript + * import { getCurrentWindow, LogicalPosition } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorPosition(new LogicalPosition(600, 300)); + * ``` + * + * @param position The new cursor position. + * @returns A promise indicating the success or failure of the operation. + */ + async setCursorPosition(position) { + return core.invoke('plugin:window|set_cursor_position', { + label: this.label, + value: position instanceof dpi.Position ? position : new dpi.Position(position) + }); + } + /** + * Changes the cursor events behavior. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setIgnoreCursorEvents(true); + * ``` + * + * @param ignore `true` to ignore the cursor events; `false` to process them as usual. + * @returns A promise indicating the success or failure of the operation. + */ + async setIgnoreCursorEvents(ignore) { + return core.invoke('plugin:window|set_ignore_cursor_events', { + label: this.label, + value: ignore + }); + } + /** + * Starts dragging the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().startDragging(); + * ``` + * + * @return A promise indicating the success or failure of the operation. + */ + async startDragging() { + return core.invoke('plugin:window|start_dragging', { + label: this.label + }); + } + /** + * Starts resize-dragging the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().startResizeDragging(); + * ``` + * + * @return A promise indicating the success or failure of the operation. + */ + async startResizeDragging(direction) { + return core.invoke('plugin:window|start_resize_dragging', { + label: this.label, + value: direction + }); + } + /** + * Sets the badge count. It is app wide and not specific to this window. + * + * #### Platform-specific + * + * - **Windows**: Unsupported. Use @{linkcode Window.setOverlayIcon} instead. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setBadgeCount(5); + * ``` + * + * @param count The badge count. Use `undefined` to remove the badge. + * @return A promise indicating the success or failure of the operation. + */ + async setBadgeCount(count) { + return core.invoke('plugin:window|set_badge_count', { + label: this.label, + value: count + }); + } + /** + * Sets the badge cont **macOS only**. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setBadgeLabel("Hello"); + * ``` + * + * @param label The badge label. Use `undefined` to remove the badge. + * @return A promise indicating the success or failure of the operation. + */ + async setBadgeLabel(label) { + return core.invoke('plugin:window|set_badge_label', { + label: this.label, + value: label + }); + } + /** + * Sets the overlay icon. **Windows only** + * The overlay icon can be set for every window. + * + * + * Note that you may need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setOverlayIcon("/tauri/awesome.png"); + * ``` + * + * @param icon Icon bytes or path to the icon file. Use `undefined` to remove the overlay icon. + * @return A promise indicating the success or failure of the operation. + */ + async setOverlayIcon(icon) { + return core.invoke('plugin:window|set_overlay_icon', { + label: this.label, + value: icon ? image.transformImage(icon) : undefined + }); + } + /** + * Sets the taskbar progress state. + * + * #### Platform-specific + * + * - **Linux / macOS**: Progress bar is app-wide and not specific to this window. + * - **Linux**: Only supported desktop environments with `libunity` (e.g. GNOME). + * + * @example + * ```typescript + * import { getCurrentWindow, ProgressBarStatus } from '@tauri-apps/api/window'; + * await getCurrentWindow().setProgressBar({ + * status: ProgressBarStatus.Normal, + * progress: 50, + * }); + * ``` + * + * @return A promise indicating the success or failure of the operation. + */ + async setProgressBar(state) { + return core.invoke('plugin:window|set_progress_bar', { + label: this.label, + value: state + }); + } + /** + * Sets whether the window should be visible on all workspaces or virtual desktops. + * + * #### Platform-specific + * + * - **Windows / iOS / Android:** Unsupported. + * + * @since 2.0.0 + */ + async setVisibleOnAllWorkspaces(visible) { + return core.invoke('plugin:window|set_visible_on_all_workspaces', { + label: this.label, + value: visible + }); + } + /** + * Sets the title bar style. **macOS only**. + * + * @since 2.0.0 + */ + async setTitleBarStyle(style) { + return core.invoke('plugin:window|set_title_bar_style', { + label: this.label, + value: style + }); + } + /** + * Set window theme, pass in `null` or `undefined` to follow system theme + * + * #### Platform-specific + * + * - **Linux / macOS**: Theme is app-wide and not specific to this window. + * - **iOS / Android:** Unsupported. + * + * @since 2.0.0 + */ + async setTheme(theme) { + return core.invoke('plugin:window|set_theme', { + label: this.label, + value: theme + }); + } + // Listeners + /** + * Listen to window resize. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onResized(({ payload: size }) => { + * console.log('Window resized', size); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onResized(handler) { + return this.listen(event.TauriEvent.WINDOW_RESIZED, (e) => { + e.payload = new dpi.PhysicalSize(e.payload); + handler(e); + }); + } + /** + * Listen to window move. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onMoved(({ payload: position }) => { + * console.log('Window moved', position); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onMoved(handler) { + return this.listen(event.TauriEvent.WINDOW_MOVED, (e) => { + e.payload = new dpi.PhysicalPosition(e.payload); + handler(e); + }); + } + /** + * Listen to window close requested. Emitted when the user requests to closes the window. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * import { confirm } from '@tauri-apps/api/dialog'; + * const unlisten = await getCurrentWindow().onCloseRequested(async (event) => { + * const confirmed = await confirm('Are you sure?'); + * if (!confirmed) { + * // user did not confirm closing the window; let's prevent it + * event.preventDefault(); + * } + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onCloseRequested(handler) { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + return this.listen(event.TauriEvent.WINDOW_CLOSE_REQUESTED, async (event) => { + const evt = new CloseRequestedEvent(event); + await handler(evt); + if (!evt.isPreventDefault()) { + await this.destroy(); + } + }); + } + /** + * Listen to a file drop event. + * The listener is triggered when the user hovers the selected files on the webview, + * drops the files or cancels the operation. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/webview"; + * const unlisten = await getCurrentWindow().onDragDropEvent((event) => { + * if (event.payload.type === 'over') { + * console.log('User hovering', event.payload.position); + * } else if (event.payload.type === 'drop') { + * console.log('User dropped', event.payload.paths); + * } else { + * console.log('File drop cancelled'); + * } + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onDragDropEvent(handler) { + const unlistenDrag = await this.listen(event.TauriEvent.DRAG_ENTER, (event) => { + handler({ + ...event, + payload: { + type: 'enter', + paths: event.payload.paths, + position: new dpi.PhysicalPosition(event.payload.position) + } + }); + }); + const unlistenDragOver = await this.listen(event.TauriEvent.DRAG_OVER, (event) => { + handler({ + ...event, + payload: { + type: 'over', + position: new dpi.PhysicalPosition(event.payload.position) + } + }); + }); + const unlistenDrop = await this.listen(event.TauriEvent.DRAG_DROP, (event) => { + handler({ + ...event, + payload: { + type: 'drop', + paths: event.payload.paths, + position: new dpi.PhysicalPosition(event.payload.position) + } + }); + }); + const unlistenCancel = await this.listen(event.TauriEvent.DRAG_LEAVE, (event) => { + handler({ ...event, payload: { type: 'leave' } }); + }); + return () => { + unlistenDrag(); + unlistenDrop(); + unlistenDragOver(); + unlistenCancel(); + }; + } + /** + * Listen to window focus change. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onFocusChanged(({ payload: focused }) => { + * console.log('Focus changed, window is focused? ' + focused); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onFocusChanged(handler) { + const unlistenFocus = await this.listen(event.TauriEvent.WINDOW_FOCUS, (event) => { + handler({ ...event, payload: true }); + }); + const unlistenBlur = await this.listen(event.TauriEvent.WINDOW_BLUR, (event) => { + handler({ ...event, payload: false }); + }); + return () => { + unlistenFocus(); + unlistenBlur(); + }; + } + /** + * Listen to window scale change. Emitted when the window's scale factor has changed. + * The following user actions can cause DPI changes: + * - Changing the display's resolution. + * - Changing the display's scale factor (e.g. in Control Panel on Windows). + * - Moving the window to a display with a different scale factor. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onScaleChanged(({ payload }) => { + * console.log('Scale changed', payload.scaleFactor, payload.size); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onScaleChanged(handler) { + return this.listen(event.TauriEvent.WINDOW_SCALE_FACTOR_CHANGED, handler); + } + /** + * Listen to the system theme change. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onThemeChanged(({ payload: theme }) => { + * console.log('New theme: ' + theme); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onThemeChanged(handler) { + return this.listen(event.TauriEvent.WINDOW_THEME_CHANGED, handler); + } +} +/** + * Background throttling policy + * + * @since 2.0.0 + */ +var BackgroundThrottlingPolicy; +(function (BackgroundThrottlingPolicy) { + BackgroundThrottlingPolicy["Disabled"] = "disabled"; + BackgroundThrottlingPolicy["Throttle"] = "throttle"; + BackgroundThrottlingPolicy["Suspend"] = "suspend"; +})(BackgroundThrottlingPolicy || (BackgroundThrottlingPolicy = {})); +/** + * The scrollbar style to use in the webview. + * + * ## Platform-specific + * + * **Windows**: This option must be given the same value for all webviews. + * + * @since 2.8.0 + */ +var ScrollBarStyle; +(function (ScrollBarStyle) { + /** + * The default scrollbar style for the webview. + */ + ScrollBarStyle["Default"] = "default"; + /** + * Fluent UI style overlay scrollbars. **Windows Only** + * + * Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions, + * see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541 + */ + ScrollBarStyle["FluentOverlay"] = "fluentOverlay"; +})(ScrollBarStyle || (ScrollBarStyle = {})); +/** + * Platform-specific window effects + * + * @since 2.0.0 + */ +exports.Effect = void 0; +(function (Effect) { + /** + * A default material appropriate for the view's effectiveAppearance. **macOS 10.14-** + * + * @deprecated since macOS 10.14. You should instead choose an appropriate semantic material. + */ + Effect["AppearanceBased"] = "appearanceBased"; + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + Effect["Light"] = "light"; + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + Effect["Dark"] = "dark"; + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + Effect["MediumLight"] = "mediumLight"; + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + Effect["UltraDark"] = "ultraDark"; + /** + * **macOS 10.10+** + */ + Effect["Titlebar"] = "titlebar"; + /** + * **macOS 10.10+** + */ + Effect["Selection"] = "selection"; + /** + * **macOS 10.11+** + */ + Effect["Menu"] = "menu"; + /** + * **macOS 10.11+** + */ + Effect["Popover"] = "popover"; + /** + * **macOS 10.11+** + */ + Effect["Sidebar"] = "sidebar"; + /** + * **macOS 10.14+** + */ + Effect["HeaderView"] = "headerView"; + /** + * **macOS 10.14+** + */ + Effect["Sheet"] = "sheet"; + /** + * **macOS 10.14+** + */ + Effect["WindowBackground"] = "windowBackground"; + /** + * **macOS 10.14+** + */ + Effect["HudWindow"] = "hudWindow"; + /** + * **macOS 10.14+** + */ + Effect["FullScreenUI"] = "fullScreenUI"; + /** + * **macOS 10.14+** + */ + Effect["Tooltip"] = "tooltip"; + /** + * **macOS 10.14+** + */ + Effect["ContentBackground"] = "contentBackground"; + /** + * **macOS 10.14+** + */ + Effect["UnderWindowBackground"] = "underWindowBackground"; + /** + * **macOS 10.14+** + */ + Effect["UnderPageBackground"] = "underPageBackground"; + /** + * **Windows 11 Only** + */ + Effect["Mica"] = "mica"; + /** + * **Windows 7/10/11(22H1) Only** + * + * #### Notes + * + * This effect has bad performance when resizing/dragging the window on Windows 11 build 22621. + */ + Effect["Blur"] = "blur"; + /** + * **Windows 10/11** + * + * #### Notes + * + * This effect has bad performance when resizing/dragging the window on Windows 10 v1903+ and Windows 11 build 22000. + */ + Effect["Acrylic"] = "acrylic"; + /** + * Tabbed effect that matches the system dark preference **Windows 11 Only** + */ + Effect["Tabbed"] = "tabbed"; + /** + * Tabbed effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only** + */ + Effect["TabbedDark"] = "tabbedDark"; + /** + * Tabbed effect with light mode **Windows 11 Only** + */ + Effect["TabbedLight"] = "tabbedLight"; +})(exports.Effect || (exports.Effect = {})); +/** + * Window effect state **macOS only** + * + * @see https://developer.apple.com/documentation/appkit/nsvisualeffectview/state + * + * @since 2.0.0 + */ +exports.EffectState = void 0; +(function (EffectState) { + /** + * Make window effect state follow the window's active state **macOS only** + */ + EffectState["FollowsWindowActiveState"] = "followsWindowActiveState"; + /** + * Make window effect state always active **macOS only** + */ + EffectState["Active"] = "active"; + /** + * Make window effect state always inactive **macOS only** + */ + EffectState["Inactive"] = "inactive"; +})(exports.EffectState || (exports.EffectState = {})); +function mapMonitor(m) { + return m === null + ? null + : { + name: m.name, + scaleFactor: m.scaleFactor, + position: new dpi.PhysicalPosition(m.position), + size: new dpi.PhysicalSize(m.size), + workArea: { + position: new dpi.PhysicalPosition(m.workArea.position), + size: new dpi.PhysicalSize(m.workArea.size) + } + }; +} +/** + * Returns the monitor on which the window currently resides. + * Returns `null` if current monitor can't be detected. + * @example + * ```typescript + * import { currentMonitor } from '@tauri-apps/api/window'; + * const monitor = await currentMonitor(); + * ``` + * + * @since 1.0.0 + */ +async function currentMonitor() { + return core.invoke('plugin:window|current_monitor').then(mapMonitor); +} +/** + * Returns the primary monitor of the system. + * Returns `null` if it can't identify any monitor as a primary one. + * @example + * ```typescript + * import { primaryMonitor } from '@tauri-apps/api/window'; + * const monitor = await primaryMonitor(); + * ``` + * + * @since 1.0.0 + */ +async function primaryMonitor() { + return core.invoke('plugin:window|primary_monitor').then(mapMonitor); +} +/** + * Returns the monitor that contains the given point. Returns `null` if can't find any. + * @example + * ```typescript + * import { monitorFromPoint } from '@tauri-apps/api/window'; + * const monitor = await monitorFromPoint(100.0, 200.0); + * ``` + * + * @since 1.0.0 + */ +async function monitorFromPoint(x, y) { + return core.invoke('plugin:window|monitor_from_point', { + x, + y + }).then(mapMonitor); +} +/** + * Returns the list of all the monitors available on the system. + * @example + * ```typescript + * import { availableMonitors } from '@tauri-apps/api/window'; + * const monitors = await availableMonitors(); + * ``` + * + * @since 1.0.0 + */ +async function availableMonitors() { + return core.invoke('plugin:window|available_monitors').then((ms) => ms.map(mapMonitor)); +} +/** + * Get the cursor position relative to the top-left hand corner of the desktop. + * + * Note that the top-left hand corner of the desktop is not necessarily the same as the screen. + * If the user uses a desktop with multiple monitors, + * the top-left hand corner of the desktop is the top-left hand corner of the main monitor on Windows and macOS + * or the top-left of the leftmost monitor on X11. + * + * The coordinates can be negative if the top-left hand corner of the window is outside of the visible screen region. + */ +async function cursorPosition() { + return core.invoke('plugin:window|cursor_position').then((v) => new dpi.PhysicalPosition(v)); +} + +exports.LogicalPosition = dpi.LogicalPosition; +exports.LogicalSize = dpi.LogicalSize; +exports.PhysicalPosition = dpi.PhysicalPosition; +exports.PhysicalSize = dpi.PhysicalSize; +exports.CloseRequestedEvent = CloseRequestedEvent; +exports.Window = Window; +exports.availableMonitors = availableMonitors; +exports.currentMonitor = currentMonitor; +exports.cursorPosition = cursorPosition; +exports.getAllWindows = getAllWindows; +exports.getCurrentWindow = getCurrentWindow; +exports.monitorFromPoint = monitorFromPoint; +exports.primaryMonitor = primaryMonitor; diff --git a/node_modules/@tauri-apps/api/window.d.ts b/node_modules/@tauri-apps/api/window.d.ts new file mode 100644 index 0000000..7aa8184 --- /dev/null +++ b/node_modules/@tauri-apps/api/window.d.ts @@ -0,0 +1,1803 @@ +/** + * Provides APIs to create windows, communicate with other windows and manipulate the current window. + * + * #### Window events + * + * Events can be listened to using {@link Window.listen}: + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * getCurrentWindow().listen("my-window-event", ({ event, payload }) => { }); + * ``` + * + * @module + */ +import { LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size } from './dpi'; +import type { Event, EventName, EventCallback, UnlistenFn } from './event'; +import { type EventTarget } from './event'; +import { WebviewWindow } from './webviewWindow'; +import type { DragDropEvent } from './webview'; +import { Image } from './image'; +/** + * Allows you to retrieve information about a given monitor. + * + * @since 1.0.0 + */ +export interface Monitor { + /** Human-readable name of the monitor */ + name: string | null; + /** The monitor's resolution. */ + size: PhysicalSize; + /** the Top-left corner position of the monitor relative to the larger full screen area. */ + position: PhysicalPosition; + /** The monitor's work area. */ + workArea: { + position: PhysicalPosition; + size: PhysicalSize; + }; + /** The scale factor that can be used to map physical pixels to logical pixels. */ + scaleFactor: number; +} +type Theme = 'light' | 'dark'; +type TitleBarStyle = 'visible' | 'transparent' | 'overlay'; +type ResizeDirection = 'East' | 'North' | 'NorthEast' | 'NorthWest' | 'South' | 'SouthEast' | 'SouthWest' | 'West'; +/** + * The payload for the `scaleChange` event. + * + * @since 1.0.2 + */ +interface ScaleFactorChanged { + /** The new window scale factor. */ + scaleFactor: number; + /** The new window size */ + size: PhysicalSize; +} +/** + * Attention type to request on a window. + * + * @since 1.0.0 + */ +declare enum UserAttentionType { + /** + * #### Platform-specific + * - **macOS:** Bounces the dock icon until the application is in focus. + * - **Windows:** Flashes both the window and the taskbar button until the application is in focus. + */ + Critical = 1, + /** + * #### Platform-specific + * - **macOS:** Bounces the dock icon once. + * - **Windows:** Flashes the taskbar button until the application is in focus. + */ + Informational = 2 +} +declare class CloseRequestedEvent { + /** Event name */ + event: EventName; + /** Event identifier used to unlisten */ + id: number; + private _preventDefault; + constructor(event: Event); + preventDefault(): void; + isPreventDefault(): boolean; +} +export type CursorIcon = 'default' | 'crosshair' | 'hand' | 'arrow' | 'move' | 'text' | 'wait' | 'help' | 'progress' | 'notAllowed' | 'contextMenu' | 'cell' | 'verticalText' | 'alias' | 'copy' | 'noDrop' | 'grab' | 'grabbing' | 'allScroll' | 'zoomIn' | 'zoomOut' | 'eResize' | 'nResize' | 'neResize' | 'nwResize' | 'sResize' | 'seResize' | 'swResize' | 'wResize' | 'ewResize' | 'nsResize' | 'neswResize' | 'nwseResize' | 'colResize' | 'rowResize'; +export declare enum ProgressBarStatus { + /** + * Hide progress bar. + */ + None = "none", + /** + * Normal state. + */ + Normal = "normal", + /** + * Indeterminate state. **Treated as Normal on Linux and macOS** + */ + Indeterminate = "indeterminate", + /** + * Paused state. **Treated as Normal on Linux** + */ + Paused = "paused", + /** + * Error state. **Treated as Normal on linux** + */ + Error = "error" +} +export interface WindowSizeConstraints { + minWidth?: number; + minHeight?: number; + maxWidth?: number; + maxHeight?: number; +} +export interface ProgressBarState { + /** + * The progress bar status. + */ + status?: ProgressBarStatus; + /** + * The progress bar progress. This can be a value ranging from `0` to `100` + */ + progress?: number; +} +/** + * Get an instance of `Window` for the current window. + * + * @since 1.0.0 + */ +declare function getCurrentWindow(): Window; +/** + * Gets a list of instances of `Window` for all available windows. + * + * @since 1.0.0 + */ +declare function getAllWindows(): Promise; +/** @ignore */ +export type WindowLabel = string; +/** + * Create new window or get a handle to an existing one. + * + * Windows are identified by a *label* a unique identifier that can be used to reference it later. + * It may only contain alphanumeric characters `a-zA-Z` plus the following special characters `-`, `/`, `:` and `_`. + * + * @example + * ```typescript + * import { Window } from "@tauri-apps/api/window" + * + * const appWindow = new Window('theUniqueLabel'); + * + * appWindow.once('tauri://created', function () { + * // window successfully created + * }); + * appWindow.once('tauri://error', function (e) { + * // an error happened creating the window + * }); + * + * // emit an event to the backend + * await appWindow.emit("some-event", "data"); + * // listen to an event from the backend + * const unlisten = await appWindow.listen("event-name", e => {}); + * unlisten(); + * ``` + * + * @since 2.0.0 + */ +declare class Window { + /** The window label. It is a unique identifier for the window, can be used to reference it later. */ + label: WindowLabel; + /** Local event listeners. */ + listeners: Record>>; + /** + * Creates a new Window. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window'; + * const appWindow = new Window('my-label'); + * appWindow.once('tauri://created', function () { + * // window successfully created + * }); + * appWindow.once('tauri://error', function (e) { + * // an error happened creating the window + * }); + * ``` + * + * @param label The unique window label. Must be alphanumeric: `a-zA-Z-/:_`. + * @returns The {@link Window} instance to communicate with the window. + */ + constructor(label: WindowLabel, options?: WindowOptions); + /** + * Gets the Window associated with the given label. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window'; + * const mainWindow = Window.getByLabel('main'); + * ``` + * + * @param label The window label. + * @returns The Window instance to communicate with the window or null if the window doesn't exist. + */ + static getByLabel(label: string): Promise; + /** + * Get an instance of `Window` for the current window. + */ + static getCurrent(): Window; + /** + * Gets a list of instances of `Window` for all available windows. + */ + static getAll(): Promise; + /** + * Gets the focused window. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window'; + * const focusedWindow = Window.getFocusedWindow(); + * ``` + * + * @returns The Window instance or `undefined` if there is not any focused window. + */ + static getFocusedWindow(): Promise; + /** + * Listen to an emitted event on this window. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const unlisten = await getCurrentWindow().listen('state-changed', (event) => { + * console.log(`Got error: ${payload}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + listen(event: EventName, handler: EventCallback): Promise; + /** + * Listen to an emitted event on this window only once. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const unlisten = await getCurrentWindow().once('initialized', (event) => { + * console.log(`Window initialized!`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + once(event: EventName, handler: EventCallback): Promise; + /** + * Emits an event to all {@link EventTarget|targets}. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().emit('window-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + emit(event: string, payload?: T): Promise; + /** + * Emits an event to all {@link EventTarget|targets} matching the given target. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().emit('main', 'window-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object. + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + emitTo(target: string | EventTarget, event: string, payload?: T): Promise; + /** @ignore */ + _handleTauriEvent(event: string, handler: EventCallback): boolean; + /** + * The scale factor that can be used to map physical pixels to logical pixels. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const factor = await getCurrentWindow().scaleFactor(); + * ``` + * + * @returns The window's monitor scale factor. + */ + scaleFactor(): Promise; + /** + * The position of the top-left hand corner of the window's client area relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const position = await getCurrentWindow().innerPosition(); + * ``` + * + * @returns The window's inner position. + */ + innerPosition(): Promise; + /** + * The position of the top-left hand corner of the window relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const position = await getCurrentWindow().outerPosition(); + * ``` + * + * @returns The window's outer position. + */ + outerPosition(): Promise; + /** + * The physical size of the window's client area. + * The client area is the content of the window, excluding the title bar and borders. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const size = await getCurrentWindow().innerSize(); + * ``` + * + * @returns The window's inner size. + */ + innerSize(): Promise; + /** + * The physical size of the entire window. + * These dimensions include the title bar and borders. If you don't want that (and you usually don't), use inner_size instead. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const size = await getCurrentWindow().outerSize(); + * ``` + * + * @returns The window's outer size. + */ + outerSize(): Promise; + /** + * Gets the window's current fullscreen state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const fullscreen = await getCurrentWindow().isFullscreen(); + * ``` + * + * @returns Whether the window is in fullscreen mode or not. + */ + isFullscreen(): Promise; + /** + * Gets the window's current minimized state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const minimized = await getCurrentWindow().isMinimized(); + * ``` + */ + isMinimized(): Promise; + /** + * Gets the window's current maximized state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const maximized = await getCurrentWindow().isMaximized(); + * ``` + * + * @returns Whether the window is maximized or not. + */ + isMaximized(): Promise; + /** + * Gets the window's current focus state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const focused = await getCurrentWindow().isFocused(); + * ``` + * + * @returns Whether the window is focused or not. + */ + isFocused(): Promise; + /** + * Gets the window's current decorated state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const decorated = await getCurrentWindow().isDecorated(); + * ``` + * + * @returns Whether the window is decorated or not. + */ + isDecorated(): Promise; + /** + * Gets the window's current resizable state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const resizable = await getCurrentWindow().isResizable(); + * ``` + * + * @returns Whether the window is resizable or not. + */ + isResizable(): Promise; + /** + * Gets the window's native maximize button state. + * + * #### Platform-specific + * + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const maximizable = await getCurrentWindow().isMaximizable(); + * ``` + * + * @returns Whether the window's native maximize button is enabled or not. + */ + isMaximizable(): Promise; + /** + * Gets the window's native minimize button state. + * + * #### Platform-specific + * + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const minimizable = await getCurrentWindow().isMinimizable(); + * ``` + * + * @returns Whether the window's native minimize button is enabled or not. + */ + isMinimizable(): Promise; + /** + * Gets the window's native close button state. + * + * #### Platform-specific + * + * - **iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const closable = await getCurrentWindow().isClosable(); + * ``` + * + * @returns Whether the window's native close button is enabled or not. + */ + isClosable(): Promise; + /** + * Gets the window's current visible state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const visible = await getCurrentWindow().isVisible(); + * ``` + * + * @returns Whether the window is visible or not. + */ + isVisible(): Promise; + /** + * Gets the window's current title. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const title = await getCurrentWindow().title(); + * ``` + */ + title(): Promise; + /** + * Gets the window's current theme. + * + * #### Platform-specific + * + * - **macOS:** Theme was introduced on macOS 10.14. Returns `light` on macOS 10.13 and below. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const theme = await getCurrentWindow().theme(); + * ``` + * + * @returns The window theme. + */ + theme(): Promise; + /** + * Whether the window is configured to be always on top of other windows or not. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const alwaysOnTop = await getCurrentWindow().isAlwaysOnTop(); + * ``` + * + * @returns Whether the window is visible or not. + */ + isAlwaysOnTop(): Promise; + /** + * Centers the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().center(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + center(): Promise; + /** + * Requests user attention to the window, this has no effect if the application + * is already focused. How requesting for user attention manifests is platform dependent, + * see `UserAttentionType` for details. + * + * Providing `null` will unset the request for user attention. Unsetting the request for + * user attention might not be done automatically by the WM when the window receives input. + * + * #### Platform-specific + * + * - **macOS:** `null` has no effect. + * - **Linux:** Urgency levels have the same effect. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().requestUserAttention(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + requestUserAttention(requestType: UserAttentionType | null): Promise; + /** + * Updates the window resizable flag. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setResizable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + setResizable(resizable: boolean): Promise; + /** + * Enable or disable the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setEnabled(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ + setEnabled(enabled: boolean): Promise; + /** + * Whether the window is enabled or disabled. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setEnabled(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ + isEnabled(): Promise; + /** + * Sets whether the window's native maximize button is enabled or not. + * If resizable is set to false, this setting is ignored. + * + * #### Platform-specific + * + * - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode. + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMaximizable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + setMaximizable(maximizable: boolean): Promise; + /** + * Sets whether the window's native minimize button is enabled or not. + * + * #### Platform-specific + * + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMinimizable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + setMinimizable(minimizable: boolean): Promise; + /** + * Sets whether the window's native close button is enabled or not. + * + * #### Platform-specific + * + * - **Linux:** GTK+ will do its best to convince the window manager not to show a close button. Depending on the system, this function may not have any effect when called on a window that is already visible + * - **iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setClosable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + setClosable(closable: boolean): Promise; + /** + * Sets the window title. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setTitle('Tauri'); + * ``` + * + * @param title The new title + * @returns A promise indicating the success or failure of the operation. + */ + setTitle(title: string): Promise; + /** + * Maximizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().maximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + maximize(): Promise; + /** + * Unmaximizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().unmaximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + unmaximize(): Promise; + /** + * Toggles the window maximized state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().toggleMaximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + toggleMaximize(): Promise; + /** + * Minimizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().minimize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + minimize(): Promise; + /** + * Unminimizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().unminimize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + unminimize(): Promise; + /** + * Sets the window visibility to true. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().show(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + show(): Promise; + /** + * Sets the window visibility to false. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().hide(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + hide(): Promise; + /** + * Closes the window. + * + * Note this emits a closeRequested event so you can intercept it. To force window close, use {@link Window.destroy}. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().close(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + close(): Promise; + /** + * Destroys the window. Behaves like {@link Window.close} but forces the window close instead of emitting a closeRequested event. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().destroy(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + destroy(): Promise; + /** + * Whether the window should have borders and bars. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setDecorations(false); + * ``` + * + * @param decorations Whether the window should have borders and bars. + * @returns A promise indicating the success or failure of the operation. + */ + setDecorations(decorations: boolean): Promise; + /** + * Whether or not the window should have shadow. + * + * #### Platform-specific + * + * - **Windows:** + * - `false` has no effect on decorated window, shadows are always ON. + * - `true` will make undecorated window have a 1px white border, + * and on Windows 11, it will have a rounded corners. + * - **Linux:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setShadow(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + setShadow(enable: boolean): Promise; + /** + * Set window effects. + */ + setEffects(effects: Effects): Promise; + /** + * Clear any applied effects if possible. + */ + clearEffects(): Promise; + /** + * Whether the window should always be on top of other windows. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setAlwaysOnTop(true); + * ``` + * + * @param alwaysOnTop Whether the window should always be on top of other windows or not. + * @returns A promise indicating the success or failure of the operation. + */ + setAlwaysOnTop(alwaysOnTop: boolean): Promise; + /** + * Whether the window should always be below other windows. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setAlwaysOnBottom(true); + * ``` + * + * @param alwaysOnBottom Whether the window should always be below other windows or not. + * @returns A promise indicating the success or failure of the operation. + */ + setAlwaysOnBottom(alwaysOnBottom: boolean): Promise; + /** + * Prevents the window contents from being captured by other apps. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setContentProtected(true); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + setContentProtected(protected_: boolean): Promise; + /** + * Resizes the window with a new inner size. + * @example + * ```typescript + * import { getCurrentWindow, LogicalSize } from '@tauri-apps/api/window'; + * await getCurrentWindow().setSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size. + * @returns A promise indicating the success or failure of the operation. + */ + setSize(size: LogicalSize | PhysicalSize | Size): Promise; + /** + * Sets the window minimum inner size. If the `size` argument is not provided, the constraint is unset. + * @example + * ```typescript + * import { getCurrentWindow, PhysicalSize } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMinSize(new PhysicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size, or `null` to unset the constraint. + * @returns A promise indicating the success or failure of the operation. + */ + setMinSize(size: LogicalSize | PhysicalSize | Size | null | undefined): Promise; + /** + * Sets the window maximum inner size. If the `size` argument is undefined, the constraint is unset. + * @example + * ```typescript + * import { getCurrentWindow, LogicalSize } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMaxSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size, or `null` to unset the constraint. + * @returns A promise indicating the success or failure of the operation. + */ + setMaxSize(size: LogicalSize | PhysicalSize | Size | null | undefined): Promise; + /** + * Sets the window inner size constraints. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setSizeConstraints({ minWidth: 300 }); + * ``` + * + * @param constraints The logical or physical inner size, or `null` to unset the constraint. + * @returns A promise indicating the success or failure of the operation. + */ + setSizeConstraints(constraints: WindowSizeConstraints | null | undefined): Promise; + /** + * Sets the window outer position. + * @example + * ```typescript + * import { getCurrentWindow, LogicalPosition } from '@tauri-apps/api/window'; + * await getCurrentWindow().setPosition(new LogicalPosition(600, 500)); + * ``` + * + * @param position The new position, in logical or physical pixels. + * @returns A promise indicating the success or failure of the operation. + */ + setPosition(position: LogicalPosition | PhysicalPosition | Position): Promise; + /** + * Sets the window fullscreen state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setFullscreen(true); + * ``` + * + * @param fullscreen Whether the window should go to fullscreen or not. + * @returns A promise indicating the success or failure of the operation. + */ + setFullscreen(fullscreen: boolean): Promise; + /** + * On macOS, Toggles a fullscreen mode that doesn’t require a new macOS space. Returns a boolean indicating whether the transition was successful (this won’t work if the window was already in the native fullscreen). + * This is how fullscreen used to work on macOS in versions before Lion. And allows the user to have a fullscreen window without using another space or taking control over the entire monitor. + * + * On other platforms, this is the same as {@link Window.setFullscreen}. + * + * @param fullscreen Whether the window should go to simple fullscreen or not. + * @returns A promise indicating the success or failure of the operation. + */ + setSimpleFullscreen(fullscreen: boolean): Promise; + /** + * Bring the window to front and focus. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setFocus(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + setFocus(): Promise; + /** + * Sets whether the window can be focused. + * + * #### Platform-specific + * + * - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. + * In this case, you might consider calling {@link Window.setFocus} but it will move the window to the back i.e. at the bottom in terms of z-order. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setFocusable(true); + * ``` + * + * @param focusable Whether the window can be focused. + * @returns A promise indicating the success or failure of the operation. + */ + setFocusable(focusable: boolean): Promise; + /** + * Sets the window icon. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setIcon('/tauri/awesome.png'); + * ``` + * + * Note that you may need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + * + * @param icon Icon bytes or path to the icon file. + * @returns A promise indicating the success or failure of the operation. + */ + setIcon(icon: string | Image | Uint8Array | ArrayBuffer | number[]): Promise; + /** + * Whether the window icon should be hidden from the taskbar or not. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setSkipTaskbar(true); + * ``` + * + * @param skip true to hide window icon, false to show it. + * @returns A promise indicating the success or failure of the operation. + */ + setSkipTaskbar(skip: boolean): Promise; + /** + * Grabs the cursor, preventing it from leaving the window. + * + * There's no guarantee that the cursor will be hidden. You should + * hide it by yourself if you want so. + * + * #### Platform-specific + * + * - **Linux:** Unsupported. + * - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorGrab(true); + * ``` + * + * @param grab `true` to grab the cursor icon, `false` to release it. + * @returns A promise indicating the success or failure of the operation. + */ + setCursorGrab(grab: boolean): Promise; + /** + * Modifies the cursor's visibility. + * + * #### Platform-specific + * + * - **Windows:** The cursor is only hidden within the confines of the window. + * - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor is + * outside of the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorVisible(false); + * ``` + * + * @param visible If `false`, this will hide the cursor. If `true`, this will show the cursor. + * @returns A promise indicating the success or failure of the operation. + */ + setCursorVisible(visible: boolean): Promise; + /** + * Modifies the cursor icon of the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorIcon('help'); + * ``` + * + * @param icon The new cursor icon. + * @returns A promise indicating the success or failure of the operation. + */ + setCursorIcon(icon: CursorIcon): Promise; + /** + * Sets the window background color. + * + * #### Platform-specific: + * + * - **Windows:** alpha channel is ignored. + * - **iOS / Android:** Unsupported. + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.1.0 + */ + setBackgroundColor(color: Color): Promise; + /** + * Changes the position of the cursor in window coordinates. + * @example + * ```typescript + * import { getCurrentWindow, LogicalPosition } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorPosition(new LogicalPosition(600, 300)); + * ``` + * + * @param position The new cursor position. + * @returns A promise indicating the success or failure of the operation. + */ + setCursorPosition(position: LogicalPosition | PhysicalPosition | Position): Promise; + /** + * Changes the cursor events behavior. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setIgnoreCursorEvents(true); + * ``` + * + * @param ignore `true` to ignore the cursor events; `false` to process them as usual. + * @returns A promise indicating the success or failure of the operation. + */ + setIgnoreCursorEvents(ignore: boolean): Promise; + /** + * Starts dragging the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().startDragging(); + * ``` + * + * @return A promise indicating the success or failure of the operation. + */ + startDragging(): Promise; + /** + * Starts resize-dragging the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().startResizeDragging(); + * ``` + * + * @return A promise indicating the success or failure of the operation. + */ + startResizeDragging(direction: ResizeDirection): Promise; + /** + * Sets the badge count. It is app wide and not specific to this window. + * + * #### Platform-specific + * + * - **Windows**: Unsupported. Use @{linkcode Window.setOverlayIcon} instead. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setBadgeCount(5); + * ``` + * + * @param count The badge count. Use `undefined` to remove the badge. + * @return A promise indicating the success or failure of the operation. + */ + setBadgeCount(count?: number): Promise; + /** + * Sets the badge cont **macOS only**. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setBadgeLabel("Hello"); + * ``` + * + * @param label The badge label. Use `undefined` to remove the badge. + * @return A promise indicating the success or failure of the operation. + */ + setBadgeLabel(label?: string): Promise; + /** + * Sets the overlay icon. **Windows only** + * The overlay icon can be set for every window. + * + * + * Note that you may need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setOverlayIcon("/tauri/awesome.png"); + * ``` + * + * @param icon Icon bytes or path to the icon file. Use `undefined` to remove the overlay icon. + * @return A promise indicating the success or failure of the operation. + */ + setOverlayIcon(icon?: string | Image | Uint8Array | ArrayBuffer | number[]): Promise; + /** + * Sets the taskbar progress state. + * + * #### Platform-specific + * + * - **Linux / macOS**: Progress bar is app-wide and not specific to this window. + * - **Linux**: Only supported desktop environments with `libunity` (e.g. GNOME). + * + * @example + * ```typescript + * import { getCurrentWindow, ProgressBarStatus } from '@tauri-apps/api/window'; + * await getCurrentWindow().setProgressBar({ + * status: ProgressBarStatus.Normal, + * progress: 50, + * }); + * ``` + * + * @return A promise indicating the success or failure of the operation. + */ + setProgressBar(state: ProgressBarState): Promise; + /** + * Sets whether the window should be visible on all workspaces or virtual desktops. + * + * #### Platform-specific + * + * - **Windows / iOS / Android:** Unsupported. + * + * @since 2.0.0 + */ + setVisibleOnAllWorkspaces(visible: boolean): Promise; + /** + * Sets the title bar style. **macOS only**. + * + * @since 2.0.0 + */ + setTitleBarStyle(style: TitleBarStyle): Promise; + /** + * Set window theme, pass in `null` or `undefined` to follow system theme + * + * #### Platform-specific + * + * - **Linux / macOS**: Theme is app-wide and not specific to this window. + * - **iOS / Android:** Unsupported. + * + * @since 2.0.0 + */ + setTheme(theme?: Theme | null): Promise; + /** + * Listen to window resize. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onResized(({ payload: size }) => { + * console.log('Window resized', size); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + onResized(handler: EventCallback): Promise; + /** + * Listen to window move. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onMoved(({ payload: position }) => { + * console.log('Window moved', position); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + onMoved(handler: EventCallback): Promise; + /** + * Listen to window close requested. Emitted when the user requests to closes the window. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * import { confirm } from '@tauri-apps/api/dialog'; + * const unlisten = await getCurrentWindow().onCloseRequested(async (event) => { + * const confirmed = await confirm('Are you sure?'); + * if (!confirmed) { + * // user did not confirm closing the window; let's prevent it + * event.preventDefault(); + * } + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + onCloseRequested(handler: (event: CloseRequestedEvent) => void | Promise): Promise; + /** + * Listen to a file drop event. + * The listener is triggered when the user hovers the selected files on the webview, + * drops the files or cancels the operation. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/webview"; + * const unlisten = await getCurrentWindow().onDragDropEvent((event) => { + * if (event.payload.type === 'over') { + * console.log('User hovering', event.payload.position); + * } else if (event.payload.type === 'drop') { + * console.log('User dropped', event.payload.paths); + * } else { + * console.log('File drop cancelled'); + * } + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + onDragDropEvent(handler: EventCallback): Promise; + /** + * Listen to window focus change. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onFocusChanged(({ payload: focused }) => { + * console.log('Focus changed, window is focused? ' + focused); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + onFocusChanged(handler: EventCallback): Promise; + /** + * Listen to window scale change. Emitted when the window's scale factor has changed. + * The following user actions can cause DPI changes: + * - Changing the display's resolution. + * - Changing the display's scale factor (e.g. in Control Panel on Windows). + * - Moving the window to a display with a different scale factor. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onScaleChanged(({ payload }) => { + * console.log('Scale changed', payload.scaleFactor, payload.size); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + onScaleChanged(handler: EventCallback): Promise; + /** + * Listen to the system theme change. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onThemeChanged(({ payload: theme }) => { + * console.log('New theme: ' + theme); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + onThemeChanged(handler: EventCallback): Promise; +} +/** + * An RGBA color. Each value has minimum of 0 and maximum of 255. + * + * It can be either a string `#ffffff`, an array of 3 or 4 elements or an object. + * + * @since 2.0.0 + */ +type Color = [number, number, number] | [number, number, number, number] | { + red: number; + green: number; + blue: number; + alpha: number; +} | string; +/** + * Background throttling policy + * + * @since 2.0.0 + */ +declare enum BackgroundThrottlingPolicy { + Disabled = "disabled", + Throttle = "throttle", + Suspend = "suspend" +} +/** + * The scrollbar style to use in the webview. + * + * ## Platform-specific + * + * **Windows**: This option must be given the same value for all webviews. + * + * @since 2.8.0 + */ +declare enum ScrollBarStyle { + /** + * The default scrollbar style for the webview. + */ + Default = "default", + /** + * Fluent UI style overlay scrollbars. **Windows Only** + * + * Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions, + * see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541 + */ + FluentOverlay = "fluentOverlay" +} +/** + * Platform-specific window effects + * + * @since 2.0.0 + */ +declare enum Effect { + /** + * A default material appropriate for the view's effectiveAppearance. **macOS 10.14-** + * + * @deprecated since macOS 10.14. You should instead choose an appropriate semantic material. + */ + AppearanceBased = "appearanceBased", + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + Light = "light", + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + Dark = "dark", + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + MediumLight = "mediumLight", + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + UltraDark = "ultraDark", + /** + * **macOS 10.10+** + */ + Titlebar = "titlebar", + /** + * **macOS 10.10+** + */ + Selection = "selection", + /** + * **macOS 10.11+** + */ + Menu = "menu", + /** + * **macOS 10.11+** + */ + Popover = "popover", + /** + * **macOS 10.11+** + */ + Sidebar = "sidebar", + /** + * **macOS 10.14+** + */ + HeaderView = "headerView", + /** + * **macOS 10.14+** + */ + Sheet = "sheet", + /** + * **macOS 10.14+** + */ + WindowBackground = "windowBackground", + /** + * **macOS 10.14+** + */ + HudWindow = "hudWindow", + /** + * **macOS 10.14+** + */ + FullScreenUI = "fullScreenUI", + /** + * **macOS 10.14+** + */ + Tooltip = "tooltip", + /** + * **macOS 10.14+** + */ + ContentBackground = "contentBackground", + /** + * **macOS 10.14+** + */ + UnderWindowBackground = "underWindowBackground", + /** + * **macOS 10.14+** + */ + UnderPageBackground = "underPageBackground", + /** + * **Windows 11 Only** + */ + Mica = "mica", + /** + * **Windows 7/10/11(22H1) Only** + * + * #### Notes + * + * This effect has bad performance when resizing/dragging the window on Windows 11 build 22621. + */ + Blur = "blur", + /** + * **Windows 10/11** + * + * #### Notes + * + * This effect has bad performance when resizing/dragging the window on Windows 10 v1903+ and Windows 11 build 22000. + */ + Acrylic = "acrylic", + /** + * Tabbed effect that matches the system dark preference **Windows 11 Only** + */ + Tabbed = "tabbed", + /** + * Tabbed effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only** + */ + TabbedDark = "tabbedDark", + /** + * Tabbed effect with light mode **Windows 11 Only** + */ + TabbedLight = "tabbedLight" +} +/** + * Window effect state **macOS only** + * + * @see https://developer.apple.com/documentation/appkit/nsvisualeffectview/state + * + * @since 2.0.0 + */ +declare enum EffectState { + /** + * Make window effect state follow the window's active state **macOS only** + */ + FollowsWindowActiveState = "followsWindowActiveState", + /** + * Make window effect state always active **macOS only** + */ + Active = "active", + /** + * Make window effect state always inactive **macOS only** + */ + Inactive = "inactive" +} +/** The window effects configuration object + * + * @since 2.0.0 + */ +interface Effects { + /** + * List of Window effects to apply to the Window. + * Conflicting effects will apply the first one and ignore the rest. + */ + effects: Effect[]; + /** + * Window effect state **macOS Only** + */ + state?: EffectState; + /** + * Window effect corner radius **macOS Only** + */ + radius?: number; + /** + * Window effect color. Affects {@link Effect.Blur} and {@link Effect.Acrylic} only + * on Windows 10 v1903+. Doesn't have any effect on Windows 7 or Windows 11. + */ + color?: Color; +} +/** + * Minimum margin to work area + */ +interface PreventOverflowMargin { + width: number; + height: number; +} +/** + * Configuration for the window to create. + * + * @since 1.0.0 + */ +interface WindowOptions { + /** Show window in the center of the screen.. */ + center?: boolean; + /** The initial vertical position in logical pixels. Only applies if `y` is also set. */ + x?: number; + /** The initial horizontal position in logical pixels. Only applies if `x` is also set. */ + y?: number; + /** The initial width in logical pixels. */ + width?: number; + /** The initial height in logical pixels. */ + height?: number; + /** The minimum width in logical pixels. Only applies if `minHeight` is also set. */ + minWidth?: number; + /** The minimum height in logical pixels. Only applies if `minWidth` is also set. */ + minHeight?: number; + /** The maximum width in logical pixels. Only applies if `maxHeight` is also set. */ + maxWidth?: number; + /** The maximum height in logical pixels. Only applies if `maxWidth` is also set. */ + maxHeight?: number; + /** + * Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) + * on creation, which means the window size will be limited to `monitor size - taskbar size` + * + * Can either be set to `true` or to a {@link PreventOverflowMargin} object to set an additional margin + * that should be considered to determine the working area + * (in this case the window size will be limited to `monitor size - taskbar size - margin`) + * + * **NOTE**: The overflow check is only performed on window creation, resizes can still overflow + * + * #### Platform-specific + * + * - **iOS / Android:** Unsupported. + */ + preventOverflow?: boolean | PreventOverflowMargin; + /** Whether the window is resizable or not. */ + resizable?: boolean; + /** Window title. */ + title?: string; + /** Whether the window is in fullscreen mode or not. */ + fullscreen?: boolean; + /** Whether the window will be initially focused or not. */ + focus?: boolean; + /** Whether the window can be focused or not. */ + focusable?: boolean; + /** + * Whether the window is transparent or not. + * Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri.conf.json > app > macOSPrivateApi`. + * WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`. + */ + transparent?: boolean; + /** Whether the window should be maximized upon creation or not. */ + maximized?: boolean; + /** Whether the window should be immediately visible upon creation or not. */ + visible?: boolean; + /** Whether the window should have borders and bars or not. */ + decorations?: boolean; + /** Whether the window should always be on top of other windows or not. */ + alwaysOnTop?: boolean; + /** Whether the window should always be below other windows. */ + alwaysOnBottom?: boolean; + /** Prevents the window contents from being captured by other apps. */ + contentProtected?: boolean; + /** Whether or not the window icon should be added to the taskbar. */ + skipTaskbar?: boolean; + /** + * Whether or not the window has shadow. + * + * #### Platform-specific + * + * - **Windows:** + * - `false` has no effect on decorated window, shadows are always ON. + * - `true` will make undecorated window have a 1px white border, + * and on Windows 11, it will have a rounded corners. + * - **Linux:** Unsupported. + * + * @since 2.0.0 + */ + shadow?: boolean; + /** + * The initial window theme. Defaults to the system theme. + * + * Only implemented on Windows and macOS 10.14+. + */ + theme?: Theme; + /** + * The style of the macOS title bar. + */ + titleBarStyle?: TitleBarStyle; + /** + * The position of the window controls on macOS. + * + * Requires `titleBarStyle: 'overlay'` and `decorations: true`. + * + * @since 2.4.0 + */ + trafficLightPosition?: LogicalPosition; + /** + * If `true`, sets the window title to be hidden on macOS. + */ + hiddenTitle?: boolean; + /** + * Defines the window [tabbing identifier](https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier) on macOS. + * + * Windows with the same tabbing identifier will be grouped together. + * If the tabbing identifier is not set, automatic tabbing will be disabled. + */ + tabbingIdentifier?: string; + /** + * Whether the window's native maximize button is enabled or not. Defaults to `true`. + */ + maximizable?: boolean; + /** + * Whether the window's native minimize button is enabled or not. Defaults to `true`. + */ + minimizable?: boolean; + /** + * Whether the window's native close button is enabled or not. Defaults to `true`. + */ + closable?: boolean; + /** + * Sets a parent to the window to be created. Can be either a {@linkcode Window} or a label of the window. + * + * #### Platform-specific + * + * - **Windows**: This sets the passed parent as an owner window to the window to be created. + * From [MSDN owned windows docs](https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#owned-windows): + * - An owned window is always above its owner in the z-order. + * - The system automatically destroys an owned window when its owner is destroyed. + * - An owned window is hidden when its owner is minimized. + * - **Linux**: This makes the new window transient for parent, see + * - **macOS**: This adds the window as a child of parent, see + */ + parent?: Window | WebviewWindow | string; + /** Whether the window should be visible on all workspaces or virtual desktops. + * + * #### Platform-specific + * + * - **Windows / iOS / Android:** Unsupported. + * + * @since 2.0.0 + */ + visibleOnAllWorkspaces?: boolean; + /** + * Window effects. + * + * Requires the window to be transparent. + * + * #### Platform-specific: + * + * - **Windows**: If using decorations or shadows, you may want to try this workaround + * - **Linux**: Unsupported + */ + windowEffects?: Effects; + /** + * Set the window background color. + * + * #### Platform-specific: + * + * - **Android / iOS:** Unsupported. + * - **Windows**: alpha channel is ignored. + * + * @since 2.1.0 + */ + backgroundColor?: Color; + /** Change the default background throttling behaviour. + * + * ## Platform-specific + * + * - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice. + * - **iOS**: Supported since version 17.0+. + * - **macOS**: Supported since version 14.0+. + * + * see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + * + * @since 2.3.0 + */ + backgroundThrottling?: BackgroundThrottlingPolicy; + /** + * Whether we should disable JavaScript code execution on the webview or not. + */ + javascriptDisabled?: boolean; + /** + * on macOS and iOS there is a link preview on long pressing links, this is enabled by default. + * see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview + */ + allowLinkPreview?: boolean; + /** + * Allows disabling the input accessory view on iOS. + * + * The accessory view is the view that appears above the keyboard when a text input element is focused. + * It usually displays a view with "Done", "Next" buttons. + */ + disableInputAccessoryView?: boolean; + /** + * Specifies the native scrollbar style to use with the webview. + * CSS styles that modify the scrollbar are applied on top of the native appearance configured here. + * + * Defaults to `default`, which is the browser default. + * + * ## Platform-specific + * + * - **Windows**: + * - `fluentOverlay` requires WebView2 Runtime version 125.0.2535.41 or higher, and does nothing + * on older versions. + * - This option must be given the same value for all webviews. + * - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation. + */ + scrollBarStyle?: ScrollBarStyle; +} +/** + * Returns the monitor on which the window currently resides. + * Returns `null` if current monitor can't be detected. + * @example + * ```typescript + * import { currentMonitor } from '@tauri-apps/api/window'; + * const monitor = await currentMonitor(); + * ``` + * + * @since 1.0.0 + */ +declare function currentMonitor(): Promise; +/** + * Returns the primary monitor of the system. + * Returns `null` if it can't identify any monitor as a primary one. + * @example + * ```typescript + * import { primaryMonitor } from '@tauri-apps/api/window'; + * const monitor = await primaryMonitor(); + * ``` + * + * @since 1.0.0 + */ +declare function primaryMonitor(): Promise; +/** + * Returns the monitor that contains the given point. Returns `null` if can't find any. + * @example + * ```typescript + * import { monitorFromPoint } from '@tauri-apps/api/window'; + * const monitor = await monitorFromPoint(100.0, 200.0); + * ``` + * + * @since 1.0.0 + */ +declare function monitorFromPoint(x: number, y: number): Promise; +/** + * Returns the list of all the monitors available on the system. + * @example + * ```typescript + * import { availableMonitors } from '@tauri-apps/api/window'; + * const monitors = await availableMonitors(); + * ``` + * + * @since 1.0.0 + */ +declare function availableMonitors(): Promise; +/** + * Get the cursor position relative to the top-left hand corner of the desktop. + * + * Note that the top-left hand corner of the desktop is not necessarily the same as the screen. + * If the user uses a desktop with multiple monitors, + * the top-left hand corner of the desktop is the top-left hand corner of the main monitor on Windows and macOS + * or the top-left of the leftmost monitor on X11. + * + * The coordinates can be negative if the top-left hand corner of the window is outside of the visible screen region. + */ +declare function cursorPosition(): Promise; +export { Window, CloseRequestedEvent, getCurrentWindow, getAllWindows, LogicalSize, PhysicalSize, LogicalPosition, PhysicalPosition, UserAttentionType, Effect, EffectState, currentMonitor, monitorFromPoint, primaryMonitor, availableMonitors, cursorPosition }; +export type { Effects, Theme, TitleBarStyle, ScaleFactorChanged, WindowOptions, Color, BackgroundThrottlingPolicy, DragDropEvent, ScrollBarStyle }; diff --git a/node_modules/@tauri-apps/api/window.js b/node_modules/@tauri-apps/api/window.js new file mode 100644 index 0000000..d775577 --- /dev/null +++ b/node_modules/@tauri-apps/api/window.js @@ -0,0 +1,2038 @@ +import { PhysicalPosition, PhysicalSize, Size, Position } from './dpi.js'; +export { LogicalPosition, LogicalSize } from './dpi.js'; +import { listen, once, emit, emitTo, TauriEvent } from './event.js'; +import { invoke } from './core.js'; +import { transformImage } from './image.js'; + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +/** + * Provides APIs to create windows, communicate with other windows and manipulate the current window. + * + * #### Window events + * + * Events can be listened to using {@link Window.listen}: + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * getCurrentWindow().listen("my-window-event", ({ event, payload }) => { }); + * ``` + * + * @module + */ +/** + * Attention type to request on a window. + * + * @since 1.0.0 + */ +var UserAttentionType; +(function (UserAttentionType) { + /** + * #### Platform-specific + * - **macOS:** Bounces the dock icon until the application is in focus. + * - **Windows:** Flashes both the window and the taskbar button until the application is in focus. + */ + UserAttentionType[UserAttentionType["Critical"] = 1] = "Critical"; + /** + * #### Platform-specific + * - **macOS:** Bounces the dock icon once. + * - **Windows:** Flashes the taskbar button until the application is in focus. + */ + UserAttentionType[UserAttentionType["Informational"] = 2] = "Informational"; +})(UserAttentionType || (UserAttentionType = {})); +class CloseRequestedEvent { + constructor(event) { + this._preventDefault = false; + this.event = event.event; + this.id = event.id; + } + preventDefault() { + this._preventDefault = true; + } + isPreventDefault() { + return this._preventDefault; + } +} +var ProgressBarStatus; +(function (ProgressBarStatus) { + /** + * Hide progress bar. + */ + ProgressBarStatus["None"] = "none"; + /** + * Normal state. + */ + ProgressBarStatus["Normal"] = "normal"; + /** + * Indeterminate state. **Treated as Normal on Linux and macOS** + */ + ProgressBarStatus["Indeterminate"] = "indeterminate"; + /** + * Paused state. **Treated as Normal on Linux** + */ + ProgressBarStatus["Paused"] = "paused"; + /** + * Error state. **Treated as Normal on linux** + */ + ProgressBarStatus["Error"] = "error"; +})(ProgressBarStatus || (ProgressBarStatus = {})); +/** + * Get an instance of `Window` for the current window. + * + * @since 1.0.0 + */ +function getCurrentWindow() { + return new Window(window.__TAURI_INTERNALS__.metadata.currentWindow.label, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }); +} +/** + * Gets a list of instances of `Window` for all available windows. + * + * @since 1.0.0 + */ +async function getAllWindows() { + return invoke('plugin:window|get_all_windows').then((windows) => windows.map((w) => new Window(w, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }))); +} +/** @ignore */ +// events that are emitted right here instead of by the created window +const localTauriEvents = ['tauri://created', 'tauri://error']; +/** + * Create new window or get a handle to an existing one. + * + * Windows are identified by a *label* a unique identifier that can be used to reference it later. + * It may only contain alphanumeric characters `a-zA-Z` plus the following special characters `-`, `/`, `:` and `_`. + * + * @example + * ```typescript + * import { Window } from "@tauri-apps/api/window" + * + * const appWindow = new Window('theUniqueLabel'); + * + * appWindow.once('tauri://created', function () { + * // window successfully created + * }); + * appWindow.once('tauri://error', function (e) { + * // an error happened creating the window + * }); + * + * // emit an event to the backend + * await appWindow.emit("some-event", "data"); + * // listen to an event from the backend + * const unlisten = await appWindow.listen("event-name", e => {}); + * unlisten(); + * ``` + * + * @since 2.0.0 + */ +class Window { + /** + * Creates a new Window. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window'; + * const appWindow = new Window('my-label'); + * appWindow.once('tauri://created', function () { + * // window successfully created + * }); + * appWindow.once('tauri://error', function (e) { + * // an error happened creating the window + * }); + * ``` + * + * @param label The unique window label. Must be alphanumeric: `a-zA-Z-/:_`. + * @returns The {@link Window} instance to communicate with the window. + */ + constructor(label, options = {}) { + var _a; + this.label = label; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.listeners = Object.create(null); + // @ts-expect-error `skip` is not a public API so it is not defined in WindowOptions + if (!(options === null || options === void 0 ? void 0 : options.skip)) { + invoke('plugin:window|create', { + options: { + ...options, + parent: typeof options.parent === 'string' + ? options.parent + : (_a = options.parent) === null || _a === void 0 ? void 0 : _a.label, + label + } + }) + .then(async () => this.emit('tauri://created')) + .catch(async (e) => this.emit('tauri://error', e)); + } + } + /** + * Gets the Window associated with the given label. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window'; + * const mainWindow = Window.getByLabel('main'); + * ``` + * + * @param label The window label. + * @returns The Window instance to communicate with the window or null if the window doesn't exist. + */ + static async getByLabel(label) { + var _a; + return (_a = (await getAllWindows()).find((w) => w.label === label)) !== null && _a !== void 0 ? _a : null; + } + /** + * Get an instance of `Window` for the current window. + */ + static getCurrent() { + return getCurrentWindow(); + } + /** + * Gets a list of instances of `Window` for all available windows. + */ + static async getAll() { + return getAllWindows(); + } + /** + * Gets the focused window. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window'; + * const focusedWindow = Window.getFocusedWindow(); + * ``` + * + * @returns The Window instance or `undefined` if there is not any focused window. + */ + static async getFocusedWindow() { + for (const w of await getAllWindows()) { + if (await w.isFocused()) { + return w; + } + } + return null; + } + /** + * Listen to an emitted event on this window. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const unlisten = await getCurrentWindow().listen('state-changed', (event) => { + * console.log(`Got error: ${payload}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async listen(event, handler) { + if (this._handleTauriEvent(event, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event]; + listeners.splice(listeners.indexOf(handler), 1); + }; + } + return listen(event, handler, { + target: { kind: 'Window', label: this.label } + }); + } + /** + * Listen to an emitted event on this window only once. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const unlisten = await getCurrentWindow().once('initialized', (event) => { + * console.log(`Window initialized!`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async once(event, handler) { + if (this._handleTauriEvent(event, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event]; + listeners.splice(listeners.indexOf(handler), 1); + }; + } + return once(event, handler, { + target: { kind: 'Window', label: this.label } + }); + } + /** + * Emits an event to all {@link EventTarget|targets}. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().emit('window-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + async emit(event, payload) { + if (localTauriEvents.includes(event)) { + // eslint-disable-next-line + for (const handler of this.listeners[event] || []) { + handler({ + event, + id: -1, + payload + }); + } + return; + } + return emit(event, payload); + } + /** + * Emits an event to all {@link EventTarget|targets} matching the given target. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().emit('main', 'window-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object. + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + async emitTo(target, event, payload) { + if (localTauriEvents.includes(event)) { + // eslint-disable-next-line security/detect-object-injection + for (const handler of this.listeners[event] || []) { + handler({ + event, + id: -1, + payload + }); + } + return; + } + return emitTo(target, event, payload); + } + /** @ignore */ + _handleTauriEvent(event, handler) { + if (localTauriEvents.includes(event)) { + if (!(event in this.listeners)) { + // eslint-disable-next-line + this.listeners[event] = [handler]; + } + else { + // eslint-disable-next-line + this.listeners[event].push(handler); + } + return true; + } + return false; + } + // Getters + /** + * The scale factor that can be used to map physical pixels to logical pixels. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const factor = await getCurrentWindow().scaleFactor(); + * ``` + * + * @returns The window's monitor scale factor. + */ + async scaleFactor() { + return invoke('plugin:window|scale_factor', { + label: this.label + }); + } + /** + * The position of the top-left hand corner of the window's client area relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const position = await getCurrentWindow().innerPosition(); + * ``` + * + * @returns The window's inner position. + */ + async innerPosition() { + return invoke('plugin:window|inner_position', { + label: this.label + }).then((p) => new PhysicalPosition(p)); + } + /** + * The position of the top-left hand corner of the window relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const position = await getCurrentWindow().outerPosition(); + * ``` + * + * @returns The window's outer position. + */ + async outerPosition() { + return invoke('plugin:window|outer_position', { + label: this.label + }).then((p) => new PhysicalPosition(p)); + } + /** + * The physical size of the window's client area. + * The client area is the content of the window, excluding the title bar and borders. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const size = await getCurrentWindow().innerSize(); + * ``` + * + * @returns The window's inner size. + */ + async innerSize() { + return invoke('plugin:window|inner_size', { + label: this.label + }).then((s) => new PhysicalSize(s)); + } + /** + * The physical size of the entire window. + * These dimensions include the title bar and borders. If you don't want that (and you usually don't), use inner_size instead. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const size = await getCurrentWindow().outerSize(); + * ``` + * + * @returns The window's outer size. + */ + async outerSize() { + return invoke('plugin:window|outer_size', { + label: this.label + }).then((s) => new PhysicalSize(s)); + } + /** + * Gets the window's current fullscreen state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const fullscreen = await getCurrentWindow().isFullscreen(); + * ``` + * + * @returns Whether the window is in fullscreen mode or not. + */ + async isFullscreen() { + return invoke('plugin:window|is_fullscreen', { + label: this.label + }); + } + /** + * Gets the window's current minimized state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const minimized = await getCurrentWindow().isMinimized(); + * ``` + */ + async isMinimized() { + return invoke('plugin:window|is_minimized', { + label: this.label + }); + } + /** + * Gets the window's current maximized state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const maximized = await getCurrentWindow().isMaximized(); + * ``` + * + * @returns Whether the window is maximized or not. + */ + async isMaximized() { + return invoke('plugin:window|is_maximized', { + label: this.label + }); + } + /** + * Gets the window's current focus state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const focused = await getCurrentWindow().isFocused(); + * ``` + * + * @returns Whether the window is focused or not. + */ + async isFocused() { + return invoke('plugin:window|is_focused', { + label: this.label + }); + } + /** + * Gets the window's current decorated state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const decorated = await getCurrentWindow().isDecorated(); + * ``` + * + * @returns Whether the window is decorated or not. + */ + async isDecorated() { + return invoke('plugin:window|is_decorated', { + label: this.label + }); + } + /** + * Gets the window's current resizable state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const resizable = await getCurrentWindow().isResizable(); + * ``` + * + * @returns Whether the window is resizable or not. + */ + async isResizable() { + return invoke('plugin:window|is_resizable', { + label: this.label + }); + } + /** + * Gets the window's native maximize button state. + * + * #### Platform-specific + * + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const maximizable = await getCurrentWindow().isMaximizable(); + * ``` + * + * @returns Whether the window's native maximize button is enabled or not. + */ + async isMaximizable() { + return invoke('plugin:window|is_maximizable', { + label: this.label + }); + } + /** + * Gets the window's native minimize button state. + * + * #### Platform-specific + * + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const minimizable = await getCurrentWindow().isMinimizable(); + * ``` + * + * @returns Whether the window's native minimize button is enabled or not. + */ + async isMinimizable() { + return invoke('plugin:window|is_minimizable', { + label: this.label + }); + } + /** + * Gets the window's native close button state. + * + * #### Platform-specific + * + * - **iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const closable = await getCurrentWindow().isClosable(); + * ``` + * + * @returns Whether the window's native close button is enabled or not. + */ + async isClosable() { + return invoke('plugin:window|is_closable', { + label: this.label + }); + } + /** + * Gets the window's current visible state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const visible = await getCurrentWindow().isVisible(); + * ``` + * + * @returns Whether the window is visible or not. + */ + async isVisible() { + return invoke('plugin:window|is_visible', { + label: this.label + }); + } + /** + * Gets the window's current title. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const title = await getCurrentWindow().title(); + * ``` + */ + async title() { + return invoke('plugin:window|title', { + label: this.label + }); + } + /** + * Gets the window's current theme. + * + * #### Platform-specific + * + * - **macOS:** Theme was introduced on macOS 10.14. Returns `light` on macOS 10.13 and below. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const theme = await getCurrentWindow().theme(); + * ``` + * + * @returns The window theme. + */ + async theme() { + return invoke('plugin:window|theme', { + label: this.label + }); + } + /** + * Whether the window is configured to be always on top of other windows or not. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const alwaysOnTop = await getCurrentWindow().isAlwaysOnTop(); + * ``` + * + * @returns Whether the window is visible or not. + */ + async isAlwaysOnTop() { + return invoke('plugin:window|is_always_on_top', { + label: this.label + }); + } + // Setters + /** + * Centers the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().center(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async center() { + return invoke('plugin:window|center', { + label: this.label + }); + } + /** + * Requests user attention to the window, this has no effect if the application + * is already focused. How requesting for user attention manifests is platform dependent, + * see `UserAttentionType` for details. + * + * Providing `null` will unset the request for user attention. Unsetting the request for + * user attention might not be done automatically by the WM when the window receives input. + * + * #### Platform-specific + * + * - **macOS:** `null` has no effect. + * - **Linux:** Urgency levels have the same effect. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().requestUserAttention(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async requestUserAttention(requestType) { + let requestType_ = null; + if (requestType) { + if (requestType === UserAttentionType.Critical) { + requestType_ = { type: 'Critical' }; + } + else { + requestType_ = { type: 'Informational' }; + } + } + return invoke('plugin:window|request_user_attention', { + label: this.label, + value: requestType_ + }); + } + /** + * Updates the window resizable flag. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setResizable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setResizable(resizable) { + return invoke('plugin:window|set_resizable', { + label: this.label, + value: resizable + }); + } + /** + * Enable or disable the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setEnabled(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ + async setEnabled(enabled) { + return invoke('plugin:window|set_enabled', { + label: this.label, + value: enabled + }); + } + /** + * Whether the window is enabled or disabled. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setEnabled(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ + async isEnabled() { + return invoke('plugin:window|is_enabled', { + label: this.label + }); + } + /** + * Sets whether the window's native maximize button is enabled or not. + * If resizable is set to false, this setting is ignored. + * + * #### Platform-specific + * + * - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode. + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMaximizable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setMaximizable(maximizable) { + return invoke('plugin:window|set_maximizable', { + label: this.label, + value: maximizable + }); + } + /** + * Sets whether the window's native minimize button is enabled or not. + * + * #### Platform-specific + * + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMinimizable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setMinimizable(minimizable) { + return invoke('plugin:window|set_minimizable', { + label: this.label, + value: minimizable + }); + } + /** + * Sets whether the window's native close button is enabled or not. + * + * #### Platform-specific + * + * - **Linux:** GTK+ will do its best to convince the window manager not to show a close button. Depending on the system, this function may not have any effect when called on a window that is already visible + * - **iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setClosable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setClosable(closable) { + return invoke('plugin:window|set_closable', { + label: this.label, + value: closable + }); + } + /** + * Sets the window title. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setTitle('Tauri'); + * ``` + * + * @param title The new title + * @returns A promise indicating the success or failure of the operation. + */ + async setTitle(title) { + return invoke('plugin:window|set_title', { + label: this.label, + value: title + }); + } + /** + * Maximizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().maximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async maximize() { + return invoke('plugin:window|maximize', { + label: this.label + }); + } + /** + * Unmaximizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().unmaximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async unmaximize() { + return invoke('plugin:window|unmaximize', { + label: this.label + }); + } + /** + * Toggles the window maximized state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().toggleMaximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async toggleMaximize() { + return invoke('plugin:window|toggle_maximize', { + label: this.label + }); + } + /** + * Minimizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().minimize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async minimize() { + return invoke('plugin:window|minimize', { + label: this.label + }); + } + /** + * Unminimizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().unminimize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async unminimize() { + return invoke('plugin:window|unminimize', { + label: this.label + }); + } + /** + * Sets the window visibility to true. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().show(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async show() { + return invoke('plugin:window|show', { + label: this.label + }); + } + /** + * Sets the window visibility to false. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().hide(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async hide() { + return invoke('plugin:window|hide', { + label: this.label + }); + } + /** + * Closes the window. + * + * Note this emits a closeRequested event so you can intercept it. To force window close, use {@link Window.destroy}. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().close(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async close() { + return invoke('plugin:window|close', { + label: this.label + }); + } + /** + * Destroys the window. Behaves like {@link Window.close} but forces the window close instead of emitting a closeRequested event. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().destroy(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async destroy() { + return invoke('plugin:window|destroy', { + label: this.label + }); + } + /** + * Whether the window should have borders and bars. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setDecorations(false); + * ``` + * + * @param decorations Whether the window should have borders and bars. + * @returns A promise indicating the success or failure of the operation. + */ + async setDecorations(decorations) { + return invoke('plugin:window|set_decorations', { + label: this.label, + value: decorations + }); + } + /** + * Whether or not the window should have shadow. + * + * #### Platform-specific + * + * - **Windows:** + * - `false` has no effect on decorated window, shadows are always ON. + * - `true` will make undecorated window have a 1px white border, + * and on Windows 11, it will have a rounded corners. + * - **Linux:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setShadow(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setShadow(enable) { + return invoke('plugin:window|set_shadow', { + label: this.label, + value: enable + }); + } + /** + * Set window effects. + */ + async setEffects(effects) { + return invoke('plugin:window|set_effects', { + label: this.label, + value: effects + }); + } + /** + * Clear any applied effects if possible. + */ + async clearEffects() { + return invoke('plugin:window|set_effects', { + label: this.label, + value: null + }); + } + /** + * Whether the window should always be on top of other windows. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setAlwaysOnTop(true); + * ``` + * + * @param alwaysOnTop Whether the window should always be on top of other windows or not. + * @returns A promise indicating the success or failure of the operation. + */ + async setAlwaysOnTop(alwaysOnTop) { + return invoke('plugin:window|set_always_on_top', { + label: this.label, + value: alwaysOnTop + }); + } + /** + * Whether the window should always be below other windows. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setAlwaysOnBottom(true); + * ``` + * + * @param alwaysOnBottom Whether the window should always be below other windows or not. + * @returns A promise indicating the success or failure of the operation. + */ + async setAlwaysOnBottom(alwaysOnBottom) { + return invoke('plugin:window|set_always_on_bottom', { + label: this.label, + value: alwaysOnBottom + }); + } + /** + * Prevents the window contents from being captured by other apps. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setContentProtected(true); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setContentProtected(protected_) { + return invoke('plugin:window|set_content_protected', { + label: this.label, + value: protected_ + }); + } + /** + * Resizes the window with a new inner size. + * @example + * ```typescript + * import { getCurrentWindow, LogicalSize } from '@tauri-apps/api/window'; + * await getCurrentWindow().setSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size. + * @returns A promise indicating the success or failure of the operation. + */ + async setSize(size) { + return invoke('plugin:window|set_size', { + label: this.label, + value: size instanceof Size ? size : new Size(size) + }); + } + /** + * Sets the window minimum inner size. If the `size` argument is not provided, the constraint is unset. + * @example + * ```typescript + * import { getCurrentWindow, PhysicalSize } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMinSize(new PhysicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size, or `null` to unset the constraint. + * @returns A promise indicating the success or failure of the operation. + */ + async setMinSize(size) { + return invoke('plugin:window|set_min_size', { + label: this.label, + value: size instanceof Size ? size : size ? new Size(size) : null + }); + } + /** + * Sets the window maximum inner size. If the `size` argument is undefined, the constraint is unset. + * @example + * ```typescript + * import { getCurrentWindow, LogicalSize } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMaxSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size, or `null` to unset the constraint. + * @returns A promise indicating the success or failure of the operation. + */ + async setMaxSize(size) { + return invoke('plugin:window|set_max_size', { + label: this.label, + value: size instanceof Size ? size : size ? new Size(size) : null + }); + } + /** + * Sets the window inner size constraints. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setSizeConstraints({ minWidth: 300 }); + * ``` + * + * @param constraints The logical or physical inner size, or `null` to unset the constraint. + * @returns A promise indicating the success or failure of the operation. + */ + async setSizeConstraints(constraints) { + function logical(pixel) { + return pixel ? { Logical: pixel } : null; + } + return invoke('plugin:window|set_size_constraints', { + label: this.label, + value: { + minWidth: logical(constraints === null || constraints === void 0 ? void 0 : constraints.minWidth), + minHeight: logical(constraints === null || constraints === void 0 ? void 0 : constraints.minHeight), + maxWidth: logical(constraints === null || constraints === void 0 ? void 0 : constraints.maxWidth), + maxHeight: logical(constraints === null || constraints === void 0 ? void 0 : constraints.maxHeight) + } + }); + } + /** + * Sets the window outer position. + * @example + * ```typescript + * import { getCurrentWindow, LogicalPosition } from '@tauri-apps/api/window'; + * await getCurrentWindow().setPosition(new LogicalPosition(600, 500)); + * ``` + * + * @param position The new position, in logical or physical pixels. + * @returns A promise indicating the success or failure of the operation. + */ + async setPosition(position) { + return invoke('plugin:window|set_position', { + label: this.label, + value: position instanceof Position ? position : new Position(position) + }); + } + /** + * Sets the window fullscreen state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setFullscreen(true); + * ``` + * + * @param fullscreen Whether the window should go to fullscreen or not. + * @returns A promise indicating the success or failure of the operation. + */ + async setFullscreen(fullscreen) { + return invoke('plugin:window|set_fullscreen', { + label: this.label, + value: fullscreen + }); + } + /** + * On macOS, Toggles a fullscreen mode that doesn’t require a new macOS space. Returns a boolean indicating whether the transition was successful (this won’t work if the window was already in the native fullscreen). + * This is how fullscreen used to work on macOS in versions before Lion. And allows the user to have a fullscreen window without using another space or taking control over the entire monitor. + * + * On other platforms, this is the same as {@link Window.setFullscreen}. + * + * @param fullscreen Whether the window should go to simple fullscreen or not. + * @returns A promise indicating the success or failure of the operation. + */ + async setSimpleFullscreen(fullscreen) { + return invoke('plugin:window|set_simple_fullscreen', { + label: this.label, + value: fullscreen + }); + } + /** + * Bring the window to front and focus. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setFocus(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setFocus() { + return invoke('plugin:window|set_focus', { + label: this.label + }); + } + /** + * Sets whether the window can be focused. + * + * #### Platform-specific + * + * - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. + * In this case, you might consider calling {@link Window.setFocus} but it will move the window to the back i.e. at the bottom in terms of z-order. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setFocusable(true); + * ``` + * + * @param focusable Whether the window can be focused. + * @returns A promise indicating the success or failure of the operation. + */ + async setFocusable(focusable) { + return invoke('plugin:window|set_focusable', { + label: this.label, + value: focusable + }); + } + /** + * Sets the window icon. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setIcon('/tauri/awesome.png'); + * ``` + * + * Note that you may need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + * + * @param icon Icon bytes or path to the icon file. + * @returns A promise indicating the success or failure of the operation. + */ + async setIcon(icon) { + return invoke('plugin:window|set_icon', { + label: this.label, + value: transformImage(icon) + }); + } + /** + * Whether the window icon should be hidden from the taskbar or not. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setSkipTaskbar(true); + * ``` + * + * @param skip true to hide window icon, false to show it. + * @returns A promise indicating the success or failure of the operation. + */ + async setSkipTaskbar(skip) { + return invoke('plugin:window|set_skip_taskbar', { + label: this.label, + value: skip + }); + } + /** + * Grabs the cursor, preventing it from leaving the window. + * + * There's no guarantee that the cursor will be hidden. You should + * hide it by yourself if you want so. + * + * #### Platform-specific + * + * - **Linux:** Unsupported. + * - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorGrab(true); + * ``` + * + * @param grab `true` to grab the cursor icon, `false` to release it. + * @returns A promise indicating the success or failure of the operation. + */ + async setCursorGrab(grab) { + return invoke('plugin:window|set_cursor_grab', { + label: this.label, + value: grab + }); + } + /** + * Modifies the cursor's visibility. + * + * #### Platform-specific + * + * - **Windows:** The cursor is only hidden within the confines of the window. + * - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor is + * outside of the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorVisible(false); + * ``` + * + * @param visible If `false`, this will hide the cursor. If `true`, this will show the cursor. + * @returns A promise indicating the success or failure of the operation. + */ + async setCursorVisible(visible) { + return invoke('plugin:window|set_cursor_visible', { + label: this.label, + value: visible + }); + } + /** + * Modifies the cursor icon of the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorIcon('help'); + * ``` + * + * @param icon The new cursor icon. + * @returns A promise indicating the success or failure of the operation. + */ + async setCursorIcon(icon) { + return invoke('plugin:window|set_cursor_icon', { + label: this.label, + value: icon + }); + } + /** + * Sets the window background color. + * + * #### Platform-specific: + * + * - **Windows:** alpha channel is ignored. + * - **iOS / Android:** Unsupported. + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.1.0 + */ + async setBackgroundColor(color) { + return invoke('plugin:window|set_background_color', { color }); + } + /** + * Changes the position of the cursor in window coordinates. + * @example + * ```typescript + * import { getCurrentWindow, LogicalPosition } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorPosition(new LogicalPosition(600, 300)); + * ``` + * + * @param position The new cursor position. + * @returns A promise indicating the success or failure of the operation. + */ + async setCursorPosition(position) { + return invoke('plugin:window|set_cursor_position', { + label: this.label, + value: position instanceof Position ? position : new Position(position) + }); + } + /** + * Changes the cursor events behavior. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setIgnoreCursorEvents(true); + * ``` + * + * @param ignore `true` to ignore the cursor events; `false` to process them as usual. + * @returns A promise indicating the success or failure of the operation. + */ + async setIgnoreCursorEvents(ignore) { + return invoke('plugin:window|set_ignore_cursor_events', { + label: this.label, + value: ignore + }); + } + /** + * Starts dragging the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().startDragging(); + * ``` + * + * @return A promise indicating the success or failure of the operation. + */ + async startDragging() { + return invoke('plugin:window|start_dragging', { + label: this.label + }); + } + /** + * Starts resize-dragging the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().startResizeDragging(); + * ``` + * + * @return A promise indicating the success or failure of the operation. + */ + async startResizeDragging(direction) { + return invoke('plugin:window|start_resize_dragging', { + label: this.label, + value: direction + }); + } + /** + * Sets the badge count. It is app wide and not specific to this window. + * + * #### Platform-specific + * + * - **Windows**: Unsupported. Use @{linkcode Window.setOverlayIcon} instead. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setBadgeCount(5); + * ``` + * + * @param count The badge count. Use `undefined` to remove the badge. + * @return A promise indicating the success or failure of the operation. + */ + async setBadgeCount(count) { + return invoke('plugin:window|set_badge_count', { + label: this.label, + value: count + }); + } + /** + * Sets the badge cont **macOS only**. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setBadgeLabel("Hello"); + * ``` + * + * @param label The badge label. Use `undefined` to remove the badge. + * @return A promise indicating the success or failure of the operation. + */ + async setBadgeLabel(label) { + return invoke('plugin:window|set_badge_label', { + label: this.label, + value: label + }); + } + /** + * Sets the overlay icon. **Windows only** + * The overlay icon can be set for every window. + * + * + * Note that you may need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setOverlayIcon("/tauri/awesome.png"); + * ``` + * + * @param icon Icon bytes or path to the icon file. Use `undefined` to remove the overlay icon. + * @return A promise indicating the success or failure of the operation. + */ + async setOverlayIcon(icon) { + return invoke('plugin:window|set_overlay_icon', { + label: this.label, + value: icon ? transformImage(icon) : undefined + }); + } + /** + * Sets the taskbar progress state. + * + * #### Platform-specific + * + * - **Linux / macOS**: Progress bar is app-wide and not specific to this window. + * - **Linux**: Only supported desktop environments with `libunity` (e.g. GNOME). + * + * @example + * ```typescript + * import { getCurrentWindow, ProgressBarStatus } from '@tauri-apps/api/window'; + * await getCurrentWindow().setProgressBar({ + * status: ProgressBarStatus.Normal, + * progress: 50, + * }); + * ``` + * + * @return A promise indicating the success or failure of the operation. + */ + async setProgressBar(state) { + return invoke('plugin:window|set_progress_bar', { + label: this.label, + value: state + }); + } + /** + * Sets whether the window should be visible on all workspaces or virtual desktops. + * + * #### Platform-specific + * + * - **Windows / iOS / Android:** Unsupported. + * + * @since 2.0.0 + */ + async setVisibleOnAllWorkspaces(visible) { + return invoke('plugin:window|set_visible_on_all_workspaces', { + label: this.label, + value: visible + }); + } + /** + * Sets the title bar style. **macOS only**. + * + * @since 2.0.0 + */ + async setTitleBarStyle(style) { + return invoke('plugin:window|set_title_bar_style', { + label: this.label, + value: style + }); + } + /** + * Set window theme, pass in `null` or `undefined` to follow system theme + * + * #### Platform-specific + * + * - **Linux / macOS**: Theme is app-wide and not specific to this window. + * - **iOS / Android:** Unsupported. + * + * @since 2.0.0 + */ + async setTheme(theme) { + return invoke('plugin:window|set_theme', { + label: this.label, + value: theme + }); + } + // Listeners + /** + * Listen to window resize. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onResized(({ payload: size }) => { + * console.log('Window resized', size); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onResized(handler) { + return this.listen(TauriEvent.WINDOW_RESIZED, (e) => { + e.payload = new PhysicalSize(e.payload); + handler(e); + }); + } + /** + * Listen to window move. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onMoved(({ payload: position }) => { + * console.log('Window moved', position); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onMoved(handler) { + return this.listen(TauriEvent.WINDOW_MOVED, (e) => { + e.payload = new PhysicalPosition(e.payload); + handler(e); + }); + } + /** + * Listen to window close requested. Emitted when the user requests to closes the window. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * import { confirm } from '@tauri-apps/api/dialog'; + * const unlisten = await getCurrentWindow().onCloseRequested(async (event) => { + * const confirmed = await confirm('Are you sure?'); + * if (!confirmed) { + * // user did not confirm closing the window; let's prevent it + * event.preventDefault(); + * } + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onCloseRequested(handler) { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + return this.listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async (event) => { + const evt = new CloseRequestedEvent(event); + await handler(evt); + if (!evt.isPreventDefault()) { + await this.destroy(); + } + }); + } + /** + * Listen to a file drop event. + * The listener is triggered when the user hovers the selected files on the webview, + * drops the files or cancels the operation. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/webview"; + * const unlisten = await getCurrentWindow().onDragDropEvent((event) => { + * if (event.payload.type === 'over') { + * console.log('User hovering', event.payload.position); + * } else if (event.payload.type === 'drop') { + * console.log('User dropped', event.payload.paths); + * } else { + * console.log('File drop cancelled'); + * } + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onDragDropEvent(handler) { + const unlistenDrag = await this.listen(TauriEvent.DRAG_ENTER, (event) => { + handler({ + ...event, + payload: { + type: 'enter', + paths: event.payload.paths, + position: new PhysicalPosition(event.payload.position) + } + }); + }); + const unlistenDragOver = await this.listen(TauriEvent.DRAG_OVER, (event) => { + handler({ + ...event, + payload: { + type: 'over', + position: new PhysicalPosition(event.payload.position) + } + }); + }); + const unlistenDrop = await this.listen(TauriEvent.DRAG_DROP, (event) => { + handler({ + ...event, + payload: { + type: 'drop', + paths: event.payload.paths, + position: new PhysicalPosition(event.payload.position) + } + }); + }); + const unlistenCancel = await this.listen(TauriEvent.DRAG_LEAVE, (event) => { + handler({ ...event, payload: { type: 'leave' } }); + }); + return () => { + unlistenDrag(); + unlistenDrop(); + unlistenDragOver(); + unlistenCancel(); + }; + } + /** + * Listen to window focus change. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onFocusChanged(({ payload: focused }) => { + * console.log('Focus changed, window is focused? ' + focused); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onFocusChanged(handler) { + const unlistenFocus = await this.listen(TauriEvent.WINDOW_FOCUS, (event) => { + handler({ ...event, payload: true }); + }); + const unlistenBlur = await this.listen(TauriEvent.WINDOW_BLUR, (event) => { + handler({ ...event, payload: false }); + }); + return () => { + unlistenFocus(); + unlistenBlur(); + }; + } + /** + * Listen to window scale change. Emitted when the window's scale factor has changed. + * The following user actions can cause DPI changes: + * - Changing the display's resolution. + * - Changing the display's scale factor (e.g. in Control Panel on Windows). + * - Moving the window to a display with a different scale factor. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onScaleChanged(({ payload }) => { + * console.log('Scale changed', payload.scaleFactor, payload.size); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onScaleChanged(handler) { + return this.listen(TauriEvent.WINDOW_SCALE_FACTOR_CHANGED, handler); + } + /** + * Listen to the system theme change. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onThemeChanged(({ payload: theme }) => { + * console.log('New theme: ' + theme); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async onThemeChanged(handler) { + return this.listen(TauriEvent.WINDOW_THEME_CHANGED, handler); + } +} +/** + * Background throttling policy + * + * @since 2.0.0 + */ +var BackgroundThrottlingPolicy; +(function (BackgroundThrottlingPolicy) { + BackgroundThrottlingPolicy["Disabled"] = "disabled"; + BackgroundThrottlingPolicy["Throttle"] = "throttle"; + BackgroundThrottlingPolicy["Suspend"] = "suspend"; +})(BackgroundThrottlingPolicy || (BackgroundThrottlingPolicy = {})); +/** + * The scrollbar style to use in the webview. + * + * ## Platform-specific + * + * **Windows**: This option must be given the same value for all webviews. + * + * @since 2.8.0 + */ +var ScrollBarStyle; +(function (ScrollBarStyle) { + /** + * The default scrollbar style for the webview. + */ + ScrollBarStyle["Default"] = "default"; + /** + * Fluent UI style overlay scrollbars. **Windows Only** + * + * Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions, + * see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541 + */ + ScrollBarStyle["FluentOverlay"] = "fluentOverlay"; +})(ScrollBarStyle || (ScrollBarStyle = {})); +/** + * Platform-specific window effects + * + * @since 2.0.0 + */ +var Effect; +(function (Effect) { + /** + * A default material appropriate for the view's effectiveAppearance. **macOS 10.14-** + * + * @deprecated since macOS 10.14. You should instead choose an appropriate semantic material. + */ + Effect["AppearanceBased"] = "appearanceBased"; + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + Effect["Light"] = "light"; + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + Effect["Dark"] = "dark"; + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + Effect["MediumLight"] = "mediumLight"; + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + Effect["UltraDark"] = "ultraDark"; + /** + * **macOS 10.10+** + */ + Effect["Titlebar"] = "titlebar"; + /** + * **macOS 10.10+** + */ + Effect["Selection"] = "selection"; + /** + * **macOS 10.11+** + */ + Effect["Menu"] = "menu"; + /** + * **macOS 10.11+** + */ + Effect["Popover"] = "popover"; + /** + * **macOS 10.11+** + */ + Effect["Sidebar"] = "sidebar"; + /** + * **macOS 10.14+** + */ + Effect["HeaderView"] = "headerView"; + /** + * **macOS 10.14+** + */ + Effect["Sheet"] = "sheet"; + /** + * **macOS 10.14+** + */ + Effect["WindowBackground"] = "windowBackground"; + /** + * **macOS 10.14+** + */ + Effect["HudWindow"] = "hudWindow"; + /** + * **macOS 10.14+** + */ + Effect["FullScreenUI"] = "fullScreenUI"; + /** + * **macOS 10.14+** + */ + Effect["Tooltip"] = "tooltip"; + /** + * **macOS 10.14+** + */ + Effect["ContentBackground"] = "contentBackground"; + /** + * **macOS 10.14+** + */ + Effect["UnderWindowBackground"] = "underWindowBackground"; + /** + * **macOS 10.14+** + */ + Effect["UnderPageBackground"] = "underPageBackground"; + /** + * **Windows 11 Only** + */ + Effect["Mica"] = "mica"; + /** + * **Windows 7/10/11(22H1) Only** + * + * #### Notes + * + * This effect has bad performance when resizing/dragging the window on Windows 11 build 22621. + */ + Effect["Blur"] = "blur"; + /** + * **Windows 10/11** + * + * #### Notes + * + * This effect has bad performance when resizing/dragging the window on Windows 10 v1903+ and Windows 11 build 22000. + */ + Effect["Acrylic"] = "acrylic"; + /** + * Tabbed effect that matches the system dark preference **Windows 11 Only** + */ + Effect["Tabbed"] = "tabbed"; + /** + * Tabbed effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only** + */ + Effect["TabbedDark"] = "tabbedDark"; + /** + * Tabbed effect with light mode **Windows 11 Only** + */ + Effect["TabbedLight"] = "tabbedLight"; +})(Effect || (Effect = {})); +/** + * Window effect state **macOS only** + * + * @see https://developer.apple.com/documentation/appkit/nsvisualeffectview/state + * + * @since 2.0.0 + */ +var EffectState; +(function (EffectState) { + /** + * Make window effect state follow the window's active state **macOS only** + */ + EffectState["FollowsWindowActiveState"] = "followsWindowActiveState"; + /** + * Make window effect state always active **macOS only** + */ + EffectState["Active"] = "active"; + /** + * Make window effect state always inactive **macOS only** + */ + EffectState["Inactive"] = "inactive"; +})(EffectState || (EffectState = {})); +function mapMonitor(m) { + return m === null + ? null + : { + name: m.name, + scaleFactor: m.scaleFactor, + position: new PhysicalPosition(m.position), + size: new PhysicalSize(m.size), + workArea: { + position: new PhysicalPosition(m.workArea.position), + size: new PhysicalSize(m.workArea.size) + } + }; +} +/** + * Returns the monitor on which the window currently resides. + * Returns `null` if current monitor can't be detected. + * @example + * ```typescript + * import { currentMonitor } from '@tauri-apps/api/window'; + * const monitor = await currentMonitor(); + * ``` + * + * @since 1.0.0 + */ +async function currentMonitor() { + return invoke('plugin:window|current_monitor').then(mapMonitor); +} +/** + * Returns the primary monitor of the system. + * Returns `null` if it can't identify any monitor as a primary one. + * @example + * ```typescript + * import { primaryMonitor } from '@tauri-apps/api/window'; + * const monitor = await primaryMonitor(); + * ``` + * + * @since 1.0.0 + */ +async function primaryMonitor() { + return invoke('plugin:window|primary_monitor').then(mapMonitor); +} +/** + * Returns the monitor that contains the given point. Returns `null` if can't find any. + * @example + * ```typescript + * import { monitorFromPoint } from '@tauri-apps/api/window'; + * const monitor = await monitorFromPoint(100.0, 200.0); + * ``` + * + * @since 1.0.0 + */ +async function monitorFromPoint(x, y) { + return invoke('plugin:window|monitor_from_point', { + x, + y + }).then(mapMonitor); +} +/** + * Returns the list of all the monitors available on the system. + * @example + * ```typescript + * import { availableMonitors } from '@tauri-apps/api/window'; + * const monitors = await availableMonitors(); + * ``` + * + * @since 1.0.0 + */ +async function availableMonitors() { + return invoke('plugin:window|available_monitors').then((ms) => ms.map(mapMonitor)); +} +/** + * Get the cursor position relative to the top-left hand corner of the desktop. + * + * Note that the top-left hand corner of the desktop is not necessarily the same as the screen. + * If the user uses a desktop with multiple monitors, + * the top-left hand corner of the desktop is the top-left hand corner of the main monitor on Windows and macOS + * or the top-left of the leftmost monitor on X11. + * + * The coordinates can be negative if the top-left hand corner of the window is outside of the visible screen region. + */ +async function cursorPosition() { + return invoke('plugin:window|cursor_position').then((v) => new PhysicalPosition(v)); +} + +export { CloseRequestedEvent, Effect, EffectState, PhysicalPosition, PhysicalSize, ProgressBarStatus, UserAttentionType, Window, availableMonitors, currentMonitor, cursorPosition, getAllWindows, getCurrentWindow, monitorFromPoint, primaryMonitor }; diff --git a/node_modules/tauri-plugin-mic-recorder-api/LICENSE b/node_modules/tauri-plugin-mic-recorder-api/LICENSE new file mode 100644 index 0000000..18e98d2 --- /dev/null +++ b/node_modules/tauri-plugin-mic-recorder-api/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 ayangweb + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/tauri-plugin-mic-recorder-api/README.md b/node_modules/tauri-plugin-mic-recorder-api/README.md new file mode 100644 index 0000000..9b43d8e --- /dev/null +++ b/node_modules/tauri-plugin-mic-recorder-api/README.md @@ -0,0 +1,95 @@ +# tauri-plugin-mic-recorder + +> This plugin only works on tauri v2, if you need the v1 plugin, feel free to submit a PR! + +Supports recording audio using a microphone and saving the recorded data as a file. + +https://github.com/user-attachments/assets/7c6f1df4-96e6-4cac-806b-098e8bccc1f7 + +## Platform Support + +| Platform | Supported | +| -------- | --------- | +| Windows | ✅ | +| macOS | ✅ | +| Linux | ✅ | +| Android | ✅ | +| iOS | ✅ | + +## Install + +```shell +cargo add tauri-plugin-mic-recorder +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```shell +pnpm add tauri-plugin-mic-recorder-api +``` + +## Usage + +`src-tauri/src/lib.rs` + +```diff +pub fn run() { + tauri::Builder::default() ++ .plugin(tauri_plugin_mic_recorder::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +`src-tauri/capabilities/default.json` + +```diff +{ + ... + "permissions": [ + ... ++ "mic-recorder:default" + ] +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```ts +import { startRecording } from "tauri-plugin-mic-recorder-api"; + +startRecording(); +``` + +## Methods + +| Method | Description | +| ---------------- | ----------------------- | +| `startRecording` | Starts recording audio. | +| `stopRecording` | Stops recording audio. | + +## Example + +```shell +git clone https://github.com/ayangweb/tauri-plugin-mic-recorder.git +``` + +```shell +pnpm install + +pnpm build + +cd examples/tauri-app + +pnpm install + +pnpm dev +``` + +## Thanks + +- Use [cpal](https://github.com/RustAudio/cpal) and [hound](https://github.com/ruuda/hound) record and generate wav files. + +## Who's Use It + +- [Coco AI](https://github.com/infinilabs/coco-app) - Search, Connect, Collaborate, Your Personal AI Search and Assistant, all in one space. diff --git a/node_modules/tauri-plugin-mic-recorder-api/dist-js/index.cjs b/node_modules/tauri-plugin-mic-recorder-api/dist-js/index.cjs new file mode 100644 index 0000000..eafd75c --- /dev/null +++ b/node_modules/tauri-plugin-mic-recorder-api/dist-js/index.cjs @@ -0,0 +1,43 @@ +'use strict'; + +var core = require('@tauri-apps/api/core'); + +const COMMAND = { + START_RECORDING: "plugin:mic-recorder|start_recording", + STOP_RECORDING: "plugin:mic-recorder|stop_recording", +}; +/** + * Starts recording audio. + * + * @example + * ``` + * import { startRecording } from 'tauri-plugin-mic-recorder-api'; + * + * startRecording().then(() => { + * console.log("Recording started"); + * }); + * ``` + */ +const startRecording = () => { + return core.invoke(COMMAND.START_RECORDING); +}; +/** + * Stops recording audio. + * + * @returns Returns the path where the recording file is stored. + * + * @example + * ``` + * import { stopRecording } from 'tauri-plugin-mic-recorder-api'; + * + * const savePath = await stopRecording(); + * console.log("Recording saved at:", savePath); + * ``` + */ +const stopRecording = () => { + return core.invoke(COMMAND.STOP_RECORDING); +}; + +exports.COMMAND = COMMAND; +exports.startRecording = startRecording; +exports.stopRecording = stopRecording; diff --git a/node_modules/tauri-plugin-mic-recorder-api/dist-js/index.d.ts b/node_modules/tauri-plugin-mic-recorder-api/dist-js/index.d.ts new file mode 100644 index 0000000..f775b20 --- /dev/null +++ b/node_modules/tauri-plugin-mic-recorder-api/dist-js/index.d.ts @@ -0,0 +1,31 @@ +export declare const COMMAND: { + START_RECORDING: string; + STOP_RECORDING: string; +}; +/** + * Starts recording audio. + * + * @example + * ``` + * import { startRecording } from 'tauri-plugin-mic-recorder-api'; + * + * startRecording().then(() => { + * console.log("Recording started"); + * }); + * ``` + */ +export declare const startRecording: () => Promise; +/** + * Stops recording audio. + * + * @returns Returns the path where the recording file is stored. + * + * @example + * ``` + * import { stopRecording } from 'tauri-plugin-mic-recorder-api'; + * + * const savePath = await stopRecording(); + * console.log("Recording saved at:", savePath); + * ``` + */ +export declare const stopRecording: () => Promise; diff --git a/node_modules/tauri-plugin-mic-recorder-api/dist-js/index.js b/node_modules/tauri-plugin-mic-recorder-api/dist-js/index.js new file mode 100644 index 0000000..e5575d0 --- /dev/null +++ b/node_modules/tauri-plugin-mic-recorder-api/dist-js/index.js @@ -0,0 +1,39 @@ +import { invoke } from '@tauri-apps/api/core'; + +const COMMAND = { + START_RECORDING: "plugin:mic-recorder|start_recording", + STOP_RECORDING: "plugin:mic-recorder|stop_recording", +}; +/** + * Starts recording audio. + * + * @example + * ``` + * import { startRecording } from 'tauri-plugin-mic-recorder-api'; + * + * startRecording().then(() => { + * console.log("Recording started"); + * }); + * ``` + */ +const startRecording = () => { + return invoke(COMMAND.START_RECORDING); +}; +/** + * Stops recording audio. + * + * @returns Returns the path where the recording file is stored. + * + * @example + * ``` + * import { stopRecording } from 'tauri-plugin-mic-recorder-api'; + * + * const savePath = await stopRecording(); + * console.log("Recording saved at:", savePath); + * ``` + */ +const stopRecording = () => { + return invoke(COMMAND.STOP_RECORDING); +}; + +export { COMMAND, startRecording, stopRecording }; diff --git a/node_modules/tauri-plugin-mic-recorder-api/package.json b/node_modules/tauri-plugin-mic-recorder-api/package.json new file mode 100644 index 0000000..b491187 --- /dev/null +++ b/node_modules/tauri-plugin-mic-recorder-api/package.json @@ -0,0 +1,48 @@ +{ + "name": "tauri-plugin-mic-recorder-api", + "version": "2.0.0", + "author": "ayangweb", + "description": "Supports recording audio using a microphone and saving the recorded data as a file.", + "keywords": [ + "audio-recording", + "microphone-recording", + "tauri", + "tauri-plugin" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/ayangweb/tauri-plugin-mic-recorder.git" + }, + "homepage": "https://github.com/ayangweb/tauri-plugin-mic-recorder#readme", + "bugs": "https://github.com/ayangweb/tauri-plugin-mic-recorder/issues", + "license": "MIT", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "files": [ + "dist-js", + "README.md" + ], + "scripts": { + "build": "rollup -c", + "prepublishOnly": "pnpm build", + "pretest": "pnpm build", + "release": "release-it" + }, + "dependencies": { + "@tauri-apps/api": ">=2.0.0-beta.6" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^11.1.6", + "release-it": "^18.1.2", + "rollup": "^4.9.6", + "tslib": "^2.6.2", + "typescript": "^5.3.3" + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0e746ed --- /dev/null +++ b/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "DevTool-master", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "tauri-plugin-mic-recorder-api": "^2.0.0" + } + }, + "node_modules/@tauri-apps/api": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/tauri-plugin-mic-recorder-api": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tauri-plugin-mic-recorder-api/-/tauri-plugin-mic-recorder-api-2.0.0.tgz", + "integrity": "sha512-04wqYCX4WIlYd6KUY7aS3+W4B5RtnSoVczaQCBSXKpQkEx9XdaaBN05XCee2unxGva0btSXBItFqQSdosnS4jQ==", + "license": "MIT", + "dependencies": { + "@tauri-apps/api": ">=2.0.0-beta.6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..59d0978 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "tauri-plugin-mic-recorder-api": "^2.0.0" + } +} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..8a7d099 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,63 @@ +# Scripts (Python-first, cross-platform) + +This folder now uses Python as the default runtime for orchestration and diagnostics. + +## Preferred scripts + +- `diag.py`: tool probing and install-plan generation (`dotnet`, `python`, `node`, `npm`, `cargo`, `tauri`) +- `build.py`: normalized build actions used by SDT workflows +- `dev_shell.py`: cross-platform shell bootstrap export/doctor helper +- `dotnet-min.py`: resilient `dotnet` wrapper with local cache env +- `pip-min.py`: resilient `pip` wrapper with local cache env and repo-local target default +- `npm-clean.py`: remove `node_modules` cross-platform +- `migration-gate.py`: build/test quality gate +- `nuget-export-cache.py`: archive `.nuget` cache +- `nuget-import-cache.py`: restore `.nuget` cache from archive +- `publish-app.py`: build web or tauri app (cross-platform) +- `publish-sidecar.py`: publish sidecar .NET service +- `publish-webgateway.py`: publish gateway .NET service and optional web assets +- `run-webgateway.py`: run gateway in dev or published-output mode +- `publish-output.py`: orchestrate sidecar/web/gateway/desktop publish steps +- `sync-output.py`: sweep newest build artifacts into `output/` +- `script_common.py`: shared helpers (repo root resolution, env shaping, command runner) + - `project.rootHints` supports glob markers (for example `*.sln`) and directory/file markers (`.git`, `package.json`) + - Windows PATH token expansion (`%NVM_HOME%`, `%NVM_SYMLINK%`, etc.) is applied during command resolution + +## Shell bootstrap wrappers + +- `dev-shell.ps1`: PowerShell wrapper over `dev_shell.py` +- `dev-shell.sh`: bash/zsh wrapper over `dev_shell.py` +- `dev-shell.cmd`: cmd wrapper over `dev_shell.py` + +## Legacy scripts + +Existing `.ps1` entrypoints are now compatibility wrappers that forward to Python scripts. +`script-common.ps1` is legacy-only compatibility and not used by active SDT workflows. + +Original PowerShell implementations are archived under `scripts/legacy/` as `*.legacy.ps1` for reference during transition. + +## Root Hint Semantics + +`project.rootHints` is evaluated in this order: +1. Exact marker exists at candidate root (file or directory) +2. Root-level glob match (`glob`) +3. Recursive glob match (`rglob`) + +Examples: +- `"*.sln"` +- `".git"` +- `"package.json"` +- `"src-tauri/tauri.conf.json"` + +## Quick usage + +```powershell +python scripts/diag.py probe --tool dotnet --json +python scripts/dotnet-min.py build +python scripts/migration-gate.py +python scripts/nuget-export-cache.py --output-zip nuget-cache-export.zip +python scripts/nuget-import-cache.py --input-zip nuget-cache-export.zip +python scripts/npm-clean.py --working-dir . +python scripts/dev_shell.py export --shell pwsh --json +python scripts/dev_shell.py doctor +``` diff --git a/scripts/WORKFLOWS.md b/scripts/WORKFLOWS.md new file mode 100644 index 0000000..67852d2 --- /dev/null +++ b/scripts/WORKFLOWS.md @@ -0,0 +1,57 @@ +# Cross-Platform Script Workflows + +## 1) Probe toolchain availability + +```powershell +python scripts/diag.py probe --tool dotnet --json +python scripts/diag.py probe --tool python --json +python scripts/diag.py probe --tool node --json +python scripts/diag.py probe --tool npm --json +python scripts/diag.py probe --tool cargo --json +python scripts/diag.py probe --tool tauri --json +python scripts/diag.py probe --tool git --json +python scripts/diag.py probe --tool docker --json +``` + +## Shell bootstrap (cross-platform) + +```powershell +python scripts/dev_shell.py export --shell pwsh --json +python scripts/dev_shell.py doctor +``` + +## 2) Build and run SDT + +```powershell +python scripts/dotnet-min.py build +dotnet run --project DevTool.csproj +``` + +## 3) Run migration gate + +```powershell +python scripts/migration-gate.py +``` + +## 4) Manage NuGet cache + +```powershell +python scripts/nuget-export-cache.py --output-zip nuget-cache-export.zip +python scripts/nuget-import-cache.py --input-zip nuget-cache-export.zip +``` + +## 5) Clean Node modules + +```powershell +python scripts/npm-clean.py --working-dir . +``` + +## 6) Build app/gateway bundles + +```powershell +python scripts/publish-app.py --target web +python scripts/publish-sidecar.py --project path/to/sidecar.csproj +python scripts/publish-webgateway.py --project path/to/gateway.csproj --skip-web-assets +python scripts/publish-output.py --dry-run +python scripts/sync-output.py +``` diff --git a/scripts/_pwsh-python-shim.ps1 b/scripts/_pwsh-python-shim.ps1 new file mode 100644 index 0000000..4a16a55 --- /dev/null +++ b/scripts/_pwsh-python-shim.ps1 @@ -0,0 +1,47 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Test-SdtIsWindows { + if (Get-Variable -Name IsWindows -Scope Global -ErrorAction SilentlyContinue) { + return [bool]$global:IsWindows + } + + return $env:OS -eq 'Windows_NT' +} + +function Resolve-SdtPython { + $candidates = @('python') + if (Test-SdtIsWindows) { $candidates += 'py' } else { $candidates += 'python3' } + foreach ($c in $candidates) { + try { + & $c --version *> $null + if ($LASTEXITCODE -eq 0) { return $c } + } catch {} + } + return 'python' +} + +function Resolve-SdtScriptPath { + param([Parameter(Mandatory=$true)][string]$ScriptName) + + $bundled = Join-Path $PSScriptRoot $ScriptName + if (Test-Path $bundled) { return $bundled } + + $project = Join-Path (Join-Path $PSScriptRoot '..') ('scripts\\' + $ScriptName) + if (Test-Path $project) { return (Resolve-Path $project).Path } + + throw "Python helper script not found: $ScriptName" +} + +function Invoke-SdtPythonScript { + param( + [Parameter(Mandatory=$true)][string]$ScriptName, + [string[]]$ForwardArgs = @() + ) + + $python = Resolve-SdtPython + $scriptPath = Resolve-SdtScriptPath -ScriptName $ScriptName + + & $python $scriptPath @ForwardArgs + exit $LASTEXITCODE +} diff --git a/scripts/build.py b/scripts/build.py new file mode 100644 index 0000000..39676ce --- /dev/null +++ b/scripts/build.py @@ -0,0 +1,597 @@ +#!/usr/bin/env python3 +import argparse +import hashlib +import json +import os +import pathlib +import shutil +import subprocess +import sys +import time +from script_common import resolve_command + + +def run_step(command, args, cwd): + resolved = resolve_command(command) + if shutil.which(resolved) is None and not pathlib.Path(resolved).exists(): + return { + "command": resolved, + "args": args, + "cwd": cwd, + "exit_code": 127, + "elapsed_seconds": 0.0, + "status": "failed", + "failure_reason": f"command_not_found:{resolved}", + } + + started = time.time() + proc = subprocess.run([resolved, *args], cwd=cwd, check=False) + elapsed = round(time.time() - started, 3) + return { + "command": resolved, + "args": args, + "cwd": cwd, + "exit_code": proc.returncode, + "elapsed_seconds": elapsed, + "status": "ok" if proc.returncode == 0 else "failed", + "failure_reason": None if proc.returncode == 0 else f"non_zero_exit:{proc.returncode}", + } + + +def resolve_python_executable(): + candidates = ["py", "python"] if os.name == "nt" else ["python3", "python"] + for c in candidates: + if shutil.which(c): + return c + return "python" + + +def parse_common(parser): + parser.add_argument("--project-root", required=True) + parser.add_argument("--working-dir", default=".") + parser.add_argument("--json", action="store_true") + + +def resolve_cwd(project_root, working_dir): + return os.path.abspath(os.path.join(project_root, working_dir)) + + +EXCLUDED_SCAN_DIRS = {".git", "node_modules", "bin", "obj", ".venv", "venv", ".sdt", "dist", "build"} + + +def discover_dotnet_target(project_root: str, cwd: str): + # Prefer local solution first (.slnx, then .sln), then csproj, then bounded scan from project root. + local_slnx = sorted(pathlib.Path(cwd).glob("*.slnx")) + if len(local_slnx) == 1: + return str(local_slnx[0]) + + local_sln = sorted(pathlib.Path(cwd).glob("*.sln")) + if len(local_sln) == 1: + return str(local_sln[0]) + + local_csproj = sorted(pathlib.Path(cwd).glob("*.csproj")) + if len(local_csproj) == 1: + return str(local_csproj[0]) + + slnx_hits = bounded_find_files(project_root, ".slnx", max_depth=4) + if len(slnx_hits) == 1: + return slnx_hits[0] + + sln_hits = bounded_find_files(project_root, ".sln", max_depth=4) + if len(sln_hits) == 1: + return sln_hits[0] + + csproj_hits = bounded_find_files(project_root, ".csproj", max_depth=4) + if len(csproj_hits) == 1: + return csproj_hits[0] + + return None + + +def bounded_find_files(root: str, extension: str, max_depth: int): + root_path = pathlib.Path(root).resolve() + results = [] + for current_root, dirs, files in os.walk(root_path): + rel = pathlib.Path(current_root).resolve().relative_to(root_path) + depth = len(rel.parts) + dirs[:] = [d for d in dirs if d not in EXCLUDED_SCAN_DIRS] + if depth > max_depth: + dirs[:] = [] + continue + + for name in files: + if name.lower().endswith(extension.lower()): + results.append(str(pathlib.Path(current_root) / name)) + return sorted(results) + + +def run_dotnet_action(project_root, working_dir, verb): + cwd = resolve_cwd(project_root, working_dir) + target = discover_dotnet_target(project_root, cwd) + if not target: + return 0, { + "command": "dotnet", + "args": [verb], + "cwd": cwd, + "exit_code": 0, + "elapsed_seconds": 0.0, + "status": "skipped", + "failure_reason": None, + "skip_reason": "not_applicable_no_dotnet_target", + "message": "No .slnx/.sln/.csproj found for this step. Skipping dotnet action.", + } + + args = [verb, target] + step = run_step("dotnet", args, cwd) + step["resolved_target"] = target + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def _deps_hash(app_root): + h = hashlib.sha256() + for name in ("package.json", "package-lock.json"): + p = pathlib.Path(app_root) / name + if p.exists(): + h.update(p.read_bytes()) + return h.hexdigest() + + +def ensure_npm_dependencies(app_root): + package_json = pathlib.Path(app_root) / "package.json" + if not package_json.exists(): + return {"installed": False, "reason": "not_applicable"} + + node_modules = pathlib.Path(app_root) / "node_modules" + deps_hash_file = node_modules / ".sdt-deps.sha256" + expected = _deps_hash(app_root) + + should_install = not node_modules.exists() + if not should_install: + if not deps_hash_file.exists(): + should_install = True + else: + current = deps_hash_file.read_text(encoding="utf-8").strip() + should_install = current != expected + + if not should_install: + return {"installed": False, "reason": "deps_unchanged"} + + lock_exists = (pathlib.Path(app_root) / "package-lock.json").exists() + install_args = ["ci", "--no-audit", "--fund=false"] if lock_exists else ["install", "--no-audit", "--fund=false"] + install_step = run_step("npm", install_args, app_root) + if install_step["exit_code"] != 0: + if lock_exists and install_args[0] == "ci": + fallback = run_step("npm", ["install", "--no-audit", "--fund=false"], app_root) + if fallback["exit_code"] != 0: + fallback["failure_reason"] = "deps_install_failed_after_ci_fallback" + return {"installed": True, "reason": "install_failed", "step": fallback} + install_step = fallback + else: + return {"installed": True, "reason": "install_failed", "step": install_step} + + node_modules.mkdir(parents=True, exist_ok=True) + deps_hash_file.write_text(expected, encoding="utf-8") + return {"installed": True, "reason": "installed", "step": install_step} + + +def read_package_json(cwd: str): + package_json = pathlib.Path(cwd) / "package.json" + if not package_json.exists(): + return None + try: + return json.loads(package_json.read_text(encoding="utf-8")) + except Exception: + return None + + +def has_npm_script(cwd: str, script_name: str) -> bool: + data = read_package_json(cwd) + if not isinstance(data, dict): + return False + scripts = data.get("scripts") + if not isinstance(scripts, dict): + return False + return script_name in scripts and isinstance(scripts.get(script_name), str) + + +def action_dotnet_build(args): + return run_dotnet_action(args.project_root, args.working_dir, "build") + + +def action_dotnet_restore(args): + return run_dotnet_action(args.project_root, args.working_dir, "restore") + + +def action_dotnet_test(args): + return run_dotnet_action(args.project_root, args.working_dir, "test") + + +def action_dotnet_publish(args): + return run_dotnet_action(args.project_root, args.working_dir, "publish") + + +def action_npm_install(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + if not (pathlib.Path(cwd) / "package.json").exists(): + return 0, { + "command": "npm", + "args": ["install"], + "cwd": cwd, + "exit_code": 0, + "elapsed_seconds": 0.0, + "status": "skipped", + "failure_reason": None, + "skip_reason": "not_applicable_no_package_json", + } + step = run_step("npm", ["install"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_npm_ci(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + if not (pathlib.Path(cwd) / "package.json").exists(): + return 0, { + "command": "npm", + "args": ["ci"], + "cwd": cwd, + "exit_code": 0, + "elapsed_seconds": 0.0, + "status": "skipped", + "failure_reason": None, + "skip_reason": "not_applicable_no_package_json", + } + step = run_step("npm", ["ci"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_npm_build(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + if not (pathlib.Path(cwd) / "package.json").exists(): + return 0, { + "command": "npm", + "args": ["run", "build"], + "cwd": cwd, + "exit_code": 0, + "elapsed_seconds": 0.0, + "status": "skipped", + "failure_reason": None, + "skip_reason": "not_applicable_no_package_json", + } + if not has_npm_script(cwd, "build"): + return 0, { + "command": "npm", + "args": ["run", "build"], + "cwd": cwd, + "exit_code": 0, + "elapsed_seconds": 0.0, + "status": "skipped", + "failure_reason": None, + "skip_reason": "not_applicable_missing_build_script", + } + deps = ensure_npm_dependencies(cwd) + if deps.get("reason") == "not_applicable": + return 0, { + "command": "npm", + "args": ["run", "build"], + "cwd": cwd, + "exit_code": 0, + "elapsed_seconds": 0.0, + "status": "skipped", + "failure_reason": None, + "skip_reason": "not_applicable_no_package_json", + } + if deps.get("reason") == "install_failed": + step = deps["step"] + step["failure_reason"] = "deps_install_failed" + return step["exit_code"], step + step = run_step("npm", ["run", "build"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_npm_test(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + if not (pathlib.Path(cwd) / "package.json").exists(): + return 0, { + "command": "npm", + "args": ["test"], + "cwd": cwd, + "exit_code": 0, + "elapsed_seconds": 0.0, + "status": "skipped", + "failure_reason": None, + "skip_reason": "not_applicable_no_package_json", + } + if not has_npm_script(cwd, "test"): + return 0, { + "command": "npm", + "args": ["test"], + "cwd": cwd, + "exit_code": 0, + "elapsed_seconds": 0.0, + "status": "skipped", + "failure_reason": None, + "skip_reason": "not_applicable_missing_test_script", + } + deps = ensure_npm_dependencies(cwd) + if deps.get("reason") == "not_applicable": + return 0, { + "command": "npm", + "args": ["test"], + "cwd": cwd, + "exit_code": 0, + "elapsed_seconds": 0.0, + "status": "skipped", + "failure_reason": None, + "skip_reason": "not_applicable_no_package_json", + } + if deps.get("reason") == "install_failed": + step = deps["step"] + step["failure_reason"] = "deps_install_failed" + return step["exit_code"], step + step = run_step("npm", ["test"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_npm_audit(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + if not (pathlib.Path(cwd) / "package.json").exists(): + return 0, { + "command": "npm", + "args": ["audit"], + "cwd": cwd, + "exit_code": 0, + "elapsed_seconds": 0.0, + "status": "skipped", + "failure_reason": None, + "skip_reason": "not_applicable_no_package_json", + } + step = run_step("npm", ["audit"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_python_venv_create(args): + cwd = resolve_cwd(args.project_root, ".") + venv_dir = args.venv_dir or ".venv" + step = run_step(resolve_python_executable(), ["-m", "venv", venv_dir], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_python_pip_install(args): + cwd = resolve_cwd(args.project_root, ".") + req = args.requirements + step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_python_pip_sync(args): + cwd = resolve_cwd(args.project_root, ".") + req = args.requirements + step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_python_pytest(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + step = run_step(resolve_python_executable(), ["-m", "pytest"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_cargo_build(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + if not (pathlib.Path(cwd) / "Cargo.toml").exists(): + return 0, { + "command": "cargo", + "args": ["build"], + "cwd": cwd, + "exit_code": 0, + "elapsed_seconds": 0.0, + "status": "skipped", + "failure_reason": None, + "skip_reason": "not_applicable_no_cargo_toml", + } + step = run_step("cargo", ["build"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_cargo_test(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + if not (pathlib.Path(cwd) / "Cargo.toml").exists(): + return 0, { + "command": "cargo", + "args": ["test"], + "cwd": cwd, + "exit_code": 0, + "elapsed_seconds": 0.0, + "status": "skipped", + "failure_reason": None, + "skip_reason": "not_applicable_no_cargo_toml", + } + step = run_step("cargo", ["test"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_tauri_build(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + tauri_conf = pathlib.Path(cwd) / "src-tauri" / "tauri.conf.json" + if not tauri_conf.exists(): + tauri_conf = pathlib.Path(cwd) / "tauri.conf.json" + if not tauri_conf.exists() or not (pathlib.Path(cwd) / "package.json").exists(): + return 0, { + "command": "npm", + "args": ["run", "tauri", "build"], + "cwd": cwd, + "exit_code": 0, + "elapsed_seconds": 0.0, + "status": "skipped", + "failure_reason": None, + "skip_reason": "not_applicable_no_tauri_project", + } + + deps = ensure_npm_dependencies(cwd) + if deps.get("reason") == "install_failed": + step = deps["step"] + step["failure_reason"] = "deps_install_failed" + return step["exit_code"], step + + tauri_args = ["run", "tauri", "build"] + if args.no_bundle: + tauri_args.extend(["--", "--no-bundle"]) + step = run_step("npm", tauri_args, cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_git_status(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + step = run_step("git", ["status"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_git_fetch(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + step = run_step("git", ["fetch"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_git_pull(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + step = run_step("git", ["pull"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_git_clean(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + step = run_step("git", ["clean", "-fd"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_docker_build(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + step = run_step("docker", ["build", "."], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_docker_compose_up(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + step = run_step("docker", ["compose", "up", "-d"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def action_docker_compose_down(args): + cwd = resolve_cwd(args.project_root, args.working_dir) + step = run_step("docker", ["compose", "down"], cwd) + return 0 if step["exit_code"] == 0 else step["exit_code"], step + + +def main(): + parser = argparse.ArgumentParser(description="SDT normalized build actions") + sub = parser.add_subparsers(dest="action", required=True) + + p0 = sub.add_parser("dotnet-restore") + parse_common(p0) + + p1 = sub.add_parser("dotnet-build") + parse_common(p1) + + p1b = sub.add_parser("dotnet-test") + parse_common(p1b) + + p1c = sub.add_parser("dotnet-publish") + parse_common(p1c) + + p2 = sub.add_parser("npm-install") + parse_common(p2) + + p2b = sub.add_parser("npm-ci") + parse_common(p2b) + + p3 = sub.add_parser("npm-build") + parse_common(p3) + + p3b = sub.add_parser("npm-test") + parse_common(p3b) + + p3c = sub.add_parser("npm-audit") + parse_common(p3c) + + p4 = sub.add_parser("python-venv-create") + parse_common(p4) + p4.add_argument("--venv-dir", default=".venv") + + p5 = sub.add_parser("python-pip-install") + parse_common(p5) + p5.add_argument("--requirements", required=True) + + p5b = sub.add_parser("python-pip-sync") + parse_common(p5b) + p5b.add_argument("--requirements", required=True) + + p5c = sub.add_parser("python-pytest") + parse_common(p5c) + + p6 = sub.add_parser("cargo-build") + parse_common(p6) + + p6b = sub.add_parser("cargo-test") + parse_common(p6b) + + p7 = sub.add_parser("tauri-build") + parse_common(p7) + p7.add_argument("--no-bundle", action="store_true") + + p8 = sub.add_parser("git-status") + parse_common(p8) + + p9 = sub.add_parser("git-fetch") + parse_common(p9) + + p10 = sub.add_parser("git-pull") + parse_common(p10) + + p11 = sub.add_parser("git-clean") + parse_common(p11) + + p12 = sub.add_parser("docker-build") + parse_common(p12) + + p13 = sub.add_parser("docker-compose-up") + parse_common(p13) + + p14 = sub.add_parser("docker-compose-down") + parse_common(p14) + + args = parser.parse_args() + + handlers = { + "dotnet-restore": action_dotnet_restore, + "dotnet-build": action_dotnet_build, + "dotnet-test": action_dotnet_test, + "dotnet-publish": action_dotnet_publish, + "npm-install": action_npm_install, + "npm-ci": action_npm_ci, + "npm-build": action_npm_build, + "npm-test": action_npm_test, + "npm-audit": action_npm_audit, + "python-venv-create": action_python_venv_create, + "python-pip-install": action_python_pip_install, + "python-pip-sync": action_python_pip_sync, + "python-pytest": action_python_pytest, + "cargo-build": action_cargo_build, + "cargo-test": action_cargo_test, + "tauri-build": action_tauri_build, + "git-status": action_git_status, + "git-fetch": action_git_fetch, + "git-pull": action_git_pull, + "git-clean": action_git_clean, + "docker-build": action_docker_build, + "docker-compose-up": action_docker_compose_up, + "docker-compose-down": action_docker_compose_down, + } + + code, summary = handlers[args.action](args) + if args.json: + print(json.dumps(summary)) + return code + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/dev-shell.cmd b/scripts/dev-shell.cmd new file mode 100644 index 0000000..b1614b7 --- /dev/null +++ b/scripts/dev-shell.cmd @@ -0,0 +1,17 @@ +@echo off +set "SCRIPT_DIR=%~dp0" + +where py >nul 2>nul +if %ERRORLEVEL%==0 ( + set "PYEXE=py" +) else ( + where python >nul 2>nul + if not %ERRORLEVEL%==0 ( + echo python not found. + exit /b 1 + ) + set "PYEXE=python" +) + +for /f "usebackq delims=" %%L in (`"%PYEXE%" "%SCRIPT_DIR%dev_shell.py" export --shell cmd`) do %%L +echo Development shell initialized from Python bootstrap script. diff --git a/scripts/dev-shell.ps1 b/scripts/dev-shell.ps1 new file mode 100644 index 0000000..7f4a3f7 --- /dev/null +++ b/scripts/dev-shell.ps1 @@ -0,0 +1,21 @@ +# Run this in PowerShell before development commands: +# . ./scripts/dev-shell.ps1 + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +. (Join-Path $PSScriptRoot '_pwsh-python-shim.ps1') + +$scriptPath = Resolve-SdtScriptPath -ScriptName 'dev_shell.py' +$python = Resolve-SdtPython + +$lines = & $python $scriptPath export --shell pwsh +if ($LASTEXITCODE -ne 0) { + throw "Failed to initialize development shell via dev_shell.py" +} + +foreach ($line in $lines) { + Invoke-Expression $line +} + +Write-Host "Development shell initialized from Python bootstrap script." diff --git a/scripts/dev-shell.sh b/scripts/dev-shell.sh new file mode 100644 index 0000000..83468f7 --- /dev/null +++ b/scripts/dev-shell.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" + +if command -v python3 >/dev/null 2>&1; then + PYTHON_EXE="python3" +elif command -v python >/dev/null 2>&1; then + PYTHON_EXE="python" +else + echo "python3/python not found." >&2 + exit 1 +fi + +eval "$("$PYTHON_EXE" "$SCRIPT_DIR/dev_shell.py" export --shell bash)" +echo "Development shell initialized from Python bootstrap script." diff --git a/scripts/dev_shell.py b/scripts/dev_shell.py new file mode 100644 index 0000000..1a5d8ea --- /dev/null +++ b/scripts/dev_shell.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +import argparse +import json +import pathlib +import sys + +from script_common import PROXY_VARS, clean_proxy_env, dotnet_env, ensure_dirs, pip_env, resolve_repo_root + + +def huggingface_env(repo_root: pathlib.Path) -> dict[str, str]: + env = {} + hf_home = repo_root / ".cache" / "huggingface" + hf_hub_cache = hf_home / "hub" + ensure_dirs([hf_hub_cache]) + env["HF_HOME"] = str(hf_home) + env["HUGGINGFACE_HUB_CACHE"] = str(hf_hub_cache) + env["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1" + return env + + +def resolved_env(repo_root: pathlib.Path) -> dict[str, str]: + env = {} + dotnet = dotnet_env(repo_root) + pip = pip_env(repo_root) + hf = huggingface_env(repo_root) + + dotnet_keys = [ + "DOTNET_CLI_HOME", + "NUGET_PACKAGES", + "NUGET_HTTP_CACHE_PATH", + "DOTNET_SKIP_FIRST_TIME_EXPERIENCE", + "DOTNET_ADD_GLOBAL_TOOLS_TO_PATH", + "DOTNET_GENERATE_ASPNET_CERTIFICATE", + "DOTNET_CLI_TELEMETRY_OPTOUT", + "NUGET_CERT_REVOCATION_MODE", + ] + pip_keys = [ + "PIP_CACHE_DIR", + "PIP_DISABLE_PIP_VERSION_CHECK", + "PIP_DEFAULT_TIMEOUT", + "PIP_RETRIES", + "TEMP", + "TMP", + ] + for key in dotnet_keys: + env[key] = dotnet[key] + for key in pip_keys: + env[key] = pip[key] + env.update(hf) + clean_proxy_env(env) + return env + + +def export_lines(shell: str, env_map: dict[str, str]) -> list[str]: + def sh_quote(value: str) -> str: + return "'" + value.replace("'", "'\"'\"'") + "'" + + if shell == "pwsh": + lines = [f"Remove-Item Env:{k} -ErrorAction SilentlyContinue" for k in PROXY_VARS] + lines.extend(f"$env:{k} = \"{v.replace('\"', '`\"')}\"" for k, v in env_map.items()) + return lines + if shell in ("bash", "zsh"): + lines = [f"unset {k}" for k in PROXY_VARS] + lines.extend(f"export {k}={sh_quote(v)}" for k, v in env_map.items()) + return lines + if shell == "cmd": + lines = [f"set {k}=" for k in PROXY_VARS] + lines.extend(f"set {k}={v}" for k, v in env_map.items()) + return lines + raise ValueError(shell) + + +def cmd_export(args): + try: + repo_root = resolve_repo_root(args.project_root) + except Exception as ex: + print(f"Failed to resolve project root: {ex}", file=sys.stderr) + return 2 + + env_map = resolved_env(repo_root) + payload = { + "projectRoot": str(repo_root), + "env": env_map, + "createdDirs": [ + str(repo_root / ".dotnet_home"), + str(repo_root / ".nuget" / "packages"), + str(repo_root / ".nuget" / "http-cache"), + str(repo_root / ".pip" / "cache"), + str(repo_root / ".tmp" / "pip-temp"), + str(repo_root / ".cache" / "huggingface" / "hub"), + ], + "warnings": [], + } + + try: + lines = export_lines(args.shell, env_map) + except ValueError: + print(f"Unsupported shell target: {args.shell}", file=sys.stderr) + return 3 + + if args.json: + print(json.dumps(payload)) + else: + for line in lines: + print(line) + return 0 + + +def cmd_doctor(args): + try: + repo_root = resolve_repo_root(args.project_root) + except Exception as ex: + print(f"Failed to resolve project root: {ex}", file=sys.stderr) + return 2 + + env_map = resolved_env(repo_root) + checks = { + "repo_root": str(repo_root), + "dotnet_home_exists": (repo_root / ".dotnet_home").exists(), + "nuget_cache_exists": (repo_root / ".nuget" / "packages").exists(), + "pip_cache_exists": (repo_root / ".pip" / "cache").exists(), + "hf_cache_exists": (repo_root / ".cache" / "huggingface" / "hub").exists(), + "env_count": len(env_map), + } + print(json.dumps(checks)) + return 0 + + +def main(): + parser = argparse.ArgumentParser(description="SDT cross-platform shell bootstrap helper") + sub = parser.add_subparsers(dest="command", required=True) + + p_export = sub.add_parser("export", help="Print env exports for a shell") + p_export.add_argument("--shell", required=True) + p_export.add_argument("--project-root") + p_export.add_argument("--json", action="store_true") + + p_doctor = sub.add_parser("doctor", help="Validate env bootstrap paths") + p_doctor.add_argument("--project-root") + + args = parser.parse_args() + if args.command == "export": + return cmd_export(args) + return cmd_doctor(args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/diag.py b/scripts/diag.py new file mode 100644 index 0000000..20bf41b --- /dev/null +++ b/scripts/diag.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import platform +import shutil +import subprocess +import sys +from script_common import resolve_command + + +def run_capture(cmd): + try: + proc = subprocess.run(cmd, capture_output=True, text=True, check=False) + out = (proc.stdout or "").strip() + err = (proc.stderr or "").strip() + text = out if out else err + return proc.returncode == 0, text + except Exception as ex: + return False, str(ex) + + +def probe_tool(tool): + mapping = { + "dotnet": ["dotnet", "--version"], + "node": ["node", "--version"], + "npm": ["npm", "--version"], + "python": ["python", "--version"], + "cargo": ["cargo", "--version"], + "tauri": ["tauri", "--version"], + "git": ["git", "--version"], + "docker": ["docker", "--version"], + } + cmd = mapping.get(tool, [tool, "--version"]) + resolved = resolve_command(cmd[0]) + if shutil.which(resolved) is None and not os.path.exists(resolved): + return {"tool": tool, "available": False, "version": None, "details": f"{cmd[0]} not found in PATH"} + cmd = [resolved, *cmd[1:]] + ok, text = run_capture(cmd) + return {"tool": tool, "available": ok, "version": text if ok else None, "details": None if ok else text} + + +def install_plan(tool): + is_windows = platform.system().lower().startswith("win") + if is_windows: + plans = { + "dotnet": [("winget", ["install", "Microsoft.DotNet.SDK.10"])], + "node": [("winget", ["install", "OpenJS.NodeJS.LTS"])], + "npm": [("winget", ["install", "OpenJS.NodeJS.LTS"])], + "python": [("winget", ["install", "Python.Python.3.12"])], + "cargo": [("winget", ["install", "Rustlang.Rustup"])], + "tauri": [("npm", ["install", "-g", "@tauri-apps/cli"])], + "git": [("winget", ["install", "Git.Git"])], + "docker": [("winget", ["install", "Docker.DockerDesktop"])], + } + else: + plans = { + "dotnet": [("sh", ["-c", "echo install dotnet sdk with your package manager"])], + "node": [("sh", ["-c", "echo install nodejs with your package manager"])], + "npm": [("sh", ["-c", "echo install npm with your package manager"])], + "python": [("sh", ["-c", "echo install python3 with your package manager"])], + "cargo": [("sh", ["-c", "curl https://sh.rustup.rs -sSf | sh"])], + "tauri": [("npm", ["install", "-g", "@tauri-apps/cli"])], + "git": [("sh", ["-c", "echo install git with your package manager"])], + "docker": [("sh", ["-c", "echo install docker with your package manager"])], + } + + cmds = plans.get(tool, []) + return { + "tool": tool, + "supported": len(cmds) > 0, + "summary": f"Install plan for {tool} on {platform.system()}", + "commands": [{"command": c, "args": a} for c, a in cmds], + } + + +def run_install(tool): + plan = install_plan(tool) + if not plan["supported"]: + return 2 + for cmd in plan["commands"]: + proc = subprocess.run([cmd["command"], *cmd["args"]], check=False) + if proc.returncode != 0: + return proc.returncode + return 0 + + +def main(): + parser = argparse.ArgumentParser(description="SDT diagnostics and install planner") + sub = parser.add_subparsers(dest="cmd", required=True) + + p_probe = sub.add_parser("probe") + p_probe.add_argument("--tool", required=True) + p_probe.add_argument("--json", action="store_true") + + p_plan = sub.add_parser("install-plan") + p_plan.add_argument("--tool", required=True) + p_plan.add_argument("--json", action="store_true") + + p_run = sub.add_parser("install-run") + p_run.add_argument("--tool", required=True) + + args = parser.parse_args() + + if args.cmd == "probe": + result = probe_tool(args.tool.lower()) + if args.json: + print(json.dumps(result)) + else: + print(result) + return 0 if result["available"] else 1 + + if args.cmd == "install-plan": + result = install_plan(args.tool.lower()) + if args.json: + print(json.dumps(result)) + else: + print(result) + return 0 if result["supported"] else 2 + + if args.cmd == "install-run": + return run_install(args.tool.lower()) + + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/dotnet-min.py b/scripts/dotnet-min.py new file mode 100644 index 0000000..c8aa0f4 --- /dev/null +++ b/scripts/dotnet-min.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +import argparse +import sys + +from script_common import dotnet_env, resolve_repo_root, run + + +DOTNET_SAFE_CMDS = {"restore", "build", "run", "test", "publish", "pack"} + + +def main() -> int: + parser = argparse.ArgumentParser(description="Cross-platform minimal dotnet wrapper") + parser.add_argument("dotnet_args", nargs=argparse.REMAINDER) + parser.add_argument("--repo-root", default=None) + args = parser.parse_args() + + if not args.dotnet_args: + print("Usage: python scripts/dotnet-min.py ", file=sys.stderr) + return 2 + + repo_root = resolve_repo_root(args.repo_root) + dotnet_args = list(args.dotnet_args) + cmd = dotnet_args[0].lower() + + if cmd in DOTNET_SAFE_CMDS: + dotnet_args.extend(["-p:RestoreIgnoreFailedSources=true", "-p:NuGetAudit=false"]) + if cmd == "restore": + dotnet_args.append("--ignore-failed-sources") + + return run("dotnet", dotnet_args, repo_root, env=dotnet_env(repo_root)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/legacy/dotnet-min.legacy.ps1 b/scripts/legacy/dotnet-min.legacy.ps1 new file mode 100644 index 0000000..9b36e3b --- /dev/null +++ b/scripts/legacy/dotnet-min.legacy.ps1 @@ -0,0 +1,62 @@ +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$DotnetArgs +) + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path + +# Keep dotnet and NuGet artifacts local to the repo for easy cleanup. +$env:DOTNET_CLI_HOME = Join-Path $repoRoot ".dotnet_home" +$env:NUGET_PACKAGES = Join-Path $repoRoot ".nuget\packages" +$env:NUGET_HTTP_CACHE_PATH = Join-Path $repoRoot ".nuget\http-cache" + +# Keep setup minimal and non-interactive. +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = "1" +$env:DOTNET_ADD_GLOBAL_TOOLS_TO_PATH = "0" +$env:DOTNET_GENERATE_ASPNET_CERTIFICATE = "0" +$env:DOTNET_CLI_TELEMETRY_OPTOUT = "1" + +# Clear proxy env vars for this process. The host machine currently points them +# to 127.0.0.1:9, which breaks NuGet restore. +Remove-Item Env:HTTP_PROXY -ErrorAction SilentlyContinue +Remove-Item Env:HTTPS_PROXY -ErrorAction SilentlyContinue +Remove-Item Env:ALL_PROXY -ErrorAction SilentlyContinue +Remove-Item Env:http_proxy -ErrorAction SilentlyContinue +Remove-Item Env:https_proxy -ErrorAction SilentlyContinue +Remove-Item Env:all_proxy -ErrorAction SilentlyContinue +Remove-Item Env:GIT_HTTP_PROXY -ErrorAction SilentlyContinue +Remove-Item Env:GIT_HTTPS_PROXY -ErrorAction SilentlyContinue + +# Prefer offline cert revocation checks to reduce flaky TLS behavior on constrained hosts. +$env:NUGET_CERT_REVOCATION_MODE = "offline" + +New-Item -ItemType Directory -Force -Path $env:DOTNET_CLI_HOME | Out-Null +New-Item -ItemType Directory -Force -Path $env:NUGET_PACKAGES | Out-Null +New-Item -ItemType Directory -Force -Path $env:NUGET_HTTP_CACHE_PATH | Out-Null + +if (-not $DotnetArgs -or $DotnetArgs.Count -eq 0) { + Write-Host "Usage: ./scripts/dotnet-min.ps1 " + Write-Host "Example: ./scripts/dotnet-min.ps1 build Journal.Sidecar/Journal.Sidecar.csproj" + exit 2 +} + +$firstArg = $DotnetArgs[0].ToLowerInvariant() +$effectiveArgs = @($DotnetArgs) + +if ($firstArg -in @("restore", "build", "run", "test", "publish", "pack")) { + if (-not ($effectiveArgs -contains "-p:RestoreIgnoreFailedSources=true")) { + $effectiveArgs += "-p:RestoreIgnoreFailedSources=true" + } + if (-not ($effectiveArgs -contains "-p:NuGetAudit=false")) { + $effectiveArgs += "-p:NuGetAudit=false" + } +} + +if ($firstArg -eq "restore") { + if (-not ($effectiveArgs -contains "--ignore-failed-sources")) { + $effectiveArgs += "--ignore-failed-sources" + } +} + +& dotnet @effectiveArgs +exit $LASTEXITCODE diff --git a/scripts/legacy/migration-gate.legacy.ps1 b/scripts/legacy/migration-gate.legacy.ps1 new file mode 100644 index 0000000..c0d2e09 --- /dev/null +++ b/scripts/legacy/migration-gate.legacy.ps1 @@ -0,0 +1,57 @@ +param( + [switch]$SkipSmoke, + [switch]$SkipApi +) + +$ErrorActionPreference = "Stop" +$repoRoot = Split-Path -Parent $PSScriptRoot +$parityReport = Join-Path $repoRoot "logs\parity_harness_results.json" + +Write-Host "migration-gate: repo root = $repoRoot" + +Push-Location $repoRoot +try { + Write-Host "migration-gate: building sidecar binary..." + & "$repoRoot\scripts\dotnet-min.ps1" build Journal.Sidecar/Journal.Sidecar.csproj + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + if (-not $SkipSmoke) { + Write-Host "migration-gate: running csharp smoke tests..." + & "$repoRoot\scripts\dotnet-min.ps1" run --project Journal.SmokeTests/Journal.SmokeTests.csproj + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + else { + Write-Host "migration-gate: skipping smoke tests (--SkipSmoke)." + } + + Write-Host "migration-gate: running parity harness + fixture matrix..." + $testsDir = Join-Path $repoRoot "tests" + if (Test-Path $testsDir) { + $env:PARITY_HARNESS_REPORT = $parityReport + & python -m unittest discover -s tests -p "test_parity_harness.py" -v + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + else { + Write-Host "migration-gate: skipping parity harness — tests/ directory not found." + } + + if (-not $SkipApi) { + Write-Host "migration-gate: running API contract tests..." + if (Test-Path $testsDir) { + & python -m unittest discover -s tests -p "test_api_contract.py" -v + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + else { + Write-Host "migration-gate: skipping API contract tests — tests/ directory not found." + } + } + else { + Write-Host "migration-gate: skipping API contract tests (--SkipApi)." + } + + Write-Host "migration-gate: PASS" + Write-Host "migration-gate: parity report => $parityReport" +} +finally { + Pop-Location +} diff --git a/scripts/legacy/npm-clean.legacy.ps1 b/scripts/legacy/npm-clean.legacy.ps1 new file mode 100644 index 0000000..ce838cd --- /dev/null +++ b/scripts/legacy/npm-clean.legacy.ps1 @@ -0,0 +1,62 @@ +param( + [switch]$RemoveLockfile, + [switch]$Force +) + +$commonScript = Join-Path $PSScriptRoot "script-common.ps1" +if (-not (Test-Path $commonScript)) { + throw "Missing helper script: $commonScript" +} +. $commonScript + +$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot +$appRoot = Resolve-JournalAppRoot -RepoRoot $repoRoot + +Write-Host "Cleaning npm artifacts for Journal.App" -ForegroundColor Cyan +Write-Host "Using app root: $appRoot" -ForegroundColor DarkGray + +$processNames = @("node", "journalapp", "tauri") +Get-Process -ErrorAction SilentlyContinue | + Where-Object { $processNames -contains $_.ProcessName } | + ForEach-Object { + try { + Stop-Process -Id $_.Id -Force -ErrorAction Stop + Write-Host "Stopped process: $($_.ProcessName) ($($_.Id))" -ForegroundColor DarkGray + } + catch { + Write-Warning "Failed to stop process $($_.ProcessName) ($($_.Id)): $($_.Exception.Message)" + } + } + +Push-Location $appRoot +try { + $nodeModulesPath = Join-Path $appRoot "node_modules" + $lockfilePath = Join-Path $appRoot "package-lock.json" + + if (Test-Path $nodeModulesPath) { + if (-not $Force) { + Write-Host "Removing node_modules (use -Force to suppress prompt)..." -ForegroundColor Yellow + } + Remove-Item -Recurse -Force $nodeModulesPath + Write-Host "Removed node_modules." -ForegroundColor Green + } + else { + Write-Host "node_modules not found; nothing to remove." -ForegroundColor DarkGray + } + + if ($RemoveLockfile) { + if (Test-Path $lockfilePath) { + Remove-Item -Force $lockfilePath + Write-Host "Removed package-lock.json." -ForegroundColor Green + } + else { + Write-Host "package-lock.json not found; nothing to remove." -ForegroundColor DarkGray + } + } + else { + Write-Host "Keeping package-lock.json (pass -RemoveLockfile to delete)." -ForegroundColor DarkGray + } +} +finally { + Pop-Location +} diff --git a/scripts/legacy/nuget-export-cache.legacy.ps1 b/scripts/legacy/nuget-export-cache.legacy.ps1 new file mode 100644 index 0000000..bee1a64 --- /dev/null +++ b/scripts/legacy/nuget-export-cache.legacy.ps1 @@ -0,0 +1,57 @@ +param( + [string]$OutputZip = "nuget-cache-export.zip", + [switch]$IncludeDotnetHome +) + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$outputPath = if ([System.IO.Path]::IsPathRooted($OutputZip)) { $OutputZip } else { Join-Path $repoRoot $OutputZip } +$outputDir = Split-Path -Parent $outputPath +if (-not (Test-Path $outputDir)) { + New-Item -ItemType Directory -Force -Path $outputDir | Out-Null +} + +Write-Host "Priming restore cache..." +& (Join-Path $PSScriptRoot "dotnet-min.ps1") restore "Journal.Sidecar/Journal.Sidecar.csproj" +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +& (Join-Path $PSScriptRoot "dotnet-min.ps1") restore "Journal.WebGateway/Journal.WebGateway.csproj" +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +& (Join-Path $PSScriptRoot "dotnet-min.ps1") restore "Journal.SmokeTests/Journal.SmokeTests.csproj" +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +$staging = Join-Path $repoRoot ".nuget-export-staging" +if (Test-Path $staging) { + Remove-Item -Recurse -Force $staging +} +New-Item -ItemType Directory -Force -Path $staging | Out-Null + +$nugetRoot = Join-Path $repoRoot ".nuget" +if (-not (Test-Path $nugetRoot)) { + Write-Error "No .nuget directory found under $repoRoot" + exit 1 +} + +Copy-Item -Recurse -Force -Path $nugetRoot -Destination (Join-Path $staging ".nuget") +if ($IncludeDotnetHome) { + $dotnetHome = Join-Path $repoRoot ".dotnet_home" + if (Test-Path $dotnetHome) { + Copy-Item -Recurse -Force -Path $dotnetHome -Destination (Join-Path $staging ".dotnet_home") + } +} + +$manifest = @( + "exported_utc=$([DateTime]::UtcNow.ToString("o"))" + "repo_root=$repoRoot" + "include_dotnet_home=$($IncludeDotnetHome.IsPresent)" + "note=Copy this zip to target machine and run scripts/nuget-import-cache.ps1" +) +$manifest | Set-Content -Encoding UTF8 -Path (Join-Path $staging "nuget-cache-manifest.txt") + +if (Test-Path $outputPath) { + Remove-Item -Force $outputPath +} + +Compress-Archive -Path (Join-Path $staging "*") -DestinationPath $outputPath -Force +Remove-Item -Recurse -Force $staging + +Write-Host "NuGet cache export created at: $outputPath" + diff --git a/scripts/legacy/nuget-import-cache.legacy.ps1 b/scripts/legacy/nuget-import-cache.legacy.ps1 new file mode 100644 index 0000000..dad8f04 --- /dev/null +++ b/scripts/legacy/nuget-import-cache.legacy.ps1 @@ -0,0 +1,25 @@ +param( + [string]$InputZip = "nuget-cache-export.zip" +) + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$inputPath = if ([System.IO.Path]::IsPathRooted($InputZip)) { $InputZip } else { Join-Path $repoRoot $InputZip } + +if (-not (Test-Path $inputPath)) { + Write-Error "Input zip not found: $inputPath" + exit 1 +} + +Write-Host "Importing cache from: $inputPath" +Expand-Archive -Path $inputPath -DestinationPath $repoRoot -Force + +Write-Host "Running restore with local cache..." +& (Join-Path $PSScriptRoot "dotnet-min.ps1") restore "Journal.Sidecar/Journal.Sidecar.csproj" +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +& (Join-Path $PSScriptRoot "dotnet-min.ps1") restore "Journal.WebGateway/Journal.WebGateway.csproj" +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +& (Join-Path $PSScriptRoot "dotnet-min.ps1") restore "Journal.SmokeTests/Journal.SmokeTests.csproj" +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +Write-Host "Cache import complete." + diff --git a/scripts/legacy/pip-min.legacy.ps1 b/scripts/legacy/pip-min.legacy.ps1 new file mode 100644 index 0000000..6bf7f2c --- /dev/null +++ b/scripts/legacy/pip-min.legacy.ps1 @@ -0,0 +1,56 @@ +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$PipArgs +) + +$commonScript = Join-Path $PSScriptRoot "script-common.ps1" +if (-not (Test-Path $commonScript)) { + throw "Missing helper script: $commonScript" +} +. $commonScript + +$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot + +Initialize-JournalPipEnv -RepoRoot $repoRoot +Clear-JournalProxyEnv + +if (-not $PipArgs -or $PipArgs.Count -eq 0) { + Write-Host "Usage: ./scripts/pip-min.ps1 " + Write-Host "Example: ./scripts/pip-min.ps1 install --index-url https://pypi.org/simple faster-whisper" + exit 2 +} + +# Default install target to a repo-local directory so installs do not require +# user/site-packages write access on constrained hosts. +$effectiveArgs = @($PipArgs) +$firstArg = $effectiveArgs[0].ToLowerInvariant() +if ($firstArg -eq "install") { + # On Windows, map PyAudio to pyaudiowpatch (wheel available for newer CPython), + # avoiding source builds that require PortAudio headers/toolchain wiring. + for ($i = 0; $i -lt $effectiveArgs.Count; $i++) { + $arg = $effectiveArgs[$i] + if ($arg -match '^(?i)pyaudio($|[<>=!~].*)') { + $suffix = $arg.Substring(7) + $effectiveArgs[$i] = "pyaudiowpatch$suffix" + Write-Host "pip-min: mapped '$arg' -> '$($effectiveArgs[$i])' on Windows." + } + } + + $hasTarget = $effectiveArgs -contains "--target" -or $effectiveArgs -contains "-t" -or $effectiveArgs -contains "--prefix" + if (-not $hasTarget) { + $effectiveArgs = $effectiveArgs | Where-Object { $_ -ne "--user" } + $localTarget = Join-Path $repoRoot ".pydeps\py314" + New-Item -ItemType Directory -Force -Path $localTarget | Out-Null + $effectiveArgs += @("--target", $localTarget) + Write-Host "pip-min: using local target $localTarget" + } +} + +$pipWrapper = Join-Path $PSScriptRoot "pip_safe.py" +if (Test-Path $pipWrapper) { + & python $pipWrapper @effectiveArgs +} +else { + & python -m pip @effectiveArgs +} +exit $LASTEXITCODE diff --git a/scripts/legacy/publish-app.legacy.ps1 b/scripts/legacy/publish-app.legacy.ps1 new file mode 100644 index 0000000..7d3a783 --- /dev/null +++ b/scripts/legacy/publish-app.legacy.ps1 @@ -0,0 +1,216 @@ +param( + [ValidateSet("web", "tauri")] + [string]$Target = "web", + [ValidateSet("Release", "Debug")] + [string]$Configuration = "Release", + [ValidateSet("none", "nsis", "msi")] + [string]$TauriBundles = "none", + [switch]$InstallDeps, + [switch]$SkipInstall, + [switch]$DryRun +) + +$commonScript = Join-Path $PSScriptRoot "script-common.ps1" +if (-not (Test-Path $commonScript)) { + throw "Missing helper script: $commonScript" +} +. $commonScript + +$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot +$appRoot = Resolve-JournalAppRoot -RepoRoot $repoRoot + +Clear-JournalProxyEnv + +# Keep npm cache and temp local to the repo. +$npmCacheDir = Join-Path $repoRoot ".npm\cache" +$npmTempDir = Join-Path $repoRoot ".tmp\npm-temp" +New-Item -ItemType Directory -Force -Path $npmCacheDir, $npmTempDir | Out-Null +$env:npm_config_cache = $npmCacheDir +$env:npm_config_update_notifier = "false" +$env:npm_config_fund = "false" +$env:npm_config_audit = "false" +$env:npm_config_offline = "false" +$env:npm_config_prefer_offline = "false" +$env:npm_config_prefer_online = "true" +$env:TEMP = $npmTempDir +$env:TMP = $npmTempDir + +if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { + throw "npm is required but was not found in PATH." +} + +$nodeModulesPath = Join-Path $appRoot "node_modules" +$packageJsonPath = Join-Path $appRoot "package.json" +$packageLockPath = Join-Path $appRoot "package-lock.json" +$depsHashPath = Join-Path $appRoot "node_modules\.journal-deps.sha256" + +function Get-JournalNodeDepsHash { + param( + [Parameter(Mandatory = $true)] + [string[]]$Paths + ) + + $hashLines = foreach ($path in $Paths) { + if (-not (Test-Path $path)) { + continue + } + (Get-FileHash -Algorithm SHA256 -Path $path).Hash + } + return ($hashLines -join "`n").Trim() +} + +$hashInputs = @() +if (Test-Path $packageJsonPath) { + $hashInputs += $packageJsonPath +} +if (Test-Path $packageLockPath) { + $hashInputs += $packageLockPath +} +if ($hashInputs.Count -eq 0) { + throw "package.json not found under $appRoot." +} + +$expectedDepsHash = Get-JournalNodeDepsHash -Paths $hashInputs +$shouldInstall = $InstallDeps -or (-not (Test-Path $nodeModulesPath)) +$installReason = $null + +if (-not $shouldInstall -and -not $SkipInstall) { + if (-not (Test-Path $depsHashPath)) { + $shouldInstall = $true + $installReason = "dependency hash missing" + } + else { + $currentDepsHash = (Get-Content $depsHashPath -Raw).Trim() + if ($currentDepsHash -ne $expectedDepsHash) { + $shouldInstall = $true + $installReason = "package.json/lockfile changed" + } + } +} + +if ($SkipInstall) { + $shouldInstall = $false + if ($installReason) { + Write-Host "SkipInstall set; dependencies may be stale ($installReason)." -ForegroundColor Yellow + } +} + +Write-Host "Building Journal.App target '$Target' ($Configuration)..." -ForegroundColor Cyan +Write-Host "Using app root: $appRoot" -ForegroundColor DarkGray + +Push-Location $appRoot +try { + if ($shouldInstall) { + $installArgs = if (Test-Path $packageLockPath) { + @("ci", "--no-audit", "--fund=false") + } + else { + @("install", "--no-audit", "--fund=false") + } + + if ($installReason) { + Write-Host "Dependencies changed ($installReason). Installing..." -ForegroundColor Yellow + } + + Write-Host "> npm $($installArgs -join ' ')" -ForegroundColor DarkGray + if (-not $DryRun) { + & npm @installArgs + if ($LASTEXITCODE -ne 0) { + throw "Dependency install failed with exit code $LASTEXITCODE." + } + + $depsDir = Split-Path $depsHashPath -Parent + if (-not (Test-Path $depsDir)) { + New-Item -ItemType Directory -Force -Path $depsDir | Out-Null + } + $expectedDepsHash | Set-Content -Path $depsHashPath -NoNewline + } + } + else { + Write-Host "Skipping dependency install (node_modules present and deps unchanged)." -ForegroundColor DarkGray + } + + if ($Target -eq "web") { + $buildArgs = @("run", "build") + Write-Host "> npm $($buildArgs -join ' ')" -ForegroundColor DarkGray + if (-not $DryRun) { + & npm @buildArgs + if ($LASTEXITCODE -ne 0) { + throw "Frontend build failed with exit code $LASTEXITCODE." + } + } + + $outputPath = Join-Path $appRoot "build" + if ($DryRun) { + Write-Host "`nDry run complete (no commands executed)." -ForegroundColor Yellow + Write-Host "Expected output: $outputPath" -ForegroundColor Gray + } + else { + Write-Host "`nFrontend build successful." -ForegroundColor Green + Write-Host "Output: $outputPath" -ForegroundColor Gray + } + } + else { + $tauriArgs = @("run", "tauri", "build") + $tauriCliArgs = @() + if ($TauriBundles -eq "none") { + $tauriCliArgs += "--no-bundle" + } + else { + $tauriCliArgs += @("--bundles", $TauriBundles) + } + if ($Configuration -eq "Debug") { + $tauriCliArgs += "--debug" + } + if ($tauriCliArgs.Count -gt 0) { + $tauriArgs += "--" + $tauriArgs += $tauriCliArgs + } + + Write-Host "> npm $($tauriArgs -join ' ')" -ForegroundColor DarkGray + if (-not $DryRun) { + & npm @tauriArgs + if ($LASTEXITCODE -ne 0) { + throw "Tauri build failed with exit code $LASTEXITCODE." + } + } + + $targetConfigDir = if ($Configuration -eq "Debug") { "debug" } else { "release" } + $tauriTargetPath = Join-Path $appRoot "src-tauri\target" + $rawExePath = Join-Path $tauriTargetPath "$targetConfigDir\journalapp.exe" + if ($DryRun) { + Write-Host "`nDry run complete (no commands executed)." -ForegroundColor Yellow + if ($TauriBundles -eq "none") { + Write-Host "Expected executable: $rawExePath" -ForegroundColor Gray + } + else { + Write-Host "Expected output root: $tauriTargetPath" -ForegroundColor Gray + } + } + else { + Write-Host "`nTauri build successful." -ForegroundColor Green + if ($TauriBundles -eq "none") { + if (Test-Path $rawExePath) { + Write-Host "Executable location: $rawExePath" -ForegroundColor Gray + } + else { + $exeCandidates = Get-ChildItem -Path (Join-Path $tauriTargetPath $targetConfigDir) -File -Filter *.exe -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending + if ($exeCandidates -and $exeCandidates.Count -gt 0) { + Write-Host "Executable location: $($exeCandidates[0].FullName)" -ForegroundColor Gray + } + else { + Write-Host "Output root: $tauriTargetPath" -ForegroundColor Gray + } + } + } + else { + Write-Host "Output root: $tauriTargetPath" -ForegroundColor Gray + } + } + } +} +finally { + Pop-Location +} + diff --git a/scripts/legacy/publish-output.legacy.ps1 b/scripts/legacy/publish-output.legacy.ps1 new file mode 100644 index 0000000..29663f6 --- /dev/null +++ b/scripts/legacy/publish-output.legacy.ps1 @@ -0,0 +1,103 @@ +param( + [ValidateSet("Release", "Debug")] + [string]$Configuration = "Release", + [string]$Runtime = "win-x64", + [switch]$SkipSidecar, + [switch]$SkipWeb, + [switch]$SkipWebGateway, + [switch]$SkipTauri, + [switch]$DryRun +) + +$commonScript = Join-Path $PSScriptRoot "script-common.ps1" +if (-not (Test-Path $commonScript)) { + throw "Missing helper script: $commonScript" +} +. $commonScript + +$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot +$appRoot = Resolve-JournalAppRoot -RepoRoot $repoRoot +$outputRoot = Join-Path $repoRoot "output" + +$publishSidecar = Join-Path $PSScriptRoot "publish-sidecar.ps1" +$publishApp = Join-Path $PSScriptRoot "publish-app.ps1" +$publishGateway = Join-Path $PSScriptRoot "publish-webgateway.ps1" + +Write-Host "Publishing all outputs to: $outputRoot" -ForegroundColor Cyan +Write-Host "Configuration: $Configuration Runtime: $Runtime" -ForegroundColor DarkGray + +if (-not (Test-Path $outputRoot)) { + New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null +} + +function Invoke-Step { + param( + [string]$Label, + [string]$ScriptPath, + [string[]]$Args + ) + + Write-Host "`n> $Label" -ForegroundColor Cyan + Write-Host " $ScriptPath $($Args -join ' ')" -ForegroundColor DarkGray + if (-not $DryRun) { + & $ScriptPath @Args + } +} + +if (-not $SkipSidecar) { + Invoke-Step "Publish Sidecar" $publishSidecar @( + "-Configuration", $Configuration, + "-Runtime", $Runtime + ) +} +else { + Write-Host "Skipping sidecar publish." -ForegroundColor DarkGray +} + +if (-not $SkipWeb) { + Invoke-Step "Build Web UI" $publishApp @( + "-Target", "web", + "-Configuration", $Configuration + ) +} +else { + Write-Host "Skipping web build." -ForegroundColor DarkGray +} + +if (-not $SkipWebGateway) { + Invoke-Step "Publish WebGateway" $publishGateway @( + "-Configuration", $Configuration, + "-Runtime", $Runtime + ) +} +else { + Write-Host "Skipping WebGateway publish." -ForegroundColor DarkGray +} + +if (-not $SkipTauri) { + Invoke-Step "Build Tauri Desktop App" $publishApp @( + "-Target", "tauri", + "-Configuration", $Configuration, + "-TauriBundles", "none" + ) + + $targetConfigDir = if ($Configuration -eq "Debug") { "debug" } else { "release" } + $tauriExePath = Join-Path $appRoot "src-tauri\\target\\$targetConfigDir\\journalapp.exe" + $stagedExePath = Join-Path $outputRoot "journalapp.exe" + + if (Test-Path $tauriExePath) { + if ($DryRun) { + Write-Host "Would copy: $tauriExePath -> $stagedExePath" -ForegroundColor Yellow + } + else { + Copy-Item -Force $tauriExePath $stagedExePath + Write-Host "Staged desktop exe: $stagedExePath" -ForegroundColor Green + } + } + else { + Write-Warning "Tauri exe not found at $tauriExePath" + } +} +else { + Write-Host "Skipping Tauri build." -ForegroundColor DarkGray +} diff --git a/scripts/legacy/publish-sidecar.legacy.ps1 b/scripts/legacy/publish-sidecar.legacy.ps1 new file mode 100644 index 0000000..ab2a855 --- /dev/null +++ b/scripts/legacy/publish-sidecar.legacy.ps1 @@ -0,0 +1,54 @@ +param( + [string]$Configuration = "Release", + [string]$Runtime = "win-x64" +) + +$commonScript = Join-Path $PSScriptRoot "script-common.ps1" +if (-not (Test-Path $commonScript)) { + throw "Missing helper script: $commonScript" +} +. $commonScript + +$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot +$csproj = Resolve-JournalSidecarProjectPath -RepoRoot $repoRoot +$outputDir = Join-Path $repoRoot "output" + +# Setup local dotnet environment (matches dotnet-min.ps1 logic) +Clear-JournalProxyEnv +Initialize-JournalDotnetEnv -RepoRoot $repoRoot + +Write-Host "Publishing Journal.Sidecar ($Configuration, $Runtime)..." -ForegroundColor Cyan +Write-Host "Using project: $csproj" -ForegroundColor DarkGray + +$publishArgs = @( + "publish", $csproj, + "-c", $Configuration, + "-r", $Runtime, + "--self-contained", + "-p:PublishSingleFile=true", + "-p:IncludeNativeLibrariesForSelfExtract=true", + "-p:RestoreIgnoreFailedSources=true", + "-p:NuGetAudit=false", + "-o", $outputDir +) + +& dotnet @publishArgs + +if ($LASTEXITCODE -eq 0) { + $binaryName = [System.IO.Path]::GetFileNameWithoutExtension($csproj) + $isWindowsRuntime = $Runtime -like "win-*" + $binaryFile = if ($isWindowsRuntime) { "$binaryName.exe" } else { $binaryName } + $binaryPath = Join-Path $outputDir $binaryFile + + Write-Host "`nPublish successful!" -ForegroundColor Green + if (Test-Path $binaryPath) { + Write-Host "Executable location: $binaryPath" -ForegroundColor Gray + } + else { + Write-Host "Output directory: $outputDir" -ForegroundColor Gray + } +} +else { + Write-Host "`nPublish failed with exit code $LASTEXITCODE" -ForegroundColor Red + exit $LASTEXITCODE +} diff --git a/scripts/legacy/publish-webgateway.legacy.ps1 b/scripts/legacy/publish-webgateway.legacy.ps1 new file mode 100644 index 0000000..b3e3322 --- /dev/null +++ b/scripts/legacy/publish-webgateway.legacy.ps1 @@ -0,0 +1,55 @@ +param( + [ValidateSet("Release", "Debug")] + [string]$Configuration = "Release", + [string]$Runtime = "win-x64", + [switch]$SelfContained, + [switch]$SkipWebAssets +) + +$commonScript = Join-Path $PSScriptRoot "script-common.ps1" +if (-not (Test-Path $commonScript)) { + throw "Missing helper script: $commonScript" +} +. $commonScript + +$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot +$gatewayProject = Resolve-JournalWebGatewayProjectPath -RepoRoot $repoRoot +$outputDir = Join-Path $repoRoot "output\webgateway" +$webBuildDir = Join-Path $repoRoot "Journal.App\build" +$webOutputDir = Join-Path $outputDir "wwwroot" + +Clear-JournalProxyEnv +Initialize-JournalDotnetEnv -RepoRoot $repoRoot + +Write-Host "Publishing Journal.WebGateway ($Configuration, $Runtime)..." -ForegroundColor Cyan +Write-Host "Project: $gatewayProject" -ForegroundColor DarkGray + +$publishArgs = @( + "publish", $gatewayProject, + "-c", $Configuration, + "-r", $Runtime, + "--self-contained", ($SelfContained.IsPresent.ToString().ToLowerInvariant()), + "-p:RestoreIgnoreFailedSources=true", + "-p:NuGetAudit=false", + "-o", $outputDir +) + +& dotnet @publishArgs +if ($LASTEXITCODE -ne 0) { + Write-Host "`nPublish failed with exit code $LASTEXITCODE" -ForegroundColor Red + exit $LASTEXITCODE +} + +if (-not $SkipWebAssets) { + if (Test-Path $webBuildDir) { + New-Item -ItemType Directory -Force -Path $webOutputDir | Out-Null + Copy-Item -Path (Join-Path $webBuildDir "*") -Destination $webOutputDir -Recurse -Force + Write-Host "Copied web assets to: $webOutputDir" -ForegroundColor Gray + } + else { + Write-Warning "Journal.App build output not found at $webBuildDir. Run ./scripts/publish-app.ps1 -Target web first." + } +} + +Write-Host "`nPublish successful." -ForegroundColor Green +Write-Host "Output directory: $outputDir" -ForegroundColor Gray diff --git a/scripts/legacy/run-webgateway.legacy.ps1 b/scripts/legacy/run-webgateway.legacy.ps1 new file mode 100644 index 0000000..c76b3a7 --- /dev/null +++ b/scripts/legacy/run-webgateway.legacy.ps1 @@ -0,0 +1,69 @@ +param( + [ValidateSet("Release", "Debug")] + [string]$Configuration = "Release", + [string]$Urls = "http://0.0.0.0:5180", + [string]$ProjectRoot, + [ValidateSet("Dev", "Output")] + [string]$Mode = "Dev" +) + +$commonScript = Join-Path $PSScriptRoot "script-common.ps1" +if (-not (Test-Path $commonScript)) { + throw "Missing helper script: $commonScript" +} +. $commonScript + +$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot +$gatewayProject = Resolve-JournalWebGatewayProjectPath -RepoRoot $repoRoot + +$effectiveProjectRoot = if ([string]::IsNullOrWhiteSpace($ProjectRoot)) { + $repoRoot +} +else { + [System.IO.Path]::GetFullPath($ProjectRoot) +} + +if (-not (Test-Path $effectiveProjectRoot)) { + throw "ProjectRoot does not exist: $effectiveProjectRoot" +} + +Clear-JournalProxyEnv +Initialize-JournalDotnetEnv -RepoRoot $repoRoot +$env:JOURNAL_PROJECT_ROOT = $effectiveProjectRoot + +if ($Mode -eq "Output") { + $exeName = if ([System.Environment]::OSVersion.Platform -eq "Win32NT") { "Journal.WebGateway.exe" } else { "Journal.WebGateway" } + $exePath = Join-Path $repoRoot "output\webgateway\$exeName" + + if (-not (Test-Path $exePath)) { + Write-Host "Output executable not found at $exePath" -ForegroundColor Red + Write-Host "Please build WebGateway first (e.g. scripts\publish-webgateway.ps1)" -ForegroundColor Yellow + exit 1 + } + + Write-Host "Running Journal.WebGateway (Published Output)..." -ForegroundColor Cyan + Write-Host "Executable: $exePath" -ForegroundColor DarkGray + Write-Host "URLs: $Urls" -ForegroundColor DarkGray + Write-Host "JOURNAL_PROJECT_ROOT: $effectiveProjectRoot" -ForegroundColor DarkGray + + & $exePath --urls $Urls +} +else { + Write-Host "Running Journal.WebGateway ($Configuration Dev Server)..." -ForegroundColor Cyan + Write-Host "Project: $gatewayProject" -ForegroundColor DarkGray + Write-Host "URLs: $Urls" -ForegroundColor DarkGray + Write-Host "JOURNAL_PROJECT_ROOT: $effectiveProjectRoot" -ForegroundColor DarkGray + + $runArgs = @( + "run", + "--project", $gatewayProject, + "-c", $Configuration, + "--no-launch-profile", + "--urls", $Urls, + "-p:RestoreIgnoreFailedSources=true", + "-p:NuGetAudit=false" + ) + + & dotnet @runArgs +} +exit $LASTEXITCODE diff --git a/scripts/legacy/sync-output.legacy.ps1 b/scripts/legacy/sync-output.legacy.ps1 new file mode 100644 index 0000000..bb00429 --- /dev/null +++ b/scripts/legacy/sync-output.legacy.ps1 @@ -0,0 +1,64 @@ +param() + +$commonScript = Join-Path $PSScriptRoot "script-common.ps1" +if (-not (Test-Path $commonScript)) { + throw "Missing helper script: $commonScript" +} +. $commonScript + +$repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot +$outputDir = Join-Path $repoRoot "output" + +# Ensure output exists +New-Item -ItemType Directory -Force -Path $outputDir | Out-Null + +Write-Host "Syncing all recent built assets to output directory..." -ForegroundColor Cyan + +# Helper to find the newest compiled binary +function Find-NewestBin([string]$SearchPath, [string]$Pattern) { + if (-not (Test-Path $SearchPath)) { return $null } + $files = Get-ChildItem -Path $SearchPath -Filter $Pattern -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch '\\obj\\' } | + Sort-Object LastWriteTime -Descending + if ($files) { return $files[0] } + return $null +} + +# 1. Front-end Web Assets +$webBuildDir = Join-Path $repoRoot "Journal.App\build" +if (Test-Path $webBuildDir) { + $webOutputDir = Join-Path $outputDir "webgateway\wwwroot" + New-Item -ItemType Directory -Force -Path $webOutputDir | Out-Null + Copy-Item -Path (Join-Path $webBuildDir "*") -Destination $webOutputDir -Recurse -Force + Write-Host "Synced web assets -> $webOutputDir" -ForegroundColor Green +} + +# 2. Sidecar +$sidecarExeName = if ([System.Environment]::OSVersion.Platform -eq "Win32NT") { "Journal.Sidecar.exe" } else { "Journal.Sidecar" } +$sidecarExe = Find-NewestBin -SearchPath (Join-Path $repoRoot "Journal.Sidecar\bin") -Pattern $sidecarExeName +if ($sidecarExe) { + Copy-Item -Path (Join-Path $sidecarExe.DirectoryName "*") -Destination $outputDir -Recurse -Force + Write-Host "Synced Journal.Sidecar -> $outputDir" -ForegroundColor Green +} + +# 3. WebGateway +$gwExeName = if ([System.Environment]::OSVersion.Platform -eq "Win32NT") { "Journal.WebGateway.exe" } else { "Journal.WebGateway" } +$gwExe = Find-NewestBin -SearchPath (Join-Path $repoRoot "Journal.WebGateway\bin") -Pattern $gwExeName +if ($gwExe) { + $gwOutputDir = Join-Path $outputDir "webgateway" + New-Item -ItemType Directory -Force -Path $gwOutputDir | Out-Null + Copy-Item -Path (Join-Path $gwExe.DirectoryName "*") -Destination $gwOutputDir -Recurse -Force + Write-Host "Synced Journal.WebGateway -> $gwOutputDir" -ForegroundColor Green +} + +# 4. Tauri Desktop App +$tauriExe = Find-NewestBin -SearchPath (Join-Path $repoRoot "Journal.App\src-tauri\target") -Pattern "*.exe" +if ($tauriExe) { + # Don't try to copy sidecar.exe again if it ended up in tauri target dir + if ($tauriExe.Name -ne "Journal.Sidecar.exe") { + Copy-Item -Path $tauriExe.FullName -Destination $outputDir -Force + Write-Host "Synced Tauri App ($($tauriExe.Name)) -> $outputDir" -ForegroundColor Green + } +} + +Write-Host "Sync complete!" -ForegroundColor Cyan diff --git a/scripts/migration-gate.py b/scripts/migration-gate.py new file mode 100644 index 0000000..398cf09 --- /dev/null +++ b/scripts/migration-gate.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +import argparse +import subprocess +import sys +from pathlib import Path + +from script_common import resolve_repo_root + + +def run_step(repo_root: Path, title: str, command: list[str]) -> int: + print(f"\n== {title} ==") + print("$", " ".join(command)) + proc = subprocess.run(command, cwd=str(repo_root), check=False) + return proc.returncode + + +def main() -> int: + parser = argparse.ArgumentParser(description="Cross-platform migration quality gate") + parser.add_argument("--repo-root", default=None) + parser.add_argument("--skip-tests", action="store_true") + parser.add_argument("--test-project", default=None, help="Optional test csproj path") + args = parser.parse_args() + + repo_root = resolve_repo_root(args.repo_root) + + code = run_step(repo_root, "Build", [sys.executable, "scripts/dotnet-min.py", "build"]) + if code != 0: + return code + + if not args.skip_tests: + if args.test_project: + test_cmd = [sys.executable, "scripts/dotnet-min.py", "test", args.test_project] + else: + test_cmd = [sys.executable, "scripts/dotnet-min.py", "test"] + code = run_step(repo_root, "Tests", test_cmd) + if code != 0: + return code + + print("\nMigration gate passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/npm-clean.py b/scripts/npm-clean.py new file mode 100644 index 0000000..48d8881 --- /dev/null +++ b/scripts/npm-clean.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import argparse +import shutil + + +from script_common import resolve_repo_root + + +def main() -> int: + parser = argparse.ArgumentParser(description="Cross-platform node_modules cleanup") + parser.add_argument("--repo-root", default=None) + parser.add_argument("--working-dir", default=".") + parser.add_argument("--also-cache", action="store_true") + args = parser.parse_args() + + repo_root = resolve_repo_root(args.repo_root) + work_dir = (repo_root / args.working_dir).resolve() + node_modules = work_dir / "node_modules" + if node_modules.exists(): + shutil.rmtree(node_modules) + print(f"Removed: {node_modules}") + else: + print(f"Not found: {node_modules}") + + if args.also_cache: + npm_cache = repo_root / ".npm" / "cache" + if npm_cache.exists(): + shutil.rmtree(npm_cache) + print(f"Removed: {npm_cache}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/nuget-export-cache.py b/scripts/nuget-export-cache.py new file mode 100644 index 0000000..17720f7 --- /dev/null +++ b/scripts/nuget-export-cache.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import argparse +import shutil +import tempfile +from pathlib import Path + +from script_common import resolve_repo_root + + +def main() -> int: + parser = argparse.ArgumentParser(description="Export local NuGet cache to zip") + parser.add_argument("--repo-root", default=None) + parser.add_argument("--output-zip", default="nuget-cache-export.zip") + parser.add_argument("--include-dotnet-home", action="store_true") + args = parser.parse_args() + + repo_root = resolve_repo_root(args.repo_root) + output_zip = (repo_root / args.output_zip).resolve() + + nuget_dir = repo_root / ".nuget" + dotnet_home = repo_root / ".dotnet_home" + if not nuget_dir.exists(): + print(f"NuGet cache not found: {nuget_dir}") + return 2 + + with tempfile.TemporaryDirectory() as td: + stage = Path(td) / "cache-export" + stage.mkdir(parents=True, exist_ok=True) + shutil.copytree(nuget_dir, stage / ".nuget") + if args.include_dotnet_home and dotnet_home.exists(): + shutil.copytree(dotnet_home, stage / ".dotnet_home") + manifest = stage / "nuget-cache-manifest.txt" + manifest.write_text("exported_by=nuget-export-cache.py\n", encoding="utf-8") + archive_base = str(output_zip.with_suffix("")) + shutil.make_archive(archive_base, "zip", root_dir=str(stage)) + + print(f"Exported cache: {output_zip}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/nuget-import-cache.py b/scripts/nuget-import-cache.py new file mode 100644 index 0000000..608ed60 --- /dev/null +++ b/scripts/nuget-import-cache.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +import argparse +import shutil + + +from script_common import resolve_repo_root + + +def main() -> int: + parser = argparse.ArgumentParser(description="Import NuGet cache from zip") + parser.add_argument("--repo-root", default=None) + parser.add_argument("--input-zip", default="nuget-cache-export.zip") + args = parser.parse_args() + + repo_root = resolve_repo_root(args.repo_root) + input_zip = (repo_root / args.input_zip).resolve() + if not input_zip.exists(): + print(f"Input zip not found: {input_zip}") + return 2 + + shutil.unpack_archive(str(input_zip), extract_dir=str(repo_root)) + print(f"Imported cache from: {input_zip}") + print("Run `python scripts/dotnet-min.py restore` to validate restore in this repo.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/pip-min.py b/scripts/pip-min.py new file mode 100644 index 0000000..fd03343 --- /dev/null +++ b/scripts/pip-min.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import argparse +import os +import sys + +from script_common import pip_env, resolve_repo_root, run + + +def main() -> int: + parser = argparse.ArgumentParser(description="Cross-platform minimal pip wrapper") + parser.add_argument("pip_args", nargs=argparse.REMAINDER) + parser.add_argument("--repo-root", default=None) + args = parser.parse_args() + + if not args.pip_args: + print("Usage: python scripts/pip-min.py ", file=sys.stderr) + return 2 + + repo_root = resolve_repo_root(args.repo_root) + pip_args = list(args.pip_args) + + # Preserve legacy behavior: for bare install, default target to repo-local deps. + if pip_args and pip_args[0].lower() == "install": + has_target = any(a in ("--target", "--prefix") for a in pip_args) + if not has_target: + pip_args = [a for a in pip_args if a != "--user"] + target = repo_root / ".pydeps" / f"py{sys.version_info.major}{sys.version_info.minor}" + os.makedirs(target, exist_ok=True) + pip_args.extend(["--target", str(target)]) + + return run(sys.executable, ["-m", "pip", *pip_args], repo_root, env=pip_env(repo_root)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/pip_safe.py b/scripts/pip_safe.py new file mode 100644 index 0000000..f520fc2 --- /dev/null +++ b/scripts/pip_safe.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import os +import tempfile + + + +def _mkdtemp_compat( + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, +) -> str: + # Python 3.14 on some Windows hosts creates mkdtemp dirs that are + # immediately non-writable by the same process when mode=0o700 is used. + # pip relies heavily on tempfile; force 0o777 for compatibility. + if dir is None: + dir = tempfile.gettempdir() + if prefix is None: + prefix = tempfile.template + if suffix is None: + suffix = "" + + names = tempfile._get_candidate_names() + for _ in range(tempfile.TMP_MAX): + name = next(names) + path = os.path.join(dir, f"{prefix}{name}{suffix}") + try: + os.mkdir(path, 0o777) + return path + except FileExistsError: + continue + + raise FileExistsError("No usable temporary directory name found.") + + +def main(argv: list[str]) -> int: + tempfile.mkdtemp = _mkdtemp_compat # type: ignore[assignment] + + from pip._internal.cli.main import main as pip_main + + return int(pip_main(argv)) + + +if __name__ == "__main__": + raise SystemExit(main(__import__("sys").argv[1:])) + diff --git a/scripts/publish-app.py b/scripts/publish-app.py new file mode 100644 index 0000000..9f82a8f --- /dev/null +++ b/scripts/publish-app.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +import argparse + + +from script_common import find_node_app_root, resolve_repo_root, run, sha256_files + + +def main() -> int: + parser = argparse.ArgumentParser(description="Cross-platform web/tauri publish helper") + parser.add_argument("--target", choices=["web", "tauri"], default="web") + parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release") + parser.add_argument("--tauri-bundles", choices=["none", "nsis", "msi"], default="none") + parser.add_argument("--install-deps", action="store_true") + parser.add_argument("--skip-install", action="store_true") + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--repo-root", default=None) + parser.add_argument("--app-root", default=None, help="Relative or absolute app root with package.json") + args = parser.parse_args() + + repo_root = resolve_repo_root(args.repo_root) + app_root = find_node_app_root(repo_root, args.app_root) + if app_root is None: + print("Unable to locate app root (no unique package.json found).") + return 2 + + package_json = app_root / "package.json" + lock_file = app_root / "package-lock.json" + node_modules = app_root / "node_modules" + deps_hash_file = node_modules / ".sdt-deps.sha256" + expected_hash = sha256_files([package_json, lock_file]) + + should_install = args.install_deps or not node_modules.exists() + if not should_install and not args.skip_install: + if not deps_hash_file.exists(): + should_install = True + else: + current = deps_hash_file.read_text(encoding="utf-8").strip() + should_install = current != expected_hash + if args.skip_install: + should_install = False + + print(f"App root: {app_root}") + print(f"Target: {args.target} ({args.configuration})") + + if should_install: + install_args = ["ci", "--no-audit", "--fund=false"] if lock_file.exists() else ["install", "--no-audit", "--fund=false"] + print("$ npm " + " ".join(install_args)) + if not args.dry_run: + code = run("npm", install_args, app_root) + if code != 0: + if lock_file.exists() and install_args[0] == "ci": + print("npm ci failed (likely lockfile out of sync). Falling back to npm install...") + fallback_args = ["install", "--no-audit", "--fund=false"] + print("$ npm " + " ".join(fallback_args)) + code = run("npm", fallback_args, app_root) + if code != 0: + return code + else: + return code + node_modules.mkdir(parents=True, exist_ok=True) + deps_hash_file.write_text(expected_hash, encoding="utf-8") + else: + print("Skipping dependency install.") + + if args.target == "web": + cmd = ["run", "build"] + print("$ npm " + " ".join(cmd)) + if not args.dry_run: + return run("npm", cmd, app_root) + return 0 + + tauri_cmd = ["run", "tauri", "build"] + tauri_tail: list[str] = [] + if args.tauri_bundles == "none": + tauri_tail.extend(["--no-bundle"]) + else: + tauri_tail.extend(["--bundles", args.tauri_bundles]) + if args.configuration == "Debug": + tauri_tail.append("--debug") + if tauri_tail: + tauri_cmd.extend(["--", *tauri_tail]) + + print("$ npm " + " ".join(tauri_cmd)) + if not args.dry_run: + return run("npm", tauri_cmd, app_root) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/publish-output.py b/scripts/publish-output.py new file mode 100644 index 0000000..82c4aaa --- /dev/null +++ b/scripts/publish-output.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +import argparse +import json +import shutil +import subprocess +import sys +from pathlib import Path + +from script_common import find_csproj_by_keyword, find_node_app_root, resolve_repo_root + + +def run_step(label: str, cmd: list[str], cwd: Path, dry_run: bool) -> int: + print(f"\n> {label}") + print("$", " ".join(cmd)) + if dry_run: + return 0 + proc = subprocess.run(cmd, cwd=str(cwd), check=False) + return proc.returncode + + +def has_package_script(app_root: Path, script_name: str) -> bool: + package_json = app_root / "package.json" + if not package_json.exists(): + return False + try: + data = json.loads(package_json.read_text(encoding="utf-8")) + except Exception: + return False + scripts = data.get("scripts") + if not isinstance(scripts, dict): + return False + value = scripts.get(script_name) + return isinstance(value, str) and value.strip() != "" + + +def main() -> int: + parser = argparse.ArgumentParser(description="Publish bundled outputs using Python script entrypoints") + parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release") + parser.add_argument("--runtime", default="win-x64") + parser.add_argument("--skip-sidecar", action="store_true") + parser.add_argument("--skip-web", action="store_true") + parser.add_argument("--skip-webgateway", action="store_true") + parser.add_argument("--skip-tauri", action="store_true") + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--repo-root", default=None) + parser.add_argument("--sidecar-project", default=None) + parser.add_argument("--gateway-project", default=None) + parser.add_argument("--app-root", default=None) + parser.add_argument("--output-dir", default="output") + args = parser.parse_args() + + repo_root = resolve_repo_root(args.repo_root) + output_root = (repo_root / args.output_dir).resolve() + output_root.mkdir(parents=True, exist_ok=True) + + sidecar_project = (repo_root / args.sidecar_project).resolve() if args.sidecar_project else find_csproj_by_keyword(repo_root, ["sidecar"]) + gateway_project = (repo_root / args.gateway_project).resolve() if args.gateway_project else find_csproj_by_keyword(repo_root, ["webgateway", "gateway"]) + app_root = (repo_root / args.app_root).resolve() if args.app_root else find_node_app_root(repo_root, None) + tauri_conf = None + if app_root is not None: + candidate_a = app_root / "src-tauri" / "tauri.conf.json" + candidate_b = app_root / "tauri.conf.json" + if candidate_a.exists(): + tauri_conf = candidate_a + elif candidate_b.exists(): + tauri_conf = candidate_b + + py = sys.executable + if not args.skip_sidecar: + if sidecar_project is None: + print("Skipping sidecar: no sidecar csproj detected.") + else: + cmd = [py, "scripts/publish-sidecar.py", "--configuration", args.configuration, "--runtime", args.runtime] + cmd.extend(["--project", str(sidecar_project)]) + code = run_step("Publish sidecar", cmd, repo_root, args.dry_run) + if code != 0: + return code + + if not args.skip_web: + if app_root is None: + print("Skipping web: no app root with package.json detected.") + elif not has_package_script(app_root, "build"): + print("Skipping web: package.json has no 'build' script.") + else: + cmd = [py, "scripts/publish-app.py", "--target", "web", "--configuration", args.configuration, "--app-root", str(app_root)] + code = run_step("Build web", cmd, repo_root, args.dry_run) + if code != 0: + return code + + if not args.skip_webgateway: + if gateway_project is None: + print("Skipping web gateway: no gateway csproj detected.") + else: + cmd = [py, "scripts/publish-webgateway.py", "--configuration", args.configuration, "--runtime", args.runtime, "--project", str(gateway_project)] + code = run_step("Publish web gateway", cmd, repo_root, args.dry_run) + if code != 0: + return code + + if not args.skip_tauri: + if app_root is None or tauri_conf is None: + print("Skipping tauri: tauri app not detected.") + elif not has_package_script(app_root, "tauri"): + print("Skipping tauri: package.json has no 'tauri' script.") + else: + cmd = [py, "scripts/publish-app.py", "--target", "tauri", "--configuration", args.configuration, "--tauri-bundles", "none", "--app-root", str(app_root)] + code = run_step("Build tauri", cmd, repo_root, args.dry_run) + if code != 0: + return code + + target_dir = app_root / "src-tauri" / "target" / ("debug" if args.configuration == "Debug" else "release") + exes = sorted(target_dir.glob("*.exe"), key=lambda p: p.stat().st_mtime, reverse=True) + if exes: + staged = output_root / exes[0].name + if args.dry_run: + print(f"Would copy: {exes[0]} -> {staged}") + else: + shutil.copy2(exes[0], staged) + print(f"Staged desktop executable: {staged}") + + print("\nPublish output workflow complete.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/publish-sidecar.ps1 b/scripts/publish-sidecar.ps1 new file mode 100644 index 0000000..692ff55 --- /dev/null +++ b/scripts/publish-sidecar.ps1 @@ -0,0 +1,10 @@ +param( + [Parameter(ValueFromRemainingArguments = $($true))] + [string[]]$ForwardArgs +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. (Join-Path $PSScriptRoot '_pwsh-python-shim.ps1') +Invoke-SdtPythonScript -ScriptName 'publish-sidecar.py' -ForwardArgs $ForwardArgs diff --git a/scripts/publish-sidecar.py b/scripts/publish-sidecar.py new file mode 100644 index 0000000..964f750 --- /dev/null +++ b/scripts/publish-sidecar.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import argparse + + +from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run + + +def main() -> int: + parser = argparse.ArgumentParser(description="Cross-platform .NET sidecar publish helper") + parser.add_argument("--configuration", default="Release") + parser.add_argument("--runtime", default="win-x64") + parser.add_argument("--repo-root", default=None) + parser.add_argument("--project", default=None, help="Relative/absolute path to sidecar csproj") + parser.add_argument("--output-dir", default="output") + args = parser.parse_args() + + repo_root = resolve_repo_root(args.repo_root) + output_dir = (repo_root / args.output_dir).resolve() + output_dir.mkdir(parents=True, exist_ok=True) + + if args.project: + csproj = (repo_root / args.project).resolve() + else: + csproj = find_csproj_by_keyword(repo_root, ["sidecar"]) + if csproj is None or not csproj.exists(): + print("Could not locate sidecar project. Pass --project .") + return 2 + + publish_args = [ + "publish", + str(csproj), + "-c", + args.configuration, + "-r", + args.runtime, + "--self-contained", + "-p:PublishSingleFile=true", + "-p:IncludeNativeLibrariesForSelfExtract=true", + "-p:RestoreIgnoreFailedSources=true", + "-p:NuGetAudit=false", + "-o", + str(output_dir), + ] + code = run("dotnet", publish_args, repo_root, env=dotnet_env(repo_root)) + if code != 0: + return code + + binary_name = csproj.stem + (".exe" if args.runtime.startswith("win-") else "") + binary_path = output_dir / binary_name + if binary_path.exists(): + print(f"Published executable: {binary_path}") + else: + print(f"Publish completed. Output directory: {output_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/publish-webgateway.py b/scripts/publish-webgateway.py new file mode 100644 index 0000000..6a2c9c0 --- /dev/null +++ b/scripts/publish-webgateway.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +import argparse +import shutil + +from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run + + +def main() -> int: + parser = argparse.ArgumentParser(description="Cross-platform ASP.NET gateway publish helper") + parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release") + parser.add_argument("--runtime", default="win-x64") + parser.add_argument("--self-contained", action="store_true") + parser.add_argument("--skip-web-assets", action="store_true") + parser.add_argument("--repo-root", default=None) + parser.add_argument("--project", default=None, help="Relative/absolute path to gateway csproj") + parser.add_argument("--web-build-dir", default=None, help="Relative path to web build assets root") + parser.add_argument("--output-dir", default="output/webgateway") + args = parser.parse_args() + + repo_root = resolve_repo_root(args.repo_root) + output_dir = (repo_root / args.output_dir).resolve() + output_dir.mkdir(parents=True, exist_ok=True) + + if args.project: + csproj = (repo_root / args.project).resolve() + else: + csproj = find_csproj_by_keyword(repo_root, ["webgateway", "gateway"]) + if csproj is None or not csproj.exists(): + print("Could not locate web gateway project. Pass --project .") + return 2 + + publish_args = [ + "publish", + str(csproj), + "-c", + args.configuration, + "-r", + args.runtime, + "--self-contained", + "true" if args.self_contained else "false", + "-p:RestoreIgnoreFailedSources=true", + "-p:NuGetAudit=false", + "-o", + str(output_dir), + ] + code = run("dotnet", publish_args, repo_root, env=dotnet_env(repo_root)) + if code != 0: + return code + + if not args.skip_web_assets: + if args.web_build_dir: + web_build_dir = (repo_root / args.web_build_dir).resolve() + else: + web_build_dir = next((p.parent for p in repo_root.rglob("package.json") if (p.parent / "build").exists()), None) + if web_build_dir is not None: + web_build_dir = web_build_dir / "build" + + if web_build_dir is None or not web_build_dir.exists(): + print("Web assets not found. Skip with --skip-web-assets or pass --web-build-dir.") + else: + web_out = output_dir / "wwwroot" + web_out.mkdir(parents=True, exist_ok=True) + for item in web_build_dir.iterdir(): + dst = web_out / item.name + if item.is_dir(): + if dst.exists(): + shutil.rmtree(dst) + shutil.copytree(item, dst) + else: + shutil.copy2(item, dst) + print(f"Copied web assets: {web_out}") + + print(f"Publish completed: {output_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run-webgateway.py b/scripts/run-webgateway.py new file mode 100644 index 0000000..3f35dbc --- /dev/null +++ b/scripts/run-webgateway.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +import argparse +import os +from pathlib import Path + +from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run + + +def main() -> int: + parser = argparse.ArgumentParser(description="Run gateway in dev or output mode") + parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release") + parser.add_argument("--urls", default="http://0.0.0.0:5180") + parser.add_argument("--project-root", default=None, help="Runtime project root exposed via SDT_PROJECT_ROOT") + parser.add_argument("--mode", choices=["Dev", "Output"], default="Dev") + parser.add_argument("--repo-root", default=None) + parser.add_argument("--project", default=None, help="Gateway csproj path") + parser.add_argument("--output-exe", default=None, help="Published gateway executable path") + args = parser.parse_args() + + repo_root = resolve_repo_root(args.repo_root) + effective_project_root = Path(args.project_root).resolve() if args.project_root else repo_root + if not effective_project_root.exists(): + print(f"Project root does not exist: {effective_project_root}") + return 2 + + env = dotnet_env(repo_root) + env["SDT_PROJECT_ROOT"] = str(effective_project_root) + + if args.mode == "Output": + exe_path = Path(args.output_exe).resolve() if args.output_exe else (repo_root / "output" / "webgateway" / ("webgateway.exe" if os.name == "nt" else "webgateway")) + if not exe_path.exists(): + print(f"Output executable not found: {exe_path}") + return 2 + return run(str(exe_path), ["--urls", args.urls], repo_root, env=env) + + if args.project: + csproj = (repo_root / args.project).resolve() + else: + csproj = find_csproj_by_keyword(repo_root, ["webgateway", "gateway"]) + if csproj is None or not csproj.exists(): + print("Could not locate gateway project. Pass --project .") + return 2 + + run_args = [ + "run", + "--project", + str(csproj), + "-c", + args.configuration, + "--no-launch-profile", + "--urls", + args.urls, + "-p:RestoreIgnoreFailedSources=true", + "-p:NuGetAudit=false", + ] + return run("dotnet", run_args, repo_root, env=env) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/script-common.ps1 b/scripts/script-common.ps1 new file mode 100644 index 0000000..36e681e --- /dev/null +++ b/scripts/script-common.ps1 @@ -0,0 +1,124 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Legacy compatibility helper only. +# Active SDT workflows and shell bootstrap now use Python scripts. + +function Clear-SdtProxyEnv { + Remove-Item Env:HTTP_PROXY -ErrorAction SilentlyContinue + Remove-Item Env:HTTPS_PROXY -ErrorAction SilentlyContinue + Remove-Item Env:ALL_PROXY -ErrorAction SilentlyContinue + Remove-Item Env:http_proxy -ErrorAction SilentlyContinue + Remove-Item Env:https_proxy -ErrorAction SilentlyContinue + Remove-Item Env:all_proxy -ErrorAction SilentlyContinue + Remove-Item Env:GIT_HTTP_PROXY -ErrorAction SilentlyContinue + Remove-Item Env:GIT_HTTPS_PROXY -ErrorAction SilentlyContinue + Remove-Item Env:PIP_NO_INDEX -ErrorAction SilentlyContinue +} + +function Resolve-SdtRepoRoot { + param([string]$StartPath) + + $candidateStarts = @() + if (-not [string]::IsNullOrWhiteSpace($StartPath)) { + $candidateStarts += $StartPath + } + $cwd = (Get-Location).Path + if (-not [string]::IsNullOrWhiteSpace($cwd) -and ($candidateStarts -notcontains $cwd)) { + $candidateStarts += $cwd + } + + $override = $env:SDT_REPO_ROOT + if ([string]::IsNullOrWhiteSpace($override)) { + $override = $env:JOURNAL_REPO_ROOT # backward compatibility + } + if (-not [string]::IsNullOrWhiteSpace($override)) { + $overridePath = [System.IO.Path]::GetFullPath($override) + if (Test-Path (Join-Path $overridePath "devtool.json")) { + return $overridePath + } + } + + foreach ($start in $candidateStarts) { + $cursor = [System.IO.Path]::GetFullPath($start) + while (-not [string]::IsNullOrWhiteSpace($cursor)) { + if (Test-Path (Join-Path $cursor "devtool.json")) { + return $cursor + } + $parent = [System.IO.Directory]::GetParent($cursor) + if ($null -eq $parent -or $parent.FullName -eq $cursor) { + break + } + $cursor = $parent.FullName + } + } + + if (Get-Command git -ErrorAction SilentlyContinue) { + foreach ($start in $candidateStarts) { + try { + $gitRoot = & git -C $start rev-parse --show-toplevel 2>$null + if ($? -and -not [string]::IsNullOrWhiteSpace($gitRoot)) { + return [System.IO.Path]::GetFullPath($gitRoot.Trim()) + } + } + catch {} + } + } + + throw "Could not locate repository root. Ensure a devtool.json exists in the project root." +} + +function Initialize-SdtDotnetEnv { + param([Parameter(Mandatory = $true)][string]$RepoRoot) + + $dotnetCliHome = Join-Path $RepoRoot ".dotnet_home" + $nugetPackages = Join-Path $RepoRoot ".nuget\packages" + $nugetHttpCachePath = Join-Path $RepoRoot ".nuget\http-cache" + + $env:DOTNET_CLI_HOME = $dotnetCliHome + $env:NUGET_PACKAGES = $nugetPackages + $env:NUGET_HTTP_CACHE_PATH = $nugetHttpCachePath + $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = "1" + $env:DOTNET_ADD_GLOBAL_TOOLS_TO_PATH = "0" + $env:DOTNET_GENERATE_ASPNET_CERTIFICATE = "0" + $env:DOTNET_CLI_TELEMETRY_OPTOUT = "1" + $env:NUGET_CERT_REVOCATION_MODE = "offline" + + New-Item -ItemType Directory -Force -Path $dotnetCliHome, $nugetPackages, $nugetHttpCachePath | Out-Null +} + +function Initialize-SdtPipEnv { + param([Parameter(Mandatory = $true)][string]$RepoRoot) + + $pipCacheDir = Join-Path $RepoRoot ".pip\cache" + $pipTempDir = Join-Path $RepoRoot ".tmp\pip-temp" + + $env:PIP_CACHE_DIR = $pipCacheDir + $env:TEMP = $pipTempDir + $env:TMP = $pipTempDir + $env:PIP_DISABLE_PIP_VERSION_CHECK = "1" + $env:PIP_DEFAULT_TIMEOUT = "30" + $env:PIP_RETRIES = "2" + + New-Item -ItemType Directory -Force -Path $pipCacheDir, $pipTempDir | Out-Null +} + +function Initialize-SdtHuggingFaceEnv { + param([Parameter(Mandatory = $true)][string]$RepoRoot) + + $hfHome = Join-Path $RepoRoot ".cache\huggingface" + $hfHubCache = Join-Path $hfHome "hub" + + $env:HF_HOME = $hfHome + $env:HUGGINGFACE_HUB_CACHE = $hfHubCache + $env:HF_HUB_DISABLE_SYMLINKS_WARNING = "1" + + New-Item -ItemType Directory -Force -Path $hfHubCache | Out-Null +} + +# Backward-compatible aliases (legacy script calls) +Set-Alias -Name Clear-JournalProxyEnv -Value Clear-SdtProxyEnv -Scope Script +Set-Alias -Name Resolve-JournalRepoRoot -Value Resolve-SdtRepoRoot -Scope Script +Set-Alias -Name Initialize-JournalDotnetEnv -Value Initialize-SdtDotnetEnv -Scope Script +Set-Alias -Name Initialize-JournalPipEnv -Value Initialize-SdtPipEnv -Scope Script +Set-Alias -Name Initialize-JournalHuggingFaceEnv -Value Initialize-SdtHuggingFaceEnv -Scope Script diff --git a/scripts/script_common.py b/scripts/script_common.py new file mode 100644 index 0000000..76ac87d --- /dev/null +++ b/scripts/script_common.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 +import hashlib +import json +import os +import pathlib +import shutil +import subprocess +import sys +from typing import Dict, Iterable, List, Sequence + + +PROXY_VARS = [ + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", + "GIT_HTTP_PROXY", + "GIT_HTTPS_PROXY", + "PIP_NO_INDEX", +] + + +def resolve_repo_root(start: str | None = None) -> pathlib.Path: + base = pathlib.Path(start or os.getcwd()).resolve() + + # Preferred marker for SDT-managed projects. + for cur in [base, *base.parents]: + cfg = cur / "devtool.json" + if cfg.exists(): + hints = load_project_root_hints(cur) + if not hints: + return cur + if any(_hint_matches(cur, hint) for hint in hints): + return cur + + # Fall back to git root when available. + try: + proc = subprocess.run( + ["git", "-C", str(base), "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=False, + ) + if proc.returncode == 0: + git_root = proc.stdout.strip() + if git_root: + return pathlib.Path(git_root).resolve() + except Exception: + pass + + return base + + +def load_project_root_hints(repo_root: pathlib.Path) -> list[str]: + cfg = repo_root / "devtool.json" + if not cfg.exists(): + return [] + try: + data = json.loads(cfg.read_text(encoding="utf-8")) + hints = data.get("project", {}).get("rootHints", []) + return [str(x) for x in hints if isinstance(x, str) and x.strip()] + except Exception: + return [] + + +def ensure_dirs(paths: List[pathlib.Path]) -> None: + for p in paths: + p.mkdir(parents=True, exist_ok=True) + + +def clean_proxy_env(env: Dict[str, str]) -> None: + for k in PROXY_VARS: + env.pop(k, None) + + +def dotnet_env(repo_root: pathlib.Path) -> Dict[str, str]: + env = dict(os.environ) + clean_proxy_env(env) + dotnet_cli_home = repo_root / ".dotnet_home" + nuget_packages = repo_root / ".nuget" / "packages" + nuget_http_cache = repo_root / ".nuget" / "http-cache" + ensure_dirs([dotnet_cli_home, nuget_packages, nuget_http_cache]) + env["DOTNET_CLI_HOME"] = str(dotnet_cli_home) + env["NUGET_PACKAGES"] = str(nuget_packages) + env["NUGET_HTTP_CACHE_PATH"] = str(nuget_http_cache) + env["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1" + env["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "0" + env["DOTNET_GENERATE_ASPNET_CERTIFICATE"] = "0" + env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1" + env["NUGET_CERT_REVOCATION_MODE"] = "offline" + return env + + +def pip_env(repo_root: pathlib.Path) -> Dict[str, str]: + env = dict(os.environ) + clean_proxy_env(env) + pip_cache = repo_root / ".pip" / "cache" + pip_tmp = repo_root / ".tmp" / "pip-temp" + ensure_dirs([pip_cache, pip_tmp]) + env["PIP_CACHE_DIR"] = str(pip_cache) + env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" + env["PIP_DEFAULT_TIMEOUT"] = "30" + env["PIP_RETRIES"] = "2" + env["TEMP"] = str(pip_tmp) + env["TMP"] = str(pip_tmp) + return env + + +def run(command: str, args: List[str], cwd: pathlib.Path, env: Dict[str, str] | None = None) -> int: + resolved = resolve_command(command) + try: + proc = subprocess.run([resolved, *args], cwd=str(cwd), env=env, check=False) + return proc.returncode + except FileNotFoundError: + print(f"Command not found: {resolved}", file=sys.stderr) + return 127 + + +def run_capture(command: str, args: Sequence[str], cwd: pathlib.Path, env: Dict[str, str] | None = None) -> tuple[int, str, str]: + resolved = resolve_command(command) + try: + proc = subprocess.run( + [resolved, *args], + cwd=str(cwd), + env=env, + capture_output=True, + text=True, + check=False, + ) + return proc.returncode, proc.stdout, proc.stderr + except FileNotFoundError: + return 127, "", f"Command not found: {resolved}" + + +def resolve_command(command: str) -> str: + if not command: + return command + + if os.name != "nt": + return command + + if any(sep in command for sep in ("\\", "/")): + return command + + if pathlib.Path(command).suffix: + found = shutil.which(command) + return found or command + + candidates = [] + lowered = command.lower() + if lowered in ("npm", "npx", "pnpm", "yarn", "tauri"): + candidates.extend([f"{command}.cmd", f"{command}.exe", f"{command}.bat", command]) + else: + candidates.append(command) + + for c in candidates: + found = _which_windows(c) + if found: + name = pathlib.Path(found).name.lower() + if name in ("npm", "npx", "pnpm", "yarn", "tauri"): + shim = pathlib.Path(found).with_name(name + ".cmd") + if shim.exists(): + return str(shim) + return found + + if lowered in ("npm", "npx", "pnpm", "yarn"): + node = _which_windows("node.exe") or _which_windows("node") + if node: + node_dir = pathlib.Path(node).parent + shim = node_dir / f"{lowered}.cmd" + if shim.exists(): + return str(shim) + + return candidates[-1] + + +def _hint_matches(root: pathlib.Path, hint: str) -> bool: + h = hint.strip() + if not h: + return False + + has_glob = any(ch in h for ch in ("*", "?", "[")) + if has_glob: + # Match both anywhere in root and directly at root-level for common hints like "*.sln". + if any(root.glob(h)): + return True + return any(root.rglob(h)) + + marker = root / h + if marker.exists(): + return True + + # If hint is just a filename marker, look bounded in tree. + if not any(sep in h for sep in ("\\", "/")): + return any(p.name == h for p in root.rglob(h)) + + return False + + +def _expand_windows_path_segment(segment: str) -> str: + expanded = segment + # Expand %VAR% tokens repeatedly for nested references. + for _ in range(4): + next_value = os.path.expandvars(expanded) + if next_value == expanded: + break + expanded = next_value + return expanded + + +def _which_windows(command: str) -> str | None: + found = shutil.which(command) + if found: + return found + + if os.name != "nt": + return None + + path_value = os.environ.get("PATH", "") + pathext = os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD") + exts = [e.lower() for e in pathext.split(";") if e] + + has_ext = pathlib.Path(command).suffix != "" + names = [command] if has_ext else [command, *(command + e.lower() for e in exts)] + + for raw_segment in path_value.split(os.pathsep): + segment = _expand_windows_path_segment(raw_segment.strip()) + if not segment: + continue + base = pathlib.Path(segment) + for name in names: + candidate = base / name + if candidate.exists(): + return str(candidate) + + return None + + +def sha256_files(paths: Iterable[pathlib.Path]) -> str: + h = hashlib.sha256() + for p in paths: + if not p.exists(): + continue + h.update(p.read_bytes()) + return h.hexdigest() + + +def first_existing(paths: Iterable[pathlib.Path]) -> pathlib.Path | None: + for p in paths: + if p.exists(): + return p + return None + + +def find_csproj(repo_root: pathlib.Path, hints: Sequence[str] | None = None) -> pathlib.Path | None: + if hints: + for hint in hints: + candidate = (repo_root / hint).resolve() + if candidate.exists() and candidate.suffix.lower() == ".csproj": + return candidate + + csprojs = sorted(repo_root.rglob("*.csproj")) + if not csprojs: + return None + if len(csprojs) == 1: + return csprojs[0] + return None + + +def find_csproj_by_keyword(repo_root: pathlib.Path, keywords: Sequence[str]) -> pathlib.Path | None: + kws = [k.lower() for k in keywords] + matches: list[pathlib.Path] = [] + for p in repo_root.rglob("*.csproj"): + text = str(p).lower() + if any(k in text for k in kws): + matches.append(p) + if len(matches) == 1: + return matches[0] + return None + + +def find_node_app_root(repo_root: pathlib.Path, preferred: str | None = None) -> pathlib.Path | None: + if preferred: + p = (repo_root / preferred).resolve() + if (p / "package.json").exists(): + return p + + direct = repo_root / "package.json" + if direct.exists(): + return repo_root + + tauri_candidates = [] + for package_json in repo_root.rglob("package.json"): + d = package_json.parent + if (d / "src-tauri" / "tauri.conf.json").exists(): + tauri_candidates.append(d) + if len(tauri_candidates) == 1: + return tauri_candidates[0] + + all_candidates = [p.parent for p in repo_root.rglob("package.json")] + if len(all_candidates) == 1: + return all_candidates[0] + return None + + +def newest_file(search_root: pathlib.Path, pattern: str) -> pathlib.Path | None: + if not search_root.exists(): + return None + files = [p for p in search_root.rglob(pattern) if p.is_file() and "\\obj\\" not in str(p).replace("/", "\\")] + if not files: + return None + files.sort(key=lambda p: p.stat().st_mtime, reverse=True) + return files[0] diff --git a/scripts/sync-output.py b/scripts/sync-output.py new file mode 100644 index 0000000..8a28a2b --- /dev/null +++ b/scripts/sync-output.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +import argparse +import os +import shutil +from pathlib import Path + +from script_common import newest_file, resolve_repo_root + + +def copy_tree_contents(src: Path, dst: Path) -> None: + dst.mkdir(parents=True, exist_ok=True) + for item in src.iterdir(): + target = dst / item.name + if item.is_dir(): + if target.exists(): + shutil.rmtree(target) + shutil.copytree(item, target) + else: + shutil.copy2(item, target) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Sync newest built assets into output folder") + parser.add_argument("--repo-root", default=None) + parser.add_argument("--output-dir", default="output") + parser.add_argument("--web-build-dir", default=None, help="Path to web build output") + parser.add_argument("--sidecar-bin-dir", default=None, help="Path to sidecar bin root") + parser.add_argument("--gateway-bin-dir", default=None, help="Path to gateway bin root") + parser.add_argument("--tauri-target-dir", default=None, help="Path to tauri target root") + args = parser.parse_args() + + repo_root = resolve_repo_root(args.repo_root) + output_dir = (repo_root / args.output_dir).resolve() + output_dir.mkdir(parents=True, exist_ok=True) + + web_build = (repo_root / args.web_build_dir).resolve() if args.web_build_dir else None + if web_build is None: + web_build = next((p for p in repo_root.rglob("build") if (p.parent / "package.json").exists()), None) + if web_build is not None and web_build.exists(): + web_out = output_dir / "webgateway" / "wwwroot" + copy_tree_contents(web_build, web_out) + print(f"Synced web assets -> {web_out}") + + sidecar_bin = (repo_root / args.sidecar_bin_dir).resolve() if args.sidecar_bin_dir else None + if sidecar_bin is None: + sidecar_proj = next((p.parent for p in repo_root.rglob("*.csproj") if "sidecar" in str(p).lower()), None) + sidecar_bin = sidecar_proj / "bin" if sidecar_proj else None + if sidecar_bin is not None: + sidecar_pattern = "*.exe" if os.name == "nt" else "*" + sidecar_exe = newest_file(sidecar_bin, sidecar_pattern) + if sidecar_exe is not None: + copy_tree_contents(sidecar_exe.parent, output_dir) + print(f"Synced sidecar -> {output_dir}") + + gateway_bin = (repo_root / args.gateway_bin_dir).resolve() if args.gateway_bin_dir else None + if gateway_bin is None: + gateway_proj = next((p.parent for p in repo_root.rglob("*.csproj") if "gateway" in str(p).lower()), None) + gateway_bin = gateway_proj / "bin" if gateway_proj else None + if gateway_bin is not None: + gateway_pattern = "*.exe" if os.name == "nt" else "*" + gw_exe = newest_file(gateway_bin, gateway_pattern) + if gw_exe is not None: + gw_out = output_dir / "webgateway" + copy_tree_contents(gw_exe.parent, gw_out) + print(f"Synced gateway -> {gw_out}") + + tauri_target = (repo_root / args.tauri_target_dir).resolve() if args.tauri_target_dir else None + if tauri_target is None: + tauri_target = next((p for p in repo_root.rglob("src-tauri") if (p / "target").exists()), None) + tauri_target = tauri_target / "target" if tauri_target else None + if tauri_target is not None: + app_exe = newest_file(tauri_target, "*.exe") + if app_exe is not None: + shutil.copy2(app_exe, output_dir / app_exe.name) + print(f"Synced desktop app ({app_exe.name}) -> {output_dir}") + + print("Sync complete.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/DevTool.Engine/Config/ConfigBootstrapper.cs b/src/DevTool.Engine/Config/ConfigBootstrapper.cs new file mode 100644 index 0000000..d9a8a3d --- /dev/null +++ b/src/DevTool.Engine/Config/ConfigBootstrapper.cs @@ -0,0 +1,869 @@ +using System.Text.Json; + +namespace Sdt.Config; + +public sealed record BootstrapScanResult( + string ProjectRoot, + string ProjectName, + string ProjectType, + IReadOnlyList ToolFamilies, + string? DotnetWorkingDir, + string? NodeWorkingDir, + string? PythonRequirementsFile, + bool HasDockerCompose, + IReadOnlyList RootHints); + +public static class ConfigBootstrapper +{ + private const int MaxScanDepth = 4; + + private static readonly HashSet ExcludedDirectories = new(StringComparer.OrdinalIgnoreCase) + { + ".git", + "node_modules", + ".venv", + "venv", + "bin", + "obj", + ".idea", + ".vscode", + "dist", + "build", + ".sdt", + }; + + private static readonly string[] RequirementCandidates = + [ + "requirements.txt", + "requirements-dev.txt", + "requirements_cpu_only.txt", + "requirements_gpu.txt", + ]; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + public static BootstrapScanResult Scan(string startDir) + { + var root = FindProjectRoot(startDir); + var start = Path.GetFullPath(startDir); + var toolFamilies = new HashSet(StringComparer.OrdinalIgnoreCase); + var rootHints = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (Directory.Exists(Path.Combine(root, ".git"))) + { + toolFamilies.Add("git"); + rootHints.Add(".git"); + } + + var slnxCandidates = EnumerateFilesBounded(root, "*.slnx", MaxScanDepth).ToList(); + var slnCandidates = EnumerateFilesBounded(root, "*.sln", MaxScanDepth).ToList(); + var csprojCandidates = EnumerateFilesBounded(root, "*.csproj", MaxScanDepth).ToList(); + var hasTopLevelSlnx = slnxCandidates.Any(p => Path.GetDirectoryName(p) == root); + var hasTopLevelSln = hasTopLevelSlnx || slnCandidates.Any(p => Path.GetDirectoryName(p) == root); + var hasCsproj = hasTopLevelSln || csprojCandidates.Count > 0; + string? dotnetWorkingDir = null; + if (hasCsproj) + { + toolFamilies.Add("dotnet"); + dotnetWorkingDir = ResolveDotnetWorkingDir(root, start, slnxCandidates, slnCandidates, csprojCandidates); + if (hasTopLevelSlnx) + rootHints.Add("*.slnx"); + rootHints.Add("*.sln"); + } + + var packageJson = SelectNodePackageJson(root); + string? nodeWorkingDir = null; + if (packageJson is not null) + { + toolFamilies.Add("node"); + toolFamilies.Add("npm"); + nodeWorkingDir = Path.GetRelativePath(root, Path.GetDirectoryName(packageJson)!); + rootHints.Add("package.json"); + } + + string? requirements = RequirementCandidates + .Select(name => Path.Combine(root, name)) + .FirstOrDefault(File.Exists); + if (requirements is null) + { + var pyproject = Path.Combine(root, "pyproject.toml"); + if (File.Exists(pyproject)) + { + toolFamilies.Add("python"); + rootHints.Add("pyproject.toml"); + } + } + else + { + toolFamilies.Add("python"); + rootHints.Add(Path.GetFileName(requirements)!); + } + + var hasCargo = File.Exists(Path.Combine(root, "Cargo.toml")) || + EnumerateFilesBounded(root, "Cargo.toml", MaxScanDepth).Any(); + if (hasCargo) + { + toolFamilies.Add("cargo"); + rootHints.Add("Cargo.toml"); + } + + var hasTauri = File.Exists(Path.Combine(root, "tauri.conf.json")) || + EnumerateFilesBounded(root, "tauri.conf.json", MaxScanDepth).Any(); + if (hasTauri) + { + toolFamilies.Add("tauri"); + toolFamilies.Add("cargo"); + toolFamilies.Add("node"); + toolFamilies.Add("npm"); + rootHints.Add("tauri.conf.json"); + } + + var hasGo = File.Exists(Path.Combine(root, "go.mod")) || + EnumerateFilesBounded(root, "go.mod", MaxScanDepth).Any(); + if (hasGo) + { + toolFamilies.Add("go"); + rootHints.Add("go.mod"); + } + + var hasMaven = File.Exists(Path.Combine(root, "pom.xml")) || + EnumerateFilesBounded(root, "pom.xml", MaxScanDepth).Any(); + if (hasMaven) + { + toolFamilies.Add("maven"); + toolFamilies.Add("java"); + rootHints.Add("pom.xml"); + } + + var hasGradle = File.Exists(Path.Combine(root, "build.gradle")) || + File.Exists(Path.Combine(root, "build.gradle.kts")) || + EnumerateFilesBounded(root, "build.gradle", MaxScanDepth).Any() || + EnumerateFilesBounded(root, "build.gradle.kts", MaxScanDepth).Any(); + if (hasGradle) + { + toolFamilies.Add("gradle"); + toolFamilies.Add("java"); + rootHints.Add("build.gradle"); + } + + var hasDockerCompose = File.Exists(Path.Combine(root, "docker-compose.yml")) || + File.Exists(Path.Combine(root, "docker-compose.yaml")); + if (hasDockerCompose || File.Exists(Path.Combine(root, "Dockerfile"))) + { + toolFamilies.Add("docker"); + rootHints.Add(hasDockerCompose ? "docker-compose.yml" : "Dockerfile"); + } + + var scriptsDir = Path.Combine(root, "scripts"); + if (Directory.Exists(scriptsDir) && + Directory.EnumerateFiles(scriptsDir, "*.py", SearchOption.TopDirectoryOnly).Any()) + { + toolFamilies.Add("python"); + rootHints.Add("scripts"); + } + + if (rootHints.Count == 0) + rootHints.Add("devtool.json"); + + return new BootstrapScanResult( + ProjectRoot: root, + ProjectName: new DirectoryInfo(root).Name, + ProjectType: InferProjectType(toolFamilies), + ToolFamilies: toolFamilies.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(), + DotnetWorkingDir: dotnetWorkingDir, + NodeWorkingDir: nodeWorkingDir, + PythonRequirementsFile: requirements is null ? null : Path.GetRelativePath(root, requirements), + HasDockerCompose: hasDockerCompose, + RootHints: rootHints.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList()); + } + + public static DevToolConfig BuildDefaultConfig(BootstrapScanResult scan) + { + var workflows = BuildWorkflows(scan).ToList(); + + var toolingTools = scan.ToolFamilies + .Select(t => new ToolInstallDefinition { Tool = t, PreferredInstallCommands = [] }) + .ToList(); + + var toolchains = new ToolchainConfig + { + Python = scan.ToolFamilies.Contains("python", StringComparer.OrdinalIgnoreCase) + ? new PythonToolchain + { + Executable = "python", + WindowsExecutable = "py", + VenvDir = ".venv", + Profiles = [] + } + : null, + Node = scan.ToolFamilies.Contains("node", StringComparer.OrdinalIgnoreCase) + ? new NodeToolchain + { + PackageManager = "npm", + WorkingDir = string.IsNullOrWhiteSpace(scan.NodeWorkingDir) ? "." : scan.NodeWorkingDir + } + : null + }; + + var debugProfiles = BuildDebugProfiles(scan).ToList(); + + return new DevToolConfig + { + Name = scan.ProjectName, + Version = "0.1.0", + Project = new ProjectMetadata + { + Type = scan.ProjectType, + RootHints = scan.RootHints.ToList(), + Artifacts = ["bin", "obj", ".sdt/debug"] + }, + Toolchains = toolchains, + Tooling = new ToolingConfig { Tools = toolingTools }, + Workflows = workflows, + Debug = new DebugConfig + { + Profiles = debugProfiles, + Diagnostics = new DebugDiagnosticsOptions + { + Enabled = true, + OutputDir = ".sdt/debug", + IncludeAllEnv = false, + CaptureEnvKeys = + [ + "SDT_LOG_LEVEL", + "DOTNET_CLI_HOME", + "NUGET_PACKAGES", + "PIP_CACHE_DIR", + "NVM_HOME", + "NVM_SYMLINK", + ], + BundleOnFailure = true + } + }, + Env = + [ + new EnvVarDef + { + Key = "SDT_LOG_LEVEL", + Description = "CLI log verbosity", + DefaultValue = "information", + Options = ["trace", "debug", "information", "warning", "error", "critical"] + } + ], + EnvProfiles = new EnvProfilesConfig + { + Active = "dev", + Profiles = + [ + new EnvProfileDefinition + { + Id = "dev", + Description = "Local development defaults", + Values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["SDT_ENV_PROFILE"] = "dev", + ["SDT_LOG_LEVEL"] = "information", + } + }, + new EnvProfileDefinition + { + Id = "ci", + Description = "Continuous integration defaults", + Inherits = ["dev"], + Values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["SDT_ENV_PROFILE"] = "ci", + ["CI"] = "true", + ["SDT_LOG_LEVEL"] = "warning", + } + }, + new EnvProfileDefinition + { + Id = "release", + Description = "Release build defaults", + Inherits = ["dev"], + Values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["SDT_ENV_PROFILE"] = "release", + ["SDT_LOG_LEVEL"] = "warning", + } + } + ] + } + }; + } + + public static string ToJson(DevToolConfig config) => JsonSerializer.Serialize(config, JsonOptions) + Environment.NewLine; + + public static string WriteDefaultConfig(string projectRoot, DevToolConfig config, bool overwrite = false) + { + var path = Path.Combine(projectRoot, "devtool.json"); + if (File.Exists(path) && !overwrite) + throw new InvalidOperationException($"devtool.json already exists at {path}"); + + File.WriteAllText(path, ToJson(config)); + return path; + } + + private static IEnumerable BuildWorkflows(BootstrapScanResult scan) + { + var has = new Func(tool => scan.ToolFamilies.Contains(tool, StringComparer.OrdinalIgnoreCase)); + var scripts = DetectScriptHelpers(scan.ProjectRoot); + + if (scripts.Contains("publish-sidecar.py") || + scripts.Contains("publish-app.py") || + scripts.Contains("publish-webgateway.py") || + scripts.Contains("publish-output.py") || + scripts.Contains("sync-output.py") || + scripts.Contains("run-webgateway.py")) + { + foreach (var workflow in BuildScriptDrivenWorkflows(scripts)) + yield return workflow; + } + + var buildSteps = new List(); + if (has("dotnet")) buildSteps.Add(StepAction("dotnet-build", "dotnet build", "dotnet-build", scan.DotnetWorkingDir)); + if (has("npm")) buildSteps.Add(StepAction("npm-build", "npm run build", "npm-build", scan.NodeWorkingDir)); + if (has("cargo")) buildSteps.Add(StepAction("cargo-build", "cargo build", "cargo-build")); + if (has("tauri")) buildSteps.Add(StepAction("tauri-build", "tauri build", "tauri-build", scan.NodeWorkingDir ?? ".")); + if (has("go")) buildSteps.Add(StepCommand("go-build", "go build ./...", "go", ["build", "./..."])); + if (has("maven")) buildSteps.Add(StepCommand("maven-build", "mvn package -DskipTests", "mvn", ["package", "-DskipTests"])); + if (has("gradle")) buildSteps.Add(StepCommand("gradle-build", "gradle build", "gradle", ["build"])); + if (buildSteps.Count > 0) + { + yield return new WorkflowDefinition + { + Id = "build", + Label = "Build", + Description = "Build detected project stacks", + Group = "Build", + Steps = buildSteps + }; + } + + var depsSteps = new List(); + if (has("dotnet")) depsSteps.Add(StepAction("dotnet-restore", "dotnet restore", "dotnet-restore", scan.DotnetWorkingDir)); + if (has("npm")) depsSteps.Add(StepAction("npm-ci", "npm ci", "npm-ci", scan.NodeWorkingDir)); + if (has("go")) depsSteps.Add(StepCommand("go-mod-download", "go mod download", "go", ["mod", "download"])); + if (has("maven")) depsSteps.Add(StepCommand("maven-deps", "mvn dependency:resolve", "mvn", ["dependency:resolve"])); + if (has("gradle")) depsSteps.Add(StepCommand("gradle-deps", "gradle dependencies", "gradle", ["dependencies"])); + if (has("python") && !string.IsNullOrWhiteSpace(scan.PythonRequirementsFile)) + { + depsSteps.Add(StepAction("python-pip-sync", "python pip sync", "python-pip-sync", ".", ["--requirements", scan.PythonRequirementsFile!])); + } + if (depsSteps.Count > 0) + { + yield return new WorkflowDefinition + { + Id = "deps-refresh", + Label = "Refresh Dependencies", + Description = "Restore/install dependency stacks", + Group = "Deps", + Steps = depsSteps + }; + } + + var testSteps = new List(); + if (has("dotnet")) testSteps.Add(StepAction("dotnet-test", "dotnet test", "dotnet-test", scan.DotnetWorkingDir)); + if (has("npm")) testSteps.Add(StepAction("npm-test", "npm test", "npm-test", scan.NodeWorkingDir)); + if (has("python")) testSteps.Add(StepAction("python-pytest", "python -m pytest", "python-pytest")); + if (has("cargo")) testSteps.Add(StepAction("cargo-test", "cargo test", "cargo-test")); + if (has("go")) testSteps.Add(StepCommand("go-test", "go test ./...", "go", ["test", "./..."])); + if (has("maven")) testSteps.Add(StepCommand("maven-test", "mvn test", "mvn", ["test"])); + if (has("gradle")) testSteps.Add(StepCommand("gradle-test", "gradle test", "gradle", ["test"])); + if (testSteps.Count > 0) + { + yield return new WorkflowDefinition + { + Id = "test", + Label = "Run Tests", + Description = "Run detected test stacks", + Group = "Test", + Steps = testSteps + }; + } + + if (has("git")) + { + yield return new WorkflowDefinition + { + Id = "repo-health", + Label = "Repo Health", + Description = "Check repo status and fetch remotes", + Group = "Repo", + Steps = + [ + StepAction("git-status", "git status", "git-status"), + StepAction("git-fetch", "git fetch", "git-fetch") + ] + }; + } + + if (has("docker")) + { + yield return new WorkflowDefinition + { + Id = "containers", + Label = "Containers", + Description = scan.HasDockerCompose ? "Manage docker compose stack" : "Build docker image", + Group = "Containers", + Steps = scan.HasDockerCompose + ? [StepAction("docker-compose-up", "docker compose up -d", "docker-compose-up")] + : [StepAction("docker-build", "docker build .", "docker-build")] + }; + } + } + + private static IEnumerable BuildDebugProfiles(BootstrapScanResult scan) + { + var has = new Func(tool => scan.ToolFamilies.Contains(tool, StringComparer.OrdinalIgnoreCase)); + if (has("dotnet")) + { + yield return new DebugProfileDefinition + { + Id = "dotnet-run", + Label = "Run .NET app", + Type = "dotnet", + Command = "dotnet", + Args = ["run"], + WorkingDir = string.IsNullOrWhiteSpace(scan.DotnetWorkingDir) ? "." : scan.DotnetWorkingDir!, + Requires = [new ToolRequirement { Tool = "dotnet", InstallPolicy = InstallPolicy.Prompt }], + Attach = new DebugAttachConfig + { + Kind = "manual", + Note = "Attach your IDE debugger to the running dotnet process." + } + }; + } + + if (has("npm")) + { + yield return new DebugProfileDefinition + { + Id = "npm-dev", + Label = "Run npm dev server", + Type = "node", + Command = "npm", + Args = ["run", "dev"], + WorkingDir = string.IsNullOrWhiteSpace(scan.NodeWorkingDir) ? "." : scan.NodeWorkingDir!, + Requires = + [ + new ToolRequirement { Tool = "node", InstallPolicy = InstallPolicy.Prompt }, + new ToolRequirement { Tool = "npm", InstallPolicy = InstallPolicy.Prompt } + ] + }; + } + } + + private static string? ResolveDotnetWorkingDir( + string root, + string startDir, + IReadOnlyList slnxCandidates, + IReadOnlyList slnCandidates, + IReadOnlyList csprojCandidates) + { + var scored = new List<(int Kind, int Nearness, int Depth, string Dir)>(); + + void AddCandidates(IEnumerable paths, int kind) + { + foreach (var candidate in paths) + { + var dir = Path.GetDirectoryName(candidate); + if (string.IsNullOrWhiteSpace(dir)) + continue; + + var fullDir = Path.GetFullPath(dir); + var nearness = ComputeNearness(fullDir, startDir); + var depth = Path.GetRelativePath(root, fullDir) + .Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + .Count(part => !string.IsNullOrWhiteSpace(part) && part != "."); + scored.Add((kind, nearness, depth, fullDir)); + } + } + + AddCandidates(slnxCandidates, kind: 0); + AddCandidates(slnCandidates, kind: 1); + AddCandidates(csprojCandidates, kind: 2); + + var best = scored + .OrderBy(x => x.Kind) + .ThenBy(x => x.Nearness) + .ThenBy(x => x.Depth) + .ThenBy(x => x.Dir, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(); + + if (string.IsNullOrWhiteSpace(best.Dir)) + return null; + + var rel = Path.GetRelativePath(root, best.Dir); + return string.IsNullOrWhiteSpace(rel) ? "." : rel; + } + + private static int ComputeNearness(string candidateDir, string startDir) + { + var candidate = Path.GetFullPath(candidateDir); + var start = Path.GetFullPath(startDir); + + if (candidate.Equals(start, StringComparison.OrdinalIgnoreCase)) + return 0; + + if (candidate.StartsWith(start + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) + return 1; + + if (start.StartsWith(candidate + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) + return 2; + + return 10; + } + + private static WorkflowStep StepAction( + string id, + string label, + string action, + string? workingDir = null, + IReadOnlyList? actionArgs = null) + { + return new WorkflowStep + { + Id = id, + Label = label, + Action = action, + ActionArgs = actionArgs?.ToList() ?? [], + WorkingDir = string.IsNullOrWhiteSpace(workingDir) ? "." : workingDir + }; + } + + private static WorkflowStep StepCommand( + string id, + string label, + string command, + IReadOnlyList args, + string? workingDir = null) + { + return new WorkflowStep + { + Id = id, + Label = label, + Command = command, + Args = args.ToList(), + WorkingDir = string.IsNullOrWhiteSpace(workingDir) ? "." : workingDir + }; + } + + private static HashSet DetectScriptHelpers(string projectRoot) + { + var scriptsDir = Path.Combine(projectRoot, "scripts"); + if (!Directory.Exists(scriptsDir)) + return new HashSet(StringComparer.OrdinalIgnoreCase); + + return Directory.EnumerateFiles(scriptsDir, "*.py", SearchOption.TopDirectoryOnly) + .Select(Path.GetFileName) + .Where(f => !string.IsNullOrWhiteSpace(f)) + .Cast() + .ToHashSet(StringComparer.OrdinalIgnoreCase); + } + + private static IEnumerable BuildScriptDrivenWorkflows(HashSet scripts) + { + static WorkflowStep ScriptStep(string id, string label, params string[] scriptArgs) => new() + { + Id = id, + Label = label, + Command = "python", + Args = scriptArgs.ToList(), + WorkingDir = ".", + Requires = [new ToolRequirement { Tool = "python", InstallPolicy = InstallPolicy.Prompt }] + }; + + if (scripts.Contains("publish-sidecar.py")) + { + yield return new WorkflowDefinition + { + Id = "sidecar", + Label = "Publish Sidecar", + Description = "Publish sidecar service", + Group = "Build", + Steps = [ScriptStep("sidecar:run", "python scripts/publish-sidecar.py", "scripts/publish-sidecar.py")] + }; + } + + if (scripts.Contains("publish-app.py")) + { + yield return new WorkflowDefinition + { + Id = "web", + Label = "Build Web UI", + Description = "Build frontend assets", + Group = "Build", + Steps = + [ + ScriptStep("web:run", "python scripts/publish-app.py --target web", "scripts/publish-app.py", "--target", "web") + ] + }; + + yield return new WorkflowDefinition + { + Id = "tauri", + Label = "Build Tauri Desktop App", + Description = "Build desktop binary", + Group = "Build", + DependsOn = scripts.Contains("publish-sidecar.py") ? ["sidecar"] : [], + Steps = + [ + ScriptStep("tauri:run", "python scripts/publish-app.py --target tauri --tauri-bundles none", + "scripts/publish-app.py", "--target", "tauri", "--tauri-bundles", "none") + ] + }; + } + + if (scripts.Contains("publish-webgateway.py")) + { + yield return new WorkflowDefinition + { + Id = "webgateway", + Label = "Publish WebGateway", + Description = "Publish ASP.NET gateway", + Group = "Build", + DependsOn = scripts.Contains("publish-app.py") ? ["web"] : [], + Steps = [ScriptStep("webgateway:run", "python scripts/publish-webgateway.py", "scripts/publish-webgateway.py")] + }; + } + + if (scripts.Contains("sync-output.py")) + { + yield return new WorkflowDefinition + { + Id = "sync-output", + Label = "Sync Output", + Description = "Sync newest artifacts to output", + Group = "Build", + Steps = [ScriptStep("sync-output:run", "python scripts/sync-output.py", "scripts/sync-output.py")] + }; + } + + if (scripts.Contains("publish-output.py")) + { + yield return new WorkflowDefinition + { + Id = "stage-output", + Label = "Stage Output Bundle", + Description = "Publish and stage distributable output", + Group = "Build", + Steps = [ScriptStep("stage-output:run", "python scripts/publish-output.py", "scripts/publish-output.py")] + }; + } + + if (scripts.Contains("run-webgateway.py")) + { + yield return new WorkflowDefinition + { + Id = "run-gateway-dev", + Label = "Run WebGateway Server (Dev)", + Description = "Run gateway in development mode", + Group = "Dev", + Steps = + [ + ScriptStep("run-gateway-dev:run", "python scripts/run-webgateway.py --mode Dev", + "scripts/run-webgateway.py", "--mode", "Dev") + ] + }; + } + } + + private static string FindProjectRoot(string startDir) + { + var start = Path.GetFullPath(startDir); + var gitRoot = TryGetGitRoot(start); + if (!string.IsNullOrWhiteSpace(gitRoot)) + return gitRoot!; + + var best = start; + var bestScore = ScoreRoot(start); + var cursor = new DirectoryInfo(start); + while (cursor.Parent is not null) + { + cursor = cursor.Parent; + var score = ScoreRoot(cursor.FullName); + if (score > bestScore) + { + best = cursor.FullName; + bestScore = score; + } + } + + return best; + } + + private static int ScoreRoot(string path) + { + var score = 0; + if (File.Exists(Path.Combine(path, "package.json"))) score += 2; + if (File.Exists(Path.Combine(path, "pyproject.toml"))) score += 2; + if (File.Exists(Path.Combine(path, "Cargo.toml"))) score += 2; + if (File.Exists(Path.Combine(path, "docker-compose.yml")) || + File.Exists(Path.Combine(path, "docker-compose.yaml")) || + File.Exists(Path.Combine(path, "Dockerfile"))) score += 1; + if (Directory.EnumerateFiles(path, "*.slnx", SearchOption.TopDirectoryOnly).Any()) score += 3; + if (Directory.EnumerateFiles(path, "*.sln", SearchOption.TopDirectoryOnly).Any()) score += 3; + if (Directory.Exists(Path.Combine(path, ".git"))) score += 1; + return score; + } + + private static string? TryGetGitRoot(string start) + { + try + { + var psi = new System.Diagnostics.ProcessStartInfo + { + FileName = "git", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WorkingDirectory = start + }; + psi.ArgumentList.Add("rev-parse"); + psi.ArgumentList.Add("--show-toplevel"); + using var process = System.Diagnostics.Process.Start(psi); + if (process is null) + return null; + var stdout = process.StandardOutput.ReadToEnd(); + process.WaitForExit(2000); + if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(stdout)) + return stdout.Trim(); + return null; + } + catch + { + return null; + } + } + + private static string InferProjectType(IEnumerable toolFamilies) + { + var tools = toolFamilies + .Where(t => !string.Equals(t, "git", StringComparison.OrdinalIgnoreCase)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + if (tools.Contains("tauri")) + return "tauri"; + + if (tools.SetEquals(["dotnet"])) + return "dotnet"; + + if (tools.SetEquals(["node", "npm"]) || tools.SetEquals(["npm"]) || tools.SetEquals(["node"])) + return "node"; + + if (tools.SetEquals(["python"])) + return "python"; + + if (tools.SetEquals(["cargo"])) + return "rust"; + + if (tools.SetEquals(["go"])) + return "go"; + + if (tools.Contains("java") || tools.Contains("maven") || tools.Contains("gradle")) + return "java"; + + if (tools.Count > 1) + return "polyglot"; + + return "generic"; + } + + private static string? SelectNodePackageJson(string root) + { + var candidates = EnumerateFilesBounded(root, "package.json", MaxScanDepth) + .OrderBy(p => p.Length) + .ThenBy(p => p, StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (candidates.Count == 0) + return null; + + foreach (var candidate in candidates) + { + if (HasNodeScripts(candidate)) + return candidate; + } + + // If no runnable scripts are found, do not enable node/npm workflows by default. + return null; + } + + private static bool HasNodeScripts(string packageJsonPath) + { + try + { + using var doc = JsonDocument.Parse(File.ReadAllText(packageJsonPath)); + if (!doc.RootElement.TryGetProperty("scripts", out var scripts) || + scripts.ValueKind != JsonValueKind.Object) + { + return false; + } + + var keys = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var prop in scripts.EnumerateObject()) + keys.Add(prop.Name); + + return keys.Contains("build") || + keys.Contains("test") || + keys.Contains("dev") || + keys.Contains("start"); + } + catch + { + return false; + } + } + + private static IEnumerable EnumerateFilesBounded(string root, string pattern, int maxDepth) + { + var queue = new Queue<(string Dir, int Depth)>(); + queue.Enqueue((root, 0)); + + while (queue.Count > 0) + { + var (dir, depth) = queue.Dequeue(); + IEnumerable files = []; + try + { + files = Directory.EnumerateFiles(dir, pattern, SearchOption.TopDirectoryOnly); + } + catch + { + // Ignore unreadable directories. + } + + foreach (var file in files) + yield return file; + + if (depth >= maxDepth) + continue; + + IEnumerable subdirs = []; + try + { + subdirs = Directory.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly); + } + catch + { + // Ignore unreadable directories. + } + + foreach (var subdir in subdirs) + { + var name = Path.GetFileName(subdir); + if (ExcludedDirectories.Contains(name)) + continue; + + queue.Enqueue((subdir, depth + 1)); + } + } + } +} diff --git a/src/DevTool.Engine/Config/ConfigLoader.cs b/src/DevTool.Engine/Config/ConfigLoader.cs new file mode 100644 index 0000000..507c9d6 --- /dev/null +++ b/src/DevTool.Engine/Config/ConfigLoader.cs @@ -0,0 +1,223 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Sdt.Core; + +namespace Sdt.Config; + +public sealed record LoadedProjectConfig( + DevToolConfig Config, + string ProjectRoot, + IReadOnlyList Warnings); + +public sealed record LegacyMigrationApplyResult( + bool Success, + string Message, + string? BackupPath = null, + string? ConfigPath = null); + +public static class ConfigLoader +{ + public const string WorkspaceDefaultsFileName = "sdt-defaults.json"; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + }; + + /// + /// Walks up from (or CWD) until it finds devtool.json. + /// Returns null if not found. + /// + public static string? FindConfigPath(string? startDir = null) + { + var dir = new DirectoryInfo(startDir ?? Directory.GetCurrentDirectory()); + while (dir is not null) + { + var candidate = Path.Combine(dir.FullName, "devtool.json"); + if (File.Exists(candidate)) + return candidate; + dir = dir.Parent!; + } + return null; + } + + public static LoadedProjectConfig? FindAndLoad(string? startDir = null) + { + var configPath = FindConfigPath(startDir); + if (configPath is null) + return null; + + var projectRoot = Path.GetDirectoryName(configPath) + ?? throw new InvalidOperationException($"Could not resolve project root from {configPath}"); + + try + { + var effectiveConfig = LoadEffectiveConfig(projectRoot, configPath, out var defaultsPath); + var warnings = new List(); + if (!string.IsNullOrWhiteSpace(defaultsPath)) + warnings.Add($"Applied workspace defaults from {defaultsPath}."); + + var legacyMode = ResolveLegacyMode(); + if (legacyMode == LegacyMode.Strict && effectiveConfig.Workflows.Count == 0 && effectiveConfig.Targets.Count > 0) + { + var previewPath = Path.Combine(projectRoot, "devtool.generated.workflows.json"); + try + { + var previewConfig = WorkflowModelBuilder.BuildMigrationPreviewConfig(effectiveConfig, new RequirementResolver()); + File.WriteAllText(previewPath, ConfigBootstrapper.ToJson(previewConfig)); + } + catch + { + // Keep strict failure even if preview generation fails. + } + + throw new InvalidOperationException( + $"Legacy targets-only config detected at {configPath}. Strict mode requires workflows. " + + "Use migration preview file 'devtool.generated.workflows.json' and migrate devtool.json. " + + "Temporary rollback: set SDT_LEGACY_MODE=compat."); + } + + var normalized = WorkflowModelBuilder.Normalize(effectiveConfig, legacyMode, new RequirementResolver()); + warnings.AddRange(normalized.Warnings); + return new LoadedProjectConfig(effectiveConfig, projectRoot, warnings); + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to parse devtool.json at {configPath}: {ex.Message}", ex); + } + } + + public static LegacyMigrationApplyResult ApplyLegacyTargetMigration( + string configPath, + bool createBackup = true) + { + try + { + if (!File.Exists(configPath)) + return new LegacyMigrationApplyResult(false, $"Config file not found: {configPath}"); + + var json = File.ReadAllText(configPath); + var config = JsonSerializer.Deserialize(json, JsonOptions) + ?? throw new InvalidOperationException("devtool.json deserialized to null."); + + if (config.Targets.Count == 0) + return new LegacyMigrationApplyResult(false, "No legacy targets found to migrate.", ConfigPath: configPath); + + var migrated = WorkflowModelBuilder.BuildMigrationPreviewConfig(config, new RequirementResolver()); + var backupPath = (string?)null; + if (createBackup) + { + backupPath = configPath + $".bak-{DateTimeOffset.Now:yyyyMMdd-HHmmss}"; + File.Copy(configPath, backupPath, overwrite: false); + } + + File.WriteAllText(configPath, ConfigBootstrapper.ToJson(migrated)); + return new LegacyMigrationApplyResult( + true, + "Legacy targets migrated to workflows.", + BackupPath: backupPath, + ConfigPath: configPath); + } + catch (Exception ex) + { + return new LegacyMigrationApplyResult(false, ex.Message, ConfigPath: configPath); + } + } + + private static LegacyMode ResolveLegacyMode() + { + var raw = Environment.GetEnvironmentVariable("SDT_LEGACY_MODE"); + return string.Equals(raw, "compat", StringComparison.OrdinalIgnoreCase) + ? LegacyMode.Compat + : LegacyMode.Strict; + } + + private static DevToolConfig LoadEffectiveConfig( + string projectRoot, + string projectConfigPath, + out string? defaultsPath) + { + defaultsPath = FindWorkspaceDefaultsPath(projectRoot); + var projectObj = LoadJsonObject(projectConfigPath, "project config"); + if (string.IsNullOrWhiteSpace(defaultsPath)) + return DeserializeConfig(projectObj, projectConfigPath); + + var defaultsObj = LoadJsonObject(defaultsPath!, "workspace defaults"); + var merged = MergeObjects(defaultsObj, projectObj); + return DeserializeConfig(merged, projectConfigPath); + } + + private static string? FindWorkspaceDefaultsPath(string startDir) + { + var workspaceBoundary = FindWorkspaceBoundary(startDir); + var dir = new DirectoryInfo(startDir); + while (dir is not null) + { + var candidate = Path.Combine(dir.FullName, WorkspaceDefaultsFileName); + if (File.Exists(candidate)) + return candidate; + if (workspaceBoundary is not null && + string.Equals(dir.FullName, workspaceBoundary, StringComparison.OrdinalIgnoreCase)) + { + break; + } + if (workspaceBoundary is null) + break; + dir = dir.Parent; + } + + return null; + } + + private static string? FindWorkspaceBoundary(string startDir) + { + var dir = new DirectoryInfo(startDir); + while (dir is not null) + { + var workspacePath = Path.Combine(dir.FullName, WorkspaceLoader.FileName); + if (File.Exists(workspacePath)) + return dir.FullName; + dir = dir.Parent; + } + + return null; + } + + private static JsonObject LoadJsonObject(string path, string label) + { + var json = File.ReadAllText(path); + var node = JsonNode.Parse(json) + ?? throw new InvalidOperationException($"{label} at {path} deserialized to null."); + if (node is not JsonObject obj) + throw new InvalidOperationException($"{label} at {path} must be a JSON object."); + return obj; + } + + private static DevToolConfig DeserializeConfig(JsonObject obj, string sourcePath) + { + return obj.Deserialize(JsonOptions) + ?? throw new InvalidOperationException($"devtool.json at {sourcePath} deserialized to null."); + } + + private static JsonObject MergeObjects(JsonObject baseObj, JsonObject overlayObj) + { + var result = (JsonObject)baseObj.DeepClone(); + foreach (var kv in overlayObj) + { + if (kv.Value is JsonObject overlayChild && + result[kv.Key] is JsonObject baseChild) + { + result[kv.Key] = MergeObjects(baseChild, overlayChild); + continue; + } + + result[kv.Key] = kv.Value?.DeepClone(); + } + + return result; + } +} diff --git a/src/DevTool.Engine/Config/DevToolConfig.cs b/src/DevTool.Engine/Config/DevToolConfig.cs new file mode 100644 index 0000000..29d996a --- /dev/null +++ b/src/DevTool.Engine/Config/DevToolConfig.cs @@ -0,0 +1,204 @@ +using System.Text.Json.Serialization; + +namespace Sdt.Config; + +public sealed class DevToolConfig +{ + public string Name { get; init; } = "SDT Project"; + public string Version { get; init; } = "0.1.0"; + public List Targets { get; init; } = []; + public List Workflows { get; init; } = []; + public List Env { get; init; } = []; + public EnvProfilesConfig? EnvProfiles { get; init; } + public ToolchainConfig? Toolchains { get; init; } + public ToolingConfig? Tooling { get; init; } + public ProjectMetadata? Project { get; init; } + public DebugConfig? Debug { get; init; } +} + +public sealed class EnvProfilesConfig +{ + public string Active { get; init; } = "dev"; + public List Profiles { get; init; } = []; +} + +public sealed class EnvProfileDefinition +{ + public string Id { get; init; } = ""; + public string Description { get; init; } = ""; + public List Inherits { get; init; } = []; + public Dictionary Values { get; init; } = new(StringComparer.OrdinalIgnoreCase); +} + +public sealed class BuildTarget +{ + public string Id { get; init; } = ""; + public string Label { get; init; } = ""; + public string Description { get; init; } = ""; + public string Group { get; init; } = "General"; + + /// Executable name. Null = virtual aggregator (runs DependsOn only). + public string? Command { get; init; } + + public List Args { get; init; } = []; + + /// Working directory relative to project root. + public string WorkingDir { get; init; } = "."; + + public List DependsOn { get; init; } = []; +} + +public sealed class EnvVarDef +{ + public string Key { get; init; } = ""; + public string Description { get; init; } = ""; + + [System.Text.Json.Serialization.JsonPropertyName("default")] + public string DefaultValue { get; init; } = ""; + + /// If non-empty, shown as a dropdown. Otherwise free-text input. + public List Options { get; init; } = []; +} + +public sealed class WorkflowDefinition +{ + public string Id { get; init; } = ""; + public string Label { get; init; } = ""; + public string Description { get; init; } = ""; + public string Group { get; init; } = "General"; + public List DependsOn { get; init; } = []; + public List Steps { get; init; } = []; +} + +public sealed class WorkflowStep +{ + public string Id { get; init; } = ""; + public string Label { get; init; } = ""; + public string? Command { get; init; } + public List Args { get; init; } = []; + public string WorkingDir { get; init; } = "."; + public string? Action { get; init; } + public List ActionArgs { get; init; } = []; + public List Requires { get; init; } = []; +} + +public sealed class ToolRequirement +{ + public string Tool { get; init; } = ""; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public InstallPolicy InstallPolicy { get; init; } = InstallPolicy.Prompt; +} + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum InstallPolicy +{ + Prompt, + Auto, + Never, +} + +public sealed class ToolingConfig +{ + public List Tools { get; init; } = []; +} + +public sealed class ToolInstallDefinition +{ + public string Tool { get; init; } = ""; + public List PreferredInstallCommands { get; init; } = []; + public List Executables { get; init; } = []; +} + +public sealed class ProjectMetadata +{ + public string Type { get; init; } = ""; + public List RootHints { get; init; } = []; + public List Artifacts { get; init; } = []; +} + +public sealed class DebugConfig +{ + public List Profiles { get; init; } = []; + public DebugDiagnosticsOptions Diagnostics { get; init; } = new(); +} + +public sealed class DebugProfileDefinition +{ + public string Id { get; init; } = ""; + public string Label { get; init; } = ""; + public string Type { get; init; } = "generic"; + public string Command { get; init; } = ""; + public List Args { get; init; } = []; + public string WorkingDir { get; init; } = "."; + public Dictionary Env { get; init; } = new(StringComparer.OrdinalIgnoreCase); + public List Requires { get; init; } = []; + public DebugAttachConfig? Attach { get; init; } +} + +public sealed class DebugAttachConfig +{ + public string Kind { get; init; } = ""; + public int? Port { get; init; } + public string? ProcessName { get; init; } + public string? Note { get; init; } +} + +public sealed class DebugDiagnosticsOptions +{ + public bool Enabled { get; init; } = true; + public string OutputDir { get; init; } = ".sdt/debug"; + public bool IncludeAllEnv { get; init; } = false; + public List CaptureEnvKeys { get; init; } = []; + public bool RedactSensitive { get; init; } = true; + public List SensitiveKeyPatterns { get; init; } = + ["TOKEN", "SECRET", "PASSWORD", "PWD", "CREDENTIAL", "API_KEY", "ACCESS_KEY", "PRIVATE_KEY"]; + public List RedactionAllowKeys { get; init; } = []; + public bool BundleOnFailure { get; init; } = true; +} + +// ── Toolchain config ────────────────────────────────────────────────────────── + +public sealed class ToolchainConfig +{ + public PythonToolchain? Python { get; init; } + public NodeToolchain? Node { get; init; } +} + +public sealed class PythonToolchain +{ + /// Python executable (e.g. "python3.14", "python"). + public string Executable { get; init; } = "python"; + + /// Windows-specific override (e.g. "py" when using the launcher). + public string? WindowsExecutable { get; init; } + + /// Optional version flag to pass (e.g. "-3.14" for py launcher). + public string? LauncherVersion { get; init; } + + /// Venv directory relative to project root. + public string VenvDir { get; init; } = ".venv"; + + public List Profiles { get; init; } = []; + + /// Optional path to a pip wrapper script (relative to project root). + public string? PipScript { get; init; } +} + +public sealed class PythonProfile +{ + public string Id { get; init; } = ""; + public string Label { get; init; } = ""; + public string RequirementsFile { get; init; } = ""; + public string? ExtraIndexUrl { get; init; } + public List PostInstallCommands { get; init; } = []; +} + +public sealed class NodeToolchain +{ + /// Package manager: "npm", "pnpm", or "yarn". + public string PackageManager { get; init; } = "npm"; + + /// Working directory for the frontend (relative to project root). + public string WorkingDir { get; init; } = "."; +} diff --git a/src/DevTool.Engine/Config/IWorkspaceInventoryService.cs b/src/DevTool.Engine/Config/IWorkspaceInventoryService.cs new file mode 100644 index 0000000..ac49f1e --- /dev/null +++ b/src/DevTool.Engine/Config/IWorkspaceInventoryService.cs @@ -0,0 +1,12 @@ +namespace Sdt.Config; + +public interface IWorkspaceInventoryService +{ + WorkspaceInventoryScanResult Scan( + string workspaceRoot, + string currentRoot, + WorkspaceInventorySettings? settings, + WorkspaceConfig? workspace = null); + + string PersistSnapshot(string workspaceRoot, WorkspaceInventorySnapshot snapshot); +} diff --git a/src/DevTool.Engine/Config/WorkflowModelBuilder.cs b/src/DevTool.Engine/Config/WorkflowModelBuilder.cs new file mode 100644 index 0000000..6501835 --- /dev/null +++ b/src/DevTool.Engine/Config/WorkflowModelBuilder.cs @@ -0,0 +1,100 @@ +using Sdt.Core; + +namespace Sdt.Config; + +public enum LegacyMode +{ + Strict, + Compat, +} + +public sealed record WorkflowNormalizationResult( + IReadOnlyList Workflows, + IReadOnlyList Warnings); + +public static class WorkflowModelBuilder +{ + public static WorkflowNormalizationResult Normalize( + DevToolConfig config, + LegacyMode legacyMode = LegacyMode.Strict, + IRequirementResolver? requirementResolver = null) + { + requirementResolver ??= new RequirementResolver(); + var warnings = new List(); + + if (config.Workflows.Count > 0) + { + if (config.Targets.Count > 0) + { + warnings.Add("Both 'workflows' and legacy 'targets' are present. SDT will use 'workflows'."); + } + + return new WorkflowNormalizationResult(config.Workflows, warnings); + } + + if (config.Targets.Count == 0) + { + warnings.Add("No 'workflows' or legacy 'targets' were found."); + return new WorkflowNormalizationResult([], warnings); + } + + if (legacyMode == LegacyMode.Strict) + { + throw new InvalidOperationException( + "Legacy 'targets' are not allowed in strict mode. Migrate to 'workflows' or set SDT_LEGACY_MODE=compat temporarily."); + } + + warnings.Add("Using legacy 'targets' schema. Migrate to 'workflows' for v1+ features."); + return new WorkflowNormalizationResult(ConvertLegacyTargets(config.Targets, requirementResolver), warnings); + } + + public static DevToolConfig BuildMigrationPreviewConfig(DevToolConfig config, IRequirementResolver? requirementResolver = null) + { + requirementResolver ??= new RequirementResolver(); + return new DevToolConfig + { + Name = config.Name, + Version = config.Version, + Targets = [], + Workflows = ConvertLegacyTargets(config.Targets, requirementResolver), + Env = config.Env, + Toolchains = config.Toolchains, + Tooling = config.Tooling, + Project = config.Project, + Debug = config.Debug, + }; + } + + private static List ConvertLegacyTargets( + IReadOnlyList targets, + IRequirementResolver requirementResolver) + { + var workflows = new List(targets.Count); + foreach (var target in targets) + { + var step = target.Command is null + ? null + : new WorkflowStep + { + Id = $"{target.Id}:run", + Label = string.IsNullOrWhiteSpace(target.Label) ? target.Id : target.Label, + Command = target.Command, + Args = target.Args, + WorkingDir = target.WorkingDir, + Requires = requirementResolver.Resolve(target), + }; + + workflows.Add(new WorkflowDefinition + { + Id = target.Id, + Label = target.Label, + Description = target.Description, + Group = target.Group, + DependsOn = target.DependsOn, + Steps = step is null ? [] : [step], + }); + } + + return workflows; + } +} diff --git a/src/DevTool.Engine/Config/WorkspaceConfig.cs b/src/DevTool.Engine/Config/WorkspaceConfig.cs new file mode 100644 index 0000000..130d012 --- /dev/null +++ b/src/DevTool.Engine/Config/WorkspaceConfig.cs @@ -0,0 +1,69 @@ +namespace Sdt.Config; + +public sealed class WorkspaceConfig +{ + public string Name { get; init; } = "SDT Workspace"; + public List Projects { get; init; } = []; + public List Favorites { get; init; } = []; + public WorkspaceInventorySettings Inventory { get; init; } = new(); +} + +public sealed class WorkspaceProject +{ + public string Name { get; init; } = ""; + public string Description { get; init; } = ""; + + /// + /// Relative or absolute path to the project root + /// (the directory containing devtool.json). + /// + public string Path { get; init; } = ""; + public List Tags { get; init; } = []; + public List ToolFamilies { get; init; } = []; + public bool Disabled { get; init; } = false; + public string? DetectedBy { get; init; } + public DateTimeOffset? LastValidatedUtc { get; init; } +} + +public sealed class WorkspaceFavorite +{ + /// + /// Relative or absolute path to the project root. + /// + public string ProjectPath { get; init; } = ""; + + /// + /// Workflow id to execute for this favorite. + /// + public string WorkflowId { get; init; } = ""; + + /// + /// Optional custom label shown in quick actions. + /// + public string? Label { get; init; } +} + +public sealed class WorkspaceInventorySettings +{ + public bool Enabled { get; init; } = true; + public int MaxDepth { get; init; } = 4; + public List ExcludeDirs { get; init; } = + [ + ".git", + "node_modules", + "bin", + "obj", + ".venv", + "venv", + "dist", + "build", + ".sdt", + ]; + public List IncludeMarkers { get; init; } = + [ + "*.slnx", + "*.sln", + "*.csproj", + "devtool.json", + ]; +} diff --git a/src/DevTool.Engine/Config/WorkspaceInventoryModels.cs b/src/DevTool.Engine/Config/WorkspaceInventoryModels.cs new file mode 100644 index 0000000..527e38f --- /dev/null +++ b/src/DevTool.Engine/Config/WorkspaceInventoryModels.cs @@ -0,0 +1,39 @@ +namespace Sdt.Config; + +public enum WorkspaceInventoryKind +{ + Devtool, + Slnx, + Sln, + Csproj, +} + +public sealed record WorkspaceInventoryItem( + string Id, + string RootPath, + string DisplayName, + IReadOnlyList Kinds, + WorkspaceInventoryKind PrimaryKind, + int Depth, + bool HasDevtoolConfig, + bool SuggestedInit, + IReadOnlyList Warnings); + +public sealed record WorkspaceInventoryScanStats( + int DirectoriesScanned, + int MarkerHits, + int KnownCount, + int CandidateCount, + long DurationMs); + +public sealed record WorkspaceInventorySnapshot( + string WorkspaceRoot, + DateTimeOffset GeneratedAtUtc, + IReadOnlyList Items, + WorkspaceInventoryScanStats ScanStats); + +public sealed record WorkspaceInventoryScanResult( + string WorkspaceRoot, + IReadOnlyList KnownProjects, + IReadOnlyList Candidates, + WorkspaceInventorySnapshot Snapshot); diff --git a/src/DevTool.Engine/Config/WorkspaceInventoryService.cs b/src/DevTool.Engine/Config/WorkspaceInventoryService.cs new file mode 100644 index 0000000..1cbdf3b --- /dev/null +++ b/src/DevTool.Engine/Config/WorkspaceInventoryService.cs @@ -0,0 +1,278 @@ +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Sdt.Config; + +public sealed class WorkspaceInventoryService : IWorkspaceInventoryService +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + }; + + static WorkspaceInventoryService() + { + JsonOptions.Converters.Add(new JsonStringEnumConverter()); + } + + private sealed record ScanDir(string Path, int Depth); + + public WorkspaceInventoryScanResult Scan( + string workspaceRoot, + string currentRoot, + WorkspaceInventorySettings? settings, + WorkspaceConfig? workspace = null) + { + var sw = Stopwatch.StartNew(); + var root = Path.GetFullPath(workspaceRoot); + var current = Path.GetFullPath(currentRoot); + var effective = settings ?? new WorkspaceInventorySettings(); + + if (!effective.Enabled) + { + var emptyStats = new WorkspaceInventoryScanStats(0, 0, 0, 0, 0); + var emptySnapshot = new WorkspaceInventorySnapshot(root, DateTimeOffset.UtcNow, [], emptyStats); + return new WorkspaceInventoryScanResult(root, [], [], emptySnapshot); + } + + var excludes = new HashSet(effective.ExcludeDirs, StringComparer.OrdinalIgnoreCase); + var includeMarkers = effective.IncludeMarkers.Count == 0 + ? new WorkspaceInventorySettings().IncludeMarkers + : effective.IncludeMarkers; + + var markerDirs = new Dictionary>(StringComparer.OrdinalIgnoreCase); + var queue = new Queue(); + var visited = new HashSet(StringComparer.OrdinalIgnoreCase); + queue.Enqueue(new ScanDir(root, 0)); + var directoriesScanned = 0; + var markerHits = 0; + + while (queue.Count > 0) + { + var entry = queue.Dequeue(); + var full = Path.GetFullPath(entry.Path); + if (!visited.Add(full)) + continue; + + directoriesScanned++; + var kinds = DetectKinds(full, includeMarkers); + if (kinds.Count > 0) + { + markerHits += kinds.Count; + markerDirs[full] = kinds; + } + + if (entry.Depth >= Math.Max(0, effective.MaxDepth)) + continue; + + IEnumerable children = []; + try + { + children = Directory.EnumerateDirectories(full); + } + catch + { + continue; + } + + foreach (var child in children) + { + var name = Path.GetFileName(child); + if (excludes.Contains(name)) + continue; + queue.Enqueue(new ScanDir(child, entry.Depth + 1)); + } + } + + var clustered = ClusterAndDedupe(markerDirs); + var items = clustered + .Select(kvp => CreateItem(root, current, kvp.Key, kvp.Value)) + .OrderBy(i => i.Depth) + .ThenBy(i => i.PrimaryKind) + .ThenBy(i => i.RootPath, StringComparer.OrdinalIgnoreCase) + .ToList(); + + var knownRoots = new HashSet(StringComparer.OrdinalIgnoreCase); + if (workspace is not null) + { + foreach (var project in workspace.Projects) + knownRoots.Add(WorkspaceLoader.ResolveProjectRoot(root, project)); + } + + // devtool roots are known by default for auto-workspace discoverability. + foreach (var item in items.Where(i => i.HasDevtoolConfig)) + knownRoots.Add(item.RootPath); + + var known = items.Where(i => knownRoots.Contains(i.RootPath)).ToList(); + var candidates = items.Where(i => !knownRoots.Contains(i.RootPath)).ToList(); + + sw.Stop(); + var stats = new WorkspaceInventoryScanStats( + directoriesScanned, + markerHits, + known.Count, + candidates.Count, + sw.ElapsedMilliseconds); + var snapshot = new WorkspaceInventorySnapshot(root, DateTimeOffset.UtcNow, items, stats); + return new WorkspaceInventoryScanResult(root, known, candidates, snapshot); + } + + public string PersistSnapshot(string workspaceRoot, WorkspaceInventorySnapshot snapshot) + { + var root = Path.GetFullPath(workspaceRoot); + var dir = Path.Combine(root, ".sdt"); + Directory.CreateDirectory(dir); + var path = Path.Combine(dir, "workspace-inventory.json"); + File.WriteAllText(path, JsonSerializer.Serialize(snapshot, JsonOptions) + Environment.NewLine); + return path; + } + + private static HashSet DetectKinds(string dir, IReadOnlyList markers) + { + var kinds = new HashSet(); + foreach (var marker in markers) + { + if (string.Equals(marker, "devtool.json", StringComparison.OrdinalIgnoreCase)) + { + if (File.Exists(Path.Combine(dir, "devtool.json"))) + kinds.Add(WorkspaceInventoryKind.Devtool); + continue; + } + + if (string.Equals(marker, "*.slnx", StringComparison.OrdinalIgnoreCase)) + { + if (Directory.EnumerateFiles(dir, "*.slnx", SearchOption.TopDirectoryOnly).Any()) + kinds.Add(WorkspaceInventoryKind.Slnx); + continue; + } + + if (string.Equals(marker, "*.sln", StringComparison.OrdinalIgnoreCase)) + { + if (Directory.EnumerateFiles(dir, "*.sln", SearchOption.TopDirectoryOnly).Any()) + kinds.Add(WorkspaceInventoryKind.Sln); + continue; + } + + if (string.Equals(marker, "*.csproj", StringComparison.OrdinalIgnoreCase)) + { + if (Directory.EnumerateFiles(dir, "*.csproj", SearchOption.TopDirectoryOnly).Any()) + kinds.Add(WorkspaceInventoryKind.Csproj); + } + } + + return kinds; + } + + private static Dictionary> ClusterAndDedupe( + Dictionary> raw) + { + var result = new Dictionary>(raw, StringComparer.OrdinalIgnoreCase); + + // Collapse sibling csproj-only marker directories to parent project cluster. + var csprojOnly = result + .Where(kvp => kvp.Value.SetEquals([WorkspaceInventoryKind.Csproj])) + .Select(kvp => kvp.Key) + .ToList(); + var byParent = csprojOnly + .GroupBy(path => Directory.GetParent(path)?.FullName ?? path, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() >= 2) + .ToList(); + + foreach (var group in byParent) + { + var parent = group.Key; + if (!result.TryGetValue(parent, out var parentKinds)) + { + result[parent] = [WorkspaceInventoryKind.Csproj]; + } + else + { + parentKinds.Add(WorkspaceInventoryKind.Csproj); + } + + foreach (var child in group) + result.Remove(child); + } + + // Drop descendants under stronger marker roots (devtool/slnx/sln). + var strongRoots = result + .Where(kvp => kvp.Value.Contains(WorkspaceInventoryKind.Devtool) || + kvp.Value.Contains(WorkspaceInventoryKind.Slnx) || + kvp.Value.Contains(WorkspaceInventoryKind.Sln)) + .Select(kvp => kvp.Key) + .OrderBy(p => p.Length) + .ToList(); + + foreach (var path in result.Keys.ToList()) + { + foreach (var strong in strongRoots) + { + if (string.Equals(path, strong, StringComparison.OrdinalIgnoreCase)) + continue; + + if (path.StartsWith(strong + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) + { + result.Remove(path); + break; + } + } + } + + return result; + } + + private static WorkspaceInventoryItem CreateItem( + string workspaceRoot, + string currentRoot, + string path, + HashSet kinds) + { + var full = Path.GetFullPath(path); + var rel = Path.GetRelativePath(workspaceRoot, full); + var depth = rel is "." ? 0 : rel.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + .Count(p => !string.IsNullOrWhiteSpace(p) && p != "."); + + var orderedKinds = kinds + .OrderBy(k => GetKindPrecedence(k)) + .ThenBy(k => k.ToString(), StringComparer.OrdinalIgnoreCase) + .ToList(); + var primary = orderedKinds.FirstOrDefault(); + var hasDevtool = kinds.Contains(WorkspaceInventoryKind.Devtool); + + var warnings = new List(); + if (!hasDevtool) + warnings.Add("No devtool.json found."); + if (full.Equals(currentRoot, StringComparison.OrdinalIgnoreCase)) + warnings.Add("Current project root."); + + return new WorkspaceInventoryItem( + Id: ComputeStableId(full), + RootPath: full, + DisplayName: new DirectoryInfo(full).Name, + Kinds: orderedKinds, + PrimaryKind: primary, + Depth: depth, + HasDevtoolConfig: hasDevtool, + SuggestedInit: !hasDevtool, + Warnings: warnings); + } + + private static int GetKindPrecedence(WorkspaceInventoryKind kind) => kind switch + { + WorkspaceInventoryKind.Devtool => 0, + WorkspaceInventoryKind.Slnx => 1, + WorkspaceInventoryKind.Sln => 2, + WorkspaceInventoryKind.Csproj => 3, + _ => 99 + }; + + private static string ComputeStableId(string path) + { + var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(path.ToLowerInvariant())); + return Convert.ToHexString(bytes[..8]).ToLowerInvariant(); + } +} diff --git a/src/DevTool.Engine/Config/WorkspaceLoader.cs b/src/DevTool.Engine/Config/WorkspaceLoader.cs new file mode 100644 index 0000000..54d63b9 --- /dev/null +++ b/src/DevTool.Engine/Config/WorkspaceLoader.cs @@ -0,0 +1,151 @@ +using System.Text.Json; + +namespace Sdt.Config; + +public static class WorkspaceLoader +{ + public const string FileName = "sdt-workspace.json"; + private static readonly IWorkspaceInventoryService InventoryService = new WorkspaceInventoryService(); + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + }; + + /// + /// Walks up from (or CWD) to find sdt-workspace.json. + /// Returns null if not found. + /// + public static (WorkspaceConfig Config, string WorkspaceRoot)? FindAndLoad(string? startDir = null) + { + var dir = new DirectoryInfo(startDir ?? Directory.GetCurrentDirectory()); + while (dir is not null) + { + var candidate = Path.Combine(dir.FullName, FileName); + if (File.Exists(candidate)) + { + try + { + var json = File.ReadAllText(candidate); + var config = JsonSerializer.Deserialize(json, JsonOptions) + ?? throw new InvalidOperationException($"{FileName} deserialized to null."); + return (config, dir.FullName); + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to parse {FileName} at {candidate}: {ex.Message}", ex); + } + } + dir = dir.Parent!; + } + + // No workspace file found; synthesize one by scanning nearby project roots. + return TryAutoDiscover(startDir ?? Directory.GetCurrentDirectory()); + } + + /// + /// Resolves the absolute project root for a workspace project entry. + /// + public static string ResolveProjectRoot(string workspaceRoot, WorkspaceProject project) + => Path.GetFullPath(Path.IsPathRooted(project.Path) + ? project.Path + : Path.Combine(workspaceRoot, project.Path)); + + public static string ResolveFavoriteProjectRoot(string workspaceRoot, WorkspaceFavorite favorite) + => Path.GetFullPath(Path.IsPathRooted(favorite.ProjectPath) + ? favorite.ProjectPath + : Path.Combine(workspaceRoot, favorite.ProjectPath)); + + public static string GetWorkspaceFilePath(string workspaceRoot) + => Path.Combine(workspaceRoot, FileName); + + public static void Save(string workspaceRoot, WorkspaceConfig workspace) + { + var path = GetWorkspaceFilePath(workspaceRoot); + var saveOptions = new JsonSerializerOptions(JsonOptions) + { + WriteIndented = true + }; + var json = JsonSerializer.Serialize(workspace, saveOptions); + File.WriteAllText(path, json + Environment.NewLine); + } + + public static WorkspaceInventoryScanResult ScanInventory( + string workspaceRoot, + string currentRoot, + WorkspaceConfig? workspace = null) + { + var settings = workspace?.Inventory ?? new WorkspaceInventorySettings(); + var result = InventoryService.Scan(workspaceRoot, currentRoot, settings, workspace); + InventoryService.PersistSnapshot(workspaceRoot, result.Snapshot); + return result; + } + + private static (WorkspaceConfig Config, string WorkspaceRoot)? TryAutoDiscover(string startDir) + { + LoadedProjectConfig? loaded; + try + { + loaded = ConfigLoader.FindAndLoad(startDir); + } + catch + { + return null; + } + + if (loaded is null) + return null; + + var currentRoot = loaded.ProjectRoot; + var parent = Directory.GetParent(currentRoot); + var workspaceRoot = parent?.FullName ?? currentRoot; + + var scan = InventoryService.Scan( + workspaceRoot, + currentRoot, + new WorkspaceInventorySettings(), + workspace: null); + InventoryService.PersistSnapshot(workspaceRoot, scan.Snapshot); + if (scan.KnownProjects.Count == 0) + return null; + + var projects = new List(); + foreach (var item in scan.KnownProjects) + { + LoadedProjectConfig? cfg; + try + { + cfg = ConfigLoader.FindAndLoad(item.RootPath); + } + catch + { + continue; + } + + var name = cfg?.Config.Name; + var relative = Path.GetRelativePath(workspaceRoot, item.RootPath); + projects.Add(new WorkspaceProject + { + Name = string.IsNullOrWhiteSpace(name) ? item.DisplayName : name!, + Description = $"Auto-discovered ({item.PrimaryKind}) at {item.RootPath}", + Path = relative, + ToolFamilies = item.Kinds.Select(k => k.ToString().ToLowerInvariant()).ToList(), + DetectedBy = "auto-devtool", + LastValidatedUtc = DateTimeOffset.UtcNow, + }); + } + + return ( + new WorkspaceConfig + { + Name = "SDT Auto Workspace", + Projects = projects, + Inventory = new WorkspaceInventorySettings(), + }, + workspaceRoot); + } +} diff --git a/src/DevTool.Engine/Core/ActionRunner.cs b/src/DevTool.Engine/Core/ActionRunner.cs new file mode 100644 index 0000000..ad65c75 --- /dev/null +++ b/src/DevTool.Engine/Core/ActionRunner.cs @@ -0,0 +1,234 @@ +using Sdt.Config; +using Sdt.Runner; + +namespace Sdt.Core; + +public sealed class ActionRunner : IActionRunner +{ + public async Task RunStepAsync( + WorkflowStep step, + string projectRoot, + Action onOutput, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + { + if (!string.IsNullOrWhiteSpace(step.Action)) + { + var scriptPath = ScriptLocator.FindHelperScript(projectRoot, "build.py"); + if (scriptPath is null) + throw new InvalidOperationException("build.py not found in bundled scripts or project scripts directory."); + + var actionArgs = new List + { + scriptPath, + step.Action, + "--project-root", + projectRoot, + }; + actionArgs.AddRange(step.ActionArgs); + + return await ProcessRunner.RunAsync( + PythonResolver.ResolveExecutable(), + actionArgs, + projectRoot, + onOutput, + envOverrides, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + if (string.IsNullOrWhiteSpace(step.Command)) + return new RunResult(0, TimeSpan.Zero); + + var workingDir = Path.GetFullPath(Path.Combine(projectRoot, step.WorkingDir)); + + var pwshReroute = await TryRunLegacyPwshScriptViaPythonAsync( + step, + projectRoot, + workingDir, + onOutput, + envOverrides, + cancellationToken).ConfigureAwait(false); + if (pwshReroute is not null) + return pwshReroute; + + return await ProcessRunner.RunAsync( + step.Command, + step.Args, + workingDir, + onOutput, + envOverrides, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + private static async Task TryRunLegacyPwshScriptViaPythonAsync( + WorkflowStep step, + string projectRoot, + string workingDir, + Action onOutput, + IReadOnlyDictionary? envOverrides, + CancellationToken cancellationToken) + { + if (!IsPowerShellCommand(step.Command)) + return null; + + var args = step.Args; + var fileIndex = FindArgIndex(args, "-File"); + if (fileIndex < 0 || fileIndex + 1 >= args.Count) + return null; + + var psScriptArg = args[fileIndex + 1]; + if (!psScriptArg.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase)) + return null; + + var pyScriptPath = ResolvePythonScriptPath(projectRoot, workingDir, psScriptArg); + if (pyScriptPath is null) + return null; + + var translated = TranslatePowerShellArgsToPython(args.Skip(fileIndex + 2)); + var pythonArgs = new List { pyScriptPath }; + pythonArgs.AddRange(translated); + + onOutput($"Legacy PowerShell target detected. Trying Python script first: {Path.GetFileName(pyScriptPath)}", false); + + var pyRun = await ProcessRunner.RunAsync( + PythonResolver.ResolveExecutable(), + pythonArgs, + workingDir, + onOutput, + envOverrides, + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (pyRun.Success) + return pyRun; + + if (!IsLegacyPowerShellFallbackEnabled()) + { + onOutput( + $"Python script failed (exit {pyRun.ExitCode}). Legacy PowerShell fallback is disabled by default. " + + "Set SDT_PWSH_LEGACY_FALLBACK=1 to allow temporary fallback.", + true); + return pyRun; + } + + var psScriptPath = ResolveScriptPath(workingDir, psScriptArg); + if (psScriptPath is null || !File.Exists(psScriptPath)) + return pyRun; + + onOutput( + $"Python script failed (exit {pyRun.ExitCode}). Falling back to legacy PowerShell script: {psScriptArg}", + true); + return null; + } + + private static string? ResolvePythonScriptPath(string projectRoot, string workingDir, string psScriptArg) + { + var pyArg = Path.ChangeExtension(psScriptArg, ".py"); + var candidate = ResolveScriptPath(workingDir, pyArg); + if (candidate is not null && File.Exists(candidate)) + return candidate; + + var fileName = Path.GetFileName(pyArg); + return ScriptLocator.FindHelperScript(projectRoot, fileName); + } + + private static string? ResolveScriptPath(string workingDir, string scriptArg) + { + if (Path.IsPathRooted(scriptArg)) + return scriptArg; + return Path.GetFullPath(Path.Combine(workingDir, scriptArg)); + } + + private static bool IsPowerShellCommand(string? command) + { + if (string.IsNullOrWhiteSpace(command)) + return false; + + var normalized = Path.GetFileNameWithoutExtension(command).ToLowerInvariant(); + return normalized is "pwsh" or "powershell"; + } + + private static bool IsLegacyPowerShellFallbackEnabled() + { + var raw = Environment.GetEnvironmentVariable("SDT_PWSH_LEGACY_FALLBACK"); + if (string.IsNullOrWhiteSpace(raw)) + return false; + + return raw.Equals("1", StringComparison.OrdinalIgnoreCase) || + raw.Equals("true", StringComparison.OrdinalIgnoreCase) || + raw.Equals("yes", StringComparison.OrdinalIgnoreCase) || + raw.Equals("on", StringComparison.OrdinalIgnoreCase); + } + + private static int FindArgIndex(IReadOnlyList args, string name) + { + for (var i = 0; i < args.Count; i++) + { + if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase)) + return i; + } + return -1; + } + + private static List TranslatePowerShellArgsToPython(IEnumerable inputArgs) + { + var result = new List(); + var list = inputArgs.ToList(); + + for (var i = 0; i < list.Count; i++) + { + var token = list[i]; + if (!token.StartsWith("-", StringComparison.Ordinal) || token == "-") + { + result.Add(token); + continue; + } + + var key = token.TrimStart('-'); + if (key.Length == 0) + continue; + + var mapped = MapPowerShellParameter(key); + var nextIsValue = (i + 1) < list.Count && !list[i + 1].StartsWith("-", StringComparison.Ordinal); + result.Add(mapped); + if (nextIsValue) + { + result.Add(list[i + 1]); + i++; + } + } + + return result; + } + + private static string MapPowerShellParameter(string key) + { + return key.ToLowerInvariant() switch + { + "tauribundles" => "--tauri-bundles", + "projectroot" => "--project-root", + "reporoot" => "--repo-root", + "outputzip" => "--output-zip", + "inputzip" => "--input-zip", + "workingdir" => "--working-dir", + "outputdir" => "--output-dir", + _ => "--" + ToKebabCase(key) + }; + } + + private static string ToKebabCase(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return value.ToLowerInvariant(); + + var chars = new List(value.Length + 4); + for (var i = 0; i < value.Length; i++) + { + var c = value[i]; + if (char.IsUpper(c) && i > 0 && value[i - 1] != '-') + chars.Add('-'); + chars.Add(char.ToLowerInvariant(c)); + } + + return new string(chars.ToArray()); + } +} diff --git a/src/DevTool.Engine/Core/ConfigDoctorAutoFixService.cs b/src/DevTool.Engine/Core/ConfigDoctorAutoFixService.cs new file mode 100644 index 0000000..d7bca94 --- /dev/null +++ b/src/DevTool.Engine/Core/ConfigDoctorAutoFixService.cs @@ -0,0 +1,67 @@ +using Sdt.Config; + +namespace Sdt.Core; + +public sealed record DoctorAutoFixResult( + bool Success, + string Message, + int CreatedDirectories = 0, + string? BackupPath = null); + +public sealed class ConfigDoctorAutoFixService +{ + public IReadOnlyList FindMissingWorkingDirectories(DevToolConfig config, string projectRoot) + { + var missing = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var workflow in config.Workflows) + { + foreach (var step in workflow.Steps) + { + if (string.IsNullOrWhiteSpace(step.WorkingDir)) + continue; + var path = Path.GetFullPath(Path.Combine(projectRoot, step.WorkingDir)); + if (!Directory.Exists(path)) + missing.Add(path); + } + } + + return missing.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(); + } + + public DoctorAutoFixResult CreateMissingWorkingDirectories(IReadOnlyList directories) + { + var created = 0; + try + { + foreach (var dir in directories) + { + if (Directory.Exists(dir)) + continue; + Directory.CreateDirectory(dir); + created++; + } + + return new DoctorAutoFixResult( + Success: true, + Message: created == 0 ? "No directories needed creation." : $"Created {created} missing working director{(created == 1 ? "y" : "ies")}.", + CreatedDirectories: created); + } + catch (Exception ex) + { + return new DoctorAutoFixResult(false, ex.Message, CreatedDirectories: created); + } + } + + public DoctorAutoFixResult ApplyLegacyMigration(string projectRoot) + { + var configPath = ConfigLoader.FindConfigPath(projectRoot); + if (string.IsNullOrWhiteSpace(configPath)) + return new DoctorAutoFixResult(false, "Could not find devtool.json for migration."); + + var migration = ConfigLoader.ApplyLegacyTargetMigration(configPath, createBackup: true); + return new DoctorAutoFixResult( + Success: migration.Success, + Message: migration.Message, + BackupPath: migration.BackupPath); + } +} diff --git a/src/DevTool.Engine/Core/ConfigDoctorService.cs b/src/DevTool.Engine/Core/ConfigDoctorService.cs new file mode 100644 index 0000000..dccbead --- /dev/null +++ b/src/DevTool.Engine/Core/ConfigDoctorService.cs @@ -0,0 +1,270 @@ +using Sdt.Config; + +namespace Sdt.Core; + +public enum DoctorStatus +{ + Pass, + Warn, + Fail, +} + +public sealed record DoctorCheck( + string Name, + DoctorStatus Status, + string Detail, + string? Fix = null); + +public sealed record DoctorReport( + IReadOnlyList Checks) +{ + public bool HasFailures => Checks.Any(c => c.Status == DoctorStatus.Fail); + public bool HasWarnings => Checks.Any(c => c.Status == DoctorStatus.Warn); +} + +public sealed class ConfigDoctorService( + IToolProbe? toolProbe = null, + IRequirementResolver? requirementResolver = null) +{ + private readonly IToolProbe _toolProbe = toolProbe ?? new ToolProbeService(); + private readonly IRequirementResolver _requirementResolver = requirementResolver ?? new RequirementResolver(); + + public async Task RunAsync( + DevToolConfig config, + string projectRoot, + CancellationToken cancellationToken = default) + { + var checks = new List(); + var workflowMap = config.Workflows.ToDictionary(w => w.Id, StringComparer.OrdinalIgnoreCase); + + AddSchemaChecks(config, checks); + AddWorkflowChecks(config.Workflows, workflowMap, projectRoot, checks); + AddPathChecks(config, projectRoot, checks); + await AddToolProbeChecksAsync(config, projectRoot, checks, cancellationToken).ConfigureAwait(false); + + return new DoctorReport(checks); + } + + private static void AddSchemaChecks(DevToolConfig config, List checks) + { + if (config.Workflows.Count == 0 && config.Targets.Count == 0) + { + checks.Add(new DoctorCheck( + "Config schema", + DoctorStatus.Fail, + "No workflows or legacy targets found.", + "Add workflows or run SDT init/bootstrap.")); + return; + } + + if (config.Workflows.Count == 0 && config.Targets.Count > 0) + { + checks.Add(new DoctorCheck( + "Legacy schema", + DoctorStatus.Fail, + "Targets-only config detected (strict mode will block execution).", + "Use SYSTEM -> Migrate legacy targets -> workflows.")); + return; + } + + if (config.Targets.Count > 0) + { + checks.Add(new DoctorCheck( + "Legacy schema", + DoctorStatus.Warn, + "Both workflows and legacy targets are present.", + "Prefer workflows-only config and remove legacy targets once migrated.")); + } + else + { + checks.Add(new DoctorCheck("Config schema", DoctorStatus.Pass, "Workflow-first config detected.")); + } + } + + private static void AddWorkflowChecks( + IReadOnlyList workflows, + IReadOnlyDictionary workflowMap, + string projectRoot, + List checks) + { + var duplicateIds = workflows + .GroupBy(w => w.Id, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToList(); + + if (duplicateIds.Count > 0) + { + checks.Add(new DoctorCheck( + "Workflow IDs", + DoctorStatus.Fail, + $"Duplicate workflow IDs: {string.Join(", ", duplicateIds)}", + "Ensure each workflow has a unique id.")); + } + else + { + checks.Add(new DoctorCheck("Workflow IDs", DoctorStatus.Pass, "No duplicate workflow IDs.")); + } + + var brokenDeps = new List(); + foreach (var workflow in workflows) + { + foreach (var dep in workflow.DependsOn) + { + if (!workflowMap.ContainsKey(dep)) + brokenDeps.Add($"{workflow.Id} -> {dep}"); + } + } + + if (brokenDeps.Count > 0) + { + checks.Add(new DoctorCheck( + "Workflow dependencies", + DoctorStatus.Fail, + $"Missing dependencies: {string.Join("; ", brokenDeps)}", + "Fix dependsOn IDs to reference existing workflows.")); + } + else + { + checks.Add(new DoctorCheck("Workflow dependencies", DoctorStatus.Pass, "All workflow dependencies are valid.")); + } + + var invalidSteps = new List(); + var missingWorkingDirs = new List(); + foreach (var workflow in workflows) + { + foreach (var step in workflow.Steps) + { + if (string.IsNullOrWhiteSpace(step.Command) && string.IsNullOrWhiteSpace(step.Action)) + invalidSteps.Add($"{workflow.Id}/{step.Id}"); + + var stepDir = Path.GetFullPath(Path.Combine(projectRoot, step.WorkingDir)); + if (!Directory.Exists(stepDir)) + missingWorkingDirs.Add($"{workflow.Id}/{step.Id} -> {step.WorkingDir}"); + } + } + + if (invalidSteps.Count > 0) + { + checks.Add(new DoctorCheck( + "Step definitions", + DoctorStatus.Fail, + $"Steps missing command/action: {string.Join(", ", invalidSteps)}", + "Each step must define either action or command.")); + } + else + { + checks.Add(new DoctorCheck("Step definitions", DoctorStatus.Pass, "All steps define an action or command.")); + } + + if (missingWorkingDirs.Count > 0) + { + checks.Add(new DoctorCheck( + "Working directories", + DoctorStatus.Warn, + $"Missing directories: {string.Join("; ", missingWorkingDirs)}", + "Create missing directories or fix step workingDir values.")); + } + else + { + checks.Add(new DoctorCheck("Working directories", DoctorStatus.Pass, "All referenced working directories exist.")); + } + } + + private static void AddPathChecks(DevToolConfig config, string projectRoot, List checks) + { + var configPath = Path.Combine(projectRoot, "devtool.json"); + checks.Add(File.Exists(configPath) + ? new DoctorCheck("Project root", DoctorStatus.Pass, $"Config found at {configPath}") + : new DoctorCheck("Project root", DoctorStatus.Fail, $"devtool.json not found at {configPath}", "Run SDT init/bootstrap.")); + + if (OperatingSystem.IsWindows()) + { + var pathValue = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + var unresolvedSegments = pathValue + .Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries) + .Where(s => s.Contains('%') && Environment.ExpandEnvironmentVariables(s) == s) + .Take(4) + .ToList(); + + if (unresolvedSegments.Count > 0) + { + checks.Add(new DoctorCheck( + "PATH expansion", + DoctorStatus.Warn, + $"Unresolved PATH tokens: {string.Join(" | ", unresolvedSegments)}", + "Set referenced env vars or remove invalid PATH segments.")); + } + else + { + checks.Add(new DoctorCheck("PATH expansion", DoctorStatus.Pass, "No unresolved PATH token segments detected.")); + } + } + + if (config.Project?.RootHints.Count > 0) + { + checks.Add(new DoctorCheck("Root hints", DoctorStatus.Pass, $"Configured root hints: {string.Join(", ", config.Project.RootHints)}")); + } + else + { + checks.Add(new DoctorCheck( + "Root hints", + DoctorStatus.Warn, + "No project.rootHints configured.", + "Add rootHints markers (for example .git, *.sln, package.json).")); + } + } + + private async Task AddToolProbeChecksAsync( + DevToolConfig config, + string projectRoot, + List checks, + CancellationToken cancellationToken) + { + var requiredTools = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var workflow in config.Workflows) + { + foreach (var step in workflow.Steps) + { + foreach (var req in _requirementResolver.Resolve(step)) + requiredTools.Add(req.Tool); + } + } + + foreach (var profile in config.Debug?.Profiles ?? []) + { + foreach (var req in profile.Requires) + requiredTools.Add(req.Tool); + } + + foreach (var toolDef in config.Tooling?.Tools ?? []) + requiredTools.Add(toolDef.Tool); + + if (requiredTools.Count == 0) + { + checks.Add(new DoctorCheck("Tool probes", DoctorStatus.Warn, "No tools discovered from workflows/debug/tooling.")); + return; + } + + foreach (var tool in requiredTools.OrderBy(t => t, StringComparer.OrdinalIgnoreCase)) + { + var probe = await _toolProbe.ProbeAsync(tool, projectRoot, config, null, cancellationToken).ConfigureAwait(false); + if (probe.IsAvailable) + { + checks.Add(new DoctorCheck( + $"Tool: {tool}", + DoctorStatus.Pass, + string.IsNullOrWhiteSpace(probe.Version) ? "available" : probe.Version!, + probe.Details)); + } + else + { + checks.Add(new DoctorCheck( + $"Tool: {tool}", + DoctorStatus.Fail, + string.IsNullOrWhiteSpace(probe.Details) ? "not available" : probe.Details!, + $"Install/configure {tool} or set tooling.tools[].executables for non-standard paths.")); + } + } + } +} diff --git a/src/DevTool.Engine/Core/Contracts.cs b/src/DevTool.Engine/Core/Contracts.cs new file mode 100644 index 0000000..7d0ac98 --- /dev/null +++ b/src/DevTool.Engine/Core/Contracts.cs @@ -0,0 +1,90 @@ +using Sdt.Config; +using Sdt.Runner; + +namespace Sdt.Core; + +public sealed record ProbeResult( + string Tool, + bool IsAvailable, + string? Version = null, + string? Details = null); + +public sealed record InstallCommand( + string Command, + IReadOnlyList Args); + +public sealed record InstallPlan( + string Tool, + bool Supported, + string Summary, + IReadOnlyList Commands); + +public sealed record WorkflowStepResult( + string WorkflowId, + string StepId, + string StepLabel, + RunResult Result); + +public enum ExecutionStopReason +{ + MissingPrereq, + InstallFailed, + CommandFailed, + ValidationFailed, + UserDeclined, +} + +public sealed record WorkflowExecutionResult( + bool Success, + ExecutionStopReason? StopReason, + string Message, + IReadOnlyList Steps); + +public interface IToolProbe +{ + Task ProbeAsync( + string tool, + string projectRoot, + DevToolConfig? config = null, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default); +} + +public interface IPrereqInstaller +{ + Task GetInstallPlanAsync( + string tool, + string projectRoot, + DevToolConfig? config = null, + CancellationToken cancellationToken = default); + + Task RunInstallAsync( + InstallCommand command, + string projectRoot, + Action onOutput, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default); +} + +public interface IActionRunner +{ + Task RunStepAsync( + WorkflowStep step, + string projectRoot, + Action onOutput, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default); +} + +public interface IWorkflowPlanner +{ + List ResolvePlan( + WorkflowDefinition workflow, + IReadOnlyDictionary allWorkflows); +} + +public interface IRequirementResolver +{ + List Resolve(WorkflowStep step); + List Resolve(BuildTarget target); +} diff --git a/src/DevTool.Engine/Core/Debug/DebugContracts.cs b/src/DevTool.Engine/Core/Debug/DebugContracts.cs new file mode 100644 index 0000000..dae91df --- /dev/null +++ b/src/DevTool.Engine/Core/Debug/DebugContracts.cs @@ -0,0 +1,53 @@ +using Sdt.Config; +using Sdt.Runner; + +namespace Sdt.Core.Debug; + +public sealed record DebugRunResult( + bool Success, + ExecutionStopReason? StopReason, + string Message, + DebugProfileDefinition Profile, + RunResult? RunResult, + IReadOnlyList OutputLines, + IReadOnlyList Probes); + +public sealed record DiagnosticsBundleResult( + bool Success, + string BundleDirectory, + string? ZipPath, + string Message); + +public sealed record DiagnosticsBundleRequest( + string Category, + string ProjectRoot, + string SummaryMessage, + IReadOnlyList OutputLines, + IReadOnlyList WorkflowSteps, + IReadOnlyList Probes, + DebugDiagnosticsOptions DiagnosticsOptions, + DevToolConfig Config, + ExecutionStopReason? StopReason = null, + RunResult? DebugRun = null, + DebugProfileDefinition? DebugProfile = null); + +public interface IDebugProfileRunner +{ + Task RunAsync( + DebugProfileDefinition profile, + DevToolConfig config, + string projectRoot, + bool verbose, + Func> confirmInstallAsync, + Action onOutput, + Action? onEvent = null, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default); +} + +public interface IDiagnosticsBundleService +{ + Task WriteBundleAsync( + DiagnosticsBundleRequest request, + CancellationToken cancellationToken = default); +} diff --git a/src/DevTool.Engine/Core/Debug/DebugProfileRunner.cs b/src/DevTool.Engine/Core/Debug/DebugProfileRunner.cs new file mode 100644 index 0000000..4bb39a8 --- /dev/null +++ b/src/DevTool.Engine/Core/Debug/DebugProfileRunner.cs @@ -0,0 +1,220 @@ +using Sdt.Config; +using Sdt.Runner; + +namespace Sdt.Core.Debug; + +public sealed class DebugProfileRunner( + IToolProbe toolProbe, + IPrereqInstaller installer) : IDebugProfileRunner +{ + private readonly IToolProbe _toolProbe = toolProbe; + private readonly IPrereqInstaller _installer = installer; + + public async Task RunAsync( + DebugProfileDefinition profile, + DevToolConfig config, + string projectRoot, + bool verbose, + Func> confirmInstallAsync, + Action onOutput, + Action? onEvent = null, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + { + var probes = new List(); + var output = new List(); + onEvent?.Invoke(new RunEvent( + Category: "debug", + Type: RunEventType.DebugStarted, + Message: $"Debug profile '{profile.Id}' started.")); + var requires = profile.Requires.Count > 0 + ? profile.Requires + : InferRequirements(profile); + + foreach (var req in requires) + { + var probe = await _toolProbe.ProbeAsync(req.Tool, projectRoot, config, envOverrides, cancellationToken).ConfigureAwait(false); + probes.Add(probe); + onEvent?.Invoke(new RunEvent( + Category: "debug", + Type: RunEventType.ProbeChecked, + Message: probe.IsAvailable + ? $"Tool '{req.Tool}' is available." + : $"Tool '{req.Tool}' is missing.", + Tool: req.Tool, + Success: probe.IsAvailable)); + if (probe.IsAvailable) + continue; + + if (!string.IsNullOrWhiteSpace(probe.Details)) + { + var line = $"Probe detail [{req.Tool}]: {probe.Details}"; + output.Add("OUT: " + line); + if (verbose) + onOutput(line, false); + } + + if (req.InstallPolicy == InstallPolicy.Never) + { + onEvent?.Invoke(new RunEvent( + Category: "debug", + Type: RunEventType.DebugCompleted, + Message: $"Missing prerequisite '{req.Tool}'.", + Tool: req.Tool, + Success: false)); + return new DebugRunResult( + Success: false, + StopReason: ExecutionStopReason.MissingPrereq, + Message: $"Missing prerequisite '{req.Tool}' for debug profile '{profile.Label}'.", + Profile: profile, + RunResult: null, + OutputLines: output, + Probes: probes); + } + + var installPlan = await _installer.GetInstallPlanAsync(req.Tool, projectRoot, config, cancellationToken).ConfigureAwait(false); + onEvent?.Invoke(new RunEvent( + Category: "debug", + Type: RunEventType.InstallPlanPrepared, + Message: installPlan.Summary, + Tool: req.Tool, + Success: installPlan.Supported)); + if (!installPlan.Supported || installPlan.Commands.Count == 0) + { + onEvent?.Invoke(new RunEvent( + Category: "debug", + Type: RunEventType.DebugCompleted, + Message: $"No installer plan available for '{req.Tool}'.", + Tool: req.Tool, + Success: false)); + return new DebugRunResult( + Success: false, + StopReason: ExecutionStopReason.MissingPrereq, + Message: $"Missing prerequisite '{req.Tool}' and no installer plan is available.", + Profile: profile, + RunResult: null, + OutputLines: output, + Probes: probes); + } + + var approved = req.InstallPolicy == InstallPolicy.Auto + ? true + : await confirmInstallAsync(req.Tool, installPlan).ConfigureAwait(false); + if (!approved) + { + onEvent?.Invoke(new RunEvent( + Category: "debug", + Type: RunEventType.InstallDeclined, + Message: $"Install declined for '{req.Tool}'.", + Tool: req.Tool, + Success: false)); + return new DebugRunResult( + Success: false, + StopReason: ExecutionStopReason.UserDeclined, + Message: $"Install declined for missing prerequisite '{req.Tool}'.", + Profile: profile, + RunResult: null, + OutputLines: output, + Probes: probes); + } + + foreach (var cmd in installPlan.Commands) + { + onEvent?.Invoke(new RunEvent( + Category: "debug", + Type: RunEventType.InstallCommandStarted, + Message: $"{cmd.Command} {string.Join(" ", cmd.Args)}", + Tool: req.Tool)); + var installResult = await _installer.RunInstallAsync(cmd, projectRoot, onOutput, envOverrides, cancellationToken).ConfigureAwait(false); + onEvent?.Invoke(new RunEvent( + Category: "debug", + Type: RunEventType.InstallCommandCompleted, + Message: $"Install command exited {installResult.ExitCode}.", + Tool: req.Tool, + Success: installResult.Success, + ExitCode: installResult.ExitCode)); + if (!installResult.Success) + { + onEvent?.Invoke(new RunEvent( + Category: "debug", + Type: RunEventType.DebugCompleted, + Message: $"Install failed for '{req.Tool}'.", + Tool: req.Tool, + Success: false, + ExitCode: installResult.ExitCode)); + return new DebugRunResult( + Success: false, + StopReason: ExecutionStopReason.InstallFailed, + Message: $"Failed to install prerequisite '{req.Tool}'.", + Profile: profile, + RunResult: installResult, + OutputLines: output, + Probes: probes); + } + } + } + + var cwd = Path.GetFullPath(Path.Combine(projectRoot, profile.WorkingDir)); + Dictionary? mergedEnv = null; + if (envOverrides is not null && envOverrides.Count > 0) + mergedEnv = new Dictionary(envOverrides, StringComparer.OrdinalIgnoreCase); + if (profile.Env.Count > 0) + { + mergedEnv ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in profile.Env) + mergedEnv[kvp.Key] = kvp.Value; + } + onEvent?.Invoke(new RunEvent( + Category: "debug", + Type: RunEventType.DebugCommandStarted, + Message: $"{profile.Command} {string.Join(" ", profile.Args)}")); + var run = await ProcessRunner.RunAsync( + profile.Command, + profile.Args, + cwd, + (line, isErr) => + { + output.Add((isErr ? "ERR: " : "OUT: ") + line); + if (verbose) + onOutput(line, isErr); + }, + mergedEnv, + cancellationToken).ConfigureAwait(false); + onEvent?.Invoke(new RunEvent( + Category: "debug", + Type: RunEventType.DebugCommandCompleted, + Message: $"Debug command exited {run.ExitCode}.", + Success: run.Success, + ExitCode: run.ExitCode)); + onEvent?.Invoke(new RunEvent( + Category: "debug", + Type: RunEventType.DebugCompleted, + Message: run.Success ? "Debug run completed." : "Debug run failed.", + Success: run.Success, + ExitCode: run.ExitCode)); + + return new DebugRunResult( + Success: run.Success, + StopReason: run.Success ? null : ExecutionStopReason.CommandFailed, + Message: run.Success + ? $"Debug profile '{profile.Label}' completed." + : $"Debug profile '{profile.Label}' exited with code {run.ExitCode}.", + Profile: profile, + RunResult: run, + OutputLines: output, + Probes: probes); + } + + private static List InferRequirements(DebugProfileDefinition profile) + { + return profile.Type.ToLowerInvariant() switch + { + "dotnet" => [new ToolRequirement { Tool = "dotnet" }], + "node" => [new ToolRequirement { Tool = "node" }, new ToolRequirement { Tool = "npm" }], + "python" => [new ToolRequirement { Tool = "python" }], + _ => string.IsNullOrWhiteSpace(profile.Command) + ? [] + : [new ToolRequirement { Tool = profile.Command }], + }; + } +} diff --git a/src/DevTool.Engine/Core/Debug/DiagnosticsBundleService.cs b/src/DevTool.Engine/Core/Debug/DiagnosticsBundleService.cs new file mode 100644 index 0000000..148a378 --- /dev/null +++ b/src/DevTool.Engine/Core/Debug/DiagnosticsBundleService.cs @@ -0,0 +1,164 @@ +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using System.Text; +using System.Text.Json; +using Sdt.Config; + +namespace Sdt.Core.Debug; + +public sealed class DiagnosticsBundleService : IDiagnosticsBundleService +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true + }; + + public async Task WriteBundleAsync( + DiagnosticsBundleRequest request, + CancellationToken cancellationToken = default) + { + try + { + var timestamp = DateTimeOffset.Now.ToString("yyyyMMdd-HHmmss"); + var root = Path.GetFullPath(Path.Combine(request.ProjectRoot, request.DiagnosticsOptions.OutputDir)); + var bundleDir = Path.Combine(root, $"{request.Category}-{timestamp}"); + Directory.CreateDirectory(bundleDir); + + var stepsPath = Path.Combine(bundleDir, "steps.json"); + var toolsPath = Path.Combine(bundleDir, "tools.json"); + var envPath = Path.Combine(bundleDir, "env.json"); + var outputPath = Path.Combine(bundleDir, "output.log"); + var summaryPath = Path.Combine(bundleDir, "summary.json"); + + await File.WriteAllTextAsync(stepsPath, JsonSerializer.Serialize(request.WorkflowSteps, JsonOptions), cancellationToken); + await File.WriteAllTextAsync(toolsPath, JsonSerializer.Serialize(request.Probes, JsonOptions), cancellationToken); + await File.WriteAllTextAsync(envPath, JsonSerializer.Serialize(CaptureEnvironment(request.DiagnosticsOptions), JsonOptions), cancellationToken); + await File.WriteAllLinesAsync(outputPath, RedactOutputLines(request.OutputLines, request.DiagnosticsOptions), cancellationToken); + + var summary = new + { + category = request.Category, + stopReason = request.StopReason?.ToString(), + message = request.SummaryMessage, + createdAt = DateTimeOffset.Now, + configHash = HashConfig(request.Config), + debugProfile = request.DebugProfile?.Id, + debugExitCode = request.DebugRun?.ExitCode, + envCapture = BuildEnvCaptureSummary(request.DiagnosticsOptions), + }; + await File.WriteAllTextAsync(summaryPath, JsonSerializer.Serialize(summary, JsonOptions), cancellationToken); + + return new DiagnosticsBundleResult( + Success: true, + BundleDirectory: bundleDir, + ZipPath: null, + Message: "Diagnostics bundle generated."); + } + catch (Exception ex) + { + return new DiagnosticsBundleResult( + Success: false, + BundleDirectory: string.Empty, + ZipPath: null, + Message: ex.Message); + } + } + + private static Dictionary CaptureEnvironment(DebugDiagnosticsOptions options) + { + if (options.IncludeAllEnv) + { + var all = Environment.GetEnvironmentVariables() + .Cast() + .ToDictionary(e => e.Key?.ToString() ?? "", e => e.Value?.ToString() ?? "", StringComparer.OrdinalIgnoreCase); + return options.RedactSensitive ? RedactEnvironmentMap(all, options) : all; + } + + if (options.CaptureEnvKeys.Count == 0) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + + var captured = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var key in options.CaptureEnvKeys) + captured[key] = Environment.GetEnvironmentVariable(key) ?? ""; + return options.RedactSensitive ? RedactEnvironmentMap(captured, options) : captured; + } + + private static string BuildEnvCaptureSummary(DebugDiagnosticsOptions options) + { + if (options.IncludeAllEnv) + return "full"; + if (options.CaptureEnvKeys.Count == 0) + return "allowlist-empty (intentional minimal capture)"; + return $"allowlist ({options.CaptureEnvKeys.Count} keys)"; + } + + private static string HashConfig(object config) + { + var json = JsonSerializer.Serialize(config); + var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(json)); + return Convert.ToHexString(bytes); + } + + private static Dictionary RedactEnvironmentMap( + Dictionary input, + DebugDiagnosticsOptions options) + { + var result = new Dictionary(input, StringComparer.OrdinalIgnoreCase); + foreach (var key in input.Keys.ToList()) + { + if (IsSensitiveKey(key, options)) + result[key] = "***REDACTED***"; + } + return result; + } + + private static IReadOnlyList RedactOutputLines( + IReadOnlyList lines, + DebugDiagnosticsOptions options) + { + if (!options.RedactSensitive) + return lines.ToList(); + + var redacted = new List(lines.Count); + foreach (var line in lines) + { + var transformed = line; + + transformed = Regex.Replace( + transformed, + "(?i)\\b(bearer\\s+)[A-Za-z0-9._\\-]+", + "$1***REDACTED***"); + + transformed = Regex.Replace( + transformed, + "(?i)\\b(token|secret|password|api[_-]?key|access[_-]?key|private[_-]?key)\\s*[:=]\\s*[^\\s;]+", + m => + { + var idx = m.Value.IndexOfAny([':', '=']); + if (idx < 0) + return m.Value; + return m.Value[..(idx + 1)] + " ***REDACTED***"; + }); + + redacted.Add(transformed); + } + + return redacted; + } + + private static bool IsSensitiveKey(string key, DebugDiagnosticsOptions options) + { + if (options.RedactionAllowKeys.Any(allowed => string.Equals(allowed, key, StringComparison.OrdinalIgnoreCase))) + return false; + + foreach (var pattern in options.SensitiveKeyPatterns) + { + if (string.IsNullOrWhiteSpace(pattern)) + continue; + if (key.Contains(pattern, StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } +} diff --git a/src/DevTool.Engine/Core/EnvProfileService.cs b/src/DevTool.Engine/Core/EnvProfileService.cs new file mode 100644 index 0000000..8a252ac --- /dev/null +++ b/src/DevTool.Engine/Core/EnvProfileService.cs @@ -0,0 +1,61 @@ +using Sdt.Config; + +namespace Sdt.Core; + +public static class EnvProfileService +{ + public static IReadOnlyDictionary ResolveEffectiveEnv( + DevToolConfig config, + string? selectedProfileId) + { + var envProfiles = config.EnvProfiles; + if (envProfiles is null || envProfiles.Profiles.Count == 0) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + + var profileId = string.IsNullOrWhiteSpace(selectedProfileId) + ? envProfiles.Active + : selectedProfileId; + if (string.IsNullOrWhiteSpace(profileId)) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + + var map = envProfiles.Profiles + .Where(p => !string.IsNullOrWhiteSpace(p.Id)) + .GroupBy(p => p.Id, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); + if (!map.TryGetValue(profileId, out var root)) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + + var resolvedOrder = new List(); + var visiting = new HashSet(StringComparer.OrdinalIgnoreCase); + var visited = new HashSet(StringComparer.OrdinalIgnoreCase); + + void Visit(EnvProfileDefinition profile) + { + if (visited.Contains(profile.Id)) + return; + if (!visiting.Add(profile.Id)) + return; // cycle guard, deterministic ignore + + foreach (var parentId in profile.Inherits) + { + if (map.TryGetValue(parentId, out var parent)) + Visit(parent); + } + + visiting.Remove(profile.Id); + visited.Add(profile.Id); + resolvedOrder.Add(profile); + } + + Visit(root); + + var merged = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var profile in resolvedOrder) + { + foreach (var kvp in profile.Values) + merged[kvp.Key] = kvp.Value; + } + + return merged; + } +} diff --git a/src/DevTool.Engine/Core/ExitCodeMapper.cs b/src/DevTool.Engine/Core/ExitCodeMapper.cs new file mode 100644 index 0000000..0195ee3 --- /dev/null +++ b/src/DevTool.Engine/Core/ExitCodeMapper.cs @@ -0,0 +1,21 @@ +namespace Sdt.Core; + +public static class ExitCodeMapper +{ + public static int FromResult(bool success, ExecutionStopReason? stopReason) + { + if (success) + return 0; + + return stopReason switch + { + ExecutionStopReason.MissingPrereq => 10, + ExecutionStopReason.InstallFailed => 11, + ExecutionStopReason.CommandFailed => 12, + ExecutionStopReason.ValidationFailed => 13, + ExecutionStopReason.UserDeclined => 14, + _ => 1, + }; + } +} + diff --git a/src/DevTool.Engine/Core/FailureCard.cs b/src/DevTool.Engine/Core/FailureCard.cs new file mode 100644 index 0000000..ac51e5e --- /dev/null +++ b/src/DevTool.Engine/Core/FailureCard.cs @@ -0,0 +1,51 @@ +namespace Sdt.Core; + +public sealed record FailureCard( + string WhatFailed, + string Why, + string ExactFixCommand, + string RetryInstruction); + +public static class FailureCardBuilder +{ + public static FailureCard Build( + ExecutionStopReason? stopReason, + string message, + string targetKind, + string targetId, + string? projectRoot = null, + string? envProfile = null) + { + var reason = stopReason ?? ExecutionStopReason.ValidationFailed; + var fix = reason switch + { + ExecutionStopReason.MissingPrereq => "Run setup wizard or install missing tool, then re-run.", + ExecutionStopReason.InstallFailed => "Run the printed installer command manually and verify tool is in PATH.", + ExecutionStopReason.UserDeclined => "Re-run and approve install prompt, or preinstall prerequisites.", + ExecutionStopReason.CommandFailed => "Run the failing command directly to inspect full output.", + _ => "Validate devtool.json and working directories, then re-run.", + }; + + var projectArg = string.IsNullOrWhiteSpace(projectRoot) + ? string.Empty + : $" --project-root \"{projectRoot}\""; + var envArg = string.IsNullOrWhiteSpace(envProfile) + ? string.Empty + : $" --env-profile \"{envProfile}\""; + + var exactFix = reason switch + { + ExecutionStopReason.MissingPrereq => "sdt --init", + ExecutionStopReason.InstallFailed => "sdt --init", + ExecutionStopReason.UserDeclined => "sdt --init", + ExecutionStopReason.CommandFailed => $"sdt {targetKind} {targetId} --json{projectArg}{envArg}", + _ => $"sdt {targetKind} {targetId} --json{projectArg}{envArg}", + }; + + return new FailureCard( + WhatFailed: $"{targetKind} '{targetId}' failed", + Why: message, + ExactFixCommand: exactFix, + RetryInstruction: $"Re-run: sdt {targetKind} {targetId} --json{projectArg}{envArg}"); + } +} diff --git a/src/DevTool.Engine/Core/HeadlessExecutionService.cs b/src/DevTool.Engine/Core/HeadlessExecutionService.cs new file mode 100644 index 0000000..b901772 --- /dev/null +++ b/src/DevTool.Engine/Core/HeadlessExecutionService.cs @@ -0,0 +1,245 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Linq; +using Sdt.Config; +using Sdt.Core.Debug; + +namespace Sdt.Core; + +public sealed record HeadlessRunRequest( + string WorkflowId, + string ProjectRoot, + string? EnvProfile, + bool NonInteractive, + bool JsonOutput); + +public sealed record HeadlessDebugRequest( + string ProfileId, + string ProjectRoot, + string? EnvProfile, + bool NonInteractive, + bool JsonOutput); + +public sealed class HeadlessExecutionService +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false + }; + + static HeadlessExecutionService() + { + JsonOptions.Converters.Add(new JsonStringEnumConverter()); + } + + private readonly WorkflowExecutor _executor = new( + new WorkflowPlanner(), + new ToolProbeService(), + new PrereqInstallerService(), + new ActionRunner(), + new RequirementResolver()); + + private readonly IDebugProfileRunner _debugRunner = new DebugProfileRunner( + new ToolProbeService(), + new PrereqInstallerService()); + + public async Task RunWorkflowAsync(LoadedProjectConfig loaded, HeadlessRunRequest request, CancellationToken cancellationToken = default) + { + var normalized = WorkflowModelBuilder.Normalize(loaded.Config, ResolveLegacyMode(), new RequirementResolver()); + var workflows = normalized.Workflows.ToDictionary(w => w.Id, StringComparer.OrdinalIgnoreCase); + if (!workflows.TryGetValue(request.WorkflowId, out var workflow)) + { + WriteSummary(new + { + category = "workflow", + success = false, + stopReason = ExecutionStopReason.ValidationFailed, + message = $"Workflow '{request.WorkflowId}' not found.", + lifecycle = BuildLifecycle(null), + failure = FailureCardBuilder.Build( + ExecutionStopReason.ValidationFailed, + $"Workflow '{request.WorkflowId}' was not found.", + "run", + request.WorkflowId, + loaded.ProjectRoot, + request.EnvProfile), + }); + return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed); + } + + var runId = Guid.NewGuid().ToString("N"); + var envOverrides = EnvProfileService.ResolveEffectiveEnv(loaded.Config, request.EnvProfile); + var eventTypes = new List(); + using var recorder = RunEventJsonlRecorder.Create(loaded.ProjectRoot, "workflow", runId, request.EnvProfile); + void Emit(RunEvent evt) + { + var enriched = evt with + { + RunId = runId, + ProjectRoot = loaded.ProjectRoot, + EnvProfile = request.EnvProfile, + RunEventVersion = "1.0", + }; + eventTypes.Add(enriched.Type); + recorder.Write(enriched); + if (request.JsonOutput) + Console.WriteLine(JsonSerializer.Serialize(enriched, JsonOptions)); + } + + var outputLines = new List(); + var result = await _executor.ExecuteAsync( + workflow, + workflows, + loaded.Config, + loaded.ProjectRoot, + confirmInstallAsync: (tool, plan) => ConfirmInstallAsync(tool, plan, request.NonInteractive), + onOutput: (line, isErr) => + { + outputLines.Add((isErr ? "ERR: " : "OUT: ") + line); + }, + onEvent: Emit, + envOverrides: envOverrides.Count == 0 ? null : envOverrides, + cancellationToken: cancellationToken).ConfigureAwait(false); + + var exit = ExitCodeMapper.FromResult(result.Success, result.StopReason); + var summary = new + { + category = "workflow", + runId, + runEventVersion = "1.0", + success = result.Success, + stopReason = result.StopReason, + message = result.Message, + exitCode = exit, + steps = result.Steps, + lifecycle = BuildLifecycle(eventTypes), + failure = result.Success + ? null + : FailureCardBuilder.Build(result.StopReason, result.Message, "run", request.WorkflowId, loaded.ProjectRoot, request.EnvProfile), + }; + WriteSummary(summary); + return exit; + } + + public async Task RunDebugAsync(LoadedProjectConfig loaded, HeadlessDebugRequest request, CancellationToken cancellationToken = default) + { + var profile = loaded.Config.Debug?.Profiles.FirstOrDefault(p => + string.Equals(p.Id, request.ProfileId, StringComparison.OrdinalIgnoreCase)); + if (profile is null) + { + WriteSummary(new + { + category = "debug", + success = false, + stopReason = ExecutionStopReason.ValidationFailed, + message = $"Debug profile '{request.ProfileId}' not found.", + lifecycle = BuildLifecycle(null), + failure = FailureCardBuilder.Build( + ExecutionStopReason.ValidationFailed, + $"Debug profile '{request.ProfileId}' was not found.", + "debug", + request.ProfileId, + loaded.ProjectRoot, + request.EnvProfile), + }); + return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed); + } + + var runId = Guid.NewGuid().ToString("N"); + var envOverrides = EnvProfileService.ResolveEffectiveEnv(loaded.Config, request.EnvProfile); + var eventTypes = new List(); + using var recorder = RunEventJsonlRecorder.Create(loaded.ProjectRoot, "debug", runId, request.EnvProfile); + void Emit(RunEvent evt) + { + var enriched = evt with + { + RunId = runId, + ProjectRoot = loaded.ProjectRoot, + EnvProfile = request.EnvProfile, + RunEventVersion = "1.0", + }; + eventTypes.Add(enriched.Type); + recorder.Write(enriched); + if (request.JsonOutput) + Console.WriteLine(JsonSerializer.Serialize(enriched, JsonOptions)); + } + + var result = await _debugRunner.RunAsync( + profile, + loaded.Config, + loaded.ProjectRoot, + verbose: false, + confirmInstallAsync: (tool, plan) => ConfirmInstallAsync(tool, plan, request.NonInteractive), + onOutput: (_, _) => { }, + onEvent: Emit, + envOverrides: envOverrides.Count == 0 ? null : envOverrides, + cancellationToken: cancellationToken).ConfigureAwait(false); + + var exit = ExitCodeMapper.FromResult(result.Success, result.StopReason); + var summary = new + { + category = "debug", + runId, + runEventVersion = "1.0", + success = result.Success, + stopReason = result.StopReason, + message = result.Message, + exitCode = exit, + profile = result.Profile.Id, + lifecycle = BuildLifecycle(eventTypes), + failure = result.Success + ? null + : FailureCardBuilder.Build(result.StopReason, result.Message, "debug", request.ProfileId, loaded.ProjectRoot, request.EnvProfile), + }; + WriteSummary(summary); + return exit; + } + + private static object BuildLifecycle(IReadOnlyCollection? eventTypes) + { + var types = eventTypes ?? []; + var planned = types.Contains(RunEventType.WorkflowPlanned) || types.Contains(RunEventType.DebugStarted); + var probed = types.Contains(RunEventType.ProbeChecked) || types.Contains(RunEventType.ProbeFailed); + var prompted = types.Contains(RunEventType.InstallPlanPrepared) || types.Contains(RunEventType.InstallDeclined); + var executed = + types.Contains(RunEventType.WorkflowStepStarted) || + types.Contains(RunEventType.WorkflowStepCompleted) || + types.Contains(RunEventType.DebugCommandStarted) || + types.Contains(RunEventType.DebugCommandCompleted); + var diagnosed = types.Contains(RunEventType.WorkflowCompleted) || types.Contains(RunEventType.DebugCompleted); + + return new + { + model = "Plan -> Probe -> Prompt -> Execute -> Diagnose", + plan = planned, + probe = probed, + prompt = prompted, + execute = executed, + diagnose = diagnosed, + }; + } + + private static Task ConfirmInstallAsync(string tool, InstallPlan plan, bool nonInteractive) + { + if (nonInteractive) + return Task.FromResult(false); + + Console.WriteLine($"Install required for {tool}: {plan.Summary}"); + foreach (var cmd in plan.Commands) + Console.WriteLine($" $ {cmd.Command} {string.Join(" ", cmd.Args)}"); + + return Task.FromResult(false); + } + + private static LegacyMode ResolveLegacyMode() + { + var raw = Environment.GetEnvironmentVariable("SDT_LEGACY_MODE"); + return string.Equals(raw, "compat", StringComparison.OrdinalIgnoreCase) + ? LegacyMode.Compat + : LegacyMode.Strict; + } + + private static void WriteSummary(object payload) + => Console.WriteLine(JsonSerializer.Serialize(payload, JsonOptions)); +} diff --git a/src/DevTool.Engine/Core/LegacyScriptRequirementResolver.cs b/src/DevTool.Engine/Core/LegacyScriptRequirementResolver.cs new file mode 100644 index 0000000..39057de --- /dev/null +++ b/src/DevTool.Engine/Core/LegacyScriptRequirementResolver.cs @@ -0,0 +1,59 @@ +using Sdt.Config; + +namespace Sdt.Core; + +internal static class LegacyScriptRequirementResolver +{ + public static List InferForPowerShellArgs(IReadOnlyList args) + { + var script = FindScriptArg(args); + if (string.IsNullOrWhiteSpace(script)) + return []; + + static ToolRequirement Req(string tool) => new() { Tool = tool, InstallPolicy = InstallPolicy.Prompt }; + + var file = Path.GetFileName(script).ToLowerInvariant(); + var lowerArgs = args.Select(a => a.ToLowerInvariant()).ToList(); + + return file switch + { + "publish-app.ps1" => IsTauriTarget(lowerArgs) + ? [Req("python"), Req("node"), Req("npm"), Req("cargo")] + : [Req("python"), Req("node"), Req("npm")], + "publish-sidecar.ps1" => [Req("python"), Req("dotnet")], + "publish-webgateway.ps1" => [Req("python"), Req("dotnet"), Req("node"), Req("npm")], + "run-webgateway.ps1" => [Req("python"), Req("dotnet")], + "migration-gate.ps1" => [Req("python"), Req("dotnet")], + "nuget-export-cache.ps1" => [Req("python"), Req("dotnet")], + "nuget-import-cache.ps1" => [Req("python"), Req("dotnet")], + "npm-clean.ps1" => [Req("python"), Req("node"), Req("npm")], + "publish-output.ps1" => [Req("python"), Req("dotnet"), Req("node"), Req("npm"), Req("cargo")], + "sync-output.ps1" => [Req("python")], + "dotnet-min.ps1" => [Req("python"), Req("dotnet")], + "pip-min.ps1" => [Req("python")], + _ => [Req("python")] + }; + } + + private static string? FindScriptArg(IReadOnlyList args) + { + for (var i = 0; i < args.Count - 1; i++) + { + if (string.Equals(args[i], "-File", StringComparison.OrdinalIgnoreCase)) + return args[i + 1]; + } + + return args.FirstOrDefault(a => a.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase)); + } + + private static bool IsTauriTarget(IReadOnlyList lowerArgs) + { + for (var i = 0; i < lowerArgs.Count - 1; i++) + { + if (lowerArgs[i] is "-target" or "--target" && lowerArgs[i + 1] == "tauri") + return true; + } + + return false; + } +} diff --git a/src/DevTool.Engine/Core/PrereqInstallerService.cs b/src/DevTool.Engine/Core/PrereqInstallerService.cs new file mode 100644 index 0000000..b73f9c9 --- /dev/null +++ b/src/DevTool.Engine/Core/PrereqInstallerService.cs @@ -0,0 +1,448 @@ +using System.Diagnostics; +using System.Text.Json; +using System.Text.RegularExpressions; +using Sdt.Config; +using Sdt.Runner; + +namespace Sdt.Core; + +public sealed class PrereqInstallerService : IPrereqInstaller +{ + public async Task GetInstallPlanAsync( + string tool, + string projectRoot, + DevToolConfig? config = null, + CancellationToken cancellationToken = default) + { + var fromConfig = TryGetPlanFromConfig(tool, config); + if (fromConfig is not null) + return NormalizePlan(fromConfig); + + var scriptPath = ScriptLocator.FindHelperScript(projectRoot, "diag.py"); + if (scriptPath is null) + return NormalizePlan(FallbackPlan(tool)); + + try + { + var psi = new ProcessStartInfo + { + FileName = PythonResolver.ResolveExecutable(), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = projectRoot, + }; + psi.ArgumentList.Add(scriptPath); + psi.ArgumentList.Add("install-plan"); + psi.ArgumentList.Add("--tool"); + psi.ArgumentList.Add(tool); + psi.ArgumentList.Add("--json"); + + using var process = new Process { StartInfo = psi }; + process.Start(); + var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + var stderr = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + + if (process.ExitCode != 0) + { + var fallback = FallbackPlan(tool); + return NormalizePlan(fallback with + { + Summary = $"diag.py install-plan failed for {tool}; using fallback templates. " + + $"{(string.IsNullOrWhiteSpace(stderr) ? "No stderr output." : stderr.Trim())}" + }); + } + + var parsed = JsonSerializer.Deserialize(stdout); + if (parsed is null) + { + var fallback = FallbackPlan(tool); + return NormalizePlan(fallback with { Summary = $"diag.py returned invalid JSON for {tool}; using fallback templates." }); + } + + var commands = parsed.Commands + .Select(c => new InstallCommand(c.Command ?? "", c.Args ?? [])) + .Where(c => !string.IsNullOrWhiteSpace(c.Command)) + .ToList(); + if (!parsed.Supported || commands.Count == 0) + { + var fallback = FallbackPlan(tool); + return NormalizePlan(fallback with { Summary = $"diag.py returned no usable commands for {tool}; using fallback templates." }); + } + + return NormalizePlan(new InstallPlan( + parsed.Tool ?? tool, + parsed.Supported, + parsed.Summary ?? $"Install {tool}", + commands)); + } + catch (Exception ex) + { + var fallback = FallbackPlan(tool); + return NormalizePlan(fallback with { Summary = $"diag.py install-plan exception for {tool}; using fallback templates. {ex.Message}" }); + } + } + + public Task RunInstallAsync( + InstallCommand command, + string projectRoot, + Action onOutput, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + { + var normalized = NormalizeInstallCommand(command); + return ProcessRunner.RunAsync( + normalized.Command, + normalized.Args, + projectRoot, + onOutput, + envOverrides, + cancellationToken: cancellationToken); + } + + private static InstallPlan FallbackPlan(string tool) + { + var isWindows = OperatingSystem.IsWindows(); + var normalized = tool.ToLowerInvariant(); + if (normalized == "tauri") + return BuildTauriFallbackPlan(); + + var installCommand = normalized switch + { + "dotnet" => isWindows + ? new InstallCommand("winget", ["install", "Microsoft.DotNet.SDK.10"]) + : new InstallCommand("sh", ["-c", "echo Install dotnet SDK from your distro package manager"]), + "node" => isWindows + ? new InstallCommand("winget", ["install", "OpenJS.NodeJS.LTS"]) + : new InstallCommand("sh", ["-c", "echo Install nodejs via package manager"]), + "npm" => isWindows + ? new InstallCommand("winget", ["install", "OpenJS.NodeJS.LTS"]) + : new InstallCommand("sh", ["-c", "echo Install npm via package manager"]), + "cargo" => isWindows + ? new InstallCommand("winget", ["install", "Rustlang.Rustup"]) + : new InstallCommand("sh", ["-c", "curl https://sh.rustup.rs -sSf | sh"]), + "git" => isWindows + ? new InstallCommand("winget", ["install", "Git.Git"]) + : new InstallCommand("sh", ["-c", "echo Install git via package manager"]), + "docker" => isWindows + ? new InstallCommand("winget", ["install", "Docker.DockerDesktop"]) + : new InstallCommand("sh", ["-c", "echo Install docker engine via package manager"]), + "python" => isWindows + ? new InstallCommand("winget", ["install", "Python.Python.3.12"]) + : new InstallCommand("sh", ["-c", "echo Install python3 via package manager"]), + "go" => isWindows + ? new InstallCommand("winget", ["install", "GoLang.Go"]) + : new InstallCommand("sh", ["-c", "echo Install golang via package manager"]), + "maven" => isWindows + ? new InstallCommand("winget", ["install", "Apache.Maven"]) + : new InstallCommand("sh", ["-c", "echo Install maven via package manager"]), + "gradle" => isWindows + ? new InstallCommand("winget", ["install", "Gradle.Gradle"]) + : new InstallCommand("sh", ["-c", "echo Install gradle via package manager"]), + "java" => isWindows + ? new InstallCommand("winget", ["install", "Microsoft.OpenJDK.21"]) + : new InstallCommand("sh", ["-c", "echo Install JDK via package manager"]), + _ => new InstallCommand("sh", ["-c", $"echo No installer template for '{tool}'"]), + }; + + return new InstallPlan( + tool, + Supported: true, + Summary: $"Fallback install plan for {tool}", + Commands: [installCommand]); + } + + private static InstallCommand NormalizeInstallCommand(InstallCommand command) + { + var normalizedCommand = command.Command.Trim(); + if (string.Equals(normalizedCommand, "winget", StringComparison.OrdinalIgnoreCase)) + return NormalizeWingetCommand(command); + if (string.Equals(normalizedCommand, "choco", StringComparison.OrdinalIgnoreCase) || + string.Equals(normalizedCommand, "chocolatey", StringComparison.OrdinalIgnoreCase)) + return NormalizeChocolateyCommand(command); + if (string.Equals(normalizedCommand, "apt-get", StringComparison.OrdinalIgnoreCase) || + string.Equals(normalizedCommand, "apt", StringComparison.OrdinalIgnoreCase)) + return NormalizeAptCommand(command); + if (string.Equals(normalizedCommand, "dnf", StringComparison.OrdinalIgnoreCase) || + string.Equals(normalizedCommand, "yum", StringComparison.OrdinalIgnoreCase) || + string.Equals(normalizedCommand, "zypper", StringComparison.OrdinalIgnoreCase)) + return NormalizeYesCommand(command); + if (string.Equals(normalizedCommand, "pacman", StringComparison.OrdinalIgnoreCase)) + return NormalizePacmanCommand(command); + if (string.Equals(normalizedCommand, "sh", StringComparison.OrdinalIgnoreCase) || + string.Equals(normalizedCommand, "bash", StringComparison.OrdinalIgnoreCase)) + return NormalizeShellCommand(command); + return command; + } + + private static InstallCommand NormalizeWingetCommand(InstallCommand command) + { + var args = command.Args.ToList(); + EnsureWingetFlag(args, "--accept-package-agreements"); + EnsureWingetFlag(args, "--accept-source-agreements"); + EnsureWingetFlag(args, "--disable-interactivity"); + EnsureWingetFlag(args, "--source"); + EnsureWingetFlag(args, "winget"); + return new InstallCommand(command.Command, args); + } + + private static void EnsureWingetFlag(List args, string flag) + { + if (args.Any(a => string.Equals(a, flag, StringComparison.OrdinalIgnoreCase))) + return; + args.Add(flag); + } + + private static InstallCommand NormalizeChocolateyCommand(InstallCommand command) + { + var args = command.Args.ToList(); + EnsureFlag(args, "-y"); + EnsureFlag(args, "--no-progress"); + return new InstallCommand(command.Command, args); + } + + private static InstallCommand NormalizeAptCommand(InstallCommand command) + { + var args = command.Args.ToList(); + EnsureFlag(args, "-y"); + return new InstallCommand(command.Command, args); + } + + private static InstallCommand NormalizeYesCommand(InstallCommand command) + { + var args = command.Args.ToList(); + EnsureFlag(args, "-y"); + return new InstallCommand(command.Command, args); + } + + private static InstallCommand NormalizePacmanCommand(InstallCommand command) + { + var args = command.Args.ToList(); + EnsureFlag(args, "--noconfirm"); + return new InstallCommand(command.Command, args); + } + + private static InstallCommand NormalizeShellCommand(InstallCommand command) + { + var args = command.Args.ToList(); + if (args.Count >= 2 && string.Equals(args[0], "-c", StringComparison.OrdinalIgnoreCase)) + { + var script = args[1]; + if (script.Contains("apt-get", StringComparison.OrdinalIgnoreCase) || + script.Contains(" apt ", StringComparison.OrdinalIgnoreCase)) + { + if (!script.Contains("DEBIAN_FRONTEND=noninteractive", StringComparison.OrdinalIgnoreCase)) + script = $"DEBIAN_FRONTEND=noninteractive {script}"; + if (!script.Contains(" -y", StringComparison.OrdinalIgnoreCase)) + { + script = script + .Replace("apt-get install ", "apt-get install -y ", StringComparison.OrdinalIgnoreCase) + .Replace("apt install ", "apt install -y ", StringComparison.OrdinalIgnoreCase); + } + } + + if (script.Contains("pacman ", StringComparison.OrdinalIgnoreCase) && + !script.Contains("--noconfirm", StringComparison.OrdinalIgnoreCase)) + { + script = script.Replace("pacman -S ", "pacman -S --noconfirm ", StringComparison.OrdinalIgnoreCase); + } + + args[1] = script; + } + + return new InstallCommand(command.Command, args); + } + + private static void EnsureFlag(List args, string flag) + { + if (args.Any(a => string.Equals(a, flag, StringComparison.OrdinalIgnoreCase))) + return; + args.Add(flag); + } + + private static InstallPlan BuildTauriFallbackPlan() + { + if (OperatingSystem.IsWindows()) + { + return new InstallPlan( + "tauri", + Supported: true, + Summary: "Fallback tauri plan (Windows): install Node.js, Rust toolchain, and Tauri CLI. Visual Studio C++ build tools/WebView2 may also be required.", + Commands: + [ + new InstallCommand("winget", ["install", "OpenJS.NodeJS.LTS"]), + new InstallCommand("winget", ["install", "Rustlang.Rustup"]), + new InstallCommand("npm", ["install", "-g", "@tauri-apps/cli"]), + ]); + } + + if (OperatingSystem.IsMacOS()) + { + return new InstallPlan( + "tauri", + Supported: true, + Summary: "Fallback tauri plan (macOS): install Xcode command line tools, Rust toolchain, and Tauri CLI.", + Commands: + [ + new InstallCommand("sh", ["-c", "xcode-select --install || true"]), + new InstallCommand("sh", ["-c", "curl https://sh.rustup.rs -sSf | sh -s -- -y"]), + new InstallCommand("npm", ["install", "-g", "@tauri-apps/cli"]), + ]); + } + + var linuxPlan = BuildLinuxTauriPrereqCommand(); + return new InstallPlan( + "tauri", + Supported: true, + Summary: $"Fallback tauri plan (Linux): detected package manager `{linuxPlan.PackageManager}` for system deps, then install Rust toolchain and Tauri CLI.", + Commands: + [ + new InstallCommand("sh", ["-c", linuxPlan.Command]), + new InstallCommand("sh", ["-c", "curl https://sh.rustup.rs -sSf | sh -s -- -y"]), + new InstallCommand("npm", ["install", "-g", "@tauri-apps/cli"]), + ]); + } + + private static (string PackageManager, string Command) BuildLinuxTauriPrereqCommand() + { + if (CommandExists("apt-get")) + { + return ("apt-get", + "sudo apt-get update && sudo apt-get install -y build-essential libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev"); + } + + if (CommandExists("dnf")) + { + return ("dnf", + "sudo dnf install -y gcc gcc-c++ make webkit2gtk4.1-devel gtk3-devel libappindicator-gtk3 librsvg2-devel"); + } + + if (CommandExists("pacman")) + { + return ("pacman", + "sudo pacman -S --needed base-devel webkit2gtk gtk3 libappindicator-gtk3 librsvg"); + } + + if (CommandExists("zypper")) + { + return ("zypper", + "sudo zypper install -y gcc gcc-c++ make webkit2gtk3-devel gtk3-devel libappindicator3-devel librsvg-devel"); + } + + if (CommandExists("apk")) + { + return ("apk", + "sudo apk add build-base webkit2gtk-dev gtk+3.0-dev libayatana-appindicator-dev librsvg-dev"); + } + + return ("unknown", + "echo Install tauri system dependencies using your distro package manager, then rerun SDT."); + } + + private static bool CommandExists(string command) + { + try + { + var psi = new ProcessStartInfo + { + FileName = OperatingSystem.IsWindows() ? "where" : "which", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + psi.ArgumentList.Add(command); + using var process = Process.Start(psi); + if (process is null) + return false; + process.WaitForExit(1500); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + private static InstallPlan? TryGetPlanFromConfig(string tool, DevToolConfig? config) + { + var preferred = config?.Tooling?.Tools + .FirstOrDefault(t => string.Equals(t.Tool, tool, StringComparison.OrdinalIgnoreCase)) + ?.PreferredInstallCommands; + + if (preferred is null || preferred.Count == 0) + return null; + + var commands = new List(); + foreach (var line in preferred) + { + var parts = SplitShellLike(line); + if (parts.Count == 0) + continue; + + commands.Add(new InstallCommand(parts[0], parts.Skip(1).ToList())); + } + + if (commands.Count == 0) + return null; + + return new InstallPlan( + tool, + Supported: true, + Summary: $"Configured install commands for {tool}", + Commands: commands); + } + + private static InstallPlan NormalizePlan(InstallPlan plan) + { + var commands = plan.Commands.Select(NormalizeInstallCommand).ToList(); + var summarySuffix = "Non-interactive flags normalized where supported."; + var summary = plan.Summary.Contains(summarySuffix, StringComparison.OrdinalIgnoreCase) + ? plan.Summary + : $"{plan.Summary} {summarySuffix}"; + + return plan with + { + Commands = commands, + Summary = summary + }; + } + + private static List SplitShellLike(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return []; + + var tokens = new List(); + var matches = Regex.Matches(input, "\"([^\"]*)\"|'([^']*)'|\\S+"); + foreach (Match match in matches) + { + var value = match.Value.Trim(); + if (value.Length >= 2 && ( + (value.StartsWith("\"", StringComparison.Ordinal) && value.EndsWith("\"", StringComparison.Ordinal)) || + (value.StartsWith("'", StringComparison.Ordinal) && value.EndsWith("'", StringComparison.Ordinal)))) + { + value = value[1..^1]; + } + if (!string.IsNullOrWhiteSpace(value)) + tokens.Add(value); + } + return tokens; + } + + private sealed class InstallPlanJson + { + public string? Tool { get; init; } + public bool Supported { get; init; } + public string? Summary { get; init; } + public List Commands { get; init; } = []; + } + + private sealed class InstallCommandJson + { + public string? Command { get; init; } + public List? Args { get; init; } + } +} diff --git a/src/DevTool.Engine/Core/PythonResolver.cs b/src/DevTool.Engine/Core/PythonResolver.cs new file mode 100644 index 0000000..928c79a --- /dev/null +++ b/src/DevTool.Engine/Core/PythonResolver.cs @@ -0,0 +1,46 @@ +using System.Diagnostics; + +namespace Sdt.Core; + +internal static class PythonResolver +{ + public static string ResolveExecutable() + { + var candidates = OperatingSystem.IsWindows() + ? new[] { "python", "py" } + : new[] { "python3", "python" }; + + foreach (var candidate in candidates) + { + if (CanRun(candidate)) + return candidate; + } + + return "python"; + } + + private static bool CanRun(string exe) + { + try + { + var psi = new ProcessStartInfo + { + FileName = exe, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + psi.ArgumentList.Add("--version"); + + using var p = new Process { StartInfo = psi }; + p.Start(); + p.WaitForExit(2000); + return p.ExitCode == 0; + } + catch + { + return false; + } + } +} diff --git a/src/DevTool.Engine/Core/RequirementResolver.cs b/src/DevTool.Engine/Core/RequirementResolver.cs new file mode 100644 index 0000000..5151806 --- /dev/null +++ b/src/DevTool.Engine/Core/RequirementResolver.cs @@ -0,0 +1,73 @@ +using Sdt.Config; + +namespace Sdt.Core; + +public sealed class RequirementResolver : IRequirementResolver +{ + public List Resolve(WorkflowStep step) + { + if (step.Requires.Count > 0) + return step.Requires.ToList(); + + if (!string.IsNullOrWhiteSpace(step.Action)) + return InferActionRequirements(step.Action); + + if (string.IsNullOrWhiteSpace(step.Command)) + return []; + + return InferCommandRequirements(step.Command, step.Args); + } + + public List Resolve(BuildTarget target) + { + if (string.IsNullOrWhiteSpace(target.Command)) + return []; + + return InferCommandRequirements(target.Command, target.Args); + } + + private static List InferActionRequirements(string action) + { + return action.ToLowerInvariant() switch + { + "dotnet-restore" or "dotnet-build" or "dotnet-test" or "dotnet-publish" => [Req("dotnet")], + "npm-install" or "npm-ci" or "npm-build" or "npm-test" or "npm-audit" => [Req("node"), Req("npm")], + "python-venv-create" or "python-pip-install" or "python-pip-sync" or "python-pytest" => [Req("python")], + "cargo-build" or "cargo-test" => [Req("cargo")], + "tauri-build" => [Req("cargo"), Req("node"), Req("npm")], + "git-status" or "git-fetch" or "git-pull" or "git-clean" => [Req("git")], + "docker-build" or "docker-compose-up" or "docker-compose-down" => [Req("docker")], + "go-build" or "go-test" => [Req("go")], + "maven-build" or "maven-test" => [Req("maven")], + "gradle-build" or "gradle-test" => [Req("gradle")], + _ => [], + }; + } + + private static List InferCommandRequirements(string command, IReadOnlyList args) + { + return command.ToLowerInvariant() switch + { + "dotnet" => [Req("dotnet")], + "npm" => [Req("node"), Req("npm")], + "pnpm" => [Req("node"), Req("pnpm")], + "yarn" => [Req("node"), Req("yarn")], + "python" or "py" => [Req("python")], + "cargo" => [Req("cargo")], + "tauri" => [Req("cargo"), Req("node"), Req("npm")], + "git" => [Req("git")], + "docker" => [Req("docker")], + "go" => [Req("go")], + "mvn" => [Req("maven")], + "gradle" => [Req("gradle")], + "pwsh" or "powershell" => LegacyScriptRequirementResolver.InferForPowerShellArgs(args), + _ => [], + }; + } + + private static ToolRequirement Req(string tool) => new() + { + Tool = tool, + InstallPolicy = InstallPolicy.Prompt, + }; +} diff --git a/src/DevTool.Engine/Core/RunEventJsonlRecorder.cs b/src/DevTool.Engine/Core/RunEventJsonlRecorder.cs new file mode 100644 index 0000000..bd9393f --- /dev/null +++ b/src/DevTool.Engine/Core/RunEventJsonlRecorder.cs @@ -0,0 +1,78 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Sdt.Core; + +public sealed class RunEventJsonlRecorder : IDisposable +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false + }; + + static RunEventJsonlRecorder() + { + JsonOptions.Converters.Add(new JsonStringEnumConverter()); + } + + private readonly StreamWriter _writer; + private readonly object _gate = new(); + private readonly string _runId; + private readonly string _projectRoot; + private readonly string? _envProfile; + private bool _disposed; + + public string FilePath { get; } + + private RunEventJsonlRecorder(string filePath, StreamWriter writer, string runId, string projectRoot, string? envProfile) + { + FilePath = filePath; + _writer = writer; + _runId = runId; + _projectRoot = projectRoot; + _envProfile = envProfile; + } + + public static RunEventJsonlRecorder Create(string projectRoot, string category, string? runId = null, string? envProfile = null) + { + var root = Path.Combine(projectRoot, ".sdt", "events"); + Directory.CreateDirectory(root); + var fileName = $"{category}-{DateTimeOffset.Now:yyyyMMdd-HHmmss}.jsonl"; + var path = Path.Combine(root, fileName); + var writer = new StreamWriter(new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + AutoFlush = true + }; + return new RunEventJsonlRecorder(path, writer, runId ?? Guid.NewGuid().ToString("N"), projectRoot, envProfile); + } + + public void Write(RunEvent evt) + { + lock (_gate) + { + if (_disposed) + return; + var normalized = evt with + { + RunId = string.IsNullOrWhiteSpace(evt.RunId) ? _runId : evt.RunId, + ProjectRoot = string.IsNullOrWhiteSpace(evt.ProjectRoot) ? _projectRoot : evt.ProjectRoot, + EnvProfile = evt.EnvProfile ?? _envProfile, + RunEventVersion = string.IsNullOrWhiteSpace(evt.EventVersion) ? "1.0" : evt.EventVersion, + }; + var line = JsonSerializer.Serialize(normalized, JsonOptions); + _writer.WriteLine(line); + } + } + + public void Dispose() + { + lock (_gate) + { + if (_disposed) + return; + _disposed = true; + _writer.Dispose(); + } + } +} diff --git a/src/DevTool.Engine/Core/RunEventLogReader.cs b/src/DevTool.Engine/Core/RunEventLogReader.cs new file mode 100644 index 0000000..2fdee3c --- /dev/null +++ b/src/DevTool.Engine/Core/RunEventLogReader.cs @@ -0,0 +1,178 @@ +using System.Text.Json; + +namespace Sdt.Core; + +public sealed record RunEventLogFile( + string Path, + string Name, + DateTimeOffset LastWriteTime, + long SizeBytes); + +public sealed record RunHistoryItem( + string FilePath, + DateTimeOffset LastWriteTime, + string Category, + string? RunId, + string? ProjectRoot, + string? EnvProfile, + string? TargetId, + bool? Success, + int? ExitCode, + string Message); + +public sealed class RunEventLogReader +{ + public IReadOnlyList ListEventFiles(string projectRoot) + { + var eventsRoot = Path.Combine(projectRoot, ".sdt", "events"); + if (!Directory.Exists(eventsRoot)) + return []; + + return Directory.EnumerateFiles(eventsRoot, "*.jsonl", SearchOption.TopDirectoryOnly) + .Select(path => + { + var info = new FileInfo(path); + return new RunEventLogFile( + Path: path, + Name: info.Name, + LastWriteTime: info.LastWriteTime, + SizeBytes: info.Length); + }) + .OrderByDescending(f => f.LastWriteTime) + .ToList(); + } + + public IReadOnlyList ReadEvents(string filePath) + { + var results = new List(); + if (!File.Exists(filePath)) + return results; + + foreach (var line in File.ReadLines(filePath)) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + + if (TryParseLine(line, out var evt)) + results.Add(evt!); + } + + return results; + } + + public IReadOnlyList ListRunHistory(string projectRoot, int maxItems = 50) + { + var files = ListEventFiles(projectRoot).Take(maxItems).ToList(); + var items = new List(); + foreach (var file in files) + { + var events = ReadEvents(file.Path); + if (events.Count == 0) + continue; + + var first = events[0]; + var last = events[^1]; + var category = first.Category; + string? targetId = null; + if (string.Equals(category, "workflow", StringComparison.OrdinalIgnoreCase)) + targetId = events.FirstOrDefault(e => e.Type == RunEventType.WorkflowStarted)?.WorkflowId; + else if (string.Equals(category, "debug", StringComparison.OrdinalIgnoreCase)) + targetId = ParseDebugProfileId(events.FirstOrDefault(e => e.Type == RunEventType.DebugStarted)?.Message); + + items.Add(new RunHistoryItem( + FilePath: file.Path, + LastWriteTime: file.LastWriteTime, + Category: category, + RunId: first.EventRunId, + ProjectRoot: first.EventProjectRoot, + EnvProfile: first.EventEnvProfile, + TargetId: targetId, + Success: last.Success, + ExitCode: last.ExitCode, + Message: last.Message)); + } + + return items; + } + + internal static bool TryParseLine(string jsonLine, out RunEvent? evt) + { + evt = null; + try + { + using var doc = JsonDocument.Parse(jsonLine); + var root = doc.RootElement; + + var category = root.TryGetProperty("category", out var c) ? c.GetString() : null; + var typeRaw = root.TryGetProperty("type", out var t) ? t.GetString() : null; + if (string.IsNullOrWhiteSpace(typeRaw) && root.TryGetProperty("event_type", out var et)) + typeRaw = et.GetString(); + var message = root.TryGetProperty("message", out var m) ? m.GetString() : null; + var workflowId = root.TryGetProperty("workflowId", out var wf) ? wf.GetString() : null; + var stepId = root.TryGetProperty("stepId", out var st) ? st.GetString() : null; + var tool = root.TryGetProperty("tool", out var tl) ? tl.GetString() : null; + var success = root.TryGetProperty("success", out var s) && s.ValueKind != JsonValueKind.Null ? s.GetBoolean() : (bool?)null; + var exitCode = root.TryGetProperty("exitCode", out var ec) && ec.ValueKind != JsonValueKind.Null ? ec.GetInt32() : (int?)null; + var runId = root.TryGetProperty("run_id", out var rid) ? rid.GetString() : null; + var projectRoot = root.TryGetProperty("project_root", out var pr) ? pr.GetString() : null; + var envProfile = root.TryGetProperty("env_profile", out var ep) ? ep.GetString() : null; + var version = root.TryGetProperty("run_event_version", out var rv) ? rv.GetString() : null; + DateTimeOffset? occurred = null; + if (root.TryGetProperty("occurredAt", out var ts) && ts.ValueKind == JsonValueKind.String && + DateTimeOffset.TryParse(ts.GetString(), out var parsed)) + { + occurred = parsed; + } + if (occurred is null && + root.TryGetProperty("timestamp_utc", out var tsUtc) && + tsUtc.ValueKind == JsonValueKind.String && + DateTimeOffset.TryParse(tsUtc.GetString(), out var parsedUtc)) + { + occurred = parsedUtc; + } + + if (string.IsNullOrWhiteSpace(category) || + string.IsNullOrWhiteSpace(typeRaw) || + string.IsNullOrWhiteSpace(message) || + !Enum.TryParse(typeRaw, ignoreCase: true, out var type)) + { + return false; + } + + evt = new RunEvent( + Category: category!, + Type: type, + Message: message!, + WorkflowId: workflowId, + StepId: stepId, + Tool: tool, + Success: success, + ExitCode: exitCode, + Timestamp: occurred, + RunId: runId, + ProjectRoot: projectRoot, + EnvProfile: envProfile, + RunEventVersion: version); + return true; + } + catch + { + return false; + } + } + + private static string? ParseDebugProfileId(string? message) + { + if (string.IsNullOrWhiteSpace(message)) + return null; + + // Message format: Debug profile 'profile-id' started. + var start = message.IndexOf('\'', StringComparison.Ordinal); + if (start < 0) + return null; + var end = message.IndexOf('\'', start + 1); + if (end <= start + 1) + return null; + return message[(start + 1)..end]; + } +} diff --git a/src/DevTool.Engine/Core/RunEvents.cs b/src/DevTool.Engine/Core/RunEvents.cs new file mode 100644 index 0000000..1db2d0c --- /dev/null +++ b/src/DevTool.Engine/Core/RunEvents.cs @@ -0,0 +1,58 @@ +using System.Text.Json.Serialization; + +namespace Sdt.Core; + +public enum RunEventType +{ + WorkflowStarted, + WorkflowPlanned, + WorkflowStepStarted, + WorkflowStepCompleted, + ProbeChecked, + ProbeFailed, + InstallPlanPrepared, + InstallDeclined, + InstallCommandStarted, + InstallCommandCompleted, + WorkflowCompleted, + DebugStarted, + DebugCommandStarted, + DebugCommandCompleted, + DebugCompleted, +} + +public sealed record RunEvent( + string Category, + RunEventType Type, + string Message, + string? WorkflowId = null, + string? StepId = null, + string? Tool = null, + bool? Success = null, + int? ExitCode = null, + [property: JsonIgnore] DateTimeOffset? Timestamp = null, + [property: JsonIgnore] string? RunId = null, + [property: JsonIgnore] string? ProjectRoot = null, + [property: JsonIgnore] string? EnvProfile = null, + [property: JsonIgnore] string? RunEventVersion = null) +{ + public DateTimeOffset OccurredAt { get; init; } = Timestamp ?? DateTimeOffset.Now; + + [JsonPropertyName("run_event_version")] + public string EventVersion => string.IsNullOrWhiteSpace(RunEventVersion) ? "1.0" : RunEventVersion!; + + [JsonPropertyName("run_id")] + public string? EventRunId => RunId; + + [JsonPropertyName("project_root")] + public string? EventProjectRoot => ProjectRoot; + + [JsonPropertyName("env_profile")] + public string? EventEnvProfile => EnvProfile; + + [JsonPropertyName("timestamp_utc")] + public DateTimeOffset TimestampUtc { get; init; } = (Timestamp ?? DateTimeOffset.UtcNow).ToUniversalTime(); + + [JsonPropertyName("event_type")] + public string EventType => Type.ToString(); +} diff --git a/src/DevTool.Engine/Core/RuntimePolicy.cs b/src/DevTool.Engine/Core/RuntimePolicy.cs new file mode 100644 index 0000000..f564ded --- /dev/null +++ b/src/DevTool.Engine/Core/RuntimePolicy.cs @@ -0,0 +1,20 @@ +namespace Sdt.Core; + +public static class RuntimePolicy +{ + public static bool IsNonInteractive(bool cliOverride = false) + { + if (cliOverride) + return true; + + var raw = Environment.GetEnvironmentVariable("SDT_NONINTERACTIVE"); + if (string.IsNullOrWhiteSpace(raw)) + return false; + + return raw.Equals("1", StringComparison.OrdinalIgnoreCase) || + raw.Equals("true", StringComparison.OrdinalIgnoreCase) || + raw.Equals("yes", StringComparison.OrdinalIgnoreCase) || + raw.Equals("on", StringComparison.OrdinalIgnoreCase); + } +} + diff --git a/src/DevTool.Engine/Core/ScriptLocator.cs b/src/DevTool.Engine/Core/ScriptLocator.cs new file mode 100644 index 0000000..63fa6da --- /dev/null +++ b/src/DevTool.Engine/Core/ScriptLocator.cs @@ -0,0 +1,19 @@ +namespace Sdt.Core; + +public static class ScriptLocator +{ + public static string? FindHelperScript(string projectRoot, string scriptFileName) + { + // Packaged location: alongside executable in ./scripts + var bundled = Path.Combine(AppContext.BaseDirectory, "scripts", scriptFileName); + if (File.Exists(bundled)) + return bundled; + + // Source/project location fallback + var project = Path.Combine(projectRoot, "scripts", scriptFileName); + if (File.Exists(project)) + return project; + + return null; + } +} diff --git a/src/DevTool.Engine/Core/SetupStateService.cs b/src/DevTool.Engine/Core/SetupStateService.cs new file mode 100644 index 0000000..9de67f2 --- /dev/null +++ b/src/DevTool.Engine/Core/SetupStateService.cs @@ -0,0 +1,30 @@ +using System.Text.Json; + +namespace Sdt.Core; + +public static class SetupStateService +{ + private const string StateDirName = ".sdt"; + private const string StateFileName = "setup-state.json"; + + public static bool IsFirstRun(string projectRoot) + { + var statePath = GetStatePath(projectRoot); + return !File.Exists(statePath); + } + + public static void MarkCompleted(string projectRoot, string appVersion) + { + var statePath = GetStatePath(projectRoot); + Directory.CreateDirectory(Path.GetDirectoryName(statePath)!); + var payload = new + { + completedAt = DateTimeOffset.Now, + version = appVersion, + }; + File.WriteAllText(statePath, JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }) + Environment.NewLine); + } + + public static string GetStatePath(string projectRoot) + => Path.Combine(projectRoot, StateDirName, StateFileName); +} diff --git a/src/DevTool.Engine/Core/SetupWizardConfigService.cs b/src/DevTool.Engine/Core/SetupWizardConfigService.cs new file mode 100644 index 0000000..5bd42c4 --- /dev/null +++ b/src/DevTool.Engine/Core/SetupWizardConfigService.cs @@ -0,0 +1,228 @@ +using Sdt.Config; + +namespace Sdt.Core; + +public sealed record SetupConfigUpdateResult( + DevToolConfig Config, + IReadOnlyList Changes); + +public sealed class SetupWizardConfigService(IRequirementResolver? requirementResolver = null) +{ + private readonly IRequirementResolver _requirementResolver = requirementResolver ?? new RequirementResolver(); + + public SetupConfigUpdateResult ApplyRecommendedDefaults(DevToolConfig config) + { + var changes = new List(); + + var env = config.Env.ToList(); + if (!env.Any(e => string.Equals(e.Key, "SDT_ENV_PROFILE", StringComparison.OrdinalIgnoreCase))) + { + env.Add(new EnvVarDef + { + Key = "SDT_ENV_PROFILE", + Description = "Active SDT runtime environment profile", + DefaultValue = "dev", + Options = ["dev", "ci", "release"] + }); + changes.Add("Added env var definition: SDT_ENV_PROFILE."); + } + + if (!env.Any(e => string.Equals(e.Key, "SDT_LOG_LEVEL", StringComparison.OrdinalIgnoreCase))) + { + env.Add(new EnvVarDef + { + Key = "SDT_LOG_LEVEL", + Description = "CLI log verbosity", + DefaultValue = "information", + Options = ["trace", "debug", "information", "warning", "error", "critical"] + }); + changes.Add("Added env var definition: SDT_LOG_LEVEL."); + } + + var envProfiles = EnsureEnvProfiles(config.EnvProfiles, changes); + var tooling = EnsureTooling(config, changes); + var debug = EnsureDebugDiagnostics(config.Debug, changes); + + var updated = new DevToolConfig + { + Name = config.Name, + Version = config.Version, + Targets = config.Targets, + Workflows = config.Workflows, + Env = env, + EnvProfiles = envProfiles, + Toolchains = config.Toolchains, + Tooling = tooling, + Project = config.Project, + Debug = debug + }; + + return new SetupConfigUpdateResult(updated, changes); + } + + private ToolingConfig EnsureTooling(DevToolConfig config, List changes) + { + var existing = config.Tooling?.Tools ?? []; + var toolMap = existing.ToDictionary(t => t.Tool, StringComparer.OrdinalIgnoreCase); + var discovered = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (config.Toolchains?.Python is not null) + discovered.Add("python"); + if (config.Toolchains?.Node is not null) + { + discovered.Add("node"); + discovered.Add(config.Toolchains.Node.PackageManager); + } + + foreach (var workflow in config.Workflows) + { + foreach (var step in workflow.Steps) + { + foreach (var req in _requirementResolver.Resolve(step)) + discovered.Add(req.Tool); + } + } + + foreach (var profile in config.Debug?.Profiles ?? []) + { + foreach (var req in profile.Requires) + discovered.Add(req.Tool); + } + + foreach (var tool in discovered) + { + if (!toolMap.ContainsKey(tool)) + { + toolMap[tool] = new ToolInstallDefinition + { + Tool = tool, + PreferredInstallCommands = [], + Executables = [] + }; + changes.Add($"Added tooling entry for '{tool}'."); + } + } + + return new ToolingConfig + { + Tools = toolMap.Values.OrderBy(t => t.Tool, StringComparer.OrdinalIgnoreCase).ToList() + }; + } + + private static EnvProfilesConfig EnsureEnvProfiles(EnvProfilesConfig? input, List changes) + { + if (input is null || input.Profiles.Count == 0) + { + changes.Add("Added default env profiles: dev, ci, release."); + return DefaultEnvProfiles(); + } + + var profiles = input.Profiles.ToList(); + if (!profiles.Any(p => string.Equals(p.Id, "dev", StringComparison.OrdinalIgnoreCase))) + { + profiles.Add(DefaultEnvProfiles().Profiles.First(p => p.Id == "dev")); + changes.Add("Added missing env profile: dev."); + } + if (!profiles.Any(p => string.Equals(p.Id, "ci", StringComparison.OrdinalIgnoreCase))) + { + profiles.Add(DefaultEnvProfiles().Profiles.First(p => p.Id == "ci")); + changes.Add("Added missing env profile: ci."); + } + if (!profiles.Any(p => string.Equals(p.Id, "release", StringComparison.OrdinalIgnoreCase))) + { + profiles.Add(DefaultEnvProfiles().Profiles.First(p => p.Id == "release")); + changes.Add("Added missing env profile: release."); + } + + var active = string.IsNullOrWhiteSpace(input.Active) ? "dev" : input.Active; + return new EnvProfilesConfig + { + Active = active, + Profiles = profiles + }; + } + + private static DebugConfig EnsureDebugDiagnostics(DebugConfig? input, List changes) + { + if (input is null) + { + changes.Add("Added default debug diagnostics configuration."); + return new DebugConfig + { + Profiles = [], + Diagnostics = DefaultDiagnostics() + }; + } + + var diagnostics = input.Diagnostics; + var patterns = diagnostics.SensitiveKeyPatterns.Count == 0 + ? DefaultDiagnostics().SensitiveKeyPatterns + : diagnostics.SensitiveKeyPatterns; + if (diagnostics.SensitiveKeyPatterns.Count == 0) + changes.Add("Added default diagnostics sensitive key patterns."); + + var normalizedDiagnostics = new DebugDiagnosticsOptions + { + Enabled = diagnostics.Enabled, + OutputDir = diagnostics.OutputDir, + IncludeAllEnv = diagnostics.IncludeAllEnv, + CaptureEnvKeys = diagnostics.CaptureEnvKeys, + RedactSensitive = diagnostics.RedactSensitive, + SensitiveKeyPatterns = patterns, + RedactionAllowKeys = diagnostics.RedactionAllowKeys, + BundleOnFailure = diagnostics.BundleOnFailure + }; + + return new DebugConfig + { + Profiles = input.Profiles, + Diagnostics = normalizedDiagnostics + }; + } + + private static EnvProfilesConfig DefaultEnvProfiles() + { + return new EnvProfilesConfig + { + Active = "dev", + Profiles = + [ + new EnvProfileDefinition + { + Id = "dev", + Description = "Local development defaults", + Values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["SDT_ENV_PROFILE"] = "dev", + ["SDT_LOG_LEVEL"] = "information", + } + }, + new EnvProfileDefinition + { + Id = "ci", + Description = "Continuous integration defaults", + Inherits = ["dev"], + Values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["SDT_ENV_PROFILE"] = "ci", + ["CI"] = "true", + ["SDT_LOG_LEVEL"] = "warning", + } + }, + new EnvProfileDefinition + { + Id = "release", + Description = "Release build defaults", + Inherits = ["dev"], + Values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["SDT_ENV_PROFILE"] = "release", + ["SDT_LOG_LEVEL"] = "warning", + } + } + ] + }; + } + + private static DebugDiagnosticsOptions DefaultDiagnostics() => new(); +} diff --git a/src/DevTool.Engine/Core/ToolProbeService.cs b/src/DevTool.Engine/Core/ToolProbeService.cs new file mode 100644 index 0000000..b91f35b --- /dev/null +++ b/src/DevTool.Engine/Core/ToolProbeService.cs @@ -0,0 +1,141 @@ +using System.Diagnostics; +using System.Text.Json; +using Sdt.Config; + +namespace Sdt.Core; + +public sealed class ToolProbeService : IToolProbe +{ + public async Task ProbeAsync( + string tool, + string projectRoot, + DevToolConfig? config = null, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + { + var direct = await ProbeDirectAsync(tool, config, envOverrides, cancellationToken).ConfigureAwait(false); + if (direct.IsAvailable) + return direct; + + var scriptPath = ScriptLocator.FindHelperScript(projectRoot, "diag.py"); + if (scriptPath is null) + return direct; + + if (!(await ProbeDirectAsync("python", config, envOverrides, cancellationToken).ConfigureAwait(false)).IsAvailable) + return direct; + + try + { + var psi = new ProcessStartInfo + { + FileName = PythonResolver.ResolveExecutable(), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = projectRoot, + }; + if (envOverrides is not null) + { + foreach (var kvp in envOverrides) + psi.Environment[kvp.Key] = kvp.Value; + } + psi.ArgumentList.Add(scriptPath); + psi.ArgumentList.Add("probe"); + psi.ArgumentList.Add("--tool"); + psi.ArgumentList.Add(tool); + psi.ArgumentList.Add("--json"); + + using var process = new Process { StartInfo = psi }; + process.Start(); + var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + var stderr = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + if (process.ExitCode != 0) + return new ProbeResult(tool, false, Details: stderr.Trim()); + + var parsed = JsonSerializer.Deserialize(stdout); + if (parsed is null) + return new ProbeResult(tool, false, Details: "diag.py returned invalid JSON"); + + return new ProbeResult(parsed.Tool ?? tool, parsed.Available, parsed.Version, parsed.Details); + } + catch (Exception ex) + { + return new ProbeResult(tool, false, Details: ex.Message); + } + } + + private static async Task ProbeDirectAsync( + string tool, + DevToolConfig? config, + IReadOnlyDictionary? envOverrides, + CancellationToken cancellationToken) + { + var command = tool.ToLowerInvariant() switch + { + "python" => PythonResolver.ResolveExecutable(), + "dotnet" => "dotnet", + "node" => "node", + "npm" => "npm", + "cargo" => "cargo", + "tauri" => "tauri", + "git" => "git", + "docker" => "docker", + "go" => "go", + "maven" => "mvn", + "gradle" => "gradle", + "java" => "java", + _ => tool, + }; + var resolution = CommandResolver.ResolveWithTrace(command, config, tool); + command = resolution.Resolved; + + var versionArg = command is "python" ? "--version" : "--version"; + try + { + var psi = new ProcessStartInfo + { + FileName = command, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + if (envOverrides is not null) + { + foreach (var kvp in envOverrides) + psi.Environment[kvp.Key] = kvp.Value; + } + psi.ArgumentList.Add(versionArg); + + using var process = new Process { StartInfo = psi }; + process.Start(); + var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + var stderr = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + + if (process.ExitCode != 0) + { + var failDetails = string.IsNullOrWhiteSpace(stderr) ? stdout.Trim() : stderr.Trim(); + var trace = $"{resolution.Source}: {resolution.Resolved}"; + return new ProbeResult(tool, false, Details: string.IsNullOrWhiteSpace(failDetails) ? trace : $"{trace} | {failDetails}"); + } + + var version = string.IsNullOrWhiteSpace(stdout) ? stderr.Trim() : stdout.Trim(); + return new ProbeResult(tool, true, Version: version, Details: $"{resolution.Source}: {resolution.Resolved}"); + } + catch (Exception ex) + { + return new ProbeResult(tool, false, Details: $"{resolution.Source}: {resolution.Resolved} | {ex.Message}"); + } + } + + private sealed class DiagProbeJson + { + public string? Tool { get; init; } + public bool Available { get; init; } + public string? Version { get; init; } + public string? Details { get; init; } + } +} diff --git a/src/DevTool.Engine/Core/ToolchainManagerService.cs b/src/DevTool.Engine/Core/ToolchainManagerService.cs new file mode 100644 index 0000000..ec0427d --- /dev/null +++ b/src/DevTool.Engine/Core/ToolchainManagerService.cs @@ -0,0 +1,123 @@ +using Sdt.Config; + +namespace Sdt.Core; + +public sealed record ToolchainProbeSummary( + string Tool, + bool IsAvailable, + string? Version, + string? Details); + +public sealed record ToolchainAutoFixResult( + string Tool, + bool Success, + string Message); + +public sealed class ToolchainManagerService( + IToolProbe? toolProbe = null, + IPrereqInstaller? installer = null) +{ + private readonly IToolProbe _toolProbe = toolProbe ?? new ToolProbeService(); + private readonly IPrereqInstaller _installer = installer ?? new PrereqInstallerService(); + + public async Task> ProbeConfiguredToolsAsync( + DevToolConfig config, + string projectRoot, + CancellationToken cancellationToken = default) + { + var tools = CollectConfiguredTools(config); + var results = new List(); + foreach (var tool in tools) + { + var probe = await _toolProbe.ProbeAsync(tool, projectRoot, config, null, cancellationToken).ConfigureAwait(false); + results.Add(new ToolchainProbeSummary(tool, probe.IsAvailable, probe.Version, probe.Details)); + } + + return results; + } + + public async Task> AutoFixMissingToolsAsync( + DevToolConfig config, + string projectRoot, + Func> confirmInstallAsync, + Action onOutput, + Action? onEvent = null, + CancellationToken cancellationToken = default) + { + var probeResults = await ProbeConfiguredToolsAsync(config, projectRoot, cancellationToken).ConfigureAwait(false); + var missing = probeResults.Where(p => !p.IsAvailable).Select(p => p.Tool).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + var results = new List(); + + foreach (var tool in missing) + { + onEvent?.Invoke(new RunEvent("toolchain", RunEventType.ProbeFailed, $"Tool '{tool}' is missing.", Tool: tool, Success: false)); + var plan = await _installer.GetInstallPlanAsync(tool, projectRoot, config, cancellationToken).ConfigureAwait(false); + onEvent?.Invoke(new RunEvent("toolchain", RunEventType.InstallPlanPrepared, plan.Summary, Tool: tool, Success: plan.Supported)); + if (!plan.Supported || plan.Commands.Count == 0) + { + results.Add(new ToolchainAutoFixResult(tool, false, "No installer plan available.")); + continue; + } + + var approved = await confirmInstallAsync(tool, plan).ConfigureAwait(false); + if (!approved) + { + results.Add(new ToolchainAutoFixResult(tool, false, "Install declined.")); + continue; + } + + var allSucceeded = true; + foreach (var cmd in plan.Commands) + { + onEvent?.Invoke(new RunEvent("toolchain", RunEventType.InstallCommandStarted, $"{cmd.Command} {string.Join(" ", cmd.Args)}", Tool: tool)); + var run = await _installer.RunInstallAsync(cmd, projectRoot, onOutput, null, cancellationToken).ConfigureAwait(false); + onEvent?.Invoke(new RunEvent("toolchain", RunEventType.InstallCommandCompleted, $"Install command exited {run.ExitCode}.", Tool: tool, Success: run.Success, ExitCode: run.ExitCode)); + if (!run.Success) + { + allSucceeded = false; + break; + } + } + + var verify = await _toolProbe.ProbeAsync(tool, projectRoot, config, null, cancellationToken).ConfigureAwait(false); + if (allSucceeded && verify.IsAvailable) + results.Add(new ToolchainAutoFixResult(tool, true, "Installed and verified.")); + else + results.Add(new ToolchainAutoFixResult(tool, false, "Install attempted but verification failed.")); + } + + return results; + } + + private static IReadOnlyList CollectConfiguredTools(DevToolConfig config) + { + var tools = new HashSet(StringComparer.OrdinalIgnoreCase); + if (config.Toolchains?.Python is not null) + tools.Add("python"); + if (config.Toolchains?.Node is not null) + { + tools.Add("node"); + tools.Add(config.Toolchains.Node.PackageManager); + } + + foreach (var tool in config.Tooling?.Tools ?? []) + tools.Add(tool.Tool); + + foreach (var workflow in config.Workflows) + { + foreach (var step in workflow.Steps) + { + foreach (var req in step.Requires) + tools.Add(req.Tool); + } + } + + foreach (var profile in config.Debug?.Profiles ?? []) + { + foreach (var req in profile.Requires) + tools.Add(req.Tool); + } + + return tools.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(); + } +} diff --git a/src/DevTool.Engine/Core/WorkflowExecutor.cs b/src/DevTool.Engine/Core/WorkflowExecutor.cs new file mode 100644 index 0000000..cbf35ae --- /dev/null +++ b/src/DevTool.Engine/Core/WorkflowExecutor.cs @@ -0,0 +1,276 @@ +using Sdt.Config; + +namespace Sdt.Core; + +public sealed class WorkflowExecutor( + IWorkflowPlanner planner, + IToolProbe toolProbe, + IPrereqInstaller installer, + IActionRunner actionRunner, + IRequirementResolver requirementResolver) +{ + private readonly IWorkflowPlanner _planner = planner; + private readonly IToolProbe _toolProbe = toolProbe; + private readonly IPrereqInstaller _installer = installer; + private readonly IActionRunner _actionRunner = actionRunner; + private readonly IRequirementResolver _requirementResolver = requirementResolver; + + public async Task ExecuteAsync( + WorkflowDefinition rootWorkflow, + IReadOnlyDictionary allWorkflows, + DevToolConfig config, + string projectRoot, + Func> confirmInstallAsync, + Action onOutput, + Action? onEvent = null, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + { + var results = new List(); + var plan = _planner.ResolvePlan(rootWorkflow, allWorkflows); + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.WorkflowStarted, + Message: $"Workflow '{rootWorkflow.Id}' started.", + WorkflowId: rootWorkflow.Id)); + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.WorkflowPlanned, + Message: $"Execution plan contains {plan.Count} workflow(s).", + WorkflowId: rootWorkflow.Id)); + + if (plan.Count == 0) + { + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.WorkflowCompleted, + Message: "No executable workflow steps were found.", + WorkflowId: rootWorkflow.Id, + Success: false)); + return new WorkflowExecutionResult( + Success: false, + StopReason: ExecutionStopReason.ValidationFailed, + Message: "This workflow has no executable steps.", + Steps: results); + } + + foreach (var workflow in plan) + { + foreach (var step in workflow.Steps) + { + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.WorkflowStepStarted, + Message: $"Step '{step.Label}' started.", + WorkflowId: workflow.Id, + StepId: step.Id)); + + var requires = _requirementResolver.Resolve(step); + foreach (var req in requires) + { + var probe = await _toolProbe.ProbeAsync(req.Tool, projectRoot, config, envOverrides, cancellationToken).ConfigureAwait(false); + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.ProbeChecked, + Message: probe.IsAvailable + ? $"Tool '{req.Tool}' is available." + : $"Tool '{req.Tool}' is missing.", + WorkflowId: workflow.Id, + StepId: step.Id, + Tool: req.Tool, + Success: probe.IsAvailable)); + if (probe.IsAvailable) + continue; + + if (!string.IsNullOrWhiteSpace(probe.Details)) + { + onOutput($"Probe detail [{req.Tool}]: {probe.Details}", false); + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.ProbeFailed, + Message: probe.Details, + WorkflowId: workflow.Id, + StepId: step.Id, + Tool: req.Tool, + Success: false)); + } + + if (req.InstallPolicy == InstallPolicy.Never) + { + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.WorkflowCompleted, + Message: $"Missing prerequisite '{req.Tool}'.", + WorkflowId: rootWorkflow.Id, + Tool: req.Tool, + Success: false)); + return new WorkflowExecutionResult( + Success: false, + StopReason: ExecutionStopReason.MissingPrereq, + Message: $"Missing prerequisite '{req.Tool}' for step '{step.Label}'.", + Steps: results); + } + + var installPlan = await _installer.GetInstallPlanAsync( + req.Tool, + projectRoot, + config, + cancellationToken).ConfigureAwait(false); + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.InstallPlanPrepared, + Message: installPlan.Summary, + WorkflowId: workflow.Id, + StepId: step.Id, + Tool: req.Tool, + Success: installPlan.Supported)); + if (!installPlan.Supported || installPlan.Commands.Count == 0) + { + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.WorkflowCompleted, + Message: $"No installer plan available for '{req.Tool}'.", + WorkflowId: rootWorkflow.Id, + Tool: req.Tool, + Success: false)); + return new WorkflowExecutionResult( + Success: false, + StopReason: ExecutionStopReason.MissingPrereq, + Message: $"Missing prerequisite '{req.Tool}' and no installer plan is available.", + Steps: results); + } + + var approved = req.InstallPolicy == InstallPolicy.Auto + ? true + : await confirmInstallAsync(req.Tool, installPlan).ConfigureAwait(false); + + if (!approved) + { + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.InstallDeclined, + Message: $"Install declined for '{req.Tool}'.", + WorkflowId: workflow.Id, + StepId: step.Id, + Tool: req.Tool, + Success: false)); + return new WorkflowExecutionResult( + Success: false, + StopReason: ExecutionStopReason.UserDeclined, + Message: $"Install declined for missing prerequisite '{req.Tool}'.", + Steps: results); + } + + foreach (var installCommand in installPlan.Commands) + { + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.InstallCommandStarted, + Message: $"{installCommand.Command} {string.Join(" ", installCommand.Args)}", + WorkflowId: workflow.Id, + StepId: step.Id, + Tool: req.Tool)); + + var installResult = await _installer.RunInstallAsync( + installCommand, + projectRoot, + onOutput, + envOverrides, + cancellationToken).ConfigureAwait(false); + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.InstallCommandCompleted, + Message: $"Install command exited {installResult.ExitCode}.", + WorkflowId: workflow.Id, + StepId: step.Id, + Tool: req.Tool, + Success: installResult.Success, + ExitCode: installResult.ExitCode)); + + if (!installResult.Success) + { + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.WorkflowCompleted, + Message: $"Install failed for '{req.Tool}'.", + WorkflowId: rootWorkflow.Id, + Tool: req.Tool, + Success: false)); + return new WorkflowExecutionResult( + Success: false, + StopReason: ExecutionStopReason.InstallFailed, + Message: $"Failed to install prerequisite '{req.Tool}'.", + Steps: results); + } + } + + var verifyProbe = await _toolProbe.ProbeAsync(req.Tool, projectRoot, config, envOverrides, cancellationToken).ConfigureAwait(false); + if (!verifyProbe.IsAvailable) + { + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.WorkflowCompleted, + Message: $"Tool '{req.Tool}' still missing after install.", + WorkflowId: rootWorkflow.Id, + Tool: req.Tool, + Success: false)); + return new WorkflowExecutionResult( + Success: false, + StopReason: ExecutionStopReason.InstallFailed, + Message: $"Prerequisite '{req.Tool}' is still missing after install attempt.", + Steps: results); + } + } + + var runResult = await _actionRunner.RunStepAsync( + step, + projectRoot, + onOutput, + envOverrides, + cancellationToken).ConfigureAwait(false); + + results.Add(new WorkflowStepResult( + workflow.Id, + step.Id, + string.IsNullOrWhiteSpace(step.Label) ? step.Id : step.Label, + runResult)); + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.WorkflowStepCompleted, + Message: $"Step '{step.Label}' exited {runResult.ExitCode}.", + WorkflowId: workflow.Id, + StepId: step.Id, + Success: runResult.Success, + ExitCode: runResult.ExitCode)); + + if (!runResult.Success) + { + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.WorkflowCompleted, + Message: $"Step '{step.Label}' failed.", + WorkflowId: rootWorkflow.Id, + Success: false, + ExitCode: runResult.ExitCode)); + return new WorkflowExecutionResult( + Success: false, + StopReason: ExecutionStopReason.CommandFailed, + Message: $"Step '{step.Label}' failed with exit code {runResult.ExitCode}.", + Steps: results); + } + } + } + + onEvent?.Invoke(new RunEvent( + Category: "workflow", + Type: RunEventType.WorkflowCompleted, + Message: "Workflow completed successfully.", + WorkflowId: rootWorkflow.Id, + Success: true)); + return new WorkflowExecutionResult( + Success: true, + StopReason: null, + Message: "Workflow completed successfully.", + Steps: results); + } +} diff --git a/src/DevTool.Engine/Core/WorkflowPlanner.cs b/src/DevTool.Engine/Core/WorkflowPlanner.cs new file mode 100644 index 0000000..d677df6 --- /dev/null +++ b/src/DevTool.Engine/Core/WorkflowPlanner.cs @@ -0,0 +1,35 @@ +using Sdt.Config; + +namespace Sdt.Core; + +public sealed class WorkflowPlanner : IWorkflowPlanner +{ + public List ResolvePlan( + WorkflowDefinition workflow, + IReadOnlyDictionary allWorkflows) + { + var visited = new HashSet(StringComparer.OrdinalIgnoreCase); + var plan = new List(); + Visit(workflow, allWorkflows, visited, plan); + return plan; + } + + private static void Visit( + WorkflowDefinition workflow, + IReadOnlyDictionary allWorkflows, + HashSet visited, + List plan) + { + if (!visited.Add(workflow.Id)) + return; + + foreach (var depId in workflow.DependsOn) + { + if (allWorkflows.TryGetValue(depId, out var dep)) + Visit(dep, allWorkflows, visited, plan); + } + + if (workflow.Steps.Count > 0) + plan.Add(workflow); + } +} diff --git a/src/DevTool.Engine/DevTool.Engine.csproj b/src/DevTool.Engine/DevTool.Engine.csproj new file mode 100644 index 0000000..67e8508 --- /dev/null +++ b/src/DevTool.Engine/DevTool.Engine.csproj @@ -0,0 +1,12 @@ + + + net10.0 + enable + enable + Sdt + + + + + + diff --git a/src/DevTool.Engine/Runner/TargetRunner.cs b/src/DevTool.Engine/Runner/TargetRunner.cs new file mode 100644 index 0000000..b4c670e --- /dev/null +++ b/src/DevTool.Engine/Runner/TargetRunner.cs @@ -0,0 +1,41 @@ +using Sdt.Config; + +namespace Sdt.Runner; + +public static class TargetRunner +{ + /// + /// Returns the ordered list of real (non-virtual) steps needed to execute , + /// respecting DependsOn chains. Each step appears at most once. + /// + public static List ResolvePlan( + BuildTarget target, + IReadOnlyDictionary allTargets) + { + var visited = new HashSet(StringComparer.OrdinalIgnoreCase); + var plan = new List(); + Visit(target, allTargets, visited, plan); + return plan; + } + + private static void Visit( + BuildTarget target, + IReadOnlyDictionary allTargets, + HashSet visited, + List plan) + { + if (!visited.Add(target.Id)) + return; + + // Recurse into dependencies first (topological order) + foreach (var depId in target.DependsOn) + { + if (allTargets.TryGetValue(depId, out var dep)) + Visit(dep, allTargets, visited, plan); + } + + // Virtual aggregator targets (null Command) are just dependency collectors + if (target.Command is not null) + plan.Add(target); + } +} diff --git a/src/DevTool.Host.Gui/README.md b/src/DevTool.Host.Gui/README.md new file mode 100644 index 0000000..b89b864 --- /dev/null +++ b/src/DevTool.Host.Gui/README.md @@ -0,0 +1,14 @@ +# GUI Workspace + +This directory contains SDT GUI front-end implementations. + +Current direction: + +- `TauriShell/` is the active GUI path for v1.x. +- TUI and GUI must both consume the same headless SDT contracts: + - `sdt workspace scan --json` + - `sdt run --json` + - `sdt debug --json` + - `.sdt/events/*.jsonl` run-event stream + +Do not put orchestration logic in GUI code. Keep execution logic in core/headless services. diff --git a/src/DevTool.Host.Gui/TauriShell/.gitignore b/src/DevTool.Host.Gui/TauriShell/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/src/DevTool.Host.Gui/TauriShell/README.md b/src/DevTool.Host.Gui/TauriShell/README.md new file mode 100644 index 0000000..33119c3 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/README.md @@ -0,0 +1,31 @@ +# SDT Tauri Shell + +This is the first real GUI shell scaffold for SDT. + +## Implemented Bridge + +- Frontend button calls Tauri command: `workspace_scan` +- Rust command runs: + - `sdt workspace scan --json` + - fallback: `sdt.exe workspace scan --json` (Windows) + - fallback: `dotnet run --project DevTool.csproj -- workspace scan --json` +- JSON output is rendered in the GUI for inspection. +- Frontend workflow runner calls Tauri command: `run_workflow` +- Rust command runs: + - `sdt run --json` + - fallback: `sdt.exe run --json` (Windows) + - fallback: `dotnet run --project DevTool.csproj -- run --json` +- Live run output is streamed into the event panel via Tauri events (`run_stream_line`, `run_stream_status`). + +## Run + +```powershell +cd src/DevTool.Host.Gui/TauriShell +npm install +npm run tauri dev +``` + +## Scope + +- This shell is a thin UI over SDT headless contracts. +- Orchestration logic remains in core/headless layers, not GUI code. diff --git a/src/DevTool.Host.Gui/TauriShell/index.html b/src/DevTool.Host.Gui/TauriShell/index.html new file mode 100644 index 0000000..07ec7d6 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/index.html @@ -0,0 +1,65 @@ + + + + + + + SDT Tauri Shell + + + + +
+
+

SDT GUI Shell

+

First bridge command: sdt workspace scan --json

+
+ +
+ +
+ + +
+

Ready.

+

+
+ +
+

Scan Output

+

+      
+ +
+

Workflow Run Bridge

+
+ + + + + + + + + + +
+

Ready.

+

+
+ +
+

Live Event Stream

+

+      
+
+ + diff --git a/src/DevTool.Host.Gui/TauriShell/package-lock.json b/src/DevTool.Host.Gui/TauriShell/package-lock.json new file mode 100644 index 0000000..b4b8cab --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/package-lock.json @@ -0,0 +1,1361 @@ +{ + "name": "sdt-tauri-shell", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sdt-tauri-shell", + "version": "0.1.0", + "dependencies": { + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-opener": "^2" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "typescript": "~5.6.2", + "vite": "^6.0.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.0.tgz", + "integrity": "sha512-ZwT0T+7bw4+DPCSWzmviwq5XbXlM0cNoleDKOYPFYqcZqeKY31KlpoMW/MOON/tOFBPgi31a2v3w9gliqwL2+Q==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.10.0", + "@tauri-apps/cli-darwin-x64": "2.10.0", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.0", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.0", + "@tauri-apps/cli-linux-arm64-musl": "2.10.0", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.0", + "@tauri-apps/cli-linux-x64-gnu": "2.10.0", + "@tauri-apps/cli-linux-x64-musl": "2.10.0", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.0", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.0", + "@tauri-apps/cli-win32-x64-msvc": "2.10.0" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.0.tgz", + "integrity": "sha512-avqHD4HRjrMamE/7R/kzJPcAJnZs0IIS+1nkDP5b+TNBn3py7N2aIo9LIpy+VQq0AkN8G5dDpZtOOBkmWt/zjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.0.tgz", + "integrity": "sha512-keDmlvJRStzVFjZTd0xYkBONLtgBC9eMTpmXnBXzsHuawV2q9PvDo2x6D5mhuoMVrJ9QWjgaPKBBCFks4dK71Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.0.tgz", + "integrity": "sha512-e5u0VfLZsMAC9iHaOEANumgl6lfnJx0Dtjkd8IJpysZ8jp0tJ6wrIkto2OzQgzcYyRCKgX72aKE0PFgZputA8g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.0.tgz", + "integrity": "sha512-YrYYk2dfmBs5m+OIMCrb+JH/oo+4FtlpcrTCgiFYc7vcs6m3QDd1TTyWu0u01ewsCtK2kOdluhr/zKku+KP7HA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.0.tgz", + "integrity": "sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.0.tgz", + "integrity": "sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.0.tgz", + "integrity": "sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.0.tgz", + "integrity": "sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.0.tgz", + "integrity": "sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.0.tgz", + "integrity": "sha512-EHyQ1iwrWy1CwMalEm9z2a6L5isQ121pe7FcA2xe4VWMJp+GHSDDGvbTv/OPdkt2Lyr7DAZBpZHM6nvlHXEc4A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.0.tgz", + "integrity": "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz", + "integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/src/DevTool.Host.Gui/TauriShell/package.json b/src/DevTool.Host.Gui/TauriShell/package.json new file mode 100644 index 0000000..0addde3 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/package.json @@ -0,0 +1,21 @@ +{ + "name": "sdt-tauri-shell", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-opener": "^2" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "vite": "^6.0.3", + "typescript": "~5.6.2" + } +} diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/.gitignore b/src/DevTool.Host.Gui/TauriShell/src-tauri/.gitignore new file mode 100644 index 0000000..b21bd68 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src-tauri/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/Cargo.lock b/src/DevTool.Host.Gui/TauriShell/src-tauri/Cargo.lock new file mode 100644 index 0000000..18b8b8e --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src-tauri/Cargo.lock @@ -0,0 +1,5305 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.11.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.12+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.13.0", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +dependencies = [ + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-javascript-core", + "objc2-security", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.13.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdt_tauri_shell" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-opener", +] + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +dependencies = [ + "bitflags 2.11.0", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692a77abd8b8773e107a42ec0e05b767b8d2b7ece76ab36c6c3947e34df9f53f" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-runtime" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wry" +version = "0.54.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.14", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.14", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.14", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 0.7.14", +] diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/Cargo.toml b/src/DevTool.Host.Gui/TauriShell/src-tauri/Cargo.toml new file mode 100644 index 0000000..52dba06 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src-tauri/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "sdt_tauri_shell" +version = "0.1.0" +description = "SDT Tauri GUI Shell" +authors = ["you"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "sdt_tauri_shell_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-opener = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/build.rs b/src/DevTool.Host.Gui/TauriShell/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/capabilities/default.json b/src/DevTool.Host.Gui/TauriShell/src-tauri/capabilities/default.json new file mode 100644 index 0000000..4cdbf49 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src-tauri/capabilities/default.json @@ -0,0 +1,10 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": [ + "core:default", + "opener:default" + ] +} diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/128x128.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/128x128.png new file mode 100644 index 0000000..6be5e50 Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/128x128.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/128x128@2x.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..e81bece Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/128x128@2x.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/32x32.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/32x32.png new file mode 100644 index 0000000..a437dd5 Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/32x32.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square107x107Logo.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..0ca4f27 Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square107x107Logo.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square142x142Logo.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..b81f820 Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square142x142Logo.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square150x150Logo.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..624c7bf Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square150x150Logo.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square284x284Logo.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..c021d2b Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square284x284Logo.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square30x30Logo.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..6219700 Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square30x30Logo.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square310x310Logo.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..f9bc048 Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square310x310Logo.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square44x44Logo.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..d5fbfb2 Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square44x44Logo.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square71x71Logo.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..63440d7 Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square71x71Logo.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square89x89Logo.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..f3f705a Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/Square89x89Logo.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/StoreLogo.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..4556388 Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/StoreLogo.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/icon.icns b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/icon.icns new file mode 100644 index 0000000..12a5bce Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/icon.icns differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/icon.ico b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/icon.ico new file mode 100644 index 0000000..b3636e4 Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/icon.ico differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/icon.png b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/icon.png new file mode 100644 index 0000000..e1cd261 Binary files /dev/null and b/src/DevTool.Host.Gui/TauriShell/src-tauri/icons/icon.png differ diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/src/domain/mod.rs b/src/DevTool.Host.Gui/TauriShell/src-tauri/src/domain/mod.rs new file mode 100644 index 0000000..fa5f1e4 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src-tauri/src/domain/mod.rs @@ -0,0 +1 @@ +pub mod sdt_bridge; diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/src/domain/sdt_bridge.rs b/src/DevTool.Host.Gui/TauriShell/src-tauri/src/domain/sdt_bridge.rs new file mode 100644 index 0000000..9b0292c --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src-tauri/src/domain/sdt_bridge.rs @@ -0,0 +1,314 @@ +use serde::Serialize; +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::sync::{Arc, Mutex}; +use std::thread; +use tauri::{AppHandle, Emitter}; + +#[derive(Clone)] +pub struct CommandInvocation { + pub program: String, + pub args: Vec, + pub working_dir: PathBuf, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandExecutionPayload { + pub command: String, + pub working_directory: String, + pub stdout: String, + pub stderr: String, + pub exit_code: i32, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RunStreamLineEvent { + pub session_id: String, + pub stream: String, + pub line: String, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RunStreamStatusEvent { + pub session_id: String, + pub state: String, + pub message: String, + pub exit_code: Option, +} + +pub fn find_repo_root(start: &Path) -> Option { + let mut current = Some(start); + while let Some(dir) = current { + if dir.join("DevTool.csproj").exists() { + return Some(dir.to_path_buf()); + } + current = dir.parent(); + } + None +} + +pub fn command_line(program: &str, args: &[String]) -> String { + let mut line = String::from(program); + for arg in args { + if arg.contains(' ') { + line.push_str(" \""); + line.push_str(arg); + line.push('"'); + } else { + line.push(' '); + line.push_str(arg); + } + } + line +} + +pub fn build_workspace_scan_attempts(selected_root: &str, repo_root: &Path) -> Vec { + let mut attempts: Vec = Vec::new(); + + attempts.push(CommandInvocation { + program: String::from("sdt"), + args: vec![ + String::from("workspace"), + String::from("scan"), + String::from("--json"), + String::from("--project-root"), + String::from(selected_root), + ], + working_dir: repo_root.to_path_buf(), + }); + + if cfg!(windows) { + attempts.push(CommandInvocation { + program: String::from("sdt.exe"), + args: vec![ + String::from("workspace"), + String::from("scan"), + String::from("--json"), + String::from("--project-root"), + String::from(selected_root), + ], + working_dir: repo_root.to_path_buf(), + }); + } + + let devtool_project = repo_root.join("DevTool.csproj"); + if devtool_project.exists() { + attempts.push(CommandInvocation { + program: String::from("dotnet"), + args: vec![ + String::from("run"), + String::from("--project"), + devtool_project.to_string_lossy().to_string(), + String::from("--"), + String::from("workspace"), + String::from("scan"), + String::from("--json"), + String::from("--project-root"), + String::from(selected_root), + ], + working_dir: repo_root.to_path_buf(), + }); + } + + attempts +} + +pub fn build_workflow_run_attempts( + workflow_id: &str, + selected_root: &str, + env_profile: Option<&str>, + repo_root: &Path, +) -> Vec { + let mut run_args = vec![ + String::from("run"), + String::from(workflow_id), + String::from("--json"), + String::from("--project-root"), + String::from(selected_root), + ]; + if let Some(profile) = env_profile { + if !profile.trim().is_empty() { + run_args.push(String::from("--env-profile")); + run_args.push(String::from(profile)); + } + } + + let mut attempts: Vec = Vec::new(); + attempts.push(CommandInvocation { + program: String::from("sdt"), + args: run_args.clone(), + working_dir: repo_root.to_path_buf(), + }); + + if cfg!(windows) { + attempts.push(CommandInvocation { + program: String::from("sdt.exe"), + args: run_args.clone(), + working_dir: repo_root.to_path_buf(), + }); + } + + let devtool_project = repo_root.join("DevTool.csproj"); + if devtool_project.exists() { + let mut dotnet_args = vec![ + String::from("run"), + String::from("--project"), + devtool_project.to_string_lossy().to_string(), + String::from("--"), + ]; + dotnet_args.extend(run_args); + attempts.push(CommandInvocation { + program: String::from("dotnet"), + args: dotnet_args, + working_dir: repo_root.to_path_buf(), + }); + } + + attempts +} + +pub fn run_attempt_to_completion(invocation: &CommandInvocation) -> Result { + let output = Command::new(&invocation.program) + .args(&invocation.args) + .current_dir(&invocation.working_dir) + .output() + .map_err(|err| { + format!( + "{} (cwd: {}): {}", + command_line(&invocation.program, &invocation.args), + invocation.working_dir.display(), + err + ) + })?; + + let code = output.status.code().unwrap_or(-1); + Ok(CommandExecutionPayload { + command: command_line(&invocation.program, &invocation.args), + working_directory: invocation.working_dir.to_string_lossy().to_string(), + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + exit_code: code, + }) +} + +pub fn spawn_streaming(invocation: &CommandInvocation) -> std::io::Result { + Command::new(&invocation.program) + .args(&invocation.args) + .current_dir(&invocation.working_dir) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() +} + +pub fn stream_child_output( + app: &AppHandle, + session_id: &str, + invocation: &CommandInvocation, + mut child: Child, +) -> Result { + let stdout = child + .stdout + .take() + .ok_or_else(|| String::from("failed to capture child stdout"))?; + let stderr = child + .stderr + .take() + .ok_or_else(|| String::from("failed to capture child stderr"))?; + + let stdout_acc = Arc::new(Mutex::new(String::new())); + let stderr_acc = Arc::new(Mutex::new(String::new())); + + let app_out = app.clone(); + let session_out = String::from(session_id); + let stdout_acc_out = Arc::clone(&stdout_acc); + let out_thread = thread::spawn(move || { + let reader = BufReader::new(stdout); + for line in reader.lines().map_while(Result::ok) { + if let Ok(mut acc) = stdout_acc_out.lock() { + acc.push_str(&line); + acc.push('\n'); + } + let _ = app_out.emit( + "run_stream_line", + RunStreamLineEvent { + session_id: session_out.clone(), + stream: String::from("stdout"), + line, + }, + ); + } + }); + + let app_err = app.clone(); + let session_err = String::from(session_id); + let stderr_acc_err = Arc::clone(&stderr_acc); + let err_thread = thread::spawn(move || { + let reader = BufReader::new(stderr); + for line in reader.lines().map_while(Result::ok) { + if let Ok(mut acc) = stderr_acc_err.lock() { + acc.push_str(&line); + acc.push('\n'); + } + let _ = app_err.emit( + "run_stream_line", + RunStreamLineEvent { + session_id: session_err.clone(), + stream: String::from("stderr"), + line, + }, + ); + } + }); + + let _ = app.emit( + "run_stream_status", + RunStreamStatusEvent { + session_id: String::from(session_id), + state: String::from("started"), + message: format!( + "{} (cwd: {})", + command_line(&invocation.program, &invocation.args), + invocation.working_dir.display() + ), + exit_code: None, + }, + ); + + let status = child + .wait() + .map_err(|err| format!("failed waiting for workflow process: {err}"))?; + let _ = out_thread.join(); + let _ = err_thread.join(); + let exit_code = status.code().unwrap_or(-1); + + let _ = app.emit( + "run_stream_status", + RunStreamStatusEvent { + session_id: String::from(session_id), + state: String::from("completed"), + message: String::from("Workflow command finished."), + exit_code: Some(exit_code), + }, + ); + + let stdout = stdout_acc + .lock() + .map(|v| v.clone()) + .unwrap_or_else(|_| String::new()); + let stderr = stderr_acc + .lock() + .map(|v| v.clone()) + .unwrap_or_else(|_| String::new()); + + Ok(CommandExecutionPayload { + command: command_line(&invocation.program, &invocation.args), + working_directory: invocation.working_dir.to_string_lossy().to_string(), + stdout, + stderr, + exit_code, + }) +} diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/src/lib.rs b/src/DevTool.Host.Gui/TauriShell/src-tauri/src/lib.rs new file mode 100644 index 0000000..ed3b417 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src-tauri/src/lib.rs @@ -0,0 +1,104 @@ +mod domain; + +use domain::sdt_bridge::{ + build_workflow_run_attempts, build_workspace_scan_attempts, find_repo_root, + run_attempt_to_completion, spawn_streaming, stream_child_output, CommandExecutionPayload, +}; +use serde::Deserialize; +use std::io::ErrorKind; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct WorkflowRunRequest { + workflow_id: String, + project_root: Option, + env_profile: Option, + session_id: String, +} + +#[tauri::command] +fn workspace_scan(project_root: Option) -> Result { + let cwd = std::env::current_dir().map_err(|err| format!("failed to get current dir: {err}"))?; + let repo_root = find_repo_root(&cwd).unwrap_or(cwd.clone()); + let selected_root = project_root.unwrap_or_else(|| repo_root.to_string_lossy().to_string()); + + let attempts = build_workspace_scan_attempts(&selected_root, &repo_root); + let mut errors: Vec = Vec::new(); + + for invocation in attempts { + match run_attempt_to_completion(&invocation) { + Ok(payload) if payload.exit_code == 0 => return Ok(payload), + Ok(payload) => errors.push(format!( + "{} (cwd: {}) exited {}\nstdout:\n{}\nstderr:\n{}", + payload.command, + payload.working_directory, + payload.exit_code, + payload.stdout, + payload.stderr + )), + Err(err) => errors.push(err), + } + } + + Err(format!( + "Unable to run workspace scan via sdt command bridge.\n{}", + errors.join("\n\n") + )) +} + +#[tauri::command] +fn run_workflow( + app: tauri::AppHandle, + request: WorkflowRunRequest, +) -> Result { + let cwd = std::env::current_dir().map_err(|err| format!("failed to get current dir: {err}"))?; + let repo_root = find_repo_root(&cwd).unwrap_or(cwd.clone()); + let selected_root = request + .project_root + .clone() + .unwrap_or_else(|| repo_root.to_string_lossy().to_string()); + + let attempts = build_workflow_run_attempts( + &request.workflow_id, + &selected_root, + request.env_profile.as_deref(), + &repo_root, + ); + + let mut spawn_errors: Vec = Vec::new(); + for invocation in attempts { + match spawn_streaming(&invocation) { + Ok(child) => return stream_child_output(&app, &request.session_id, &invocation, child), + Err(err) if err.kind() == ErrorKind::NotFound => { + spawn_errors.push(format!( + "{} (cwd: {}): {}", + invocation.program, + invocation.working_dir.display(), + err + )); + } + Err(err) => { + return Err(format!( + "{} (cwd: {}): {}", + invocation.program, + invocation.working_dir.display(), + err + )); + } + } + } + + Err(format!( + "Unable to start workflow command bridge. No runnable command found.\n{}", + spawn_errors.join("\n") + )) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .invoke_handler(tauri::generate_handler![workspace_scan, run_workflow]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/src/main.rs b/src/DevTool.Host.Gui/TauriShell/src-tauri/src/main.rs new file mode 100644 index 0000000..09e46c9 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + sdt_tauri_shell_lib::run() +} diff --git a/src/DevTool.Host.Gui/TauriShell/src-tauri/tauri.conf.json b/src/DevTool.Host.Gui/TauriShell/src-tauri/tauri.conf.json new file mode 100644 index 0000000..759ea62 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src-tauri/tauri.conf.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "sdt-tauri-shell", + "version": "0.1.0", + "identifier": "com.rosea.sdttaurishell", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "SDT Tauri Shell", + "width": 800, + "height": 600 + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/src/DevTool.Host.Gui/TauriShell/src/assets/tauri.svg b/src/DevTool.Host.Gui/TauriShell/src/assets/tauri.svg new file mode 100644 index 0000000..31b62c9 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src/assets/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/DevTool.Host.Gui/TauriShell/src/assets/typescript.svg b/src/DevTool.Host.Gui/TauriShell/src/assets/typescript.svg new file mode 100644 index 0000000..30a5edd --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src/assets/typescript.svg @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/src/DevTool.Host.Gui/TauriShell/src/assets/vite.svg b/src/DevTool.Host.Gui/TauriShell/src/assets/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src/assets/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/DevTool.Host.Gui/TauriShell/src/domain/workflow.ts b/src/DevTool.Host.Gui/TauriShell/src/domain/workflow.ts new file mode 100644 index 0000000..3029e9f --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src/domain/workflow.ts @@ -0,0 +1,27 @@ +export type WorkflowRunRequest = { + workflowId: string; + projectRoot: string | null; + envProfile: string | null; + sessionId: string; +}; + +export type WorkflowRunPayload = { + command: string; + workingDirectory: string; + stdout: string; + stderr: string; + exitCode: number; +}; + +export type RunStreamLineEvent = { + sessionId: string; + stream: "stdout" | "stderr"; + line: string; +}; + +export type RunStreamStatusEvent = { + sessionId: string; + state: "started" | "completed"; + message: string; + exitCode: number | null; +}; diff --git a/src/DevTool.Host.Gui/TauriShell/src/domain/workspace.ts b/src/DevTool.Host.Gui/TauriShell/src/domain/workspace.ts new file mode 100644 index 0000000..2d131e2 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src/domain/workspace.ts @@ -0,0 +1,7 @@ +export type WorkspaceScanPayload = { + command: string; + workingDirectory: string; + stdout: string; + stderr: string; + exitCode: number; +}; diff --git a/src/DevTool.Host.Gui/TauriShell/src/features/workflowRun.ts b/src/DevTool.Host.Gui/TauriShell/src/features/workflowRun.ts new file mode 100644 index 0000000..5d4ee0f --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src/features/workflowRun.ts @@ -0,0 +1,104 @@ +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import type { + RunStreamLineEvent, + RunStreamStatusEvent, +} from "../domain/workflow"; +import { runWorkflow } from "../services/sdtBridge"; + +function createSessionId(): string { + return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}`; +} + +function appendLine(output: HTMLElement, stream: "stdout" | "stderr", line: string): void { + const prefix = stream === "stderr" ? "ERR" : "OUT"; + output.textContent += `[${prefix}] ${line}\n`; + output.scrollTop = output.scrollHeight; +} + +export async function setupWorkflowRunFeature(): Promise { + const runBtn = document.querySelector("#run-btn"); + const workflowInput = document.querySelector("#workflow-id"); + const rootInput = document.querySelector("#run-project-root"); + const envInput = document.querySelector("#env-profile"); + const statusEl = document.querySelector("#run-status"); + const commandEl = document.querySelector("#run-command"); + const outputEl = document.querySelector("#run-output"); + + if (!runBtn || !workflowInput || !statusEl || !commandEl || !outputEl) { + return; + } + + let activeSessionId: string | null = null; + + const unlistenFns: UnlistenFn[] = []; + unlistenFns.push( + await listen("run_stream_line", (event) => { + if (!activeSessionId || event.payload.sessionId !== activeSessionId) { + return; + } + appendLine(outputEl, event.payload.stream, event.payload.line); + }), + ); + unlistenFns.push( + await listen("run_stream_status", (event) => { + if (!activeSessionId || event.payload.sessionId !== activeSessionId) { + return; + } + const state = event.payload.state; + const suffix = + event.payload.exitCode === null ? "" : ` (exit ${event.payload.exitCode})`; + statusEl.textContent = `Workflow ${state}.${suffix}`; + statusEl.className = + state === "completed" && event.payload.exitCode === 0 + ? "status ok" + : "status error"; + }), + ); + + window.addEventListener("beforeunload", () => { + for (const stop of unlistenFns) { + stop(); + } + }); + + runBtn.addEventListener("click", (event) => { + event.preventDefault(); + void (async () => { + const workflowId = workflowInput.value.trim(); + if (!workflowId) { + statusEl.textContent = "Workflow ID is required."; + statusEl.className = "status error"; + return; + } + + activeSessionId = createSessionId(); + statusEl.textContent = "Starting workflow run..."; + statusEl.className = "status ok"; + commandEl.textContent = ""; + outputEl.textContent = ""; + + try { + const payload = await runWorkflow({ + workflowId, + projectRoot: rootInput?.value.trim() || null, + envProfile: envInput?.value.trim() || null, + sessionId: activeSessionId, + }); + + commandEl.textContent = `${payload.command} (cwd: ${payload.workingDirectory})`; + if (payload.stderr.trim().length > 0) { + appendLine(outputEl, "stderr", payload.stderr.trimEnd()); + } + statusEl.textContent = `Workflow finished with exit code ${payload.exitCode}.`; + statusEl.className = payload.exitCode === 0 ? "status ok" : "status error"; + } catch (error) { + statusEl.textContent = "Workflow run failed to start."; + statusEl.className = "status error"; + outputEl.textContent = + error instanceof Error ? error.message : String(error); + } finally { + activeSessionId = null; + } + })(); + }); +} diff --git a/src/DevTool.Host.Gui/TauriShell/src/features/workspaceScan.ts b/src/DevTool.Host.Gui/TauriShell/src/features/workspaceScan.ts new file mode 100644 index 0000000..4cd87b7 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src/features/workspaceScan.ts @@ -0,0 +1,43 @@ +import { workspaceScan } from "../services/sdtBridge"; + +function setStatus( + statusEl: HTMLElement, + message: string, + isError = false, +): void { + statusEl.textContent = message; + statusEl.className = isError ? "status error" : "status ok"; +} + +export function setupWorkspaceScanFeature(): void { + const runBtn = document.querySelector("#scan-btn"); + const rootInput = document.querySelector("#project-root"); + const statusEl = document.querySelector("#scan-status"); + const commandEl = document.querySelector("#scan-command"); + const outputEl = document.querySelector("#scan-output"); + + if (!runBtn || !statusEl || !commandEl || !outputEl) { + return; + } + + runBtn.addEventListener("click", (event) => { + event.preventDefault(); + void (async () => { + const projectRoot = rootInput?.value.trim(); + setStatus(statusEl, "Running `sdt workspace scan --json`..."); + commandEl.textContent = ""; + outputEl.textContent = ""; + + try { + const payload = await workspaceScan(projectRoot ? projectRoot : null); + commandEl.textContent = `${payload.command} (cwd: ${payload.workingDirectory})`; + outputEl.textContent = JSON.stringify(JSON.parse(payload.stdout), null, 2); + setStatus(statusEl, "Workspace scan completed."); + } catch (error) { + setStatus(statusEl, "Workspace scan failed.", true); + outputEl.textContent = + error instanceof Error ? error.message : String(error); + } + })(); + }); +} diff --git a/src/DevTool.Host.Gui/TauriShell/src/main.ts b/src/DevTool.Host.Gui/TauriShell/src/main.ts new file mode 100644 index 0000000..ae318ba --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src/main.ts @@ -0,0 +1,5 @@ +import { setupWorkspaceScanFeature } from "./features/workspaceScan"; +import { setupWorkflowRunFeature } from "./features/workflowRun"; + +setupWorkspaceScanFeature(); +void setupWorkflowRunFeature(); diff --git a/src/DevTool.Host.Gui/TauriShell/src/services/sdtBridge.ts b/src/DevTool.Host.Gui/TauriShell/src/services/sdtBridge.ts new file mode 100644 index 0000000..3fc199b --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src/services/sdtBridge.ts @@ -0,0 +1,15 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { WorkspaceScanPayload } from "../domain/workspace"; +import type { WorkflowRunPayload, WorkflowRunRequest } from "../domain/workflow"; + +export async function workspaceScan( + projectRoot: string | null, +): Promise { + return invoke("workspace_scan", { projectRoot }); +} + +export async function runWorkflow( + request: WorkflowRunRequest, +): Promise { + return invoke("run_workflow", { request }); +} diff --git a/src/DevTool.Host.Gui/TauriShell/src/styles.css b/src/DevTool.Host.Gui/TauriShell/src/styles.css new file mode 100644 index 0000000..0785b26 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/src/styles.css @@ -0,0 +1,122 @@ +:root { + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + font-size: 16px; + line-height: 1.4; + font-weight: 400; + color: #e8ecf1; + background: radial-gradient(circle at top, #1e293b, #0b1220 60%); + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + min-height: 100vh; +} + +.shell { + max-width: 980px; + margin: 0 auto; + padding: 24px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.panel { + border: 1px solid #3f4f69; + border-radius: 10px; + background: rgba(15, 23, 42, 0.6); + padding: 14px; +} + +.row { + display: flex; + gap: 8px; +} + +.stack { + display: flex; + flex-direction: column; + gap: 8px; +} + +label { + display: block; + margin-bottom: 8px; +} + +input, +button { + border-radius: 8px; + border: 1px solid #3f4f69; + padding: 0.6em 0.8em; + font-size: 0.95em; + font-family: inherit; + color: #e8ecf1; + background-color: #122033; +} + +input { + width: 100%; +} + +button { + cursor: pointer; + white-space: nowrap; + background: #14532d; + border-color: #166534; +} + +button:hover { + background: #15803d; +} + +.status { + margin: 10px 0 0; +} + +.status.ok { + color: #7dd3fc; +} + +.status.error { + color: #fca5a5; +} + +.command { + margin: 6px 0 0; + color: #9ca3af; + font-family: Consolas, "Courier New", monospace; +} + +pre { + margin: 0; + max-height: 420px; + overflow: auto; + background: #020617; + border: 1px solid #1f2937; + border-radius: 8px; + padding: 10px; + font-family: Consolas, "Courier New", monospace; + font-size: 0.9em; +} + +code { + font-family: Consolas, "Courier New", monospace; + color: #c4b5fd; +} + +@media (max-width: 720px) { + .row { + flex-direction: column; + } + + button { + width: 100%; + } +} diff --git a/src/DevTool.Host.Gui/TauriShell/tsconfig.json b/src/DevTool.Host.Gui/TauriShell/tsconfig.json new file mode 100644 index 0000000..75abdef --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/src/DevTool.Host.Gui/TauriShell/vite.config.ts b/src/DevTool.Host.Gui/TauriShell/vite.config.ts new file mode 100644 index 0000000..fc3fe85 --- /dev/null +++ b/src/DevTool.Host.Gui/TauriShell/vite.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from "vite"; + +// @ts-expect-error process is a nodejs global +const host = process.env.TAURI_DEV_HOST; + +// https://vite.dev/config/ +export default defineConfig(async () => ({ + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent Vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: 1421, + } + : undefined, + watch: { + // 3. tell Vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + }, +})); diff --git a/src/DevTool.Host.Tui/DevTool.Host.Tui.csproj b/src/DevTool.Host.Tui/DevTool.Host.Tui.csproj new file mode 100644 index 0000000..d153731 --- /dev/null +++ b/src/DevTool.Host.Tui/DevTool.Host.Tui.csproj @@ -0,0 +1,17 @@ + + + net10.0 + enable + enable + Sdt + + + + + + + + + + + diff --git a/src/DevTool.Host.Tui/Tui/App.cs b/src/DevTool.Host.Tui/Tui/App.cs new file mode 100644 index 0000000..198970f --- /dev/null +++ b/src/DevTool.Host.Tui/Tui/App.cs @@ -0,0 +1,1435 @@ +using Sdt.Config; +using Sdt.Core; +using Sdt.Core.Debug; +using Spectre.Console; + +namespace Sdt.Tui; + +internal sealed record MenuItem(string Display, string Value); +internal sealed record PaletteItem(string Display, string Value); + +public enum AppExitReason { Quit, SwitchProject } +public sealed record AppResult(AppExitReason Reason, string? NewProjectRoot = null, string? RunWorkflowId = null); + +public sealed class App +{ + private DevToolConfig _config; + private string _projectRoot; + private readonly WorkspaceConfig? _workspace; + private readonly string? _workspaceRoot; + private List _workflows; + private readonly List _warnings; + private readonly IDebugProfileRunner _debugRunner; + private readonly IDiagnosticsBundleService _diagnostics; + private readonly IRequirementResolver _requirementResolver = new RequirementResolver(); + private bool _firstRunPromptShown; + private string? _activeEnvProfile; + private string? _startupWorkflowId; + + private readonly WorkflowExecutor _executor = new( + new WorkflowPlanner(), + new ToolProbeService(), + new PrereqInstallerService(), + new ActionRunner(), + new RequirementResolver()); + + private IReadOnlyDictionary WorkflowMap => + _workflows.ToDictionary(t => t.Id, StringComparer.OrdinalIgnoreCase); + + public App( + DevToolConfig config, + string projectRoot, + IReadOnlyList? warnings = null, + WorkspaceConfig? workspace = null, + string? workspaceRoot = null, + string? startupWorkflowId = null) + { + _config = config; + _projectRoot = projectRoot; + _workspace = workspace; + _workspaceRoot = workspaceRoot; + _startupWorkflowId = startupWorkflowId; + _activeEnvProfile = config.EnvProfiles?.Active; + var normalized = WorkflowModelBuilder.Normalize(config, ResolveLegacyMode(), _requirementResolver); + _workflows = normalized.Workflows.ToList(); + _warnings = []; + _debugRunner = new DebugProfileRunner(new ToolProbeService(), new PrereqInstallerService()); + _diagnostics = new DiagnosticsBundleService(); + if (warnings is not null) + _warnings.AddRange(warnings); + _warnings.AddRange(normalized.Warnings); + } + + private static LegacyMode ResolveLegacyMode() + { + var raw = Environment.GetEnvironmentVariable("SDT_LEGACY_MODE"); + return string.Equals(raw, "compat", StringComparison.OrdinalIgnoreCase) + ? LegacyMode.Compat + : LegacyMode.Strict; + } + + public async Task RunAsync() + { + while (true) + { + AnsiConsole.Clear(); + RenderBanner(); + await PromptFirstRunSetupAsync(); + + if (_warnings.Count > 0) + { + foreach (var warning in _warnings.Distinct(StringComparer.OrdinalIgnoreCase)) + AnsiConsole.MarkupLine(Theme.Warn("Config warning: " + warning)); + AnsiConsole.WriteLine(); + } + + if (!string.IsNullOrWhiteSpace(_startupWorkflowId)) + { + var requestedWorkflow = _startupWorkflowId; + _startupWorkflowId = null; + var startupWorkflowMap = WorkflowMap; + if (requestedWorkflow is not null && + startupWorkflowMap.TryGetValue(requestedWorkflow, out var startupWorkflow)) + { + await RunWorkflowAsync(startupWorkflow, startupWorkflowMap); + AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to return to the menu...")); + Console.ReadKey(intercept: true); + continue; + } + + AnsiConsole.MarkupLine(Theme.Warn($"Requested quick action '{requestedWorkflow}' was not found in this project.")); + AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to continue...")); + Console.ReadKey(intercept: true); + } + + if (TryConsumeCommandPaletteHotkey()) + { + var paletteAction = ShowCommandPalette(); + if (paletteAction is not null) + { + var paletteResult = await HandlePaletteActionAsync(paletteAction); + if (paletteResult is not null) + return paletteResult; + AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to return to the menu...")); + Console.ReadKey(intercept: true); + continue; + } + } + else if (TryConsumeKeybindingHelpHotkey()) + { + new KeybindingsScreen(_projectRoot, _config.Name, _config.Version).Run(); + continue; + } + + var choice = ShowMainMenu(); + switch (choice) + { + case "__env__": + EditEnvironment(); + break; + + case "__envprofile__": + SelectEnvProfile(); + break; + + case "__toolchains__": + await new ToolchainScreen(_config, _projectRoot).RunAsync(); + break; + + case "__doctor__": + await RunConfigDoctorAsync(); + break; + + case "__setup__": + await new SetupWizardScreen().RunAsync(_config, _projectRoot); + ReloadConfigFromDisk(); + break; + + case "__events__": + new EventsScreen(_projectRoot, _config.Name, _config.Version, _workspace, _workspaceRoot).Run(); + break; + + case "__history__": + { + var history = new RunHistoryScreen(_projectRoot, _config.Name, _config.Version); + var action = history.Run(); + if (action is not null && action.Kind == "rerun" && !string.IsNullOrWhiteSpace(action.TargetId)) + { + if (!string.Equals(action.ProjectRoot, _projectRoot, StringComparison.OrdinalIgnoreCase)) + { + if (string.Equals(action.Category, "workflow", StringComparison.OrdinalIgnoreCase)) + return new AppResult(AppExitReason.SwitchProject, action.ProjectRoot, action.TargetId); + return new AppResult(AppExitReason.SwitchProject, action.ProjectRoot); + } + + if (string.Equals(action.Category, "workflow", StringComparison.OrdinalIgnoreCase)) + { + var map = WorkflowMap; + if (map.TryGetValue(action.TargetId, out var wf)) + await RunWorkflowAsync(wf, map); + } + else if (string.Equals(action.Category, "debug", StringComparison.OrdinalIgnoreCase)) + { + await RunDebugProfileAsync(action.TargetId, verbose: false); + } + } + break; + } + + case "__keys__": + new KeybindingsScreen(_projectRoot, _config.Name, _config.Version).Run(); + break; + + case "__palette__": + { + var paletteAction = ShowCommandPalette(); + if (paletteAction is not null) + { + var paletteResult = await HandlePaletteActionAsync(paletteAction); + if (paletteResult is not null) + return paletteResult; + } + break; + } + + case "__workspace__": + if (_workspace is not null && _workspaceRoot is not null) + { + var switcher = new WorkspaceScreen(_workspace, _workspaceRoot, _projectRoot); + var newRoot = switcher.SelectProject(); + if (newRoot is not null) + return new AppResult(AppExitReason.SwitchProject, newRoot); + } + break; + + case "__favorites_manage__": + ManageFavorites(); + break; + + case "__migrate_legacy__": + ApplyLegacyMigration(); + break; + + case "__quit__": + AnsiConsole.MarkupLine("\n" + Theme.Faint("Later.") + "\n"); + return new AppResult(AppExitReason.Quit); + + default: + if (choice.StartsWith("__debugattach__:", StringComparison.Ordinal)) + { + var profileId = choice["__debugattach__:".Length..]; + ShowAttachInstructions(profileId); + break; + } + + if (choice.StartsWith("__debug__:", StringComparison.Ordinal)) + { + var parts = choice.Split(':', 3, StringSplitOptions.None); + if (parts.Length == 3) + await RunDebugProfileAsync(parts[1], string.Equals(parts[2], "verbose", StringComparison.OrdinalIgnoreCase)); + break; + } + + if (choice.StartsWith("__favorite__:", StringComparison.Ordinal)) + { + var favoriteSelection = ParseFavoriteSelection(choice); + if (favoriteSelection is null) + break; + + var (favoriteProjectRoot, workflowId) = favoriteSelection.Value; + if (string.Equals(favoriteProjectRoot, _projectRoot, StringComparison.OrdinalIgnoreCase)) + { + var currentMap = WorkflowMap; + if (currentMap.TryGetValue(workflowId, out var favoriteWorkflow)) + await RunWorkflowAsync(favoriteWorkflow, currentMap); + else + AnsiConsole.MarkupLine(Theme.Warn($"Quick action '{workflowId}' is not available in current project.")); + break; + } + + return new AppResult(AppExitReason.SwitchProject, favoriteProjectRoot, workflowId); + } + + var workflowMap = WorkflowMap; + if (workflowMap.TryGetValue(choice, out var workflow)) + await RunWorkflowAsync(workflow, workflowMap); + break; + } + + if (choice != "__quit__") + { + AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to return to the menu...")); + Console.ReadKey(intercept: true); + } + } + } + + private void RenderBanner() + { + AnsiConsole.Write(new FigletText("SDT").Color(Theme.GreenColor)); + var wsInfo = _workspace is not null + ? $" [{Theme.GreenDim}]∙ {Markup.Escape(_workspace.Name)}[/]" + : string.Empty; + + AnsiConsole.Write( + new Rule($"[bold {Theme.GreenBold}]{Markup.Escape(_config.Name)}[/] [{Theme.GreenDim}]v{Markup.Escape(_config.Version)}[/]{wsInfo}") + .RuleStyle(Theme.DimStyle)); + AnsiConsole.MarkupLine(Theme.Faint($"root: {_projectRoot}") + "\n"); + } + + private string ShowMainMenu() + { + var prompt = new SelectionPrompt() + .Title($"[{Theme.Green}]What would you like to do?[/]") + .PageSize(28) + .MoreChoicesText(Theme.Faint("(scroll to see more)")) + .UseConverter(m => m.Display); + + var favoriteItems = BuildFavoriteMenuItems(); + if (favoriteItems.Count > 0) + { + prompt.AddChoiceGroup( + new MenuItem($"[bold {Theme.Amber}]QUICK ACTIONS[/]", "__group__"), + favoriteItems); + } + + var groups = _workflows + .Where(t => !string.IsNullOrWhiteSpace(t.Label)) + .GroupBy(t => string.IsNullOrWhiteSpace(t.Group) ? "General" : t.Group); + + foreach (var group in groups) + { + var header = new MenuItem( + $"[bold {Theme.Amber}]{Markup.Escape(group.Key.ToUpperInvariant())}[/]", + "__group__"); + + var items = group.Select(t => new MenuItem( + $"[{Theme.Green}]{Markup.Escape(t.Label)}[/] [{Theme.GreenDim}]{Markup.Escape(t.Description)}[/]", + t.Id)).ToList(); + + prompt.AddChoiceGroup(header, items); + } + + var debugProfiles = _config.Debug?.Profiles ?? []; + if (debugProfiles.Count > 0) + { + var debugItems = new List(); + foreach (var profile in debugProfiles) + { + debugItems.Add(new MenuItem( + $"[{Theme.Green}]Run {Markup.Escape(profile.Label)}[/] [{Theme.GreenDim}]debug profile[/]", + $"__debug__:{profile.Id}:normal")); + debugItems.Add(new MenuItem( + $"[{Theme.Green}]Run {Markup.Escape(profile.Label)} (verbose)[/] [{Theme.GreenDim}]stream full output[/]", + $"__debug__:{profile.Id}:verbose")); + if (profile.Attach is not null) + { + debugItems.Add(new MenuItem( + $"[{Theme.Green}]Attach instructions: {Markup.Escape(profile.Label)}[/]", + $"__debugattach__:{profile.Id}")); + } + } + + prompt.AddChoiceGroup( + new MenuItem($"[bold {Theme.Amber}]DEBUG[/]", "__group__"), + debugItems); + } + + var systemItems = new List + { + new($"[{Theme.Green}]{Theme.Glyph("⌘", "CMD")} Command palette[/] [{Theme.GreenDim}]quick action / run / switch (Ctrl+K)[/]", "__palette__"), + new($"[{Theme.Green}]{Theme.Glyph("?", "HELP")} Keybinding help[/] [{Theme.GreenDim}]cross-platform shortcut map[/]", "__keys__"), + new($"[{Theme.Green}]{Theme.Glyph("🕘", "HIST")} Run history[/] [{Theme.GreenDim}]re-run from prior context[/]", "__history__"), + new($"[{Theme.Green}]{Theme.Glyph("✨", "SETUP")} Run setup wizard[/] [{Theme.GreenDim}]first-run bootstrap + fixes[/]", "__setup__"), + new($"[{Theme.Green}]{Theme.Glyph("🩺", "DOC")} Run config doctor[/] [{Theme.GreenDim}]validate config, tools, paths[/]", "__doctor__"), + new($"[{Theme.Green}]{Theme.Glyph("📜", "LOG")} View run events[/] [{Theme.GreenDim}].sdt/events JSONL viewer[/]", "__events__"), + new($"[{Theme.Green}]{Theme.Glyph("🧩", "ENV")} Select env profile[/] [{Theme.GreenDim}]current: {Markup.Escape(_activeEnvProfile ?? "(none)") }[/]", "__envprofile__"), + new($"[{Theme.Green}]{Theme.Glyph("⚙", "CFG")} Edit environment variables[/]", "__env__"), + }; + + if (_workspace is not null) + systemItems.Insert(0, new MenuItem( + $"[{Theme.Green}]★ Manage quick actions[/] [{Theme.GreenDim}]favorite workflows across projects[/]", + "__favorites_manage__")); + + if (_config.Toolchains is not null) + systemItems.Insert(0, new MenuItem( + $"[{Theme.Green}]⬡ Toolchain management[/] [{Theme.GreenDim}]python / node[/]", + "__toolchains__")); + + if (_workspace is not null) + systemItems.Insert(0, new MenuItem( + $"[{Theme.Green}]⇄ Workspace projects[/] [{Theme.GreenDim}]{Markup.Escape(_workspace.Name)}[/]", + "__workspace__")); + + if (_config.Targets.Count > 0) + systemItems.Insert(0, new MenuItem( + $"[{Theme.Green}]⇪ Migrate legacy targets → workflows[/] [{Theme.GreenDim}]writes devtool.json + backup[/]", + "__migrate_legacy__")); + + systemItems.Add(new MenuItem($"[{Theme.GreenDim}]✗ Quit[/]", "__quit__")); + + prompt.AddChoiceGroup( + new MenuItem($"[bold {Theme.Amber}]SYSTEM[/]", "__group__"), + systemItems); + + return AnsiConsole.Prompt(prompt).Value; + } + + private async Task RunWorkflowAsync( + WorkflowDefinition workflow, + IReadOnlyDictionary workflowMap) + { + AnsiConsole.Clear(); + AnsiConsole.Write(Theme.SectionRule(workflow.Label)); + var runId = Guid.NewGuid().ToString("N"); + RenderRunContextBlock( + category: "workflow", + targetId: workflow.Id, + runId: runId, + workingDir: "."); + RenderLifecycleModel(); + + var plan = new WorkflowPlanner().ResolvePlan(workflow, workflowMap); + if (plan.Count == 0) + { + AnsiConsole.MarkupLine(Theme.Warn("This workflow has no executable steps.")); + return; + } + + if (plan.Count > 1) + { + AnsiConsole.MarkupLine(Theme.Faint($"Execution plan — {plan.Count} workflow(s):")); + foreach (var item in plan) + AnsiConsole.MarkupLine($" [{Theme.GreenDim}]→[/] [{Theme.Green}]{Markup.Escape(item.Label)}[/]"); + AnsiConsole.WriteLine(); + } + + var outputLines = new List(); + using var eventRecorder = RunEventJsonlRecorder.Create(_projectRoot, "workflow", runId, _activeEnvProfile); + + var result = await _executor.ExecuteAsync( + workflow, + workflowMap, + _config, + _projectRoot, + confirmInstallAsync: ConfirmInstallAsync, + onOutput: (line, isErr) => + { + outputLines.Add((isErr ? "ERR: " : "OUT: ") + line); + var escaped = Markup.Escape(line); + AnsiConsole.MarkupLine(isErr + ? $"[{Theme.Amber}]{escaped}[/]" + : $"[{Theme.Green}]{escaped}[/]"); + }, + onEvent: evt => + { + eventRecorder.Write(evt); + RenderRunEvent(evt); + }, + envOverrides: ResolveActiveEnvOverrides()); + + AnsiConsole.Write(Theme.SectionRule()); + RenderStepSummary(result); + RenderProbeDiagnosticsSummary(outputLines); + if (result.Success) + { + var totalSeconds = result.Steps.Sum(s => s.Result.Elapsed.TotalSeconds); + AnsiConsole.MarkupLine("\n" + Theme.Ok($"Done! Total: {totalSeconds:F1}s")); + AnsiConsole.MarkupLine(Theme.Faint($"Run events: {eventRecorder.FilePath}")); + PromptPinQuickActionFromRunResult(workflow); + return; + } + + AnsiConsole.MarkupLine("\n" + Theme.Fail($"{result.StopReason}: {result.Message}")); + RenderFailureCard( + result.StopReason, + result.Message, + "run", + workflow.Id, + _projectRoot, + _activeEnvProfile); + AnsiConsole.MarkupLine(Theme.Faint($"Run events: {eventRecorder.FilePath}")); + PromptPinQuickActionFromRunResult(workflow); + await WriteWorkflowDiagnosticsAsync(workflow, workflowMap, result, outputLines); + } + + private static void RenderStepSummary(WorkflowExecutionResult result) + { + if (result.Steps.Count == 0) + return; + + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Workflow[/]")) + .AddColumn(new TableColumn($"[{Theme.Amber}]Step[/]")) + .AddColumn(new TableColumn($"[{Theme.Amber}]Exit[/]").Width(8)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Seconds[/]").Width(10)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Status[/]").Width(12)); + + foreach (var step in result.Steps) + { + var ok = step.Result.Success; + table.AddRow( + Theme.Faint(step.WorkflowId), + Theme.G(step.StepLabel), + Theme.Bold(step.Result.ExitCode.ToString()), + Theme.Faint($"{step.Result.Elapsed.TotalSeconds:F1}"), + ok ? Theme.Ok("ok") : Theme.Fail("failed")); + } + + AnsiConsole.Write(table); + } + + private async Task RunDebugProfileAsync(string profileId, bool verbose) + { + var profile = _config.Debug?.Profiles.FirstOrDefault(p => string.Equals(p.Id, profileId, StringComparison.OrdinalIgnoreCase)); + if (profile is null) + { + AnsiConsole.MarkupLine(Theme.Fail($"Debug profile not found: {profileId}")); + return; + } + + AnsiConsole.Clear(); + AnsiConsole.Write(Theme.SectionRule($"DEBUG — {profile.Label}")); + var runId = Guid.NewGuid().ToString("N"); + RenderRunContextBlock( + category: "debug", + targetId: profile.Id, + runId: runId, + workingDir: profile.WorkingDir); + RenderLifecycleModel(); + using var eventRecorder = RunEventJsonlRecorder.Create(_projectRoot, "debug", runId, _activeEnvProfile); + + if (profile.Attach is not null) + { + AnsiConsole.MarkupLine(Theme.Faint($"Attach: {profile.Attach.Kind}")); + if (profile.Attach.Port is not null) + AnsiConsole.MarkupLine(Theme.Faint($"Port: {profile.Attach.Port}")); + if (!string.IsNullOrWhiteSpace(profile.Attach.ProcessName)) + AnsiConsole.MarkupLine(Theme.Faint($"Process: {profile.Attach.ProcessName}")); + if (!string.IsNullOrWhiteSpace(profile.Attach.Note)) + AnsiConsole.MarkupLine(Theme.Faint(profile.Attach.Note!)); + AnsiConsole.WriteLine(); + } + + var result = await _debugRunner.RunAsync( + profile, + _config, + _projectRoot, + verbose, + ConfirmInstallAsync, + (line, isErr) => + { + var escaped = Markup.Escape(line); + AnsiConsole.MarkupLine(isErr + ? $"[{Theme.Amber}]{escaped}[/]" + : $"[{Theme.Green}]{escaped}[/]"); + }, + evt => + { + eventRecorder.Write(evt); + RenderRunEvent(evt); + }, + envOverrides: ResolveActiveEnvOverrides()); + + if (result.Success) + { + var seconds = result.RunResult?.Elapsed.TotalSeconds ?? 0; + AnsiConsole.MarkupLine("\n" + Theme.Ok($"Debug run completed in {seconds:F1}s")); + AnsiConsole.MarkupLine(Theme.Faint($"Run events: {eventRecorder.FilePath}")); + return; + } + + AnsiConsole.MarkupLine("\n" + Theme.Fail($"{result.StopReason}: {result.Message}")); + RenderFailureCard( + result.StopReason, + result.Message, + "debug", + profile.Id, + _projectRoot, + _activeEnvProfile); + AnsiConsole.MarkupLine(Theme.Faint($"Run events: {eventRecorder.FilePath}")); + + var diagnostics = _config.Debug?.Diagnostics ?? new DebugDiagnosticsOptions(); + if (!diagnostics.Enabled || !diagnostics.BundleOnFailure) + return; + + var bundle = await _diagnostics.WriteBundleAsync( + new DiagnosticsBundleRequest( + Category: "debug", + ProjectRoot: _projectRoot, + SummaryMessage: result.Message, + OutputLines: result.OutputLines, + WorkflowSteps: [], + Probes: result.Probes, + DiagnosticsOptions: diagnostics, + Config: _config, + StopReason: result.StopReason, + DebugRun: result.RunResult, + DebugProfile: result.Profile)); + + if (bundle.Success) + AnsiConsole.MarkupLine(Theme.Warn($"Diagnostics bundle: {bundle.BundleDirectory}")); + else + AnsiConsole.MarkupLine(Theme.Warn($"Diagnostics bundle failed: {bundle.Message}")); + } + + private static void RenderProbeDiagnosticsSummary(IReadOnlyList outputLines) + { + var diagnostics = new List<(string Tool, string Detail)>(); + foreach (var line in outputLines) + { + const string marker = "Probe detail ["; + var markerIndex = line.IndexOf(marker, StringComparison.OrdinalIgnoreCase); + if (markerIndex < 0) + continue; + + var toolStart = markerIndex + marker.Length; + var toolEnd = line.IndexOf(']', toolStart); + if (toolEnd <= toolStart) + continue; + + var tool = line[toolStart..toolEnd].Trim(); + var detailStart = line.IndexOf(':', toolEnd); + var detail = detailStart >= 0 && detailStart + 1 < line.Length + ? line[(detailStart + 1)..].Trim() + : ""; + + if (!string.IsNullOrWhiteSpace(tool)) + diagnostics.Add((tool, detail)); + } + + if (diagnostics.Count == 0) + return; + + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Probe Tool[/]").Width(14)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Resolver / Probe Detail[/]")) + .AddColumn(new TableColumn($"[{Theme.Amber}]Suggested Fix[/]")); + + foreach (var diag in diagnostics.Distinct()) + { + table.AddRow( + Theme.Warn(diag.Tool), + Theme.Faint(diag.Detail), + Theme.Faint(GetProbeFixHint(diag.Tool, diag.Detail))); + } + + AnsiConsole.WriteLine(); + AnsiConsole.Write(Theme.SectionRule("PROBE DIAGNOSTICS")); + AnsiConsole.Write(table); + } + + private static string GetProbeFixHint(string tool, string detail) + { + if (detail.Contains("ConfiguredOverride", StringComparison.OrdinalIgnoreCase)) + return "Check tooling.tools[].executables paths."; + if (detail.Contains("NodeAdjacentShim", StringComparison.OrdinalIgnoreCase)) + return "Node found; verify npm/yarn shim in node directory."; + if (detail.Contains("Fallback", StringComparison.OrdinalIgnoreCase)) + return $"Add {tool} to PATH or set tooling.tools[].executables."; + return $"Install/configure {tool} then rerun."; + } + + private static void RenderRunEvent(RunEvent evt) + { + var shouldRender = evt.Type is + RunEventType.WorkflowStarted or + RunEventType.WorkflowStepStarted or + RunEventType.WorkflowStepCompleted or + RunEventType.ProbeFailed or + RunEventType.InstallPlanPrepared or + RunEventType.DebugStarted or + RunEventType.DebugCommandStarted or + RunEventType.DebugCommandCompleted or + RunEventType.WorkflowCompleted or + RunEventType.DebugCompleted; + + if (!shouldRender) + return; + + var prefix = evt.Category.Equals("debug", StringComparison.OrdinalIgnoreCase) ? "DBG" : "RUN"; + var tone = evt.Success is false ? Theme.Amber : Theme.GreenDim; + AnsiConsole.MarkupLine($"[{tone}]{prefix} {Markup.Escape(evt.Message)}[/]"); + } + + private static void RenderFailureCard( + ExecutionStopReason? reason, + string message, + string targetKind, + string targetId, + string? projectRoot = null, + string? envProfile = null) + { + var card = FailureCardBuilder.Build(reason, message, targetKind, targetId, projectRoot, envProfile); + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Field[/]").Width(16)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Value[/]")); + + table.AddRow(Theme.Warn("What failed"), Theme.Faint(card.WhatFailed)); + table.AddRow(Theme.Warn("Why"), Theme.Faint(card.Why)); + table.AddRow(Theme.Warn("Exact fix"), Theme.Faint(card.ExactFixCommand)); + table.AddRow(Theme.Warn("Retry"), Theme.Faint(card.RetryInstruction)); + + AnsiConsole.WriteLine(); + AnsiConsole.Write(Theme.SectionRule("FAILURE CARD")); + AnsiConsole.Write(table); + } + + private static void RenderLifecycleModel() + { + var lifecycle = "Plan -> Probe -> Prompt -> Execute -> Diagnose"; + AnsiConsole.MarkupLine(Theme.Faint($"Lifecycle: {lifecycle}")); + AnsiConsole.WriteLine(); + } + + private void RenderRunContextBlock(string category, string targetId, string runId, string workingDir) + { + var cwd = Path.GetFullPath(Path.Combine(_projectRoot, string.IsNullOrWhiteSpace(workingDir) ? "." : workingDir)); + var panelText = string.Join(Environment.NewLine, + [ + $"category: {category}", + $"target: {targetId}", + $"runId: {runId}", + $"project: {_projectRoot}", + $"envProfile: {_activeEnvProfile ?? "(none)"}", + $"cwd: {cwd}", + "toolResolutionSource: probe diagnostics (Path/Shim/ConfiguredOverride/Fallback)", + ]); + + AnsiConsole.Write( + new Panel(Markup.Escape(panelText)) + .Header($"[{Theme.Amber}]RUN CONTEXT[/]") + .BorderStyle(Theme.DimStyle)); + AnsiConsole.WriteLine(); + } + + private bool TryConsumeCommandPaletteHotkey() + { + if (Console.IsInputRedirected) + return false; + + if (!Console.KeyAvailable) + return false; + + var key = Console.ReadKey(intercept: true); + return key.Key == ConsoleKey.K && key.Modifiers.HasFlag(ConsoleModifiers.Control); + } + + private bool TryConsumeKeybindingHelpHotkey() + { + if (Console.IsInputRedirected) + return false; + + if (!Console.KeyAvailable) + return false; + + var key = Console.ReadKey(intercept: true); + return key.Key == ConsoleKey.F1 || key.KeyChar == '?'; + } + + private string? ShowCommandPalette() + { + var items = new List(); + + foreach (var workflow in _workflows) + { + items.Add(new PaletteItem( + $"[{Theme.Green}]Run workflow:[/] {Markup.Escape(workflow.Label)} [{Theme.GreenDim}]({Markup.Escape(workflow.Id)})[/]", + $"workflow:{workflow.Id}")); + } + + foreach (var profile in _config.Debug?.Profiles ?? []) + { + items.Add(new PaletteItem( + $"[{Theme.Green}]Run debug:[/] {Markup.Escape(profile.Label)} [{Theme.GreenDim}]({Markup.Escape(profile.Id)})[/]", + $"debug:{profile.Id}")); + } + + if (_workspace is not null && _workspaceRoot is not null) + { + foreach (var project in _workspace.Projects.Where(p => !p.Disabled)) + { + var abs = WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, project); + if (string.Equals(abs, _projectRoot, StringComparison.OrdinalIgnoreCase)) + continue; + + items.Add(new PaletteItem( + $"[{Theme.Green}]Switch project:[/] {Markup.Escape(project.Name)} [{Theme.GreenDim}]{Markup.Escape(abs)}[/]", + $"switch:{abs}")); + } + } + + if (items.Count == 0) + { + AnsiConsole.MarkupLine(Theme.Warn("No command palette actions available.")); + return null; + } + + items.Add(new PaletteItem(Theme.Faint("Cancel"), "__cancel__")); + + var selected = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[{Theme.Green}]Command palette (Ctrl+K):[/]") + .PageSize(20) + .EnableSearch() + .UseConverter(i => i.Display) + .AddChoices(items)); + + return selected.Value == "__cancel__" ? null : selected.Value; + } + + private async Task HandlePaletteActionAsync(string action) + { + if (action.StartsWith("workflow:", StringComparison.Ordinal)) + { + var workflowId = action["workflow:".Length..]; + var map = WorkflowMap; + if (map.TryGetValue(workflowId, out var workflow)) + await RunWorkflowAsync(workflow, map); + else + AnsiConsole.MarkupLine(Theme.Warn($"Workflow '{workflowId}' not found.")); + return null; + } + + if (action.StartsWith("debug:", StringComparison.Ordinal)) + { + var profileId = action["debug:".Length..]; + await RunDebugProfileAsync(profileId, verbose: false); + return null; + } + + if (action.StartsWith("switch:", StringComparison.Ordinal)) + { + var projectRoot = action["switch:".Length..]; + return new AppResult(AppExitReason.SwitchProject, projectRoot); + } + + return null; + } + + private void PromptPinQuickActionFromRunResult(WorkflowDefinition workflow) + { + if (_workspace is null || _workspaceRoot is null) + return; + + if (RuntimePolicy.IsNonInteractive()) + return; + + var existing = _workspace.Favorites.Any(f => + string.Equals(WorkspaceLoader.ResolveFavoriteProjectRoot(_workspaceRoot, f), _projectRoot, StringComparison.OrdinalIgnoreCase) && + string.Equals(f.WorkflowId, workflow.Id, StringComparison.OrdinalIgnoreCase)); + if (existing) + return; + + var pin = AnsiConsole.Confirm( + $"[{Theme.Amber}]Pin '{Markup.Escape(workflow.Label)}' as a workspace quick action?[/]", + defaultValue: false); + if (!pin) + return; + + var projectPath = _workspace.Projects + .FirstOrDefault(p => string.Equals( + WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, p), + _projectRoot, + StringComparison.OrdinalIgnoreCase)) + ?.Path ?? _projectRoot; + + _workspace.Favorites.Add(new WorkspaceFavorite + { + ProjectPath = projectPath, + WorkflowId = workflow.Id, + Label = workflow.Label + }); + WorkspaceLoader.Save(_workspaceRoot, _workspace); + AnsiConsole.MarkupLine(Theme.Ok($"Pinned quick action: {workflow.Id}")); + } + + private List BuildFavoriteMenuItems() + { + if (_workspace is null || _workspaceRoot is null || _workspace.Favorites.Count == 0) + return []; + + var items = new List(); + var index = 0; + foreach (var favorite in _workspace.Favorites) + { + var projectRoot = WorkspaceLoader.ResolveFavoriteProjectRoot(_workspaceRoot, favorite); + var isCurrent = string.Equals(projectRoot, _projectRoot, StringComparison.OrdinalIgnoreCase); + var workflow = isCurrent + ? _workflows.FirstOrDefault(w => string.Equals(w.Id, favorite.WorkflowId, StringComparison.OrdinalIgnoreCase)) + : null; + + var projectName = ResolveWorkspaceProjectName(projectRoot); + var workflowLabel = workflow?.Label ?? favorite.WorkflowId; + var detail = isCurrent + ? "current project" + : projectName; + + var displayLabel = !string.IsNullOrWhiteSpace(favorite.Label) + ? favorite.Label! + : workflowLabel; + + var display = $"[{Theme.Green}]▶ {Markup.Escape(displayLabel)}[/] [{Theme.GreenDim}]{Markup.Escape(detail)}[/]"; + items.Add(new MenuItem(display, $"__favorite__:{index}")); + index++; + } + + return items; + } + + private (string ProjectRoot, string WorkflowId)? ParseFavoriteSelection(string choice) + { + if (_workspace is null || _workspaceRoot is null) + return null; + + var value = choice["__favorite__:".Length..]; + if (!int.TryParse(value, out var index)) + return null; + + if (index < 0 || index >= _workspace.Favorites.Count) + return null; + + var favorite = _workspace.Favorites[index]; + var projectRoot = WorkspaceLoader.ResolveFavoriteProjectRoot(_workspaceRoot, favorite); + return (projectRoot, favorite.WorkflowId); + } + + private string ResolveWorkspaceProjectName(string projectRoot) + { + if (_workspace is null || _workspaceRoot is null) + return new DirectoryInfo(projectRoot).Name; + + foreach (var project in _workspace.Projects) + { + var resolved = WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, project); + if (string.Equals(resolved, projectRoot, StringComparison.OrdinalIgnoreCase)) + return project.Name; + } + + return new DirectoryInfo(projectRoot).Name; + } + + private async Task PromptFirstRunSetupAsync() + { + if (_firstRunPromptShown) + return; + _firstRunPromptShown = true; + + if (!SetupStateService.IsFirstRun(_projectRoot)) + return; + + AnsiConsole.MarkupLine(Theme.Warn("First run detected for this project.")); + var runSetup = AnsiConsole.Confirm( + $"[{Theme.Amber}]Run setup wizard now?[/]", + defaultValue: true); + if (!runSetup) + return; + + await new SetupWizardScreen().RunAsync(_config, _projectRoot); + ReloadConfigFromDisk(); + } + + private void ReloadConfigFromDisk() + { + var reloaded = ConfigLoader.FindAndLoad(_projectRoot); + if (reloaded is null) + return; + _config = reloaded.Config; + var normalized = WorkflowModelBuilder.Normalize(_config, ResolveLegacyMode(), _requirementResolver); + _workflows = normalized.Workflows.ToList(); + _warnings.Clear(); + _warnings.AddRange(reloaded.Warnings); + _warnings.AddRange(normalized.Warnings); + } + + private void ShowAttachInstructions(string profileId) + { + var profile = _config.Debug?.Profiles.FirstOrDefault(p => string.Equals(p.Id, profileId, StringComparison.OrdinalIgnoreCase)); + if (profile?.Attach is null) + { + AnsiConsole.MarkupLine(Theme.Warn("No attach instructions configured for this profile.")); + return; + } + + AnsiConsole.Clear(); + AnsiConsole.Write(Theme.SectionRule($"ATTACH — {profile.Label}")); + AnsiConsole.MarkupLine(Theme.Faint($"Kind: {profile.Attach.Kind}")); + if (profile.Attach.Port is not null) + AnsiConsole.MarkupLine(Theme.Faint($"Port: {profile.Attach.Port}")); + if (!string.IsNullOrWhiteSpace(profile.Attach.ProcessName)) + AnsiConsole.MarkupLine(Theme.Faint($"Process: {profile.Attach.ProcessName}")); + if (!string.IsNullOrWhiteSpace(profile.Attach.Note)) + AnsiConsole.MarkupLine(Theme.G(profile.Attach.Note!)); + } + + private async Task WriteWorkflowDiagnosticsAsync( + WorkflowDefinition workflow, + IReadOnlyDictionary workflowMap, + WorkflowExecutionResult result, + IReadOnlyList outputLines) + { + var diagnostics = _config.Debug?.Diagnostics ?? new DebugDiagnosticsOptions(); + if (!diagnostics.Enabled || !diagnostics.BundleOnFailure) + return; + + var probes = await SnapshotWorkflowToolsAsync(workflow, workflowMap); + + var bundle = await _diagnostics.WriteBundleAsync( + new DiagnosticsBundleRequest( + Category: "workflow", + ProjectRoot: _projectRoot, + SummaryMessage: result.Message, + OutputLines: outputLines, + WorkflowSteps: result.Steps, + Probes: probes, + DiagnosticsOptions: diagnostics, + Config: _config, + StopReason: result.StopReason)); + + if (bundle.Success) + AnsiConsole.MarkupLine(Theme.Warn($"Diagnostics bundle: {bundle.BundleDirectory}")); + else + AnsiConsole.MarkupLine(Theme.Warn($"Diagnostics bundle failed: {bundle.Message}")); + } + + private async Task> SnapshotWorkflowToolsAsync( + WorkflowDefinition workflow, + IReadOnlyDictionary workflowMap) + { + var plan = new WorkflowPlanner().ResolvePlan(workflow, workflowMap); + var tools = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var item in plan) + { + foreach (var step in item.Steps) + { + foreach (var req in _requirementResolver.Resolve(step)) + { + if (!string.IsNullOrWhiteSpace(req.Tool)) + tools.Add(req.Tool); + } + } + } + + var probeService = new ToolProbeService(); + var probes = new List(); + foreach (var tool in tools) + probes.Add(await probeService.ProbeAsync(tool, _projectRoot, _config, ResolveActiveEnvOverrides())); + return probes; + } + + private Task ConfirmInstallAsync(string tool, InstallPlan plan) + { + if (RuntimePolicy.IsNonInteractive()) + { + AnsiConsole.MarkupLine(Theme.Warn($"Non-interactive mode enabled; declining install prompt for '{tool}'.")); + return Task.FromResult(false); + } + + AnsiConsole.MarkupLine(Theme.Warn($"Missing prerequisite: {tool}")); + AnsiConsole.MarkupLine(Theme.Faint(plan.Summary)); + foreach (var cmd in plan.Commands) + AnsiConsole.MarkupLine(Theme.Faint($" $ {cmd.Command} {string.Join(" ", cmd.Args)}")); + + var allow = AnsiConsole.Confirm( + $"[{Theme.Amber}]Run install commands for {Markup.Escape(tool)} now?[/]", + defaultValue: false); + return Task.FromResult(allow); + } + + private async Task RunConfigDoctorAsync() + { + AnsiConsole.Clear(); + AnsiConsole.Write(Theme.SectionRule("CONFIG DOCTOR")); + AnsiConsole.WriteLine(); + + var service = new ConfigDoctorService(new ToolProbeService(), _requirementResolver); + var report = await service.RunAsync(_config, _projectRoot); + + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Check[/]").Width(26)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Status[/]").Width(10)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Detail[/]")) + .AddColumn(new TableColumn($"[{Theme.Amber}]Fix[/]")); + + foreach (var check in report.Checks) + { + var statusText = check.Status switch + { + DoctorStatus.Pass => Theme.Ok("ok"), + DoctorStatus.Warn => Theme.Warn("warn"), + DoctorStatus.Fail => Theme.Fail("fail"), + _ => Theme.Faint("n/a"), + }; + + table.AddRow( + Theme.G(check.Name), + statusText, + Theme.Faint(check.Detail), + string.IsNullOrWhiteSpace(check.Fix) ? Theme.Faint("-") : Theme.Faint(check.Fix)); + } + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + + if (report.HasFailures) + AnsiConsole.MarkupLine(Theme.Fail("Doctor found blocking issues.")); + else if (report.HasWarnings) + AnsiConsole.MarkupLine(Theme.Warn("Doctor completed with warnings.")); + else + AnsiConsole.MarkupLine(Theme.Ok("Doctor completed: no issues found.")); + + if (!report.HasFailures && !report.HasWarnings) + return; + + var applyFixes = AnsiConsole.Confirm( + $"[{Theme.Amber}]Apply common autofixes now?[/]", + defaultValue: false); + if (!applyFixes) + return; + + var fixer = new ConfigDoctorAutoFixService(); + var missingDirs = fixer.FindMissingWorkingDirectories(_config, _projectRoot); + if (missingDirs.Count > 0) + { + var createDirs = AnsiConsole.Confirm( + $"[{Theme.Amber}]Create {missingDirs.Count} missing working director{(missingDirs.Count == 1 ? "y" : "ies")}?[/]", + defaultValue: true); + if (createDirs) + { + var dirFix = fixer.CreateMissingWorkingDirectories(missingDirs); + AnsiConsole.MarkupLine(dirFix.Success ? Theme.Ok(dirFix.Message) : Theme.Fail(dirFix.Message)); + } + } + + if (_config.Targets.Count > 0) + { + var migrate = AnsiConsole.Confirm( + $"[{Theme.Amber}]Migrate legacy targets to workflows now?[/]", + defaultValue: true); + if (migrate) + { + var migration = fixer.ApplyLegacyMigration(_projectRoot); + if (migration.Success) + { + AnsiConsole.MarkupLine(Theme.Ok("Legacy migration applied from doctor.")); + if (!string.IsNullOrWhiteSpace(migration.BackupPath)) + AnsiConsole.MarkupLine(Theme.Faint($"Backup: {migration.BackupPath}")); + + var reloaded = ConfigLoader.FindAndLoad(_projectRoot); + if (reloaded is not null) + { + _config = reloaded.Config; + var normalized = WorkflowModelBuilder.Normalize(_config, ResolveLegacyMode(), _requirementResolver); + _workflows = normalized.Workflows.ToList(); + _warnings.Clear(); + _warnings.AddRange(reloaded.Warnings); + _warnings.AddRange(normalized.Warnings); + } + } + else + { + AnsiConsole.MarkupLine(Theme.Fail(migration.Message)); + } + } + } + } + + private void ApplyLegacyMigration() + { + if (_config.Targets.Count == 0) + { + AnsiConsole.MarkupLine(Theme.Warn("No legacy targets found in this config.")); + return; + } + + var configPath = ConfigLoader.FindConfigPath(_projectRoot); + if (string.IsNullOrWhiteSpace(configPath)) + { + AnsiConsole.MarkupLine(Theme.Fail("Could not locate devtool.json to migrate.")); + return; + } + + var confirm = AnsiConsole.Confirm( + $"[{Theme.Amber}]Migrate legacy targets to workflows and overwrite devtool.json (with backup)?[/]", + defaultValue: true); + if (!confirm) + return; + + var result = ConfigLoader.ApplyLegacyTargetMigration(configPath, createBackup: true); + if (!result.Success) + { + AnsiConsole.MarkupLine(Theme.Fail(result.Message)); + return; + } + + var reloaded = ConfigLoader.FindAndLoad(_projectRoot); + if (reloaded is null) + { + AnsiConsole.MarkupLine(Theme.Fail("Migration wrote config, but reload failed.")); + return; + } + + _config = reloaded.Config; + var normalized = WorkflowModelBuilder.Normalize(_config, ResolveLegacyMode(), _requirementResolver); + _workflows = normalized.Workflows.ToList(); + _warnings.Clear(); + _warnings.AddRange(reloaded.Warnings); + _warnings.AddRange(normalized.Warnings); + + AnsiConsole.MarkupLine(Theme.Ok("Migration complete.")); + if (!string.IsNullOrWhiteSpace(result.BackupPath)) + AnsiConsole.MarkupLine(Theme.Faint($"Backup: {result.BackupPath}")); + } + + private void ManageFavorites() + { + if (_workspace is null || _workspaceRoot is null) + { + AnsiConsole.MarkupLine(Theme.Warn("Workspace not loaded. Favorites are workspace-scoped.")); + return; + } + + string projectPath = _workspace.Projects + .FirstOrDefault(p => string.Equals( + WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, p), + _projectRoot, + StringComparison.OrdinalIgnoreCase)) + ?.Path + ?? _projectRoot; + + while (true) + { + AnsiConsole.Clear(); + AnsiConsole.Write(Theme.SectionRule("QUICK ACTIONS")); + AnsiConsole.MarkupLine(Theme.Faint($"Project: {_config.Name}")); + AnsiConsole.MarkupLine(Theme.Faint("Select a workflow to toggle as a workspace quick action.\n")); + + var choices = _workflows + .Select(w => + { + var isFavorite = _workspace.Favorites.Any(f => + string.Equals(WorkspaceLoader.ResolveFavoriteProjectRoot(_workspaceRoot, f), _projectRoot, StringComparison.OrdinalIgnoreCase) && + string.Equals(f.WorkflowId, w.Id, StringComparison.OrdinalIgnoreCase)); + var marker = isFavorite ? "★" : " "; + return new MenuItem( + $"[{Theme.Green}]{marker} {Markup.Escape(w.Label)}[/] [{Theme.GreenDim}]{Markup.Escape(w.Description)}[/]", + w.Id); + }) + .Append(new MenuItem($"[{Theme.Green}]🧹 Prune invalid quick actions[/]", "__prune__")) + .Append(new MenuItem(Theme.Faint("← Back"), "__back__")) + .ToList(); + + var selected = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[{Theme.Green}]Quick action manager:[/]") + .PageSize(24) + .UseConverter(m => m.Display) + .AddChoices(choices)); + + if (selected.Value == "__back__") + return; + + if (selected.Value == "__prune__") + { + var removed = PruneInvalidFavorites(); + if (removed > 0) + { + WorkspaceLoader.Save(_workspaceRoot, _workspace); + AnsiConsole.MarkupLine(Theme.Ok($"Removed {removed} invalid quick action(s).")); + } + else + { + AnsiConsole.MarkupLine(Theme.Faint("No invalid quick actions found.")); + } + + Thread.Sleep(700); + continue; + } + + var workflowId = selected.Value; + var existing = _workspace.Favorites.FirstOrDefault(f => + string.Equals(WorkspaceLoader.ResolveFavoriteProjectRoot(_workspaceRoot, f), _projectRoot, StringComparison.OrdinalIgnoreCase) && + string.Equals(f.WorkflowId, workflowId, StringComparison.OrdinalIgnoreCase)); + + if (existing is not null) + { + _workspace.Favorites.Remove(existing); + WorkspaceLoader.Save(_workspaceRoot, _workspace); + AnsiConsole.MarkupLine(Theme.Warn($"Removed quick action: {workflowId}")); + } + else + { + _workspace.Favorites.Add(new WorkspaceFavorite + { + ProjectPath = projectPath, + WorkflowId = workflowId, + Label = _workflows.First(w => string.Equals(w.Id, workflowId, StringComparison.OrdinalIgnoreCase)).Label + }); + WorkspaceLoader.Save(_workspaceRoot, _workspace); + AnsiConsole.MarkupLine(Theme.Ok($"Added quick action: {workflowId}")); + } + + Thread.Sleep(700); + } + } + + private int PruneInvalidFavorites() + { + if (_workspace is null || _workspaceRoot is null || _workspace.Favorites.Count == 0) + return 0; + + var removed = 0; + for (var i = _workspace.Favorites.Count - 1; i >= 0; i--) + { + var favorite = _workspace.Favorites[i]; + var projectRoot = WorkspaceLoader.ResolveFavoriteProjectRoot(_workspaceRoot, favorite); + var configPath = Path.Combine(projectRoot, "devtool.json"); + if (!File.Exists(configPath)) + { + _workspace.Favorites.RemoveAt(i); + removed++; + continue; + } + + LoadedProjectConfig? loaded = null; + try + { + loaded = ConfigLoader.FindAndLoad(projectRoot); + } + catch + { + // If config can't be loaded, treat this favorite as stale. + } + + var exists = loaded?.Config.Workflows.Any(w => + string.Equals(w.Id, favorite.WorkflowId, StringComparison.OrdinalIgnoreCase)) == true; + + if (!exists) + { + _workspace.Favorites.RemoveAt(i); + removed++; + } + } + + return removed; + } + + private void EditEnvironment() + { + AnsiConsole.Clear(); + + if (_config.Env.Count == 0) + { + AnsiConsole.MarkupLine(Theme.Warn("No environment variables defined in devtool.json.")); + AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to go back...")); + Console.ReadKey(intercept: true); + return; + } + + while (true) + { + AnsiConsole.Write(Theme.SectionRule("ENVIRONMENT")); + + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Variable[/]")) + .AddColumn(new TableColumn($"[{Theme.Amber}]Current Value[/]")) + .AddColumn(new TableColumn($"[{Theme.Amber}]Description[/]")); + + foreach (var def in _config.Env) + { + var val = Environment.GetEnvironmentVariable(def.Key) ?? def.DefaultValue; + table.AddRow( + Theme.Warn(def.Key), + Theme.Bold(val.Length > 0 ? val : "(not set)"), + Theme.Faint(def.Description)); + } + + AnsiConsole.Write(table); + AnsiConsole.MarkupLine(Theme.Faint("Changes apply to this SDT session only.\n")); + + var choices = _config.Env + .Select(e => + { + var curr = Environment.GetEnvironmentVariable(e.Key) ?? e.DefaultValue; + return new MenuItem( + $"[{Theme.Amber}]{Markup.Escape(e.Key)}[/] [{Theme.GreenDim}]= {Markup.Escape(curr)}[/]", + e.Key); + }) + .Append(new MenuItem(Theme.Faint("← Back"), "__back__")) + .ToList(); + + var selected = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[{Theme.Green}]Select a variable to edit:[/]") + .UseConverter(m => m.Display) + .AddChoices(choices)); + + if (selected.Value == "__back__") break; + + var envDef = _config.Env.First(e => e.Key == selected.Value); + var current = Environment.GetEnvironmentVariable(envDef.Key) ?? envDef.DefaultValue; + + string newVal; + if (envDef.Options.Count > 0) + { + newVal = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[{Theme.Amber}]{Markup.Escape(envDef.Key)}[/] [{Theme.GreenDim}]current: {Markup.Escape(current)}[/]") + .AddChoices(envDef.Options)); + } + else + { + newVal = AnsiConsole.Ask( + $"[{Theme.Amber}]{Markup.Escape(envDef.Key)}[/]", current); + } + + Environment.SetEnvironmentVariable(envDef.Key, newVal); + AnsiConsole.MarkupLine("\n" + Theme.Ok($"{envDef.Key} = {newVal}") + "\n"); + Thread.Sleep(500); + AnsiConsole.Clear(); + } + } + + private void SelectEnvProfile() + { + var profiles = _config.EnvProfiles?.Profiles ?? []; + if (profiles.Count == 0) + { + AnsiConsole.MarkupLine(Theme.Warn("No envProfiles configured in devtool.json.")); + return; + } + + var choices = profiles + .Select(p => new MenuItem( + $"[{Theme.Green}]{Markup.Escape(p.Id)}[/] [{Theme.GreenDim}]{Markup.Escape(p.Description)}[/]", + p.Id)) + .Append(new MenuItem(Theme.Faint("Clear profile (none)"), "__none__")) + .ToList(); + + var selected = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[{Theme.Green}]Select active env profile:[/]") + .UseConverter(m => m.Display) + .AddChoices(choices)); + + _activeEnvProfile = selected.Value == "__none__" ? null : selected.Value; + AnsiConsole.MarkupLine(Theme.Ok($"Active env profile: {_activeEnvProfile ?? "(none)"}")); + } + + private IReadOnlyDictionary? ResolveActiveEnvOverrides() + { + var resolved = EnvProfileService.ResolveEffectiveEnv(_config, _activeEnvProfile); + return resolved.Count == 0 ? null : resolved; + } +} diff --git a/src/DevTool.Host.Tui/Tui/EventsScreen.cs b/src/DevTool.Host.Tui/Tui/EventsScreen.cs new file mode 100644 index 0000000..593926e --- /dev/null +++ b/src/DevTool.Host.Tui/Tui/EventsScreen.cs @@ -0,0 +1,179 @@ +using Sdt.Core; +using Spectre.Console; +using Sdt.Config; + +namespace Sdt.Tui; + +public sealed class EventsScreen( + string projectRoot, + string projectName, + string version, + WorkspaceConfig? workspace = null, + string? workspaceRoot = null) +{ + private readonly string _projectRoot = projectRoot; + private readonly string _projectName = projectName; + private readonly string _version = version; + private readonly WorkspaceConfig? _workspace = workspace; + private readonly string? _workspaceRoot = workspaceRoot; + private readonly RunEventLogReader _reader = new(); + + public void Run() + { + while (true) + { + AnsiConsole.Clear(); + RenderHeader("EVENTS"); + var files = _reader.ListEventFiles(_projectRoot); + if (files.Count == 0) + { + AnsiConsole.MarkupLine(Theme.Warn("No event logs found yet.")); + AnsiConsole.MarkupLine(Theme.Faint("Run a workflow/debug action first, then return here.")); + AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to go back...")); + Console.ReadKey(intercept: true); + return; + } + + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]File[/]")) + .AddColumn(new TableColumn($"[{Theme.Amber}]Updated[/]").Width(21)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Size[/]").Width(10)); + foreach (var file in files.Take(12)) + { + table.AddRow( + Theme.G(file.Name), + Theme.Faint(file.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")), + Theme.Faint($"{Math.Max(1, file.SizeBytes / 1024)} KB")); + } + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + + var choices = files + .Take(20) + .Select(f => new MenuItem( + $"[{Theme.Green}]{Markup.Escape(f.Name)}[/] [{Theme.GreenDim}]{f.LastWriteTime:yyyy-MM-dd HH:mm:ss}[/]", + f.Path)) + .ToList(); + choices.Add(new MenuItem(Theme.Faint("← Back"), "__back__")); + + var selected = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[{Theme.Green}]Select an event log to view:[/]") + .PageSize(20) + .UseConverter(m => m.Display) + .AddChoices(choices)); + + if (selected.Value == "__back__") + return; + + ShowSingleFile(selected.Value); + } + } + + public void ShowSingleFile(string filePath) + { + var events = _reader.ReadEvents(filePath); + AnsiConsole.Clear(); + RenderHeader("EVENTS VIEWER"); + AnsiConsole.MarkupLine(Theme.Faint(Path.GetFileName(filePath))); + AnsiConsole.MarkupLine(Theme.Faint(filePath)); + AnsiConsole.WriteLine(); + + if (events.Count == 0) + { + AnsiConsole.MarkupLine(Theme.Warn("No parseable events in selected file.")); + AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to return...")); + Console.ReadKey(intercept: true); + return; + } + + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Time[/]").Width(12)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Type[/]").Width(26)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Message[/]")) + .AddColumn(new TableColumn($"[{Theme.Amber}]Status[/]").Width(10)); + + foreach (var evt in events.TakeLast(120)) + { + var status = evt.Success switch + { + true => Theme.Ok("ok"), + false => Theme.Fail("fail"), + null => Theme.Faint("-"), + }; + var message = evt.Message; + if (!string.IsNullOrWhiteSpace(evt.Tool)) + message += $" [{evt.Tool}]"; + if (evt.ExitCode is not null) + message += $" (exit {evt.ExitCode})"; + + table.AddRow( + Theme.Faint(evt.OccurredAt.ToString("HH:mm:ss")), + Theme.G(evt.Type.ToString()), + Theme.Faint(message), + status); + } + + AnsiConsole.Write(table); + + var workflowId = events.FirstOrDefault(e => e.Type == RunEventType.WorkflowStarted)?.WorkflowId; + if (!string.IsNullOrWhiteSpace(workflowId) && _workspace is not null && _workspaceRoot is not null) + { + AnsiConsole.WriteLine(); + var pin = AnsiConsole.Confirm( + $"[{Theme.Amber}]Pin workflow '{Markup.Escape(workflowId!)}' as a quick action?[/]", + defaultValue: false); + if (pin) + { + AddFavoriteFromEvent(workflowId!); + } + } + + AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to return...")); + Console.ReadKey(intercept: true); + } + + private void AddFavoriteFromEvent(string workflowId) + { + if (_workspace is null || _workspaceRoot is null) + return; + + var projectEntry = _workspace.Projects.FirstOrDefault(p => + string.Equals( + WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, p), + _projectRoot, + StringComparison.OrdinalIgnoreCase)); + var projectPath = projectEntry?.Path ?? _projectRoot; + + var exists = _workspace.Favorites.Any(f => + string.Equals(WorkspaceLoader.ResolveFavoriteProjectRoot(_workspaceRoot, f), _projectRoot, StringComparison.OrdinalIgnoreCase) && + string.Equals(f.WorkflowId, workflowId, StringComparison.OrdinalIgnoreCase)); + if (exists) + { + AnsiConsole.MarkupLine(Theme.Faint("Quick action already pinned.")); + return; + } + + _workspace.Favorites.Add(new WorkspaceFavorite + { + ProjectPath = projectPath, + WorkflowId = workflowId, + Label = workflowId + }); + WorkspaceLoader.Save(_workspaceRoot, _workspace); + AnsiConsole.MarkupLine(Theme.Ok($"Pinned quick action: {workflowId}")); + } + + private void RenderHeader(string section) + { + AnsiConsole.Write(new FigletText("SDT").Color(Theme.GreenColor)); + AnsiConsole.Write( + new Rule($"[bold {Theme.GreenBold}]{Markup.Escape(_projectName)}[/] [{Theme.GreenDim}]v{Markup.Escape(_version)}[/] [{Theme.Amber}]{Markup.Escape(section)}[/]") + .RuleStyle(Theme.DimStyle)); + AnsiConsole.MarkupLine(Theme.Faint($"root: {_projectRoot}") + "\n"); + } +} diff --git a/src/DevTool.Host.Tui/Tui/KeybindingsScreen.cs b/src/DevTool.Host.Tui/Tui/KeybindingsScreen.cs new file mode 100644 index 0000000..d4f4d86 --- /dev/null +++ b/src/DevTool.Host.Tui/Tui/KeybindingsScreen.cs @@ -0,0 +1,40 @@ +using Spectre.Console; + +namespace Sdt.Tui; + +public sealed class KeybindingsScreen(string projectRoot, string projectName, string version) +{ + private readonly string _projectRoot = projectRoot; + private readonly string _projectName = projectName; + private readonly string _version = version; + + public void Run() + { + AnsiConsole.Clear(); + AnsiConsole.Write(new FigletText("SDT").Color(Theme.GreenColor)); + AnsiConsole.Write( + new Rule($"[bold {Theme.GreenBold}]{Markup.Escape(_projectName)}[/] [{Theme.GreenDim}]v{Markup.Escape(_version)}[/] [{Theme.Amber}]KEYBINDINGS[/]") + .RuleStyle(Theme.DimStyle)); + AnsiConsole.MarkupLine(Theme.Faint($"root: {_projectRoot}") + "\n"); + + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Action[/]").Width(28)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Shortcut[/]").Width(20)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Notes[/]")); + + table.AddRow(Theme.G("Command palette"), Theme.Bold("Ctrl+K"), Theme.Faint("Quick run/debug/switch actions.")); + table.AddRow(Theme.G("Keybinding help"), Theme.Bold("? / F1"), Theme.Faint("Open this help screen.")); + table.AddRow(Theme.G("Quit menu"), Theme.Bold("Select Quit"), Theme.Faint("Consistent across all OS terminals.")); + table.AddRow(Theme.G("Menu search"), Theme.Bold("Type to search"), Theme.Faint("Selection prompts support filtering.")); + table.AddRow(Theme.G("Scroll list"), Theme.Bold("Arrows/PgUp/PgDn"), Theme.Faint("Terminal-native navigation.")); + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine(Theme.Faint("Normalization policy: SDT uses the same shortcut labels and menu paths across Windows/Linux/macOS terminals.")); + AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to return...")); + Console.ReadKey(intercept: true); + } +} + diff --git a/src/DevTool.Host.Tui/Tui/RunHistoryScreen.cs b/src/DevTool.Host.Tui/Tui/RunHistoryScreen.cs new file mode 100644 index 0000000..c91ede4 --- /dev/null +++ b/src/DevTool.Host.Tui/Tui/RunHistoryScreen.cs @@ -0,0 +1,142 @@ +using Sdt.Core; +using Spectre.Console; + +namespace Sdt.Tui; + +public sealed record RunHistoryAction( + string Kind, + string? ProjectRoot, + string? TargetId, + string? Category); + +public sealed class RunHistoryScreen(string projectRoot, string projectName, string version) +{ + private readonly string _projectRoot = projectRoot; + private readonly string _projectName = projectName; + private readonly string _version = version; + private readonly RunEventLogReader _reader = new(); + + public RunHistoryAction? Run() + { + while (true) + { + AnsiConsole.Clear(); + RenderHeader("RUN HISTORY"); + var history = _reader.ListRunHistory(_projectRoot, 60); + if (history.Count == 0) + { + AnsiConsole.MarkupLine(Theme.Warn("No run history entries found.")); + AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to return...")); + Console.ReadKey(intercept: true); + return null; + } + + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Time[/]").Width(20)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Category[/]").Width(10)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Target[/]").Width(24)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Status[/]").Width(12)); + + foreach (var item in history.Take(20)) + { + var status = item.Success switch + { + true => Theme.Ok("ok"), + false => Theme.Fail("fail"), + _ => Theme.Faint("unknown") + }; + table.AddRow( + Theme.Faint(item.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")), + Theme.G(item.Category), + Theme.Faint(item.TargetId ?? "(unknown)"), + status); + } + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + + var choices = history + .Take(40) + .Select(h => new MenuItem( + $"[{Theme.Green}]{h.LastWriteTime:yyyy-MM-dd HH:mm:ss}[/] [{Theme.GreenDim}]{Markup.Escape(h.Category)}:{Markup.Escape(h.TargetId ?? "(unknown)")}[/]", + h.FilePath)) + .ToList(); + choices.Add(new MenuItem(Theme.Faint("← Back"), "__back__")); + + var selected = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[{Theme.Green}]Select a run history item:[/]") + .PageSize(20) + .UseConverter(m => m.Display) + .AddChoices(choices)); + + if (selected.Value == "__back__") + return null; + + var itemSelected = history.FirstOrDefault(h => h.FilePath == selected.Value); + if (itemSelected is null) + continue; + + var action = ShowRunHistoryActions(itemSelected); + if (action is not null) + return action; + } + } + + private RunHistoryAction? ShowRunHistoryActions(RunHistoryItem item) + { + var actions = new List + { + new($"[{Theme.Green}]Open event log[/]", "__open__"), + }; + + if (!string.IsNullOrWhiteSpace(item.TargetId) && + (string.Equals(item.Category, "workflow", StringComparison.OrdinalIgnoreCase) || + string.Equals(item.Category, "debug", StringComparison.OrdinalIgnoreCase))) + { + actions.Add(new MenuItem( + $"[{Theme.Green}]Re-run this {Markup.Escape(item.Category)} target[/]", + "__rerun__")); + } + + actions.Add(new MenuItem(Theme.Faint("← Back"), "__back__")); + + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[{Theme.Green}]History action:[/]") + .UseConverter(m => m.Display) + .AddChoices(actions)); + + if (choice.Value == "__back__") + return null; + + if (choice.Value == "__open__") + { + new EventsScreen(_projectRoot, _projectName, _version).ShowSingleFile(item.FilePath); + return null; + } + + if (choice.Value == "__rerun__") + { + return new RunHistoryAction( + Kind: "rerun", + ProjectRoot: item.ProjectRoot ?? _projectRoot, + TargetId: item.TargetId, + Category: item.Category); + } + + return null; + } + + private void RenderHeader(string section) + { + AnsiConsole.Write(new FigletText("SDT").Color(Theme.GreenColor)); + AnsiConsole.Write( + new Rule($"[bold {Theme.GreenBold}]{Markup.Escape(_projectName)}[/] [{Theme.GreenDim}]v{Markup.Escape(_version)}[/] [{Theme.Amber}]{Markup.Escape(section)}[/]") + .RuleStyle(Theme.DimStyle)); + AnsiConsole.MarkupLine(Theme.Faint($"root: {_projectRoot}") + "\n"); + } +} + diff --git a/src/DevTool.Host.Tui/Tui/SetupWizardScreen.cs b/src/DevTool.Host.Tui/Tui/SetupWizardScreen.cs new file mode 100644 index 0000000..90e7de5 --- /dev/null +++ b/src/DevTool.Host.Tui/Tui/SetupWizardScreen.cs @@ -0,0 +1,197 @@ +using Sdt.Config; +using Sdt.Core; +using Spectre.Console; + +namespace Sdt.Tui; + +public sealed class SetupWizardScreen +{ + public async Task RunAsync(DevToolConfig config, string projectRoot) + { + var nonInteractive = RuntimePolicy.IsNonInteractive(); + var runId = Guid.NewGuid().ToString("N"); + using var eventRecorder = RunEventJsonlRecorder.Create(projectRoot, "setup", runId, null); + void Emit(RunEvent evt) => eventRecorder.Write(evt with + { + RunId = runId, + ProjectRoot = projectRoot, + RunEventVersion = "1.0", + }); + + AnsiConsole.Clear(); + AnsiConsole.Write(new FigletText("SDT").Color(Theme.GreenColor)); + AnsiConsole.Write(new Rule($"[bold {Theme.GreenBold}]Setup Wizard[/]").RuleStyle(Theme.DimStyle)); + AnsiConsole.MarkupLine(Theme.Faint($"root: {projectRoot}") + "\n"); + RenderLifecycleModel(); + Emit(new RunEvent("setup", RunEventType.WorkflowStarted, "Setup wizard started.")); + Emit(new RunEvent("setup", RunEventType.WorkflowPlanned, "Setup lifecycle initialized.")); + + AnsiConsole.MarkupLine(Theme.G("Step 1/4: Running config doctor...")); + Emit(new RunEvent("setup", RunEventType.WorkflowStepStarted, "Running config doctor.", StepId: "setup:doctor")); + var doctor = new ConfigDoctorService(new ToolProbeService(), new RequirementResolver()); + var report = await doctor.RunAsync(config, projectRoot); + var failCount = report.Checks.Count(c => c.Status == DoctorStatus.Fail); + var warnCount = report.Checks.Count(c => c.Status == DoctorStatus.Warn); + AnsiConsole.MarkupLine(Theme.Faint($"Doctor summary: {failCount} fail, {warnCount} warn, {report.Checks.Count} total checks.")); + Emit(new RunEvent("setup", RunEventType.WorkflowStepCompleted, "Config doctor completed.", StepId: "setup:doctor", Success: failCount == 0, ExitCode: failCount)); + + var fixer = new ConfigDoctorAutoFixService(); + var toolchainManager = new ToolchainManagerService(new ToolProbeService(), new PrereqInstallerService()); + var configUpdater = new SetupWizardConfigService(new RequirementResolver()); + if (failCount > 0 || warnCount > 0) + { + var applyFixes = nonInteractive + ? true + : AnsiConsole.Confirm( + $"[{Theme.Amber}]Step 2/4: Apply common autofixes now?[/]", + defaultValue: true); + if (applyFixes) + { + var missingDirs = fixer.FindMissingWorkingDirectories(config, projectRoot); + if (missingDirs.Count > 0) + { + var dirFix = fixer.CreateMissingWorkingDirectories(missingDirs); + AnsiConsole.MarkupLine(dirFix.Success ? Theme.Ok(dirFix.Message) : Theme.Fail(dirFix.Message)); + } + + if (config.Targets.Count > 0) + { + var migrate = nonInteractive + ? true + : AnsiConsole.Confirm( + $"[{Theme.Amber}]Migrate legacy targets to workflows now?[/]", + defaultValue: true); + if (migrate) + { + var migration = fixer.ApplyLegacyMigration(projectRoot); + if (migration.Success) + { + AnsiConsole.MarkupLine(Theme.Ok("Legacy migration applied.")); + if (!string.IsNullOrWhiteSpace(migration.BackupPath)) + AnsiConsole.MarkupLine(Theme.Faint($"Backup: {migration.BackupPath}")); + } + else + { + AnsiConsole.MarkupLine(Theme.Fail(migration.Message)); + } + } + } + } + } + else + { + AnsiConsole.MarkupLine(Theme.Ok("Step 2/4: No autofixes needed.")); + } + + AnsiConsole.MarkupLine("\n" + Theme.G("Step 3/4: Checking configured toolchains for missing tools...")); + Emit(new RunEvent("setup", RunEventType.WorkflowStepStarted, "Checking configured toolchains.", StepId: "setup:toolchains")); + var missingToolFixes = await toolchainManager.AutoFixMissingToolsAsync( + config, + projectRoot, + confirmInstallAsync: ConfirmInstallAsync, + onOutput: (line, isErr) => + { + var escaped = Markup.Escape(line); + AnsiConsole.MarkupLine(isErr + ? $"[{Theme.Amber}]{escaped}[/]" + : $"[{Theme.Green}]{escaped}[/]"); + }, + onEvent: Emit); + + if (missingToolFixes.Count == 0) + { + AnsiConsole.MarkupLine(Theme.Ok("No missing configured tools detected.")); + } + else + { + foreach (var fix in missingToolFixes) + AnsiConsole.MarkupLine(fix.Success ? Theme.Ok($"{fix.Tool}: {fix.Message}") : Theme.Warn($"{fix.Tool}: {fix.Message}")); + } + Emit(new RunEvent("setup", RunEventType.WorkflowStepCompleted, "Toolchain check completed.", StepId: "setup:toolchains", Success: missingToolFixes.All(x => x.Success))); + + AnsiConsole.MarkupLine("\n" + Theme.G("Step 4/4: Apply recommended config enhancements...")); + var update = configUpdater.ApplyRecommendedDefaults(config); + if (update.Changes.Count == 0) + { + AnsiConsole.MarkupLine(Theme.Ok("No config enhancements needed.")); + } + else + { + var applyConfig = nonInteractive + ? true + : AnsiConsole.Confirm( + $"[{Theme.Amber}]Apply {update.Changes.Count} recommended config update(s) to devtool.json?[/]", + defaultValue: true); + if (applyConfig) + { + var saveResult = SaveConfig(projectRoot, update.Config); + if (!saveResult.Success) + AnsiConsole.MarkupLine(Theme.Fail(saveResult.Message)); + else + { + foreach (var change in update.Changes) + AnsiConsole.MarkupLine(Theme.Faint($"- {change}")); + if (!string.IsNullOrWhiteSpace(saveResult.BackupPath)) + AnsiConsole.MarkupLine(Theme.Faint($"Backup: {saveResult.BackupPath}")); + } + } + } + + var openToolchain = nonInteractive + ? false + : AnsiConsole.Confirm( + $"[{Theme.Amber}]Open toolchain manager now?[/]", + defaultValue: false); + if (openToolchain && config.Toolchains is not null) + await new ToolchainScreen(config, projectRoot).RunAsync(); + + SetupStateService.MarkCompleted(projectRoot, config.Version); + AnsiConsole.MarkupLine("\n" + Theme.Ok("Setup wizard completed.")); + Emit(new RunEvent("setup", RunEventType.WorkflowCompleted, "Setup wizard completed.", Success: true)); + return true; + } + + private static Task ConfirmInstallAsync(string tool, InstallPlan plan) + { + if (RuntimePolicy.IsNonInteractive()) + { + AnsiConsole.MarkupLine(Theme.Warn($"Non-interactive mode enabled; declining install prompt for '{tool}'.")); + return Task.FromResult(false); + } + + AnsiConsole.MarkupLine(Theme.Warn($"Missing prerequisite: {tool}")); + AnsiConsole.MarkupLine(Theme.Faint(plan.Summary)); + foreach (var cmd in plan.Commands) + AnsiConsole.MarkupLine(Theme.Faint($" $ {cmd.Command} {string.Join(" ", cmd.Args)}")); + + var allow = AnsiConsole.Confirm( + $"[{Theme.Amber}]Run install commands for {Markup.Escape(tool)} now?[/]", + defaultValue: false); + return Task.FromResult(allow); + } + + private static LegacyMigrationApplyResult SaveConfig(string projectRoot, DevToolConfig config) + { + try + { + var path = ConfigLoader.FindConfigPath(projectRoot); + if (string.IsNullOrWhiteSpace(path)) + return new LegacyMigrationApplyResult(false, "Could not find devtool.json for saving."); + + var backup = path + $".bak-{DateTimeOffset.Now:yyyyMMdd-HHmmss}"; + File.Copy(path, backup, overwrite: false); + File.WriteAllText(path, ConfigBootstrapper.ToJson(config)); + return new LegacyMigrationApplyResult(true, "Saved updated devtool.json.", BackupPath: backup, ConfigPath: path); + } + catch (Exception ex) + { + return new LegacyMigrationApplyResult(false, ex.Message); + } + } + + private static void RenderLifecycleModel() + { + AnsiConsole.MarkupLine(Theme.Faint("Lifecycle: Plan -> Probe -> Prompt -> Execute -> Diagnose")); + AnsiConsole.WriteLine(); + } +} diff --git a/src/DevTool.Host.Tui/Tui/Theme.cs b/src/DevTool.Host.Tui/Tui/Theme.cs new file mode 100644 index 0000000..19c85d9 --- /dev/null +++ b/src/DevTool.Host.Tui/Tui/Theme.cs @@ -0,0 +1,70 @@ +using Spectre.Console; + +namespace Sdt.Tui; + +/// +/// SDT phosphor-green colour palette. +/// Primary text is classic terminal phosphor (#00FF41). +/// Modern accent colours are kept for highlights and status. +/// +public static class Theme +{ + private static readonly bool NoColorRequested = + !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("NO_COLOR")) || + !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SDT_NO_COLOR")); + + private static readonly bool NoUnicodeRequested = + !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SDT_NO_UNICODE")); + + public static bool SupportsUnicode => !NoUnicodeRequested && AnsiConsole.Profile.Capabilities.Unicode; + public static bool SupportsColor => !NoColorRequested && AnsiConsole.Profile.Capabilities.Ansi; + + // ── Hex colour constants (use in Spectre markup strings) ───────────────── + public const string Green = "#00ff41"; // primary phosphor — all normal text + public const string GreenDim = "#005c1b"; // muted — borders, secondary info + public const string GreenBold = "#a8ff90"; // bright — selections, emphasis + public const string Amber = "#ffb300"; // warnings / group titles + public const string Red = "#ff4040"; // errors + public const string Ghost = "#003d12"; // near-invisible — decorative scanlines + + // ── Spectre Color instances (for FigletText, Rule styles, etc.) ────────── + public static readonly Color GreenColor = new(0, 255, 65); + public static readonly Color GreenDimColor = new(0, 92, 27); + public static readonly Color GreenBoldColor = new(168, 255, 144); + public static readonly Color AmberColor = new(255, 179, 0); + public static readonly Color RedColor = new(255, 64, 64); + + // ── Pre-built Style objects ─────────────────────────────────────────────── + public static readonly Style PrimaryStyle = new(GreenColor); + public static readonly Style DimStyle = new(GreenDimColor); + public static readonly Style BrightStyle = new(GreenBoldColor, decoration: Decoration.Bold); + public static readonly Style AmberStyle = new(AmberColor); + public static readonly Style RedStyle = new(RedColor, decoration: Decoration.Bold); + + // ── Markup helper methods (auto-escape user content) ───────────────────── + public static string G(string t) => SupportsColor ? $"[{Green}]{Markup.Escape(t)}[/]" : Markup.Escape(t); + public static string Faint(string t) => SupportsColor ? $"[{GreenDim}]{Markup.Escape(t)}[/]" : Markup.Escape(t); + public static string Bold(string t) => SupportsColor ? $"[bold {GreenBold}]{Markup.Escape(t)}[/]" : Markup.Escape(t); + public static string Warn(string t) => SupportsColor ? $"[{Amber}]{Markup.Escape(t)}[/]" : Markup.Escape(t); + public static string Err(string t) => SupportsColor ? $"[bold {Red}]{Markup.Escape(t)}[/]" : Markup.Escape(t); + public static string Ok(string t) => SupportsColor + ? $"[bold {Green}]{Glyph("✓", "OK")} {Markup.Escape(t)}[/]" + : $"{Glyph("✓", "OK")} {Markup.Escape(t)}"; + public static string Fail(string t) => SupportsColor + ? $"[bold {Red}]{Glyph("✗", "FAIL")} {Markup.Escape(t)}[/]" + : $"{Glyph("✗", "FAIL")} {Markup.Escape(t)}"; + + public static string Glyph(string unicode, string ascii) => SupportsUnicode ? unicode : ascii; + + // ── Shared UI components ────────────────────────────────────────────────── + public static Rule SectionRule(string? title = null) => title is null + ? new Rule().RuleStyle(DimStyle) + : new Rule($"[bold {GreenBold}]{Markup.Escape(title)}[/]").RuleStyle(DimStyle); + + public static Rule DimRule() => new Rule().RuleStyle(new Style(new Color(0, 40, 12))); + + public static Panel StatusPanel(string markup) => + new Panel(markup) + .BorderStyle(DimStyle) + .Padding(1, 0); +} diff --git a/src/DevTool.Host.Tui/Tui/ToolchainScreen.cs b/src/DevTool.Host.Tui/Tui/ToolchainScreen.cs new file mode 100644 index 0000000..0477532 --- /dev/null +++ b/src/DevTool.Host.Tui/Tui/ToolchainScreen.cs @@ -0,0 +1,439 @@ +using Sdt.Config; +using Sdt.Core; +using Sdt.Runner; +using Sdt.Tui; +using Spectre.Console; + +namespace Sdt.Tui; + +public sealed class ToolchainScreen(DevToolConfig config, string projectRoot) +{ + private readonly DevToolConfig _config = config; + private readonly string _projectRoot = projectRoot; + private readonly ToolchainManagerService _toolchainService = new(new ToolProbeService(), new PrereqInstallerService()); + + public async Task RunAsync() + { + while (true) + { + AnsiConsole.Clear(); + AnsiConsole.Write(Theme.SectionRule("TOOLCHAINS")); + AnsiConsole.WriteLine(); + + var tc = _config.Toolchains; + if (tc is null) + { + AnsiConsole.MarkupLine(Theme.Warn("No toolchains configured in devtool.json.")); + AnsiConsole.MarkupLine(Theme.Faint("Add a \"toolchains\" section with \"python\" and/or \"node\" entries.")); + AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to go back...")); + Console.ReadKey(intercept: true); + return; + } + + // Build menu from available toolchains + var choices = new List(); + + if (tc.Python is not null) + { + choices.Add(new MenuItem($"[bold {Theme.GreenBold}]PYTHON[/]", "__group__")); + choices.Add(new MenuItem($"{Theme.G("Check environment")} {Theme.Faint("detect python, venv, pip")}", "py:check")); + choices.Add(new MenuItem($"{Theme.G("Create / recreate venv")} {Theme.Faint($"python -m venv {tc.Python.VenvDir}")}", "py:venv")); + if (tc.Python.Profiles.Count > 0) + choices.Add(new MenuItem($"{Theme.G("Install requirements profile")} {Theme.Faint("select cpu / gpu / nlp...")}", "py:install")); + choices.Add(new MenuItem($"{Theme.G("Upgrade pip")} {Theme.Faint("pip install --upgrade pip")}", "py:upgradepip")); + } + + if (tc.Node is not null) + { + choices.Add(new MenuItem($"[bold {Theme.GreenBold}]NODE / NPM[/]", "__group__")); + choices.Add(new MenuItem($"{Theme.G("Check environment")} {Theme.Faint("detect node, npm, node_modules")}", "node:check")); + choices.Add(new MenuItem($"{Theme.G($"{tc.Node.PackageManager} install")} {Theme.Faint($"in {tc.Node.WorkingDir}")}", "node:install")); + } + + choices.Add(new MenuItem($"[bold {Theme.GreenBold}]GENERAL[/]", "__group__")); + choices.Add(new MenuItem($"{Theme.G("Toolchain doctor")} {Theme.Faint("probe configured tools + resolver details")}", "general:doctor")); + choices.Add(new MenuItem($"{Theme.G("Auto-fix missing tools")} {Theme.Faint("prompted install plans + verify")}", "general:autofix")); + + choices.Add(new MenuItem($"[bold {Theme.GreenBold}]──[/]", "__group__")); + choices.Add(new MenuItem(Theme.Faint("← Back"), "__back__")); + + var prompt = new SelectionPrompt() + .Title($"[{Theme.Green}]Select a toolchain action:[/]") + .PageSize(20) + .UseConverter(m => m.Display) + .AddChoices(choices); + + var selected = AnsiConsole.Prompt(prompt); + if (selected.Value == "__back__" || selected.Value == "__group__") return; + + AnsiConsole.Clear(); + AnsiConsole.Write(Theme.SectionRule(selected.Value.Split(':')[0].ToUpperInvariant() + " › " + selected.Value.Split(':')[1])); + AnsiConsole.WriteLine(); + + await HandleActionAsync(selected.Value, tc); + + AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to continue...")); + Console.ReadKey(intercept: true); + } + } + + // ── Actions ─────────────────────────────────────────────────────────────── + + private async Task HandleActionAsync(string action, ToolchainConfig tc) + { + switch (action) + { + case "py:check": await CheckPythonAsync(tc.Python!); break; + case "py:venv": await CreateVenvAsync(tc.Python!); break; + case "py:install": await InstallProfileAsync(tc.Python!); break; + case "py:upgradepip": await UpgradePipAsync(tc.Python!); break; + case "node:check": await CheckNodeAsync(tc.Node!); break; + case "node:install": await NodeInstallAsync(tc.Node!); break; + case "general:doctor": await RunToolchainDoctorAsync(); break; + case "general:autofix": await AutoFixToolchainsAsync(); break; + } + } + + // ── Python ──────────────────────────────────────────────────────────────── + + private async Task CheckPythonAsync(PythonToolchain py) + { + var exe = ResolvePythonExe(py); + var venvPath = Path.GetFullPath(Path.Combine(_projectRoot, py.VenvDir)); + var venvPython = GetVenvPython(venvPath); + + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Check[/]").Width(24)) + .AddColumn(new TableColumn($"[{Theme.GreenBold}]Result[/]")); + + // System Python + var pyProbe = await new ToolProbeService().ProbeAsync("python", _projectRoot, _config); + var pyVersion = pyProbe.IsAvailable ? (pyProbe.Version ?? "available") : null; + table.AddRow(Theme.G("System Python"), pyVersion is not null + ? Theme.Ok(pyVersion.Trim()) + : Theme.Fail($"{exe} not found")); + if (!string.IsNullOrWhiteSpace(pyProbe.Details)) + table.AddRow(Theme.Faint("Resolver"), Theme.Faint(pyProbe.Details)); + + // Venv exists? + table.AddRow(Theme.G($"Venv ({py.VenvDir})"), Directory.Exists(venvPath) + ? Theme.Ok("exists " + venvPath) + : Theme.Warn("not found — use 'Create venv'")); + + // Venv Python + if (File.Exists(venvPython)) + { + var venvVersion = await ProbeAsync(venvPython, "--version"); + table.AddRow(Theme.G("Venv Python"), venvVersion is not null + ? Theme.Ok(venvVersion.Trim()) + : Theme.Fail("could not launch")); + } + + // Pip in venv + if (File.Exists(venvPython)) + { + var pipVersion = await ProbeAsync(venvPython, "-m", "pip", "--version"); + table.AddRow(Theme.G("Pip (venv)"), pipVersion is not null + ? Theme.Ok(pipVersion.Trim()) + : Theme.Fail("pip not available")); + } + + AnsiConsole.Write(table); + } + + private async Task CreateVenvAsync(PythonToolchain py) + { + var exe = ResolvePythonExe(py); + var venvDir = py.VenvDir; + var venvPath = Path.GetFullPath(Path.Combine(_projectRoot, venvDir)); + + if (Directory.Exists(venvPath)) + { + var overwrite = AnsiConsole.Confirm( + $"[{Theme.Amber}]Venv already exists at {venvDir}. Recreate it?[/]", defaultValue: false); + if (!overwrite) return; + Directory.Delete(venvPath, recursive: true); + } + + AnsiConsole.MarkupLine(Theme.G($"Creating venv: {exe} -m venv {venvDir}")); + AnsiConsole.WriteLine(); + + await RunLiveAsync(exe, ["-m", "venv", venvDir], _projectRoot); + } + + private async Task InstallProfileAsync(PythonToolchain py) + { + var venvPath = Path.GetFullPath(Path.Combine(_projectRoot, py.VenvDir)); + var venvPy = GetVenvPython(venvPath); + + if (!File.Exists(venvPy)) + { + AnsiConsole.MarkupLine(Theme.Warn("Venv not found. Create it first.")); + return; + } + + var profile = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[{Theme.Green}]Select requirements profile:[/]") + .UseConverter(p => $"{Theme.Bold(p.Label)} {Theme.Faint(p.RequirementsFile)}") + .AddChoices(py.Profiles)); + + var reqFile = Path.GetFullPath(Path.Combine(_projectRoot, profile.RequirementsFile)); + if (!File.Exists(reqFile)) + { + AnsiConsole.MarkupLine(Theme.Fail($"Requirements file not found: {reqFile}")); + return; + } + + // Upgrade pip first + AnsiConsole.MarkupLine(Theme.Faint("Upgrading pip...")); + await RunPipAsync(py, venvPy, ["install", "--upgrade", "pip"]); + + // Build install args + var installArgs = new List { "-m", "pip", "install" }; + if (!string.IsNullOrWhiteSpace(profile.ExtraIndexUrl)) + { + installArgs.Add("--extra-index-url"); + installArgs.Add(profile.ExtraIndexUrl); + } + installArgs.Add("-r"); + installArgs.Add(reqFile); + + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine(Theme.G($"Installing {profile.Label}...")); + AnsiConsole.WriteLine(); + await RunPipAsync(py, venvPy, installArgs.Skip(2)); // strip leading "-m pip" + + // Post-install commands + foreach (var cmd in profile.PostInstallCommands) + { + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine(Theme.Faint($"Post-install: {cmd}")); + var parts = cmd.Split(' ', 2); + var postArgs = parts.Length > 1 ? parts[1].Split(' ') : Array.Empty(); + await RunLiveAsync(venvPy, ["-m", .. postArgs], _projectRoot); + } + } + + private async Task UpgradePipAsync(PythonToolchain py) + { + var venvPath = Path.GetFullPath(Path.Combine(_projectRoot, py.VenvDir)); + var venvPy = GetVenvPython(venvPath); + var exe = File.Exists(venvPy) ? venvPy : ResolvePythonExe(py); + AnsiConsole.MarkupLine(Theme.G($"Upgrading pip using: {exe}")); + AnsiConsole.WriteLine(); + await RunPipAsync(py, exe, ["install", "--upgrade", "pip"]); + } + + // ── Node ────────────────────────────────────────────────────────────────── + + private async Task CheckNodeAsync(NodeToolchain node) + { + var nodeModules = Path.GetFullPath( + Path.Combine(_projectRoot, node.WorkingDir, "node_modules")); + + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Check[/]").Width(24)) + .AddColumn(new TableColumn($"[{Theme.GreenBold}]Result[/]")); + + var probeService = new ToolProbeService(); + var nodeProbe = await probeService.ProbeAsync("node", _projectRoot, _config); + var npmProbe = await probeService.ProbeAsync(node.PackageManager, _projectRoot, _config); + var nodeVersion = nodeProbe.IsAvailable ? nodeProbe.Version : null; + table.AddRow(Theme.G("Node.js"), nodeVersion is not null + ? Theme.Ok(nodeVersion.Trim()) + : Theme.Fail("node not found in PATH")); + + var npmVersion = npmProbe.IsAvailable ? npmProbe.Version : null; + table.AddRow(Theme.G(node.PackageManager), npmVersion is not null + ? Theme.Ok(npmVersion.Trim()) + : Theme.Fail($"{node.PackageManager} not found in PATH")); + + if (!string.IsNullOrWhiteSpace(nodeProbe.Details)) + table.AddRow(Theme.Faint("Node resolver"), Theme.Faint(nodeProbe.Details)); + if (!string.IsNullOrWhiteSpace(npmProbe.Details)) + table.AddRow(Theme.Faint($"{node.PackageManager} resolver"), Theme.Faint(npmProbe.Details)); + + table.AddRow(Theme.G("node_modules"), Directory.Exists(nodeModules) + ? Theme.Ok("exists") + : Theme.Warn($"not found — run {node.PackageManager} install")); + + AnsiConsole.Write(table); + } + + private async Task NodeInstallAsync(NodeToolchain node) + { + var workDir = Path.GetFullPath(Path.Combine(_projectRoot, node.WorkingDir)); + AnsiConsole.MarkupLine(Theme.G($"{node.PackageManager} install ({workDir})")); + AnsiConsole.WriteLine(); + await RunLiveAsync(node.PackageManager, ["install"], workDir); + } + + private async Task RunToolchainDoctorAsync() + { + var probes = await _toolchainService.ProbeConfiguredToolsAsync(_config, _projectRoot); + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Tool[/]").Width(12)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Result[/]").Width(16)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Version[/]").Width(24)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Resolver Detail[/]")); + + foreach (var probe in probes) + { + table.AddRow( + Theme.G(probe.Tool), + probe.IsAvailable ? Theme.Ok("available") : Theme.Fail("missing"), + Theme.Faint(probe.Version ?? "-"), + Theme.Faint(probe.Details ?? "-")); + } + + AnsiConsole.Write(table); + } + + private async Task AutoFixToolchainsAsync() + { + var results = await _toolchainService.AutoFixMissingToolsAsync( + _config, + _projectRoot, + confirmInstallAsync: ConfirmInstallAsync, + onOutput: (line, isErr) => + { + var escaped = Markup.Escape(line); + AnsiConsole.MarkupLine(isErr + ? $"[{Theme.Amber}]{escaped}[/]" + : $"[{Theme.Green}]{escaped}[/]"); + }); + + if (results.Count == 0) + { + AnsiConsole.MarkupLine(Theme.Ok("No missing tools detected.")); + return; + } + + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Tool[/]").Width(12)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Status[/]").Width(10)) + .AddColumn(new TableColumn($"[{Theme.Amber}]Message[/]")); + + foreach (var result in results) + { + table.AddRow( + Theme.G(result.Tool), + result.Success ? Theme.Ok("ok") : Theme.Fail("fail"), + Theme.Faint(result.Message)); + } + + AnsiConsole.Write(table); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private static string ResolvePythonExe(PythonToolchain py) + { + if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(py.WindowsExecutable)) + return py.WindowsExecutable; + return py.Executable; + } + + private static string GetVenvPython(string venvPath) + { + // Windows: .venv\Scripts\python.exe | Linux/Mac: .venv/bin/python + return OperatingSystem.IsWindows() + ? Path.Combine(venvPath, "Scripts", "python.exe") + : Path.Combine(venvPath, "bin", "python"); + } + + private static async Task ProbeAsync(string command, params string[] args) + { + try + { + var psi = new System.Diagnostics.ProcessStartInfo + { + FileName = CommandResolver.Resolve(command), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + foreach (var a in args) psi.ArgumentList.Add(a); + + using var p = new System.Diagnostics.Process { StartInfo = psi }; + p.Start(); + var output = await p.StandardOutput.ReadToEndAsync(); + var err = await p.StandardError.ReadToEndAsync(); + await p.WaitForExitAsync(); + return p.ExitCode == 0 ? (output + err) : null; + } + catch { return null; } + } + + private static async Task RunLiveAsync(string command, IEnumerable args, string workingDir) + { + var result = await ProcessRunner.RunAsync( + command, args, workingDir, + (line, isErr) => AnsiConsole.MarkupLine( + isErr + ? $"[{Theme.Amber}]{Markup.Escape(line)}[/]" + : $"[{Theme.Green}]{Markup.Escape(line)}[/]")); + + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine(result.Success + ? Theme.Ok($"Done ({result.Elapsed.TotalSeconds:F1}s)") + : Theme.Fail($"Exited {result.ExitCode} ({result.Elapsed.TotalSeconds:F1}s)")); + } + + private static Task ConfirmInstallAsync(string tool, InstallPlan plan) + { + AnsiConsole.MarkupLine(Theme.Warn($"Missing prerequisite: {tool}")); + AnsiConsole.MarkupLine(Theme.Faint(plan.Summary)); + foreach (var cmd in plan.Commands) + AnsiConsole.MarkupLine(Theme.Faint($" $ {cmd.Command} {string.Join(" ", cmd.Args)}")); + + var allow = AnsiConsole.Confirm( + $"[{Theme.Amber}]Run install commands for {Markup.Escape(tool)} now?[/]", + defaultValue: false); + return Task.FromResult(allow); + } + + private async Task RunPipAsync(PythonToolchain py, string pythonExe, IEnumerable pipArgs) + { + if (!string.IsNullOrWhiteSpace(py.PipScript)) + { + var pipScriptPath = ResolvePipScriptPath(py.PipScript); + if (File.Exists(pipScriptPath)) + { + var ext = Path.GetExtension(pipScriptPath).ToLowerInvariant(); + if (ext == ".py") + { + await RunLiveAsync(ResolvePythonExe(py), [pipScriptPath, .. pipArgs], _projectRoot); + return; + } + + AnsiConsole.MarkupLine(Theme.Warn($"Ignoring non-Python pipScript: {pipScriptPath}")); + } + } + + await RunLiveAsync(pythonExe, ["-m", "pip", .. pipArgs], _projectRoot); + } + + private string ResolvePipScriptPath(string pipScriptConfigPath) + { + if (Path.IsPathRooted(pipScriptConfigPath)) + return pipScriptConfigPath; + + var fileName = Path.GetFileName(pipScriptConfigPath); + var bundled = ScriptLocator.FindHelperScript(_projectRoot, fileName); + if (bundled is not null) + return bundled; + + return Path.GetFullPath(Path.Combine(_projectRoot, pipScriptConfigPath)); + } +} diff --git a/src/DevTool.Host.Tui/Tui/WorkspaceScreen.cs b/src/DevTool.Host.Tui/Tui/WorkspaceScreen.cs new file mode 100644 index 0000000..29ce6a3 --- /dev/null +++ b/src/DevTool.Host.Tui/Tui/WorkspaceScreen.cs @@ -0,0 +1,335 @@ +using Sdt.Config; +using Spectre.Console; + +namespace Sdt.Tui; + +public sealed class WorkspaceScreen(WorkspaceConfig workspace, string workspaceRoot, string currentProjectRoot) +{ + private readonly WorkspaceConfig _workspace = workspace; + private readonly string _workspaceRoot = workspaceRoot; + private readonly string _currentProjectRoot = currentProjectRoot; + private readonly HashSet _ignoredCandidates = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Shows the project switcher. Returns the absolute path to the selected project root, + /// or null if the user cancelled. + /// + public string? SelectProject() + { + while (true) + { + AnsiConsole.Clear(); + AnsiConsole.Write(Theme.SectionRule("WORKSPACE — " + _workspace.Name)); + AnsiConsole.WriteLine(); + + var projects = _workspace.Projects; + var inventory = WorkspaceLoader.ScanInventory(_workspaceRoot, _currentProjectRoot, _workspace); + var candidates = inventory.Candidates + .Where(c => !_ignoredCandidates.Contains(c.RootPath)) + .ToList(); + + if (projects.Count == 0) + { + AnsiConsole.MarkupLine(Theme.Warn("No configured projects in sdt-workspace.json.")); + if (candidates.Count == 0) + { + AnsiConsole.MarkupLine(Theme.Faint("No inventory candidates found.")); + if (AnsiConsole.Confirm($"[{Theme.Amber}]Add an external project now?[/]", defaultValue: true)) + { + AddExternalProject(); + continue; + } + + AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to go back...")); + Console.ReadKey(intercept: true); + return null; + } + } + + var choices = new List(); + foreach (var proj in projects) + { + var absPath = WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, proj); + var devtoolPath = Path.Combine(absPath, "devtool.json"); + var isCurrent = string.Equals(absPath, _currentProjectRoot, StringComparison.OrdinalIgnoreCase); + var exists = File.Exists(devtoolPath); + + var label = isCurrent + ? $"[bold {Theme.GreenBold}]► {Markup.Escape(proj.Name)}[/] [{Theme.GreenDim}](current)[/]" + : $"[{Theme.Green}] {Markup.Escape(proj.Name)}[/]"; + if (proj.Disabled) + label += $" [{Theme.Amber}](disabled)[/]"; + + var desc = !exists + ? $" [{Theme.Red}]devtool.json not found[/]" + : string.IsNullOrWhiteSpace(proj.Description) + ? $" [{Theme.GreenDim}]{Markup.Escape(absPath)}[/]" + : $" [{Theme.GreenDim}]{Markup.Escape(proj.Description)}[/]"; + if (proj.Tags.Count > 0) + desc += $" [{Theme.GreenDim}]tags: {Markup.Escape(string.Join(",", proj.Tags))}[/]"; + + choices.Add(new WorkspaceMenuItem(label + "\n" + desc, absPath, exists && !isCurrent && !proj.Disabled)); + } + + foreach (var candidate in candidates) + { + var kinds = string.Join("/", candidate.Kinds.Select(k => k.ToString().ToLowerInvariant())); + choices.Add(new WorkspaceMenuItem( + $"[{Theme.Amber}]✚ Add candidate[/] [{Theme.Green}]{Markup.Escape(candidate.DisplayName)}[/]\n" + + $" [{Theme.GreenDim}]{Markup.Escape(candidate.RootPath)}[/] [{Theme.GreenDim}]markers: {Markup.Escape(kinds)}[/]", + $"__candidate_add__:{candidate.RootPath}", + true)); + if (candidate.SuggestedInit) + { + choices.Add(new WorkspaceMenuItem( + $"[{Theme.Amber}]◇ Add + initialize config[/] [{Theme.Green}]{Markup.Escape(candidate.DisplayName)}[/]\n" + + $" [{Theme.GreenDim}]{Markup.Escape(candidate.RootPath)}[/]", + $"__candidate_init__:{candidate.RootPath}", + true)); + } + + choices.Add(new WorkspaceMenuItem( + $"[{Theme.GreenDim}]○ Ignore for this session[/] {Markup.Escape(candidate.DisplayName)}", + $"__candidate_ignore__:{candidate.RootPath}", + true)); + } + + choices.Add(new WorkspaceMenuItem($"[{Theme.Green}]+ Add external project[/]", "__add__", true)); + choices.Add(new WorkspaceMenuItem($"[{Theme.GreenDim}]← Cancel[/]", null, true)); + + RenderConfiguredProjectsTable(projects); + RenderCandidatesTable(candidates); + + var switchable = choices.Where(c => c.Selectable).ToList(); + var selected = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[{Theme.Green}]Switch or manage project:[/]") + .PageSize(20) + .UseConverter(m => m.Display) + .AddChoices(switchable)); + + if (selected.AbsPath == "__add__") + { + AddExternalProject(); + continue; + } + + if (selected.AbsPath is not null && selected.AbsPath.StartsWith("__candidate_add__:", StringComparison.Ordinal)) + { + var candidatePath = selected.AbsPath["__candidate_add__:".Length..]; + AddInventoryCandidate(candidatePath, initializeConfig: false); + continue; + } + + if (selected.AbsPath is not null && selected.AbsPath.StartsWith("__candidate_init__:", StringComparison.Ordinal)) + { + var candidatePath = selected.AbsPath["__candidate_init__:".Length..]; + AddInventoryCandidate(candidatePath, initializeConfig: true); + continue; + } + + if (selected.AbsPath is not null && selected.AbsPath.StartsWith("__candidate_ignore__:", StringComparison.Ordinal)) + { + var candidatePath = selected.AbsPath["__candidate_ignore__:".Length..]; + _ignoredCandidates.Add(candidatePath); + AnsiConsole.MarkupLine(Theme.Faint("Candidate ignored for this session.")); + Thread.Sleep(700); + continue; + } + + return selected.AbsPath; + } + } + + private void RenderConfiguredProjectsTable(IReadOnlyList projects) + { + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Configured Project[/]")) + .AddColumn(new TableColumn($"[{Theme.Amber}]Path[/]")) + .AddColumn(new TableColumn($"[{Theme.Amber}]Status[/]").Width(12)); + + if (projects.Count == 0) + { + table.AddRow(Theme.Faint("(none)"), Theme.Faint("-"), Theme.Warn("empty")); + } + else + { + foreach (var proj in projects) + { + var absPath = WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, proj); + var isCurrent = string.Equals(absPath, _currentProjectRoot, StringComparison.OrdinalIgnoreCase); + var hasConfig = File.Exists(Path.Combine(absPath, "devtool.json")); + var status = proj.Disabled + ? Theme.Warn("disabled") + : hasConfig ? Theme.Ok("ready") : Theme.Fail("no config"); + + table.AddRow( + isCurrent + ? $"[bold {Theme.GreenBold}]► {Markup.Escape(proj.Name)}[/]" + : Theme.G(proj.Name), + Theme.Faint(proj.Path), + status); + } + } + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + } + + private static void RenderCandidatesTable(IReadOnlyList candidates) + { + var table = new Table() + .Border(TableBorder.Rounded) + .BorderStyle(Theme.DimStyle) + .AddColumn(new TableColumn($"[{Theme.Amber}]Candidate[/]")) + .AddColumn(new TableColumn($"[{Theme.Amber}]Path[/]")) + .AddColumn(new TableColumn($"[{Theme.Amber}]Markers[/]")) + .AddColumn(new TableColumn($"[{Theme.Amber}]Init[/]").Width(10)); + + if (candidates.Count == 0) + { + table.AddRow(Theme.Faint("(none)"), Theme.Faint("-"), Theme.Faint("-"), Theme.Ok("no")); + } + else + { + foreach (var candidate in candidates) + { + var kinds = string.Join("/", candidate.Kinds.Select(k => k.ToString().ToLowerInvariant())); + table.AddRow( + Theme.G(candidate.DisplayName), + Theme.Faint(candidate.RootPath), + Theme.Faint(kinds), + candidate.SuggestedInit ? Theme.Warn("suggested") : Theme.Ok("no")); + } + } + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + } + + private void AddExternalProject() + { + var raw = AnsiConsole.Ask($"[{Theme.Amber}]Project root path[/]"); + if (string.IsNullOrWhiteSpace(raw)) + return; + + var absolutePath = Path.GetFullPath(raw.Trim()); + if (!Directory.Exists(absolutePath)) + { + AnsiConsole.MarkupLine(Theme.Fail("Directory does not exist.")); + Thread.Sleep(700); + return; + } + + var configPath = Path.Combine(absolutePath, "devtool.json"); + if (!File.Exists(configPath)) + { + var create = AnsiConsole.Confirm( + $"[{Theme.Amber}]No devtool.json found. Create a minimal template?[/]", + defaultValue: true); + if (!create) + return; + + File.WriteAllText(configPath, "{\n \"name\": \"SDT Project\",\n \"version\": \"0.1.0\",\n \"workflows\": []\n}\n"); + } + + if (_workspace.Projects.Any(p => + string.Equals(WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, p), absolutePath, StringComparison.OrdinalIgnoreCase))) + { + AnsiConsole.MarkupLine(Theme.Warn("Project already exists in workspace.")); + Thread.Sleep(700); + return; + } + + var relativePath = Path.GetRelativePath(_workspaceRoot, absolutePath); + var useRelative = !relativePath.StartsWith("..", StringComparison.OrdinalIgnoreCase) && !Path.IsPathRooted(relativePath); + var projectEntry = new WorkspaceProject + { + Name = new DirectoryInfo(absolutePath).Name, + Description = $"External project at {absolutePath}", + Path = useRelative ? relativePath : absolutePath, + Disabled = false, + DetectedBy = "manual", + LastValidatedUtc = DateTimeOffset.UtcNow, + }; + + _workspace.Projects.Add(projectEntry); + WorkspaceLoader.Save(_workspaceRoot, _workspace); + AnsiConsole.MarkupLine(Theme.Ok("Project added to workspace.")); + Thread.Sleep(700); + } + + private void AddInventoryCandidate(string absolutePath, bool initializeConfig) + { + if (!Directory.Exists(absolutePath)) + { + AnsiConsole.MarkupLine(Theme.Fail("Candidate path no longer exists.")); + Thread.Sleep(700); + return; + } + + var configPath = Path.Combine(absolutePath, "devtool.json"); + if (initializeConfig && !File.Exists(configPath)) + { + try + { + var scan = ConfigBootstrapper.Scan(absolutePath); + var generated = ConfigBootstrapper.BuildDefaultConfig(scan); + ConfigBootstrapper.WriteDefaultConfig(scan.ProjectRoot, generated, overwrite: false); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine(Theme.Fail($"Failed to initialize config: {ex.Message}")); + Thread.Sleep(900); + return; + } + } + + if (!File.Exists(configPath)) + { + AnsiConsole.MarkupLine(Theme.Warn("Candidate has no devtool.json yet. Use Add + initialize.")); + Thread.Sleep(900); + return; + } + + if (_workspace.Projects.Any(p => + string.Equals(WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, p), absolutePath, StringComparison.OrdinalIgnoreCase))) + { + AnsiConsole.MarkupLine(Theme.Warn("Project already exists in workspace.")); + Thread.Sleep(700); + return; + } + + LoadedProjectConfig? loaded = null; + try + { + loaded = ConfigLoader.FindAndLoad(absolutePath); + } + catch + { + // best effort + } + + var relativePath = Path.GetRelativePath(_workspaceRoot, absolutePath); + var useRelative = !relativePath.StartsWith("..", StringComparison.OrdinalIgnoreCase) && !Path.IsPathRooted(relativePath); + var entry = new WorkspaceProject + { + Name = loaded?.Config.Name ?? new DirectoryInfo(absolutePath).Name, + Description = $"Inventory candidate at {absolutePath}", + Path = useRelative ? relativePath : absolutePath, + Disabled = false, + DetectedBy = "inventory", + LastValidatedUtc = DateTimeOffset.UtcNow, + }; + + _workspace.Projects.Add(entry); + WorkspaceLoader.Save(_workspaceRoot, _workspace); + _ignoredCandidates.Remove(absolutePath); + AnsiConsole.MarkupLine(Theme.Ok("Candidate added to workspace.")); + Thread.Sleep(700); + } + + private sealed record WorkspaceMenuItem(string Display, string? AbsPath, bool Selectable); +} diff --git a/src/DevTool.Runtime/Core/CommandResolver.cs b/src/DevTool.Runtime/Core/CommandResolver.cs new file mode 100644 index 0000000..57d940b --- /dev/null +++ b/src/DevTool.Runtime/Core/CommandResolver.cs @@ -0,0 +1,219 @@ +namespace Sdt.Core; + +public enum CommandResolutionSource +{ + Exact, + Path, + Shim, + NodeAdjacentShim, + ConfiguredOverride, + Fallback, +} + +public sealed record CommandResolutionResult( + string Requested, + string Resolved, + CommandResolutionSource Source); + +public static class CommandResolver +{ + public static CommandResolutionResult ResolveWithTrace(string command, object? config = null, string? tool = null) + { + if (string.IsNullOrWhiteSpace(command)) + return new CommandResolutionResult(command, command, CommandResolutionSource.Exact); + + if (!OperatingSystem.IsWindows()) + return new CommandResolutionResult(command, command, CommandResolutionSource.Exact); + + if (command.Contains(Path.DirectorySeparatorChar) || command.Contains(Path.AltDirectorySeparatorChar)) + return new CommandResolutionResult(command, command, CommandResolutionSource.Exact); + + var normalized = command.ToLowerInvariant(); + if (Path.HasExtension(command)) + { + var extensionResolved = ResolveFromPath(command); + return extensionResolved is null + ? new CommandResolutionResult(command, command, CommandResolutionSource.Fallback) + : new CommandResolutionResult(command, extensionResolved, CommandResolutionSource.Path); + } + + var overrideTool = string.IsNullOrWhiteSpace(tool) ? normalized : tool.ToLowerInvariant(); + var configuredCandidates = TryGetConfiguredExecutables(config, overrideTool); + + if (configuredCandidates is not null) + { + foreach (var configured in configuredCandidates) + { + var resolvedConfigured = ResolveFromPath(configured!) ?? configured!; + if (IsUsableExecutable(resolvedConfigured)) + return new CommandResolutionResult(command, resolvedConfigured, CommandResolutionSource.ConfiguredOverride); + } + } + + foreach (var candidate in BuildWindowsCandidates(command, normalized)) + { + var resolved = ResolveFromPath(candidate); + if (!string.IsNullOrWhiteSpace(resolved)) + { + var source = candidate.EndsWith(".cmd", StringComparison.OrdinalIgnoreCase) || + candidate.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) || + candidate.EndsWith(".bat", StringComparison.OrdinalIgnoreCase) + ? CommandResolutionSource.Shim + : CommandResolutionSource.Path; + return new CommandResolutionResult(command, resolved!, source); + } + } + + if (normalized is "npm" or "npx" or "pnpm" or "yarn") + { + var nodePath = ResolveFromPath("node.exe") ?? ResolveFromPath("node"); + if (!string.IsNullOrWhiteSpace(nodePath)) + { + var nodeDir = Path.GetDirectoryName(nodePath); + if (!string.IsNullOrWhiteSpace(nodeDir)) + { + var shim = Path.Combine(nodeDir, normalized + ".cmd"); + if (File.Exists(shim)) + return new CommandResolutionResult(command, shim, CommandResolutionSource.NodeAdjacentShim); + } + } + } + + var fallback = BuildWindowsCandidates(command, normalized).LastOrDefault() ?? command; + return new CommandResolutionResult(command, fallback, CommandResolutionSource.Fallback); + } + + public static string Resolve(string command) + { + return ResolveWithTrace(command).Resolved; + } + + private static string? ResolveFromPath(string executable) + { + var pathValue = Environment.GetEnvironmentVariable("PATH"); + if (string.IsNullOrWhiteSpace(pathValue)) + return null; + + foreach (var segment in pathValue.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)) + { + try + { + var expandedSegment = ExpandWindowsPathTokens(segment.Trim()); + var candidate = Path.Combine(expandedSegment, executable); + if (File.Exists(candidate)) + { + // If PATH lookup hit extensionless npm but npm.cmd exists beside it, prefer npm.cmd. + var fileName = Path.GetFileName(candidate).ToLowerInvariant(); + if (fileName is "npm" or "npx" or "pnpm" or "yarn" or "tauri") + { + var shim = candidate + ".cmd"; + if (File.Exists(shim)) + return shim; + } + return candidate; + } + } + catch + { + // Ignore malformed PATH segments. + } + } + + return null; + } + + private static string ExpandWindowsPathTokens(string segment) + { + if (string.IsNullOrWhiteSpace(segment) || !OperatingSystem.IsWindows()) + return segment; + + var expanded = segment; + for (var i = 0; i < 4; i++) + { + var next = Environment.ExpandEnvironmentVariables(expanded); + if (string.Equals(next, expanded, StringComparison.Ordinal)) + break; + expanded = next; + } + return expanded; + } + + private static List BuildWindowsCandidates(string command, string normalized) + { + var candidates = new List(); + if (normalized is "npm" or "npx" or "pnpm" or "yarn" or "tauri") + { + candidates.Add(command + ".cmd"); + candidates.Add(command + ".exe"); + candidates.Add(command + ".bat"); + candidates.Add(command); + } + else + { + candidates.Add(command); + } + + return candidates; + } + + private static bool IsUsableExecutable(string resolved) + { + if (string.IsNullOrWhiteSpace(resolved)) + return false; + + if (Path.IsPathRooted(resolved)) + return File.Exists(resolved); + + return ResolveFromPath(resolved) is not null; + } + + private static List? TryGetConfiguredExecutables(object? config, string tool) + { + if (config is null) + return null; + + try + { + var toolingProp = config.GetType().GetProperty("Tooling"); + var tooling = toolingProp?.GetValue(config); + if (tooling is null) + return null; + + var toolsProp = tooling.GetType().GetProperty("Tools"); + var toolsValue = toolsProp?.GetValue(tooling); + if (toolsValue is not System.Collections.IEnumerable toolsEnum) + return null; + + foreach (var item in toolsEnum) + { + if (item is null) + continue; + + var toolProp = item.GetType().GetProperty("Tool"); + var toolName = toolProp?.GetValue(item) as string; + if (!string.Equals(toolName, tool, StringComparison.OrdinalIgnoreCase)) + continue; + + var executablesProp = item.GetType().GetProperty("Executables"); + var executablesVal = executablesProp?.GetValue(item); + if (executablesVal is not System.Collections.IEnumerable execs) + return null; + + var result = new List(); + foreach (var exec in execs) + { + if (exec is string s && !string.IsNullOrWhiteSpace(s)) + result.Add(s); + } + + return result.Count > 0 ? result : null; + } + } + catch + { + // Best effort only; fallback resolver continues without config overrides. + } + + return null; + } +} diff --git a/src/DevTool.Runtime/DevTool.Runtime.csproj b/src/DevTool.Runtime/DevTool.Runtime.csproj new file mode 100644 index 0000000..276a4da --- /dev/null +++ b/src/DevTool.Runtime/DevTool.Runtime.csproj @@ -0,0 +1,8 @@ + + + net10.0 + enable + enable + Sdt + + diff --git a/src/DevTool.Runtime/Runner/ProcessRunner.cs b/src/DevTool.Runtime/Runner/ProcessRunner.cs new file mode 100644 index 0000000..3dbb3a5 --- /dev/null +++ b/src/DevTool.Runtime/Runner/ProcessRunner.cs @@ -0,0 +1,83 @@ +using System.Diagnostics; + +namespace Sdt.Runner; + +public sealed record RunResult(int ExitCode, TimeSpan Elapsed) +{ + public bool Success => ExitCode == 0; +} + +public static class ProcessRunner +{ + /// + /// Runs a command with the given args, streaming stdout/stderr via . + /// onOutput receives (line, isStderr). + /// + public static async Task RunAsync( + string command, + IEnumerable args, + string workingDir, + Action onOutput, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + { + var psi = new ProcessStartInfo + { + FileName = Core.CommandResolver.Resolve(command), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = workingDir, + }; + + if (envOverrides is not null) + { + foreach (var kvp in envOverrides) + psi.Environment[kvp.Key] = kvp.Value; + } + + foreach (var arg in args) + psi.ArgumentList.Add(arg); + + var sw = Stopwatch.StartNew(); + + using var process = new Process { StartInfo = psi }; + + void OnCancel(object? sender, ConsoleCancelEventArgs e) + { + e.Cancel = true; // Prevent SDT from exiting immediately + try { process.Kill(entireProcessTree: true); } catch { } + } + + Console.CancelKeyPress += OnCancel; + + try + { + process.Start(); + + var stdoutTask = DrainAsync(process.StandardOutput, line => onOutput(line, false), cancellationToken); + var stderrTask = DrainAsync(process.StandardError, line => onOutput(line, true), cancellationToken); + + await Task.WhenAll(stdoutTask, stderrTask).ConfigureAwait(false); + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + Console.CancelKeyPress -= OnCancel; + } + + sw.Stop(); + return new RunResult(process.ExitCode, sw.Elapsed); + } + + private static async Task DrainAsync(StreamReader reader, Action emit, CancellationToken ct) + { + string? line; + while ((line = await reader.ReadLineAsync(ct).ConfigureAwait(false)) is not null + && !ct.IsCancellationRequested) + { + emit(line); + } + } +} diff --git a/tests/DevTool.Tests/ActionRunnerLegacyPwshTests.cs b/tests/DevTool.Tests/ActionRunnerLegacyPwshTests.cs new file mode 100644 index 0000000..17f9c6c --- /dev/null +++ b/tests/DevTool.Tests/ActionRunnerLegacyPwshTests.cs @@ -0,0 +1,66 @@ +using Sdt.Config; +using Sdt.Core; +using Xunit; + +namespace DevTool.Tests; + +public sealed class ActionRunnerLegacyPwshTests +{ + [Fact] + public async Task LegacyPwshTarget_ReroutesToPythonScript_WhenPs1Missing() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-actionrunner-" + Guid.NewGuid().ToString("N")); + var scripts = Path.Combine(root, "scripts"); + Directory.CreateDirectory(scripts); + File.WriteAllText(Path.Combine(scripts, "publish-app.py"), "print('rerouted')"); + + var step = new WorkflowStep + { + Id = "legacy", + Label = "legacy", + Command = "pwsh", + Args = ["-NoProfile", "-File", "scripts/publish-app.ps1", "-Target", "web"], + WorkingDir = "." + }; + + var runner = new ActionRunner(); + var run = await runner.RunStepAsync(step, root, (_, _) => { }); + + Assert.True(run.Success); + } + + [Fact] + public async Task LegacyPwshTarget_DoesNotFallbackToPowerShell_ByDefaultWhenPythonFails() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-actionrunner-" + Guid.NewGuid().ToString("N")); + var scripts = Path.Combine(root, "scripts"); + Directory.CreateDirectory(scripts); + File.WriteAllText(Path.Combine(scripts, "publish-app.py"), "import sys\nsys.exit(7)\n"); + File.WriteAllText(Path.Combine(scripts, "publish-app.ps1"), "Write-Host 'legacy'"); + + var step = new WorkflowStep + { + Id = "legacy", + Label = "legacy", + Command = "pwsh", + Args = ["-NoProfile", "-File", "scripts/publish-app.ps1", "-Target", "web"], + WorkingDir = "." + }; + + Environment.SetEnvironmentVariable("SDT_PWSH_LEGACY_FALLBACK", null); + try + { + var output = new List(); + var runner = new ActionRunner(); + var run = await runner.RunStepAsync(step, root, (line, _) => output.Add(line)); + + Assert.False(run.Success); + Assert.Equal(7, run.ExitCode); + Assert.Contains(output, line => line.Contains("Legacy PowerShell fallback is disabled by default", StringComparison.OrdinalIgnoreCase)); + } + finally + { + Environment.SetEnvironmentVariable("SDT_PWSH_LEGACY_FALLBACK", null); + } + } +} diff --git a/tests/DevTool.Tests/CommandResolverTests.cs b/tests/DevTool.Tests/CommandResolverTests.cs new file mode 100644 index 0000000..ac995eb --- /dev/null +++ b/tests/DevTool.Tests/CommandResolverTests.cs @@ -0,0 +1,124 @@ +using Sdt.Core; +using Sdt.Config; +using Xunit; + +namespace DevTool.Tests; + +public sealed class CommandResolverTests +{ + [Fact] + public void Resolve_Npm_OnWindows_UsesCmdShim() + { + var resolved = CommandResolver.Resolve("npm"); + if (OperatingSystem.IsWindows()) + Assert.True( + resolved.EndsWith("npm.cmd", StringComparison.OrdinalIgnoreCase) || + resolved.EndsWith("\\npm", StringComparison.OrdinalIgnoreCase) || + string.Equals(resolved, "npm.cmd", StringComparison.OrdinalIgnoreCase), + $"Resolved npm path was '{resolved}'"); + else + Assert.Equal("npm", resolved); + } + + [Fact] + public void Resolve_PathOrExtension_Unchanged() + { + Assert.Equal("C:\\tools\\npm.cmd", CommandResolver.Resolve("C:\\tools\\npm.cmd")); + var resolved = CommandResolver.Resolve("dotnet.exe"); + if (OperatingSystem.IsWindows() && Path.IsPathRooted(resolved)) + Assert.EndsWith("dotnet.exe", resolved, StringComparison.OrdinalIgnoreCase); + else + Assert.Equal("dotnet.exe", resolved); + } + + [Fact] + public void Resolve_Npm_PrefersCmdShim_WhenBothBareAndCmdExist() + { + if (!OperatingSystem.IsWindows()) + return; + + var temp = Path.Combine(Path.GetTempPath(), "sdt-cmdresolve-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(temp); + File.WriteAllText(Path.Combine(temp, "npm"), ""); + File.WriteAllText(Path.Combine(temp, "npm.cmd"), "@echo off"); + + var originalPath = Environment.GetEnvironmentVariable("PATH"); + try + { + Environment.SetEnvironmentVariable("PATH", temp); + var resolved = CommandResolver.Resolve("npm"); + Assert.EndsWith("npm.cmd", resolved, StringComparison.OrdinalIgnoreCase); + } + finally + { + Environment.SetEnvironmentVariable("PATH", originalPath); + try { Directory.Delete(temp, recursive: true); } catch { } + } + } + + [Fact] + public void ResolveWithTrace_ConfiguredOverride_IsUsed() + { + if (!OperatingSystem.IsWindows()) + return; + + var temp = Path.Combine(Path.GetTempPath(), "sdt-cmdresolve-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(temp); + var overridePath = Path.Combine(temp, "npm.cmd"); + File.WriteAllText(overridePath, "@echo off"); + + var cfg = new DevToolConfig + { + Tooling = new ToolingConfig + { + Tools = + [ + new ToolInstallDefinition + { + Tool = "npm", + Executables = [overridePath] + } + ] + } + }; + + var resolved = CommandResolver.ResolveWithTrace("npm", cfg, "npm"); + Assert.Equal(CommandResolutionSource.ConfiguredOverride, resolved.Source); + Assert.EndsWith("npm.cmd", resolved.Resolved, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void ResolveWithTrace_ExpandsWindowsPathTokens() + { + if (!OperatingSystem.IsWindows()) + return; + + var nvmHome = Path.Combine(Path.GetTempPath(), "sdt-nvmhome-" + Guid.NewGuid().ToString("N")); + var nvmLink = Path.Combine(Path.GetTempPath(), "sdt-nvmlink-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(nvmHome); + Directory.CreateDirectory(nvmLink); + File.WriteAllText(Path.Combine(nvmLink, "npm.cmd"), "@echo off"); + + var originalPath = Environment.GetEnvironmentVariable("PATH"); + var originalHome = Environment.GetEnvironmentVariable("NVM_HOME"); + var originalLink = Environment.GetEnvironmentVariable("NVM_SYMLINK"); + try + { + Environment.SetEnvironmentVariable("NVM_HOME", nvmHome); + Environment.SetEnvironmentVariable("NVM_SYMLINK", nvmLink); + Environment.SetEnvironmentVariable("PATH", "%NVM_HOME%;%NVM_SYMLINK%"); + + var result = CommandResolver.ResolveWithTrace("npm"); + Assert.True(result.Source is CommandResolutionSource.Shim or CommandResolutionSource.Path); + Assert.EndsWith("npm.cmd", result.Resolved, StringComparison.OrdinalIgnoreCase); + } + finally + { + Environment.SetEnvironmentVariable("PATH", originalPath); + Environment.SetEnvironmentVariable("NVM_HOME", originalHome); + Environment.SetEnvironmentVariable("NVM_SYMLINK", originalLink); + try { Directory.Delete(nvmHome, recursive: true); } catch { } + try { Directory.Delete(nvmLink, recursive: true); } catch { } + } + } +} diff --git a/tests/DevTool.Tests/ConfigBootstrapperMatrixTests.cs b/tests/DevTool.Tests/ConfigBootstrapperMatrixTests.cs new file mode 100644 index 0000000..d109270 --- /dev/null +++ b/tests/DevTool.Tests/ConfigBootstrapperMatrixTests.cs @@ -0,0 +1,109 @@ +using Sdt.Config; +using Xunit; + +namespace DevTool.Tests; + +public sealed class ConfigBootstrapperMatrixTests +{ + [Fact] + public void DotnetProject_GeneratesDotnetCoreWorkflows() + { + var root = CreateTempDir("sdt-matrix-dotnet-"); + File.WriteAllText(Path.Combine(root, "sample.sln"), ""); + + var scan = ConfigBootstrapper.Scan(root); + var config = ConfigBootstrapper.BuildDefaultConfig(scan); + + var build = config.Workflows.FirstOrDefault(w => w.Id == "build"); + var deps = config.Workflows.FirstOrDefault(w => w.Id == "deps-refresh"); + var test = config.Workflows.FirstOrDefault(w => w.Id == "test"); + + Assert.NotNull(build); + Assert.NotNull(deps); + Assert.NotNull(test); + Assert.Contains(build!.Steps, s => s.Action == "dotnet-build"); + Assert.Contains(deps!.Steps, s => s.Action == "dotnet-restore"); + Assert.Contains(test!.Steps, s => s.Action == "dotnet-test"); + } + + [Fact] + public void NodeProject_GeneratesNodeCoreWorkflows() + { + var root = CreateTempDir("sdt-matrix-node-"); + File.WriteAllText(Path.Combine(root, "package.json"), """{ "name": "demo", "scripts": { "build": "echo build", "test": "echo test" } }"""); + + var scan = ConfigBootstrapper.Scan(root); + var config = ConfigBootstrapper.BuildDefaultConfig(scan); + + var build = config.Workflows.FirstOrDefault(w => w.Id == "build"); + var deps = config.Workflows.FirstOrDefault(w => w.Id == "deps-refresh"); + var test = config.Workflows.FirstOrDefault(w => w.Id == "test"); + + Assert.NotNull(build); + Assert.NotNull(deps); + Assert.NotNull(test); + Assert.Contains(build!.Steps, s => s.Action == "npm-build"); + Assert.Contains(deps!.Steps, s => s.Action == "npm-ci"); + Assert.Contains(test!.Steps, s => s.Action == "npm-test"); + } + + [Fact] + public void TauriCargoProject_GeneratesTauriAndCargoWorkflows() + { + var root = CreateTempDir("sdt-matrix-tauri-"); + File.WriteAllText(Path.Combine(root, "package.json"), """{ "name": "demo", "scripts": { "build": "echo build" } }"""); + File.WriteAllText(Path.Combine(root, "Cargo.toml"), "[package]\nname=\"demo\"\nversion=\"0.1.0\""); + File.WriteAllText(Path.Combine(root, "tauri.conf.json"), "{ }"); + + var scan = ConfigBootstrapper.Scan(root); + var config = ConfigBootstrapper.BuildDefaultConfig(scan); + + var build = config.Workflows.FirstOrDefault(w => w.Id == "build"); + var deps = config.Workflows.FirstOrDefault(w => w.Id == "deps-refresh"); + var test = config.Workflows.FirstOrDefault(w => w.Id == "test"); + + Assert.NotNull(build); + Assert.NotNull(deps); + Assert.NotNull(test); + Assert.Contains(build!.Steps, s => s.Action == "tauri-build"); + Assert.Contains(build.Steps, s => s.Action == "cargo-build"); + Assert.Contains(deps!.Steps, s => s.Action == "npm-ci"); + Assert.Contains(test!.Steps, s => s.Action == "cargo-test"); + } + + [Fact] + public void GoMavenGradle_ActionsRemainConservativeAndRunnable() + { + var root = CreateTempDir("sdt-matrix-jvm-go-"); + File.WriteAllText(Path.Combine(root, "go.mod"), "module example.com/demo"); + File.WriteAllText(Path.Combine(root, "pom.xml"), ""); + File.WriteAllText(Path.Combine(root, "build.gradle"), "plugins {}"); + + var scan = ConfigBootstrapper.Scan(root); + var config = ConfigBootstrapper.BuildDefaultConfig(scan); + + var build = config.Workflows.First(w => w.Id == "build"); + var deps = config.Workflows.First(w => w.Id == "deps-refresh"); + var test = config.Workflows.First(w => w.Id == "test"); + + Assert.Contains(build.Steps, s => s.Command == "go" && s.Args.SequenceEqual(["build", "./..."])); + Assert.Contains(build.Steps, s => s.Command == "mvn" && s.Args.SequenceEqual(["package", "-DskipTests"])); + Assert.Contains(build.Steps, s => s.Command == "gradle" && s.Args.SequenceEqual(["build"])); + + Assert.Contains(deps.Steps, s => s.Command == "go" && s.Args.SequenceEqual(["mod", "download"])); + Assert.Contains(deps.Steps, s => s.Command == "mvn" && s.Args.SequenceEqual(["dependency:resolve"])); + Assert.Contains(deps.Steps, s => s.Command == "gradle" && s.Args.SequenceEqual(["dependencies"])); + + Assert.Contains(test.Steps, s => s.Command == "go" && s.Args.SequenceEqual(["test", "./..."])); + Assert.Contains(test.Steps, s => s.Command == "mvn" && s.Args.SequenceEqual(["test"])); + Assert.Contains(test.Steps, s => s.Command == "gradle" && s.Args.SequenceEqual(["test"])); + } + + private static string CreateTempDir(string prefix) + { + var path = Path.Combine(Path.GetTempPath(), prefix + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(path); + return path; + } +} + diff --git a/tests/DevTool.Tests/ConfigBootstrapperTests.cs b/tests/DevTool.Tests/ConfigBootstrapperTests.cs new file mode 100644 index 0000000..b0b9aa4 --- /dev/null +++ b/tests/DevTool.Tests/ConfigBootstrapperTests.cs @@ -0,0 +1,169 @@ +using Sdt.Config; +using Xunit; + +namespace DevTool.Tests; + +public sealed class ConfigBootstrapperTests +{ + [Fact] + public void Scan_DetectsDotnetAndNode() + { + var root = CreateTempDir(); + File.WriteAllText(Path.Combine(root, "sample.sln"), ""); + File.WriteAllText(Path.Combine(root, "package.json"), """{ "name": "demo", "scripts": { "build": "echo build" } }"""); + + var scan = ConfigBootstrapper.Scan(root); + + Assert.Contains("dotnet", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); + Assert.Contains("node", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); + Assert.Contains("npm", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public void Scan_DetectsPythonFromScriptsDirectory() + { + var root = CreateTempDir(); + var scripts = Path.Combine(root, "scripts"); + Directory.CreateDirectory(scripts); + File.WriteAllText(Path.Combine(scripts, "publish-app.py"), "print('ok')"); + + var scan = ConfigBootstrapper.Scan(root); + Assert.Contains("python", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public void Scan_DoesNotDetectNode_ForDependencyOnlyPackageJson() + { + var root = CreateTempDir(); + File.WriteAllText(Path.Combine(root, "package.json"), """{ "dependencies": { "leftpad": "1.0.0" } }"""); + + var scan = ConfigBootstrapper.Scan(root); + + Assert.DoesNotContain("node", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); + Assert.DoesNotContain("npm", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public void BuildDefaultConfig_ProducesWorkflowsAndDebugSection() + { + var scan = new BootstrapScanResult( + ProjectRoot: Path.GetTempPath(), + ProjectName: "demo", + ProjectType: "dotnet", + ToolFamilies: ["dotnet", "git"], + DotnetWorkingDir: ".", + NodeWorkingDir: null, + PythonRequirementsFile: null, + HasDockerCompose: false, + RootHints: ["*.sln"]); + + var cfg = ConfigBootstrapper.BuildDefaultConfig(scan); + + Assert.NotNull(cfg.Debug); + Assert.Contains(cfg.Workflows, w => w.Id == "build"); + Assert.Contains(cfg.Workflows, w => w.Id == "repo-health"); + Assert.False(cfg.Debug!.Diagnostics.IncludeAllEnv); + Assert.Contains("SDT_LOG_LEVEL", cfg.Debug.Diagnostics.CaptureEnvKeys); + Assert.NotNull(cfg.EnvProfiles); + Assert.Contains(cfg.EnvProfiles!.Profiles, p => p.Id == "dev"); + Assert.Contains(cfg.EnvProfiles.Profiles, p => p.Id == "ci"); + Assert.Contains(cfg.EnvProfiles.Profiles, p => p.Id == "release"); + } + + [Fact] + public void BuildDefaultConfig_IncludesScriptDrivenWorkflow_WhenHelpersExist() + { + var root = CreateTempDir(); + var scripts = Path.Combine(root, "scripts"); + Directory.CreateDirectory(scripts); + File.WriteAllText(Path.Combine(scripts, "publish-app.py"), "print('ok')"); + File.WriteAllText(Path.Combine(scripts, "publish-sidecar.py"), "print('ok')"); + + var scan = ConfigBootstrapper.Scan(root); + var cfg = ConfigBootstrapper.BuildDefaultConfig(scan); + + Assert.Contains(cfg.Workflows, w => w.Id == "web"); + Assert.Contains(cfg.Workflows, w => w.Id == "sidecar"); + } + + [Fact] + public void WriteDefaultConfig_WritesDevtoolJson() + { + var root = CreateTempDir(); + var scan = new BootstrapScanResult( + ProjectRoot: root, + ProjectName: "demo", + ProjectType: "dotnet", + ToolFamilies: ["dotnet"], + DotnetWorkingDir: ".", + NodeWorkingDir: null, + PythonRequirementsFile: null, + HasDockerCompose: false, + RootHints: ["*.sln"]); + var cfg = ConfigBootstrapper.BuildDefaultConfig(scan); + + var path = ConfigBootstrapper.WriteDefaultConfig(root, cfg); + + Assert.True(File.Exists(path)); + } + + [Fact] + public void Scan_IgnoresExcludedDirectories_ForToolDetection() + { + var root = CreateTempDir(); + var nodeModules = Path.Combine(root, "node_modules", "nested"); + Directory.CreateDirectory(nodeModules); + File.WriteAllText(Path.Combine(nodeModules, "package.json"), "{}"); + + var scan = ConfigBootstrapper.Scan(root); + Assert.DoesNotContain("node", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public void Scan_DetectsGoAndSetsProjectType() + { + var root = CreateTempDir(); + File.WriteAllText(Path.Combine(root, "go.mod"), "module example.com/demo"); + + var scan = ConfigBootstrapper.Scan(root); + Assert.Contains("go", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); + Assert.Equal("go", scan.ProjectType, ignoreCase: true); + } + + [Fact] + public void Scan_DetectsMavenAndGradleAsJavaProject() + { + var root = CreateTempDir(); + File.WriteAllText(Path.Combine(root, "pom.xml"), ""); + File.WriteAllText(Path.Combine(root, "build.gradle"), "plugins {}"); + + var scan = ConfigBootstrapper.Scan(root); + Assert.Contains("maven", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); + Assert.Contains("gradle", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); + Assert.Contains("java", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); + Assert.Equal("java", scan.ProjectType, ignoreCase: true); + } + + [Fact] + public void Scan_ResolvesDotnetWorkingDir_FromNearestLaunchPath() + { + var workspace = CreateTempDir(); + File.WriteAllText(Path.Combine(workspace, "package.json"), """{ "name": "workspace-root" }"""); + var appA = Path.Combine(workspace, "AppA"); + var appB = Path.Combine(workspace, "AppB"); + Directory.CreateDirectory(appA); + Directory.CreateDirectory(appB); + File.WriteAllText(Path.Combine(appA, "AppA.csproj"), ""); + File.WriteAllText(Path.Combine(appB, "AppB.csproj"), ""); + + var scan = ConfigBootstrapper.Scan(appB); + Assert.Equal("AppB", scan.DotnetWorkingDir, ignoreCase: true); + } + + private static string CreateTempDir() + { + var path = Path.Combine(Path.GetTempPath(), "sdt-bootstrap-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(path); + return path; + } +} diff --git a/tests/DevTool.Tests/ConfigDoctorAutoFixServiceTests.cs b/tests/DevTool.Tests/ConfigDoctorAutoFixServiceTests.cs new file mode 100644 index 0000000..9e51eec --- /dev/null +++ b/tests/DevTool.Tests/ConfigDoctorAutoFixServiceTests.cs @@ -0,0 +1,54 @@ +using Sdt.Config; +using Sdt.Core; +using Xunit; + +namespace DevTool.Tests; + +public sealed class ConfigDoctorAutoFixServiceTests +{ + [Fact] + public void FindMissingWorkingDirectories_ReturnsMissingPaths() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-autofix-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + Directory.CreateDirectory(Path.Combine(root, "exists")); + + var cfg = new DevToolConfig + { + Workflows = + [ + new WorkflowDefinition + { + Id = "build", + Label = "Build", + Steps = + [ + new WorkflowStep { Id = "s1", Label = "S1", Action = "dotnet-build", WorkingDir = "exists" }, + new WorkflowStep { Id = "s2", Label = "S2", Action = "dotnet-build", WorkingDir = "missing/sub" } + ] + } + ] + }; + + var service = new ConfigDoctorAutoFixService(); + var missing = service.FindMissingWorkingDirectories(cfg, root); + + Assert.Single(missing); + Assert.EndsWith(Path.Combine("missing", "sub"), missing[0], StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void CreateMissingWorkingDirectories_CreatesPaths() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-autofix-" + Guid.NewGuid().ToString("N")); + var path = Path.Combine(root, "a", "b", "c"); + Directory.CreateDirectory(root); + + var service = new ConfigDoctorAutoFixService(); + var result = service.CreateMissingWorkingDirectories([path]); + + Assert.True(result.Success); + Assert.True(Directory.Exists(path)); + Assert.Equal(1, result.CreatedDirectories); + } +} diff --git a/tests/DevTool.Tests/ConfigDoctorServiceTests.cs b/tests/DevTool.Tests/ConfigDoctorServiceTests.cs new file mode 100644 index 0000000..bd64b29 --- /dev/null +++ b/tests/DevTool.Tests/ConfigDoctorServiceTests.cs @@ -0,0 +1,82 @@ +using Sdt.Config; +using Sdt.Core; +using Xunit; + +namespace DevTool.Tests; + +public sealed class ConfigDoctorServiceTests +{ + [Fact] + public async Task TargetsOnly_Config_IsFlaggedAsFail() + { + var config = new DevToolConfig + { + Targets = + [ + new BuildTarget + { + Id = "build", + Label = "Build", + Command = "dotnet", + Args = ["build"] + } + ], + Workflows = [] + }; + + var doctor = new ConfigDoctorService(new AlwaysAvailableProbe(), new RequirementResolver()); + var report = await doctor.RunAsync(config, Directory.GetCurrentDirectory()); + + Assert.Contains(report.Checks, c => c.Name == "Legacy schema" && c.Status == DoctorStatus.Fail); + } + + [Fact] + public async Task MissingTool_IsReportedWithFix() + { + var config = new DevToolConfig + { + Workflows = + [ + new WorkflowDefinition + { + Id = "build", + Label = "Build", + Steps = + [ + new WorkflowStep { Id = "s1", Label = "Build", Command = "dotnet", Args = ["build"] } + ] + } + ] + }; + + var doctor = new ConfigDoctorService(new AlwaysMissingProbe(), new RequirementResolver()); + var report = await doctor.RunAsync(config, Directory.GetCurrentDirectory()); + + Assert.Contains(report.Checks, c => + c.Name.Equals("Tool: dotnet", StringComparison.OrdinalIgnoreCase) && + c.Status == DoctorStatus.Fail && + !string.IsNullOrWhiteSpace(c.Fix)); + } + + private sealed class AlwaysAvailableProbe : IToolProbe + { + public Task ProbeAsync( + string tool, + string projectRoot, + DevToolConfig? config = null, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + => Task.FromResult(new ProbeResult(tool, true, Version: "1.0.0")); + } + + private sealed class AlwaysMissingProbe : IToolProbe + { + public Task ProbeAsync( + string tool, + string projectRoot, + DevToolConfig? config = null, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + => Task.FromResult(new ProbeResult(tool, false, Details: "Fallback: unresolved command")); + } +} diff --git a/tests/DevTool.Tests/DebugConfigTests.cs b/tests/DevTool.Tests/DebugConfigTests.cs new file mode 100644 index 0000000..e77a53a --- /dev/null +++ b/tests/DevTool.Tests/DebugConfigTests.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using Sdt.Config; +using Xunit; + +namespace DevTool.Tests; + +public sealed class DebugConfigTests +{ + [Fact] + public void DebugSectionAbsent_DeserializesWithSafeDefaults() + { + const string json = """ + { + "name": "Test", + "version": "1.0.0", + "workflows": [] + } + """; + + var cfg = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip + }); + + Assert.NotNull(cfg); + Assert.Null(cfg!.Debug); + } + + [Fact] + public void DebugDiagnostics_DefaultOutputDir_IsSdtDebug() + { + var options = new DebugDiagnosticsOptions(); + Assert.True(options.Enabled); + Assert.True(options.BundleOnFailure); + Assert.Equal(".sdt/debug", options.OutputDir); + } +} diff --git a/tests/DevTool.Tests/DebugServicesTests.cs b/tests/DevTool.Tests/DebugServicesTests.cs new file mode 100644 index 0000000..15625b8 --- /dev/null +++ b/tests/DevTool.Tests/DebugServicesTests.cs @@ -0,0 +1,257 @@ +using Sdt.Config; +using Sdt.Core; +using Sdt.Core.Debug; +using Sdt.Runner; +using System.Text.Json; +using Xunit; + +namespace DevTool.Tests; + +public sealed class DebugServicesTests +{ + [Fact] + public async Task DebugRunner_MissingPrereqDeclined_ReturnsUserDeclined() + { + var runner = new DebugProfileRunner( + new FakeProbeService(false), + new FakeInstallerService(true)); + + var profile = new DebugProfileDefinition + { + Id = "p1", + Label = "Profile", + Type = "python", + Command = "python", + Args = ["--version"], + Requires = [new ToolRequirement { Tool = "python", InstallPolicy = InstallPolicy.Prompt }] + }; + + var result = await runner.RunAsync( + profile, + new DevToolConfig(), + Directory.GetCurrentDirectory(), + verbose: false, + confirmInstallAsync: (_, _) => Task.FromResult(false), + onOutput: (_, _) => { }); + + Assert.False(result.Success); + Assert.Equal(ExecutionStopReason.UserDeclined, result.StopReason); + } + + [Fact] + public async Task DiagnosticsBundle_WritesFiles() + { + var service = new DiagnosticsBundleService(); + var root = Path.Combine(Path.GetTempPath(), "sdt-diag-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + + var request = new DiagnosticsBundleRequest( + Category: "workflow", + ProjectRoot: root, + SummaryMessage: "failed", + OutputLines: ["hello"], + WorkflowSteps: [], + Probes: [], + DiagnosticsOptions: new DebugDiagnosticsOptions { OutputDir = ".sdt/debug" }, + Config: new DevToolConfig(), + StopReason: ExecutionStopReason.CommandFailed); + + var result = await service.WriteBundleAsync(request); + Assert.True(result.Success); + Assert.True(File.Exists(Path.Combine(result.BundleDirectory, "summary.json"))); + Assert.True(File.Exists(Path.Combine(result.BundleDirectory, "output.log"))); + } + + [Fact] + public async Task DiagnosticsBundle_EmptyAllowlist_CapturesNoEnvByDefault() + { + var service = new DiagnosticsBundleService(); + var root = Path.Combine(Path.GetTempPath(), "sdt-diag-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + + var request = new DiagnosticsBundleRequest( + Category: "workflow", + ProjectRoot: root, + SummaryMessage: "failed", + OutputLines: [], + WorkflowSteps: [], + Probes: [], + DiagnosticsOptions: new DebugDiagnosticsOptions + { + OutputDir = ".sdt/debug", + IncludeAllEnv = false, + CaptureEnvKeys = [] + }, + Config: new DevToolConfig(), + StopReason: ExecutionStopReason.CommandFailed); + + var result = await service.WriteBundleAsync(request); + Assert.True(result.Success); + + var envJson = await File.ReadAllTextAsync(Path.Combine(result.BundleDirectory, "env.json")); + using var doc = JsonDocument.Parse(envJson); + Assert.Empty(doc.RootElement.EnumerateObject()); + } + + [Fact] + public async Task DiagnosticsBundle_Allowlist_CapturesOnlyListedKeys() + { + var service = new DiagnosticsBundleService(); + var root = Path.Combine(Path.GetTempPath(), "sdt-diag-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + + Environment.SetEnvironmentVariable("SDT_TEST_ENV_A", "A"); + Environment.SetEnvironmentVariable("SDT_TEST_ENV_B", "B"); + + var request = new DiagnosticsBundleRequest( + Category: "workflow", + ProjectRoot: root, + SummaryMessage: "failed", + OutputLines: [], + WorkflowSteps: [], + Probes: [], + DiagnosticsOptions: new DebugDiagnosticsOptions + { + OutputDir = ".sdt/debug", + IncludeAllEnv = false, + CaptureEnvKeys = ["SDT_TEST_ENV_A"] + }, + Config: new DevToolConfig(), + StopReason: ExecutionStopReason.CommandFailed); + + var result = await service.WriteBundleAsync(request); + Assert.True(result.Success); + + var envJson = await File.ReadAllTextAsync(Path.Combine(result.BundleDirectory, "env.json")); + using var doc = JsonDocument.Parse(envJson); + Assert.True(doc.RootElement.TryGetProperty("SDT_TEST_ENV_A", out _)); + Assert.False(doc.RootElement.TryGetProperty("SDT_TEST_ENV_B", out _)); + } + + [Fact] + public async Task DiagnosticsBundle_RedactsSensitiveEnvKeys() + { + var service = new DiagnosticsBundleService(); + var root = Path.Combine(Path.GetTempPath(), "sdt-diag-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + + Environment.SetEnvironmentVariable("MY_SECRET_TOKEN", "super-secret"); + + var request = new DiagnosticsBundleRequest( + Category: "workflow", + ProjectRoot: root, + SummaryMessage: "failed", + OutputLines: [], + WorkflowSteps: [], + Probes: [], + DiagnosticsOptions: new DebugDiagnosticsOptions + { + OutputDir = ".sdt/debug", + IncludeAllEnv = false, + CaptureEnvKeys = ["MY_SECRET_TOKEN"], + RedactSensitive = true + }, + Config: new DevToolConfig(), + StopReason: ExecutionStopReason.CommandFailed); + + var result = await service.WriteBundleAsync(request); + Assert.True(result.Success); + + var envJson = await File.ReadAllTextAsync(Path.Combine(result.BundleDirectory, "env.json")); + using var doc = JsonDocument.Parse(envJson); + Assert.Equal("***REDACTED***", doc.RootElement.GetProperty("MY_SECRET_TOKEN").GetString()); + } + + [Fact] + public async Task DiagnosticsBundle_RedactsSensitiveOutputPatterns() + { + var service = new DiagnosticsBundleService(); + var root = Path.Combine(Path.GetTempPath(), "sdt-diag-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + + var request = new DiagnosticsBundleRequest( + Category: "workflow", + ProjectRoot: root, + SummaryMessage: "failed", + OutputLines: ["token=abc123", "Authorization: Bearer verysecretvalue"], + WorkflowSteps: [], + Probes: [], + DiagnosticsOptions: new DebugDiagnosticsOptions + { + OutputDir = ".sdt/debug", + RedactSensitive = true + }, + Config: new DevToolConfig(), + StopReason: ExecutionStopReason.CommandFailed); + + var result = await service.WriteBundleAsync(request); + Assert.True(result.Success); + + var output = await File.ReadAllTextAsync(Path.Combine(result.BundleDirectory, "output.log")); + Assert.Contains("***REDACTED***", output, StringComparison.Ordinal); + Assert.DoesNotContain("verysecretvalue", output, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task DebugRunner_EmitsRunEvents() + { + var runner = new DebugProfileRunner( + new FakeProbeService(true), + new FakeInstallerService(true)); + + var profile = new DebugProfileDefinition + { + Id = "p1", + Label = "Profile", + Type = "python", + Command = "python", + Args = ["--version"], + Requires = [new ToolRequirement { Tool = "python", InstallPolicy = InstallPolicy.Prompt }] + }; + + var events = new List(); + var result = await runner.RunAsync( + profile, + new DevToolConfig(), + Directory.GetCurrentDirectory(), + verbose: false, + confirmInstallAsync: (_, _) => Task.FromResult(false), + onOutput: (_, _) => { }, + onEvent: events.Add); + + Assert.True(result.Success); + Assert.Contains(events, e => e.Type == RunEventType.DebugStarted); + Assert.Contains(events, e => e.Type == RunEventType.DebugCommandStarted); + Assert.Contains(events, e => e.Type == RunEventType.DebugCommandCompleted); + Assert.Contains(events, e => e.Type == RunEventType.DebugCompleted && e.Success == true); + } + + private sealed class FakeProbeService(bool isAvailable) : IToolProbe + { + public Task ProbeAsync( + string tool, + string projectRoot, + DevToolConfig? config = null, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + => Task.FromResult(new ProbeResult(tool, isAvailable, Version: isAvailable ? "1.0.0" : null)); + } + + private sealed class FakeInstallerService(bool success) : IPrereqInstaller + { + public Task GetInstallPlanAsync( + string tool, + string projectRoot, + DevToolConfig? config = null, + CancellationToken cancellationToken = default) + => Task.FromResult(new InstallPlan(tool, Supported: true, "test", [new InstallCommand("echo", ["ok"])])); + + public Task RunInstallAsync( + InstallCommand command, + string projectRoot, + Action onOutput, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + => Task.FromResult(new RunResult(success ? 0 : 1, TimeSpan.FromMilliseconds(5))); + } +} diff --git a/tests/DevTool.Tests/DevShellScriptTests.cs b/tests/DevTool.Tests/DevShellScriptTests.cs new file mode 100644 index 0000000..794d35d --- /dev/null +++ b/tests/DevTool.Tests/DevShellScriptTests.cs @@ -0,0 +1,116 @@ +using System.Diagnostics; +using System.Text.Json; +using System.Runtime.CompilerServices; +using Xunit; + +namespace DevTool.Tests; + +public sealed class DevShellScriptTests +{ + [Theory] + [InlineData("pwsh")] + [InlineData("bash")] + [InlineData("zsh")] + [InlineData("cmd")] + public async Task DevShellExport_ReturnsSuccess_ForSupportedShells(string shell) + { + var python = ResolvePython(); + var result = await RunAsync( + python, + ["scripts/dev_shell.py", "export", "--shell", shell, "--json"]); + + Assert.Equal(0, result.ExitCode); + using var doc = JsonDocument.Parse(result.StdOut); + Assert.True(doc.RootElement.TryGetProperty("projectRoot", out _)); + Assert.True(doc.RootElement.TryGetProperty("env", out _)); + } + + [Fact] + public async Task DevShellExport_InvalidShell_ReturnsExitCode3() + { + var python = ResolvePython(); + var result = await RunAsync( + python, + ["scripts/dev_shell.py", "export", "--shell", "fish"]); + + Assert.Equal(3, result.ExitCode); + } + + [Fact] + public async Task DevShellDoctor_ReturnsJson() + { + var python = ResolvePython(); + var result = await RunAsync( + python, + ["scripts/dev_shell.py", "doctor"]); + + Assert.Equal(0, result.ExitCode); + using var doc = JsonDocument.Parse(result.StdOut); + Assert.True(doc.RootElement.TryGetProperty("repo_root", out _)); + } + + private static string ResolvePython() + { + var candidates = OperatingSystem.IsWindows() + ? new[] { "python", "py" } + : new[] { "python3", "python" }; + + foreach (var candidate in candidates) + { + try + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = candidate, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.StartInfo.ArgumentList.Add("--version"); + process.Start(); + process.WaitForExit(2000); + if (process.ExitCode == 0) + return candidate; + } + catch + { + } + } + + throw new InvalidOperationException("Python executable not found."); + } + + private static async Task<(int ExitCode, string StdOut, string StdErr)> RunAsync(string command, IReadOnlyList args) + { + var psi = new ProcessStartInfo + { + FileName = command, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = RepoRoot(), + }; + foreach (var arg in args) + psi.ArgumentList.Add(arg); + + using var process = new Process { StartInfo = psi }; + process.Start(); + var stdout = await process.StandardOutput.ReadToEndAsync(); + var stderr = await process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); + return (process.ExitCode, stdout, stderr); + } + + private static string RepoRoot([CallerFilePath] string file = "") + { + var repoRoot = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(file)!, "..", "..")); + if (!File.Exists(Path.Combine(repoRoot, "scripts", "dev_shell.py"))) + throw new InvalidOperationException("Could not locate repo root."); + return repoRoot; + } +} diff --git a/tests/DevTool.Tests/DevTool.Tests.csproj b/tests/DevTool.Tests/DevTool.Tests.csproj new file mode 100644 index 0000000..957d8c0 --- /dev/null +++ b/tests/DevTool.Tests/DevTool.Tests.csproj @@ -0,0 +1,18 @@ + + + net10.0 + enable + enable + false + + + + + + + + + + + + diff --git a/tests/DevTool.Tests/EnvProfileServiceTests.cs b/tests/DevTool.Tests/EnvProfileServiceTests.cs new file mode 100644 index 0000000..1e38932 --- /dev/null +++ b/tests/DevTool.Tests/EnvProfileServiceTests.cs @@ -0,0 +1,74 @@ +using Sdt.Config; +using Sdt.Core; +using Xunit; + +namespace DevTool.Tests; + +public sealed class EnvProfileServiceTests +{ + [Fact] + public void ResolveEffectiveEnv_MergesInheritedProfiles_Deterministically() + { + var config = new DevToolConfig + { + EnvProfiles = new EnvProfilesConfig + { + Active = "dev", + Profiles = + [ + new EnvProfileDefinition + { + Id = "base", + Values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["A"] = "1", + ["B"] = "base", + } + }, + new EnvProfileDefinition + { + Id = "dev", + Inherits = ["base"], + Values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["B"] = "dev", + ["C"] = "3", + } + }, + new EnvProfileDefinition + { + Id = "ci", + Inherits = ["dev"], + Values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["C"] = "ci", + ["D"] = "4", + } + } + ] + } + }; + + var env = EnvProfileService.ResolveEffectiveEnv(config, "ci"); + Assert.Equal("1", env["A"]); + Assert.Equal("dev", env["B"]); + Assert.Equal("ci", env["C"]); + Assert.Equal("4", env["D"]); + } + + [Fact] + public void ResolveEffectiveEnv_UnknownProfile_ReturnsEmpty() + { + var config = new DevToolConfig + { + EnvProfiles = new EnvProfilesConfig + { + Active = "dev", + Profiles = [new EnvProfileDefinition { Id = "dev" }] + } + }; + + var env = EnvProfileService.ResolveEffectiveEnv(config, "missing"); + Assert.Empty(env); + } +} diff --git a/tests/DevTool.Tests/ExitCodeMapperTests.cs b/tests/DevTool.Tests/ExitCodeMapperTests.cs new file mode 100644 index 0000000..2fbbe85 --- /dev/null +++ b/tests/DevTool.Tests/ExitCodeMapperTests.cs @@ -0,0 +1,25 @@ +using Sdt.Core; +using Xunit; + +namespace DevTool.Tests; + +public sealed class ExitCodeMapperTests +{ + [Fact] + public void Success_MapsToZero() + { + Assert.Equal(0, ExitCodeMapper.FromResult(true, null)); + } + + [Theory] + [InlineData(ExecutionStopReason.MissingPrereq, 10)] + [InlineData(ExecutionStopReason.InstallFailed, 11)] + [InlineData(ExecutionStopReason.CommandFailed, 12)] + [InlineData(ExecutionStopReason.ValidationFailed, 13)] + [InlineData(ExecutionStopReason.UserDeclined, 14)] + public void Failure_MapsDeterministically(ExecutionStopReason reason, int expected) + { + Assert.Equal(expected, ExitCodeMapper.FromResult(false, reason)); + } +} + diff --git a/tests/DevTool.Tests/HeadlessExecutionTests.cs b/tests/DevTool.Tests/HeadlessExecutionTests.cs new file mode 100644 index 0000000..2a71268 --- /dev/null +++ b/tests/DevTool.Tests/HeadlessExecutionTests.cs @@ -0,0 +1,186 @@ +using System.Diagnostics; +using Xunit; + +namespace DevTool.Tests; + +public sealed class HeadlessExecutionTests +{ + [Fact] + public async Task HeadlessRun_JsonMode_EmitsVersionedEventsAndSummary() + { + var root = CreateTempProject(""" +{ + "name": "headless-demo", + "version": "0.1.0", + "workflows": [ + { + "id": "build", + "label": "Build", + "description": "Build", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "s1", + "label": "dotnet --version", + "command": "dotnet", + "args": ["--version"], + "workingDir": ".", + "requires": [] + } + ] + } + ] +} +"""); + + var result = await RunSdtAsync(["run", "build", "--json", "--project-root", root]); + + Assert.Equal(0, result.ExitCode); + Assert.Contains("\"run_event_version\":\"1.0\"", result.StdOut, StringComparison.Ordinal); + Assert.Contains("\"event_type\":\"WorkflowStarted\"", result.StdOut, StringComparison.Ordinal); + Assert.Contains("\"category\":\"workflow\"", result.StdOut, StringComparison.Ordinal); + Assert.Contains("\"success\":true", result.StdOut, StringComparison.Ordinal); + } + + [Fact] + public async Task HeadlessRun_NonInteractive_MissingPrereq_UsesDeterministicExitCode() + { + var root = CreateTempProject(""" +{ + "name": "headless-demo", + "version": "0.1.0", + "workflows": [ + { + "id": "build", + "label": "Build", + "description": "Build", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "s1", + "label": "dotnet --version", + "command": "dotnet", + "args": ["--version"], + "workingDir": ".", + "requires": [ + { "tool": "definitely-missing-tool", "installPolicy": "Prompt" } + ] + } + ] + } + ] +} +"""); + + var result = await RunSdtAsync(["run", "build", "--json", "--project-root", root, "--non-interactive"]); + + Assert.Equal(14, result.ExitCode); + Assert.Contains("\"stopReason\":\"UserDeclined\"", result.StdOut, StringComparison.Ordinal); + Assert.Contains("\"exactFixCommand\"", result.StdOut, StringComparison.Ordinal); + } + + [Fact] + public async Task HeadlessDebug_JsonMode_EmitsDebugSummary() + { + var root = CreateTempProject(""" +{ + "name": "headless-demo", + "version": "0.1.0", + "workflows": [], + "debug": { + "profiles": [ + { + "id": "d1", + "label": "Dotnet Info", + "type": "dotnet", + "command": "dotnet", + "args": ["--info"], + "workingDir": ".", + "env": {}, + "requires": [] + } + ] + } +} +"""); + + var result = await RunSdtAsync(["debug", "d1", "--json", "--project-root", root]); + + Assert.Equal(0, result.ExitCode); + Assert.Contains("\"category\":\"debug\"", result.StdOut, StringComparison.Ordinal); + Assert.Contains("\"event_type\":\"DebugStarted\"", result.StdOut, StringComparison.Ordinal); + Assert.Contains("\"success\":true", result.StdOut, StringComparison.Ordinal); + } + + [Fact] + public async Task HeadlessRun_CommandFailure_MapsToDeterministicExitCode() + { + var root = CreateTempProject(""" +{ + "name": "headless-demo", + "version": "0.1.0", + "workflows": [ + { + "id": "build", + "label": "Build", + "description": "Build", + "group": "Build", + "dependsOn": [], + "steps": [ + { + "id": "s1", + "label": "dotnet bad command", + "command": "dotnet", + "args": ["__definitely_invalid_command__"], + "workingDir": ".", + "requires": [] + } + ] + } + ] +} +"""); + + var result = await RunSdtAsync(["run", "build", "--json", "--project-root", root]); + + Assert.Equal(12, result.ExitCode); + Assert.Contains("\"stopReason\":\"CommandFailed\"", result.StdOut, StringComparison.Ordinal); + Assert.Contains("\"retryInstruction\"", result.StdOut, StringComparison.Ordinal); + } + + private static async Task<(int ExitCode, string StdOut, string StdErr)> RunSdtAsync(IReadOnlyList args) + { + var exe = Path.Combine(AppContext.BaseDirectory, OperatingSystem.IsWindows() ? "sdt.exe" : "sdt"); + if (!File.Exists(exe)) + throw new InvalidOperationException($"Could not find test runtime executable: {exe}"); + + var psi = new ProcessStartInfo + { + FileName = exe, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + foreach (var arg in args) + psi.ArgumentList.Add(arg); + + using var process = new Process { StartInfo = psi }; + process.Start(); + var stdout = await process.StandardOutput.ReadToEndAsync(); + var stderr = await process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); + return (process.ExitCode, stdout, stderr); + } + + private static string CreateTempProject(string devtoolJson) + { + var root = Path.Combine(Path.GetTempPath(), "sdt-headless-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + File.WriteAllText(Path.Combine(root, "devtool.json"), devtoolJson); + return root; + } +} diff --git a/tests/DevTool.Tests/LegacyModeTests.cs b/tests/DevTool.Tests/LegacyModeTests.cs new file mode 100644 index 0000000..f4f5a1b --- /dev/null +++ b/tests/DevTool.Tests/LegacyModeTests.cs @@ -0,0 +1,88 @@ +using System.Text.Json; +using Sdt.Config; +using Xunit; + +namespace DevTool.Tests; + +public sealed class LegacyModeTests +{ + [Fact] + public void ConfigLoader_StrictMode_TargetsOnly_FailsAndWritesPreview() + { + var root = CreateTempDir(); + WriteLegacyTargetsOnlyConfig(root); + Environment.SetEnvironmentVariable("SDT_LEGACY_MODE", null); + + var ex = Assert.Throws(() => ConfigLoader.FindAndLoad(root)); + Assert.Contains("Strict mode requires workflows", ex.Message, StringComparison.OrdinalIgnoreCase); + Assert.True(File.Exists(Path.Combine(root, "devtool.generated.workflows.json"))); + } + + [Fact] + public void ConfigLoader_CompatMode_TargetsOnly_Loads() + { + var root = CreateTempDir(); + WriteLegacyTargetsOnlyConfig(root); + Environment.SetEnvironmentVariable("SDT_LEGACY_MODE", "compat"); + try + { + var loaded = ConfigLoader.FindAndLoad(root); + Assert.NotNull(loaded); + Assert.Contains(loaded!.Warnings, w => w.Contains("legacy 'targets'", StringComparison.OrdinalIgnoreCase)); + } + finally + { + Environment.SetEnvironmentVariable("SDT_LEGACY_MODE", null); + } + } + + [Fact] + public void ApplyLegacyTargetMigration_RewritesConfigAndCreatesBackup() + { + var root = CreateTempDir(); + WriteLegacyTargetsOnlyConfig(root); + var path = Path.Combine(root, "devtool.json"); + + var result = ConfigLoader.ApplyLegacyTargetMigration(path, createBackup: true); + + Assert.True(result.Success); + Assert.True(File.Exists(path)); + Assert.False(string.IsNullOrWhiteSpace(result.BackupPath)); + Assert.True(File.Exists(result.BackupPath!)); + + var loaded = ConfigLoader.FindAndLoad(root); + Assert.NotNull(loaded); + Assert.NotEmpty(loaded!.Config.Workflows); + Assert.Empty(loaded.Config.Targets); + } + + private static void WriteLegacyTargetsOnlyConfig(string root) + { + var cfg = new DevToolConfig + { + Name = "legacy", + Version = "0.1.0", + Targets = + [ + new BuildTarget + { + Id = "build", + Label = "Build", + Command = "dotnet", + Args = ["build"] + } + ], + Workflows = [] + }; + + var json = JsonSerializer.Serialize(cfg, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }); + File.WriteAllText(Path.Combine(root, "devtool.json"), json); + } + + private static string CreateTempDir() + { + var path = Path.Combine(Path.GetTempPath(), "sdt-legacy-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(path); + return path; + } +} diff --git a/tests/DevTool.Tests/PrereqInstallerServiceTests.cs b/tests/DevTool.Tests/PrereqInstallerServiceTests.cs new file mode 100644 index 0000000..35583dd --- /dev/null +++ b/tests/DevTool.Tests/PrereqInstallerServiceTests.cs @@ -0,0 +1,159 @@ +using Sdt.Config; +using Sdt.Core; +using Xunit; + +namespace DevTool.Tests; + +public sealed class PrereqInstallerServiceTests +{ + [Fact] + public async Task PreferredInstallCommands_AreUsedFirst() + { + var svc = new PrereqInstallerService(); + var cfg = new DevToolConfig + { + Tooling = new ToolingConfig + { + Tools = + [ + new ToolInstallDefinition + { + Tool = "dotnet", + PreferredInstallCommands = + [ + "echo install dotnet", + "dotnet --info" + ] + } + ] + } + }; + + var plan = await svc.GetInstallPlanAsync("dotnet", Directory.GetCurrentDirectory(), cfg); + + Assert.True(plan.Supported); + Assert.Equal("dotnet", plan.Tool); + Assert.Equal(2, plan.Commands.Count); + Assert.Equal("echo", plan.Commands[0].Command); + Assert.Equal("dotnet", plan.Commands[1].Command); + } + + [Fact] + public async Task PreferredWingetCommand_IsNormalizedForNonInteractiveInstall() + { + var svc = new PrereqInstallerService(); + var cfg = new DevToolConfig + { + Tooling = new ToolingConfig + { + Tools = + [ + new ToolInstallDefinition + { + Tool = "dotnet", + PreferredInstallCommands = + [ + "winget install Microsoft.DotNet.SDK.10" + ] + } + ] + } + }; + + var plan = await svc.GetInstallPlanAsync("dotnet", Directory.GetCurrentDirectory(), cfg); + var args = plan.Commands[0].Args; + Assert.Contains("--accept-package-agreements", args, StringComparer.OrdinalIgnoreCase); + Assert.Contains("--accept-source-agreements", args, StringComparer.OrdinalIgnoreCase); + Assert.Contains("--disable-interactivity", args, StringComparer.OrdinalIgnoreCase); + Assert.Contains("--source", args, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public async Task PreferredShellAptCommand_IsNormalizedForNonInteractiveInstall() + { + var svc = new PrereqInstallerService(); + var cfg = new DevToolConfig + { + Tooling = new ToolingConfig + { + Tools = + [ + new ToolInstallDefinition + { + Tool = "git", + PreferredInstallCommands = + [ + "sh -c \"sudo apt-get update && sudo apt-get install git\"" + ] + } + ] + } + }; + + var plan = await svc.GetInstallPlanAsync("git", Directory.GetCurrentDirectory(), cfg); + Assert.Equal("sh", plan.Commands[0].Command, ignoreCase: true); + Assert.True(plan.Commands[0].Args.Count >= 2); + var script = plan.Commands[0].Args[1]; + Assert.Contains("DEBIAN_FRONTEND=noninteractive", script, StringComparison.OrdinalIgnoreCase); + Assert.Contains("apt-get install -y", script, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task DiagInstallPlanFailure_FallsBackToTemplatePlan() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-prereq-" + Guid.NewGuid().ToString("N")); + var scripts = Path.Combine(root, "scripts"); + Directory.CreateDirectory(scripts); + await File.WriteAllTextAsync(Path.Combine(scripts, "diag.py"), "import sys\nsys.exit(2)\n"); + + var svc = new PrereqInstallerService(); + var plan = await svc.GetInstallPlanAsync("npm", root, new DevToolConfig()); + + Assert.True(plan.Supported); + Assert.NotEmpty(plan.Commands); + Assert.Contains("fallback", plan.Summary, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task DiagInstallPlanInvalidJson_FallsBackToTemplatePlan() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-prereq-" + Guid.NewGuid().ToString("N")); + var scripts = Path.Combine(root, "scripts"); + Directory.CreateDirectory(scripts); + await File.WriteAllTextAsync(Path.Combine(scripts, "diag.py"), "print('not-json')\n"); + + var svc = new PrereqInstallerService(); + var plan = await svc.GetInstallPlanAsync("dotnet", root, new DevToolConfig()); + + Assert.True(plan.Supported); + Assert.NotEmpty(plan.Commands); + Assert.Contains("fallback", plan.Summary, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task TauriFallbackPlan_IsMultiStepAndClear() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-prereq-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + + var svc = new PrereqInstallerService(); + var plan = await svc.GetInstallPlanAsync("tauri", root, new DevToolConfig()); + + Assert.True(plan.Supported); + Assert.True(plan.Commands.Count >= 3); + Assert.Contains("tauri", plan.Summary, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task TauriFallbackPlan_IncludesRustAndCliCommands() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-prereq-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + + var svc = new PrereqInstallerService(); + var plan = await svc.GetInstallPlanAsync("tauri", root, new DevToolConfig()); + + Assert.Contains(plan.Commands, c => c.Args.Any(a => a.Contains("rustup", StringComparison.OrdinalIgnoreCase))); + Assert.Contains(plan.Commands, c => c.Args.Any(a => a.Contains("@tauri-apps/cli", StringComparison.OrdinalIgnoreCase))); + } +} diff --git a/tests/DevTool.Tests/RequirementResolverTests.cs b/tests/DevTool.Tests/RequirementResolverTests.cs new file mode 100644 index 0000000..84de776 --- /dev/null +++ b/tests/DevTool.Tests/RequirementResolverTests.cs @@ -0,0 +1,59 @@ +using Sdt.Config; +using Sdt.Core; +using Xunit; + +namespace DevTool.Tests; + +public sealed class RequirementResolverTests +{ + private readonly RequirementResolver _resolver = new(); + + [Fact] + public void TauriBuildAction_RequiresNodeNpmCargo_NotGlobalTauri() + { + var step = new WorkflowStep + { + Id = "tauri", + Label = "tauri", + Action = "tauri-build", + }; + + var tools = _resolver.Resolve(step).Select(r => r.Tool).ToHashSet(StringComparer.OrdinalIgnoreCase); + Assert.Contains("node", tools); + Assert.Contains("npm", tools); + Assert.Contains("cargo", tools); + Assert.DoesNotContain("tauri", tools); + } + + [Fact] + public void LegacyPwshTarget_InferenceMatchesExpected() + { + var target = new BuildTarget + { + Id = "web", + Label = "Web", + Command = "pwsh", + Args = ["-NoProfile", "-File", "scripts/publish-app.ps1", "-Target", "web"], + }; + + var tools = _resolver.Resolve(target).Select(r => r.Tool).ToHashSet(StringComparer.OrdinalIgnoreCase); + Assert.Contains("python", tools); + Assert.Contains("node", tools); + Assert.Contains("npm", tools); + } + + [Fact] + public void MavenCommand_InferenceRequiresMaven() + { + var step = new WorkflowStep + { + Id = "maven-build", + Label = "maven", + Command = "mvn", + Args = ["package"], + }; + + var tools = _resolver.Resolve(step).Select(r => r.Tool).ToHashSet(StringComparer.OrdinalIgnoreCase); + Assert.Contains("maven", tools); + } +} diff --git a/tests/DevTool.Tests/RunEventJsonlRecorderTests.cs b/tests/DevTool.Tests/RunEventJsonlRecorderTests.cs new file mode 100644 index 0000000..5cecd1b --- /dev/null +++ b/tests/DevTool.Tests/RunEventJsonlRecorderTests.cs @@ -0,0 +1,35 @@ +using System.Text.Json; +using Sdt.Core; +using Xunit; + +namespace DevTool.Tests; + +public sealed class RunEventJsonlRecorderTests +{ + [Fact] + public void Recorder_WritesJsonlEvents() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-events-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + + string path; + using (var recorder = RunEventJsonlRecorder.Create(root, "workflow")) + { + path = recorder.FilePath; + recorder.Write(new RunEvent("workflow", RunEventType.WorkflowStarted, "started", WorkflowId: "build")); + recorder.Write(new RunEvent("workflow", RunEventType.WorkflowCompleted, "done", WorkflowId: "build", Success: true)); + } + + Assert.True(File.Exists(path)); + var lines = File.ReadAllLines(path); + Assert.Equal(2, lines.Length); + + using var doc = JsonDocument.Parse(lines[0]); + Assert.Equal("workflow", doc.RootElement.GetProperty("category").GetString()); + Assert.Equal("WorkflowStarted", doc.RootElement.GetProperty("type").GetString()); + Assert.Equal("1.0", doc.RootElement.GetProperty("run_event_version").GetString()); + Assert.True(doc.RootElement.TryGetProperty("run_id", out _)); + Assert.True(doc.RootElement.TryGetProperty("timestamp_utc", out _)); + Assert.True(doc.RootElement.TryGetProperty("event_type", out _)); + } +} diff --git a/tests/DevTool.Tests/RunEventLogReaderTests.cs b/tests/DevTool.Tests/RunEventLogReaderTests.cs new file mode 100644 index 0000000..6403fc0 --- /dev/null +++ b/tests/DevTool.Tests/RunEventLogReaderTests.cs @@ -0,0 +1,70 @@ +using Sdt.Core; +using Xunit; + +namespace DevTool.Tests; + +public sealed class RunEventLogReaderTests +{ + [Fact] + public void ReadEvents_ParsesValidJsonlLines() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-logreader-" + Guid.NewGuid().ToString("N")); + var dir = Path.Combine(root, ".sdt", "events"); + Directory.CreateDirectory(dir); + var file = Path.Combine(dir, "workflow-test.jsonl"); + File.WriteAllLines(file, + [ + """{"category":"workflow","type":"WorkflowStarted","message":"start","workflowId":"build","occurredAt":"2026-03-01T10:00:00Z"}""", + """{"category":"workflow","type":"WorkflowCompleted","message":"done","workflowId":"build","success":true,"exitCode":0,"occurredAt":"2026-03-01T10:00:01Z"}""" + ]); + + var reader = new RunEventLogReader(); + var events = reader.ReadEvents(file); + + Assert.Equal(2, events.Count); + Assert.Equal(RunEventType.WorkflowStarted, events[0].Type); + Assert.Equal(RunEventType.WorkflowCompleted, events[1].Type); + Assert.True(events[1].Success); + } + + [Fact] + public void ReadEvents_ParsesVersionedEventFields() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-logreader-" + Guid.NewGuid().ToString("N")); + var dir = Path.Combine(root, ".sdt", "events"); + Directory.CreateDirectory(dir); + var file = Path.Combine(dir, "workflow-test-v1.jsonl"); + File.WriteAllLines(file, + [ + """{"category":"workflow","event_type":"WorkflowStarted","message":"start","run_event_version":"1.0","run_id":"abc","project_root":"C:/repo","timestamp_utc":"2026-03-01T10:00:00Z"}""" + ]); + + var reader = new RunEventLogReader(); + var events = reader.ReadEvents(file); + + Assert.Single(events); + Assert.Equal(RunEventType.WorkflowStarted, events[0].Type); + Assert.Equal("abc", events[0].EventRunId); + Assert.Equal("C:/repo", events[0].EventProjectRoot); + Assert.Equal("1.0", events[0].EventVersion); + } + + [Fact] + public void ListEventFiles_ReturnsNewestFirst() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-logreader-" + Guid.NewGuid().ToString("N")); + var dir = Path.Combine(root, ".sdt", "events"); + Directory.CreateDirectory(dir); + var older = Path.Combine(dir, "older.jsonl"); + var newer = Path.Combine(dir, "newer.jsonl"); + File.WriteAllText(older, "{}"); + Thread.Sleep(20); + File.WriteAllText(newer, "{}"); + + var reader = new RunEventLogReader(); + var files = reader.ListEventFiles(root); + + Assert.True(files.Count >= 2); + Assert.Equal("newer.jsonl", files[0].Name); + } +} diff --git a/tests/DevTool.Tests/RunHistoryTests.cs b/tests/DevTool.Tests/RunHistoryTests.cs new file mode 100644 index 0000000..4182701 --- /dev/null +++ b/tests/DevTool.Tests/RunHistoryTests.cs @@ -0,0 +1,52 @@ +using Sdt.Core; +using Xunit; + +namespace DevTool.Tests; + +public sealed class RunHistoryTests +{ + [Fact] + public void ListRunHistory_ParsesWorkflowTarget() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-history-" + Guid.NewGuid().ToString("N")); + var dir = Path.Combine(root, ".sdt", "events"); + Directory.CreateDirectory(dir); + var file = Path.Combine(dir, "workflow-test.jsonl"); + File.WriteAllLines(file, + [ + """{"category":"workflow","event_type":"WorkflowStarted","message":"Workflow 'build' started.","workflowId":"build","run_event_version":"1.0","run_id":"rid-1","project_root":"C:/repo","timestamp_utc":"2026-03-01T10:00:00Z"}""", + """{"category":"workflow","event_type":"WorkflowCompleted","message":"Workflow completed successfully.","workflowId":"build","success":true,"exitCode":0,"run_event_version":"1.0","run_id":"rid-1","project_root":"C:/repo","timestamp_utc":"2026-03-01T10:00:01Z"}""" + ]); + + var reader = new RunEventLogReader(); + var history = reader.ListRunHistory(root); + + Assert.Single(history); + Assert.Equal("workflow", history[0].Category); + Assert.Equal("build", history[0].TargetId); + Assert.True(history[0].Success); + } + + [Fact] + public void ListRunHistory_ParsesDebugProfileFromMessage() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-history-" + Guid.NewGuid().ToString("N")); + var dir = Path.Combine(root, ".sdt", "events"); + Directory.CreateDirectory(dir); + var file = Path.Combine(dir, "debug-test.jsonl"); + File.WriteAllLines(file, + [ + """{"category":"debug","event_type":"DebugStarted","message":"Debug profile 'dotnet-run' started.","run_event_version":"1.0","run_id":"rid-2","project_root":"C:/repo","timestamp_utc":"2026-03-01T10:00:00Z"}""", + """{"category":"debug","event_type":"DebugCompleted","message":"Debug run completed.","success":true,"exitCode":0,"run_event_version":"1.0","run_id":"rid-2","project_root":"C:/repo","timestamp_utc":"2026-03-01T10:00:01Z"}""" + ]); + + var reader = new RunEventLogReader(); + var history = reader.ListRunHistory(root); + + Assert.Single(history); + Assert.Equal("debug", history[0].Category); + Assert.Equal("dotnet-run", history[0].TargetId); + Assert.True(history[0].Success); + } +} + diff --git a/tests/DevTool.Tests/RuntimePolicyTests.cs b/tests/DevTool.Tests/RuntimePolicyTests.cs new file mode 100644 index 0000000..e7b70af --- /dev/null +++ b/tests/DevTool.Tests/RuntimePolicyTests.cs @@ -0,0 +1,23 @@ +using Sdt.Core; +using Xunit; + +namespace DevTool.Tests; + +public sealed class RuntimePolicyTests +{ + [Fact] + public void IsNonInteractive_True_WhenEnvVarSet() + { + Environment.SetEnvironmentVariable("SDT_NONINTERACTIVE", "1"); + Assert.True(RuntimePolicy.IsNonInteractive()); + Environment.SetEnvironmentVariable("SDT_NONINTERACTIVE", null); + } + + [Fact] + public void IsNonInteractive_True_WhenCliOverrideProvided() + { + Environment.SetEnvironmentVariable("SDT_NONINTERACTIVE", null); + Assert.True(RuntimePolicy.IsNonInteractive(cliOverride: true)); + } +} + diff --git a/tests/DevTool.Tests/ScriptCommonTests.cs b/tests/DevTool.Tests/ScriptCommonTests.cs new file mode 100644 index 0000000..49dd87c --- /dev/null +++ b/tests/DevTool.Tests/ScriptCommonTests.cs @@ -0,0 +1,159 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Xunit; + +namespace DevTool.Tests; + +public sealed class ScriptCommonTests +{ + [Fact] + public async Task ResolveRepoRoot_UsesGlobRootHints() + { + var root = CreateTempDir("sdt-script-root-"); + var nested = Path.Combine(root, "src", "app"); + Directory.CreateDirectory(nested); + await File.WriteAllTextAsync(Path.Combine(root, "sample.sln"), ""); + await File.WriteAllTextAsync(Path.Combine(root, "devtool.json"), """ +{ + "name": "demo", + "version": "0.1.0", + "workflows": [], + "project": { + "rootHints": ["*.sln"] + } +} +"""); + + var output = await RunPythonAsync(nested, "import script_common; print(script_common.resolve_repo_root(r'" + Escape(nested) + "'))"); + Assert.Equal(Path.GetFullPath(root), output.Trim()); + } + + [Fact] + public async Task ResolveRepoRoot_UsesDirectoryMarkerHints() + { + var root = CreateTempDir("sdt-script-root-"); + var nested = Path.Combine(root, "child", "leaf"); + Directory.CreateDirectory(nested); + Directory.CreateDirectory(Path.Combine(root, ".git")); + await File.WriteAllTextAsync(Path.Combine(root, "devtool.json"), """ +{ + "name": "demo", + "version": "0.1.0", + "workflows": [], + "project": { + "rootHints": [".git", "package.json"] + } +} +"""); + + var output = await RunPythonAsync(nested, "import script_common; print(script_common.resolve_repo_root(r'" + Escape(nested) + "'))"); + Assert.Equal(Path.GetFullPath(root), output.Trim()); + } + + [Fact] + public async Task ResolveCommand_ExpandsWindowsPathTokens() + { + if (!OperatingSystem.IsWindows()) + return; + + var root = CreateTempDir("sdt-script-cmd-"); + var shimDir = Path.Combine(root, "nodejs"); + Directory.CreateDirectory(shimDir); + await File.WriteAllTextAsync(Path.Combine(shimDir, "npm.cmd"), "@echo off"); + + var output = await RunPythonAsync( + root, + "import script_common; print(script_common.resolve_command('npm'))", + new Dictionary + { + ["NVM_HOME"] = root, + ["NVM_SYMLINK"] = shimDir, + ["PATH"] = "%NVM_HOME%;%NVM_SYMLINK%", + }); + + Assert.EndsWith("npm.cmd", output.Trim(), StringComparison.OrdinalIgnoreCase); + } + + private static async Task RunPythonAsync( + string workingDir, + string script, + IReadOnlyDictionary? env = null) + { + var python = ResolvePython(); + var psi = new ProcessStartInfo + { + FileName = python, + WorkingDirectory = workingDir, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + psi.ArgumentList.Add("-c"); + psi.ArgumentList.Add($"import sys; sys.path.insert(0, r'{Escape(Path.Combine(ProjectRepoRoot(), "scripts"))}'); {script}"); + + if (env is not null) + { + foreach (var pair in env) + psi.Environment[pair.Key] = pair.Value ?? string.Empty; + } + + using var process = new Process { StartInfo = psi }; + process.Start(); + var stdout = await process.StandardOutput.ReadToEndAsync(); + var stderr = await process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); + if (process.ExitCode != 0) + throw new InvalidOperationException($"Python exited {process.ExitCode}: {stderr}"); + return stdout; + } + + private static string ResolvePython() + { + var candidates = OperatingSystem.IsWindows() ? new[] { "python", "py" } : new[] { "python3", "python" }; + foreach (var candidate in candidates) + { + try + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = candidate, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.StartInfo.ArgumentList.Add("--version"); + process.Start(); + process.WaitForExit(2000); + if (process.ExitCode == 0) + return candidate; + } + catch + { + } + } + + throw new InvalidOperationException("Python executable not found."); + } + + private static string CreateTempDir(string prefix) + { + var path = Path.Combine(Path.GetTempPath(), prefix + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(path); + return path; + } + + private static string Escape(string value) => value.Replace("\\", "\\\\").Replace("'", "\\'"); + + private static string ProjectRepoRoot([CallerFilePath] string file = "") + { + var repoRoot = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(file)!, "..", "..")); + if (!File.Exists(Path.Combine(repoRoot, "scripts", "script_common.py"))) + throw new InvalidOperationException("Could not locate project repo root."); + return repoRoot; + } +} diff --git a/tests/DevTool.Tests/ScriptSmokeTests.cs b/tests/DevTool.Tests/ScriptSmokeTests.cs new file mode 100644 index 0000000..9f869a0 --- /dev/null +++ b/tests/DevTool.Tests/ScriptSmokeTests.cs @@ -0,0 +1,349 @@ +using System.Diagnostics; +using System.Text.Json; +using System.Runtime.CompilerServices; +using Xunit; + +namespace DevTool.Tests; + +public sealed class ScriptSmokeTests +{ + [Fact] + public async Task DiagProbe_JsonContract_IsValid() + { + var python = ResolvePython(); + var result = await RunAsync( + python, + ["scripts/diag.py", "probe", "--tool", "python", "--json"]); + + Assert.Equal(0, result.ExitCode); + using var doc = JsonDocument.Parse(result.StdOut); + Assert.True(doc.RootElement.TryGetProperty("tool", out _)); + Assert.True(doc.RootElement.TryGetProperty("available", out _)); + } + + [Fact] + public async Task BuildAction_InvalidRequirements_PropagatesNonZeroAndJson() + { + var python = ResolvePython(); + var missingReq = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".txt"); + + var result = await RunAsync( + python, + [ + "scripts/build.py", + "python-pip-install", + "--project-root", + RepoRoot(), + "--requirements", + missingReq, + "--json" + ]); + + Assert.NotEqual(0, result.ExitCode); + var jsonText = ExtractLastJsonObject(result.StdOut); + using var doc = JsonDocument.Parse(jsonText); + Assert.True(doc.RootElement.TryGetProperty("exit_code", out var code)); + Assert.NotEqual(0, code.GetInt32()); + Assert.True(doc.RootElement.TryGetProperty("failure_reason", out _)); + } + + [Fact] + public async Task BuildAction_DotnetRestore_CommandNotFoundStillReturnsJson() + { + var python = ResolvePython(); + + var result = await RunAsync( + python, + [ + "scripts/build.py", + "dotnet-restore", + "--project-root", + RepoRoot(), + "--json" + ]); + + var jsonText = ExtractLastJsonObject(result.StdOut); + using var doc = JsonDocument.Parse(jsonText); + Assert.True(doc.RootElement.TryGetProperty("exit_code", out _)); + Assert.True(doc.RootElement.TryGetProperty("status", out _)); + } + + [Fact] + public async Task BuildAction_DotnetBuild_AutoSelectsSlnTarget_WhenSingleSlnFound() + { + var python = ResolvePython(); + var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-dotnet-target-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempRoot); + var sln = Path.Combine(tempRoot, "sample.sln"); + await File.WriteAllTextAsync(sln, "Microsoft Visual Studio Solution File, Format Version 12.00"); + + var result = await RunAsync( + python, + [ + "scripts/build.py", + "dotnet-build", + "--project-root", + tempRoot, + "--working-dir", + ".", + "--json" + ]); + + var jsonText = ExtractLastJsonObject(result.StdOut); + using var doc = JsonDocument.Parse(jsonText); + Assert.True(doc.RootElement.TryGetProperty("args", out var args)); + Assert.Contains(args.EnumerateArray().Select(x => x.GetString()), x => string.Equals(x, sln, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task BuildAction_DotnetBuild_AutoSelectsSlnxTarget_WhenSingleSlnxFound() + { + var python = ResolvePython(); + var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-dotnet-target-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempRoot); + var slnx = Path.Combine(tempRoot, "sample.slnx"); + await File.WriteAllTextAsync(slnx, ""); + + var result = await RunAsync( + python, + [ + "scripts/build.py", + "dotnet-build", + "--project-root", + tempRoot, + "--working-dir", + ".", + "--json" + ]); + + var jsonText = ExtractLastJsonObject(result.StdOut); + using var doc = JsonDocument.Parse(jsonText); + Assert.True(doc.RootElement.TryGetProperty("args", out var args)); + Assert.Contains(args.EnumerateArray().Select(x => x.GetString()), x => string.Equals(x, slnx, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task BuildAction_DotnetBuild_FallsBackToCsproj_WhenNoSolutionFound() + { + var python = ResolvePython(); + var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-dotnet-target-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempRoot); + var csproj = Path.Combine(tempRoot, "sample.csproj"); + await File.WriteAllTextAsync(csproj, ""); + + var result = await RunAsync( + python, + [ + "scripts/build.py", + "dotnet-build", + "--project-root", + tempRoot, + "--working-dir", + ".", + "--json" + ]); + + var jsonText = ExtractLastJsonObject(result.StdOut); + using var doc = JsonDocument.Parse(jsonText); + Assert.True(doc.RootElement.TryGetProperty("args", out var args)); + Assert.Contains(args.EnumerateArray().Select(x => x.GetString()), x => string.Equals(x, csproj, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task BuildAction_DotnetBuild_Skips_WhenNoDotnetTargetFound() + { + var python = ResolvePython(); + var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-dotnet-target-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempRoot); + + var result = await RunAsync( + python, + [ + "scripts/build.py", + "dotnet-build", + "--project-root", + tempRoot, + "--working-dir", + ".", + "--json" + ]); + + Assert.Equal(0, result.ExitCode); + var jsonText = ExtractLastJsonObject(result.StdOut); + using var doc = JsonDocument.Parse(jsonText); + Assert.Equal("skipped", doc.RootElement.GetProperty("status").GetString()); + Assert.Equal("not_applicable_no_dotnet_target", doc.RootElement.GetProperty("skip_reason").GetString()); + } + + [Fact] + public async Task BuildAction_NpmBuild_Skips_WhenNoPackageJsonInWorkingDir() + { + var python = ResolvePython(); + var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-node-target-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempRoot); + + var result = await RunAsync( + python, + [ + "scripts/build.py", + "npm-build", + "--project-root", + tempRoot, + "--working-dir", + ".", + "--json" + ]); + + Assert.Equal(0, result.ExitCode); + var jsonText = ExtractLastJsonObject(result.StdOut); + using var doc = JsonDocument.Parse(jsonText); + Assert.Equal("skipped", doc.RootElement.GetProperty("status").GetString()); + Assert.Equal("not_applicable_no_package_json", doc.RootElement.GetProperty("skip_reason").GetString()); + } + + [Fact] + public async Task BuildAction_NpmBuild_Skips_WhenBuildScriptMissing() + { + var python = ResolvePython(); + var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-node-target-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempRoot); + await File.WriteAllTextAsync(Path.Combine(tempRoot, "package.json"), """{ "name": "demo", "dependencies": { "x": "1.0.0" } }"""); + + var result = await RunAsync( + python, + [ + "scripts/build.py", + "npm-build", + "--project-root", + tempRoot, + "--working-dir", + ".", + "--json" + ]); + + Assert.Equal(0, result.ExitCode); + var jsonText = ExtractLastJsonObject(result.StdOut); + using var doc = JsonDocument.Parse(jsonText); + Assert.Equal("skipped", doc.RootElement.GetProperty("status").GetString()); + Assert.Equal("not_applicable_missing_build_script", doc.RootElement.GetProperty("skip_reason").GetString()); + } + + [Fact] + public async Task PublishOutput_SkipsNonApplicableStacks_InGenericRepo() + { + var python = ResolvePython(); + var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-publish-output-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempRoot); + + var result = await RunAsync( + python, + [ + "scripts/publish-output.py", + "--repo-root", + tempRoot + ]); + + Assert.Equal(0, result.ExitCode); + Assert.Contains("Publish output workflow complete.", result.StdOut, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Skipping sidecar", result.StdOut, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task PublishOutput_SkipsWeb_WhenBuildScriptMissing() + { + var python = ResolvePython(); + var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-publish-output-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempRoot); + await File.WriteAllTextAsync(Path.Combine(tempRoot, "package.json"), """{ "name": "demo", "scripts": { "test": "echo ok" } }"""); + + var result = await RunAsync( + python, + [ + "scripts/publish-output.py", + "--repo-root", + tempRoot + ]); + + Assert.Equal(0, result.ExitCode); + Assert.Contains("Skipping web: package.json has no 'build' script.", result.StdOut, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Publish output workflow complete.", result.StdOut, StringComparison.OrdinalIgnoreCase); + } + + private static string ResolvePython() + { + var candidates = OperatingSystem.IsWindows() + ? new[] { "python", "py" } + : new[] { "python3", "python" }; + + foreach (var candidate in candidates) + { + try + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = candidate, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.StartInfo.ArgumentList.Add("--version"); + process.Start(); + process.WaitForExit(2000); + if (process.ExitCode == 0) + return candidate; + } + catch + { + } + } + + throw new InvalidOperationException("Python executable not found for script smoke tests."); + } + + private static async Task<(int ExitCode, string StdOut, string StdErr)> RunAsync(string command, IReadOnlyList args) + { + var psi = new ProcessStartInfo + { + FileName = command, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = RepoRoot(), + }; + foreach (var arg in args) + psi.ArgumentList.Add(arg); + + using var process = new Process { StartInfo = psi }; + process.Start(); + var stdout = await process.StandardOutput.ReadToEndAsync(); + var stderr = await process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); + return (process.ExitCode, stdout, stderr); + } + + private static string RepoRoot([CallerFilePath] string file = "") + { + var repoRoot = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(file)!, "..", "..")); + if (!File.Exists(Path.Combine(repoRoot, "scripts", "diag.py"))) + throw new InvalidOperationException("Could not locate repo root (scripts/diag.py not found)."); + return repoRoot; + } + + private static string ExtractLastJsonObject(string text) + { + var lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + for (var i = lines.Length - 1; i >= 0; i--) + { + var line = lines[i]; + if (line.StartsWith("{", StringComparison.Ordinal) && line.EndsWith("}", StringComparison.Ordinal)) + return line; + } + + throw new InvalidOperationException("No JSON object line found in script output."); + } +} diff --git a/tests/DevTool.Tests/SetupStateServiceTests.cs b/tests/DevTool.Tests/SetupStateServiceTests.cs new file mode 100644 index 0000000..ad7b8f7 --- /dev/null +++ b/tests/DevTool.Tests/SetupStateServiceTests.cs @@ -0,0 +1,19 @@ +using Sdt.Core; +using Xunit; + +namespace DevTool.Tests; + +public sealed class SetupStateServiceTests +{ + [Fact] + public void MarkCompleted_CreatesStateFile_AndIsFirstRunBecomesFalse() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-setupstate-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + + Assert.True(SetupStateService.IsFirstRun(root)); + SetupStateService.MarkCompleted(root, "0.1.0"); + Assert.False(SetupStateService.IsFirstRun(root)); + Assert.True(File.Exists(SetupStateService.GetStatePath(root))); + } +} diff --git a/tests/DevTool.Tests/SetupWizardConfigServiceTests.cs b/tests/DevTool.Tests/SetupWizardConfigServiceTests.cs new file mode 100644 index 0000000..f16700a --- /dev/null +++ b/tests/DevTool.Tests/SetupWizardConfigServiceTests.cs @@ -0,0 +1,49 @@ +using Sdt.Config; +using Sdt.Core; +using Xunit; + +namespace DevTool.Tests; + +public sealed class SetupWizardConfigServiceTests +{ + [Fact] + public void ApplyRecommendedDefaults_AddsEnvProfilesAndToolingAndDiagnostics() + { + var config = new DevToolConfig + { + Workflows = + [ + new WorkflowDefinition + { + Id = "build", + Label = "Build", + Steps = + [ + new WorkflowStep + { + Id = "s1", + Label = "S1", + Action = "dotnet-build" + } + ] + } + ], + Tooling = null, + EnvProfiles = null, + Debug = null, + Env = [] + }; + + var service = new SetupWizardConfigService(new RequirementResolver()); + var result = service.ApplyRecommendedDefaults(config); + + Assert.NotEmpty(result.Changes); + Assert.NotNull(result.Config.EnvProfiles); + Assert.Contains(result.Config.EnvProfiles!.Profiles, p => p.Id == "dev"); + Assert.NotNull(result.Config.Tooling); + Assert.Contains(result.Config.Tooling!.Tools, t => t.Tool == "dotnet"); + Assert.NotNull(result.Config.Debug); + Assert.True(result.Config.Debug!.Diagnostics.RedactSensitive); + Assert.Contains(result.Config.Env, e => e.Key == "SDT_ENV_PROFILE"); + } +} diff --git a/tests/DevTool.Tests/ToolchainManagerServiceTests.cs b/tests/DevTool.Tests/ToolchainManagerServiceTests.cs new file mode 100644 index 0000000..1abf574 --- /dev/null +++ b/tests/DevTool.Tests/ToolchainManagerServiceTests.cs @@ -0,0 +1,113 @@ +using Sdt.Config; +using Sdt.Core; +using Sdt.Runner; +using Xunit; + +namespace DevTool.Tests; + +public sealed class ToolchainManagerServiceTests +{ + [Fact] + public async Task ProbeConfiguredTools_IncludesToolchainAndWorkflowRequirements() + { + var config = new DevToolConfig + { + Toolchains = new ToolchainConfig + { + Python = new PythonToolchain(), + Node = new NodeToolchain { PackageManager = "npm" } + }, + Workflows = + [ + new WorkflowDefinition + { + Id = "build", + Label = "Build", + Steps = + [ + new WorkflowStep + { + Id = "s1", + Label = "S1", + Requires = [new ToolRequirement { Tool = "dotnet" }] + } + ] + } + ] + }; + + var service = new ToolchainManagerService(new AvailableProbe(), new NoOpInstaller()); + var probes = await service.ProbeConfiguredToolsAsync(config, Directory.GetCurrentDirectory()); + + var tools = probes.Select(p => p.Tool).ToHashSet(StringComparer.OrdinalIgnoreCase); + Assert.Contains("python", tools); + Assert.Contains("node", tools); + Assert.Contains("npm", tools); + Assert.Contains("dotnet", tools); + } + + [Fact] + public async Task AutoFixMissingTools_AttemptsInstallAndVerifies() + { + var config = new DevToolConfig + { + Toolchains = new ToolchainConfig + { + Node = new NodeToolchain { PackageManager = "npm" } + } + }; + + var probe = new SequenceProbe(); + var installer = new SuccessInstaller(); + var service = new ToolchainManagerService(probe, installer); + + var results = await service.AutoFixMissingToolsAsync( + config, + Directory.GetCurrentDirectory(), + (_, _) => Task.FromResult(true), + (_, _) => { }); + + Assert.Contains(results, r => r.Tool.Equals("npm", StringComparison.OrdinalIgnoreCase) && r.Success); + } + + private sealed class AvailableProbe : IToolProbe + { + public Task ProbeAsync(string tool, string projectRoot, DevToolConfig? config = null, IReadOnlyDictionary? envOverrides = null, CancellationToken cancellationToken = default) + => Task.FromResult(new ProbeResult(tool, true, Version: "1.0.0")); + } + + private sealed class SequenceProbe : IToolProbe + { + private readonly Dictionary _count = new(StringComparer.OrdinalIgnoreCase); + + public Task ProbeAsync(string tool, string projectRoot, DevToolConfig? config = null, IReadOnlyDictionary? envOverrides = null, CancellationToken cancellationToken = default) + { + _count.TryGetValue(tool, out var c); + _count[tool] = c + 1; + if (tool.Equals("node", StringComparison.OrdinalIgnoreCase)) + return Task.FromResult(new ProbeResult(tool, true, Version: "1.0.0")); + + // npm: first probe missing, later available (after install). + var available = c > 0; + return Task.FromResult(new ProbeResult(tool, available, Version: available ? "1.0.0" : null)); + } + } + + private sealed class NoOpInstaller : IPrereqInstaller + { + public Task GetInstallPlanAsync(string tool, string projectRoot, DevToolConfig? config = null, CancellationToken cancellationToken = default) + => Task.FromResult(new InstallPlan(tool, true, "noop", [new InstallCommand("echo", ["ok"])])); + + public Task RunInstallAsync(InstallCommand command, string projectRoot, Action onOutput, IReadOnlyDictionary? envOverrides = null, CancellationToken cancellationToken = default) + => Task.FromResult(new RunResult(0, TimeSpan.Zero)); + } + + private sealed class SuccessInstaller : IPrereqInstaller + { + public Task GetInstallPlanAsync(string tool, string projectRoot, DevToolConfig? config = null, CancellationToken cancellationToken = default) + => Task.FromResult(new InstallPlan(tool, true, "install", [new InstallCommand("echo", ["install"])])); + + public Task RunInstallAsync(InstallCommand command, string projectRoot, Action onOutput, IReadOnlyDictionary? envOverrides = null, CancellationToken cancellationToken = default) + => Task.FromResult(new RunResult(0, TimeSpan.FromMilliseconds(5))); + } +} diff --git a/tests/DevTool.Tests/WorkflowExecutorTests.cs b/tests/DevTool.Tests/WorkflowExecutorTests.cs new file mode 100644 index 0000000..f02d170 --- /dev/null +++ b/tests/DevTool.Tests/WorkflowExecutorTests.cs @@ -0,0 +1,316 @@ +using Sdt.Config; +using Sdt.Core; +using Sdt.Runner; +using Xunit; + +namespace DevTool.Tests; + +public sealed class WorkflowExecutorTests +{ + [Fact] + public async Task MissingPrereq_UserDeclines_ReturnsUserDeclined() + { + var executor = new WorkflowExecutor( + new WorkflowPlanner(), + new FakeProbeService(isAvailable: false), + new FakeInstallerService(success: true), + new FakeActionRunner(success: true), + new RequirementResolver()); + + var cfg = new DevToolConfig(); + var wf = BuildSingleStepWorkflow("w", "dotnet"); + var map = new Dictionary(StringComparer.OrdinalIgnoreCase) { [wf.Id] = wf }; + + var result = await executor.ExecuteAsync( + wf, map, cfg, ".", (_, _) => Task.FromResult(false), (_, _) => { }); + + Assert.False(result.Success); + Assert.Equal(ExecutionStopReason.UserDeclined, result.StopReason); + } + + [Fact] + public async Task MissingPrereq_InstallFails_ReturnsInstallFailed() + { + var executor = new WorkflowExecutor( + new WorkflowPlanner(), + new FakeProbeService(isAvailable: false), + new FakeInstallerService(success: false), + new FakeActionRunner(success: true), + new RequirementResolver()); + + var cfg = new DevToolConfig(); + var wf = BuildSingleStepWorkflow("w", "dotnet"); + var map = new Dictionary(StringComparer.OrdinalIgnoreCase) { [wf.Id] = wf }; + + var result = await executor.ExecuteAsync( + wf, map, cfg, ".", (_, _) => Task.FromResult(true), (_, _) => { }); + + Assert.False(result.Success); + Assert.Equal(ExecutionStopReason.InstallFailed, result.StopReason); + } + + [Fact] + public async Task StepFailure_StopsImmediately() + { + var executor = new WorkflowExecutor( + new WorkflowPlanner(), + new FakeProbeService(isAvailable: true), + new FakeInstallerService(success: true), + new FakeActionRunner(success: false), + new RequirementResolver()); + + var wf = new WorkflowDefinition + { + Id = "w", + Label = "W", + Steps = + [ + new WorkflowStep { Id = "s1", Label = "S1", Command = "dotnet", Args = ["build"] }, + new WorkflowStep { Id = "s2", Label = "S2", Command = "dotnet", Args = ["build"] }, + ] + }; + var map = new Dictionary(StringComparer.OrdinalIgnoreCase) { [wf.Id] = wf }; + + var result = await executor.ExecuteAsync( + wf, map, new DevToolConfig(), ".", (_, _) => Task.FromResult(true), (_, _) => { }); + + Assert.False(result.Success); + Assert.Equal(ExecutionStopReason.CommandFailed, result.StopReason); + Assert.Single(result.Steps); + } + + [Fact] + public async Task LegacyPwshScriptStep_MissingPrereq_PromptsBeforeRun() + { + var executor = new WorkflowExecutor( + new WorkflowPlanner(), + new FakeProbeService(isAvailable: false), + new FakeInstallerService(success: true), + new FakeActionRunner(success: true), + new RequirementResolver()); + + var wf = new WorkflowDefinition + { + Id = "w", + Label = "W", + Steps = + [ + new WorkflowStep + { + Id = "ps1", + Label = "Legacy PS1", + Command = "pwsh", + Args = ["-NoProfile", "-File", "scripts/publish-app.ps1", "-Target", "web"], + } + ] + }; + var map = new Dictionary(StringComparer.OrdinalIgnoreCase) { [wf.Id] = wf }; + + var result = await executor.ExecuteAsync( + wf, map, new DevToolConfig(), ".", (_, _) => Task.FromResult(false), (_, _) => { }); + + Assert.False(result.Success); + Assert.Equal(ExecutionStopReason.UserDeclined, result.StopReason); + } + + [Fact] + public async Task TauriBuild_DoesNotRequireGlobalTauri_WhenNodeNpmCargoAvailable() + { + var executor = new WorkflowExecutor( + new WorkflowPlanner(), + new ConditionalProbeService(), + new FakeInstallerService(success: true), + new FakeActionRunner(success: true), + new RequirementResolver()); + + var wf = new WorkflowDefinition + { + Id = "w", + Label = "W", + Steps = + [ + new WorkflowStep + { + Id = "tauri", + Label = "Tauri Build", + Action = "tauri-build", + } + ] + }; + var map = new Dictionary(StringComparer.OrdinalIgnoreCase) { [wf.Id] = wf }; + + var result = await executor.ExecuteAsync( + wf, map, new DevToolConfig(), ".", (_, _) => Task.FromResult(true), (_, _) => { }); + + Assert.True(result.Success); + Assert.Null(result.StopReason); + Assert.Single(result.Steps); + } + + [Fact] + public async Task MissingPrereq_EmitsProbeDiagnosticsToOutput() + { + var executor = new WorkflowExecutor( + new WorkflowPlanner(), + new DetailedProbeService(), + new FakeInstallerService(success: true), + new FakeActionRunner(success: true), + new RequirementResolver()); + + var wf = BuildSingleStepWorkflow("w", "dotnet"); + var map = new Dictionary(StringComparer.OrdinalIgnoreCase) { [wf.Id] = wf }; + var lines = new List(); + + var result = await executor.ExecuteAsync( + wf, map, new DevToolConfig(), ".", (_, _) => Task.FromResult(false), (line, _) => lines.Add(line)); + + Assert.False(result.Success); + Assert.Contains(lines, l => l.Contains("Probe detail [dotnet]", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task ExecuteAsync_EmitsRunEvents_ForStepLifecycle() + { + var executor = new WorkflowExecutor( + new WorkflowPlanner(), + new FakeProbeService(isAvailable: true), + new FakeInstallerService(success: true), + new FakeActionRunner(success: true), + new RequirementResolver()); + + var wf = BuildSingleStepWorkflow("w", "dotnet"); + var map = new Dictionary(StringComparer.OrdinalIgnoreCase) { [wf.Id] = wf }; + var events = new List(); + + var result = await executor.ExecuteAsync( + wf, map, new DevToolConfig(), ".", (_, _) => Task.FromResult(true), (_, _) => { }, events.Add); + + Assert.True(result.Success); + Assert.Contains(events, e => e.Type == RunEventType.WorkflowStarted); + Assert.Contains(events, e => e.Type == RunEventType.WorkflowStepStarted); + Assert.Contains(events, e => e.Type == RunEventType.WorkflowStepCompleted); + Assert.Contains(events, e => e.Type == RunEventType.WorkflowCompleted && e.Success == true); + } + + [Fact] + public void AggregatorWorkflow_ExecutesDependenciesOnly() + { + var planner = new WorkflowPlanner(); + var dep = new WorkflowDefinition + { + Id = "dep", + Label = "Dependency", + Steps = [new WorkflowStep { Id = "s", Label = "S", Command = "dotnet", Args = ["build"] }] + }; + var agg = new WorkflowDefinition + { + Id = "agg", + Label = "Aggregator", + DependsOn = ["dep"], + Steps = [] + }; + + var map = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [dep.Id] = dep, + [agg.Id] = agg + }; + + var plan = planner.ResolvePlan(agg, map); + + Assert.Single(plan); + Assert.Equal("dep", plan[0].Id); + } + + private static WorkflowDefinition BuildSingleStepWorkflow(string id, string tool) + { + return new WorkflowDefinition + { + Id = id, + Label = id, + Steps = + [ + new WorkflowStep + { + Id = "step", + Label = "step", + Command = tool, + Args = ["--version"], + Requires = [new ToolRequirement { Tool = tool, InstallPolicy = InstallPolicy.Prompt }], + } + ] + }; + } + + private sealed class FakeProbeService(bool isAvailable) : IToolProbe + { + public Task ProbeAsync( + string tool, + string projectRoot, + DevToolConfig? config = null, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + => Task.FromResult(new ProbeResult(tool, isAvailable, Version: isAvailable ? "1.0.0" : null)); + } + + private sealed class FakeInstallerService(bool success) : IPrereqInstaller + { + public Task GetInstallPlanAsync( + string tool, + string projectRoot, + DevToolConfig? config = null, + CancellationToken cancellationToken = default) + => Task.FromResult(new InstallPlan(tool, Supported: true, "test", [new InstallCommand("echo", ["ok"])])); + + public Task RunInstallAsync( + InstallCommand command, + string projectRoot, + Action onOutput, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + => Task.FromResult(new RunResult(success ? 0 : 1, TimeSpan.FromMilliseconds(5))); + } + + private sealed class FakeActionRunner(bool success) : IActionRunner + { + public Task RunStepAsync( + WorkflowStep step, + string projectRoot, + Action onOutput, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + => Task.FromResult(new RunResult(success ? 0 : 2, TimeSpan.FromMilliseconds(10))); + } + + private sealed class ConditionalProbeService : IToolProbe + { + public Task ProbeAsync( + string tool, + string projectRoot, + DevToolConfig? config = null, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + { + var available = tool.ToLowerInvariant() switch + { + "node" => true, + "npm" => true, + "cargo" => true, + "tauri" => false, + _ => true + }; + return Task.FromResult(new ProbeResult(tool, available, Version: available ? "1.0.0" : null)); + } + } + + private sealed class DetailedProbeService : IToolProbe + { + public Task ProbeAsync( + string tool, + string projectRoot, + DevToolConfig? config = null, + IReadOnlyDictionary? envOverrides = null, + CancellationToken cancellationToken = default) + => Task.FromResult(new ProbeResult(tool, false, Details: "Fallback: unresolved command")); + } +} diff --git a/tests/DevTool.Tests/WorkflowModelBuilderTests.cs b/tests/DevTool.Tests/WorkflowModelBuilderTests.cs new file mode 100644 index 0000000..96c4f4b --- /dev/null +++ b/tests/DevTool.Tests/WorkflowModelBuilderTests.cs @@ -0,0 +1,149 @@ +using Sdt.Config; +using Xunit; + +namespace DevTool.Tests; + +public sealed class WorkflowModelBuilderTests +{ + [Fact] + public void TargetsOnly_Strict_ThrowsMigrationError() + { + var cfg = new DevToolConfig + { + Targets = + [ + new BuildTarget + { + Id = "build", + Label = "Build", + Command = "dotnet", + Args = ["build"], + } + ] + }; + + var ex = Assert.Throws(() => WorkflowModelBuilder.Normalize(cfg, LegacyMode.Strict)); + Assert.Contains("Legacy 'targets' are not allowed in strict mode", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void TargetsOnly_Compat_ProducesWarningAndConvertedWorkflow() + { + var cfg = new DevToolConfig + { + Targets = + [ + new BuildTarget + { + Id = "build", + Label = "Build", + Command = "dotnet", + Args = ["build"], + } + ] + }; + + var result = WorkflowModelBuilder.Normalize(cfg, LegacyMode.Compat); + + Assert.Single(result.Workflows); + Assert.Contains(result.Warnings, w => w.Contains("legacy 'targets'", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void WorkflowsOnly_HasNoLegacyWarning() + { + var cfg = new DevToolConfig + { + Workflows = + [ + new WorkflowDefinition + { + Id = "build", + Label = "Build", + Steps = + [ + new WorkflowStep + { + Id = "run", + Label = "Run", + Command = "dotnet", + Args = ["build"], + } + ] + } + ] + }; + + var result = WorkflowModelBuilder.Normalize(cfg, LegacyMode.Strict); + + Assert.Single(result.Workflows); + Assert.DoesNotContain(result.Warnings, w => w.Contains("legacy 'targets'", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void Mixed_PrefersWorkflowsDeterministically() + { + var cfg = new DevToolConfig + { + Targets = + [ + new BuildTarget + { + Id = "legacy", + Label = "Legacy", + Command = "dotnet", + Args = ["build"], + } + ], + Workflows = + [ + new WorkflowDefinition + { + Id = "new", + Label = "New", + Steps = + [ + new WorkflowStep + { + Id = "step", + Label = "Step", + Command = "dotnet", + Args = ["build"], + } + ] + } + ] + }; + + var result = WorkflowModelBuilder.Normalize(cfg, LegacyMode.Strict); + + Assert.Single(result.Workflows); + Assert.Equal("new", result.Workflows[0].Id); + Assert.Contains(result.Warnings, w => w.Contains("Both 'workflows' and legacy 'targets'", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void LegacyPwshTarget_InfersToolRequirements_FromScript() + { + var cfg = new DevToolConfig + { + Targets = + [ + new BuildTarget + { + Id = "web", + Label = "Web", + Command = "pwsh", + Args = ["-NoProfile", "-File", "scripts/publish-app.ps1", "-Target", "web"], + } + ] + }; + + var result = WorkflowModelBuilder.Normalize(cfg, LegacyMode.Compat); + var step = Assert.Single(result.Workflows).Steps.Single(); + + Assert.Contains(step.Requires, r => r.Tool == "python"); + Assert.Contains(step.Requires, r => r.Tool == "node"); + Assert.Contains(step.Requires, r => r.Tool == "npm"); + } +} diff --git a/tests/DevTool.Tests/WorkspaceDefaultsTests.cs b/tests/DevTool.Tests/WorkspaceDefaultsTests.cs new file mode 100644 index 0000000..99d4880 --- /dev/null +++ b/tests/DevTool.Tests/WorkspaceDefaultsTests.cs @@ -0,0 +1,141 @@ +using Sdt.Config; +using Xunit; + +namespace DevTool.Tests; + +public sealed class WorkspaceDefaultsTests +{ + [Fact] + public void ConfigLoader_AppliesWorkspaceDefaults_FromAncestorDirectory() + { + var workspaceRoot = CreateTempDir("sdt-ws-defaults-"); + var projectRoot = Path.Combine(workspaceRoot, "proj-a"); + Directory.CreateDirectory(projectRoot); + File.WriteAllText(Path.Combine(workspaceRoot, WorkspaceLoader.FileName), """ +{ + "name": "Test Workspace", + "projects": [] +} +"""); + + File.WriteAllText(Path.Combine(workspaceRoot, ConfigLoader.WorkspaceDefaultsFileName), """ +{ + "toolchains": { + "node": { + "packageManager": "pnpm", + "workingDir": "frontend" + } + }, + "env": [ + { "key": "DOTNET_ENVIRONMENT", "description": "default env", "default": "Development", "options": ["Development", "Production"] } + ] +} +"""); + + File.WriteAllText(Path.Combine(projectRoot, "devtool.json"), """ +{ + "name": "Project A", + "version": "1.0.0", + "workflows": [ + { + "id": "build", + "label": "Build", + "description": "Build app", + "group": "Build", + "dependsOn": [], + "steps": [ + { "id": "build-step", "label": "dotnet build", "command": "dotnet", "args": ["build"], "workingDir": "." } + ] + } + ] +} +"""); + + var loaded = ConfigLoader.FindAndLoad(projectRoot); + + Assert.NotNull(loaded); + Assert.Equal("pnpm", loaded!.Config.Toolchains?.Node?.PackageManager); + Assert.Equal("frontend", loaded.Config.Toolchains?.Node?.WorkingDir); + Assert.Single(loaded.Config.Env); + Assert.Contains(loaded.Warnings, w => w.Contains("Applied workspace defaults", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void ConfigLoader_ProjectValuesOverrideWorkspaceDefaults() + { + var workspaceRoot = CreateTempDir("sdt-ws-override-"); + var projectRoot = Path.Combine(workspaceRoot, "proj-b"); + Directory.CreateDirectory(projectRoot); + File.WriteAllText(Path.Combine(workspaceRoot, WorkspaceLoader.FileName), """ +{ + "name": "Test Workspace", + "projects": [] +} +"""); + + File.WriteAllText(Path.Combine(workspaceRoot, ConfigLoader.WorkspaceDefaultsFileName), """ +{ + "name": "Workspace Defaults", + "workflows": [ + { + "id": "from-defaults", + "label": "Defaults Workflow", + "description": "", + "group": "General", + "dependsOn": [], + "steps": [] + } + ], + "debug": { + "diagnostics": { + "enabled": true, + "outputDir": ".sdt/workspace-debug", + "includeAllEnv": true, + "bundleOnFailure": true + } + } +} +"""); + + File.WriteAllText(Path.Combine(projectRoot, "devtool.json"), """ +{ + "name": "Project B", + "version": "1.0.0", + "workflows": [ + { + "id": "project-workflow", + "label": "Project Workflow", + "description": "Only this one should remain", + "group": "Build", + "dependsOn": [], + "steps": [] + } + ], + "debug": { + "diagnostics": { + "enabled": false + } + } +} +"""); + + var loaded = ConfigLoader.FindAndLoad(projectRoot); + + Assert.NotNull(loaded); + Assert.Equal("Project B", loaded!.Config.Name); + Assert.Single(loaded.Config.Workflows); + Assert.Equal("project-workflow", loaded.Config.Workflows[0].Id); + Assert.NotNull(loaded.Config.Debug); + Assert.NotNull(loaded.Config.Debug!.Diagnostics); + Assert.False(loaded.Config.Debug.Diagnostics.Enabled); + Assert.Equal(".sdt/workspace-debug", loaded.Config.Debug.Diagnostics.OutputDir); + Assert.True(loaded.Config.Debug.Diagnostics.IncludeAllEnv); + } + + private static string CreateTempDir(string prefix) + { + var path = Path.Combine(Path.GetTempPath(), prefix + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(path); + return path; + } +} diff --git a/tests/DevTool.Tests/WorkspaceFavoritesTests.cs b/tests/DevTool.Tests/WorkspaceFavoritesTests.cs new file mode 100644 index 0000000..49b5bf5 --- /dev/null +++ b/tests/DevTool.Tests/WorkspaceFavoritesTests.cs @@ -0,0 +1,60 @@ +using Sdt.Config; +using Xunit; + +namespace DevTool.Tests; + +public sealed class WorkspaceFavoritesTests +{ + [Fact] + public void SaveAndLoad_PreservesFavorites() + { + var workspaceRoot = CreateTempDir("sdt-favorites-"); + var workspace = new WorkspaceConfig + { + Name = "My Workspace", + Projects = + [ + new WorkspaceProject + { + Name = "proj-a", + Path = "proj-a" + } + ], + Favorites = + [ + new WorkspaceFavorite + { + ProjectPath = "proj-a", + WorkflowId = "build", + Label = "Build A" + } + ] + }; + + WorkspaceLoader.Save(workspaceRoot, workspace); + var loaded = WorkspaceLoader.FindAndLoad(workspaceRoot); + + Assert.NotNull(loaded); + Assert.Single(loaded!.Value.Config.Favorites); + Assert.Equal("build", loaded.Value.Config.Favorites[0].WorkflowId); + Assert.Equal("Build A", loaded.Value.Config.Favorites[0].Label); + } + + [Fact] + public void ResolveFavoriteProjectRoot_AcceptsAbsolutePaths() + { + var workspaceRoot = CreateTempDir("sdt-favorites-abs-"); + var abs = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "fav-proj-" + Guid.NewGuid().ToString("N"))); + var favorite = new WorkspaceFavorite { ProjectPath = abs, WorkflowId = "build" }; + + var resolved = WorkspaceLoader.ResolveFavoriteProjectRoot(workspaceRoot, favorite); + Assert.Equal(abs, resolved, ignoreCase: OperatingSystem.IsWindows()); + } + + private static string CreateTempDir(string prefix) + { + var path = Path.Combine(Path.GetTempPath(), prefix + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(path); + return path; + } +} diff --git a/tests/DevTool.Tests/WorkspaceInventoryServiceTests.cs b/tests/DevTool.Tests/WorkspaceInventoryServiceTests.cs new file mode 100644 index 0000000..58f9554 --- /dev/null +++ b/tests/DevTool.Tests/WorkspaceInventoryServiceTests.cs @@ -0,0 +1,102 @@ +using Sdt.Config; +using Xunit; + +namespace DevTool.Tests; + +public sealed class WorkspaceInventoryServiceTests +{ + [Fact] + public void Scan_DetectsKnownAndCandidates_WithMarkerPrecedence() + { + var root = CreateTempDir("sdt-inventory-"); + var c = Path.Combine(root, "C"); + var b = Path.Combine(root, "B"); + var a = Path.Combine(root, "A"); + Directory.CreateDirectory(c); + Directory.CreateDirectory(b); + Directory.CreateDirectory(a); + + File.WriteAllText(Path.Combine(c, "devtool.json"), """{"name":"c","version":"0.1.0","workflows":[]}"""); + File.WriteAllText(Path.Combine(b, "B.slnx"), ""); + File.WriteAllText(Path.Combine(a, "A.csproj"), ""); + + var workspace = new WorkspaceConfig + { + Name = "test", + Projects = [new WorkspaceProject { Name = "c", Path = "C" }] + }; + + var service = new WorkspaceInventoryService(); + var result = service.Scan(root, c, workspace.Inventory, workspace); + + Assert.Contains(result.KnownProjects, x => x.RootPath.Equals(Path.GetFullPath(c), StringComparison.OrdinalIgnoreCase)); + Assert.Contains(result.Candidates, x => x.RootPath.Equals(Path.GetFullPath(b), StringComparison.OrdinalIgnoreCase)); + Assert.Contains(result.Candidates, x => x.RootPath.Equals(Path.GetFullPath(a), StringComparison.OrdinalIgnoreCase)); + + var bItem = result.Candidates.First(x => x.RootPath.Equals(Path.GetFullPath(b), StringComparison.OrdinalIgnoreCase)); + Assert.Equal(WorkspaceInventoryKind.Slnx, bItem.PrimaryKind); + } + + [Fact] + public void Scan_DedupesSiblingCsprojRoots_ToParentCluster() + { + var root = CreateTempDir("sdt-inventory-cluster-"); + var cluster = Path.Combine(root, "X"); + var app = Path.Combine(cluster, "App"); + var tests = Path.Combine(cluster, "App.Tests"); + Directory.CreateDirectory(app); + Directory.CreateDirectory(tests); + File.WriteAllText(Path.Combine(app, "App.csproj"), ""); + File.WriteAllText(Path.Combine(tests, "App.Tests.csproj"), ""); + + var service = new WorkspaceInventoryService(); + var result = service.Scan(root, root, new WorkspaceInventorySettings(), new WorkspaceConfig()); + + Assert.Contains(result.Candidates, x => x.RootPath.Equals(Path.GetFullPath(cluster), StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(result.Candidates, x => x.RootPath.Equals(Path.GetFullPath(app), StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(result.Candidates, x => x.RootPath.Equals(Path.GetFullPath(tests), StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void Scan_RespectsExclusions() + { + var root = CreateTempDir("sdt-inventory-excl-"); + var excluded = Path.Combine(root, "node_modules", "Pkg"); + Directory.CreateDirectory(excluded); + File.WriteAllText(Path.Combine(excluded, "Pkg.csproj"), ""); + File.WriteAllText(Path.Combine(root, "devtool.json"), """{"name":"root","version":"0.1.0","workflows":[]}"""); + + var service = new WorkspaceInventoryService(); + var result = service.Scan(root, root, new WorkspaceInventorySettings(), new WorkspaceConfig()); + + Assert.DoesNotContain(result.Candidates, x => x.RootPath.Contains("node_modules", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void Scan_IsDeterministicallyOrdered() + { + var root = CreateTempDir("sdt-inventory-order-"); + var b = Path.Combine(root, "B"); + var a = Path.Combine(root, "A"); + Directory.CreateDirectory(a); + Directory.CreateDirectory(b); + File.WriteAllText(Path.Combine(a, "a.csproj"), ""); + File.WriteAllText(Path.Combine(b, "b.csproj"), ""); + File.WriteAllText(Path.Combine(root, "devtool.json"), """{"name":"root","version":"0.1.0","workflows":[]}"""); + + var service = new WorkspaceInventoryService(); + var first = service.Scan(root, root, new WorkspaceInventorySettings(), new WorkspaceConfig()); + var second = service.Scan(root, root, new WorkspaceInventorySettings(), new WorkspaceConfig()); + + Assert.Equal( + first.Candidates.Select(x => x.RootPath).ToArray(), + second.Candidates.Select(x => x.RootPath).ToArray()); + } + + private static string CreateTempDir(string prefix) + { + var path = Path.Combine(Path.GetTempPath(), prefix + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(path); + return path; + } +} diff --git a/tests/DevTool.Tests/WorkspaceLoaderTests.cs b/tests/DevTool.Tests/WorkspaceLoaderTests.cs new file mode 100644 index 0000000..8e96365 --- /dev/null +++ b/tests/DevTool.Tests/WorkspaceLoaderTests.cs @@ -0,0 +1,85 @@ +using Sdt.Config; +using Xunit; + +namespace DevTool.Tests; + +public sealed class WorkspaceLoaderTests +{ + [Fact] + public void FindAndLoad_DoesNotThrow_WhenAutoDiscoverHitsStrictLegacyProject() + { + var root = Path.Combine(Path.GetTempPath(), "sdt-workspace-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + File.WriteAllText(Path.Combine(root, "devtool.json"), """ +{ + "name": "legacy", + "version": "0.1.0", + "targets": [ + { + "id": "build", + "label": "Build", + "group": "Build", + "command": "dotnet", + "args": ["build"], + "workingDir": ".", + "dependsOn": [] + } + ], + "workflows": [] +} +"""); + + Environment.SetEnvironmentVariable("SDT_LEGACY_MODE", null); + var result = WorkspaceLoader.FindAndLoad(root); + Assert.Null(result); + } + + [Fact] + public void ResolveProjectRoot_AcceptsAbsolutePaths() + { + var root = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "ws-" + Guid.NewGuid().ToString("N"))); + var abs = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "proj-" + Guid.NewGuid().ToString("N"))); + var project = new WorkspaceProject { Path = abs }; + + var resolved = WorkspaceLoader.ResolveProjectRoot(root, project); + Assert.Equal(abs, resolved, ignoreCase: OperatingSystem.IsWindows()); + } + + [Fact] + public void WorkspaceProject_AdditionalFields_DefaultsAreSafe() + { + var project = new WorkspaceProject(); + Assert.Empty(project.Tags); + Assert.Empty(project.ToolFamilies); + Assert.False(project.Disabled); + } + + [Fact] + public void WorkspaceConfig_FavoritesDefaultToEmpty() + { + var workspace = new WorkspaceConfig(); + Assert.Empty(workspace.Favorites); + Assert.True(workspace.Inventory.Enabled); + Assert.Equal(4, workspace.Inventory.MaxDepth); + Assert.Contains("*.slnx", workspace.Inventory.IncludeMarkers); + } + + [Fact] + public void FindAndLoad_AutoDiscover_AddsOnlyDevtoolProjects_NotMarkerOnlyCandidates() + { + var workspaceRoot = Path.Combine(Path.GetTempPath(), "sdt-ws-auto-" + Guid.NewGuid().ToString("N")); + var current = Path.Combine(workspaceRoot, "current"); + var markerOnly = Path.Combine(workspaceRoot, "marker"); + Directory.CreateDirectory(current); + Directory.CreateDirectory(markerOnly); + File.WriteAllText(Path.Combine(current, "devtool.json"), """{"name":"current","version":"0.1.0","workflows":[]}"""); + File.WriteAllText(Path.Combine(markerOnly, "marker.csproj"), ""); + + var loaded = WorkspaceLoader.FindAndLoad(current); + Assert.NotNull(loaded); + Assert.Contains(loaded!.Value.Config.Projects, p => + WorkspaceLoader.ResolveProjectRoot(loaded.Value.WorkspaceRoot, p).Equals(Path.GetFullPath(current), StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(loaded.Value.Config.Projects, p => + WorkspaceLoader.ResolveProjectRoot(loaded.Value.WorkspaceRoot, p).Equals(Path.GetFullPath(markerOnly), StringComparison.OrdinalIgnoreCase)); + } +}