diff --git a/.github/workflows/split-upm.yml b/.github/workflows/split-upm.yml
index d5fcc74..794c593 100644
--- a/.github/workflows/split-upm.yml
+++ b/.github/workflows/split-upm.yml
@@ -24,13 +24,13 @@ jobs:
contents: read
steps:
- name: Check out source
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install pinned Rust toolchain
run: rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal
- name: Cache Cargo inputs
- uses: Swatinem/rust-cache@aa7c1c80a07a27a84c0aa76d0cef0aad3830e330 # v2.7.8
+ uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: native
@@ -83,7 +83,7 @@ jobs:
git diff --exit-code -- "Packages/dev.tnayuki.unterm/Third Party Notices.md"
- name: Upload SBOM and notices
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: unterm-supply-chain
path: |
@@ -94,12 +94,12 @@ jobs:
retention-days: 30
build-macos:
- runs-on: macos-latest
+ runs-on: macos-15
permissions:
contents: read
steps:
- name: Check out source
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install pinned Rust toolchain
run: |
@@ -107,7 +107,7 @@ jobs:
rustup default "$RUST_TOOLCHAIN"
- name: Cache Cargo inputs
- uses: Swatinem/rust-cache@aa7c1c80a07a27a84c0aa76d0cef0aad3830e330 # v2.7.8
+ uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: native
@@ -117,7 +117,7 @@ jobs:
cargo test -p unterm --lib --locked
# Deny new warnings while preserving the upstream crate's documented
# legacy lint debt until those broad refactors can be reviewed separately.
- cargo clippy -p unterm --lib --tests --locked -- \
+ cargo clippy -p unterm --lib --bins --tests --locked -- \
-D warnings \
-A clippy::too_many_arguments \
-A clippy::type_complexity \
@@ -135,23 +135,32 @@ jobs:
- name: Write checksum and build provenance
run: |
- BINARY="Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm.dylib"
- shasum -a 256 "$BINARY" > unterm-macos.sha256
+ LIBRARY="Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm.dylib"
+ DEBUGGER="Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm-debugger"
+ test -x "$DEBUGGER"
+ library_sha256="$(shasum -a 256 "$LIBRARY" | awk '{print $1}')"
+ debugger_sha256="$(shasum -a 256 "$DEBUGGER" | awk '{print $1}')"
+ printf '%s %s\n%s %s\n' \
+ "$library_sha256" "$(basename "$LIBRARY")" \
+ "$debugger_sha256" "$(basename "$DEBUGGER")" \
+ > unterm-macos.sha256
jq -n \
--arg commit "$GITHUB_SHA" \
- --arg runner "macos-latest" \
+ --arg runner "macos-15" \
--arg toolchain "$RUST_TOOLCHAIN" \
--arg sourceDateEpoch "$(git log -1 --pretty=%ct)" \
- --arg sha256 "$(awk '{print $1}' unterm-macos.sha256)" \
- '{schemaVersion:1, commit:$commit, runner:$runner, rustToolchain:$toolchain, sourceDateEpoch:$sourceDateEpoch, artifact:"unterm.dylib", sha256:$sha256}' \
+ --arg sha256 "$library_sha256" \
+ --arg debuggerSha256 "$debugger_sha256" \
+ '{schemaVersion:1, commit:$commit, runner:$runner, rustToolchain:$toolchain, sourceDateEpoch:$sourceDateEpoch, artifact:"unterm.dylib", sha256:$sha256, debuggerArtifact:"unterm-debugger", debuggerSha256:$debuggerSha256, debuggerSignature:"ad-hoc"}' \
> build-provenance-macos.json
- name: Upload macOS build
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: unterm-macos
path: |
Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm.dylib
+ Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm-debugger
unterm-macos.sha256
build-provenance-macos.json
if-no-files-found: error
@@ -163,7 +172,7 @@ jobs:
contents: read
steps:
- name: Check out source
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install pinned Rust toolchain
shell: pwsh
@@ -172,7 +181,7 @@ jobs:
rustup default $env:RUST_TOOLCHAIN
- name: Cache Cargo inputs
- uses: Swatinem/rust-cache@aa7c1c80a07a27a84c0aa76d0cef0aad3830e330 # v2.7.8
+ uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: native
@@ -184,8 +193,13 @@ jobs:
shell: pwsh
run: |
$binary = 'Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm.dll'
+ $debugger = 'Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm-debugger.exe'
$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $binary).Hash.ToLowerInvariant()
- "$hash unterm.dll" | Set-Content -Encoding ascii unterm-windows.sha256
+ $debuggerHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $debugger).Hash.ToLowerInvariant()
+ @(
+ "$hash unterm.dll"
+ "$debuggerHash unterm-debugger.exe"
+ ) | Set-Content -Encoding ascii unterm-windows.sha256
$sourceDateEpoch = (git log -1 --pretty=%ct)
[ordered]@{
schemaVersion = 1
@@ -195,14 +209,17 @@ jobs:
sourceDateEpoch = $sourceDateEpoch
artifact = 'unterm.dll'
sha256 = $hash
+ debuggerArtifact = 'unterm-debugger.exe'
+ debuggerSha256 = $debuggerHash
} | ConvertTo-Json | Set-Content -Encoding utf8 build-provenance-windows.json
- name: Upload Windows build
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: unterm-windows
path: |
Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm.dll
+ Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm-debugger.exe
unterm-windows.sha256
build-provenance-windows.json
if-no-files-found: error
@@ -218,24 +235,24 @@ jobs:
attestations: write
steps:
- name: Download macOS build
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: unterm-macos
path: artifacts/macos
- name: Download Windows build
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: unterm-windows
path: artifacts/windows
- name: Download supply-chain evidence
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: unterm-supply-chain
path: artifacts/supply-chain
- - name: Attest native binaries and evidence
- uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
+ - name: Attest native libraries, debugger executables, and evidence
+ uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: artifacts/**/*
diff --git a/.gitignore b/.gitignore
index a1e061e..ebaa189 100644
--- a/.gitignore
+++ b/.gitignore
@@ -117,4 +117,6 @@ InitTestScene*.unity*
# with `native/build-macos.sh` and `cargo about` (see native/about.toml). Their
# `.meta` files stay tracked so GUIDs are stable.
/Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm.dylib
+/Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm-debugger
/Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm.dll
+/Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm-debugger.exe
diff --git a/Packages/dev.tnayuki.unterm/CHANGELOG.md b/Packages/dev.tnayuki.unterm/CHANGELOG.md
index 05c569e..d0828b0 100644
--- a/Packages/dev.tnayuki.unterm/CHANGELOG.md
+++ b/Packages/dev.tnayuki.unterm/CHANGELOG.md
@@ -1,5 +1,23 @@
# Changelog
+## [Unreleased]
+
+### Added
+
+- Unterm now ships a full Mono debugger for play mode. Enable it under Preferences ▸ Unterm ▸ Debugger and click in the code editor's gutter to set breakpoints (a dedicated dot column appears next to the line numbers); pressing Play then just debugs — the debugger attaches to the editor's Mono runtime over the soft-debugger protocol, arms your breakpoints as their types load (so a breakpoint in `Awake`/`Start` hits from the very first frame), and pops to the front when one hits. It runs as a standalone process with its own window (`Window ▸ Unterm ▸ Debugger (Standalone Process)`, ⇧⌘D), so it stays fully interactive while the editor is frozen at a breakpoint, and it persists across Play/Stop cycles.
+- The debugger window gives you the essentials of an IDE debugger, rendered with the same native code editor as Unterm's own: a project file tree, a read-only source view with the executing line highlighted, continue / step over / into / out (also `c`/`n`/`i`/`o`), pause ("break all", `p`), a call stack you can click to inspect any frame, a thread list when a stop involves multiple managed threads, and a variables panel showing locals, `this`, and inherited fields — private members included — with lazy expansion of objects and arrays (reference cycles like `transform ↔ gameObject` are handled). Hovering an identifier in the source shows its current value, and the breakpoint list jumps to source on click.
+- Breakpoints live in `Library/Unterm/breakpoints.json`, shared between the editor and the debugger and synced live in both directions: toggling in the code editor arms/disarms in a running debug session immediately (even mid-play), and toggling in the debugger — including while the editor is frozen at a breakpoint — updates the editor's gutter dots as soon as it runs again.
+- The debugger can attach to more than the editor: development builds with script debugging enabled are discovered on the network via Unity's player multicast and offered in a target picker (labelled with their project and host), and source locations reported by a player built on another machine are mapped back to this project's files for display and breakpoints.
+
+- The code editor now syntax-highlights Markdown while editing and can render it (toggle Preview with ⇧⌘V, or the tab ⋮ / right-click menu); a `.md` opened from a transcript or preview link opens rendered by default, while a Project double-click opens for editing.
+
+- The set of file extensions the code editor opens is now its own preference (Preferences ▸ Unterm ▸ Code Editor, semicolon-separated, with a reset button) instead of borrowing Unity's C# project-generation extension settings — Unterm never generates `.csproj` files, so tying "what opens" to a generation setting that never runs was misleading. The default covers Unity's code and text formats plus the docs, configs, and native-plugin sources an agent transcript typically links (`md`, `yml`, `toml`, `rs`, `c`/`cpp`, `java`/`kt`, `swift`, shell scripts, …), so those open in-editor out of the box.
+
+### Fixed
+
+- The Unterm code editor stays selected as the External Script Editor across package updates — it's now recognized by package identity rather than the exact cached path (which changes each update), so script and link opens keep landing in Unterm instead of silently falling back to another editor.
+- File-path links in the Claude Code transcript now underline every existing file instead of only a hard-coded list of code extensions, and a click always opens the file somewhere: the configured script editor first, then Unity's own asset pipeline on decline (a scene link loads the scene, a prefab opens in prefab mode, an image opens in its associated app), and the OS default app for files outside the asset database — so paths like `native/**/*.rs` or `docs/*.md` no longer show an underline that does nothing when clicked.
+
## [0.6.0] - 2026-07-08
### Added
diff --git a/Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm-debugger.exe.meta b/Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm-debugger.exe.meta
new file mode 100644
index 0000000..778ea17
--- /dev/null
+++ b/Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm-debugger.exe.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: cb9a07383c2428e7f826a00406c94bae
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm-debugger.meta b/Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm-debugger.meta
new file mode 100644
index 0000000..813609b
--- /dev/null
+++ b/Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm-debugger.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 863b76ae04ad815c6731490daa3f7fed
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/dev.tnayuki.unterm/Editor/UntermAgentWindow.cs b/Packages/dev.tnayuki.unterm/Editor/UntermAgentWindow.cs
index da2a8d2..27abe2c 100644
--- a/Packages/dev.tnayuki.unterm/Editor/UntermAgentWindow.cs
+++ b/Packages/dev.tnayuki.unterm/Editor/UntermAgentWindow.cs
@@ -923,9 +923,10 @@ private void HandlePanelMouse(Rect rect)
case EventType.MouseUp when _selecting:
_selecting = false;
- // A plain click (no drag-selection) on a file path opens it through
- // the configured script editor. OpenFromAgent no-ops for non-file /
- // non-editable tokens (the underline only marks files that exist).
+ // A plain click (no drag-selection) on a file path opens it: the
+ // configured script editor first, then Unity's asset pipeline or
+ // the OS default app on decline. OpenFromAgent no-ops for tokens
+ // that aren't existing files (matching the underline).
if (!_native.AgentviewPanelHasSelection(Vid))
{
string tok = _native.AgentviewPanelTokenAt(Vid, lx, ly);
diff --git a/Packages/dev.tnayuki.unterm/Editor/UntermBreakpoints.cs b/Packages/dev.tnayuki.unterm/Editor/UntermBreakpoints.cs
new file mode 100644
index 0000000..f3566b2
--- /dev/null
+++ b/Packages/dev.tnayuki.unterm/Editor/UntermBreakpoints.cs
@@ -0,0 +1,148 @@
+using System.Collections.Generic;
+using System.IO;
+using UnityEditor;
+using UnityEngine;
+
+namespace Unterm.Editor
+{
+ ///
+ /// Project-wide debugger breakpoint store, persisted to
+ /// Library/Unterm/breakpoints.json (the same convention as the rest of
+ /// Unterm's editor state). It is the single source of truth shared by BOTH the
+ /// editor and the standalone debugger process — the debugger writes the same file
+ /// directly when you toggle a breakpoint while stopped (when the editor is frozen
+ /// and can't run), so edits from either side persist.
+ ///
+ /// Lines are 0-based, matching the native code editor's gutter line indices
+ /// (converted to 1-based when handed to the SDB debugger, whose sequence points
+ /// are 1-based source lines).
+ ///
+ [InitializeOnLoad]
+ internal static class UntermBreakpoints
+ {
+ [System.Serializable]
+ private class Entry { public string path; public int[] lines; }
+ [System.Serializable]
+ private class Store { public Entry[] files; }
+
+ private static Dictionary> _map;
+
+ private static string Dir =>
+ Path.Combine(Path.GetDirectoryName(Application.dataPath), "Library", "Unterm");
+ private static string StorePath => Path.Combine(Dir, "breakpoints.json");
+
+ /// Raised when the store file changes on disk — i.e. the debugger toggled a
+ /// breakpoint while we run — so open code editors can refresh their gutter dots.
+ public static event System.Action Changed;
+
+ // Last write time we've accounted for; poll for anything newer (the debugger's
+ // edits), and stamp it after our own writes so we don't fire on those.
+ private static long _seenTicks;
+
+ static UntermBreakpoints()
+ {
+ _seenTicks = FileTicks();
+ EditorApplication.update += Poll;
+ }
+
+ private static long FileTicks()
+ {
+ try { return File.Exists(StorePath) ? File.GetLastWriteTimeUtc(StorePath).Ticks : 0; }
+ catch { return 0; }
+ }
+
+ private static void Poll()
+ {
+ long ticks = FileTicks();
+ if (ticks == _seenTicks) return;
+ _seenTicks = ticks;
+ _map = null; // drop the cache; next read reloads from disk
+ Changed?.Invoke();
+ }
+
+ private static Dictionary> Map
+ {
+ get { if (_map == null) Load(); return _map; }
+ }
+
+ /// Re-read from disk (e.g. to pick up edits the debugger made while stopped).
+ public static void Reload() => _map = null;
+
+ /// 0-based breakpoint lines for a file (ascending), as uint[] for the FFI.
+ public static uint[] For(string path)
+ {
+ if (!string.IsNullOrEmpty(path) && Map.TryGetValue(Norm(path), out var s))
+ {
+ var a = new uint[s.Count];
+ int i = 0;
+ foreach (var l in s) a[i++] = (uint)l;
+ return a;
+ }
+ return System.Array.Empty();
+ }
+
+ /// Toggle a 0-based breakpoint line; returns the file's new full set.
+ public static uint[] Toggle(string path, int line)
+ {
+ if (string.IsNullOrEmpty(path)) return System.Array.Empty();
+ var key = Norm(path);
+ if (!Map.TryGetValue(key, out var s)) { s = new SortedSet(); Map[key] = s; }
+ if (!s.Remove(line)) s.Add(line);
+ if (s.Count == 0) Map.Remove(key);
+ Save();
+ return For(path);
+ }
+
+ /// All (absolute file path, 0-based line) breakpoints across the project.
+ public static List> All()
+ {
+ var r = new List>();
+ foreach (var kv in Map)
+ foreach (var l in kv.Value)
+ r.Add(new KeyValuePair(kv.Key, l));
+ return r;
+ }
+
+ private static string Norm(string p)
+ {
+ try { return Path.GetFullPath(p); } catch { return p; }
+ }
+
+ private static void Load()
+ {
+ _map = new Dictionary>();
+ try
+ {
+ if (!File.Exists(StorePath)) return;
+ var store = JsonUtility.FromJson(File.ReadAllText(StorePath));
+ if (store?.files == null) return;
+ foreach (var e in store.files)
+ {
+ if (string.IsNullOrEmpty(e.path) || e.lines == null) continue;
+ var set = new SortedSet();
+ foreach (var l in e.lines) set.Add(l);
+ if (set.Count > 0) _map[Norm(e.path)] = set;
+ }
+ }
+ catch { /* corrupt store: start empty */ }
+ }
+
+ private static void Save()
+ {
+ try
+ {
+ Directory.CreateDirectory(Dir);
+ var entries = new List(_map.Count);
+ foreach (var kv in _map)
+ {
+ var arr = new int[kv.Value.Count];
+ kv.Value.CopyTo(arr);
+ entries.Add(new Entry { path = kv.Key, lines = arr });
+ }
+ File.WriteAllText(StorePath, JsonUtility.ToJson(new Store { files = entries.ToArray() }));
+ _seenTicks = FileTicks(); // our own write; don't re-fire Changed for it
+ }
+ catch { /* best effort */ }
+ }
+ }
+}
diff --git a/Packages/dev.tnayuki.unterm/Editor/UntermBreakpoints.cs.meta b/Packages/dev.tnayuki.unterm/Editor/UntermBreakpoints.cs.meta
new file mode 100644
index 0000000..163aa7a
--- /dev/null
+++ b/Packages/dev.tnayuki.unterm/Editor/UntermBreakpoints.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3637239f1719f4940a32dc3f182f1fc8
\ No newline at end of file
diff --git a/Packages/dev.tnayuki.unterm/Editor/UntermCodeEditorWindow.cs b/Packages/dev.tnayuki.unterm/Editor/UntermCodeEditorWindow.cs
index ea82f65..694ee14 100644
--- a/Packages/dev.tnayuki.unterm/Editor/UntermCodeEditorWindow.cs
+++ b/Packages/dev.tnayuki.unterm/Editor/UntermCodeEditorWindow.cs
@@ -97,6 +97,16 @@ public sealed class UntermCodeEditorWindow : EditorWindow, IHasCustomMenu
private GUIStyle _imeHidden;
private bool _mouseDragging;
+ // Markdown preview: a read-only rendered view of the LIVE buffer (unsaved
+ // edits included), served by the native EditorView's document-mode panel.
+ // Only offered for Markdown files. Serialized so a domain reload keeps the
+ // mode; re-applied to the native view in LoadNative.
+ [SerializeField] private bool _preview;
+ // Preview click-to-open: the file-path token under the press, opened on a
+ // release that didn't drag (a drag is a text selection instead).
+ private bool _pvMoved;
+ private string _pvDownToken = "";
+
// Find / replace / goto bar overlay.
private enum BarMode { None, Find, Replace, Goto }
private BarMode _bar = BarMode.None;
@@ -192,22 +202,45 @@ private static bool OpenSelectedValidate()
// Open a file path clicked in the Claude Code transcript. `root` resolves a
// project-relative path (the agent often reports paths relative to its
- // working directory). Routes through the configured script editor: when Unterm
- // is selected in External Tools the file opens here; otherwise it opens in
- // whatever editor is configured. No-op for missing or non-editable files.
+ // working directory). The transcript underlines every existing file, so any
+ // click must land somewhere: the configured script editor first (when Unterm
+ // is selected in External Tools, code/text files open here), then Unity's
+ // asset pipeline for anything the editor declined (a scene loads, a prefab
+ // opens in prefab mode, an image in its associated app), and the OS default
+ // app for files outside the asset database. No-op only for missing files.
public static void OpenFromAgent(string path, string root)
{
if (string.IsNullOrEmpty(path)) return;
if (!Path.IsPathRooted(path) && !string.IsNullOrEmpty(root))
path = Path.Combine(root, path);
- if (Directory.Exists(path) || !File.Exists(path) ||
- !UntermExternalCodeEditor.HandlesExtension(path)) return;
- CodeEditor.Editor.CurrentCodeEditor?.OpenProject(Path.GetFullPath(path), -1, -1);
+ if (Directory.Exists(path) || !File.Exists(path)) return;
+ string full = Path.GetFullPath(path);
+ // A link clicked in a rendered view (the agent transcript or a Markdown
+ // preview) is a reading action, so a linked Markdown file opens in
+ // preview by default. The hint is honored only by Unterm's OpenProject
+ // (a different selected external editor ignores it and opens normally);
+ // clear it afterwards so it can't leak into a later, unrelated open.
+ UntermExternalCodeEditor.NextOpenPrefersPreview = IsMarkdownPath(full);
+ try
+ {
+ if (CodeEditor.Editor.CurrentCodeEditor?.OpenProject(full, -1, -1) == true)
+ return;
+ }
+ finally
+ {
+ UntermExternalCodeEditor.NextOpenPrefersPreview = false;
+ }
+ string rel = FileUtil.GetProjectRelativePath(full.Replace('\\', '/'));
+ var asset = string.IsNullOrEmpty(rel) ? null : AssetDatabase.LoadMainAssetAtPath(rel);
+ if (asset != null) AssetDatabase.OpenAsset(asset);
+ else EditorUtility.OpenWithDefaultApp(full);
}
// Reuse an already-open window for the same file; otherwise a new one. `line`
// is 1-based (-1 = none) and jumps the caret once the editor is ready.
- internal static void OpenPath(string path, int line = -1)
+ // `preview` opens a new Markdown window rendered rather than in edit mode; an
+ // already-open window keeps whatever mode it's in (it may be mid-edit).
+ internal static void OpenPath(string path, int line = -1, bool preview = false)
{
string full = Path.GetFullPath(path);
foreach (var w in Resources.FindObjectsOfTypeAll())
@@ -226,6 +259,9 @@ internal static void OpenPath(string path, int line = -1)
var win = CreateInstance();
win.minSize = new Vector2(320, 200);
DockIntoCenter(win);
+ // Set the preview intent before LoadFile: LoadFile applies it (gated to
+ // Markdown via CanPreview) once the native view is ready.
+ win._preview = preview;
win.LoadFile(path);
win._pendingLine = line; // applied once the native editor is ready
win.Focus();
@@ -309,17 +345,64 @@ private static Rect RectUnion(Rect a, Rect b)
return new Rect(x, y, xMax - x, yMax - y);
}
- // Extension -> tree-sitter language token (null = no grammar / plain). The
- // first milestone only bundles C#; others fall through to plain.
+ // Extension -> tree-sitter language token (null = no grammar / plain). We
+ // bundle C# and Markdown grammars; others fall through to plain.
private static string LangTokenFor(string path)
{
switch (Path.GetExtension(path).ToLowerInvariant())
{
case ".cs": return "cs";
+ case ".md":
+ case ".markdown": return "md";
default: return null;
}
}
+ // Whether `path` is a Markdown file (the preview toggle is offered only then).
+ internal static bool IsMarkdownPath(string path)
+ {
+ switch (Path.GetExtension(path ?? "").ToLowerInvariant())
+ {
+ case ".md":
+ case ".markdown": return true;
+ default: return false;
+ }
+ }
+
+ private bool CanPreview =>
+ _native != null && _native.EditorPreviewSupported && IsMarkdownPath(_filePath);
+
+ // Flip Markdown preview on/off. Preview is read-only, so it also drops the
+ // completion popup / signature hint and closes the find bar.
+ private void TogglePreview()
+ {
+ if (!CanPreview && !_preview) return;
+ SetPreview(!_preview);
+ }
+
+ private void SetPreview(bool on)
+ {
+ if (_native == null || _editorId == 0) return;
+ _preview = on && CanPreview;
+ _native.EditorSetPreview(Eid, _preview);
+ if (_preview)
+ {
+ CloseCompletion();
+ CloseSignatureHelp();
+ if (_bar != BarMode.None) CloseBar();
+ }
+ else
+ {
+ _refocus = true; // hand focus back to the editing surface
+ // Diff polling paused while previewing: re-read HEAD + index now so
+ // the gutter reflects any commits / staging done in the meantime.
+ _native.EditorRefreshDiff(Eid);
+ }
+ UpdateTitle();
+ RenderView();
+ Repaint();
+ }
+
// Decide the line ending to preserve: keep the file's if it has one
// (CRLF if any \r\n present, else LF), otherwise the OS default.
private static bool DetectCrlf(string text)
@@ -349,8 +432,14 @@ private void LoadFile(string path)
{
_native.EditorSetLanguage(Eid, _langToken);
_native.EditorSetText(Eid, text);
+ if (UntermDebuggerPrefs.Enabled)
+ _native.EditorSetBreakpoints(Eid, UntermBreakpoints.For(_filePath)); // restore dots
_savedSerial = _native.EditorEditSerial(Eid); // baseline: just-loaded = clean
_native.EditorSetPath(Eid, _filePath); // fetch git texts for diff gutter markers
+ // A non-Markdown file can't be previewed: drop the mode carried over
+ // from a previously-open Markdown file in this window.
+ if (_preview && !CanPreview) _preview = false;
+ _native.EditorSetPreview(Eid, _preview);
RenderView();
Repaint();
}
@@ -382,6 +471,32 @@ private void OnEnable()
// serialized flag is just the pre-reload display value).
MarkDirty();
UpdateTitle();
+ UntermDebuggerPrefs.Changed += OnDebuggerPrefChanged;
+ // The debugger toggling a breakpoint (while we run) writes the store; refresh
+ // this file's dots when that happens.
+ UntermBreakpoints.Changed += OnBreakpointsChanged;
+ }
+
+ // Debugging toggled in Preferences: re-apply the gutter mode (dot column) and
+ // show/clear this file's breakpoint dots immediately.
+ private void OnDebuggerPrefChanged()
+ {
+ if (_native == null || _editorId == 0) return;
+ _native.EditorSetBpGutter(Eid, UntermDebuggerPrefs.Enabled);
+ OnBreakpointsChanged();
+ }
+
+ // The shared store changed on disk (debugger-side toggle): re-apply this file's
+ // dots. No-op'd cheaply when the file/line set is unchanged.
+ private void OnBreakpointsChanged()
+ {
+ if (_native == null || _editorId == 0) return;
+ _native.EditorSetBreakpoints(Eid,
+ UntermDebuggerPrefs.Enabled && !string.IsNullOrEmpty(_filePath)
+ ? UntermBreakpoints.For(_filePath)
+ : Array.Empty());
+ RenderView();
+ Repaint();
}
private void OnBeforeReload()
@@ -396,6 +511,8 @@ private void OnBeforeReload()
private void OnDisable()
{
+ UntermDebuggerPrefs.Changed -= OnDebuggerPrefChanged;
+ UntermBreakpoints.Changed -= OnBreakpointsChanged;
AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeReload;
EditorApplication.update -= OnEditorUpdate;
_native?.PopupHide();
@@ -476,6 +593,18 @@ private void LoadNative()
_native.EditorRefreshDiff(Eid);
}
+ // Sync the breakpoint gutter to the current pref for BOTH fresh and
+ // re-adopted views (a re-adopted native view keeps its old flag, which
+ // would otherwise leave the gutter/dots out of sync after a reload).
+ _native.EditorSetBpGutter(Eid, UntermDebuggerPrefs.Enabled);
+ if (UntermDebuggerPrefs.Enabled && !string.IsNullOrEmpty(_filePath))
+ _native.EditorSetBreakpoints(Eid, UntermBreakpoints.For(_filePath));
+
+ // Re-apply preview mode after a reload / re-adopt (the serialized flag
+ // is the intent; a re-adopted native view keeps its own, which may drift).
+ if (_preview && !CanPreview) _preview = false;
+ _native.EditorSetPreview(Eid, _preview);
+
_refocus = true;
RenderView();
_status = "ready";
@@ -666,13 +795,18 @@ private void OnGUI()
DrawBar();
- DrawImeField(rect);
- SyncIme();
- // Don't steal focus from the find bar's field while it's open.
- if (_bar == BarMode.None && _refocus && Event.current.type == EventType.Repaint)
+ // Preview is read-only: no hidden IME field (its commit would mutate the
+ // buffer) and no focus grab.
+ if (!_preview)
{
- EditorGUI.FocusTextInControl(InputControl);
- _refocus = false;
+ DrawImeField(rect);
+ SyncIme();
+ // Don't steal focus from the find bar's field while it's open.
+ if (_bar == BarMode.None && _refocus && Event.current.type == EventType.Repaint)
+ {
+ EditorGUI.FocusTextInControl(InputControl);
+ _refocus = false;
+ }
}
}
@@ -688,9 +822,30 @@ private void HandleMouse(Rect rect)
float lx = (e.mousePosition.x - rect.x) * ppp;
float ly = (e.mousePosition.y - rect.y) * ppp;
+ // Preview: read-only text selection + click-to-open file paths; none of
+ // the editing / gutter / breakpoint / hover-tooltip handling applies.
+ if (_preview) { HandlePreviewMouse(rect, e, lx, ly); return; }
+
switch (e.type)
{
case EventType.MouseDown when e.button == 0 && rect.Contains(e.mousePosition):
+ // Gutter clicks with debugging enabled: the thin left diff-marker
+ // lane (~8px) keeps its Stage/Revert menu; the rest of the gutter
+ // (breakpoint column + line numbers) toggles a breakpoint.
+ if (UntermDebuggerPrefs.Enabled
+ && lx < _native.EditorGutterWidth(Eid) && !string.IsNullOrEmpty(_filePath))
+ {
+ if (lx < 8f * ppp)
+ {
+ int laneHunk = _native.EditorHunkAt(Eid, lx, ly);
+ if (laneHunk >= 0) { ShowHunkMenu(laneHunk); e.Use(); break; }
+ }
+ uint bpLine = _native.EditorLineAtY(Eid, ly);
+ var set = UntermBreakpoints.Toggle(_filePath, (int)bpLine);
+ _native.EditorSetBreakpoints(Eid, set);
+ RenderView(); Repaint(); e.Use();
+ break;
+ }
CloseCompletion(); // a click dismisses the popup
// A click on a gutter diff marker opens its Stage/Revert menu
// instead of moving the caret (VS Code-style).
@@ -726,6 +881,56 @@ private void HandleMouse(Rect rect)
}
}
+ // Mouse in Markdown preview: drag selects text; a click (no drag) on an
+ // underlined file-path token opens it (the same targets the transcript
+ // opens); a context click offers copy / select-all / exit.
+ private void HandlePreviewMouse(Rect rect, Event e, float lx, float ly)
+ {
+ switch (e.type)
+ {
+ case EventType.MouseDown when e.button == 0 && rect.Contains(e.mousePosition):
+ _pvDownToken = _native.EditorPreviewTokenAt(Eid, lx, ly) ?? "";
+ _pvMoved = false;
+ _native.EditorMouse(Eid, lx, ly, 0); // begin selection
+ _mouseDragging = true;
+ RenderView(); Repaint(); e.Use();
+ break;
+ case EventType.MouseDrag when _mouseDragging:
+ _pvMoved = true;
+ _native.EditorMouse(Eid, lx, ly, 1); // extend selection
+ RenderView(); Repaint(); e.Use();
+ break;
+ case EventType.MouseUp when _mouseDragging:
+ _mouseDragging = false;
+ if (!_pvMoved && !string.IsNullOrEmpty(_pvDownToken))
+ OpenFromAgent(_pvDownToken, Path.GetDirectoryName(_filePath) ?? "");
+ _pvDownToken = "";
+ e.Use();
+ break;
+ case EventType.ContextClick when rect.Contains(e.mousePosition):
+ ShowPreviewContextMenu();
+ e.Use();
+ break;
+ }
+ }
+
+ private void ShowPreviewContextMenu()
+ {
+ var menu = new GenericMenu();
+ menu.AddItem(new GUIContent("Copy"), false, () =>
+ {
+ string s = _native.EditorCopy(Eid);
+ if (!string.IsNullOrEmpty(s)) EditorGUIUtility.systemCopyBuffer = s;
+ });
+ menu.AddItem(new GUIContent("Select All"), false, () =>
+ {
+ _native.EditorSelectAll(Eid); RenderView(); Repaint();
+ });
+ menu.AddSeparator("");
+ menu.AddItem(new GUIContent($"Edit (exit preview) {KCmd}{KSep}{KShift}{KSep}V"), false, () => SetPreview(false));
+ menu.ShowAsContext();
+ }
+
private Vector2 _lastHoverPos = new Vector2(float.NaN, float.NaN);
// Show/hide the diff-peek tooltip for the pointer at `pos`. Deduped by
@@ -814,6 +1019,11 @@ private void ShowContextMenu()
{
_native.EditorToggleComment(Eid); MarkDirty(); RenderView(); Repaint();
});
+ if (CanPreview)
+ {
+ menu.AddSeparator("");
+ menu.AddItem(new GUIContent($"Preview Markdown {KCmd}{KSep}{KShift}{KSep}V"), false, TogglePreview);
+ }
menu.AddSeparator("");
menu.AddItem(new GUIContent($"Save {KCmd}{KSep}S"), false, Save);
menu.ShowAsContext();
@@ -824,6 +1034,11 @@ private void ShowContextMenu()
public void AddItemsToMenu(GenericMenu menu)
{
if (_native == null || _editorId == 0) return;
+ if (CanPreview || _preview)
+ {
+ menu.AddItem(new GUIContent($"Preview Markdown {KCmd}{KSep}{KShift}{KSep}V"), _preview, TogglePreview);
+ menu.AddSeparator("");
+ }
menu.AddItem(new GUIContent($"Find… {KCmd}{KSep}F"), false, () => OpenBar(BarMode.Find));
menu.AddItem(new GUIContent($"Replace… {KCmd}{KSep}{KAlt}{KSep}F"), false, () => OpenBar(BarMode.Replace));
menu.AddItem(new GUIContent($"Go to Line… {KCmd}{KSep}L"), false, () => OpenBar(BarMode.Goto));
@@ -859,6 +1074,29 @@ private void HandleKeys()
return;
}
+ // Markdown preview is read-only: allow copy / select-all / toggle / exit,
+ // and swallow every editing key so nothing reaches the buffer.
+ if (_preview)
+ {
+ if ((e.command || e.control) && e.shift && e.keyCode == KeyCode.V) { TogglePreview(); e.Use(); return; }
+ if (e.keyCode == KeyCode.Escape) { SetPreview(false); e.Use(); return; }
+ if (e.command || e.control)
+ {
+ switch (e.keyCode)
+ {
+ case KeyCode.C:
+ {
+ string s = _native.EditorCopy(Eid);
+ if (!string.IsNullOrEmpty(s)) EditorGUIUtility.systemCopyBuffer = s;
+ e.Use(); return;
+ }
+ case KeyCode.A:
+ _native.EditorSelectAll(Eid); RenderView(); Repaint(); e.Use(); return;
+ }
+ }
+ return;
+ }
+
// While composing, let every key reach the IME field.
if (_composing) return;
@@ -962,6 +1200,8 @@ private void HandleKeys()
{
case KeyCode.S:
Save(); e.Use(); return;
+ case KeyCode.V when e.shift && CanPreview:
+ TogglePreview(); e.Use(); return;
case KeyCode.V:
_native.EditorInsert(Eid, EditorGUIUtility.systemCopyBuffer);
MarkDirty(); RenderView(); Repaint(); e.Use(); return;
@@ -1469,13 +1709,16 @@ private void OnEditorUpdate()
if (EditorApplication.timeSinceStartup - _lastExtCheck > 1.0)
{
_lastExtCheck = EditorApplication.timeSinceStartup;
- CheckExternalChange();
- _native.EditorRefreshDiff(Eid); // background re-read of HEAD + index
+ CheckExternalChange(); // still needed in preview (file may change on disk)
+ // The diff gutter is an edit-mode surface: don't re-read git while
+ // previewing (SetPreview refreshes it again on exit).
+ if (!_preview) _native.EditorRefreshDiff(Eid); // background HEAD + index
}
// A background git fetch may have finished: poll every tick (cheap bool);
// native applies it and returns true only when the git texts actually
- // changed, so the steady-state 1s refresh doesn't cause re-renders.
- if (_native.EditorPollDiff(Eid)) { RenderView(); Repaint(); }
+ // changed, so the steady-state 1s refresh doesn't cause re-renders. Skipped
+ // in preview (the gutter isn't shown, so there's nothing to apply).
+ if (!_preview && _native.EditorPollDiff(Eid)) { RenderView(); Repaint(); }
PollSignatureTask();
// While a hint is shown, re-evaluate whenever the caret moved by ANY means
// (arrows, click, edits) so it tracks the active parameter and closes once
@@ -2029,7 +2272,8 @@ private void UpdateTitle()
{
// No manual dirty marker — Unity overlays the unsaved indicator from
// hasUnsavedChanges.
- titleContent = new GUIContent(string.IsNullOrEmpty(_filePath) ? "Untitled" : Path.GetFileName(_filePath));
+ string name = string.IsNullOrEmpty(_filePath) ? "Untitled" : Path.GetFileName(_filePath);
+ titleContent = new GUIContent(_preview ? name + " (Preview)" : name);
}
// Cmd/Ctrl+S while this window is focused saves the file (a window-context
diff --git a/Packages/dev.tnayuki.unterm/Editor/UntermDebuggerLauncher.cs b/Packages/dev.tnayuki.unterm/Editor/UntermDebuggerLauncher.cs
new file mode 100644
index 0000000..f39c98b
--- /dev/null
+++ b/Packages/dev.tnayuki.unterm/Editor/UntermDebuggerLauncher.cs
@@ -0,0 +1,190 @@
+using System.Diagnostics;
+using System.IO;
+using UnityEditor;
+using UnityEngine;
+
+namespace Unterm.Editor
+{
+ ///
+ /// Whether debugging is enabled (Preferences > Unterm). Gates the debugger menu
+ /// item, the Play-mode auto-launch, and the code editor's breakpoint gutter (the
+ /// gutter only reserves the dot column and toggles breakpoints when enabled).
+ ///
+ internal static class UntermDebuggerPrefs
+ {
+ private const string EnabledKey = "Unterm.Debugger.Enabled";
+
+ /// Raised when changes, so open code editors can
+ /// re-apply their gutter mode immediately.
+ public static event System.Action Changed;
+
+ public static bool Enabled
+ {
+ get => EditorPrefs.GetBool(EnabledKey, false);
+ set
+ {
+ if (value == Enabled) return;
+ EditorPrefs.SetBool(EnabledKey, value);
+ Changed?.Invoke();
+ }
+ }
+ }
+
+ ///
+ /// Manages the standalone Unterm debugger — a separate process with its own window
+ /// that attaches to the editor's Mono agent. It can be opened explicitly from the
+ /// Window/Unterm/Debugger (Standalone Process) menu, and is also opened automatically when you
+ /// enter Play mode with breakpoints set. The window is persistent: it stays alive
+ /// across Play/Stop cycles (surfacing itself when a breakpoint is hit) and closes
+ /// itself when the editor it is attached to goes away.
+ ///
+ [InitializeOnLoad]
+ internal static class UntermDebuggerLauncher
+ {
+ private const string ProcessName = "unterm-debugger";
+#if UNITY_EDITOR_WIN
+ private const string ExecutableName = ProcessName + ".exe";
+#else
+ private const string ExecutableName = ProcessName;
+#endif
+ private static Process _proc;
+
+ static UntermDebuggerLauncher()
+ {
+ EditorApplication.playModeStateChanged -= OnPlayModeChanged;
+ EditorApplication.playModeStateChanged += OnPlayModeChanged;
+ // Best-effort cleanup of the instance we launched when the editor quits.
+ EditorApplication.quitting -= Stop;
+ EditorApplication.quitting += Stop;
+ // (Following the editor to the foreground is handled natively by the debugger
+ // via an NSWorkspace observer, so it works even while suspended at a
+ // breakpoint — the editor's main thread is frozen then and can't signal.)
+ }
+
+ private static string ProjectRoot =>
+ Directory.GetParent(Application.dataPath)?.FullName ?? Application.dataPath;
+
+ // The debugger and editor coordinate through this project's Library/Unterm dir,
+ // so single-instance and focus are scoped to THIS project (not by process name,
+ // which would clash with a debugger running for another Unity project).
+ private static string StateDir => Path.Combine(ProjectRoot, "Library", "Unterm");
+ private static string PidFile => Path.Combine(StateDir, "debugger.pid");
+ private static string FocusFile => Path.Combine(StateDir, "focus.request");
+
+ /// Open the debugger window from the menu. If this project's debugger is already
+ /// running, bring it forward; otherwise launch it (in Edit mode it attaches,
+ /// arms the current breakpoints and waits for Play — you can also use Pause).
+ /// Greyed out until debugging is enabled in Preferences > Unterm.
+ [MenuItem("Window/Unterm/Debugger (Standalone Process) %#d")]
+ private static void Open() => EnsureRunning();
+
+ [MenuItem("Window/Unterm/Debugger (Standalone Process) %#d", validate = true)]
+ private static bool OpenValidate() => UntermDebuggerPrefs.Enabled;
+
+ private static void OnPlayModeChanged(PlayModeStateChange state)
+ {
+ // Auto-open on Play so a breakpoint has something to stop into. The window
+ // is left running when leaving Play; it re-syncs breakpoints each run.
+ // The debugger is a persistent, always-attached process (like an IDE that
+ // stays attached across Play/Stop), so once it's up there's no launch race
+ // to wait out — and the type-load watch suspends the VM to arm in time.
+ if (state == PlayModeStateChange.ExitingEditMode && UntermDebuggerPrefs.Enabled)
+ {
+ UntermBreakpoints.Reload();
+ if (UntermBreakpoints.All().Count > 0) EnsureRunning();
+ }
+ }
+
+ // The PID of this project's running debugger (from its pid file), or 0 if none.
+ // A domain reload discards the managed `_proc` handle, so the pid file — written
+ // by the debugger into THIS project's Library/Unterm — is the source of truth.
+ private static int RunningPid()
+ {
+ try
+ {
+ if (!File.Exists(PidFile)) return 0;
+ if (!int.TryParse(File.ReadAllText(PidFile).Trim(), out var pid) || pid <= 0) return 0;
+ var p = Process.GetProcessById(pid); // throws if no such process
+ if (p.HasExited) return 0;
+ // Guard against PID reuse: confirm it's actually our debugger.
+ return p.ProcessName.Contains(ProcessName) ? pid : 0;
+ }
+ catch { return 0; }
+ }
+
+ private static bool IsRunning() => RunningPid() != 0;
+
+ // Touch the focus file; the running debugger polls it and comes to the front.
+ private static void RequestFocus()
+ {
+ try
+ {
+ Directory.CreateDirectory(StateDir);
+ File.WriteAllText(FocusFile, System.DateTime.UtcNow.Ticks.ToString());
+ }
+ catch { /* best effort */ }
+ }
+
+ // The debugger binary ships next to the native plugin; fall back to the cargo
+ // dev build during development.
+ private static string DebuggerPath()
+ {
+ try
+ {
+ var dir = Path.GetDirectoryName(UntermWindow.PluginPath);
+ if (!string.IsNullOrEmpty(dir))
+ {
+ var p = Path.Combine(dir, ExecutableName);
+ if (File.Exists(p)) return p;
+ }
+ }
+ catch { /* fall through to dev path */ }
+ var dev = Path.Combine(ProjectRoot, "native", "target", "debug", ExecutableName);
+ return File.Exists(dev) ? dev : null;
+ }
+
+ private static void EnsureRunning()
+ {
+ // Single instance per project: if it's already up, just bring it forward.
+ if (IsRunning()) { RequestFocus(); return; }
+
+ var exe = DebuggerPath();
+ if (string.IsNullOrEmpty(exe))
+ {
+ UnityEngine.Debug.LogWarning("Unterm: unterm-debugger binary not found; cannot start debugger.");
+ return;
+ }
+
+ // No breakpoint arguments: the debugger seeds itself from the shared store
+ // (Library/Unterm/breakpoints.json) — the same file it re-reads on every
+ // play-mode domain reload — so both sides share one source of truth.
+ try
+ {
+ _proc = Process.Start(new ProcessStartInfo
+ {
+ FileName = exe,
+ WorkingDirectory = ProjectRoot,
+ UseShellExecute = false,
+ });
+ }
+ catch (System.Exception e)
+ {
+ UnityEngine.Debug.LogWarning("Unterm: failed to launch debugger: " + e.Message);
+ _proc = null;
+ }
+ }
+
+ private static void Stop()
+ {
+ try { if (_proc != null && !_proc.HasExited) _proc.Kill(); }
+ catch { /* already gone */ }
+ _proc = null;
+ // Also stop the instance recorded in this project's pid file, in case our
+ // managed handle was lost to a domain reload. This is project-scoped (the
+ // pid file lives in this project's Library), so it won't touch another
+ // project's debugger.
+ try { var pid = RunningPid(); if (pid != 0) Process.GetProcessById(pid).Kill(); }
+ catch { /* already gone */ }
+ }
+ }
+}
diff --git a/Packages/dev.tnayuki.unterm/Editor/UntermDebuggerLauncher.cs.meta b/Packages/dev.tnayuki.unterm/Editor/UntermDebuggerLauncher.cs.meta
new file mode 100644
index 0000000..1d4412d
--- /dev/null
+++ b/Packages/dev.tnayuki.unterm/Editor/UntermDebuggerLauncher.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9a81c29b68a4f41258d3be7ee3960599
\ No newline at end of file
diff --git a/Packages/dev.tnayuki.unterm/Editor/UntermExternalCodeEditor.cs b/Packages/dev.tnayuki.unterm/Editor/UntermExternalCodeEditor.cs
index 973bea6..fa84604 100644
--- a/Packages/dev.tnayuki.unterm/Editor/UntermExternalCodeEditor.cs
+++ b/Packages/dev.tnayuki.unterm/Editor/UntermExternalCodeEditor.cs
@@ -5,6 +5,44 @@
namespace Unterm.Editor
{
+ ///
+ /// The file extensions the Unterm code editor claims, user-editable under
+ /// "Preferences > Unterm > Code Editor" (semicolon-separated; dots and
+ /// case don't matter). The VSCode/Rider packages gate opening on Unity's C#
+ /// project-generation extension settings because for them "openable" means
+ /// "part of the generated project" — Unterm generates no .csproj at all, so
+ /// borrowing a generation setting it never runs would be misleading; it keeps
+ /// its own list instead. The default covers Unity's code/text formats plus the
+ /// docs, configs and native-plugin sources an agent transcript typically links.
+ ///
+ internal static class UntermOpenExtensions
+ {
+ private const string Key = "Unterm.CodeEditor.OpenExtensions";
+
+ public const string Default =
+ "cs;uxml;uss;shader;compute;cginc;hlsl;glslinc;template;raytrace;" +
+ "asmdef;asmref;rsp;json;log;txt;xml;md;markdown;yml;yaml;toml;ini;cfg;csv;tsv;properties;" +
+ "js;ts;py;rs;lua;sh;bat;ps1;c;h;cc;cpp;hpp;mm;m;swift;java;kt;gradle;pro;plist;html;css;" +
+ "gitignore;gitattributes";
+
+ public static string Value
+ {
+ get => EditorPrefs.GetString(Key, Default);
+ set => EditorPrefs.SetString(Key, value);
+ }
+
+ /// Whether `ext` (no dot) is in the configured list. Parses on each call —
+ /// this sits behind user clicks, never a per-frame path.
+ public static bool Contains(string ext)
+ {
+ foreach (var e in Value.Split(';'))
+ if (string.Equals(e.Trim().TrimStart('.'), ext,
+ System.StringComparison.OrdinalIgnoreCase))
+ return true;
+ return false;
+ }
+ }
+
///
/// Registers the Unterm code editor as a selectable "External Script Editor"
/// (Preferences > External Tools). When Unterm is chosen, every script open
@@ -17,11 +55,25 @@ namespace Unterm.Editor
[InitializeOnLoad]
internal sealed class UntermExternalCodeEditor : IExternalCodeEditor
{
+ // One-shot hint: the next OpenProject should open a Markdown file in preview
+ // (rendered) rather than edit mode. Set by the transcript / preview link
+ // flow just before it routes through the generic OpenProject, and consumed
+ // (cleared) here — so only a link click in a rendered view prefers preview,
+ // while a Project-window double-click (which also lands in OpenProject) opens
+ // in edit mode. A different selected external editor never reads it.
+ internal static bool NextOpenPrefersPreview;
+
+ // Unity's package id, used to recognize our stored selection by identity
+ // rather than an exact path (see TryGetInstallationForPath).
+ private const string PackageId = "dev.tnayuki.unterm";
+
// The "installation path" Unity stores as the selected editor. Unterm is
// in-editor (no executable), but Unity's dropdown only lists installations
// whose path exists on disk. The compiled assembly is real and stable within
// a project, and unlike a package.json path it remains valid when Unterm is
- // embedded under another package's Editor/ThirdParty subtree.
+ // embedded under another package's Editor/ThirdParty subtree. The identity
+ // fallback below still recognizes selections made by standalone upstream
+ // package versions that used package.json as their installation path.
internal static readonly string EditorKey =
Path.GetFullPath(typeof(UntermExternalCodeEditor).Assembly.Location);
@@ -30,6 +82,15 @@ static UntermExternalCodeEditor()
try
{
CodeEditor.Register(new UntermExternalCodeEditor());
+ // After a package update the stored selection still points at the
+ // PREVIOUS cache path (a git/UPM install resolves to
+ // `Library/PackageCache/dev.tnayuki.unterm@/`, and ``
+ // changes each update). Opens still route here — the current-editor
+ // resolution matches by identity — but Unity's Preferences dropdown
+ // compares exact paths, so it would show Unterm as unselected. Refresh
+ // the stored path to the current one so the dropdown stays correct.
+ // Deferred so it runs once the editor is settled, not mid-registration.
+ EditorApplication.delayCall += RefreshSelectionIfOurs;
}
catch (System.Exception e)
{
@@ -37,6 +98,26 @@ static UntermExternalCodeEditor()
}
}
+ // If the selected external editor is a previous build of THIS package (its
+ // cache path changed on update), re-store the current path so the selection
+ // is recognized exactly, not just by identity. No-op when it's already current
+ // or when the user has chosen a different editor.
+ private static void RefreshSelectionIfOurs()
+ {
+ try
+ {
+ // Unity stores the selection under this pref (see CodeEditor's own
+ // CurrentEditorPath); read it directly to avoid an internal API.
+ string stored = EditorPrefs.GetString("kScriptsDefaultApp", string.Empty);
+ if (stored != EditorKey && IdentifiesPackage(stored))
+ CodeEditor.SetExternalScriptEditor(EditorKey);
+ }
+ catch (System.Exception e)
+ {
+ Debug.LogWarning("[Unterm] External editor selection refresh failed: " + e);
+ }
+ }
+
public CodeEditor.Installation[] Installations { get; } =
{
new CodeEditor.Installation { Name = "Unterm Code Editor", Path = EditorKey },
@@ -44,7 +125,14 @@ static UntermExternalCodeEditor()
public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation)
{
- if (editorPath == EditorKey)
+ // Match the current path OR any package.json belonging to this package.
+ // Unity stores the selected editor as the path it had when chosen; after a
+ // package update relocates the package (the UPM cache folder's `@`
+ // changes), that stored path no longer equals `EditorKey`. Keying only off
+ // the exact string would then make Unity treat Unterm as unselected and
+ // silently route script / Markdown opens to another editor (or the OS).
+ // Recognizing it by identity keeps the selection across updates.
+ if (editorPath == EditorKey || IdentifiesPackage(editorPath))
{
installation = Installations[0];
return true;
@@ -53,6 +141,20 @@ public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installa
return false;
}
+ // Whether `editorPath` is this package's `package.json`, wherever it currently
+ // resolves — embedded (`.../dev.tnayuki.unterm/package.json`) or UPM-cached
+ // (`.../dev.tnayuki.unterm@/package.json`).
+ private static bool IdentifiesPackage(string editorPath)
+ {
+ if (string.IsNullOrEmpty(editorPath)) return false;
+ if (!string.Equals(Path.GetFileName(editorPath), "package.json",
+ System.StringComparison.OrdinalIgnoreCase))
+ return false;
+ string dir = Path.GetFileName(Path.GetDirectoryName(editorPath) ?? "");
+ return dir == PackageId
+ || dir.StartsWith(PackageId + "@", System.StringComparison.Ordinal);
+ }
+
public void Initialize(string editorInstallationPath) { }
// Unterm highlights with tree-sitter and completes with in-process Roslyn, so
@@ -79,39 +181,28 @@ public bool OpenProject(string filePath = "", int line = -1, int column = -1)
return true;
}
if (!File.Exists(filePath)) return false;
+ // Consume the one-shot preview hint (only meaningful for Markdown).
+ bool preview = NextOpenPrefersPreview;
+ NextOpenPrefersPreview = false;
// Only claim files we consider code/text. Scenes, prefabs, materials and
// other binary/asset opens Unity routes through here must fall through
// (return false) so Unity's own handler opens them — otherwise a scene
// double-click would land in the text editor.
if (!HandlesExtension(filePath)) return false;
- UntermCodeEditorWindow.OpenPath(filePath, line);
+ UntermCodeEditorWindow.OpenPath(filePath, line, preview);
return true;
}
- // Decides which double-clicked assets Unterm claims: the extensions Unity
- // treats as project code — its C# project-generation set plus whatever the
- // user added under Project Settings > Editor — together with the few text
- // formats Unity's own VSCode/Rider packages force-add. Anything else — scenes,
- // prefabs, materials, other assets — is declined so Unity opens it with its
- // native handler. Shared with the transcript path-click flow in
- // .
+ // Decides which double-clicked assets Unterm claims: the extension list
+ // configured under Preferences > Unterm ().
+ // Anything else — scenes, prefabs, materials, other assets — is declined so
+ // Unity opens it with its native handler. The transcript path-click flow
+ // () reaches this through
+ // and falls back to Unity's own open on decline.
internal static bool HandlesExtension(string filePath)
{
- string ext = Path.GetExtension(filePath).TrimStart('.').ToLowerInvariant();
- if (ext.Length == 0) return false;
- if (ext == "json" || ext == "asmdef" || ext == "asmref" || ext == "log")
- return true;
- return HasExtension(EditorSettings.projectGenerationBuiltinExtensions, ext)
- || HasExtension(EditorSettings.projectGenerationUserExtensions, ext);
- }
-
- private static bool HasExtension(string[] extensions, string ext)
- {
- if (extensions == null) return false;
- foreach (var e in extensions)
- if (string.Equals(e, ext, System.StringComparison.OrdinalIgnoreCase))
- return true;
- return false;
+ string ext = Path.GetExtension(filePath).TrimStart('.');
+ return ext.Length > 0 && UntermOpenExtensions.Contains(ext);
}
}
}
diff --git a/Packages/dev.tnayuki.unterm/Editor/UntermNative.cs b/Packages/dev.tnayuki.unterm/Editor/UntermNative.cs
index cdcff8e..dcce183 100644
--- a/Packages/dev.tnayuki.unterm/Editor/UntermNative.cs
+++ b/Packages/dev.tnayuki.unterm/Editor/UntermNative.cs
@@ -134,6 +134,9 @@ private static string NativeError()
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.I1)] private delegate bool EdHoverFn(ulong id, float x, float y);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate int EdHunkAtFn(ulong id, float x, float y);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.I1)] private delegate bool EdStageHunkFn(ulong id, uint hunk);
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate uint EdLineAtYFn(ulong id, float y);
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void EdSetBpsFn(ulong id, IntPtr lines, UIntPtr count);
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void EdSetBoolFn(ulong id, [MarshalAs(UnmanagedType.I1)] bool on);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.I1)] private delegate bool EdFindFn(ulong id, [MarshalAs(UnmanagedType.LPUTF8Str)] string query, [MarshalAs(UnmanagedType.I1)] bool forward, [MarshalAs(UnmanagedType.I1)] bool caseSensitive);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate uint EdReplaceAllFn(ulong id, [MarshalAs(UnmanagedType.LPUTF8Str)] string query, [MarshalAs(UnmanagedType.LPUTF8Str)] string repl, [MarshalAs(UnmanagedType.I1)] bool caseSensitive);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void EdCompleteFn(ulong id, uint prefixLen, [MarshalAs(UnmanagedType.LPUTF8Str)] string text);
@@ -192,6 +195,8 @@ private static string NativeError()
private AvUintSetFn _edSetUndoLimit;
// Git-diff gutter markers (optional: an older native bundle may lack them).
private AvStrFn _edSetPath; private AvVoidFn _edRefreshDiff; private BoolFn _edPollDiff;
+ // Markdown preview (optional: an older native bundle may lack them).
+ private EdSetBoolFn _edSetPreview; private BoolFn _edPreviewActive; private AvTokenFn _edPreviewTokenAt;
private AvVoidFn _edRender; private AvPtrFn _edRawTexture; private AvFloatFn _edContentHeight; private AvCaretFn _edCaret;
private U64IdFn _edEditSerial;
private AvInputKeyFn _edKey; private SetFocusFn _edSetFocus; private AvStrFn _edInsert; private AvStrFn _edSetPreedit; private AvStrFn _edSetText; private AvStrFn _edAddUsing;
@@ -199,6 +204,7 @@ private static string NativeError()
private AvBufFn _edCopy; private AvBufFn _edCut; private EdMouseFn _edMouse; private EdHoverFn _edHover; private AvF1Fn _edScroll;
private EdHunkAtFn _edHunkAt; private EdStageHunkFn _edHunkStaged; private EdStageHunkFn _edHunkHasStaged; private EdStageHunkFn _edHunkStagedOnly; private EdStageHunkFn _edStageHunk; private EdStageHunkFn _edUnstageHunk; private AvUintSetFn _edRevertHunk;
private AvF1Fn _edSetScroll; private AvFloatFn _edScrollOffset; private AvF1Fn _edScrollH;
+ private AvFloatFn _edGutterWidth; private EdLineAtYFn _edLineAtY; private AvUintSetFn _edToggleBp; private EdSetBpsFn _edSetBps; private EdSetBoolFn _edSetBpGutter;
private AvVoidFn _edIndent, _edOutdent, _edToggleComment, _edMoveUp, _edMoveDown, _edDuplicate, _edDeleteLine;
private AvUintSetFn _edGotoLine; private EdFindFn _edFind; private AvStrFn _edReplaceSel; private EdReplaceAllFn _edReplaceAll;
private AvBufFn _edWordPrefix; private EdCompleteFn _edComplete; private EdSetComplFn _edSetCompletions; private AvUintGetFn _edCaretOffset;
@@ -360,6 +366,9 @@ public void Load(string bundlePath)
_edSetLanguage = Sym("unterm_editor_set_language");
_edSetPath = SymOpt("unterm_editor_set_path");
_edRefreshDiff = SymOpt("unterm_editor_refresh_diff");
+ _edSetPreview = SymOpt("unterm_editor_set_preview");
+ _edPreviewActive = SymOpt("unterm_editor_preview_active");
+ _edPreviewTokenAt = SymOpt("unterm_editor_preview_token_at");
_edPollDiff = SymOpt("unterm_editor_poll_diff");
_edRender = Sym("unterm_editor_render");
_edRawTexture = Sym("unterm_editor_raw_texture");
@@ -391,6 +400,13 @@ public void Load(string bundlePath)
_edSetScroll = Sym("unterm_editor_set_scroll");
_edScrollOffset = Sym("unterm_editor_scroll_offset");
_edScrollH = Sym("unterm_editor_scroll_h");
+ // Optional: tolerate an older plugin .dylib that predates these symbols
+ // (features stay disabled rather than breaking the whole editor at load).
+ _edGutterWidth = SymOpt("unterm_editor_gutter_width");
+ _edLineAtY = SymOpt("unterm_editor_line_at_y");
+ _edToggleBp = SymOpt("unterm_editor_toggle_breakpoint");
+ _edSetBps = SymOpt("unterm_editor_set_breakpoints");
+ _edSetBpGutter = SymOpt("unterm_editor_set_bp_gutter");
_edIndent = Sym("unterm_editor_indent");
_edOutdent = Sym("unterm_editor_outdent");
_edToggleComment = Sym("unterm_editor_toggle_comment");
@@ -650,6 +666,19 @@ public void EditorSetTheme(ulong id, Color bg, Color32 fg, bool dark) =>
public void EditorRefreshDiff(ulong id) => _edRefreshDiff?.Invoke(id);
/// Apply a finished background git fetch; true if new markers arrived (re-render).
public bool EditorPollDiff(ulong id) => _edPollDiff?.Invoke(id) ?? false;
+ /// Whether the native bundle supports Markdown preview (older bundles don't).
+ public bool EditorPreviewSupported => _edSetPreview != null;
+ /// Toggle Markdown-preview mode (render/texture/scroll/mouse/copy route to it).
+ public void EditorSetPreview(ulong id, bool on) => _edSetPreview?.Invoke(id, on);
+ /// Whether Markdown-preview mode is on.
+ public bool EditorPreviewActive(ulong id) => _edPreviewActive?.Invoke(id) ?? false;
+ /// The existing-file path token under (x,y) in preview mode (empty if none).
+ public string EditorPreviewTokenAt(ulong id, float x, float y)
+ {
+ if (_edPreviewTokenAt == null) return string.Empty;
+ var p = _edPreviewTokenAt(id, x, y, out UIntPtr len);
+ return Utf8(p, len);
+ }
public void EditorRender(ulong id) => _edRender(id);
public IntPtr EditorRawTexture(ulong id) => _edRawTexture(id);
public float EditorContentHeight(ulong id) => _edContentHeight(id);
@@ -692,6 +721,18 @@ public void EditorKey(ulong id, string name, bool ctrl, bool alt, bool shift) =>
public void EditorScrollH(ulong id, float dx) => _edScrollH(id, dx);
public void EditorSetScroll(ulong id, float px) => _edSetScroll(id, px);
public float EditorScrollOffset(ulong id) => _edScrollOffset(id);
+ public float EditorGutterWidth(ulong id) => _edGutterWidth != null ? _edGutterWidth(id) : 0f;
+ public uint EditorLineAtY(ulong id, float y) => _edLineAtY != null ? _edLineAtY(id, y) : 0u;
+ public void EditorToggleBreakpoint(ulong id, uint line) => _edToggleBp?.Invoke(id, line);
+ public void EditorSetBpGutter(ulong id, bool on) => _edSetBpGutter?.Invoke(id, on);
+ public void EditorSetBreakpoints(ulong id, uint[] lines)
+ {
+ if (_edSetBps == null) return;
+ if (lines == null || lines.Length == 0) { _edSetBps(id, IntPtr.Zero, UIntPtr.Zero); return; }
+ var h = GCHandle.Alloc(lines, GCHandleType.Pinned);
+ try { _edSetBps(id, h.AddrOfPinnedObject(), (UIntPtr)lines.Length); }
+ finally { h.Free(); }
+ }
public void EditorIndent(ulong id) => _edIndent(id);
public void EditorOutdent(ulong id) => _edOutdent(id);
public void EditorToggleComment(ulong id) => _edToggleComment(id);
@@ -768,11 +809,13 @@ public void Dispose()
_edCreate = null; _edExists = null; _edDestroy = null; _edResize = null; _edSetScale = null; _edSetUndoLimit = null;
_edSetFont = null; _edSetTheme = null; _edSetLanguage = null; _edRender = null; _edRawTexture = null;
_edSetPath = null; _edRefreshDiff = null; _edPollDiff = null;
+ _edSetPreview = null; _edPreviewActive = null; _edPreviewTokenAt = null;
_edContentHeight = null; _edEditSerial = null; _edCaret = null; _edKey = null; _edSetFocus = null; _edInsert = null; _edSetPreedit = null;
_edSetText = null; _edAddUsing = null; _edText = null; _edUndo = null; _edRedo = null; _edSelectAll = null;
_edCopy = null; _edCut = null; _edMouse = null; _edHover = null; _edScroll = null;
_edHunkAt = null; _edHunkStaged = null; _edHunkHasStaged = null; _edHunkStagedOnly = null; _edStageHunk = null; _edUnstageHunk = null; _edRevertHunk = null;
_edSetScroll = null; _edScrollOffset = null; _edScrollH = null; _edIndent = null; _edOutdent = null;
+ _edGutterWidth = null; _edLineAtY = null; _edToggleBp = null; _edSetBps = null;
_edToggleComment = null; _edMoveUp = null; _edMoveDown = null; _edDuplicate = null;
_edDeleteLine = null; _edGotoLine = null; _edFind = null; _edReplaceSel = null; _edReplaceAll = null;
_edWordPrefix = null; _edComplete = null; _edSetCompletions = null; _edCaretOffset = null;
diff --git a/Packages/dev.tnayuki.unterm/Editor/UntermSettingsProvider.cs b/Packages/dev.tnayuki.unterm/Editor/UntermSettingsProvider.cs
index 1d6079a..955750e 100644
--- a/Packages/dev.tnayuki.unterm/Editor/UntermSettingsProvider.cs
+++ b/Packages/dev.tnayuki.unterm/Editor/UntermSettingsProvider.cs
@@ -35,6 +35,8 @@ public static SettingsProvider Create()
{
"unterm", "claude", "claude code", "agent", "terminal", "download",
"code editor", "undo", "history", "sound", "notify", "notification", "chime",
+ "debug", "debugger", "breakpoint", "extension", "extensions", "open",
+ "unity", "mcp", "tools", "security", "approval",
},
};
}
@@ -89,6 +91,20 @@ private static void OnGui()
if (nextLimit != curLimit)
UntermCodeEditorPrefs.UndoLimit = nextLimit;
+ string curExts = UntermOpenExtensions.Value;
+ string nextExts = EditorGUILayout.DelayedTextField(
+ new GUIContent("Openable extensions",
+ "Semicolon-separated file extensions the Unterm code editor claims: " +
+ "double-clicked assets (when Unterm is the External Script Editor) and " +
+ "file links clicked in the agent transcript. Anything else falls through " +
+ "to Unity's own handler or the OS default app."),
+ curExts);
+ if (nextExts != curExts)
+ UntermOpenExtensions.Value = nextExts;
+ if (nextExts != UntermOpenExtensions.Default &&
+ GUILayout.Button("Reset extensions to default", GUILayout.ExpandWidth(false)))
+ UntermOpenExtensions.Value = UntermOpenExtensions.Default;
+
EditorGUILayout.Space();
EditorGUILayout.LabelField("Agent", EditorStyles.boldLabel);
bool notify = UntermAgentPrefs.NotifySoundEnabled;
@@ -101,6 +117,18 @@ private static void OnGui()
if (nextNotify != notify)
UntermAgentPrefs.NotifySoundEnabled = nextNotify;
+ EditorGUILayout.Space();
+ EditorGUILayout.LabelField("Debugger", EditorStyles.boldLabel);
+ bool curDbg = UntermDebuggerPrefs.Enabled;
+ bool nextDbg = EditorGUILayout.Toggle(
+ new GUIContent("Enable debugging",
+ "Enables the Window/Unterm/Debugger (Standalone Process) menu and the code editor's breakpoint " +
+ "gutter (click left of the line numbers to set breakpoints; entering Play " +
+ "mode with breakpoints launches the debugger)."),
+ curDbg);
+ if (nextDbg != curDbg)
+ UntermDebuggerPrefs.Enabled = nextDbg;
+
EditorGUILayout.Space();
EditorGUILayout.LabelField("Unity MCP", EditorStyles.boldLabel);
bool mcpEnabled = UntermMcpSecurity.Enabled;
diff --git a/Packages/dev.tnayuki.unterm/Third Party Notices.md b/Packages/dev.tnayuki.unterm/Third Party Notices.md
index ea57124..75e657d 100644
--- a/Packages/dev.tnayuki.unterm/Third Party Notices.md
+++ b/Packages/dev.tnayuki.unterm/Third Party Notices.md
@@ -7,17 +7,19 @@ require preserving the copyright notice and license text reproduced here.
This file is generated with [`cargo about`](https://github.com/EmbarkStudios/cargo-about);
run `native/generate-notices.sh` from the repository root to regenerate it.
-- MIT License (244)
-- Apache License 2.0 (12)
+- MIT License (287)
+- Apache License 2.0 (15)
- zlib License (3)
- Creative Commons Zero v1.0 Universal (2)
- ISC License (1)
+- SIL Open Font License 1.1 (1)
+- Ubuntu Font Licence v1.0 (1)
- Unicode License v3 (1)
## Apache License 2.0
- moxcms 0.8.1 — https://github.com/awxkee/moxcms.git
-- pxfm 0.1.29 — https://github.com/awxkee/pxfm
+- pxfm 0.1.30 — https://github.com/awxkee/pxfm
```
Apache License
@@ -437,6 +439,215 @@ run `native/generate-notices.sh` from the repository root to regenerate it.
## Apache License 2.0
+- unicode-general-category 1.1.0 — https://github.com/yeslogic/unicode-general-category
+
+```
+ 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
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+```
+
+## Apache License 2.0
+
- alacritty_terminal 0.25.1 — https://github.com/alacritty/alacritty
```
@@ -778,84 +989,294 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
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.
+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
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+```
+
+## Apache License 2.0
+
+- glutin_wgl_sys 0.6.1 — https://github.com/rust-windowing/glutin
+
+```
+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.
+ 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.
+ 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.
+ 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.
+ 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
+ END OF TERMS AND CONDITIONS
-APPENDIX: How to apply the Apache License to your work.
+ APPENDIX: How to apply the Apache License to your work.
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
-Copyright [yyyy] [name of copyright owner]
+ Copyright 2022 Kirill Chibisov
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ http://www.apache.org/licenses/LICENSE-2.0
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
```
## Apache License 2.0
-- glutin_wgl_sys 0.6.1 — https://github.com/rust-windowing/glutin
+- dpi 0.1.2 — https://github.com/rust-windowing/winit
+- winit 0.30.13 — https://github.com/rust-windowing/winit
```
Apache License
@@ -1046,7 +1467,7 @@ Apache License
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2022 Kirill Chibisov
+ Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1059,7 +1480,6 @@ Apache License
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-
```
## Apache License 2.0
@@ -1380,6 +1800,9 @@ DEALINGS IN THE SOFTWARE.
- core-foundation-sys 0.8.7 — https://github.com/servo/core-foundation-rs
- core-foundation 0.10.1 — https://github.com/servo/core-foundation-rs
+- core-foundation 0.9.4 — https://github.com/servo/core-foundation-rs
+- core-graphics-types 0.1.3 — https://github.com/servo/core-foundation-rs
+- core-graphics 0.23.2 — https://github.com/servo/core-foundation-rs
- euclid 0.22.14 — https://github.com/servo/euclid
```
@@ -1413,13 +1836,14 @@ DEALINGS IN THE SOFTWARE.
## MIT License
-- cc 1.2.65 — https://github.com/rust-lang/cc-rs
+- cc 1.2.67 — https://github.com/rust-lang/cc-rs
- cfg-if 1.0.4 — https://github.com/rust-lang/cfg-if
- filetime 0.2.29 — https://github.com/alexcrichton/filetime
- find-msvc-tools 0.1.9 — https://github.com/rust-lang/cc-rs
- git2 0.21.0 — https://github.com/rust-lang/git2-rs
- jobserver 0.1.35 — https://github.com/rust-lang/jobserver-rs
- pkg-config 0.3.33 — https://github.com/rust-lang/pkg-config-rs
+- socket2 0.5.10 — https://github.com/rust-lang/socket2
```
Copyright (c) 2014 Alex Crichton
@@ -1554,14 +1978,48 @@ DEALINGS IN THE SOFTWARE.
- bitflags 1.3.2 — https://github.com/bitflags/bitflags
- bitflags 2.13.0 — https://github.com/bitflags/bitflags
-- log 0.4.32 — https://github.com/rust-lang/log
+- log 0.4.33 — https://github.com/rust-lang/log
- num-traits 0.2.19 — https://github.com/rust-num/num-traits
-- regex-automata 0.4.14 — https://github.com/rust-lang/regex
+- regex-automata 0.4.15 — https://github.com/rust-lang/regex
- regex-syntax 0.8.11 — https://github.com/rust-lang/regex
-- regex 1.12.4 — https://github.com/rust-lang/regex
+- regex 1.13.0 — https://github.com/rust-lang/regex
+
+```
+Copyright (c) 2014 The Rust Project Developers
+
+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.
+
+```
+
+## MIT License
+
+- uuid 1.23.5 — https://github.com/uuid-rs/uuid
```
Copyright (c) 2014 The Rust Project Developers
+Copyright (c) 2018 Ashley Mannix, Christopher Armstrong, Dylan DPC, Hunar Roop Kahlon
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -1651,6 +2109,40 @@ THE SOFTWARE.
## MIT License
+- either 1.16.0 — https://github.com/rayon-rs/either
+- itertools 0.14.0 — https://github.com/rust-itertools/itertools
+
+```
+Copyright (c) 2015
+
+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.
+
+```
+
+## MIT License
+
- ioctl-rs 0.1.6 — https://github.com/dcuddeback/ioctl-rs
- serial-core 0.4.0 — https://github.com/dcuddeback/serial-rs
- serial-unix 0.4.0 — https://github.com/dcuddeback/serial-rs
@@ -2081,12 +2573,78 @@ SOFTWARE.
## MIT License
-- lock_api 0.4.14 — https://github.com/Amanieu/parking_lot
-- parking_lot 0.12.5 — https://github.com/Amanieu/parking_lot
-- parking_lot_core 0.9.12 — https://github.com/Amanieu/parking_lot
+- lock_api 0.4.14 — https://github.com/Amanieu/parking_lot
+- parking_lot 0.12.5 — https://github.com/Amanieu/parking_lot
+- parking_lot_core 0.9.12 — https://github.com/Amanieu/parking_lot
+
+```
+Copyright (c) 2016 The Rust Project Developers
+
+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.
+
+```
+
+## MIT License
+
+- shell-words 1.1.1 — https://github.com/tmiasko/shell-words
+
+```
+Copyright (c) 2016 Tomasz Miąsko
+
+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.
+
+```
+
+## MIT License
+
+- indexmap 2.14.0 — https://github.com/indexmap-rs/indexmap
```
-Copyright (c) 2016 The Rust Project Developers
+Copyright (c) 2016--2017
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -2116,10 +2674,10 @@ DEALINGS IN THE SOFTWARE.
## MIT License
-- shell-words 1.1.1 — https://github.com/tmiasko/shell-words
+- equivalent 1.0.2 — https://github.com/indexmap-rs/equivalent
```
-Copyright (c) 2016 Tomasz Miąsko
+Copyright (c) 2016--2023
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -2149,10 +2707,10 @@ DEALINGS IN THE SOFTWARE.
## MIT License
-- indexmap 2.14.0 — https://github.com/indexmap-rs/indexmap
+- scopeguard 1.2.0 — https://github.com/bluss/scopeguard
```
-Copyright (c) 2016--2017
+Copyright (c) 2016-2019 Ulrik Sverdrup "bluss" and scopeguard developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -2182,10 +2740,10 @@ DEALINGS IN THE SOFTWARE.
## MIT License
-- equivalent 1.0.2 — https://github.com/indexmap-rs/equivalent
+- fnv 1.0.7 — https://github.com/servo/rust-fnv
```
-Copyright (c) 2016--2023
+Copyright (c) 2017 Contributors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -2215,10 +2773,36 @@ DEALINGS IN THE SOFTWARE.
## MIT License
-- scopeguard 1.2.0 — https://github.com/bluss/scopeguard
+- memoffset 0.6.5 — https://github.com/Gilnaa/memoffset
```
-Copyright (c) 2016-2019 Ulrik Sverdrup "bluss" and scopeguard developers
+Copyright (c) 2017 Gilad Naaman
+
+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.
+```
+
+## MIT License
+
+- vcpkg 0.2.15 — https://github.com/mcgoo/vcpkg-rs
+
+```
+Copyright (c) 2017 Jim McGrath
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -2248,10 +2832,10 @@ DEALINGS IN THE SOFTWARE.
## MIT License
-- fnv 1.0.7 — https://github.com/servo/rust-fnv
+- shared_library 0.1.9 — https://github.com/tomaka/shared_library/
```
-Copyright (c) 2017 Contributors
+Copyright (c) 2017 Pierre Krieger
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -2276,15 +2860,16 @@ 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.
-
```
## MIT License
-- memoffset 0.6.5 — https://github.com/Gilnaa/memoffset
+- foreign-types-macros 0.2.3 — https://github.com/sfackler/foreign-types
+- foreign-types-shared 0.3.1 — https://github.com/sfackler/foreign-types
+- foreign-types 0.5.0 — https://github.com/sfackler/foreign-types
```
-Copyright (c) 2017 Gilad Naaman
+Copyright (c) 2017 The foreign-types Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -2303,14 +2888,16 @@ 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.
+
```
## MIT License
-- vcpkg 0.2.15 — https://github.com/mcgoo/vcpkg-rs
+- signal-hook-registry 1.4.8 — https://github.com/vorner/signal-hook
+- signal-hook 0.3.18 — https://github.com/vorner/signal-hook
```
-Copyright (c) 2017 Jim McGrath
+Copyright (c) 2017 tokio-jsonrpc developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -2340,10 +2927,10 @@ DEALINGS IN THE SOFTWARE.
## MIT License
-- shared_library 0.1.9 — https://github.com/tomaka/shared_library/
+- autocfg 1.5.1 — https://github.com/cuviper/autocfg
```
-Copyright (c) 2017 Pierre Krieger
+Copyright (c) 2018 Josh Stone
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -2368,15 +2955,18 @@ 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.
+
```
## MIT License
-- signal-hook-registry 1.4.8 — https://github.com/vorner/signal-hook
-- signal-hook 0.3.18 — https://github.com/vorner/signal-hook
+- fearless_simd 0.4.1 — https://github.com/linebender/fearless_simd
+- kurbo 0.13.1 — https://github.com/linebender/kurbo
+- peniko 0.6.1 — https://github.com/linebender/peniko
+- polycool 0.4.0 — https://github.com/linebender/kurbo
```
-Copyright (c) 2017 tokio-jsonrpc developers
+Copyright (c) 2018 Raph Levien
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -2406,10 +2996,10 @@ DEALINGS IN THE SOFTWARE.
## MIT License
-- autocfg 1.5.1 — https://github.com/cuviper/autocfg
+- smallvec 1.15.2 — https://github.com/servo/rust-smallvec
```
-Copyright (c) 2018 Josh Stone
+Copyright (c) 2018 The Servo Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -2439,10 +3029,10 @@ DEALINGS IN THE SOFTWARE.
## MIT License
-- smallvec 1.15.2 — https://github.com/servo/rust-smallvec
+- pin-utils 0.1.0 — https://github.com/rust-lang-nursery/pin-utils
```
-Copyright (c) 2018 The Servo Project Developers
+Copyright (c) 2018 The pin-utils authors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -2472,10 +3062,10 @@ DEALINGS IN THE SOFTWARE.
## MIT License
-- pin-utils 0.1.0 — https://github.com/rust-lang-nursery/pin-utils
+- ahash 0.8.12 — https://github.com/tkaitchuck/ahash
```
-Copyright (c) 2018 The pin-utils authors
+Copyright (c) 2018 Tom Kaitchuck
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -2636,6 +3226,40 @@ DEALINGS IN THE SOFTWARE.
## MIT License
+- tracing-core 0.1.36 — https://github.com/tokio-rs/tracing
+- tracing 0.1.44 — https://github.com/tokio-rs/tracing
+
+```
+Copyright (c) 2019 Tokio 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.
+
+```
+
+## MIT License
+
- downcast-rs 1.2.1 — https://github.com/marcianx/downcast-rs
```
@@ -3089,8 +3713,8 @@ DEALINGS IN THE SOFTWARE.
- anstyle-wincon 3.0.11 — https://github.com/rust-cli/anstyle.git
- anstyle 1.0.14 — https://github.com/rust-cli/anstyle.git
- colorchoice 1.0.5 — https://github.com/rust-cli/anstyle.git
-- env_filter 1.0.1 — https://github.com/rust-cli/env_logger
-- env_logger 0.11.10 — https://github.com/rust-cli/env_logger
+- env_filter 2.0.0 — https://github.com/rust-cli/env_logger
+- env_logger 0.11.11 — https://github.com/rust-cli/env_logger
- is_terminal_polyfill 1.70.2 — https://github.com/polyfill-rs/is_terminal_polyfill
- once_cell_polyfill 1.70.2 — https://github.com/polyfill-rs/once_cell_polyfill
@@ -3147,7 +3771,7 @@ SOFTWARE.
## MIT License
- time-core 0.1.9 — https://github.com/time-rs/time
-- time 0.3.49 — https://github.com/time-rs/time
+- time 0.3.53 — https://github.com/time-rs/time
```
Copyright (c) Jacob Pratt et al.
@@ -3207,7 +3831,7 @@ DEALINGS IN THE SOFTWARE.
## MIT License
-- arrayvec 0.7.6 — https://github.com/bluss/arrayvec
+- arrayvec 0.7.8 — https://github.com/bluss/arrayvec
```
Copyright (c) Ulrik Sverdrup "bluss" 2015-2023
@@ -3240,6 +3864,32 @@ DEALINGS IN THE SOFTWARE.
## MIT License
+- nohash-hasher 0.2.0 — https://github.com/paritytech/nohash-hasher
+
+```
+Copyright 2018 Parity Technologies (UK) Ltd.
+
+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.
+
+```
+
+## MIT License
+
- rangemap 1.7.1 — https://github.com/jeffparsons/rangemap
```
@@ -3255,8 +3905,42 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
## MIT License
-- zerocopy-derive 0.8.52 — https://github.com/google/zerocopy
-- zerocopy 0.8.52 — https://github.com/google/zerocopy
+- vello_common 0.0.9 — https://github.com/linebender/vello
+- vello_cpu 0.0.9 — https://github.com/linebender/vello
+
+```
+Copyright 2020 the Vello Authors
+
+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.
+
+```
+
+## MIT License
+
+- zerocopy-derive 0.8.54 — https://github.com/google/zerocopy
+- zerocopy 0.8.54 — https://github.com/google/zerocopy
```
Copyright 2023 The Fuchsia Authors
@@ -3289,6 +3973,23 @@ DEALINGS IN THE SOFTWARE.
## MIT License
+- epaint_default_fonts 0.35.0 — https://github.com/emilk/egui/tree/main/crates/epaint_default_fonts
+
+```
+MIT License
+
+Copyright (c) 2014 John Slegers
+
+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.
+
+```
+
+## MIT License
+
- lru 0.16.4 — https://github.com/jeromefroe/lru-rs.git
```
@@ -3463,8 +4164,8 @@ SOFTWARE.
## MIT License
-- bytemuck 1.25.0 — https://github.com/Lokathor/bytemuck
-- bytemuck_derive 1.10.2 — https://github.com/Lokathor/bytemuck
+- bytemuck 1.25.1 — https://github.com/Lokathor/bytemuck
+- bytemuck_derive 1.11.0 — https://github.com/Lokathor/bytemuck
```
MIT License
@@ -3614,6 +4315,35 @@ SOFTWARE.
## MIT License
+- web-time 1.1.0 — https://github.com/daxpedda/web-time
+
+```
+MIT License
+
+Copyright (c) 2023 dAxpeDDa
+
+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.
+
+```
+
+## MIT License
+
- core_maths 0.1.1 — https://github.com/robertbastian/core_maths
```
@@ -3642,14 +4372,14 @@ SOFTWARE.
## MIT License
-- naga 29.0.3 — https://github.com/gfx-rs/wgpu
-- wgpu-core-deps-apple 29.0.3 — https://github.com/gfx-rs/wgpu
-- wgpu-core-deps-windows-linux-android 29.0.3 — https://github.com/gfx-rs/wgpu
-- wgpu-core 29.0.3 — https://github.com/gfx-rs/wgpu
-- wgpu-hal 29.0.3 — https://github.com/gfx-rs/wgpu
-- wgpu-naga-bridge 29.0.3 — https://github.com/gfx-rs/wgpu
-- wgpu-types 29.0.3 — https://github.com/gfx-rs/wgpu
-- wgpu 29.0.3 — https://github.com/gfx-rs/wgpu
+- naga 29.0.4 — https://github.com/gfx-rs/wgpu
+- wgpu-core-deps-apple 29.0.4 — https://github.com/gfx-rs/wgpu
+- wgpu-core-deps-windows-linux-android 29.0.4 — https://github.com/gfx-rs/wgpu
+- wgpu-core 29.0.4 — https://github.com/gfx-rs/wgpu
+- wgpu-hal 29.0.4 — https://github.com/gfx-rs/wgpu
+- wgpu-naga-bridge 29.0.4 — https://github.com/gfx-rs/wgpu
+- wgpu-types 29.0.4 — https://github.com/gfx-rs/wgpu
+- wgpu 29.0.4 — https://github.com/gfx-rs/wgpu
```
MIT License
@@ -3679,12 +4409,25 @@ SOFTWARE.
## MIT License
- unterm 0.1.0
+- accesskit 0.24.1 — https://github.com/AccessKit/accesskit
+- block2 0.5.1 — https://github.com/madsmtm/objc2
- block2 0.6.2 — https://github.com/madsmtm/objc2
- dispatch2 0.3.1 — https://github.com/madsmtm/objc2
+- dispatch 0.2.0 — http://github.com/SSheldon/rust-dispatch
+- dpi 0.1.2 — https://github.com/rust-windowing/winit
+- ecolor 0.35.0 — https://github.com/emilk/egui
+- egui-wgpu 0.35.0 — https://github.com/emilk/egui/tree/main/crates/egui-wgpu
+- egui-winit 0.35.0 — https://github.com/emilk/egui/tree/main/crates/egui-winit
+- egui 0.35.0 — https://github.com/emilk/egui
+- emath 0.35.0 — https://github.com/emilk/egui/tree/main/crates/emath
+- epaint 0.35.0 — https://github.com/emilk/egui/tree/main/crates/epaint
- gpu-descriptor-types 0.2.0 — https://github.com/zakarumych/gpu-descriptor
- gpu-descriptor 0.3.2 — https://github.com/zakarumych/gpu-descriptor
- harfrust 0.5.2 — https://github.com/harfbuzz/harfrust
+- harfrust 0.7.0 — https://github.com/harfbuzz/harfrust
- libm 0.2.16 — https://github.com/rust-lang/compiler-builtins
+- objc-sys 0.3.5 — https://github.com/madsmtm/objc2
+- objc2-app-kit 0.2.2 — https://github.com/madsmtm/objc2
- objc2-app-kit 0.3.2 — https://github.com/madsmtm/objc2
- objc2-cloud-kit 0.3.2 — https://github.com/madsmtm/objc2
- objc2-core-data 0.3.2 — https://github.com/madsmtm/objc2
@@ -3694,16 +4437,20 @@ SOFTWARE.
- objc2-core-text 0.3.2 — https://github.com/madsmtm/objc2
- objc2-core-video 0.3.2 — https://github.com/madsmtm/objc2
- objc2-encode 4.1.0 — https://github.com/madsmtm/objc2
+- objc2-foundation 0.2.2 — https://github.com/madsmtm/objc2
- objc2-foundation 0.3.2 — https://github.com/madsmtm/objc2
- objc2-io-surface 0.3.2 — https://github.com/madsmtm/objc2
- objc2-metal 0.3.2 — https://github.com/madsmtm/objc2
- objc2-quartz-core 0.3.2 — https://github.com/madsmtm/objc2
+- objc2 0.5.2 — https://github.com/madsmtm/objc2
- objc2 0.6.4 — https://github.com/madsmtm/objc2
- profiling 1.0.18 — https://github.com/aclysma/profiling
- serial-windows 0.4.0 — https://github.com/dcuddeback/serial-rs
- serial 0.4.0 — https://github.com/dcuddeback/serial-rs
- siphasher 1.0.3 — https://github.com/jedisct1/rust-siphash
+- tree-sitter-md 0.5.3 — https://github.com/tree-sitter-grammars/tree-sitter-markdown
- tree-sitter 0.25.10 — https://github.com/tree-sitter/tree-sitter
+- type-map 0.5.1 — https://github.com/kardeiz/type-map
- unity-native-plugin-sys 0.9.0 — https://github.com/aosoft/unity-native-plugin-rs
- unity-native-plugin 0.9.0 — https://github.com/aosoft/unity-native-plugin-rs
- windows-collections 0.3.2 — https://github.com/microsoft/windows-rs
@@ -3716,6 +4463,7 @@ SOFTWARE.
- windows-result 0.4.1 — https://github.com/microsoft/windows-rs
- windows-strings 0.5.1 — https://github.com/microsoft/windows-rs
- windows-sys 0.48.0 — https://github.com/microsoft/windows-rs
+- windows-sys 0.52.0 — https://github.com/microsoft/windows-rs
- windows-sys 0.59.0 — https://github.com/microsoft/windows-rs
- windows-sys 0.61.2 — https://github.com/microsoft/windows-rs
- windows-targets 0.48.5 — https://github.com/microsoft/windows-rs
@@ -3844,6 +4592,7 @@ DEALINGS IN THE SOFTWARE.
## MIT License
+- color 0.3.3 — https://github.com/linebender/color
- half 2.7.1 — https://github.com/VoidStarKat/half-rs
- linebender_resource_handle 0.1.1 — https://github.com/linebender/raw_resource_handle
@@ -3907,7 +4656,7 @@ DEALINGS IN THE SOFTWARE.
## MIT License
-- rustc-hash 2.1.2 — https://github.com/rust-lang/rustc-hash
+- rustc-hash 2.1.3 — https://github.com/rust-lang/rustc-hash
```
Permission is hereby granted, free of charge, to any
@@ -3950,7 +4699,7 @@ DEALINGS IN THE SOFTWARE.
- piper 0.2.5 — https://github.com/smol-rs/piper
- polling 3.11.0 — https://github.com/smol-rs/polling
- proc-macro2 1.0.106 — https://github.com/dtolnay/proc-macro2
-- quote 1.0.45 — https://github.com/dtolnay/quote
+- quote 1.0.46 — https://github.com/dtolnay/quote
- rustc-hash 1.1.0 — https://github.com/rust-lang-nursery/rustc-hash
- rustix-openpty 0.2.0 — https://github.com/sunfishcode/rustix-openpty
- rustix 1.1.4 — https://github.com/bytecodealliance/rustix
@@ -3958,6 +4707,7 @@ DEALINGS IN THE SOFTWARE.
- serde_core 1.0.228 — https://github.com/serde-rs/serde
- serde_derive 1.0.228 — https://github.com/serde-rs/serde
- serde_json 1.0.150 — https://github.com/serde-rs/json
+- smol_str 0.2.2 — https://github.com/rust-analyzer/smol_str
- smol_str 0.3.6 — https://github.com/rust-lang/rust-analyzer/tree/master/lib/smol_str
- syn 2.0.118 — https://github.com/dtolnay/syn
- thiserror-impl 1.0.69 — https://github.com/dtolnay/thiserror
@@ -3965,7 +4715,7 @@ DEALINGS IN THE SOFTWARE.
- thiserror 1.0.69 — https://github.com/dtolnay/thiserror
- thiserror 2.0.18 — https://github.com/dtolnay/thiserror
- unicode-ident 1.0.24 — https://github.com/dtolnay/unicode-ident
-- zmij 1.0.21 — https://github.com/dtolnay/zmij
+- zmij 1.0.23 — https://github.com/dtolnay/zmij
```
Permission is hereby granted, free of charge, to any
@@ -4083,7 +4833,7 @@ SOFTWARE.
## MIT License
-- tinyvec 1.11.0 — https://github.com/Lokathor/tinyvec
+- tinyvec 1.12.0 — https://github.com/Lokathor/tinyvec
```
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:
@@ -4272,8 +5022,8 @@ THE SOFTWARE.
- aho-corasick 1.1.4 — https://github.com/BurntSushi/aho-corasick
- byteorder-lite 0.1.0 — https://github.com/image-rs/byteorder-lite
-- jiff 0.2.28 — https://github.com/BurntSushi/jiff
-- memchr 2.8.2 — https://github.com/BurntSushi/memchr
+- jiff 0.2.32 — https://github.com/BurntSushi/jiff
+- memchr 2.8.3 — https://github.com/BurntSushi/memchr
- walkdir 2.5.0 — https://github.com/BurntSushi/walkdir
```
@@ -4478,7 +5228,35 @@ SOFTWARE.
## MIT License
-- crossbeam-utils 0.8.21 — https://github.com/crossbeam-rs/crossbeam
+- guillotiere 0.7.0 — https://github.com/nical/guillotiere
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2019 Nicolas Silva
+
+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.
+
+```
+
+## MIT License
+
+- crossbeam-utils 0.8.22 — https://github.com/crossbeam-rs/crossbeam
```
The MIT License (MIT)
@@ -4712,6 +5490,210 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
```
+## SIL Open Font License 1.1
+
+- epaint_default_fonts 0.35.0 — https://github.com/emilk/egui/tree/main/crates/epaint_default_fonts
+
+```
+This Font Software is licensed under the SIL Open Font License,
+Version 1.1.
+
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font
+creation efforts of academic and linguistic communities, and to
+provide a free and open framework in which fonts may be shared and
+improved in partnership with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply to
+any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software
+components as distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to,
+deleting, or substituting -- in part or in whole -- any of the
+components of the Original Version, by changing formats or by porting
+the Font Software to a new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed,
+modify, redistribute, and sell modified and unmodified copies of the
+Font Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components, in
+Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the
+corresponding Copyright Holder. This restriction only applies to the
+primary font name as presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created using
+the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
+
+```
+
+## Ubuntu Font Licence v1.0
+
+- epaint_default_fonts 0.35.0 — https://github.com/emilk/egui/tree/main/crates/epaint_default_fonts
+
+```
+-------------------------------
+UBUNTU FONT LICENCE Version 1.0
+-------------------------------
+
+PREAMBLE
+This licence allows the licensed fonts to be used, studied, modified and
+redistributed freely. The fonts, including any derivative works, can be
+bundled, embedded, and redistributed provided the terms of this licence
+are met. The fonts and derivatives, however, cannot be released under
+any other licence. The requirement for fonts to remain under this
+licence does not require any document created using the fonts or their
+derivatives to be published under this licence, as long as the primary
+purpose of the document is not to be a vehicle for the distribution of
+the fonts.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this licence and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Original Version" refers to the collection of Font Software components
+as received under this licence.
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to
+a new environment.
+
+"Copyright Holder(s)" refers to all individuals and companies who have a
+copyright ownership of the Font Software.
+
+"Substantially Changed" refers to Modified Versions which can be easily
+identified as dissimilar to the Font Software by users of the Font
+Software comparing the Original Version with the Modified Version.
+
+To "Propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification and with or without charging
+a redistribution fee), making available to the public, and in some
+countries other activities as well.
+
+PERMISSION & CONDITIONS
+This licence does not grant any rights under trademark law and all such
+rights are reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of the Font Software, to propagate the Font Software, subject to
+the below conditions:
+
+1) Each copy of the Font Software must contain the above copyright
+notice and this licence. These can be included either as stand-alone
+text files, human-readable headers or in the appropriate machine-
+readable metadata fields within text or binary files as long as those
+fields can be easily viewed by the user.
+
+2) The font name complies with the following:
+(a) The Original Version must retain its name, unmodified.
+(b) Modified Versions which are Substantially Changed must be renamed to
+avoid use of the name of the Original Version or similar names entirely.
+(c) Modified Versions which are not Substantially Changed must be
+renamed to both (i) retain the name of the Original Version and (ii) add
+additional naming elements to distinguish the Modified Version from the
+Original Version. The name of such Modified Versions must be the name of
+the Original Version, with "derivative X" where X represents the name of
+the new work, appended to that name.
+
+3) The name(s) of the Copyright Holder(s) and any contributor to the
+Font Software shall not be used to promote, endorse or advertise any
+Modified Version, except (i) as required by this licence, (ii) to
+acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
+their explicit written permission.
+
+4) The Font Software, modified or unmodified, in part or in whole, must
+be distributed entirely under this licence, and must not be distributed
+under any other licence. The requirement for fonts to remain under this
+licence does not affect any document created using the Font Software,
+except any version of the Font Software extracted from a document
+created using the Font Software may only be distributed under this
+licence.
+
+TERMINATION
+This licence becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
+COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
+DEALINGS IN THE FONT SOFTWARE.
+
+```
+
## Unicode License v3
- unicode-ident 1.0.24 — https://github.com/dtolnay/unicode-ident
diff --git a/native/Cargo.lock b/native/Cargo.lock
index db38e59..b898464 100644
--- a/native/Cargo.lock
+++ b/native/Cargo.lock
@@ -2,12 +2,33 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "accesskit"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320"
+dependencies = [
+ "uuid",
+]
+
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
[[package]]
name = "aho-corasick"
version = "1.1.4"
@@ -33,7 +54,7 @@ dependencies = [
"piper",
"polling",
"regex-automata",
- "rustix",
+ "rustix 1.1.4",
"rustix-openpty",
"serde",
"signal-hook",
@@ -48,6 +69,31 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+[[package]]
+name = "android-activity"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd"
+dependencies = [
+ "android-properties",
+ "bitflags 2.13.0",
+ "cc",
+ "jni",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "num_enum",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "android-properties"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
+
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -115,9 +161,9 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "arrayvec"
-version = "0.7.6"
+version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
[[package]]
name = "ash"
@@ -200,13 +246,22 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "block2"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2 0.5.2",
+]
+
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
- "objc2",
+ "objc2 0.6.4",
]
[[package]]
@@ -217,18 +272,18 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytemuck"
-version = "1.25.0"
+version = "1.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
-version = "1.10.2"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
+checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5"
dependencies = [
"proc-macro2",
"quote",
@@ -241,11 +296,31 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "calloop"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
+dependencies = [
+ "bitflags 2.13.0",
+ "log",
+ "polling",
+ "rustix 0.38.44",
+ "slab",
+ "thiserror 1.0.69",
+]
+
[[package]]
name = "cc"
-version = "1.2.65"
+version = "1.2.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
+checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -276,12 +351,31 @@ dependencies = [
"unicode-width 0.2.2",
]
+[[package]]
+name = "color"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf"
+dependencies = [
+ "bytemuck",
+]
+
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+[[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"
@@ -291,6 +385,16 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "core-foundation"
version = "0.10.1"
@@ -307,6 +411,30 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+[[package]]
+name = "core-graphics"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation 0.9.4",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation 0.9.4",
+ "libc",
+]
+
[[package]]
name = "core_maths"
version = "0.1.1"
@@ -324,14 +452,14 @@ checksum = "bbe782a9e7520cc7de2232c957a47f99d3a35e855552677d07a557bc1a3b66ed"
dependencies = [
"bitflags 2.13.0",
"fontdb",
- "harfrust",
+ "harfrust 0.5.2",
"linebender_resource_handle",
"log",
"rangemap",
- "rustc-hash 2.1.2",
+ "rustc-hash 2.1.3",
"self_cell",
"skrifa 0.40.0",
- "smol_str",
+ "smol_str 0.3.6",
"swash",
"sys-locale",
"unicode-bidi",
@@ -351,9 +479,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
-version = "0.8.21"
+version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
[[package]]
name = "crunchy"
@@ -367,12 +495,49 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror 2.0.18",
+]
+
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+[[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"
@@ -380,7 +545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags 2.13.0",
- "objc2",
+ "objc2 0.6.4",
]
[[package]]
@@ -407,11 +572,96 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+[[package]]
+name = "dpi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+
+[[package]]
+name = "ecolor"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6758be723a3f298bbfda4db75748bc2ba0abafe096b6383c7c32da264764fbc3"
+dependencies = [
+ "bytemuck",
+ "emath",
+]
+
+[[package]]
+name = "egui"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2796c98d50b79631281d516343a6f6e93c0666462ca36e2c93b39f25d7793325"
+dependencies = [
+ "accesskit",
+ "ahash",
+ "bitflags 2.13.0",
+ "emath",
+ "epaint",
+ "itertools",
+ "log",
+ "nohash-hasher",
+ "profiling",
+ "smallvec",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "egui-wgpu"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2e6cfac0725563555fa4f91e9f799b9d7c6c5dd831fca6abc8234afc64b7a34"
+dependencies = [
+ "ahash",
+ "bytemuck",
+ "document-features",
+ "epaint",
+ "log",
+ "profiling",
+ "thiserror 2.0.18",
+ "type-map",
+ "web-time",
+ "wgpu",
+]
+
+[[package]]
+name = "egui-winit"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ea6bf3608db949588b95b8b341ee358d0c3f95cf4dc3f53d8d76717edee87db"
+dependencies = [
+ "egui",
+ "log",
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
+ "objc2-ui-kit 0.3.2",
+ "profiling",
+ "raw-window-handle",
+ "web-time",
+ "winit",
+]
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
+[[package]]
+name = "emath"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd4ec073c9898516584d8c6cfdcee95b530b3d941cd5031ef4050aa36812308b"
+dependencies = [
+ "bytemuck",
+]
+
[[package]]
name = "env_filter"
-version = "1.0.1"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
+checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
dependencies = [
"log",
"regex",
@@ -419,9 +669,9 @@ dependencies = [
[[package]]
name = "env_logger"
-version = "0.11.10"
+version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
+checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
dependencies = [
"anstream",
"anstyle",
@@ -430,6 +680,37 @@ dependencies = [
"log",
]
+[[package]]
+name = "epaint"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e60a8888b51da911df23918fd7301359b1d43a406a0ff3b8863af093dd7fc6c"
+dependencies = [
+ "ahash",
+ "bytemuck",
+ "ecolor",
+ "emath",
+ "epaint_default_fonts",
+ "font-types",
+ "harfrust 0.7.0",
+ "log",
+ "nohash-hasher",
+ "parking_lot",
+ "profiling",
+ "self_cell",
+ "skrifa 0.42.1",
+ "smallvec",
+ "unicode-general-category",
+ "unicode-segmentation",
+ "vello_cpu",
+]
+
+[[package]]
+name = "epaint_default_fonts"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13ee4e1f553a3584c301f3a56ff1a775f1384781396cea301c8d952e9b93f560"
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -490,6 +771,12 @@ dependencies = [
"simd-adler32",
]
+[[package]]
+name = "fearless_simd"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693"
+
[[package]]
name = "filedescriptor"
version = "0.8.3"
@@ -577,6 +864,33 @@ dependencies = [
"ttf-parser",
]
+[[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",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
[[package]]
name = "fsevent-sys"
version = "4.1.0"
@@ -650,6 +964,22 @@ dependencies = [
"xml-rs",
]
+[[package]]
+name = "glifo"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d99fc21d493812643aae86d53b7bbd02f376434a90317e8a790bc209fdd6605e"
+dependencies = [
+ "bytemuck",
+ "foldhash 0.2.0",
+ "hashbrown 0.17.1",
+ "log",
+ "peniko",
+ "skrifa 0.42.1",
+ "smallvec",
+ "vello_common",
+]
+
[[package]]
name = "glow"
version = "0.17.0"
@@ -680,7 +1010,7 @@ dependencies = [
"cosmic-text",
"etagere",
"lru",
- "rustc-hash 2.1.2",
+ "rustc-hash 2.1.3",
"wgpu",
]
@@ -718,6 +1048,15 @@ dependencies = [
"bitflags 2.13.0",
]
+[[package]]
+name = "guillotiere"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab"
+dependencies = [
+ "euclid",
+]
+
[[package]]
name = "half"
version = "2.7.1"
@@ -743,6 +1082,18 @@ dependencies = [
"smallvec",
]
+[[package]]
+name = "harfrust"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0431e8e389aa0f1e72bb9d1c2db8957a1a7a3580e8ed97db819c14837aac9b3e"
+dependencies = [
+ "bitflags 2.13.0",
+ "bytemuck",
+ "read-fonts 0.39.2",
+ "smallvec",
+]
+
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -768,6 +1119,9 @@ name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+dependencies = [
+ "foldhash 0.2.0",
+]
[[package]]
name = "hermit-abi"
@@ -826,9 +1180,9 @@ dependencies = [
[[package]]
name = "inotify-sys"
-version = "0.1.7"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ea94e891b3606826e9c998be69ddca42247dad8ad50b1649a5cb7e1c9ae06fd"
+checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d"
dependencies = [
"libc",
]
@@ -857,6 +1211,15 @@ dependencies = [
"phf",
]
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itoa"
version = "1.0.18"
@@ -865,10 +1228,11 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
-version = "0.2.28"
+version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102"
+checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e"
dependencies = [
+ "defmt",
"jiff-static",
"log",
"portable-atomic",
@@ -878,12 +1242,42 @@ dependencies = [
[[package]]
name = "jiff-static"
-version = "0.2.28"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys 0.4.1",
+ "log",
+ "simd_cesu8",
+ "thiserror 2.0.18",
+ "walkdir",
+ "windows-link",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
+ "rustc_version",
+ "simd_cesu8",
"syn",
]
@@ -927,9 +1321,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.102"
+version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
dependencies = [
"cfg-if",
"futures-util",
@@ -973,6 +1367,18 @@ dependencies = [
"libc",
]
+[[package]]
+name = "kurbo"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2"
+dependencies = [
+ "arrayvec",
+ "euclid",
+ "polycool",
+ "smallvec",
+]
+
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -1013,6 +1419,18 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+[[package]]
+name = "libredox"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
+dependencies = [
+ "bitflags 2.13.0",
+ "libc",
+ "plain",
+ "redox_syscall 0.9.0",
+]
+
[[package]]
name = "libz-sys"
version = "1.1.29"
@@ -1037,6 +1455,12 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -1060,9 +1484,9 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.32"
+version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "lru"
@@ -1072,9 +1496,9 @@ checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
[[package]]
name = "memchr"
-version = "2.8.2"
+version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "memmap2"
@@ -1137,9 +1561,9 @@ dependencies = [
[[package]]
name = "naga"
-version = "29.0.3"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0dd91265cc2454558f659b3b4b9640f0ddb8cc6521277f166b8a8c181c898079"
+checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df"
dependencies = [
"arrayvec",
"bit-set 0.9.1",
@@ -1161,6 +1585,27 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.13.0",
+ "jni-sys 0.3.1",
+ "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"
@@ -1184,6 +1629,12 @@ dependencies = [
"pin-utils",
]
+[[package]]
+name = "nohash-hasher"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
+
[[package]]
name = "notify"
version = "6.1.1"
@@ -1218,6 +1669,44 @@ dependencies = [
"libm",
]
+[[package]]
+name = "num_enum"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "objc-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+
+[[package]]
+name = "objc2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
[[package]]
name = "objc2"
version = "0.6.4"
@@ -1227,6 +1716,22 @@ dependencies = [
"objc2-encode",
]
+[[package]]
+name = "objc2-app-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2 0.5.1",
+ "libc",
+ "objc2 0.5.2",
+ "objc2-core-data 0.2.2",
+ "objc2-core-image 0.2.2",
+ "objc2-foundation 0.2.2",
+ "objc2-quartz-core 0.2.2",
+]
+
[[package]]
name = "objc2-app-kit"
version = "0.3.2"
@@ -1234,18 +1739,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags 2.13.0",
- "block2",
+ "block2 0.6.2",
"libc",
- "objc2",
- "objc2-cloud-kit",
- "objc2-core-data",
+ "objc2 0.6.4",
+ "objc2-cloud-kit 0.3.2",
+ "objc2-core-data 0.3.2",
"objc2-core-foundation",
"objc2-core-graphics",
- "objc2-core-image",
+ "objc2-core-image 0.3.2",
"objc2-core-text",
"objc2-core-video",
- "objc2-foundation",
- "objc2-quartz-core",
+ "objc2-foundation 0.3.2",
+ "objc2-quartz-core 0.3.2",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
]
[[package]]
@@ -1255,8 +1773,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
dependencies = [
"bitflags 2.13.0",
- "objc2",
- "objc2-foundation",
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-contacts"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
@@ -1266,8 +1807,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
dependencies = [
"bitflags 2.13.0",
- "objc2",
- "objc2-foundation",
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
]
[[package]]
@@ -1278,7 +1819,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.13.0",
"dispatch2",
- "objc2",
+ "objc2 0.6.4",
]
[[package]]
@@ -1289,19 +1830,43 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags 2.13.0",
"dispatch2",
- "objc2",
+ "objc2 0.6.4",
"objc2-core-foundation",
"objc2-io-surface",
]
+[[package]]
+name = "objc2-core-image"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal 0.2.2",
+]
+
[[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",
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-core-location"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-contacts",
+ "objc2-foundation 0.2.2",
]
[[package]]
@@ -1311,7 +1876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
dependencies = [
"bitflags 2.13.0",
- "objc2",
+ "objc2 0.6.4",
"objc2-core-foundation",
"objc2-core-graphics",
]
@@ -1323,7 +1888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
dependencies = [
"bitflags 2.13.0",
- "objc2",
+ "objc2 0.6.4",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-io-surface",
@@ -1337,60 +1902,177 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "objc2-foundation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2 0.5.1",
+ "dispatch",
+ "libc",
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2 0.6.2",
+ "libc",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-io-surface"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
+dependencies = [
+ "bitflags 2.13.0",
+ "libc",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-link-presentation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2 0.6.2",
+ "dispatch2",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "objc2-io-surface",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal 0.2.2",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2 0.6.2",
+ "libc",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-video",
+ "objc2-foundation 0.3.2",
+ "objc2-metal 0.3.2",
+]
+
+[[package]]
+name = "objc2-symbols"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
dependencies = [
- "bitflags 2.13.0",
- "block2",
- "libc",
- "objc2",
- "objc2-core-foundation",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
-name = "objc2-io-surface"
+name = "objc2-ui-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-cloud-kit 0.2.2",
+ "objc2-core-data 0.2.2",
+ "objc2-core-image 0.2.2",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
+ "objc2-link-presentation",
+ "objc2-quartz-core 0.2.2",
+ "objc2-symbols",
+ "objc2-uniform-type-identifiers",
+ "objc2-user-notifications",
+]
+
+[[package]]
+name = "objc2-ui-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
+checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
dependencies = [
"bitflags 2.13.0",
- "libc",
- "objc2",
+ "objc2 0.6.4",
"objc2-core-foundation",
- "objc2-foundation",
+ "objc2-foundation 0.3.2",
]
[[package]]
-name = "objc2-metal"
-version = "0.3.2"
+name = "objc2-uniform-type-identifiers"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
+checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [
- "bitflags 2.13.0",
- "block2",
- "dispatch2",
- "objc2",
- "objc2-core-foundation",
- "objc2-foundation",
- "objc2-io-surface",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
-name = "objc2-quartz-core"
-version = "0.3.2"
+name = "objc2-user-notifications"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
+checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
"bitflags 2.13.0",
- "block2",
- "libc",
- "objc2",
- "objc2-core-foundation",
- "objc2-core-graphics",
- "objc2-core-video",
- "objc2-foundation",
- "objc2-metal",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
]
[[package]]
@@ -1405,6 +2087,16 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+[[package]]
+name = "orbclient"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747"
+dependencies = [
+ "libc",
+ "libredox",
+]
+
[[package]]
name = "ordered-float"
version = "5.3.0"
@@ -1432,11 +2124,24 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
- "redox_syscall",
+ "redox_syscall 0.5.18",
"smallvec",
"windows-link",
]
+[[package]]
+name = "peniko"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6"
+dependencies = [
+ "bytemuck",
+ "color",
+ "kurbo",
+ "linebender_resource_handle",
+ "smallvec",
+]
+
[[package]]
name = "phf"
version = "0.11.3"
@@ -1455,6 +2160,26 @@ dependencies = [
"siphasher",
]
+[[package]]
+name = "pin-project"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "pin-project-lite"
version = "0.2.17"
@@ -1484,6 +2209,12 @@ version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
[[package]]
name = "plist"
version = "1.10.0"
@@ -1520,7 +2251,7 @@ dependencies = [
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
- "rustix",
+ "rustix 1.1.4",
"windows-sys 0.61.2",
]
@@ -1530,6 +2261,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2"
+[[package]]
+name = "polycool"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6"
+dependencies = [
+ "arrayvec",
+]
+
[[package]]
name = "portable-atomic"
version = "1.13.1"
@@ -1578,6 +2318,15 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -1606,9 +2355,9 @@ dependencies = [
[[package]]
name = "pxfm"
-version = "0.1.29"
+version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
+checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
[[package]]
name = "quick-xml"
@@ -1621,9 +2370,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.45"
+version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
@@ -1658,10 +2407,10 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135"
dependencies = [
- "objc2",
+ "objc2 0.6.4",
"objc2-core-foundation",
- "objc2-foundation",
- "objc2-quartz-core",
+ "objc2-foundation 0.3.2",
+ "objc2-quartz-core 0.3.2",
]
[[package]]
@@ -1685,6 +2434,15 @@ dependencies = [
"font-types",
]
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -1694,11 +2452,20 @@ dependencies = [
"bitflags 2.13.0",
]
+[[package]]
+name = "redox_syscall"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759"
+dependencies = [
+ "bitflags 2.13.0",
+]
+
[[package]]
name = "regex"
-version = "1.12.4"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
+checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2"
dependencies = [
"aho-corasick",
"memchr",
@@ -1708,9 +2475,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.14"
+version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db"
dependencies = [
"aho-corasick",
"memchr",
@@ -1743,9 +2510,31 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
-version = "2.1.2"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[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 = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags 2.13.0",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
[[package]]
name = "rustix"
@@ -1756,7 +2545,7 @@ dependencies = [
"bitflags 2.13.0",
"errno",
"libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
]
@@ -1768,14 +2557,14 @@ checksum = "1de16c7c59892b870a6336f185dc10943517f1327447096bbb7bb32cd85e2393"
dependencies = [
"errno",
"libc",
- "rustix",
+ "rustix 1.1.4",
]
[[package]]
name = "rustversion"
-version = "1.0.22"
+version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "same-file"
@@ -1798,6 +2587,12 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
[[package]]
name = "serde"
version = "1.0.228"
@@ -1932,6 +2727,22 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+[[package]]
+name = "simd_cesu8"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
[[package]]
name = "similar"
version = "2.7.0"
@@ -1985,12 +2796,31 @@ version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+[[package]]
+name = "smol_str"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "smol_str"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523"
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "spirv"
version = "0.4.0+sdk-1.4.341.0"
@@ -2124,9 +2954,9 @@ dependencies = [
[[package]]
name = "time"
-version = "0.3.49"
+version = "0.3.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469"
+checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
dependencies = [
"deranged",
"num-conv",
@@ -2144,9 +2974,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
-version = "0.2.29"
+version = "0.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d"
+checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
dependencies = [
"num-conv",
"time-core",
@@ -2163,9 +2993,9 @@ dependencies = [
[[package]]
name = "tinyvec"
-version = "1.11.0"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
dependencies = [
"tinyvec_macros",
]
@@ -2176,6 +3006,52 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+
[[package]]
name = "tree-sitter"
version = "0.25.10"
@@ -2206,6 +3082,16 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782"
+[[package]]
+name = "tree-sitter-md"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2efd398be546456c814598ee56c0f51769a77241511b4a58077815d120afa882"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
[[package]]
name = "ttf-parser"
version = "0.25.1"
@@ -2215,6 +3101,15 @@ dependencies = [
"core_maths",
]
+[[package]]
+name = "type-map"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
+dependencies = [
+ "rustc-hash 2.1.3",
+]
+
[[package]]
name = "unicase"
version = "2.9.0"
@@ -2227,6 +3122,12 @@ version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+[[package]]
+name = "unicode-general-category"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
+
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -2269,9 +3170,9 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b87595f1063af99a3bd106e3ab6d99c38ccf68d9ead7ef61f7f2388d07a357e"
dependencies = [
- "objc2",
- "objc2-foundation",
- "objc2-metal",
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
+ "objc2-metal 0.3.2",
"unity-native-plugin-sys",
]
@@ -2286,9 +3187,13 @@ name = "unterm"
version = "0.1.0"
dependencies = [
"alacritty_terminal",
+ "block2 0.6.2",
"bytemuck",
- "core-foundation",
+ "core-foundation 0.10.1",
"core-foundation-sys",
+ "egui",
+ "egui-wgpu",
+ "egui-winit",
"env_logger",
"git2",
"glyphon",
@@ -2297,29 +3202,32 @@ dependencies = [
"libc",
"log",
"notify",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "objc2-foundation 0.3.2",
"objc2-io-surface",
- "objc2-metal",
- "objc2-quartz-core",
+ "objc2-metal 0.3.2",
+ "objc2-quartz-core 0.3.2",
"pollster",
"portable-pty",
"pulldown-cmark",
"raw-window-handle",
"serde_json",
"similar",
+ "socket2",
"streaming-iterator",
"syntect",
"sys-locale",
"timeago",
"tree-sitter",
"tree-sitter-c-sharp",
+ "tree-sitter-md",
"unicode-script",
"unicode-width 0.1.14",
"unity-native-plugin",
"wgpu",
"windows",
+ "winit",
]
[[package]]
@@ -2328,12 +3236,46 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+[[package]]
+name = "uuid"
+version = "1.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a"
+
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+[[package]]
+name = "vello_common"
+version = "0.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d672facaa2d697285a786cd9d44d614cd2ce54cdc022504bf339f8fff3b750"
+dependencies = [
+ "bytemuck",
+ "fearless_simd",
+ "guillotiere",
+ "hashbrown 0.17.1",
+ "log",
+ "peniko",
+ "smallvec",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "vello_cpu"
+version = "0.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "588691169aed86b5c8fb487266afee01323234e6fd0a3f2aaec0eaa8e4007f23"
+dependencies = [
+ "bytemuck",
+ "glifo",
+ "hashbrown 0.17.1",
+ "vello_common",
+]
+
[[package]]
name = "version_check"
version = "0.9.5"
@@ -2372,9 +3314,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasm-bindgen"
-version = "0.2.125"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
dependencies = [
"cfg-if",
"once_cell",
@@ -2385,9 +3327,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.75"
+version = "0.4.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280"
+checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -2395,9 +3337,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.125"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -2405,9 +3347,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.125"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -2418,9 +3360,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.125"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
dependencies = [
"unicode-ident",
]
@@ -2439,9 +3381,19 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.102"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -2449,9 +3401,9 @@ dependencies = [
[[package]]
name = "wgpu"
-version = "29.0.3"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb3feacc458f7bee8bc1737149b42b6c731aa461039a4264a67bb6681646b250"
+checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07"
dependencies = [
"arrayvec",
"bitflags 2.13.0",
@@ -2479,9 +3431,9 @@ dependencies = [
[[package]]
name = "wgpu-core"
-version = "29.0.3"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02da3ad1b568337f25513b317870960ef87073ea0945502e44b864b67a8c77b7"
+checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7"
dependencies = [
"arrayvec",
"bit-set 0.9.1",
@@ -2504,6 +3456,7 @@ dependencies = [
"thiserror 2.0.18",
"wgpu-core-deps-apple",
"wgpu-core-deps-emscripten",
+ "wgpu-core-deps-wasm",
"wgpu-core-deps-windows-linux-android",
"wgpu-hal",
"wgpu-naga-bridge",
@@ -2512,43 +3465,52 @@ dependencies = [
[[package]]
name = "wgpu-core-deps-apple"
-version = "29.0.3"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62e51b5447e144b3dbba4feb01f80f4fa21696fa0cd99afb2c3df1affd6fdb28"
+checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-core-deps-emscripten"
-version = "29.0.3"
+version = "29.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01e09be551dc939498bdd5f6b2c66e55ab275dad25825267a08605a80fc9f0af"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-core-deps-wasm"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3487cd6293a963bc5c0c0396f6a2192043c50003c07f4efdccbad3d90ec9d819"
+checksum = "af1fb1798be2a912497d4c224f72d39bb0cb34af50e8bcc29865bc339c943059"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-core-deps-windows-linux-android"
-version = "29.0.3"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bfb01076d0aa08b0ba9bd741e178b5cc440f5abe99d9581323a4c8b5d1a1916"
+checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-hal"
-version = "29.0.3"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31f8e1a9e7a8512f276f7c62e018c7fa8d60954303fed2e5750114332049193f"
+checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d"
dependencies = [
"android_system_properties",
"arrayvec",
"ash",
"bit-set 0.9.1",
"bitflags 2.13.0",
- "block2",
+ "block2 0.6.2",
"bytemuck",
"cfg-if",
"cfg_aliases",
@@ -2564,11 +3526,11 @@ dependencies = [
"log",
"naga",
"ndk-sys",
- "objc2",
+ "objc2 0.6.4",
"objc2-core-foundation",
- "objc2-foundation",
- "objc2-metal",
- "objc2-quartz-core",
+ "objc2-foundation 0.3.2",
+ "objc2-metal 0.3.2",
+ "objc2-quartz-core 0.3.2",
"once_cell",
"ordered-float",
"parking_lot",
@@ -2593,9 +3555,9 @@ dependencies = [
[[package]]
name = "wgpu-naga-bridge"
-version = "29.0.3"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59c654c483f058800972c3645e95388a7eca31bf9fe1933bc20e036588a0be02"
+checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161"
dependencies = [
"naga",
"wgpu-types",
@@ -2603,9 +3565,9 @@ dependencies = [
[[package]]
name = "wgpu-types"
-version = "29.0.3"
+version = "29.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9bcc31518a0e9735aefebedb5f7a9ef3ed1c42549c9f4c882fa9060ceaac639"
+checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5"
dependencies = [
"bitflags 2.13.0",
"bytemuck",
@@ -2756,6 +3718,15 @@ dependencies = [
"windows-targets 0.48.5",
]
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -2904,6 +3875,55 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+[[package]]
+name = "winit"
+version = "0.30.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d"
+dependencies = [
+ "android-activity",
+ "atomic-waker",
+ "bitflags 2.13.0",
+ "block2 0.5.1",
+ "calloop",
+ "cfg_aliases",
+ "concurrent-queue",
+ "core-foundation 0.9.4",
+ "core-graphics",
+ "cursor-icon",
+ "dpi",
+ "js-sys",
+ "libc",
+ "ndk",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+ "objc2-ui-kit 0.2.2",
+ "orbclient",
+ "pin-project",
+ "raw-window-handle",
+ "redox_syscall 0.4.1",
+ "rustix 0.38.44",
+ "smol_str 0.2.2",
+ "tracing",
+ "unicode-segmentation",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "web-time",
+ "windows-sys 0.52.0",
+ "xkbcommon-dl",
+]
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "winreg"
version = "0.10.1"
@@ -2913,6 +3933,25 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "xkbcommon-dl"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
+dependencies = [
+ "bitflags 2.13.0",
+ "dlib",
+ "log",
+ "once_cell",
+ "xkeysym",
+]
+
+[[package]]
+name = "xkeysym"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
+
[[package]]
name = "xml-rs"
version = "0.8.28"
@@ -2942,18 +3981,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
[[package]]
name = "zerocopy"
-version = "0.8.52"
+version = "0.8.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
+checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.52"
+version = "0.8.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
+checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
dependencies = [
"proc-macro2",
"quote",
@@ -2962,6 +4001,6 @@ dependencies = [
[[package]]
name = "zmij"
-version = "1.0.21"
+version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/native/about.toml b/native/about.toml
index b07b871..16bcfaa 100644
--- a/native/about.toml
+++ b/native/about.toml
@@ -15,6 +15,13 @@ accepted = [
"Unlicense",
"0BSD",
"MPL-2.0",
+ # The debugger's egui window: clipboard-win/error-code (BSL-1.0) via
+ # egui-winit's clipboard, and egui's bundled fonts (OFL-1.1 +
+ # Ubuntu-font-1.0 via epaint_default_fonts). All permissive; the font
+ # licenses are satisfied by shipping their notices, which this generates.
+ "BSL-1.0",
+ "OFL-1.1",
+ "Ubuntu-font-1.0",
]
# The shipped targets: macOS aarch64/x86_64 and Windows x86_64. This prunes
diff --git a/native/build-macos.sh b/native/build-macos.sh
index 34c8b05..a4841b0 100755
--- a/native/build-macos.sh
+++ b/native/build-macos.sh
@@ -30,19 +30,36 @@ echo "==> building unterm ($PROFILE)"
for arch in "${ARCHS[@]}"; do
# Bash 3.2 (macOS) treats "${CARGO_FLAGS[@]}" as unbound under `set -u` when the
# array is empty (the debug profile), so expand it guardedly.
- cargo build -p unterm --locked ${CARGO_FLAGS[@]+"${CARGO_FLAGS[@]}"} --target "$arch"
+ cargo build -p unterm --locked --lib --bin unterm-debugger \
+ ${CARGO_FLAGS[@]+"${CARGO_FLAGS[@]}"} --target "$arch"
done
-DEST="../Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm.dylib"
-mkdir -p "$(dirname "$DEST")"
+PLUGIN_DIR="../Packages/dev.tnayuki.unterm/Editor/Plugins/macOS"
+LIB_DEST="$PLUGIN_DIR/unterm.dylib"
+DEBUGGER_DEST="$PLUGIN_DIR/unterm-debugger"
+mkdir -p "$PLUGIN_DIR"
LIBS=()
+DEBUGGERS=()
for arch in "${ARCHS[@]}"; do
LIBS+=("target/$arch/${TARGET_DIR}/libunterm.dylib")
+ DEBUGGERS+=("target/$arch/${TARGET_DIR}/unterm-debugger")
done
-echo "==> lipo -> $DEST"
-lipo -create "${LIBS[@]}" -output "$DEST"
+echo "==> lipo library -> $LIB_DEST"
+lipo -create "${LIBS[@]}" -output "$LIB_DEST"
-echo "==> done: $DEST"
-lipo -info "$DEST"
+echo "==> lipo debugger -> $DEBUGGER_DEST"
+lipo -create "${DEBUGGERS[@]}" -output "$DEBUGGER_DEST"
+chmod 0755 "$DEBUGGER_DEST"
+# Lipo invalidates per-architecture Mach-O signatures. Re-sign the universal
+# executable deterministically (ad-hoc, no timestamp) before publishing it.
+codesign --force --sign - --timestamp=none \
+ --identifier dev.tnayuki.unterm.debugger "$DEBUGGER_DEST"
+codesign --verify --strict --verbose=2 "$DEBUGGER_DEST"
+
+echo "==> done: $LIB_DEST"
+lipo -info "$LIB_DEST"
+echo "==> done: $DEBUGGER_DEST"
+lipo -info "$DEBUGGER_DEST"
+test -x "$DEBUGGER_DEST"
diff --git a/native/build-windows.ps1 b/native/build-windows.ps1
index bf186fd..a410493 100644
--- a/native/build-windows.ps1
+++ b/native/build-windows.ps1
@@ -39,17 +39,23 @@ if ($Configuration -eq 'release') {
rustup target add $Target | Out-Null
Write-Host "==> building unterm ($Configuration, $Target)"
-cargo build -p unterm --locked @cargoFlags --target $Target
+cargo build -p unterm --locked --lib --bin unterm-debugger @cargoFlags --target $Target
if ($LASTEXITCODE -ne 0) { throw "cargo build failed (exit $LASTEXITCODE)" }
-$dest = Join-Path $PSScriptRoot '..\Packages\dev.tnayuki.unterm\Editor\Plugins\Windows\x86_64\unterm.dll'
-$destDir = Split-Path -Parent $dest
-New-Item -ItemType Directory -Force -Path $destDir | Out-Null
+$pluginDir = Join-Path $PSScriptRoot '..\Packages\dev.tnayuki.unterm\Editor\Plugins\Windows\x86_64'
+$libDest = Join-Path $pluginDir 'unterm.dll'
+$debuggerDest = Join-Path $pluginDir 'unterm-debugger.exe'
+New-Item -ItemType Directory -Force -Path $pluginDir | Out-Null
$lib = Join-Path $PSScriptRoot "target\$Target\$targetDir\unterm.dll"
if (-not (Test-Path -LiteralPath $lib)) { throw "built dll not found: $lib" }
+$debugger = Join-Path $PSScriptRoot "target\$Target\$targetDir\unterm-debugger.exe"
+if (-not (Test-Path -LiteralPath $debugger)) { throw "built debugger not found: $debugger" }
-Write-Host "==> copy -> $dest"
-Copy-Item -LiteralPath $lib -Destination $dest -Force
+Write-Host "==> copy -> $libDest"
+Copy-Item -LiteralPath $lib -Destination $libDest -Force
+Write-Host "==> copy -> $debuggerDest"
+Copy-Item -LiteralPath $debugger -Destination $debuggerDest -Force
-Write-Host "==> done: $dest"
+Write-Host "==> done: $libDest"
+Write-Host "==> done: $debuggerDest"
diff --git a/native/deny.toml b/native/deny.toml
index e231214..9fa6235 100644
--- a/native/deny.toml
+++ b/native/deny.toml
@@ -23,6 +23,10 @@ allow = [
"ISC",
"MIT",
"MPL-2.0",
+ # egui's bundled debugger fonts. Redistribution requirements are satisfied
+ # by the generated Third Party Notices shipped with the Unity package.
+ "OFL-1.1",
+ "Ubuntu-font-1.0",
"Unicode-3.0",
"Unlicense",
"Zlib",
diff --git a/native/unterm/Cargo.toml b/native/unterm/Cargo.toml
index 09cc1f9..f4e0783 100644
--- a/native/unterm/Cargo.toml
+++ b/native/unterm/Cargo.toml
@@ -65,8 +65,25 @@ notify = { version = "6", default-features = false, features = ["macos_fsevent"]
timeago = { version = "0.6.1", default-features = false, features = ["isolang", "translations"] }
isolang = { version = "2.4.0", default-features = false }
+# Standalone debugger window (`unterm-debugger` bin / `debugger` module): its own
+# winit event loop + wgpu surface, so it stays live while the editor is frozen at a
+# breakpoint. Only entered by the bin; the Unity-loaded cdylib never calls into it.
+# The debugger ships only for macOS and Windows. Disable winit/egui-winit's
+# default Linux X11/Wayland backends so unused platform crates do not enter the
+# audited lockfile (and the shipped dependency surface stays target-accurate).
+winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
+egui-wgpu = "0.35.0"
+egui = "0.35"
+egui-winit = { version = "0.35", default-features = false }
+# Player discovery: bind the Unity multicast group with SO_REUSEPORT (shared with the
+# editor's own listener) — std's UdpSocket can't set the sockopt before bind.
+socket2 = { version = "0.5", features = ["all"] }
+tree-sitter-md = "0.5.3"
+
# Graceful child shutdown: SIGTERM the `claude` process before falling back to
# SIGKILL, so it can finalize its transcript (std's Child::kill is SIGKILL-only).
+# Also player discovery: getifaddrs enumerates interfaces so the multicast group is
+# joined on each (macOS won't receive from a secondary interface via INADDR_ANY alone).
[target.'cfg(unix)'.dependencies]
libc = "0.2"
@@ -89,9 +106,16 @@ objc2-io-surface = "0.3"
# we render to with wgpu (so the list can overflow the editor window).
objc2-app-kit = { version = "0.3", features = [
"NSResponder", "NSWindow", "NSPanel", "NSView", "NSScreen",
+ # Debugger: observe the editor coming to the foreground (NSWorkspace notice) to
+ # tuck the debugger window just behind it.
+ "NSWorkspace",
] }
objc2-quartz-core = { version = "0.3", features = ["CALayer", "CAMetalLayer"] }
objc2-foundation = { version = "0.3", features = ["NSGeometry"] }
+# Debugger foreground-follow: the NSWorkspace observer block + the winit window's
+# NSWindow handle (to orderWindow:relativeTo: it below the editor's window).
+block2 = "0.6"
+raw-window-handle = "0.6"
# IOSurface creation stays on the plain CoreFoundation FFI; the raw pointer is
# bridged into objc2's `&IOSurfaceRef` for the Metal texture call.
core-foundation = "0.10"
@@ -122,6 +146,9 @@ windows = { version = "0.62", features = [
# Probe whether a `claude` pid from the session registry is still alive.
"Win32_System_Threading",
"Win32_UI_WindowsAndMessaging",
+ # Debugger foreground-follow: SetWinEventHook(EVENT_SYSTEM_FOREGROUND) to detect the
+ # editor coming forward, then SetWindowPos to slide the window just behind it.
+ "Win32_UI_Accessibility",
] }
# Same crate the macOS side uses; the d3d features pull in the
# IUnityGraphicsD3D11/D3D12 interfaces for the device capture in unity.rs.
diff --git a/native/unterm/examples/discover_test.rs b/native/unterm/examples/discover_test.rs
new file mode 100644
index 0000000..87c754e
--- /dev/null
+++ b/native/unterm/examples/discover_test.rs
@@ -0,0 +1,35 @@
+//! Ad-hoc: verify player discovery receives + parses live announcements.
+//! cargo run -p unterm --example discover_test
+use std::time::{Duration, Instant};
+
+fn main() {
+ let sock = match unterm::sdb::discovery_socket() {
+ Ok(s) => s,
+ Err(e) => {
+ eprintln!("discovery_socket failed: {e}");
+ return;
+ }
+ };
+ eprintln!("bound + joined; listening 10s…");
+ let mut buf = [0u8; 2048];
+ let mut seen = std::collections::HashSet::new();
+ let end = Instant::now() + Duration::from_secs(10);
+ while Instant::now() < end {
+ match sock.recv_from(&mut buf) {
+ Ok((n, from)) => {
+ if let Some(p) = unterm::sdb::parse_player(&buf[..n]) {
+ if seen.insert(p.guid) {
+ eprintln!(
+ "PLAYER from {from}: guid={} ip={} port={} project={:?} label={:?}",
+ p.guid, p.ip, p.port, p.project, p.label
+ );
+ }
+ } else {
+ eprintln!("(non-debuggable or unparsable {n}B from {from})");
+ }
+ }
+ Err(_) => {}
+ }
+ }
+ eprintln!("done: {} unique player(s)", seen.len());
+}
diff --git a/native/unterm/examples/dump_debug.rs b/native/unterm/examples/dump_debug.rs
new file mode 100644
index 0000000..8c9800c
--- /dev/null
+++ b/native/unterm/examples/dump_debug.rs
@@ -0,0 +1,225 @@
+//! Headless SDB debug session against a running Unity editor.
+//!
+//! Attaches to the editor's Mono agent, arms a source breakpoint, and — once you
+//! press Play with the target script on a GameObject — resolves it via sequence
+//! points, stops on the hit, and dumps the call stack + locals.
+//!
+//! cargo run -p unterm --example dump_debug # Rotator.cs:37
+//! cargo run -p unterm --example dump_debug -- Rotator.cs 43
+//!
+//! The project root is discovered from the cwd (walks up to Library/EditorInstance.json).
+
+use std::time::{Duration, Instant};
+
+use unterm::sdb::{self, kind_name, value::Value, wire, DebugInfo};
+
+fn main() {
+ env_logger::try_init().ok();
+
+ let mut args = std::env::args().skip(1);
+ let file = args.next().unwrap_or_else(|| "Rotator.cs".to_string());
+ let line: i32 = args.next().and_then(|s| s.parse().ok()).unwrap_or(37);
+
+ let root = std::env::current_dir().unwrap();
+ let mut conn = match sdb::connect_editor(&root) {
+ Ok(c) => c,
+ Err(e) => {
+ eprintln!("connect failed: {e} (is the Unity editor open under {}?)", root.display());
+ std::process::exit(1);
+ }
+ };
+ let v = conn.version().clone();
+ println!("attached: {} (protocol {}.{})", v.vm, v.major, v.minor);
+ while conn.take_queued().is_some() {} // drain VM_START
+
+ validate_debug_info_framing(&mut conn);
+
+ // Arm: subscribe to TYPE_LOAD for the target file. Types already loaded resolve
+ // immediately; everything else resolves when the play-mode domain loads them.
+ let watch = conn.watch_source_files(&[file.clone()]).expect("watch source");
+ println!("watching TYPE_LOAD for {file} (request {watch})");
+ let mut armed = false;
+ if let Ok(types) = conn.types_for_source_file(&file, true) {
+ if !types.is_empty() {
+ println!("{} type(s) already loaded for {file}", types.len());
+ armed |= try_arm(&mut conn, &types, &file, line);
+ }
+ }
+
+ println!("\n>>> Press Play in Unity (attach `{file}` to a GameObject). Waiting for the breakpoint...\n");
+ conn.set_read_timeout(Some(Duration::from_millis(500))).ok();
+ let deadline = Instant::now() + Duration::from_secs(300);
+
+ while Instant::now() < deadline {
+ let composite = match conn.next_event() {
+ Ok(c) => c,
+ Err(wire::SdbError::Io(ref e))
+ if matches!(
+ e.kind(),
+ std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
+ ) =>
+ {
+ continue;
+ }
+ Err(wire::SdbError::Eof) => {
+ println!("editor disconnected");
+ return;
+ }
+ Err(e) => {
+ println!("event error: {e}");
+ continue;
+ }
+ };
+
+ for ev in &composite.events {
+ match ev.kind {
+ wire::kind::TYPE_LOAD => {
+ if !armed {
+ armed = try_arm(&mut conn, &[ev.type_id], &file, line);
+ }
+ }
+ wire::kind::BREAKPOINT => {
+ println!("\n=== BREAKPOINT hit on thread {} ===", ev.thread);
+ dump_stop(&mut conn, ev.thread);
+ println!("\nresuming...");
+ conn.resume().ok();
+ return;
+ }
+ other => {
+ println!("event: {} ({})", kind_name(other), other);
+ }
+ }
+ }
+ }
+ println!("timed out waiting for the breakpoint");
+}
+
+/// Get type methods, read their sequence points, resolve the line, and set the BP.
+fn try_arm(conn: &mut sdb::Connection, types: &[u32], file: &str, line: i32) -> bool {
+ let mut candidates = Vec::new();
+ for &t in types {
+ let methods = match conn.type_methods(t) {
+ Ok(m) => m,
+ Err(_) => continue,
+ };
+ for m in methods {
+ if let Ok(info) = conn.debug_info(m) {
+ candidates.push((m, info));
+ }
+ }
+ }
+ match sdb::resolve_line(&candidates, file, line) {
+ Some((method, il)) => {
+ let name = conn.method_name(method).unwrap_or_default();
+ match conn.set_breakpoint(method, il) {
+ Ok(req) => {
+ println!("armed breakpoint at {file}:{line} -> {name}+0x{il:x} (request {req})");
+ true
+ }
+ Err(e) => {
+ println!("set_breakpoint failed: {e}");
+ false
+ }
+ }
+ }
+ None => {
+ println!("could not resolve {file}:{line} in {} method(s) yet", candidates.len());
+ false
+ }
+ }
+}
+
+/// Dump the call stack and the top frame's `this` + in-scope locals.
+fn dump_stop(conn: &mut sdb::Connection, thread: u32) {
+ let frames = match conn.frames(thread) {
+ Ok(f) => f,
+ Err(e) => {
+ println!("frames error: {e}");
+ return;
+ }
+ };
+ println!("call stack ({} frames):", frames.len());
+ for (i, f) in frames.iter().enumerate().take(12) {
+ let name = conn.method_name(f.method).unwrap_or_default();
+ let loc = conn
+ .debug_info(f.method)
+ .ok()
+ .and_then(|info| il_to_source(&info, f.il_offset))
+ .unwrap_or_default();
+ println!(" #{i:<2} {name} (il 0x{:x}) {loc}", f.il_offset);
+ }
+
+ let Some(top) = frames.first() else { return };
+
+ match conn.frame_this(thread, top.id) {
+ Ok(Value::Object { id, .. }) if id != 0 => println!("\nthis = obj#{id}"),
+ Ok(v) => println!("\nthis = {}", v.summary()),
+ Err(e) => println!("\nthis: "),
+ }
+
+ let locals = match conn.locals_info(top.method) {
+ Ok(l) => l,
+ Err(e) => {
+ println!("locals_info error: {e}");
+ return;
+ }
+ };
+ // Only request locals in scope at the current IL offset (others may be invalid).
+ let in_scope: Vec<(usize, &sdb::Local)> = locals
+ .iter()
+ .enumerate()
+ .filter(|(_, l)| top.il_offset >= l.live_start && top.il_offset < l.live_end)
+ .collect();
+ println!("locals ({}/{} in scope at il 0x{:x}):", in_scope.len(), locals.len(), top.il_offset);
+ if in_scope.is_empty() {
+ return;
+ }
+ let positions: Vec = in_scope.iter().map(|(i, _)| *i as i32).collect();
+ match conn.frame_values(thread, top.id, &positions) {
+ Ok(values) => {
+ for ((_, l), v) in in_scope.iter().zip(values) {
+ println!(" {} = {}", l.name, v.summary());
+ }
+ }
+ Err(e) => println!(" "),
+ }
+}
+
+/// Map an IL offset back to a source line: the nearest non-hidden seq point at or
+/// before it.
+fn il_to_source(info: &DebugInfo, il: i32) -> Option {
+ let sp = info
+ .seq_points
+ .iter()
+ .filter(|s| !s.is_hidden() && s.il_offset <= il)
+ .max_by_key(|s| s.il_offset)?;
+ let src = info.sources.get(sp.source_idx as usize).cloned().unwrap_or_default();
+ Some(format!("{}:{}", src.rsplit(['/', '\\']).next().unwrap_or(&src), sp.line))
+}
+
+/// Sanity-check the GET_DEBUG_INFO decoder against a loaded method (mscorlib is
+/// always present) so a layout error surfaces before we depend on it for breakpoints.
+fn validate_debug_info_framing(conn: &mut sdb::Connection) {
+ let probe = (|| -> Option<()> {
+ let dom = conn.root_domain().ok()?;
+ let asm = *conn.domain_assemblies(dom).ok()?.first()?;
+ // ASSEMBLY.GET_TYPE for a ubiquitous type.
+ let type_id = conn.assembly_type(asm, "System.Object").ok()?;
+ if type_id == 0 {
+ return None;
+ }
+ let methods = conn.type_methods(type_id).ok()?;
+ let m = *methods.first()?;
+ let info = conn.debug_info(m).ok()?;
+ println!(
+ "debug_info framing ok: mscorlib method maxil={} sources={} seq_points={}",
+ info.max_il,
+ info.sources.len(),
+ info.seq_points.len()
+ );
+ Some(())
+ })();
+ if probe.is_none() {
+ println!("debug_info framing probe skipped (no symbols on mscorlib, expected)");
+ }
+}
diff --git a/native/unterm/src/bin/unterm-debugger.rs b/native/unterm/src/bin/unterm-debugger.rs
new file mode 100644
index 0000000..7ca0c4d
--- /dev/null
+++ b/native/unterm/src/bin/unterm-debugger.rs
@@ -0,0 +1,24 @@
+//! The standalone Unterm debugger window.
+//!
+//! Thin launcher: all logic lives in `unterm::debugger`. Usage:
+//! unterm-debugger [file.cs:line ...]
+//! Each argument is a breakpoint as `SourceFile.cs:line`. The working directory must
+//! be the Unity project root (so `Library/EditorInstance.json` is found). Defaults to
+//! Rotator.cs:45 when no breakpoints are given.
+
+fn main() {
+ let mut bps: Vec<(String, i32)> = Vec::new();
+ for arg in std::env::args().skip(1) {
+ if let Some((file, line)) = arg.rsplit_once(':') {
+ if let Ok(n) = line.parse::() {
+ bps.push((file.to_string(), n));
+ continue;
+ }
+ }
+ eprintln!("ignoring malformed breakpoint arg: {arg:?} (expected file.cs:line)");
+ }
+ if bps.is_empty() {
+ bps.push(("Rotator.cs".to_string(), 45));
+ }
+ unterm::debugger::run(bps);
+}
diff --git a/native/unterm/src/debugger.rs b/native/unterm/src/debugger.rs
new file mode 100644
index 0000000..6a7d97d
--- /dev/null
+++ b/native/unterm/src/debugger.rs
@@ -0,0 +1,2751 @@
+//! Standalone debugger window, built with egui.
+//!
+//! Runs its own winit event loop + wgpu surface in a separate process, so it stays
+//! alive and interactive while the Unity editor is frozen at a breakpoint. The SDB
+//! session runs on a background thread that owns the socket (never blocked by the
+//! managed suspend) and communicates with the egui UI via a shared `UiState` plus a
+//! command channel.
+//!
+//! The UI is themed to match Unity's editor (Inter/RobotoMono fonts, dark palette,
+//! 18px rows). Launched by the `unterm-debugger` bin.
+
+use std::collections::BTreeMap;
+use std::sync::mpsc::{Receiver, Sender};
+use std::sync::{Arc, Mutex};
+use std::time::Duration;
+
+use egui::{Color32, FontId, RichText};
+use winit::application::ApplicationHandler;
+use winit::event::WindowEvent;
+use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy};
+use winit::window::{Window, WindowId};
+
+use crate::editorview::EditorView;
+use crate::gpu;
+use crate::sdb::{self, value, wire};
+
+// ---------------------------------------------------------------------------
+// Messages + shared state
+// ---------------------------------------------------------------------------
+
+/// A command from the UI thread to the SDB session thread.
+#[derive(Debug, Clone)]
+enum Cmd {
+ Resume,
+ StepOver,
+ StepInto,
+ StepOut,
+ /// Toggle a breakpoint at a 1-based source line in the currently-viewed file.
+ ToggleBp(i32),
+ /// The UI switched the displayed file (so the session reports its breakpoints).
+ View(String),
+ /// Break into a running program (VM suspend) and inspect the current state.
+ Pause,
+ /// Inspect a different (managed) thread's stack and variables.
+ SelectThread(u32),
+ /// Inspect a different call-stack frame (0 = innermost).
+ SelectFrame(usize),
+ /// Lazily fetch the children (fields or elements) of an expandable value.
+ ExpandVar { id: u32, array: bool },
+ /// Attach to a different target (the editor, or a discovered player).
+ SelectTarget(Target),
+}
+
+/// An attachable SDB endpoint: this project's editor, or a discovered Unity player.
+#[derive(Clone, Debug)]
+struct Target {
+ ip: String,
+ port: u16,
+ label: String,
+}
+
+/// Wakeups posted from the SDB thread to the winit loop.
+#[derive(Debug, Clone, Copy)]
+enum Wake {
+ Redraw,
+ /// Surface the window (un-minimize + focus), e.g. when a breakpoint is hit.
+ Focus,
+ /// Tuck the window just behind the editor's window (the editor came forward), so it
+ /// rides above other apps without covering the editor or stealing focus.
+ Raise,
+ /// Close the window (the editor we were attached to went away).
+ Quit,
+}
+
+/// Input the source pane collected this frame, applied to the embedded editor after
+/// the egui pass (the editor lives on `App`, out of reach inside the UI closure).
+struct SourcePaneOut {
+ /// The pane's rect in egui points (for sizing the editor to it).
+ rect: egui::Rect,
+ acts: Vec,
+ /// Pointer position (egui points) while hovering the source, for the value tooltip.
+ hover: Option,
+}
+
+impl Default for SourcePaneOut {
+ fn default() -> Self {
+ Self { rect: egui::Rect::NOTHING, acts: Vec::new(), hover: None }
+ }
+}
+
+/// A forwarded editor input; coordinates are physical px relative to the pane.
+enum EditorAct {
+ Mouse { x: f32, y: f32, kind: u8 },
+ Scroll { dx: f32, dy: f32 },
+ Key(String, bool, bool, bool),
+}
+
+/// Which pane has keyboard focus (drives ↑/↓ navigation). Focus follows the last click.
+#[derive(Clone, Copy, PartialEq, Default)]
+enum Focus {
+ #[default]
+ None,
+ Tree,
+ Source,
+}
+
+/// Shared, rendered debugger state (snapshotted by the UI each frame).
+#[derive(Default, Clone)]
+struct UiState {
+ /// An error / special message (shown only when not normally attached).
+ status: String,
+ /// Whether the SDB connection is established.
+ attached: bool,
+ /// Runtime/protocol version detail (shown on hover).
+ version: String,
+ stopped: bool,
+ /// Absolute path of the source file the top frame is in.
+ cur_file: String,
+ /// 1-based current line, or 0 if unknown.
+ cur_line: i32,
+ /// Managed threads at the current stop (only populated while stopped).
+ threads: Vec,
+ /// The thread whose stack/variables are shown (SDB thread id).
+ cur_thread: u32,
+ stack: Vec,
+ /// Which call-stack frame's variables are shown (0 = innermost).
+ cur_frame: usize,
+ locals: Vec,
+ members: Vec,
+ this_label: String,
+ /// Lazily-fetched children of expandable values, keyed by object id.
+ children: std::collections::HashMap>,
+ /// Bumps on every new stop / frame switch so the UI can drop stale expand state.
+ gen: u64,
+ /// 1-based breakpoint lines in the currently-displayed file (for gutter dots).
+ bp_lines: Vec,
+ /// All breakpoints across files (display file, 1-based line) for the list panel.
+ all_bps: Vec<(String, i32)>,
+ log: Vec,
+ /// Debuggable Unity players found via multicast discovery (for the target picker).
+ players: Vec,
+ /// Label of the target we're currently attached to (for the target picker).
+ cur_target: String,
+}
+
+/// A variable row in the inspector. `expand` is set when the value is a reference
+/// object or array whose fields/elements can be drilled into on demand.
+#[derive(Clone)]
+struct Var {
+ name: String,
+ value: String,
+ expand: Option,
+}
+
+#[derive(Clone, Copy)]
+struct Expand {
+ id: u32,
+ array: bool,
+}
+
+/// A managed thread shown in the Threads strip (name + where it is stopped).
+#[derive(Clone)]
+struct ThreadInfo {
+ id: u32,
+ name: String,
+ location: String,
+}
+
+impl UiState {
+ fn push_log(&mut self, msg: impl Into) {
+ self.log.push(msg.into());
+ let n = self.log.len();
+ if n > 8 {
+ self.log.drain(0..n - 8);
+ }
+ }
+}
+
+/// Entry point. `breakpoints` is the (file, line) set to arm (file may be a basename).
+pub fn run(breakpoints: Vec<(String, i32)>) {
+ env_logger::try_init().ok();
+
+ // Advertise our PID so the editor launcher can tell this project's debugger is
+ // already running (project-scoped single instance) and signal it to come forward.
+ write_pid_file();
+
+ let mut builder = EventLoop::::with_user_event();
+ #[cfg(target_os = "macos")]
+ {
+ use winit::platform::macos::{ActivationPolicy, EventLoopBuilderExtMacOS};
+ // Accessory: no Dock icon / menu-bar ownership (it's a helper for the editor);
+ // its window can still become key. It follows the editor forward via the
+ // activation observer below, so it needn't be in the Dock / Cmd+Tab.
+ builder.with_activation_policy(ActivationPolicy::Accessory);
+ builder.with_activate_ignoring_other_apps(true);
+ }
+ let event_loop = builder.build().expect("create event loop");
+ let proxy = event_loop.create_proxy();
+
+ // Follow the editor to the foreground: when it activates, slide this window up to
+ // just behind it (above other apps, without covering the editor or taking focus).
+ install_activation_observer(proxy.clone());
+
+ let state = Arc::new(Mutex::new(UiState::default()));
+ let (tx, rx) = std::sync::mpsc::channel::();
+ {
+ let state = state.clone();
+ let proxy = proxy.clone();
+ std::thread::spawn(move || session_thread(breakpoints, state, rx, proxy));
+ }
+ // Discover debuggable Unity players on the network (for the target picker).
+ spawn_discovery(state.clone(), proxy.clone());
+
+ let files = scan_cs_files();
+ let tree = build_tree(&files);
+ let mut app = App {
+ state,
+ tx,
+ gfx: None,
+ view_file: String::new(),
+ editor: None,
+ editor_tex: None,
+ editor_file: String::new(),
+ editor_dims: (0, 0),
+ files,
+ tree,
+ scroll_to_cur: false,
+ goto: None,
+ focus: Focus::None,
+ expanded: ["Assets".to_string(), "Packages".to_string()]
+ .into_iter()
+ .collect(),
+ tree_sel: 0,
+ var_open: std::collections::HashSet::new(),
+ last_gen: 0,
+ hover_tip: None,
+ };
+ event_loop.run_app(&mut app).expect("run app");
+}
+
+// ---------------------------------------------------------------------------
+// egui + winit + wgpu integration
+// ---------------------------------------------------------------------------
+
+struct Gfx {
+ window: Arc,
+ surface: wgpu::Surface<'static>,
+ config: wgpu::SurfaceConfiguration,
+ egui_ctx: egui::Context,
+ egui_state: egui_winit::State,
+ egui_renderer: egui_wgpu::Renderer,
+}
+
+struct App {
+ state: Arc>,
+ tx: Sender,
+ gfx: Option,
+ // Which file the source pane shows (may differ from the stopped file).
+ view_file: String,
+ // Embedded read-only code editor (shared with the Unity code editor) rendered
+ // into a texture and shown in the source pane; None until the window exists.
+ editor: Option,
+ /// egui handle for the editor's texture, and the file/size it was last set to.
+ editor_tex: Option,
+ editor_file: String,
+ editor_dims: (u32, u32),
+ // Project .cs files (absolute path, display name) + the prebuilt folder tree.
+ files: Vec<(String, String)>,
+ tree: Tree,
+ scroll_to_cur: bool,
+ /// A line to scroll the source to next frame (e.g. from a breakpoint-list click).
+ goto: Option,
+ /// Which pane has keyboard focus (for ↑/↓ navigation).
+ focus: Focus,
+ /// Expanded folder keys in the file tree, and the selected (cursor) row index.
+ expanded: std::collections::HashSet,
+ tree_sel: usize,
+ /// Object ids the user has expanded in the variables inspector.
+ var_open: std::collections::HashSet,
+ /// Last-seen inspector generation; when it changes, `var_open` is dropped.
+ last_gen: u64,
+ /// Hover-watch tooltip (screen pos + text), computed one frame behind the pointer.
+ hover_tip: Option<(egui::Pos2, String)>,
+}
+
+impl ApplicationHandler for App {
+ fn resumed(&mut self, el: &ActiveEventLoop) {
+ if self.gfx.is_some() {
+ return;
+ }
+ #[allow(unused_mut)]
+ let mut attrs = Window::default_attributes()
+ .with_title("Unterm Debugger")
+ .with_inner_size(winit::dpi::LogicalSize::new(1100.0, 680.0))
+ // Keep the tree / source / inspector columns and the three stacked
+ // inspector panes usable — below this they'd collapse into each other.
+ .with_min_inner_size(winit::dpi::LogicalSize::new(720.0, 420.0));
+ // Windows: a tool window (no taskbar button) — the counterpart to the macOS
+ // Accessory policy. It follows the editor forward via the activation hook.
+ #[cfg(target_os = "windows")]
+ {
+ use winit::platform::windows::WindowAttributesExtWindows;
+ attrs = attrs.with_skip_taskbar(true);
+ }
+ let window = Arc::new(el.create_window(attrs).expect("create window"));
+ window.focus_window();
+
+ let g = gpu::gpu();
+ let size = window.inner_size();
+ let surface = g.instance.create_surface(window.clone()).expect("surface");
+ let caps = surface.get_capabilities(&g.adapter);
+ // egui expects a linear (non-sRGB) framebuffer; prefer one to get correct colors.
+ let format = caps
+ .formats
+ .iter()
+ .copied()
+ .find(|f| !f.is_srgb())
+ .unwrap_or(caps.formats[0]);
+ let config = wgpu::SurfaceConfiguration {
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+ format,
+ width: size.width.max(1),
+ height: size.height.max(1),
+ present_mode: wgpu::PresentMode::Fifo,
+ alpha_mode: caps.alpha_modes.first().copied().unwrap_or(wgpu::CompositeAlphaMode::Auto),
+ view_formats: vec![],
+ desired_maximum_frame_latency: 2,
+ };
+ surface.configure(&g.device, &config);
+
+ let egui_ctx = egui::Context::default();
+ setup_theme(&egui_ctx);
+ let egui_state = egui_winit::State::new(
+ egui_ctx.clone(),
+ egui::ViewportId::ROOT,
+ window.as_ref(),
+ Some(window.scale_factor() as f32),
+ None,
+ None,
+ );
+ let egui_renderer =
+ egui_wgpu::Renderer::new(&g.device, format, egui_wgpu::RendererOptions::default());
+
+ let gfx = Gfx {
+ window,
+ surface,
+ config,
+ egui_ctx,
+ egui_state,
+ egui_renderer,
+ };
+ gfx.window.request_redraw(); // paint the first frame
+ self.gfx = Some(gfx);
+ }
+
+ fn user_event(&mut self, el: &ActiveEventLoop, ev: Wake) {
+ if matches!(ev, Wake::Quit) {
+ el.exit();
+ return;
+ }
+ if let Some(g) = &self.gfx {
+ match ev {
+ Wake::Focus => {
+ // Bring a hidden/behind/minimized window forward on a breakpoint hit.
+ g.window.set_visible(true);
+ g.window.set_minimized(false);
+ g.window.focus_window();
+ }
+ Wake::Raise => order_behind_editor(&g.window),
+ _ => {}
+ }
+ g.window.request_redraw();
+ }
+ }
+
+ fn window_event(&mut self, el: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
+ let Some(gfx) = self.gfx.as_mut() else {
+ return;
+ };
+ let resp = gfx.egui_state.on_window_event(&gfx.window, &event);
+ if resp.repaint {
+ gfx.window.request_redraw();
+ }
+ match event {
+ WindowEvent::CloseRequested => el.exit(),
+ WindowEvent::Resized(size) => {
+ gfx.config.width = size.width.max(1);
+ gfx.config.height = size.height.max(1);
+ gfx.surface.configure(&gpu::gpu().device, &gfx.config);
+ gfx.window.request_redraw();
+ }
+ WindowEvent::RedrawRequested => self.paint(),
+ _ => {}
+ }
+ }
+}
+
+impl App {
+ fn paint(&mut self) {
+ let snap = self.state.lock().map(|s| s.clone()).unwrap_or_default();
+
+ // `gen` bumps on every stop, call-stack frame select, and thread select — the
+ // exact moments the source should jump. Follow the (resolved) current location
+ // and drop stale object-expansion state. Keying on `gen` (not the line number)
+ // means selecting a frame in another file at the same line still jumps, and a
+ // stop at the same line after browsing away still returns to it.
+ if snap.gen != self.last_gen {
+ self.last_gen = snap.gen;
+ self.var_open.clear();
+ if snap.stopped {
+ self.view_file = self.resolve_local(&snap.cur_file);
+ self.scroll_to_cur = true;
+ }
+ }
+ if self.view_file.is_empty() {
+ self.view_file = self.resolve_local(&snap.cur_file);
+ }
+
+ // What source line to scroll the editor to this frame (a fresh stop, or a
+ // breakpoint-list jump).
+ let scroll_line = if self.scroll_to_cur && snap.stopped {
+ self.scroll_to_cur = false;
+ Some(snap.cur_line)
+ } else {
+ self.goto.take()
+ };
+
+ let g = gpu::gpu();
+ let mut cmds: Vec = Vec::new();
+ let mut new_view: Option = None;
+ let mut goto: Option = None;
+ let mut focus = self.focus;
+ let mut src_out = SourcePaneOut::default();
+ // Taken out (not borrowed) so build_ui can mutate them without conflicting
+ // with ui_ctx's immutable borrows of other App fields.
+ let mut expanded = std::mem::take(&mut self.expanded);
+ let mut tree_sel = self.tree_sel;
+ let mut var_open = std::mem::take(&mut self.var_open);
+
+ let gfx = self.gfx.as_mut().unwrap();
+ let raw = gfx.egui_state.take_egui_input(&gfx.window);
+ let ctx = gfx.egui_ctx.clone();
+ let ui_ctx = UiCtx {
+ s: &snap,
+ tree: &self.tree,
+ files: &self.files,
+ editor_tex: self.editor_tex,
+ // Shown a frame behind the pointer (it needs the editor, updated below).
+ hover_tip: self.hover_tip.clone(),
+ };
+ let full = ctx.run_ui(raw, |ui| {
+ build_ui(
+ ui,
+ &ui_ctx,
+ &mut cmds,
+ &mut new_view,
+ &mut goto,
+ &mut focus,
+ &mut expanded,
+ &mut tree_sel,
+ &mut var_open,
+ &mut src_out,
+ );
+ });
+ drop(ui_ctx);
+ self.focus = focus;
+ self.expanded = expanded;
+ self.tree_sel = tree_sel;
+ self.var_open = var_open;
+
+ for cmd in cmds {
+ self.tx.send(cmd).ok();
+ }
+ if let Some(v) = new_view {
+ if v != self.view_file {
+ self.view_file = v.clone();
+ self.scroll_to_cur = false;
+ self.tx.send(Cmd::View(v)).ok();
+ }
+ // A jump from the breakpoint list also scrolls the source next frame.
+ self.goto = goto;
+ }
+
+ // Drive the embedded editor (size, content, input, breakpoints, render).
+ let needs_reg = self.sync_editor(&snap, &src_out, full.pixels_per_point, scroll_line);
+ // Recompute the hover-watch tooltip for next frame (needs the live editor).
+ self.hover_tip = self.compute_hover(&snap, &src_out, full.pixels_per_point);
+
+ let gfx = self.gfx.as_mut().unwrap();
+ // (Re)bind the editor's texture so the source pane's image samples it.
+ if let Some(ed) = &self.editor {
+ let view = ed.texture_view();
+ match self.editor_tex {
+ Some(_) if !needs_reg => {}
+ Some(id) => gfx.egui_renderer.update_egui_texture_from_wgpu_texture(
+ &g.device,
+ view,
+ wgpu::FilterMode::Nearest,
+ id,
+ ),
+ None => {
+ self.editor_tex = Some(gfx.egui_renderer.register_native_texture(
+ &g.device,
+ view,
+ wgpu::FilterMode::Nearest,
+ ))
+ }
+ }
+ }
+ gfx.egui_state.handle_platform_output(&gfx.window, full.platform_output);
+ let tris = gfx.egui_ctx.tessellate(full.shapes, full.pixels_per_point);
+ let screen = egui_wgpu::ScreenDescriptor {
+ size_in_pixels: [gfx.config.width, gfx.config.height],
+ pixels_per_point: full.pixels_per_point,
+ };
+ for (id, delta) in &full.textures_delta.set {
+ gfx.egui_renderer.update_texture(&g.device, &g.queue, *id, delta);
+ }
+ let mut encoder = g
+ .device
+ .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("egui") });
+ let user_cmds =
+ gfx.egui_renderer.update_buffers(&g.device, &g.queue, &mut encoder, &tris, &screen);
+
+ let frame = match gfx.surface.get_current_texture() {
+ wgpu::CurrentSurfaceTexture::Success(t) | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
+ _ => {
+ gfx.surface.configure(&g.device, &gfx.config);
+ return;
+ }
+ };
+ let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
+ {
+ let mut pass = encoder
+ .begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("egui-pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &view,
+ depth_slice: None,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0 }),
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ multiview_mask: None,
+ })
+ .forget_lifetime();
+ gfx.egui_renderer.render(&mut pass, &tris, &screen);
+ }
+ g.queue.submit(user_cmds.into_iter().chain([encoder.finish()]));
+ frame.present();
+ for id in &full.textures_delta.free {
+ gfx.egui_renderer.free_texture(id);
+ }
+ // Keep animating while egui wants it (panel drag, hover, etc.).
+ if gfx.egui_ctx.has_requested_repaint() {
+ gfx.window.request_redraw();
+ }
+ }
+
+ /// Map a source path from the target's debug info to a local project file. Debug
+ /// info records the path as compiled — the local editor's absolute path (which
+ /// exists as-is), or a remote build machine's path (which does not). We match by
+ /// basename, disambiguating same-named files by the longest common path suffix, so
+ /// the developer's local checkout of the same project resolves regardless of where
+ /// it ran. Falls back to the original path when nothing matches.
+ fn resolve_local(&self, dbg_path: &str) -> String {
+ if dbg_path.is_empty() {
+ return String::new();
+ }
+ // Fast path: the recorded path exists here (local editor debugging).
+ if std::path::Path::new(dbg_path).is_file() {
+ return dbg_path.to_string();
+ }
+ let base = basename(dbg_path);
+ let want: Vec<&str> = dbg_path.rsplit(['/', '\\']).collect(); // trailing-first
+ let mut best: Option<(usize, &str)> = None;
+ for (abs, name) in &self.files {
+ if name != base {
+ continue;
+ }
+ let have = abs.rsplit(['/', '\\']);
+ let score = want.iter().zip(have).take_while(|(a, b)| *a == b).count();
+ if best.map(|(s, _)| score > s).unwrap_or(true) {
+ best = Some((score, abs));
+ }
+ }
+ best.map(|(_, abs)| abs.to_string())
+ .unwrap_or_else(|| dbg_path.to_string())
+ }
+
+ /// Hover-watch: resolve the identifier under the pointer to its current value
+ /// (in-scope locals / `this` members). Returns the screen pos + `name = value`.
+ fn compute_hover(
+ &mut self,
+ snap: &UiState,
+ src_out: &SourcePaneOut,
+ ppp: f32,
+ ) -> Option<(egui::Pos2, String)> {
+ if !snap.stopped {
+ return None;
+ }
+ let screen = src_out.hover?;
+ let ed = self.editor.as_mut()?;
+ let x = (screen.x - src_out.rect.min.x) * ppp;
+ let y = (screen.y - src_out.rect.min.y) * ppp;
+ let (line, col) = ed.pos_at_pixel(x, y)?;
+ let word = word_at(&line, col)?;
+ let val = lookup_var(snap, &word)?;
+ Some((screen, format!("{word} = {val}")))
+ }
+
+ /// Create/size the embedded editor, load the viewed file, apply forwarded input,
+ /// push breakpoints + the current execution line, and render it. Returns whether
+ /// its texture needs (re)registering with egui (first use or after a resize).
+ fn sync_editor(
+ &mut self,
+ snap: &UiState,
+ src_out: &SourcePaneOut,
+ ppp: f32,
+ scroll_line: Option,
+ ) -> bool {
+ let rect = src_out.rect;
+ if !rect.is_finite() || rect.width() < 1.0 || rect.height() < 1.0 {
+ return false;
+ }
+ let w = (rect.width() * ppp).round().max(1.0) as u32;
+ let h = (rect.height() * ppp).round().max(1.0) as u32;
+
+ let mut needs_register = false;
+ if self.editor.is_none() {
+ let mut ed = EditorView::new(w, h, ppp);
+ ed.set_read_only(true);
+ ed.set_bp_gutter(true); // the debugger always shows/toggles breakpoints
+ ed.set_language("cs");
+ // Match the Unity code editor's look (dark theme + RobotoMono).
+ ed.set_theme(0.156, 0.156, 0.156, 1.0, 0xd2, 0xd2, 0xd2, true);
+ if let Some(p) = unity_font_path("RobotoMono-Regular.ttf") {
+ ed.set_font(&p);
+ }
+ self.editor = Some(ed);
+ self.editor_dims = (w, h);
+ needs_register = true;
+ } else if self.editor_dims != (w, h) {
+ if let Some(editor) = self.editor.as_mut() {
+ editor.resize(w, h, ppp);
+ }
+ self.editor_dims = (w, h);
+ needs_register = true; // the shared surface (and its view) was recreated
+ }
+
+ // Load the viewed file when it changes.
+ if self.view_file != self.editor_file {
+ self.editor_file = self.view_file.clone();
+ let text = std::fs::read_to_string(&self.view_file).unwrap_or_default();
+ self.editor.as_mut().unwrap().set_text(&text);
+ }
+
+ let ed = self.editor.as_mut().unwrap();
+
+ // Forward collected input. A press in the gutter toggles a breakpoint (routed
+ // through the session, the source of truth) instead of moving the caret.
+ let gutter = ed.gutter_width();
+ for act in &src_out.acts {
+ match act {
+ EditorAct::Mouse { x, y, kind } => {
+ if *kind == 0 && *x < gutter {
+ let line = ed.line_at_y(*y);
+ self.tx.send(Cmd::ToggleBp(line as i32 + 1)).ok();
+ } else {
+ ed.mouse(*x, *y, *kind);
+ }
+ }
+ EditorAct::Scroll { dx, dy } => {
+ ed.scroll(*dy);
+ ed.scroll_h(*dx);
+ }
+ EditorAct::Key(name, ctrl, alt, shift) => ed.key(name, *ctrl, *alt, *shift),
+ }
+ }
+
+ // Breakpoints (1-based store → 0-based editor) + the execution line.
+ let bp0: Vec = snap
+ .bp_lines
+ .iter()
+ .filter(|&&l| l > 0)
+ .map(|&l| (l - 1) as u32)
+ .collect();
+ ed.set_breakpoints(&bp0);
+ let viewing_stopped =
+ snap.stopped && basename(&self.view_file) == basename(&snap.cur_file);
+ ed.set_exec_line(if viewing_stopped && snap.cur_line > 0 {
+ (snap.cur_line - 1) as usize
+ } else {
+ usize::MAX
+ });
+
+ if let Some(line) = scroll_line {
+ if line > 0 {
+ ed.goto_line((line - 1) as usize);
+ }
+ }
+
+ ed.render();
+ needs_register
+ }
+}
+
+/// Absolute path to a Unity-bundled font (`Contents/Resources/Fonts/`), if present.
+fn unity_font_path(name: &str) -> Option {
+ let inst = sdb::find_editor_instance(&std::env::current_dir().ok()?)?;
+ let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(inst).ok()?).ok()?;
+ let contents = json.get("app_contents_path")?.as_str()?;
+ let p = std::path::Path::new(contents)
+ .join("Resources")
+ .join("Fonts")
+ .join(name);
+ p.exists().then(|| p.to_string_lossy().into_owned())
+}
+
+// ---------------------------------------------------------------------------
+// Theme (Unity dark editor)
+// ---------------------------------------------------------------------------
+
+fn c(r: u8, g: u8, b: u8) -> Color32 {
+ Color32::from_rgb(r, g, b)
+}
+
+fn setup_theme(ctx: &egui::Context) {
+ // Fonts: Unity's Inter (UI) + RobotoMono (code), if available.
+ let mut fonts = egui::FontDefinitions::default();
+ if let Some(b) = unity_font_bytes("Inter-Regular.ttf") {
+ fonts.font_data.insert("inter".into(), Arc::new(egui::FontData::from_owned(b)));
+ fonts
+ .families
+ .entry(egui::FontFamily::Proportional)
+ .or_default()
+ .insert(0, "inter".into());
+ }
+ if let Some(b) = unity_font_bytes("RobotoMono-Regular.ttf") {
+ fonts.font_data.insert("robotomono".into(), Arc::new(egui::FontData::from_owned(b)));
+ fonts
+ .families
+ .entry(egui::FontFamily::Monospace)
+ .or_default()
+ .insert(0, "robotomono".into());
+ }
+ // Japanese fallback (Inter/RobotoMono have no CJK; Unity bundles no CJK font),
+ // appended last so kanji/kana render instead of tofu. Try each platform's system
+ // fonts; non-existent paths are simply skipped.
+ for jp in [
+ // macOS
+ "/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc",
+ "/System/Library/Fonts/Hiragino Sans GB.ttc",
+ // Windows
+ "C:\\Windows\\Fonts\\YuGothM.ttc",
+ "C:\\Windows\\Fonts\\meiryo.ttc",
+ "C:\\Windows\\Fonts\\msgothic.ttc",
+ // Linux (Noto CJK, common install paths)
+ "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
+ "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
+ ] {
+ if let Ok(b) = std::fs::read(jp) {
+ fonts.font_data.insert("jp".into(), Arc::new(egui::FontData::from_owned(b)));
+ for fam in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
+ fonts.families.entry(fam).or_default().push("jp".into());
+ }
+ break;
+ }
+ }
+ ctx.set_fonts(fonts);
+
+ use egui::{FontFamily, TextStyle};
+ let text_styles: std::collections::BTreeMap = [
+ (TextStyle::Small, FontId::new(10.0, FontFamily::Proportional)),
+ (TextStyle::Body, FontId::new(12.0, FontFamily::Proportional)),
+ (TextStyle::Button, FontId::new(12.0, FontFamily::Proportional)),
+ (TextStyle::Heading, FontId::new(13.0, FontFamily::Proportional)),
+ (TextStyle::Monospace, FontId::new(12.0, FontFamily::Monospace)),
+ ]
+ .into();
+
+ let mut v = egui::Visuals::dark();
+ v.window_fill = c(0x38, 0x38, 0x38);
+ v.panel_fill = c(0x38, 0x38, 0x38);
+ v.extreme_bg_color = c(0x28, 0x28, 0x28);
+ v.faint_bg_color = c(0x3f, 0x3f, 0x3f);
+ v.code_bg_color = c(0x28, 0x28, 0x28);
+ v.override_text_color = Some(c(0xc4, 0xc4, 0xc4));
+ v.hyperlink_color = c(0x4c, 0x7e, 0xff);
+ v.selection.bg_fill = c(0x2c, 0x5d, 0x87);
+ v.selection.stroke = egui::Stroke::new(1.0, c(0x4c, 0x7e, 0xff));
+ v.widgets.noninteractive.bg_fill = c(0x38, 0x38, 0x38);
+ v.widgets.noninteractive.fg_stroke.color = c(0xc4, 0xc4, 0xc4);
+ v.widgets.inactive.bg_fill = c(0x58, 0x58, 0x58);
+ v.widgets.inactive.weak_bg_fill = c(0x58, 0x58, 0x58);
+ v.widgets.inactive.fg_stroke.color = c(0xc4, 0xc4, 0xc4);
+ v.widgets.hovered.bg_fill = c(0x67, 0x67, 0x67);
+ v.widgets.hovered.weak_bg_fill = c(0x67, 0x67, 0x67);
+ v.widgets.hovered.fg_stroke.color = c(0xe0, 0xe0, 0xe0);
+ v.widgets.active.bg_fill = c(0x4c, 0x7e, 0xff);
+ v.widgets.active.weak_bg_fill = c(0x4c, 0x7e, 0xff);
+ v.widgets.open.bg_fill = c(0x45, 0x45, 0x45);
+ // Visible panel separators / borders (#232323), brighter on hover for resizing.
+ v.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, c(0x20, 0x20, 0x20));
+ v.widgets.hovered.bg_stroke = egui::Stroke::new(2.0, c(0x4c, 0x7e, 0xff));
+ v.widgets.active.bg_stroke = egui::Stroke::new(2.0, c(0x4c, 0x7e, 0xff));
+ ctx.all_styles_mut(move |style| {
+ style.text_styles = text_styles.clone();
+ style.visuals = v.clone();
+ style.spacing.item_spacing = egui::vec2(6.0, 3.0);
+ style.interaction.resize_grab_radius_side = 8.0;
+ // Only the source code + variable values opt into selection (below); keep
+ // titles, the file tree, the stack and the breakpoint list non-selectable.
+ style.interaction.selectable_labels = false;
+ });
+}
+
+fn unity_font_bytes(name: &str) -> Option> {
+ let inst = sdb::find_editor_instance(&std::env::current_dir().ok()?)?;
+ let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(inst).ok()?).ok()?;
+ let contents = json.get("app_contents_path")?.as_str()?;
+ let p = std::path::Path::new(contents)
+ .join("Resources")
+ .join("Fonts")
+ .join(name);
+ std::fs::read(p).ok()
+}
+
+// ---------------------------------------------------------------------------
+// UI
+// ---------------------------------------------------------------------------
+
+/// Borrowed data the UI reads each frame (avoids capturing `&mut App` in nested
+/// egui closures, which would be a borrow conflict).
+struct UiCtx<'a> {
+ s: &'a UiState,
+ tree: &'a Tree,
+ files: &'a [(String, String)],
+ /// egui texture handle for the embedded editor (None until its first render).
+ editor_tex: Option,
+ /// Hover-watch tooltip to draw over the source (screen pos + text).
+ hover_tip: Option<(egui::Pos2, String)>,
+}
+
+fn build_ui(
+ ui: &mut egui::Ui,
+ ctx: &UiCtx,
+ cmds: &mut Vec,
+ new_view: &mut Option,
+ goto: &mut Option,
+ focus: &mut Focus,
+ expanded: &mut std::collections::HashSet,
+ tree_sel: &mut usize,
+ var_open: &mut std::collections::HashSet,
+ src_out: &mut SourcePaneOut,
+) {
+ let s = ctx.s;
+ let accent = c(0x4c, 0x7e, 0xff);
+ let amber = c(0xe6, 0xb4, 0x40);
+
+ // --- toolbar ---
+ egui::Panel::top("toolbar").show(ui, |ui| {
+ ui.horizontal(|ui| {
+ let enabled = s.stopped;
+ ui.add_enabled_ui(enabled, |ui| {
+ if ui.button("▶").on_hover_text("Continue (c)").clicked() {
+ cmds.push(Cmd::Resume);
+ }
+ if ui.button("⤼").on_hover_text("Step Over (n)").clicked() {
+ cmds.push(Cmd::StepOver);
+ }
+ if ui.button("↓").on_hover_text("Step Into (i)").clicked() {
+ cmds.push(Cmd::StepInto);
+ }
+ if ui.button("↑").on_hover_text("Step Out (o)").clicked() {
+ cmds.push(Cmd::StepOut);
+ }
+ });
+ ui.add_enabled_ui(s.attached && !s.stopped, |ui| {
+ if ui.button("⏸").on_hover_text("Pause / Break All (p)").clicked() {
+ cmds.push(Cmd::Pause);
+ }
+ });
+ ui.separator();
+ let (word, col) = if !s.attached {
+ if s.status.is_empty() {
+ ("Connecting…", c(0x9a, 0x9a, 0x9a))
+ } else {
+ (s.status.as_str(), c(0xd6, 0x5a, 0x5a))
+ }
+ } else if s.stopped {
+ ("Paused", amber)
+ } else {
+ ("Running", c(0x88, 0xc0, 0x70))
+ };
+ ui.label(RichText::new("●").color(col));
+ let resp = ui.label(RichText::new(word).color(col));
+ if s.attached {
+ let tip = if s.version.is_empty() {
+ "attached".to_string()
+ } else {
+ format!("attached — {}", s.version)
+ };
+ resp.on_hover_text(tip);
+ }
+ if s.stopped && !s.cur_file.is_empty() {
+ ui.label(RichText::new(format!("{}:{}", basename(&s.cur_file), s.cur_line)).weak());
+ }
+
+ // Target picker: the editor, plus any debuggable players found on the network.
+ ui.separator();
+ let cur = if s.cur_target.is_empty() { "Editor" } else { s.cur_target.as_str() };
+ egui::ComboBox::from_id_salt("target")
+ .selected_text(cur)
+ .show_ui(ui, |ui| {
+ if ui.selectable_label(cur == "Editor", "Editor (this project)").clicked() {
+ cmds.push(Cmd::SelectTarget(Target {
+ ip: String::new(),
+ port: 0,
+ label: "Editor".to_string(),
+ }));
+ }
+ for p in &s.players {
+ let label = format!("{} ({})", p.label, p.ip);
+ let text = if p.project.is_empty() {
+ label.clone()
+ } else {
+ format!("{} · {}", p.project, label)
+ };
+ if ui.selectable_label(cur == label, text).clicked() {
+ cmds.push(Cmd::SelectTarget(Target {
+ ip: p.ip.clone(),
+ port: p.port,
+ label,
+ }));
+ }
+ }
+ if s.players.is_empty() {
+ ui.label(RichText::new("(no players found)").weak());
+ }
+ });
+ });
+ });
+
+ // keyboard shortcuts
+ ui.input(|i| {
+ if s.stopped {
+ if i.key_pressed(egui::Key::C) {
+ cmds.push(Cmd::Resume);
+ }
+ if i.key_pressed(egui::Key::N) {
+ cmds.push(Cmd::StepOver);
+ }
+ if i.key_pressed(egui::Key::I) {
+ cmds.push(Cmd::StepInto);
+ }
+ if i.key_pressed(egui::Key::O) {
+ cmds.push(Cmd::StepOut);
+ }
+ } else if s.attached && i.key_pressed(egui::Key::P) {
+ cmds.push(Cmd::Pause);
+ }
+ });
+
+ // --- left: file tree (no title; full rows; scrollable) ---
+ let files_resp = egui::Panel::left("files")
+ .resizable(true)
+ .default_size(210.0)
+ .show(ui, |ui| {
+ egui::ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
+ tree_panel(ui, ctx.tree, *focus == Focus::Tree, expanded, tree_sel, new_view);
+ });
+ });
+
+ // --- right: three vertically-stacked, resizable panes ---
+ egui::Panel::right("inspector")
+ .resizable(true)
+ .default_size(330.0)
+ .show(ui, |ui| {
+ // Cap each pane's max height so the next pane's title (>= ~46px) always
+ // stays visible — egui doesn't auto-negotiate sizes between stacked panels.
+ let min = 46.0_f32;
+ let total = ui.available_height();
+ let stack_max = (total - 2.0 * min).max(min);
+ egui::Panel::top("p_stack").resizable(true).default_size(150.0).size_range(egui::Rangef::new(min, stack_max)).show(ui, |ui| {
+ // Threads strip: only when the stop involves more than one managed thread.
+ if s.threads.len() > 1 {
+ ui.label(RichText::new("Threads").strong());
+ egui::ScrollArea::both().id_salt("sa_threads").max_height(72.0).auto_shrink([false, false]).show(ui, |ui| {
+ for t in &s.threads {
+ let selected = t.id == s.cur_thread;
+ let col = if selected { accent } else { c(0xc4, 0xc4, 0xc4) };
+ let resp = ui.add(
+ egui::Label::new(
+ RichText::new(format!("{} · {}", t.name, t.location)).monospace().color(col),
+ )
+ .selectable(false)
+ .truncate()
+ .sense(egui::Sense::click()),
+ );
+ if selected {
+ ui.painter().rect_filled(
+ resp.rect.expand2(egui::vec2(4.0, 1.0)),
+ 2.0,
+ accent.gamma_multiply(0.18),
+ );
+ }
+ if resp.clicked() && !selected {
+ cmds.push(Cmd::SelectThread(t.id));
+ }
+ }
+ });
+ ui.separator();
+ }
+ ui.label(RichText::new("Call Stack").strong());
+ egui::ScrollArea::both().id_salt("sa_stack").auto_shrink([false, false]).show(ui, |ui| {
+ for (i, f) in s.stack.iter().enumerate() {
+ let selected = i == s.cur_frame;
+ let col = if selected { accent } else { c(0xc4, 0xc4, 0xc4) };
+ let resp = ui.add(
+ egui::Label::new(RichText::new(f).monospace().color(col))
+ .selectable(false)
+ .sense(egui::Sense::click()),
+ );
+ if selected {
+ ui.painter().rect_filled(
+ resp.rect.expand2(egui::vec2(4.0, 1.0)),
+ 2.0,
+ accent.gamma_multiply(0.18),
+ );
+ }
+ if resp.clicked() && i != s.cur_frame {
+ cmds.push(Cmd::SelectFrame(i));
+ }
+ }
+ });
+ });
+ let vars_max = (ui.available_height() - min).max(min);
+ egui::Panel::top("p_vars").resizable(true).default_size(240.0).size_range(egui::Rangef::new(min, vars_max)).show(ui, |ui| {
+ ui.label(RichText::new("Variables").strong());
+ egui::ScrollArea::both().id_salt("sa_vars").auto_shrink([false, false]).show(ui, |ui| {
+ egui::CollapsingHeader::new("Local").default_open(true).show(ui, |ui| {
+ if s.locals.is_empty() {
+ ui.label(RichText::new("(none)").weak());
+ }
+ var_tree(ui, &s.locals, &s.children, var_open, cmds, 0, &mut Vec::new());
+ });
+ if !s.this_label.is_empty() {
+ egui::CollapsingHeader::new(format!("this : {}", s.this_label))
+ .default_open(true)
+ .show(ui, |ui| {
+ var_tree(ui, &s.members, &s.children, var_open, cmds, 0, &mut Vec::new());
+ });
+ }
+ });
+ });
+ egui::CentralPanel::default().show(ui, |ui| {
+ ui.label(RichText::new("Breakpoints").strong());
+ egui::ScrollArea::both().id_salt("sa_bps").auto_shrink([false, false]).show(ui, |ui| {
+ if s.all_bps.is_empty() {
+ ui.label(RichText::new("(none)").weak());
+ }
+ for (file, line) in &s.all_bps {
+ let resp = ui
+ .horizontal(|ui| {
+ ui.label(RichText::new("●").color(c(0xc8, 0x3a, 0x3a)));
+ ui.add(
+ egui::Label::new(RichText::new(format!("{file}:{line}")).monospace())
+ .sense(egui::Sense::click()),
+ )
+ })
+ .inner;
+ if resp.clicked() {
+ if let Some((abs, _)) = ctx.files.iter().find(|(_, n)| n == file) {
+ *new_view = Some(abs.clone());
+ *goto = Some(*line);
+ }
+ }
+ }
+ });
+ });
+ });
+
+ // --- center: source ---
+ let src_resp = egui::CentralPanel::default().show(ui, |ui| {
+ source_view(ui, ctx, *focus == Focus::Source, src_out);
+ });
+
+ // Focus follows the last click (updates next frame's arrow-key target).
+ if ui.ctx().input(|i| i.pointer.any_pressed()) {
+ if let Some(p) = ui.ctx().input(|i| i.pointer.interact_pos()) {
+ *focus = if files_resp.response.rect.contains(p) {
+ Focus::Tree
+ } else if src_resp.response.rect.contains(p) {
+ Focus::Source
+ } else {
+ Focus::None
+ };
+ }
+ }
+}
+
+/// Render variable rows, recursing into expanded objects/arrays (children fetched
+/// lazily via `Cmd::ExpandVar` the first time a node is opened).
+///
+/// `path` is the chain of object ids currently descended through. Because open state
+/// is keyed by object id, a cyclic graph (e.g. Unity's `transform ↔ gameObject`) would
+/// otherwise recurse forever — expanding both ends makes every occurrence auto-open —
+/// exhausting memory. We refuse to descend into an id already on the path.
+fn var_tree(
+ ui: &mut egui::Ui,
+ vars: &[Var],
+ children: &std::collections::HashMap>,
+ var_open: &mut std::collections::HashSet,
+ cmds: &mut Vec,
+ depth: usize,
+ path: &mut Vec,
+) {
+ for v in vars {
+ let open = v.expand.map(|e| var_open.contains(&e.id)).unwrap_or(false);
+ ui.horizontal(|ui| {
+ ui.add_space(depth as f32 * 14.0);
+ // Expand triangle (or a spacer, to keep names aligned).
+ if let Some(e) = v.expand {
+ let tri = if open { "▾" } else { "▸" };
+ let hit = ui.add(
+ egui::Label::new(RichText::new(tri).monospace().color(c(0x9a, 0x9a, 0x9a)))
+ .sense(egui::Sense::click()),
+ );
+ let name = ui.add(
+ egui::Label::new(RichText::new(&v.name).monospace().color(c(0xa0, 0xa0, 0xa0)))
+ .selectable(false)
+ .sense(egui::Sense::click()),
+ );
+ if hit.clicked() || name.clicked() {
+ if open {
+ var_open.remove(&e.id);
+ } else {
+ var_open.insert(e.id);
+ if !children.contains_key(&e.id) {
+ cmds.push(Cmd::ExpandVar { id: e.id, array: e.array });
+ }
+ }
+ }
+ } else {
+ ui.add_space(12.0);
+ ui.label(RichText::new(&v.name).monospace().color(c(0xa0, 0xa0, 0xa0)));
+ }
+ if !v.value.is_empty() {
+ ui.label(RichText::new("=").weak());
+ // The value is selectable (copy it); names/triangles are not.
+ ui.add(
+ egui::Label::new(RichText::new(&v.value).monospace().color(c(0xd2, 0xd2, 0xd2)))
+ .selectable(true),
+ );
+ }
+ });
+ if open {
+ if let Some(e) = v.expand {
+ if path.contains(&e.id) || depth >= 64 {
+ // Cycle (or pathologically deep): stop descending to bound memory.
+ leaf_note(ui, depth + 1, "…");
+ } else {
+ match children.get(&e.id) {
+ Some(kids) if !kids.is_empty() => {
+ path.push(e.id);
+ var_tree(ui, kids, children, var_open, cmds, depth + 1, path);
+ path.pop();
+ }
+ Some(_) => leaf_note(ui, depth + 1, "(empty)"),
+ None => leaf_note(ui, depth + 1, "loading…"),
+ }
+ }
+ }
+ }
+ }
+}
+
+fn leaf_note(ui: &mut egui::Ui, depth: usize, text: &str) {
+ ui.horizontal(|ui| {
+ ui.add_space(depth as f32 * 14.0 + 12.0);
+ ui.label(RichText::new(text).weak());
+ });
+}
+
+/// The source pane: draws the embedded editor's texture and forwards pointer/keys
+/// to it (applied to the editor on `App` after the egui pass). The editor renders
+/// the code, gutter, breakpoints, caret and current line — this is just its host.
+fn source_view(ui: &mut egui::Ui, ctx: &UiCtx, focused: bool, out: &mut SourcePaneOut) {
+ let (rect, resp) = ui.allocate_exact_size(ui.available_size(), egui::Sense::click_and_drag());
+ out.rect = rect;
+
+ if let Some(tex) = ctx.editor_tex {
+ ui.painter().image(
+ tex,
+ rect,
+ egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
+ Color32::WHITE,
+ );
+ } else {
+ ui.painter().rect_filled(rect, 0.0, c(0x28, 0x28, 0x28));
+ }
+
+ // egui points → physical px relative to the pane's top-left (matching the code
+ // editor window's `(mouse - rect) * ppp` convention).
+ let ppp = ui.ctx().pixels_per_point();
+ let phys = |p: egui::Pos2| ((p.x - rect.min.x) * ppp, (p.y - rect.min.y) * ppp);
+
+ // Mouse (kinds: 0 click, 1 drag, 2 double-click).
+ if resp.double_clicked() {
+ if let Some(p) = resp.interact_pointer_pos() {
+ let (x, y) = phys(p);
+ out.acts.push(EditorAct::Mouse { x, y, kind: 2 });
+ }
+ } else if resp.drag_started() || resp.clicked() {
+ if let Some(p) = resp.interact_pointer_pos() {
+ let (x, y) = phys(p);
+ out.acts.push(EditorAct::Mouse { x, y, kind: 0 });
+ }
+ }
+ if resp.dragged() {
+ if let Some(p) = resp.interact_pointer_pos() {
+ let (x, y) = phys(p);
+ out.acts.push(EditorAct::Mouse { x, y, kind: 1 });
+ }
+ }
+
+ // Wheel scroll + hover position (for the value tooltip) while over the pane.
+ if resp.hovered() {
+ let d = ui.input(|i| i.smooth_scroll_delta);
+ if d.x != 0.0 || d.y != 0.0 {
+ out.acts.push(EditorAct::Scroll { dx: -d.x * ppp, dy: -d.y * ppp });
+ }
+ out.hover = resp.hover_pos();
+ }
+
+ // Hover-watch tooltip (computed a frame behind, so it needs the live editor).
+ if let Some((pos, text)) = &ctx.hover_tip {
+ egui::Area::new(egui::Id::new("unterm_hover_watch"))
+ .fixed_pos(*pos + egui::vec2(12.0, 16.0))
+ .order(egui::Order::Tooltip)
+ .interactable(false)
+ .show(ui.ctx(), |ui| {
+ egui::Frame::popup(ui.style()).show(ui, |ui| {
+ ui.label(RichText::new(text).monospace());
+ });
+ });
+ }
+
+ // Keyboard navigation when the pane has focus (read-only ⇒ only motions apply).
+ if focused {
+ let events = ui.input(|i| i.events.clone());
+ for ev in events {
+ if let egui::Event::Key { key, pressed: true, modifiers, .. } = ev {
+ if let Some(name) = map_editor_key(key, &modifiers) {
+ out.acts
+ .push(EditorAct::Key(name, modifiers.ctrl, modifiers.alt, modifiers.shift));
+ }
+ }
+ }
+ }
+}
+
+/// Translate an egui key press into the editor's key-name convention (navigation
+/// only; shift extends the selection, alt/ctrl = word, cmd = line/document ends).
+fn map_editor_key(key: egui::Key, m: &egui::Modifiers) -> Option {
+ use egui::Key;
+ let cmd = m.command || m.mac_cmd;
+ let word = m.alt || m.ctrl;
+ let name = match key {
+ Key::ArrowLeft => {
+ if word {
+ "WordLeft"
+ } else if cmd {
+ "Home"
+ } else {
+ "LeftArrow"
+ }
+ }
+ Key::ArrowRight => {
+ if word {
+ "WordRight"
+ } else if cmd {
+ "End"
+ } else {
+ "RightArrow"
+ }
+ }
+ Key::ArrowUp => {
+ if cmd {
+ "DocStart"
+ } else {
+ "UpArrow"
+ }
+ }
+ Key::ArrowDown => {
+ if cmd {
+ "DocEnd"
+ } else {
+ "DownArrow"
+ }
+ }
+ Key::Home => "Home",
+ Key::End => "End",
+ Key::PageUp => "PageUp",
+ Key::PageDown => "PageDown",
+ _ => return None,
+ };
+ Some(name.to_string())
+}
+
+// --- file tree ---
+
+#[derive(Default)]
+struct Tree {
+ dirs: BTreeMap,
+ files: Vec<(String, String)>, // (name, abspath)
+}
+
+fn build_tree(files: &[(String, String)]) -> Tree {
+ let root = project_root();
+ let mut tree = Tree::default();
+ for (abs, name) in files {
+ let rel = std::path::Path::new(abs)
+ .strip_prefix(&root)
+ .ok()
+ .map(|p| p.to_path_buf())
+ .unwrap_or_else(|| std::path::PathBuf::from(name));
+ let mut comps: Vec = rel
+ .components()
+ .map(|c| c.as_os_str().to_string_lossy().into_owned())
+ .collect();
+ // Show cached packages (Library/PackageCache/…) under the "Packages" node.
+ if comps.len() >= 2 && comps[0] == "Library" && comps[1] == "PackageCache" {
+ comps.splice(0..2, ["Packages".to_string()]);
+ }
+ let mut node = &mut tree;
+ for (k, comp) in comps.iter().enumerate() {
+ if k + 1 == comps.len() {
+ node.files.push((comp.clone(), abs.clone()));
+ } else {
+ node = node.dirs.entry(comp.clone()).or_default();
+ }
+ }
+ }
+ tree
+}
+
+/// One visible row of the file tree, in display order (respecting `expanded`).
+struct TreeRow {
+ depth: usize,
+ is_dir: bool,
+ label: String,
+ /// Folder path (tracked in `expanded`) for dirs, or the file abspath for files.
+ key: String,
+}
+
+/// Flatten the tree into ordered visible rows, descending into open folders only.
+fn flatten_tree(
+ tree: &Tree,
+ prefix: &str,
+ depth: usize,
+ expanded: &std::collections::HashSet,
+ out: &mut Vec,
+) {
+ for (dname, child) in &tree.dirs {
+ let key = if prefix.is_empty() {
+ dname.clone()
+ } else {
+ format!("{prefix}/{dname}")
+ };
+ out.push(TreeRow {
+ depth,
+ is_dir: true,
+ label: dname.clone(),
+ key: key.clone(),
+ });
+ if expanded.contains(&key) {
+ flatten_tree(child, &key, depth + 1, expanded, out);
+ }
+ }
+ for (name, abs) in &tree.files {
+ out.push(TreeRow {
+ depth,
+ is_dir: false,
+ label: name.clone(),
+ key: abs.clone(),
+ });
+ }
+}
+
+/// Custom file tree. Keyboard (when the pane is focused): ↑/↓ move the cursor over
+/// the visible rows (folders included), → expands a folder (or steps into its first
+/// child), ← collapses it (or steps out to the parent), and ↑/↓/Enter open a file
+/// row. Clicking toggles a folder or opens a file. Full-width Unity-style selection.
+fn tree_panel(
+ ui: &mut egui::Ui,
+ tree: &Tree,
+ focused: bool,
+ expanded: &mut std::collections::HashSet,
+ tree_sel: &mut usize,
+ new_view: &mut Option,
+) {
+ let mut rows = Vec::new();
+ flatten_tree(tree, "", 0, expanded, &mut rows);
+ if rows.is_empty() {
+ return;
+ }
+ *tree_sel = (*tree_sel).min(rows.len() - 1);
+
+ let mut scroll_sel = false;
+ if focused {
+ let (up, down, left, right, enter) = ui.input(|i| {
+ (
+ i.key_pressed(egui::Key::ArrowUp),
+ i.key_pressed(egui::Key::ArrowDown),
+ i.key_pressed(egui::Key::ArrowLeft),
+ i.key_pressed(egui::Key::ArrowRight),
+ i.key_pressed(egui::Key::Enter),
+ )
+ });
+ scroll_sel = up || down || left || right || enter;
+ if down {
+ *tree_sel = (*tree_sel + 1).min(rows.len() - 1);
+ }
+ if up {
+ *tree_sel = tree_sel.saturating_sub(1);
+ }
+ if right {
+ let r = &rows[*tree_sel];
+ if r.is_dir {
+ if !expanded.contains(&r.key) {
+ expanded.insert(r.key.clone());
+ } else if *tree_sel + 1 < rows.len() && rows[*tree_sel + 1].depth > r.depth {
+ *tree_sel += 1; // already open: step into first child
+ }
+ }
+ }
+ if left {
+ let (is_dir, key, depth) = (
+ rows[*tree_sel].is_dir,
+ rows[*tree_sel].key.clone(),
+ rows[*tree_sel].depth,
+ );
+ if is_dir && expanded.contains(&key) {
+ expanded.remove(&key); // collapse
+ } else if depth > 0 {
+ for k in (0..*tree_sel).rev() {
+ if rows[k].depth < depth {
+ *tree_sel = k; // step out to parent
+ break;
+ }
+ }
+ }
+ }
+ if up || down || enter {
+ let r = &rows[*tree_sel];
+ if !r.is_dir {
+ *new_view = Some(r.key.clone());
+ } else if enter {
+ if expanded.contains(&r.key) {
+ expanded.remove(&r.key);
+ } else {
+ expanded.insert(r.key.clone());
+ }
+ }
+ }
+ }
+
+ let row_h = 18.0;
+ let font = FontId::proportional(12.0);
+ for (idx, r) in rows.iter().enumerate() {
+ let selected = idx == *tree_sel;
+ let text_col = if selected {
+ c(0xf0, 0xf0, 0xf2)
+ } else {
+ c(0xc4, 0xc4, 0xc4)
+ };
+ let galley = ui
+ .painter()
+ .layout_no_wrap(r.label.clone(), font.clone(), text_col);
+ let indent = 6.0 + r.depth as f32 * 14.0;
+ let row_w = (indent + 16.0 + galley.size().x).max(ui.available_width());
+ let (rect, resp) = ui.allocate_exact_size(egui::vec2(row_w, row_h), egui::Sense::click());
+ if selected {
+ ui.painter().rect_filled(rect, 0.0, c(0x2c, 0x5d, 0x87));
+ if scroll_sel {
+ ui.scroll_to_rect(rect, None);
+ }
+ } else if resp.hovered() {
+ ui.painter().rect_filled(rect, 0.0, c(0x45, 0x45, 0x45));
+ }
+ let x = rect.left() + indent;
+ let cy = rect.center().y;
+ if r.is_dir {
+ // Draw the disclosure triangle as a vector shape: the loaded UI font has no
+ // reliable ▸/▾ glyph, so a painted triangle renders crisply everywhere.
+ let col = c(0x9a, 0x9a, 0x9a);
+ let pts = if expanded.contains(&r.key) {
+ vec![
+ egui::pos2(x + 1.0, cy - 2.0),
+ egui::pos2(x + 9.0, cy - 2.0),
+ egui::pos2(x + 5.0, cy + 3.0),
+ ]
+ } else {
+ vec![
+ egui::pos2(x + 2.0, cy - 4.0),
+ egui::pos2(x + 2.0, cy + 4.0),
+ egui::pos2(x + 7.0, cy),
+ ]
+ };
+ ui.painter()
+ .add(egui::Shape::convex_polygon(pts, col, egui::Stroke::NONE));
+ }
+ ui.painter().galley(
+ egui::pos2(x + 16.0, cy - galley.size().y / 2.0),
+ galley,
+ text_col,
+ );
+ if resp.clicked() {
+ *tree_sel = idx;
+ if r.is_dir {
+ if expanded.contains(&r.key) {
+ expanded.remove(&r.key);
+ } else {
+ expanded.insert(r.key.clone());
+ }
+ } else {
+ *new_view = Some(r.key.clone());
+ }
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// SDB session thread (unchanged from the glyphon version)
+// ---------------------------------------------------------------------------
+
+struct Stop {
+ stack: Vec,
+ frames: Vec,
+ locals: Vec,
+ members: Vec,
+ this_label: String,
+ file: String,
+ line: i32,
+}
+
+struct Bp {
+ file: String,
+ line: i32,
+ armed: bool,
+ req: Option,
+}
+
+/// Listen for Unity player-discovery multicast and keep `s.players` in sync (deduped
+/// by Guid, expiring players not heard from for a few seconds). Runs for the process's
+/// lifetime; a bind failure (locked-down env) just leaves the player list empty.
+fn spawn_discovery(state: Arc>, proxy: EventLoopProxy) {
+ std::thread::spawn(move || {
+ let Ok(sock) = sdb::discovery_socket() else {
+ return;
+ };
+ let mut seen: std::collections::HashMap =
+ std::collections::HashMap::new();
+ let mut last_sig: Vec = Vec::new();
+ let mut buf = [0u8; 2048];
+ loop {
+ match sock.recv_from(&mut buf) {
+ Ok((n, _)) => {
+ if let Some(p) = sdb::parse_player(&buf[..n]) {
+ seen.insert(p.guid, (p, std::time::Instant::now()));
+ }
+ }
+ Err(ref e)
+ if matches!(
+ e.kind(),
+ std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
+ ) => {}
+ Err(_) => {}
+ }
+ seen.retain(|_, (_, t)| t.elapsed() < std::time::Duration::from_secs(4));
+ // Only publish (and wake the UI) when the set of players actually changes.
+ let mut sig: Vec = seen.keys().copied().collect();
+ sig.sort_unstable();
+ if sig != last_sig {
+ last_sig = sig;
+ let mut players: Vec = seen.values().map(|(p, _)| p.clone()).collect();
+ players.sort_by(|a, b| a.project.cmp(&b.project).then(a.label.cmp(&b.label)));
+ set(&state, &proxy, |s| s.players = players);
+ }
+ }
+ });
+}
+
+fn session_thread(
+ breakpoints: Vec<(String, i32)>,
+ state: Arc>,
+ rx: Receiver,
+ proxy: EventLoopProxy,
+) {
+ let root = std::env::current_dir().unwrap_or_default();
+ // Breakpoints seeded on every (re)connect — the store is the source of truth; CLI
+ // pairs, when given, override it for a manual run.
+ let seed = if breakpoints.is_empty() {
+ load_breakpoints().unwrap_or_default()
+ } else {
+ breakpoints
+ };
+ // Default target: this project's editor; the user can switch to a discovered player.
+ // An empty ip is the "Editor" sentinel — resolved fresh (its pid can change) below.
+ let mut target = Target { ip: String::new(), port: 0, label: "Editor".to_string() };
+ loop {
+ if target.ip.is_empty() {
+ match sdb::editor_endpoint(&root) {
+ Ok((ip, port)) => {
+ target.ip = ip;
+ target.port = port;
+ }
+ Err(_) => {
+ target.ip = "127.0.0.1".to_string();
+ }
+ }
+ }
+ set(&state, &proxy, |s| {
+ s.attached = false;
+ s.stopped = false;
+ s.status = format!("Connecting to {}…", target.label);
+ });
+ let conn = match sdb::connect(&target.ip, target.port) {
+ Ok(c) => c,
+ Err(e) => {
+ set(&state, &proxy, |s| s.status = format!("{}: connect failed: {e}", target.label));
+ // Nothing to do until the user picks a (different) target.
+ match wait_for_target(&rx) {
+ Some(t) => {
+ target = t;
+ continue;
+ }
+ None => return, // UI channel closed → shutting down
+ }
+ }
+ };
+ match run_connection(conn, &seed, state.clone(), &rx, proxy.clone(), &target.label) {
+ Some(t) => target = t, // switch targets and reconnect
+ None => return, // disconnected / died / channel closed
+ }
+ }
+}
+
+/// Block until the UI asks to attach to a target (or the channel closes → `None`).
+fn wait_for_target(rx: &Receiver) -> Option {
+ loop {
+ match rx.recv() {
+ Ok(Cmd::SelectTarget(t)) => return Some(t),
+ Ok(_) => {}
+ Err(_) => return None,
+ }
+ }
+}
+
+/// One attached session. Returns `Some(next)` to switch targets, or `None` to exit.
+fn run_connection(
+ mut conn: sdb::Connection,
+ seed: &[(String, i32)],
+ state: Arc>,
+ rx: &Receiver,
+ proxy: EventLoopProxy,
+ target_label: &str,
+) -> Option {
+ let v = conn.version().clone();
+ while conn.take_queued().is_some() {}
+
+ let mut bps: Vec = seed
+ .iter()
+ .cloned()
+ .map(|(file, line)| Bp { file, line, armed: false, req: None })
+ .collect();
+ set(&state, &proxy, |s| {
+ s.attached = true;
+ s.cur_target = target_label.to_string();
+ s.status.clear();
+ s.version = format!("{} (protocol {}.{})", v.vm, v.major, v.minor);
+ s.push_log(format!("attached: {target_label} ({} bp)", bps.len()));
+ });
+
+ // Watch for a new play-mode AppDomain: the Play domain reload invalidates any
+ // breakpoint armed on an edit-mode method, so we re-sync + re-arm on the fresh types.
+ conn.set_event(wire::kind::APPDOMAIN_CREATE, wire::suspend::NONE, &[]).ok();
+ let mut watch_req: Option = None;
+ rewatch(&mut conn, &bps, &mut watch_req);
+ arm_loaded(&mut conn, &mut bps, &state, &proxy);
+ sync_bps(&state, &proxy, &bps, "");
+
+ conn.set_read_timeout(Some(Duration::from_millis(50))).ok();
+ let mut cur_thread: u32 = 0;
+ let mut cur_frames: Vec = Vec::new();
+ let mut step_req: Option = None;
+ let mut ui_view = String::new();
+
+ // The editor menu, re-opened while this debugger is already running, touches
+ // `focus.request` to ask us to come forward instead of spawning a second window.
+ let focus_path = unterm_state_path("focus.request");
+ let mut last_focus = std::fs::metadata(&focus_path).ok().and_then(|m| m.modified().ok());
+
+ // Live breakpoint sync: watch the shared store for edits the editor makes while we
+ // run, and reconcile. `last_bp` is bumped after our own writes so we don't re-read
+ // them back.
+ let bp_path = unterm_state_path("breakpoints.json");
+ let bp_mtime = || std::fs::metadata(&bp_path).ok().and_then(|m| m.modified().ok());
+ let mut last_bp = bp_mtime();
+
+ // Retry arming: when we attach mid-play the agent may not answer type queries yet,
+ // so the initial `arm_loaded` finds nothing; keep retrying un-armed breakpoints
+ // (against already-loaded types) so they take without a manual toggle.
+ let mut last_arm = std::time::Instant::now();
+
+ loop {
+ let f = std::fs::metadata(&focus_path).ok().and_then(|m| m.modified().ok());
+ if f != last_focus {
+ last_focus = f;
+ proxy.send_event(Wake::Focus).ok();
+ }
+
+ // Editor edited the breakpoint store while we run → reconcile live.
+ let cur_bp = bp_mtime();
+ if cur_bp != last_bp {
+ last_bp = cur_bp;
+ reconcile_breakpoints(&mut conn, &mut bps, &state, &proxy, &ui_view);
+ }
+
+ if bps.iter().any(|b| !b.armed)
+ && last_arm.elapsed() >= Duration::from_millis(500)
+ {
+ last_arm = std::time::Instant::now();
+ arm_loaded(&mut conn, &mut bps, &state, &proxy);
+ sync_bps(&state, &proxy, &bps, &ui_view);
+ }
+
+ while let Ok(cmd) = rx.try_recv() {
+ match &cmd {
+ // Attach elsewhere: drop this connection (the agent detaches/resumes on
+ // disconnect) and let the outer loop reconnect to the chosen target.
+ Cmd::SelectTarget(t) => return Some(t.clone()),
+ // View + breakpoint edits work any time (even while running).
+ Cmd::View(file) => {
+ ui_view = file.clone();
+ sync_bps(&state, &proxy, &bps, &ui_view);
+ }
+ Cmd::ToggleBp(line) => {
+ let line = *line;
+ let base = basename(&ui_view).to_string();
+ if base.is_empty() {
+ continue;
+ }
+ if let Some(pos) = bps.iter().position(|b| basename(&b.file) == base && b.line == line) {
+ let req = bps[pos].req;
+ req.map(|q| conn.clear_event(wire::kind::BREAKPOINT, q));
+ bps.remove(pos);
+ } else {
+ let mut bp = Bp { file: base.clone(), line, armed: false, req: None };
+ if let Ok(types) = conn.types_for_source_file(&bp.file, true) {
+ if !types.is_empty() {
+ if let Some(req) = arm(&mut conn, &types, &bp.file, bp.line, &state, &proxy) {
+ bp.armed = true;
+ bp.req = Some(req);
+ }
+ }
+ }
+ bps.push(bp);
+ }
+ sync_bps(&state, &proxy, &bps, &ui_view);
+ persist_breakpoints(&ui_view, &bps);
+ last_bp = bp_mtime(); // our own write; don't reconcile it back
+ }
+ // Break into a running program: suspend, then inspect a thread that
+ // has a resolvable managed stack (Unity spawns many native threads).
+ Cmd::Pause if cur_thread == 0 => {
+ if conn.suspend().is_ok() {
+ let threads = conn.all_threads().unwrap_or_default();
+ let chosen = threads
+ .iter()
+ .copied()
+ .find(|&t| {
+ conn.frames(t)
+ .ok()
+ .and_then(|f| f.into_iter().next())
+ .and_then(|f| conn.debug_info(f.method).ok().map(|i| (i, f.il_offset)))
+ .and_then(|(i, il)| il_to_source(&i, il))
+ .map(|(_, ln)| ln > 0)
+ .unwrap_or(false)
+ })
+ .or_else(|| threads.first().copied())
+ .unwrap_or(0);
+ if chosen != 0 {
+ cur_thread = chosen;
+ let stop = dump(&mut conn, chosen);
+ cur_frames = stop.frames;
+ let threads = collect_threads(&mut conn);
+ ui_view = stop.file.clone();
+ set(&state, &proxy, |s| {
+ s.stopped = true;
+ s.threads = threads;
+ s.cur_thread = chosen;
+ s.cur_frame = 0;
+ s.cur_file = stop.file;
+ s.cur_line = stop.line;
+ s.stack = stop.stack;
+ s.locals = stop.locals;
+ s.members = stop.members;
+ s.this_label = stop.this_label;
+ s.children.clear();
+ s.gen = s.gen.wrapping_add(1);
+ });
+ sync_bps(&state, &proxy, &bps, &ui_view);
+ proxy.send_event(Wake::Focus).ok();
+ } else {
+ conn.resume().ok();
+ }
+ }
+ }
+ // Inspect a different thread: switch the active thread and dump its
+ // innermost frame. Subsequent steps then act on this thread.
+ Cmd::SelectThread(id) if cur_thread != 0 => {
+ let id = *id;
+ cur_thread = id;
+ let stop = dump(&mut conn, id);
+ cur_frames = stop.frames;
+ ui_view = stop.file.clone();
+ set(&state, &proxy, |s| {
+ s.cur_thread = id;
+ s.cur_frame = 0;
+ s.cur_file = stop.file;
+ s.cur_line = stop.line;
+ s.stack = stop.stack;
+ s.locals = stop.locals;
+ s.members = stop.members;
+ s.this_label = stop.this_label;
+ s.children.clear();
+ s.gen = s.gen.wrapping_add(1);
+ });
+ sync_bps(&state, &proxy, &bps, &ui_view);
+ }
+ // Inspect a different call-stack frame: recompute its variables and
+ // jump the source view to its location.
+ Cmd::SelectFrame(idx) if cur_thread != 0 => {
+ if let Some(frame) = cur_frames.get(*idx).cloned() {
+ let (locals, members, this_label) = frame_vars(&mut conn, cur_thread, &frame);
+ let (file, line) = conn
+ .debug_info(frame.method)
+ .ok()
+ .and_then(|info| il_to_source(&info, frame.il_offset))
+ .unwrap_or_default();
+ ui_view = file.clone();
+ let idx = *idx;
+ set(&state, &proxy, |s| {
+ s.cur_frame = idx;
+ s.cur_file = file;
+ s.cur_line = line;
+ s.locals = locals;
+ s.members = members;
+ s.this_label = this_label;
+ s.children.clear();
+ s.gen = s.gen.wrapping_add(1);
+ });
+ sync_bps(&state, &proxy, &bps, &ui_view);
+ }
+ }
+ // Lazily fetch an expandable value's children.
+ Cmd::ExpandVar { id, array } if cur_thread != 0 => {
+ let (id, array) = (*id, *array);
+ let children = if array {
+ fetch_array(&mut conn, id)
+ } else {
+ fetch_object_fields(&mut conn, id)
+ };
+ set(&state, &proxy, |s| {
+ s.children.insert(id, children);
+ });
+ }
+ // Resume/step only make sense while stopped.
+ Cmd::Resume if cur_thread != 0 => {
+ running(&state, &proxy);
+ conn.resume().ok();
+ cur_thread = 0;
+ cur_frames.clear();
+ }
+ step @ (Cmd::StepOver | Cmd::StepInto | Cmd::StepOut) if cur_thread != 0 => {
+ let depth = match step {
+ Cmd::StepInto => wire::step::DEPTH_INTO,
+ Cmd::StepOut => wire::step::DEPTH_OUT,
+ _ => wire::step::DEPTH_OVER,
+ };
+ if let Ok(req) = conn.step(cur_thread, wire::step::SIZE_LINE, depth) {
+ step_req = Some(req);
+ }
+ running(&state, &proxy);
+ conn.resume().ok();
+ cur_thread = 0;
+ cur_frames.clear();
+ }
+ _ => {}
+ }
+ }
+
+ match conn.next_event() {
+ Ok(comp) => {
+ // Whether a breakpoint/step actually stopped us in this batch — if so we
+ // stay suspended; otherwise a suspended batch (e.g. the type-load watch)
+ // must be resumed once we've handled it.
+ let mut stopped_here = false;
+ for ev in &comp.events {
+ match ev.kind {
+ wire::kind::APPDOMAIN_CREATE => {
+ // Fresh (play-mode) domain: clear the old event requests (so
+ // they can't linger as duplicates), re-sync breakpoints from
+ // the shared store (so editor-side edits apply on this run),
+ // then re-watch + re-arm on TYPE_LOAD.
+ for bp in &mut bps {
+ if let Some(req) = bp.req.take() {
+ conn.clear_event(wire::kind::BREAKPOINT, req).ok();
+ }
+ bp.armed = false;
+ }
+ if let Some(fresh) = load_breakpoints() {
+ bps = fresh
+ .into_iter()
+ .map(|(file, line)| Bp { file, line, armed: false, req: None })
+ .collect();
+ }
+ rewatch(&mut conn, &bps, &mut watch_req);
+ sync_bps(&state, &proxy, &bps, &ui_view);
+ }
+ wire::kind::TYPE_LOAD => {
+ for bp in &mut bps {
+ if !bp.armed {
+ if let Some(req) =
+ arm(&mut conn, &[ev.type_id], &bp.file, bp.line, &state, &proxy)
+ {
+ bp.armed = true;
+ bp.req = Some(req);
+ }
+ }
+ }
+ }
+ wire::kind::BREAKPOINT | wire::kind::STEP => {
+ // A landed step keeps focus where it is; a real breakpoint
+ // hit surfaces the window (the user is over in the editor).
+ let was_step = step_req.is_some();
+ if let Some(req) = step_req.take() {
+ conn.clear_event(wire::kind::STEP, req).ok();
+ }
+ stopped_here = true;
+ cur_thread = ev.thread;
+ let stop = dump(&mut conn, ev.thread);
+ cur_frames = stop.frames;
+ let threads = collect_threads(&mut conn);
+ ui_view = stop.file.clone();
+ set(&state, &proxy, |s| {
+ s.stopped = true;
+ s.threads = threads;
+ s.cur_thread = ev.thread;
+ s.cur_frame = 0;
+ s.cur_file = stop.file;
+ s.cur_line = stop.line;
+ s.stack = stop.stack;
+ s.locals = stop.locals;
+ s.members = stop.members;
+ s.this_label = stop.this_label;
+ s.children.clear();
+ s.gen = s.gen.wrapping_add(1);
+ });
+ sync_bps(&state, &proxy, &bps, &ui_view);
+ if !was_step {
+ proxy.send_event(Wake::Focus).ok();
+ }
+ }
+ wire::kind::VM_DEATH => {
+ set(&state, &proxy, |s| {
+ s.attached = false;
+ s.status = "target exited".into();
+ });
+ proxy.send_event(Wake::Quit).ok();
+ return None;
+ }
+ _ => {}
+ }
+ }
+ // The type-load watch suspends the VM so we can arm a breakpoint on the
+ // freshly-loaded type BEFORE its methods (Awake/Start) run — winning the
+ // launch-vs-run race. Resume once armed (unless we actually stopped).
+ if comp.suspend_policy != wire::suspend::NONE && !stopped_here {
+ conn.resume().ok();
+ }
+ }
+ Err(wire::SdbError::Io(ref e))
+ if matches!(
+ e.kind(),
+ std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
+ ) => {}
+ Err(wire::SdbError::Eof) => {
+ set(&state, &proxy, |s| {
+ s.attached = false;
+ s.status = format!("{target_label} disconnected");
+ });
+ proxy.send_event(Wake::Quit).ok();
+ return None;
+ }
+ Err(e) => set(&state, &proxy, |s| s.push_log(format!("event error: {e}"))),
+ }
+ }
+}
+
+/// Publish bp_lines (current file) + all_bps (everything) for the UI.
+fn sync_bps(state: &Arc>, proxy: &EventLoopProxy, bps: &[Bp], cur_file: &str) {
+ let base = basename(cur_file);
+ let lines: Vec = bps
+ .iter()
+ .filter(|b| basename(&b.file) == base)
+ .map(|b| b.line)
+ .collect();
+ let all: Vec<(String, i32)> = bps
+ .iter()
+ .map(|b| (basename(&b.file).to_string(), b.line))
+ .collect();
+ set(state, proxy, |s| {
+ s.bp_lines = lines;
+ s.all_bps = all;
+ });
+}
+
+fn persist_breakpoints(ui_view: &str, bps: &[Bp]) {
+ if ui_view.is_empty() {
+ return;
+ }
+ let dir = project_root().join("Library").join("Unterm");
+ let path = dir.join("breakpoints.json");
+ let mut files: Vec = std::fs::read_to_string(&path)
+ .ok()
+ .and_then(|t| serde_json::from_str::(&t).ok())
+ .and_then(|v| v.get("files").and_then(|f| f.as_array()).cloned())
+ .unwrap_or_default();
+ let base = basename(ui_view);
+ let lines: Vec = bps
+ .iter()
+ .filter(|b| basename(&b.file) == base)
+ .map(|b| (b.line - 1) as i64)
+ .collect();
+ files.retain(|e| e.get("path").and_then(|p| p.as_str()) != Some(ui_view));
+ if !lines.is_empty() {
+ files.push(serde_json::json!({ "path": ui_view, "lines": lines }));
+ }
+ let out = serde_json::json!({ "files": files });
+ let _ = std::fs::create_dir_all(&dir);
+ let _ = std::fs::write(&path, serde_json::to_string(&out).unwrap_or_default());
+}
+
+fn running(state: &Arc>, proxy: &EventLoopProxy) {
+ set(state, proxy, |s| {
+ s.stopped = false;
+ s.threads.clear();
+ s.cur_thread = 0;
+ s.cur_frame = 0;
+ s.stack.clear();
+ s.locals.clear();
+ s.members.clear();
+ s.this_label.clear();
+ s.children.clear();
+ });
+}
+
+/// (Re)subscribe to TYPE_LOAD for the files that currently hold breakpoints, clearing
+/// any previous subscription so a newly-added breakpoint's file gets watched too.
+fn rewatch(conn: &mut sdb::Connection, bps: &[Bp], watch_req: &mut Option) {
+ if let Some(req) = watch_req.take() {
+ conn.clear_event(wire::kind::TYPE_LOAD, req).ok();
+ }
+ let mut files: Vec = bps.iter().map(|b| b.file.clone()).collect();
+ files.sort();
+ files.dedup();
+ if !files.is_empty() {
+ *watch_req = conn.watch_source_files(&files).ok();
+ }
+}
+
+/// Arm every not-yet-armed breakpoint against the types already loaded for its file.
+fn arm_loaded(
+ conn: &mut sdb::Connection,
+ bps: &mut [Bp],
+ state: &Arc>,
+ proxy: &EventLoopProxy,
+) {
+ for bp in bps.iter_mut() {
+ if bp.armed {
+ continue;
+ }
+ if let Ok(types) = conn.types_for_source_file(&bp.file, true) {
+ if !types.is_empty() {
+ if let Some(req) = arm(conn, &types, &bp.file, bp.line, state, proxy) {
+ bp.armed = true;
+ bp.req = Some(req);
+ }
+ }
+ }
+ }
+}
+
+/// Reconcile our live breakpoints with the shared store (edited by the editor while
+/// we run): clear breakpoints dropped from the store and arm ones newly added, leaving
+/// unchanged ones (and their armed state) intact.
+fn reconcile_breakpoints(
+ conn: &mut sdb::Connection,
+ bps: &mut Vec,
+ state: &Arc>,
+ proxy: &EventLoopProxy,
+ ui_view: &str,
+) {
+ let Some(fresh) = load_breakpoints() else {
+ return; // unreadable store: keep what we have rather than wiping
+ };
+ // Drop breakpoints no longer in the store (clearing their agent request).
+ let mut i = 0;
+ while i < bps.len() {
+ if fresh.iter().any(|(f, l)| *f == bps[i].file && *l == bps[i].line) {
+ i += 1;
+ } else {
+ if let Some(req) = bps[i].req.take() {
+ conn.clear_event(wire::kind::BREAKPOINT, req).ok();
+ }
+ bps.remove(i);
+ }
+ }
+ // Arm breakpoints newly added to the store (against already-loaded types; ones
+ // whose type isn't loaded yet are picked up by the arm-retry / TYPE_LOAD watch).
+ for (file, line) in fresh {
+ if bps.iter().any(|b| b.file == file && b.line == line) {
+ continue;
+ }
+ let mut bp = Bp { file: file.clone(), line, armed: false, req: None };
+ if let Ok(types) = conn.types_for_source_file(&bp.file, true) {
+ if !types.is_empty() {
+ if let Some(req) = arm(conn, &types, &bp.file, bp.line, state, proxy) {
+ bp.armed = true;
+ bp.req = Some(req);
+ }
+ }
+ }
+ bps.push(bp);
+ }
+ sync_bps(state, proxy, bps, ui_view);
+}
+
+/// Read the shared breakpoint store as (basename, 1-based line) pairs. Returns `None`
+/// on a missing/unreadable file so a transient error can't wipe live breakpoints.
+fn load_breakpoints() -> Option> {
+ let path = project_root().join("Library").join("Unterm").join("breakpoints.json");
+ let text = std::fs::read_to_string(&path).ok()?;
+ let v: serde_json::Value = serde_json::from_str(&text).ok()?;
+ let mut out = Vec::new();
+ if let Some(files) = v.get("files").and_then(|f| f.as_array()) {
+ for e in files {
+ let Some(p) = e.get("path").and_then(|p| p.as_str()) else {
+ continue;
+ };
+ let base = basename(p).to_string();
+ if let Some(lines) = e.get("lines").and_then(|l| l.as_array()) {
+ for ln in lines.iter().filter_map(|l| l.as_i64()) {
+ out.push((base.clone(), ln as i32 + 1)); // store is 0-based
+ }
+ }
+ }
+ }
+ Some(out)
+}
+
+fn arm(
+ conn: &mut sdb::Connection,
+ types: &[u32],
+ file: &str,
+ line: i32,
+ state: &Arc>,
+ proxy: &EventLoopProxy,
+) -> Option {
+ let mut candidates = Vec::new();
+ for &t in types {
+ let Ok(methods) = conn.type_methods(t) else {
+ continue;
+ };
+ for m in methods {
+ if let Ok(info) = conn.debug_info(m) {
+ candidates.push((m, info));
+ }
+ }
+ }
+ let (method, il) = sdb::resolve_line(&candidates, file, line)?;
+ match conn.set_breakpoint(method, il) {
+ Ok(req) => {
+ let name = conn.method_name(method).unwrap_or_default();
+ set(state, proxy, |s| s.push_log(format!("breakpoint: {name}+0x{il:x}")));
+ Some(req)
+ }
+ Err(_) => None,
+ }
+}
+
+/// Enumerate the managed threads (those with a resolvable source frame) for the
+/// Threads strip, labelling each with its name and topmost source location.
+fn collect_threads(conn: &mut sdb::Connection) -> Vec {
+ let mut out = Vec::new();
+ for id in conn.all_threads().unwrap_or_default() {
+ let frames = conn.frames(id).unwrap_or_default();
+ let top = frames.iter().find_map(|f| {
+ conn.debug_info(f.method)
+ .ok()
+ .and_then(|info| il_to_source(&info, f.il_offset))
+ .filter(|(_, ln)| *ln > 0)
+ .map(|(src, ln)| (f.method, src, ln))
+ });
+ let Some((method, src, ln)) = top else {
+ continue; // native/threadpool thread with no managed frame: skip
+ };
+ let raw = conn.thread_name(id).unwrap_or_default();
+ let name = if raw.is_empty() { format!("Thread {id}") } else { raw };
+ let mname = conn.method_name(method).unwrap_or_default();
+ out.push(ThreadInfo {
+ id,
+ name,
+ location: format!("{mname} {}:{ln}", basename(&src)),
+ });
+ }
+ out
+}
+
+fn dump(conn: &mut sdb::Connection, thread: u32) -> Stop {
+ let frames = conn.frames(thread).unwrap_or_default();
+ let mut stack = Vec::new();
+ let mut file = String::new();
+ let mut line = 0;
+ for (i, f) in frames.iter().enumerate().take(20) {
+ let name = conn.method_name(f.method).unwrap_or_default();
+ let (src, ln) = conn
+ .debug_info(f.method)
+ .ok()
+ .and_then(|info| il_to_source(&info, f.il_offset))
+ .unwrap_or_default();
+ if i == 0 {
+ file = src.clone();
+ line = ln;
+ }
+ let loc = if ln > 0 { format!("{}:{ln}", basename(&src)) } else { String::new() };
+ stack.push(format!("#{i} {name} {loc}"));
+ }
+
+ let (locals, members, this_label) = match frames.first() {
+ Some(top) => frame_vars(conn, thread, top),
+ None => (Vec::new(), Vec::new(), String::new()),
+ };
+ Stop { stack, frames, locals, members, this_label, file, line }
+}
+
+/// Locals, `this` members, and the `this` type label for a single call-stack frame.
+fn frame_vars(
+ conn: &mut sdb::Connection,
+ thread: u32,
+ frame: &sdb::Frame,
+) -> (Vec, Vec, String) {
+ let mut locals = Vec::new();
+ if let Ok(infos) = conn.locals_info(frame.method) {
+ let in_scope: Vec<(usize, sdb::Local)> = infos
+ .into_iter()
+ .enumerate()
+ .filter(|(_, l)| frame.il_offset >= l.live_start && frame.il_offset < l.live_end)
+ .collect();
+ let positions: Vec = in_scope.iter().map(|(i, _)| *i as i32).collect();
+ if let Ok(values) = conn.frame_values(thread, frame.id, &positions) {
+ for ((_, l), val) in in_scope.iter().zip(values) {
+ locals.push(make_var(conn, l.name.clone(), &val));
+ }
+ }
+ }
+
+ let mut members = Vec::new();
+ let mut this_label = String::new();
+ if let Ok(value::Value::Object { id, .. }) = conn.frame_this(thread, frame.id) {
+ if id != 0 {
+ if let Ok(ty) = conn.object_type(id) {
+ this_label = conn.type_name(ty).unwrap_or_default();
+ }
+ members = fetch_object_fields(conn, id);
+ }
+ }
+ (locals, members, this_label)
+}
+
+/// Build a variable row, tagging it as expandable when it's a non-null object/array.
+fn make_var(conn: &mut sdb::Connection, name: String, val: &value::Value) -> Var {
+ let expand = match val {
+ value::Value::Object { tag, id } if *id != 0 => Some(Expand {
+ id: *id,
+ array: *tag == value::tag::ARRAY || *tag == value::tag::SZARRAY,
+ }),
+ _ => None,
+ };
+ Var {
+ name,
+ value: render_value(conn, val),
+ expand,
+ }
+}
+
+/// The instance fields of an object as variable rows.
+fn fetch_object_fields(conn: &mut sdb::Connection, id: u32) -> Vec {
+ let mut out = Vec::new();
+ let Ok(mut ty) = conn.object_type(id) else {
+ return out;
+ };
+ // Walk the type hierarchy so inherited (base-class) instance fields show too —
+ // `TYPE.GET_FIELDS` returns only the fields declared on each type. Derived fields
+ // come first, then each base class up to (but not including) System.Object.
+ for _ in 0..32 {
+ if let Ok(fields) = conn.type_fields(ty) {
+ let inst: Vec = fields.into_iter().filter(|f| !f.is_static()).collect();
+ let ids: Vec = inst.iter().map(|f| f.id).collect();
+ if !ids.is_empty() {
+ if let Ok(vals) = conn.object_values(id, &ids) {
+ for (f, val) in inst.iter().zip(vals) {
+ out.push(make_var(conn, f.name.clone(), &val));
+ }
+ }
+ }
+ }
+ match conn.type_base(ty) {
+ Ok(base) if base != 0 => ty = base,
+ _ => break,
+ }
+ }
+ out
+}
+
+/// The elements of an array as variable rows (capped, with an ellipsis overflow row).
+fn fetch_array(conn: &mut sdb::Connection, id: u32) -> Vec {
+ const CAP: i32 = 500;
+ let len = conn.array_length(id).unwrap_or(0).max(0);
+ let n = len.min(CAP);
+ let mut out = Vec::new();
+ if n > 0 {
+ if let Ok(vals) = conn.array_values(id, 0, n) {
+ for (i, val) in vals.iter().enumerate() {
+ out.push(make_var(conn, format!("[{i}]"), val));
+ }
+ }
+ }
+ if len > n {
+ out.push(Var {
+ name: format!("… {} more", len - n),
+ value: String::new(),
+ expand: None,
+ });
+ }
+ out
+}
+
+fn render_value(conn: &mut sdb::Connection, v: &value::Value) -> String {
+ use value::Value;
+ match v {
+ Value::String(0) => "null".into(),
+ Value::String(id) => format!("\"{}\"", conn.string_value(*id).unwrap_or_default()),
+ Value::Object { id, .. } if *id == 0 => "null".into(),
+ Value::Object { id, .. } => conn
+ .object_type(*id)
+ .ok()
+ .and_then(|t| conn.type_name(t).ok())
+ .map(|n| format!("{{{n}}}"))
+ .unwrap_or_else(|| format!("obj#{id}")),
+ Value::ValueType { fields, .. } => format!(
+ "({})",
+ fields.iter().map(|f| render_value(conn, f)).collect::>().join(", ")
+ ),
+ other => other.summary(),
+ }
+}
+
+fn il_to_source(info: &sdb::DebugInfo, il: i32) -> Option<(String, i32)> {
+ let sp = info
+ .seq_points
+ .iter()
+ .filter(|s| !s.is_hidden() && s.il_offset <= il)
+ .max_by_key(|s| s.il_offset)?;
+ let src = info.sources.first().cloned().unwrap_or_default();
+ Some((src, sp.line))
+}
+
+fn basename(path: &str) -> &str {
+ path.rsplit(['/', '\\']).next().unwrap_or(path)
+}
+
+/// The C# identifier covering character column `col` in `line`, if any.
+fn word_at(line: &str, col: usize) -> Option {
+ let chars: Vec = line.chars().collect();
+ if col >= chars.len() {
+ return None;
+ }
+ let is_ident = |c: char| c.is_alphanumeric() || c == '_';
+ if !is_ident(chars[col]) {
+ return None;
+ }
+ let mut start = col;
+ while start > 0 && is_ident(chars[start - 1]) {
+ start -= 1;
+ }
+ let mut end = col;
+ while end + 1 < chars.len() && is_ident(chars[end + 1]) {
+ end += 1;
+ }
+ if chars[start].is_ascii_digit() {
+ return None;
+ }
+ Some(chars[start..=end].iter().collect())
+}
+
+/// Look up a variable name among the in-scope locals and `this` members.
+fn lookup_var<'a>(s: &'a UiState, name: &str) -> Option<&'a str> {
+ s.locals
+ .iter()
+ .chain(s.members.iter())
+ .find(|v| v.name == name)
+ .map(|v| v.value.as_str())
+}
+
+/// The Unity project root (the dir containing `Library/EditorInstance.json`), so the
+/// file tree and breakpoint store work regardless of the process's launch directory.
+fn project_root() -> std::path::PathBuf {
+ let cwd = std::env::current_dir().unwrap_or_default();
+ if let Some(inst) = sdb::find_editor_instance(&cwd) {
+ if let Some(root) = inst.parent().and_then(|p| p.parent()) {
+ return root.to_path_buf();
+ }
+ }
+ cwd
+}
+
+/// `Library/Unterm/` in this project (the editor and debugger's shared dir).
+fn unterm_state_path(name: &str) -> std::path::PathBuf {
+ project_root().join("Library").join("Unterm").join(name)
+}
+
+/// Publish our PID so the editor launcher can detect (per project, via the path of
+/// this file) that this project's debugger is already running.
+fn write_pid_file() {
+ let p = unterm_state_path("debugger.pid");
+ if let Some(dir) = p.parent() {
+ let _ = std::fs::create_dir_all(dir);
+ }
+ let _ = std::fs::write(p, std::process::id().to_string());
+}
+
+/// The editor's process id (from `Library/EditorInstance.json`), to recognise it among
+/// activating apps and among the on-screen window list.
+#[cfg(any(target_os = "macos", target_os = "windows"))]
+fn editor_pid() -> Option {
+ let inst = sdb::find_editor_instance(&std::env::current_dir().ok()?)?;
+ let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(inst).ok()?).ok()?;
+ json.get("process_id").and_then(|v| v.as_i64()).map(|v| v as i32)
+}
+
+/// Observe `NSWorkspace` app-activation; when our editor becomes frontmost, post
+/// `Wake::Raise`. Runs in the debugger's own run loop, so it fires even while the
+/// editor is frozen (suspended at a breakpoint).
+#[cfg(target_os = "macos")]
+fn install_activation_observer(proxy: EventLoopProxy) {
+ use objc2::{class, msg_send, runtime::AnyObject};
+ use objc2_app_kit::NSWorkspaceDidActivateApplicationNotification;
+ let Some(epid) = editor_pid() else {
+ return; // no editor to follow
+ };
+ unsafe {
+ let ws: *mut AnyObject = msg_send![class!(NSWorkspace), sharedWorkspace];
+ if ws.is_null() {
+ return;
+ }
+ let center: *mut AnyObject = msg_send![ws, notificationCenter];
+ if center.is_null() {
+ return;
+ }
+ let block = block2::RcBlock::new(move |_notif: *mut AnyObject| {
+ let ws: *mut AnyObject = msg_send![class!(NSWorkspace), sharedWorkspace];
+ if ws.is_null() {
+ return;
+ }
+ let front: *mut AnyObject = msg_send![ws, frontmostApplication];
+ if front.is_null() {
+ return;
+ }
+ let pid: i32 = msg_send![front, processIdentifier];
+ if pid == epid {
+ proxy.send_event(Wake::Raise).ok();
+ }
+ });
+ let null = std::ptr::null::();
+ // The center copies the block, so it may outlive this scope's `block`.
+ let _: *mut AnyObject = msg_send![
+ center,
+ addObserverForName: NSWorkspaceDidActivateApplicationNotification,
+ object: null,
+ queue: null,
+ usingBlock: &*block,
+ ];
+ }
+}
+
+/// Slide our window to just below the editor's frontmost window — above other apps, but
+/// not covering the editor (so no covering-flicker) and without stealing focus.
+#[cfg(target_os = "macos")]
+fn order_behind_editor(window: &Window) {
+ use objc2::{msg_send, runtime::AnyObject};
+ use raw_window_handle::{HasWindowHandle, RawWindowHandle};
+ let Some(pid) = editor_pid() else {
+ return;
+ };
+ let Some(editor_win) = cgwindow::frontmost_window(pid) else {
+ return;
+ };
+ let Ok(handle) = window.window_handle() else {
+ return;
+ };
+ if let RawWindowHandle::AppKit(a) = handle.as_raw() {
+ unsafe {
+ let ns_view = a.ns_view.as_ptr() as *mut AnyObject;
+ let ns_window: *mut AnyObject = msg_send![ns_view, window];
+ if !ns_window.is_null() {
+ // orderWindow:NSWindowBelow(-1) relativeTo:.
+ let below: isize = -1;
+ let _: () =
+ msg_send![ns_window, orderWindow: below, relativeTo: editor_win as isize];
+ }
+ }
+ }
+}
+
+/// Minimal CoreGraphics window-list FFI: find a process's frontmost on-screen window
+/// number. Window number + owner pid + layer need no screen-recording permission.
+#[cfg(target_os = "macos")]
+mod cgwindow {
+ use core_foundation_sys::array::{CFArrayGetCount, CFArrayGetValueAtIndex, CFArrayRef};
+ use core_foundation_sys::base::{CFRelease, CFTypeRef};
+ use core_foundation_sys::dictionary::{CFDictionaryGetValueIfPresent, CFDictionaryRef};
+ use core_foundation_sys::number::{kCFNumberSInt64Type, CFNumberGetValue, CFNumberRef};
+ use core_foundation_sys::string::CFStringRef;
+ use std::ffi::c_void;
+
+ #[link(name = "CoreGraphics", kind = "framework")]
+ extern "C" {
+ fn CGWindowListCopyWindowInfo(option: u32, relative_to: u32) -> CFArrayRef;
+ static kCGWindowOwnerPID: CFStringRef;
+ static kCGWindowNumber: CFStringRef;
+ static kCGWindowLayer: CFStringRef;
+ }
+ const ON_SCREEN_ONLY: u32 = 1 << 0;
+ const NULL_WINDOW: u32 = 0;
+
+ unsafe fn dict_i64(dict: CFDictionaryRef, key: CFStringRef) -> Option {
+ let mut val: *const c_void = std::ptr::null();
+ if CFDictionaryGetValueIfPresent(dict, key as *const c_void, &mut val) == 0 || val.is_null()
+ {
+ return None;
+ }
+ let mut out: i64 = 0;
+ if CFNumberGetValue(
+ val as CFNumberRef,
+ kCFNumberSInt64Type,
+ &mut out as *mut i64 as *mut c_void,
+ ) {
+ Some(out)
+ } else {
+ None
+ }
+ }
+
+ pub fn frontmost_window(pid: i32) -> Option {
+ unsafe {
+ let arr = CGWindowListCopyWindowInfo(ON_SCREEN_ONLY, NULL_WINDOW);
+ if arr.is_null() {
+ return None;
+ }
+ let mut found = None;
+ // The list is front-to-back, so the first match is the frontmost window.
+ for i in 0..CFArrayGetCount(arr) {
+ let dict = CFArrayGetValueAtIndex(arr, i) as CFDictionaryRef;
+ if dict.is_null() {
+ continue;
+ }
+ // Only normal-level windows (layer 0); skip menus/overlays/etc.
+ if dict_i64(dict, kCGWindowOwnerPID) == Some(pid as i64)
+ && dict_i64(dict, kCGWindowLayer) == Some(0)
+ {
+ found = dict_i64(dict, kCGWindowNumber);
+ break;
+ }
+ }
+ CFRelease(arr as CFTypeRef);
+ found
+ }
+ }
+}
+
+// --- Windows: same "follow the editor forward" via a foreground WinEvent hook ---
+
+#[cfg(windows)]
+static WIN_EDITOR_PID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
+#[cfg(windows)]
+static WIN_PROXY: Mutex