mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-06 22:20:52 +00:00
Merge remote-tracking branch 'upstream/main' into grapheme-width-changes
This commit is contained in:
2
.github/workflows/nix.yml
vendored
2
.github/workflows/nix.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
|
||||
2
.github/workflows/release-tag.yml
vendored
2
.github/workflows/release-tag.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
|
||||
2
.github/workflows/release-tip.yml
vendored
2
.github/workflows/release-tip.yml
vendored
@@ -161,7 +161,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
|
||||
2
.github/workflows/snap.yml
vendored
2
.github/workflows/snap.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
|
||||
50
.github/workflows/test.yml
vendored
50
.github/workflows/test.yml
vendored
@@ -77,7 +77,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -187,7 +187,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -267,7 +267,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -296,7 +296,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -329,7 +329,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -375,7 +375,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -604,7 +604,7 @@ jobs:
|
||||
echo "version=$(sed -n -E 's/^\s*\.?minimum_zig_version\s*=\s*"([^"]+)".*/\1/p' build.zig.zon)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -646,7 +646,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -694,7 +694,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -729,7 +729,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -793,7 +793,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -822,7 +822,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -852,7 +852,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -881,7 +881,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -908,7 +908,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -935,7 +935,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -962,7 +962,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -994,7 +994,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -1021,7 +1021,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -1056,7 +1056,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -1085,7 +1085,7 @@ jobs:
|
||||
uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10
|
||||
|
||||
- name: Configure Namespace powered Buildx
|
||||
uses: namespacelabs/nscloud-setup-buildx-action@a7e525416136ee2842da3c800e7067b72a27200e # v0.0.21
|
||||
uses: namespacelabs/nscloud-setup-buildx-action@f5814dcf37a16cce0624d5bec2ab879654294aa0 # v0.0.22
|
||||
|
||||
- name: Download Source Tarball Artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
@@ -1118,7 +1118,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
|
||||
2
.github/workflows/update-colorschemes.yml
vendored
2
.github/workflows/update-colorschemes.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@a4cc4697b9de3b562bec5a856e6f3c31f94506e2 # v1.3.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
|
||||
@@ -116,8 +116,8 @@
|
||||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz",
|
||||
.hash = "N-V-__8AAIdIAwDt5PxH-cwCxEcTfw4jBV8sR6fZ_XLh-cR7",
|
||||
.url = "https://deps.files.ghostty.org/ghostty-themes-release-20260126-150817-02c580d.tgz",
|
||||
.hash = "N-V-__8AAM5RAwB5jHC1P1uqrabiz_ieQvfrIYNzs4eAY__c",
|
||||
.lazy = true,
|
||||
},
|
||||
},
|
||||
|
||||
6
build.zig.zon.json
generated
6
build.zig.zon.json
generated
@@ -54,10 +54,10 @@
|
||||
"url": "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz",
|
||||
"hash": "sha256-yBbCDox18+Fa6Gc1DnmSVQLRpqhZOLsac7iSfl8x+cs="
|
||||
},
|
||||
"N-V-__8AAIdIAwDt5PxH-cwCxEcTfw4jBV8sR6fZ_XLh-cR7": {
|
||||
"N-V-__8AAM5RAwB5jHC1P1uqrabiz_ieQvfrIYNzs4eAY__c": {
|
||||
"name": "iterm2_themes",
|
||||
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz",
|
||||
"hash": "sha256-NIqF12KqXhIrP+LyBtg6WtkHxNUdWOyziAdq8S45RrU="
|
||||
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260126-150817-02c580d.tgz",
|
||||
"hash": "sha256-jWbZAS2GSIH8kGdafJua81OMJY8djEQUhvTtmnT90oI="
|
||||
},
|
||||
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
|
||||
"name": "jetbrains_mono",
|
||||
|
||||
6
build.zig.zon.nix
generated
6
build.zig.zon.nix
generated
@@ -171,11 +171,11 @@ in
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "N-V-__8AAIdIAwDt5PxH-cwCxEcTfw4jBV8sR6fZ_XLh-cR7";
|
||||
name = "N-V-__8AAM5RAwB5jHC1P1uqrabiz_ieQvfrIYNzs4eAY__c";
|
||||
path = fetchZigArtifact {
|
||||
name = "iterm2_themes";
|
||||
url = "https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz";
|
||||
hash = "sha256-NIqF12KqXhIrP+LyBtg6WtkHxNUdWOyziAdq8S45RrU=";
|
||||
url = "https://deps.files.ghostty.org/ghostty-themes-release-20260126-150817-02c580d.tgz";
|
||||
hash = "sha256-jWbZAS2GSIH8kGdafJua81OMJY8djEQUhvTtmnT90oI=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
||||
2
build.zig.zon.txt
generated
2
build.zig.zon.txt
generated
@@ -6,7 +6,7 @@ https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918
|
||||
https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz
|
||||
https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz
|
||||
https://deps.files.ghostty.org/gettext-0.24.tar.gz
|
||||
https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz
|
||||
https://deps.files.ghostty.org/ghostty-themes-release-20260126-150817-02c580d.tgz
|
||||
https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz
|
||||
https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst
|
||||
https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
formatter = forAllPlatforms (pkgs: pkgs.alejandra);
|
||||
|
||||
apps = forBuildablePlatforms (pkgs: let
|
||||
runVM = module: desc: let
|
||||
runVM = module: let
|
||||
vm = import ./nix/vm/create.nix {
|
||||
inherit (pkgs.stdenv.hostPlatform) system;
|
||||
inherit module nixpkgs;
|
||||
|
||||
@@ -67,9 +67,9 @@
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz",
|
||||
"dest": "vendor/p/N-V-__8AAIdIAwDt5PxH-cwCxEcTfw4jBV8sR6fZ_XLh-cR7",
|
||||
"sha256": "348a85d762aa5e122b3fe2f206d83a5ad907c4d51d58ecb388076af12e3946b5"
|
||||
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260126-150817-02c580d.tgz",
|
||||
"dest": "vendor/p/N-V-__8AAM5RAwB5jHC1P1uqrabiz_ieQvfrIYNzs4eAY__c",
|
||||
"sha256": "8d66d9012d864881fc90675a7c9b9af3538c258f1d8c441486f4ed9a74fdd282"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
||||
@@ -1335,7 +1335,7 @@ extension Ghostty {
|
||||
mode: ghostty_action_inspector_e) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("toggle split zoom does nothing with an app target")
|
||||
Ghostty.logger.warning("toggle inspector does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
|
||||
100
macos/Sources/Ghostty/Ghostty.Inspector.swift
Normal file
100
macos/Sources/Ghostty/Ghostty.Inspector.swift
Normal file
@@ -0,0 +1,100 @@
|
||||
import GhosttyKit
|
||||
import Metal
|
||||
|
||||
extension Ghostty {
|
||||
/// Represents the inspector for a surface within Ghostty.
|
||||
///
|
||||
/// Wraps a `ghostty_inspector_t`
|
||||
final class Inspector: Sendable {
|
||||
private let inspector: ghostty_inspector_t
|
||||
|
||||
/// Read the underlying C value for this inspector. This is unsafe because the value will be
|
||||
/// freed when the Inspector class is deinitialized.
|
||||
var unsafeCValue: ghostty_inspector_t {
|
||||
inspector
|
||||
}
|
||||
|
||||
/// Initialize from the C structure.
|
||||
init(cInspector: ghostty_inspector_t) {
|
||||
self.inspector = cInspector
|
||||
}
|
||||
|
||||
/// Set the focus state of the inspector.
|
||||
@MainActor
|
||||
func setFocus(_ focused: Bool) {
|
||||
ghostty_inspector_set_focus(inspector, focused)
|
||||
}
|
||||
|
||||
/// Set the content scale of the inspector.
|
||||
@MainActor
|
||||
func setContentScale(x: Double, y: Double) {
|
||||
ghostty_inspector_set_content_scale(inspector, x, y)
|
||||
}
|
||||
|
||||
/// Set the size of the inspector.
|
||||
@MainActor
|
||||
func setSize(width: UInt32, height: UInt32) {
|
||||
ghostty_inspector_set_size(inspector, width, height)
|
||||
}
|
||||
|
||||
/// Send a mouse button event to the inspector.
|
||||
@MainActor
|
||||
func mouseButton(
|
||||
_ state: ghostty_input_mouse_state_e,
|
||||
button: ghostty_input_mouse_button_e,
|
||||
mods: ghostty_input_mods_e
|
||||
) {
|
||||
ghostty_inspector_mouse_button(inspector, state, button, mods)
|
||||
}
|
||||
|
||||
/// Send a mouse position event to the inspector.
|
||||
@MainActor
|
||||
func mousePos(x: Double, y: Double) {
|
||||
ghostty_inspector_mouse_pos(inspector, x, y)
|
||||
}
|
||||
|
||||
/// Send a mouse scroll event to the inspector.
|
||||
@MainActor
|
||||
func mouseScroll(x: Double, y: Double, mods: ghostty_input_scroll_mods_t) {
|
||||
ghostty_inspector_mouse_scroll(inspector, x, y, mods)
|
||||
}
|
||||
|
||||
/// Send a key event to the inspector.
|
||||
@MainActor
|
||||
func key(
|
||||
_ action: ghostty_input_action_e,
|
||||
key: ghostty_input_key_e,
|
||||
mods: ghostty_input_mods_e
|
||||
) {
|
||||
ghostty_inspector_key(inspector, action, key, mods)
|
||||
}
|
||||
|
||||
/// Send text to the inspector.
|
||||
@MainActor
|
||||
func text(_ text: String) {
|
||||
text.withCString { ptr in
|
||||
ghostty_inspector_text(inspector, ptr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize Metal rendering for the inspector.
|
||||
@MainActor
|
||||
func metalInit(device: MTLDevice) -> Bool {
|
||||
let devicePtr = Unmanaged.passRetained(device).toOpaque()
|
||||
return ghostty_inspector_metal_init(inspector, devicePtr)
|
||||
}
|
||||
|
||||
/// Render the inspector using Metal.
|
||||
@MainActor
|
||||
func metalRender(
|
||||
commandBuffer: MTLCommandBuffer,
|
||||
descriptor: MTLRenderPassDescriptor
|
||||
) {
|
||||
ghostty_inspector_metal_render(
|
||||
inspector,
|
||||
Unmanaged.passRetained(commandBuffer).toOpaque(),
|
||||
Unmanaged.passRetained(descriptor).toOpaque()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ extension Ghostty {
|
||||
didSet { surfaceViewDidChange() }
|
||||
}
|
||||
|
||||
private var inspector: ghostty_inspector_t? {
|
||||
private var inspector: Ghostty.Inspector? {
|
||||
guard let surfaceView = self.surfaceView else { return nil }
|
||||
return surfaceView.inspector
|
||||
}
|
||||
@@ -120,9 +120,9 @@ extension Ghostty {
|
||||
self.commandQueue = commandQueue
|
||||
super.init(frame: frame, device: device)
|
||||
|
||||
// This makes it so renders only happen when we request
|
||||
self.enableSetNeedsDisplay = true
|
||||
self.isPaused = true
|
||||
// Use timed updates mode. This is required for the inspector.
|
||||
self.isPaused = false
|
||||
self.preferredFramesPerSecond = 30
|
||||
|
||||
// After initializing the parent we can set our own properties
|
||||
self.device = MTLCreateSystemDefaultDevice()
|
||||
@@ -130,6 +130,13 @@ extension Ghostty {
|
||||
|
||||
// Setup our tracking areas for mouse events
|
||||
updateTrackingAreas()
|
||||
|
||||
// Observe occlusion state to pause rendering when not visible
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(windowDidChangeOcclusionState),
|
||||
name: NSWindow.didChangeOcclusionStateNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
@@ -141,28 +148,19 @@ extension Ghostty {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
@objc private func windowDidChangeOcclusionState(_ notification: NSNotification) {
|
||||
guard let window = notification.object as? NSWindow,
|
||||
window == self.window else { return }
|
||||
// Pause rendering when our window isn't visible.
|
||||
isPaused = !window.occlusionState.contains(.visible)
|
||||
}
|
||||
|
||||
// MARK: Internal Inspector Funcs
|
||||
|
||||
private func surfaceViewDidChange() {
|
||||
let center = NotificationCenter.default
|
||||
center.removeObserver(self)
|
||||
|
||||
guard let surfaceView = self.surfaceView else { return }
|
||||
guard let inspector = self.inspector else { return }
|
||||
guard let device = self.device else { return }
|
||||
let devicePtr = Unmanaged.passRetained(device).toOpaque()
|
||||
ghostty_inspector_metal_init(inspector, devicePtr)
|
||||
|
||||
// Register an observer for render requests
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(didRequestRender),
|
||||
name: Ghostty.Notification.inspectorNeedsDisplay,
|
||||
object: surfaceView)
|
||||
}
|
||||
|
||||
@objc private func didRequestRender(notification: SwiftUI.Notification) {
|
||||
self.needsDisplay = true
|
||||
_ = inspector.metalInit(device: device)
|
||||
}
|
||||
|
||||
private func updateSize() {
|
||||
@@ -172,10 +170,10 @@ extension Ghostty {
|
||||
let fbFrame = self.convertToBacking(self.frame)
|
||||
let xScale = fbFrame.size.width / self.frame.size.width
|
||||
let yScale = fbFrame.size.height / self.frame.size.height
|
||||
ghostty_inspector_set_content_scale(inspector, xScale, yScale)
|
||||
inspector.setContentScale(x: xScale, y: yScale)
|
||||
|
||||
// When our scale factor changes, so does our fb size so we send that too
|
||||
ghostty_inspector_set_size(inspector, UInt32(fbFrame.size.width), UInt32(fbFrame.size.height))
|
||||
inspector.setSize(width: UInt32(fbFrame.size.width), height: UInt32(fbFrame.size.height))
|
||||
}
|
||||
|
||||
// MARK: NSView
|
||||
@@ -184,7 +182,7 @@ extension Ghostty {
|
||||
let result = super.becomeFirstResponder()
|
||||
if (result) {
|
||||
if let inspector = self.inspector {
|
||||
ghostty_inspector_set_focus(inspector, true)
|
||||
inspector.setFocus(true)
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -194,7 +192,7 @@ extension Ghostty {
|
||||
let result = super.resignFirstResponder()
|
||||
if (result) {
|
||||
if let inspector = self.inspector {
|
||||
ghostty_inspector_set_focus(inspector, false)
|
||||
inspector.setFocus(false)
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -229,25 +227,25 @@ extension Ghostty {
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
guard let inspector = self.inspector else { return }
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_inspector_mouse_button(inspector, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_LEFT, mods)
|
||||
inspector.mouseButton(GHOSTTY_MOUSE_PRESS, button: GHOSTTY_MOUSE_LEFT, mods: mods)
|
||||
}
|
||||
|
||||
override func mouseUp(with event: NSEvent) {
|
||||
guard let inspector = self.inspector else { return }
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_inspector_mouse_button(inspector, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_LEFT, mods)
|
||||
inspector.mouseButton(GHOSTTY_MOUSE_RELEASE, button: GHOSTTY_MOUSE_LEFT, mods: mods)
|
||||
}
|
||||
|
||||
override func rightMouseDown(with event: NSEvent) {
|
||||
guard let inspector = self.inspector else { return }
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_inspector_mouse_button(inspector, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_RIGHT, mods)
|
||||
inspector.mouseButton(GHOSTTY_MOUSE_PRESS, button: GHOSTTY_MOUSE_RIGHT, mods: mods)
|
||||
}
|
||||
|
||||
override func rightMouseUp(with event: NSEvent) {
|
||||
guard let inspector = self.inspector else { return }
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_inspector_mouse_button(inspector, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods)
|
||||
inspector.mouseButton(GHOSTTY_MOUSE_RELEASE, button: GHOSTTY_MOUSE_RIGHT, mods: mods)
|
||||
}
|
||||
|
||||
override func mouseMoved(with event: NSEvent) {
|
||||
@@ -255,7 +253,7 @@ extension Ghostty {
|
||||
|
||||
// Convert window position to view position. Note (0, 0) is bottom left.
|
||||
let pos = self.convert(event.locationInWindow, from: nil)
|
||||
ghostty_inspector_mouse_pos(inspector, pos.x, frame.height - pos.y)
|
||||
inspector.mousePos(x: pos.x, y: frame.height - pos.y)
|
||||
|
||||
}
|
||||
|
||||
@@ -297,7 +295,7 @@ extension Ghostty {
|
||||
// Pack our momentum value into the mods bitmask
|
||||
mods |= Int32(momentum.rawValue) << 1
|
||||
|
||||
ghostty_inspector_mouse_scroll(inspector, x, y, mods)
|
||||
inspector.mouseScroll(x: x, y: y, mods: mods)
|
||||
}
|
||||
|
||||
override func keyDown(with event: NSEvent) {
|
||||
@@ -336,7 +334,7 @@ extension Ghostty {
|
||||
guard let inspector = self.inspector else { return }
|
||||
guard let key = Ghostty.Input.Key(keyCode: event.keyCode) else { return }
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_inspector_key(inspector, action, key.cKey, mods)
|
||||
inspector.key(action, key: key.cKey, mods: mods)
|
||||
}
|
||||
|
||||
// MARK: NSTextInputClient
|
||||
@@ -406,9 +404,7 @@ extension Ghostty {
|
||||
let len = chars.utf8CString.count
|
||||
if (len == 0) { return }
|
||||
|
||||
chars.withCString { ptr in
|
||||
ghostty_inspector_text(inspector, ptr)
|
||||
}
|
||||
inspector.text(chars)
|
||||
}
|
||||
|
||||
override func doCommand(by selector: Selector) {
|
||||
@@ -435,11 +431,7 @@ extension Ghostty {
|
||||
updateSize()
|
||||
|
||||
// Render
|
||||
ghostty_inspector_metal_render(
|
||||
inspector,
|
||||
Unmanaged.passRetained(commandBuffer).toOpaque(),
|
||||
Unmanaged.passRetained(descriptor).toOpaque()
|
||||
)
|
||||
inspector.metalRender(commandBuffer: commandBuffer, descriptor: descriptor)
|
||||
|
||||
guard let drawable = self.currentDrawable else { return }
|
||||
commandBuffer.present(drawable)
|
||||
|
||||
@@ -173,10 +173,11 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
// Returns the inspector instance for this surface, or nil if the
|
||||
// surface has been closed.
|
||||
var inspector: ghostty_inspector_t? {
|
||||
// surface has been closed or no inspector is active.
|
||||
var inspector: Ghostty.Inspector? {
|
||||
guard let surface = self.surface else { return nil }
|
||||
return ghostty_surface_inspector(surface)
|
||||
guard let cInspector = ghostty_surface_inspector(surface) else { return nil }
|
||||
return Ghostty.Inspector(cInspector: cInspector)
|
||||
}
|
||||
|
||||
// True if the inspector should be visible
|
||||
|
||||
@@ -49,6 +49,7 @@ pub fn build(b: *std.Build) !void {
|
||||
var flags: std.ArrayList([]const u8) = .empty;
|
||||
defer flags.deinit(b.allocator);
|
||||
try flags.appendSlice(b.allocator, &.{
|
||||
"-DIMGUI_HAS_DOCK=1",
|
||||
"-DIMGUI_USE_WCHAR32=1",
|
||||
"-DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1",
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ pub const c = @cImport({
|
||||
// during import time to get the right types. Without this
|
||||
// you get stack size mismatches on some structs.
|
||||
@cDefine("IMGUI_USE_WCHAR32", "1");
|
||||
|
||||
@cDefine("IMGUI_HAS_DOCK", "1");
|
||||
@cInclude("dcimgui.h");
|
||||
});
|
||||
|
||||
@@ -25,11 +27,39 @@ pub extern fn ImGui_ImplOSX_Init(*anyopaque) callconv(.c) bool;
|
||||
pub extern fn ImGui_ImplOSX_Shutdown() callconv(.c) void;
|
||||
pub extern fn ImGui_ImplOSX_NewFrame(*anyopaque) callconv(.c) void;
|
||||
|
||||
// Internal API functions from dcimgui_internal.h
|
||||
// Internal API types and functions from dcimgui_internal.h
|
||||
// We declare these manually because the internal header contains bitfields
|
||||
// that Zig's cImport cannot translate.
|
||||
pub const ImGuiDockNodeFlagsPrivate = struct {
|
||||
pub const DockSpace: c.ImGuiDockNodeFlags = 1 << 10;
|
||||
pub const CentralNode: c.ImGuiDockNodeFlags = 1 << 11;
|
||||
pub const NoTabBar: c.ImGuiDockNodeFlags = 1 << 12;
|
||||
pub const HiddenTabBar: c.ImGuiDockNodeFlags = 1 << 13;
|
||||
pub const NoWindowMenuButton: c.ImGuiDockNodeFlags = 1 << 14;
|
||||
pub const NoCloseButton: c.ImGuiDockNodeFlags = 1 << 15;
|
||||
pub const NoResizeX: c.ImGuiDockNodeFlags = 1 << 16;
|
||||
pub const NoResizeY: c.ImGuiDockNodeFlags = 1 << 17;
|
||||
pub const DockedWindowsInFocusRoute: c.ImGuiDockNodeFlags = 1 << 18;
|
||||
pub const NoDockingSplitOther: c.ImGuiDockNodeFlags = 1 << 19;
|
||||
pub const NoDockingOverMe: c.ImGuiDockNodeFlags = 1 << 20;
|
||||
pub const NoDockingOverOther: c.ImGuiDockNodeFlags = 1 << 21;
|
||||
pub const NoDockingOverEmpty: c.ImGuiDockNodeFlags = 1 << 22;
|
||||
};
|
||||
pub extern fn ImGui_DockBuilderDockWindow(window_name: [*:0]const u8, node_id: c.ImGuiID) callconv(.c) void;
|
||||
pub extern fn ImGui_DockBuilderGetNode(node_id: c.ImGuiID) callconv(.c) ?*anyopaque;
|
||||
pub extern fn ImGui_DockBuilderGetCentralNode(node_id: c.ImGuiID) callconv(.c) ?*anyopaque;
|
||||
pub extern fn ImGui_DockBuilderAddNode() callconv(.c) c.ImGuiID;
|
||||
pub extern fn ImGui_DockBuilderAddNodeEx(node_id: c.ImGuiID, flags: c.ImGuiDockNodeFlags) callconv(.c) c.ImGuiID;
|
||||
pub extern fn ImGui_DockBuilderRemoveNode(node_id: c.ImGuiID) callconv(.c) void;
|
||||
pub extern fn ImGui_DockBuilderRemoveNodeDockedWindows(node_id: c.ImGuiID) callconv(.c) void;
|
||||
pub extern fn ImGui_DockBuilderRemoveNodeDockedWindowsEx(node_id: c.ImGuiID, clear_settings_refs: bool) callconv(.c) void;
|
||||
pub extern fn ImGui_DockBuilderRemoveNodeChildNodes(node_id: c.ImGuiID) callconv(.c) void;
|
||||
pub extern fn ImGui_DockBuilderSetNodePos(node_id: c.ImGuiID, pos: c.ImVec2) callconv(.c) void;
|
||||
pub extern fn ImGui_DockBuilderSetNodeSize(node_id: c.ImGuiID, size: c.ImVec2) callconv(.c) void;
|
||||
pub extern fn ImGui_DockBuilderSplitNode(node_id: c.ImGuiID, split_dir: c.ImGuiDir, size_ratio_for_node_at_dir: f32, out_id_at_dir: *c.ImGuiID, out_id_at_opposite_dir: *c.ImGuiID) callconv(.c) c.ImGuiID;
|
||||
pub extern fn ImGui_DockBuilderCopyDockSpace(src_dockspace_id: c.ImGuiID, dst_dockspace_id: c.ImGuiID, in_window_remap_pairs: *c.ImVector_const_charPtr) callconv(.c) void;
|
||||
pub extern fn ImGui_DockBuilderCopyNode(src_node_id: c.ImGuiID, dst_node_id: c.ImGuiID, out_node_remap_pairs: *c.ImVector_ImGuiID) callconv(.c) void;
|
||||
pub extern fn ImGui_DockBuilderCopyWindowSettings(src_name: [*:0]const u8, dst_name: [*:0]const u8) callconv(.c) void;
|
||||
pub extern fn ImGui_DockBuilderFinish(node_id: c.ImGuiID) callconv(.c) void;
|
||||
|
||||
// Extension functions from ext.cpp
|
||||
|
||||
18
src/App.zig
18
src/App.zig
@@ -237,14 +237,19 @@ pub fn needsConfirmQuit(self: *const App) bool {
|
||||
/// Drain the mailbox.
|
||||
fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
while (self.mailbox.pop()) |message| {
|
||||
log.debug("mailbox message={s}", .{@tagName(message)});
|
||||
if (comptime std.log.logEnabled(.debug, .app)) {
|
||||
switch (message) {
|
||||
// these tend to be way too verbose for normal debugging
|
||||
.redraw_surface => {},
|
||||
else => log.debug("mailbox message={t}", .{message}),
|
||||
}
|
||||
}
|
||||
switch (message) {
|
||||
.open_config => try self.performAction(rt_app, .open_config),
|
||||
.new_window => |msg| try self.newWindow(rt_app, msg),
|
||||
.close => |surface| self.closeSurface(surface),
|
||||
.surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message),
|
||||
.redraw_surface => |surface| try self.redrawSurface(rt_app, surface),
|
||||
.redraw_inspector => |surface| self.redrawInspector(rt_app, surface),
|
||||
|
||||
// If we're quitting, then we set the quit flag and stop
|
||||
// draining the mailbox immediately. This lets us defer
|
||||
@@ -283,11 +288,6 @@ fn redrawSurface(
|
||||
);
|
||||
}
|
||||
|
||||
fn redrawInspector(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) void {
|
||||
if (!self.hasRtSurface(surface)) return;
|
||||
rt_app.redrawInspector(surface);
|
||||
}
|
||||
|
||||
/// Create a new window
|
||||
pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
|
||||
const target: apprt.Target = target: {
|
||||
@@ -559,10 +559,6 @@ pub const Message = union(enum) {
|
||||
/// message if it needs to.
|
||||
redraw_surface: *apprt.Surface,
|
||||
|
||||
/// Redraw the inspector. This is called whenever some non-OS event
|
||||
/// causes the inspector to need to be redrawn.
|
||||
redraw_inspector: *apprt.Surface,
|
||||
|
||||
const NewWindow = struct {
|
||||
/// The parent surface
|
||||
parent: ?*Surface = null,
|
||||
|
||||
@@ -803,7 +803,7 @@ pub fn deinit(self: *Surface) void {
|
||||
self.io.deinit();
|
||||
|
||||
if (self.inspector) |v| {
|
||||
v.deinit();
|
||||
v.deinit(self.alloc);
|
||||
self.alloc.destroy(v);
|
||||
}
|
||||
|
||||
@@ -879,8 +879,10 @@ pub fn activateInspector(self: *Surface) !void {
|
||||
// Setup the inspector
|
||||
const ptr = try self.alloc.create(inspectorpkg.Inspector);
|
||||
errdefer self.alloc.destroy(ptr);
|
||||
ptr.* = try inspectorpkg.Inspector.init(self);
|
||||
ptr.* = try inspectorpkg.Inspector.init(self.alloc);
|
||||
errdefer ptr.deinit(self.alloc);
|
||||
self.inspector = ptr;
|
||||
errdefer self.inspector = null;
|
||||
|
||||
// Put the inspector onto the render state
|
||||
{
|
||||
@@ -912,7 +914,7 @@ pub fn deactivateInspector(self: *Surface) void {
|
||||
self.queueIo(.{ .inspector = false }, .unlocked);
|
||||
|
||||
// Deinit the inspector
|
||||
insp.deinit();
|
||||
insp.deinit(self.alloc);
|
||||
self.alloc.destroy(insp);
|
||||
self.inspector = null;
|
||||
}
|
||||
@@ -2618,7 +2620,7 @@ pub fn keyCallback(
|
||||
defer crash.sentry.thread_state = null;
|
||||
|
||||
// Setup our inspector event if we have an inspector.
|
||||
var insp_ev: ?inspectorpkg.key.Event = if (self.inspector != null) ev: {
|
||||
var insp_ev: ?inspectorpkg.KeyEvent = if (self.inspector != null) ev: {
|
||||
var copy = event;
|
||||
copy.utf8 = "";
|
||||
if (event.utf8.len > 0) copy.utf8 = try self.alloc.dupe(u8, event.utf8);
|
||||
@@ -2635,7 +2637,7 @@ pub fn keyCallback(
|
||||
break :ev;
|
||||
};
|
||||
|
||||
if (insp.recordKeyEvent(ev)) {
|
||||
if (insp.recordKeyEvent(self.alloc, ev)) {
|
||||
self.queueRender() catch {};
|
||||
} else |err| {
|
||||
log.warn("error adding key event to inspector err={}", .{err});
|
||||
@@ -2798,7 +2800,7 @@ pub fn keyCallback(
|
||||
fn maybeHandleBinding(
|
||||
self: *Surface,
|
||||
event: input.KeyEvent,
|
||||
insp_ev: ?*inspectorpkg.key.Event,
|
||||
insp_ev: ?*inspectorpkg.KeyEvent,
|
||||
) !?InputEffect {
|
||||
switch (event.action) {
|
||||
// Release events never trigger a binding but we need to check if
|
||||
@@ -3131,7 +3133,7 @@ fn endKeySequence(
|
||||
fn encodeKey(
|
||||
self: *Surface,
|
||||
event: input.KeyEvent,
|
||||
insp_ev: ?*inspectorpkg.key.Event,
|
||||
insp_ev: ?*inspectorpkg.KeyEvent,
|
||||
) !?termio.Message.WriteReq {
|
||||
const write_req: termio.Message.WriteReq = req: {
|
||||
// Build our encoding options, which requires the lock.
|
||||
@@ -3872,36 +3874,8 @@ pub fn mouseButtonCallback(
|
||||
// log.debug("mouse action={} button={} mods={}", .{ action, button, mods });
|
||||
|
||||
// If we have an inspector, we always queue a render
|
||||
if (self.inspector) |insp| {
|
||||
if (self.inspector != null) {
|
||||
defer self.queueRender() catch {};
|
||||
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// If the inspector is requesting a cell, then we intercept
|
||||
// left mouse clicks and send them to the inspector.
|
||||
if (insp.cell == .requested and
|
||||
button == .left and
|
||||
action == .press)
|
||||
{
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
const point = self.posToViewport(pos.x, pos.y);
|
||||
const screen: *terminal.Screen = self.renderer_state.terminal.screens.active;
|
||||
const p = screen.pages.pin(.{ .viewport = point }) orelse {
|
||||
log.warn("failed to get pin for clicked point", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
insp.cell.select(
|
||||
self.alloc,
|
||||
p,
|
||||
point.x,
|
||||
point.y,
|
||||
) catch |err| {
|
||||
log.warn("error selecting cell for inspector err={}", .{err});
|
||||
};
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Always record our latest mouse state
|
||||
@@ -4321,7 +4295,7 @@ fn clickMoveCursor(self: *Surface, to: terminal.Pin) !void {
|
||||
// This flag is only set if we've seen at least one semantic prompt
|
||||
// OSC sequence. If we've never seen that sequence, we can't possibly
|
||||
// move the cursor so we can fast path out of here.
|
||||
if (!t.flags.shell_redraws_prompt) return;
|
||||
if (!t.screens.active.flags.semantic_content) return;
|
||||
|
||||
// Get our path
|
||||
const from = t.screens.active.cursor.page_pin.*;
|
||||
|
||||
@@ -1061,7 +1061,7 @@ pub const Inspector = struct {
|
||||
render: {
|
||||
const surface = &self.surface.core_surface;
|
||||
const inspector = surface.inspector orelse break :render;
|
||||
inspector.render();
|
||||
inspector.render(surface);
|
||||
}
|
||||
|
||||
// Render
|
||||
|
||||
@@ -63,6 +63,12 @@ pub const ImguiWidget = extern struct {
|
||||
/// Our previous instant used to calculate delta time for animations.
|
||||
instant: ?std.time.Instant = null,
|
||||
|
||||
/// Tick callback ID for timed updates.
|
||||
tick_callback_id: c_uint = 0,
|
||||
|
||||
/// Last render time for throttling to 30 FPS.
|
||||
last_render_time: ?std.time.Instant = null,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
@@ -231,11 +237,26 @@ pub const ImguiWidget = extern struct {
|
||||
|
||||
// Call the virtual method to setup the UI.
|
||||
self.setup();
|
||||
|
||||
// Add a tick callback to drive timed updates via the frame clock.
|
||||
priv.tick_callback_id = self.as(gtk.Widget).addTickCallback(
|
||||
tickCallback,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle a request to unrealize the GLArea
|
||||
fn glAreaUnrealize(_: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
|
||||
assert(self.private().ig_context != null);
|
||||
const priv = self.private();
|
||||
assert(priv.ig_context != null);
|
||||
|
||||
// Remove the tick callback if it was registered.
|
||||
if (priv.tick_callback_id != 0) {
|
||||
self.as(gtk.Widget).removeTickCallback(priv.tick_callback_id);
|
||||
priv.tick_callback_id = 0;
|
||||
}
|
||||
|
||||
self.setCurrentContext() catch return;
|
||||
cimgui.ImGui_ImplOpenGL3_Shutdown();
|
||||
}
|
||||
@@ -265,6 +286,10 @@ pub const ImguiWidget = extern struct {
|
||||
fn glAreaRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Self) callconv(.c) c_int {
|
||||
self.setCurrentContext() catch return @intFromBool(false);
|
||||
|
||||
// Update last render time for tick callback throttling.
|
||||
const priv = self.private();
|
||||
priv.last_render_time = std.time.Instant.now() catch null;
|
||||
|
||||
// Setup our frame. We render twice because some ImGui behaviors
|
||||
// take multiple renders to process. I don't know how to make this
|
||||
// more efficient.
|
||||
@@ -411,6 +436,34 @@ pub const ImguiWidget = extern struct {
|
||||
cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes);
|
||||
}
|
||||
|
||||
/// Tick callback for timed updates. This drives periodic redraws.
|
||||
/// Redraws are limited to 30 FPS max since our imgui widgets don't
|
||||
/// usually need higher frame rates than that.
|
||||
fn tickCallback(
|
||||
widget: *gtk.Widget,
|
||||
_: *gdk.FrameClock,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) c_int {
|
||||
const self: *Self = gobject.ext.cast(Self, widget) orelse return 0;
|
||||
const priv = self.private();
|
||||
|
||||
const now = std.time.Instant.now() catch {
|
||||
self.queueRender();
|
||||
return 1;
|
||||
};
|
||||
|
||||
// Throttle to 30 FPS (~33ms between frames)
|
||||
const frame_time_ns: u64 = std.time.ns_per_s / 30;
|
||||
const should_render = if (priv.last_render_time) |last|
|
||||
now.since(last) >= frame_time_ns
|
||||
else
|
||||
true;
|
||||
|
||||
if (should_render) self.queueRender();
|
||||
|
||||
return 1; // Continue the tick callback
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Default virtual method handlers
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ pub const InspectorWidget = extern struct {
|
||||
const surface = priv.surface orelse return;
|
||||
const core_surface = surface.core() orelse return;
|
||||
const inspector = core_surface.inspector orelse return;
|
||||
inspector.render();
|
||||
inspector.render(core_surface);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
|
||||
12
src/inspector/AGENTS.md
Normal file
12
src/inspector/AGENTS.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Inspector Subsystem
|
||||
|
||||
The inspector is a feature of Ghostty that works similar to a
|
||||
browser's developer tools. It allows the user to inspect and modify the
|
||||
terminal state.
|
||||
|
||||
- See the full C API by finding `dcimgui.h` in the `.zig-cache` folder
|
||||
in the root: `find . -type f -name dcimgui.h`. Use the newest version.
|
||||
- See full examples of how to use every widget by loading this file:
|
||||
<https://raw.githubusercontent.com/ocornut/imgui/refs/heads/master/imgui_demo.cpp>
|
||||
- On macOS, run builds with `-Demit-macos-app=false` to verify API usage.
|
||||
- There are no unit tests in this package.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,223 +0,0 @@
|
||||
const std = @import("std");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cimgui = @import("dcimgui");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
|
||||
/// A cell being inspected. This duplicates much of the data in
|
||||
/// the terminal data structure because we want the inspector to
|
||||
/// not have a reference to the terminal state or to grab any
|
||||
/// locks.
|
||||
pub const Cell = struct {
|
||||
/// The main codepoint for this cell.
|
||||
codepoint: u21,
|
||||
|
||||
/// Codepoints for this cell to produce a single grapheme cluster.
|
||||
/// This is only non-empty if the cell is part of a multi-codepoint
|
||||
/// grapheme cluster. This does NOT include the primary codepoint.
|
||||
cps: []const u21,
|
||||
|
||||
/// The style of this cell.
|
||||
style: terminal.Style,
|
||||
|
||||
/// Wide state of the terminal cell
|
||||
wide: terminal.Cell.Wide,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
pin: terminal.Pin,
|
||||
) !Cell {
|
||||
const cell = pin.rowAndCell().cell;
|
||||
const style = pin.style(cell);
|
||||
const cps: []const u21 = if (cell.hasGrapheme()) cps: {
|
||||
const src = pin.grapheme(cell).?;
|
||||
assert(src.len > 0);
|
||||
break :cps try alloc.dupe(u21, src);
|
||||
} else &.{};
|
||||
errdefer if (cps.len > 0) alloc.free(cps);
|
||||
|
||||
return .{
|
||||
.codepoint = cell.codepoint(),
|
||||
.cps = cps,
|
||||
.style = style,
|
||||
.wide = cell.wide,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Cell, alloc: Allocator) void {
|
||||
if (self.cps.len > 0) alloc.free(self.cps);
|
||||
}
|
||||
|
||||
pub fn renderTable(
|
||||
self: *const Cell,
|
||||
t: *const terminal.Terminal,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) void {
|
||||
// We have a selected cell, show information about it.
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"table_cursor",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Grid Position");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("row=%d col=%d", y, x);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: we don't currently write the character itself because
|
||||
// we haven't hooked up imgui to our font system. That's hard! We
|
||||
// can/should instead hook up our renderer to imgui and just render
|
||||
// the single glyph in an image view so it looks _identical_ to the
|
||||
// terminal.
|
||||
codepoint: {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Codepoints");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (cimgui.c.ImGui_BeginListBox("##codepoints", .{ .x = 0, .y = 0 })) {
|
||||
defer cimgui.c.ImGui_EndListBox();
|
||||
|
||||
if (self.codepoint == 0) {
|
||||
_ = cimgui.c.ImGui_SelectableEx("(empty)", false, 0, .{});
|
||||
break :codepoint;
|
||||
}
|
||||
|
||||
// Primary codepoint
|
||||
var buf: [256]u8 = undefined;
|
||||
{
|
||||
const key = std.fmt.bufPrintZ(&buf, "U+{X}", .{self.codepoint}) catch
|
||||
"<internal error>";
|
||||
_ = cimgui.c.ImGui_SelectableEx(key.ptr, false, 0, .{});
|
||||
}
|
||||
|
||||
// All extras
|
||||
for (self.cps) |cp| {
|
||||
const key = std.fmt.bufPrintZ(&buf, "U+{X}", .{cp}) catch
|
||||
"<internal error>";
|
||||
_ = cimgui.c.ImGui_SelectableEx(key.ptr, false, 0, .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Character width property
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Width Property");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(@tagName(self.wide));
|
||||
|
||||
// If we have a color then we show the color
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Foreground Color");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
switch (self.style.fg_color) {
|
||||
.none => cimgui.c.ImGui_Text("default"),
|
||||
.palette => |idx| {
|
||||
const rgb = t.colors.palette.current[idx];
|
||||
cimgui.c.ImGui_Text("Palette %d", idx);
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.g)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.b)) / 255,
|
||||
};
|
||||
_ = cimgui.c.ImGui_ColorEdit3(
|
||||
"color_fg",
|
||||
&color,
|
||||
cimgui.c.ImGuiColorEditFlags_DisplayHex |
|
||||
cimgui.c.ImGuiColorEditFlags_NoPicker |
|
||||
cimgui.c.ImGuiColorEditFlags_NoLabel,
|
||||
);
|
||||
},
|
||||
|
||||
.rgb => |rgb| {
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.g)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.b)) / 255,
|
||||
};
|
||||
_ = cimgui.c.ImGui_ColorEdit3(
|
||||
"color_fg",
|
||||
&color,
|
||||
cimgui.c.ImGuiColorEditFlags_DisplayHex |
|
||||
cimgui.c.ImGuiColorEditFlags_NoPicker |
|
||||
cimgui.c.ImGuiColorEditFlags_NoLabel,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Background Color");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
switch (self.style.bg_color) {
|
||||
.none => cimgui.c.ImGui_Text("default"),
|
||||
.palette => |idx| {
|
||||
const rgb = t.colors.palette.current[idx];
|
||||
cimgui.c.ImGui_Text("Palette %d", idx);
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.g)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.b)) / 255,
|
||||
};
|
||||
_ = cimgui.c.ImGui_ColorEdit3(
|
||||
"color_bg",
|
||||
&color,
|
||||
cimgui.c.ImGuiColorEditFlags_DisplayHex |
|
||||
cimgui.c.ImGuiColorEditFlags_NoPicker |
|
||||
cimgui.c.ImGuiColorEditFlags_NoLabel,
|
||||
);
|
||||
},
|
||||
|
||||
.rgb => |rgb| {
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.g)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.b)) / 255,
|
||||
};
|
||||
_ = cimgui.c.ImGui_ColorEdit3(
|
||||
"color_bg",
|
||||
&color,
|
||||
cimgui.c.ImGuiColorEditFlags_DisplayHex |
|
||||
cimgui.c.ImGuiColorEditFlags_NoPicker |
|
||||
cimgui.c.ImGuiColorEditFlags_NoLabel,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
// Boolean styles
|
||||
const styles = .{
|
||||
"bold", "italic", "faint", "blink",
|
||||
"inverse", "invisible", "strikethrough",
|
||||
};
|
||||
inline for (styles) |style| style: {
|
||||
if (!@field(self.style.flags, style)) break :style;
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text(style.ptr);
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("true");
|
||||
}
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_TextDisabled("(Any styles not shown are not currently set)");
|
||||
}
|
||||
};
|
||||
@@ -1,142 +0,0 @@
|
||||
const cimgui = @import("dcimgui");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
|
||||
/// Render cursor information with a table already open.
|
||||
pub fn renderInTable(
|
||||
t: *const terminal.Terminal,
|
||||
cursor: *const terminal.Screen.Cursor,
|
||||
) void {
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Position (x, y)");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("(%d, %d)", cursor.x, cursor.y);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Style");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(cursor.cursor_style).ptr);
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor.pending_wrap) {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Pending Wrap");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%s", if (cursor.pending_wrap) "true".ptr else "false".ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a color then we show the color
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Foreground Color");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
switch (cursor.style.fg_color) {
|
||||
.none => cimgui.c.ImGui_Text("default"),
|
||||
.palette => |idx| {
|
||||
const rgb = t.colors.palette.current[idx];
|
||||
cimgui.c.ImGui_Text("Palette %d", idx);
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.g)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.b)) / 255,
|
||||
};
|
||||
_ = cimgui.c.ImGui_ColorEdit3(
|
||||
"color_fg",
|
||||
&color,
|
||||
cimgui.c.ImGuiColorEditFlags_DisplayHex |
|
||||
cimgui.c.ImGuiColorEditFlags_NoPicker |
|
||||
cimgui.c.ImGuiColorEditFlags_NoLabel,
|
||||
);
|
||||
},
|
||||
|
||||
.rgb => |rgb| {
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.g)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.b)) / 255,
|
||||
};
|
||||
_ = cimgui.c.ImGui_ColorEdit3(
|
||||
"color_fg",
|
||||
&color,
|
||||
cimgui.c.ImGuiColorEditFlags_DisplayHex |
|
||||
cimgui.c.ImGuiColorEditFlags_NoPicker |
|
||||
cimgui.c.ImGuiColorEditFlags_NoLabel,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Background Color");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
switch (cursor.style.bg_color) {
|
||||
.none => cimgui.c.ImGui_Text("default"),
|
||||
.palette => |idx| {
|
||||
const rgb = t.colors.palette.current[idx];
|
||||
cimgui.c.ImGui_Text("Palette %d", idx);
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.g)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.b)) / 255,
|
||||
};
|
||||
_ = cimgui.c.ImGui_ColorEdit3(
|
||||
"color_bg",
|
||||
&color,
|
||||
cimgui.c.ImGuiColorEditFlags_DisplayHex |
|
||||
cimgui.c.ImGuiColorEditFlags_NoPicker |
|
||||
cimgui.c.ImGuiColorEditFlags_NoLabel,
|
||||
);
|
||||
},
|
||||
|
||||
.rgb => |rgb| {
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.g)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.b)) / 255,
|
||||
};
|
||||
_ = cimgui.c.ImGui_ColorEdit3(
|
||||
"color_bg",
|
||||
&color,
|
||||
cimgui.c.ImGuiColorEditFlags_DisplayHex |
|
||||
cimgui.c.ImGuiColorEditFlags_NoPicker |
|
||||
cimgui.c.ImGuiColorEditFlags_NoLabel,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
// Boolean styles
|
||||
const styles = .{
|
||||
"bold", "italic", "faint", "blink",
|
||||
"inverse", "invisible", "strikethrough",
|
||||
};
|
||||
inline for (styles) |style| style: {
|
||||
if (!@field(cursor.style.flags, style)) break :style;
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text(style.ptr);
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("true");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const input = @import("../input.zig");
|
||||
const CircBuf = @import("../datastruct/main.zig").CircBuf;
|
||||
const cimgui = @import("dcimgui");
|
||||
|
||||
/// Circular buffer of key events.
|
||||
pub const EventRing = CircBuf(Event, undefined);
|
||||
|
||||
/// Represents a recorded keyboard event.
|
||||
pub const Event = struct {
|
||||
/// The input event.
|
||||
event: input.KeyEvent,
|
||||
|
||||
/// The binding that was triggered as a result of this event.
|
||||
/// Multiple bindings are possible if they are chained.
|
||||
binding: []const input.Binding.Action = &.{},
|
||||
|
||||
/// The data sent to the pty as a result of this keyboard event.
|
||||
/// This is allocated using the inspector allocator.
|
||||
pty: []const u8 = "",
|
||||
|
||||
/// State for the inspector GUI. Do not set this unless you're the inspector.
|
||||
imgui_state: struct {
|
||||
selected: bool = false,
|
||||
} = .{},
|
||||
|
||||
pub fn init(alloc: Allocator, event: input.KeyEvent) !Event {
|
||||
var copy = event;
|
||||
copy.utf8 = "";
|
||||
if (event.utf8.len > 0) copy.utf8 = try alloc.dupe(u8, event.utf8);
|
||||
return .{ .event = copy };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Event, alloc: Allocator) void {
|
||||
alloc.free(self.binding);
|
||||
if (self.event.utf8.len > 0) alloc.free(self.event.utf8);
|
||||
if (self.pty.len > 0) alloc.free(self.pty);
|
||||
}
|
||||
|
||||
/// Returns a label that can be used for this event. This is null-terminated
|
||||
/// so it can be easily used with C APIs.
|
||||
pub fn label(self: *const Event, buf: []u8) ![:0]const u8 {
|
||||
var buf_stream = std.io.fixedBufferStream(buf);
|
||||
const writer = buf_stream.writer();
|
||||
|
||||
switch (self.event.action) {
|
||||
.press => try writer.writeAll("Press: "),
|
||||
.release => try writer.writeAll("Release: "),
|
||||
.repeat => try writer.writeAll("Repeat: "),
|
||||
}
|
||||
|
||||
if (self.event.mods.shift) try writer.writeAll("Shift+");
|
||||
if (self.event.mods.ctrl) try writer.writeAll("Ctrl+");
|
||||
if (self.event.mods.alt) try writer.writeAll("Alt+");
|
||||
if (self.event.mods.super) try writer.writeAll("Super+");
|
||||
|
||||
// Write our key. If we have an invalid key we attempt to write
|
||||
// the utf8 associated with it if we have it to handle non-ascii.
|
||||
try writer.writeAll(switch (self.event.key) {
|
||||
.unidentified => if (self.event.utf8.len > 0) self.event.utf8 else @tagName(self.event.key),
|
||||
else => @tagName(self.event.key),
|
||||
});
|
||||
|
||||
// Deadkey
|
||||
if (self.event.composing) try writer.writeAll(" (composing)");
|
||||
|
||||
// Null-terminator
|
||||
try writer.writeByte(0);
|
||||
return buf[0..(buf_stream.getWritten().len - 1) :0];
|
||||
}
|
||||
|
||||
/// Render this event in the inspector GUI.
|
||||
pub fn render(self: *const Event) void {
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"##event",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
if (self.binding.len > 0) {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Triggered Binding");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
|
||||
const height: f32 = height: {
|
||||
const item_count: f32 = @floatFromInt(@min(self.binding.len, 5));
|
||||
const padding = cimgui.c.ImGui_GetStyle().*.FramePadding.y * 2;
|
||||
break :height cimgui.c.ImGui_GetTextLineHeightWithSpacing() * item_count + padding;
|
||||
};
|
||||
if (cimgui.c.ImGui_BeginListBox("##bindings", .{ .x = 0, .y = height })) {
|
||||
defer cimgui.c.ImGui_EndListBox();
|
||||
for (self.binding) |action| {
|
||||
_ = cimgui.c.ImGui_SelectableEx(
|
||||
@tagName(action).ptr,
|
||||
false,
|
||||
cimgui.c.ImGuiSelectableFlags_None,
|
||||
.{ .x = 0, .y = 0 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pty: {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Encoding to Pty");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (self.pty.len == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("(no data)");
|
||||
break :pty;
|
||||
}
|
||||
|
||||
self.renderPty() catch {
|
||||
cimgui.c.ImGui_TextDisabled("(error rendering pty data)");
|
||||
break :pty;
|
||||
};
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Action");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(self.event.action).ptr);
|
||||
}
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Key");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(self.event.key).ptr);
|
||||
}
|
||||
if (!self.event.mods.empty()) {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Mods");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (self.event.mods.shift) cimgui.c.ImGui_Text("shift ");
|
||||
if (self.event.mods.ctrl) cimgui.c.ImGui_Text("ctrl ");
|
||||
if (self.event.mods.alt) cimgui.c.ImGui_Text("alt ");
|
||||
if (self.event.mods.super) cimgui.c.ImGui_Text("super ");
|
||||
}
|
||||
if (self.event.composing) {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Composing");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("true");
|
||||
}
|
||||
utf8: {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("UTF-8");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (self.event.utf8.len == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("(empty)");
|
||||
break :utf8;
|
||||
}
|
||||
|
||||
self.renderUtf8(self.event.utf8) catch {
|
||||
cimgui.c.ImGui_TextDisabled("(error rendering utf-8)");
|
||||
break :utf8;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn renderUtf8(self: *const Event, utf8: []const u8) !void {
|
||||
_ = self;
|
||||
|
||||
// Format the codepoint sequence
|
||||
var buf: [1024]u8 = undefined;
|
||||
var buf_stream = std.io.fixedBufferStream(&buf);
|
||||
const writer = buf_stream.writer();
|
||||
if (std.unicode.Utf8View.init(utf8)) |view| {
|
||||
var it = view.iterator();
|
||||
while (it.nextCodepoint()) |cp| {
|
||||
try writer.print("U+{X} ", .{cp});
|
||||
}
|
||||
} else |_| {
|
||||
try writer.writeAll("(invalid utf-8)");
|
||||
}
|
||||
try writer.writeByte(0);
|
||||
|
||||
// Render as a textbox
|
||||
_ = cimgui.c.ImGui_InputText(
|
||||
"##utf8",
|
||||
&buf,
|
||||
buf_stream.getWritten().len - 1,
|
||||
cimgui.c.ImGuiInputTextFlags_ReadOnly,
|
||||
);
|
||||
}
|
||||
|
||||
fn renderPty(self: *const Event) !void {
|
||||
// Format the codepoint sequence
|
||||
var buf: [1024]u8 = undefined;
|
||||
var buf_stream = std.io.fixedBufferStream(&buf);
|
||||
const writer = buf_stream.writer();
|
||||
|
||||
for (self.pty) |byte| {
|
||||
// Print ESC special because its so common
|
||||
if (byte == 0x1B) {
|
||||
try writer.writeAll("ESC ");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Print ASCII as-is
|
||||
if (byte > 0x20 and byte < 0x7F) {
|
||||
try writer.writeByte(byte);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Everything else as a hex byte
|
||||
try writer.print("0x{X} ", .{byte});
|
||||
}
|
||||
|
||||
try writer.writeByte(0);
|
||||
|
||||
// Render as a textbox
|
||||
_ = cimgui.c.ImGui_InputText(
|
||||
"##pty",
|
||||
&buf,
|
||||
buf_stream.getWritten().len - 1,
|
||||
cimgui.c.ImGuiInputTextFlags_ReadOnly,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
test "event string" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var event = try Event.init(alloc, .{ .key = .key_a });
|
||||
defer event.deinit(alloc);
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
try testing.expectEqualStrings("Press: key_a", try event.label(&buf));
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
const std = @import("std");
|
||||
pub const cell = @import("cell.zig");
|
||||
pub const cursor = @import("cursor.zig");
|
||||
pub const key = @import("key.zig");
|
||||
pub const page = @import("page.zig");
|
||||
pub const termio = @import("termio.zig");
|
||||
|
||||
pub const Cell = cell.Cell;
|
||||
pub const widgets = @import("widgets.zig");
|
||||
pub const Inspector = @import("Inspector.zig");
|
||||
|
||||
pub const KeyEvent = widgets.key.Event;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cimgui = @import("dcimgui");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const units = @import("units.zig");
|
||||
|
||||
pub fn render(page: *const terminal.Page) void {
|
||||
cimgui.c.ImGui_PushIDPtr(page);
|
||||
defer cimgui.c.ImGui_PopID();
|
||||
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"##page_state",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Memory Size");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d bytes (%d KiB)", page.memory.len, units.toKibiBytes(page.memory.len));
|
||||
cimgui.c.ImGui_Text("%d VM pages", page.memory.len / std.heap.page_size_min);
|
||||
}
|
||||
}
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Unique Styles");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d", page.styles.count());
|
||||
}
|
||||
}
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Grapheme Entries");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d", page.graphemeCount());
|
||||
}
|
||||
}
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Capacity");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"##capacity",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
const cap = page.capacity;
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Columns");
|
||||
}
|
||||
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.cols)));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Rows");
|
||||
}
|
||||
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.rows)));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Unique Styles");
|
||||
}
|
||||
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.styles)));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Grapheme Bytes");
|
||||
}
|
||||
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d", cap.grapheme_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Size");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"##size",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
const size = page.size;
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Columns");
|
||||
}
|
||||
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d", @as(u32, @intCast(size.cols)));
|
||||
}
|
||||
}
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Rows");
|
||||
}
|
||||
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d", @as(u32, @intCast(size.rows)));
|
||||
}
|
||||
}
|
||||
}
|
||||
} // size table
|
||||
}
|
||||
@@ -1,398 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cimgui = @import("dcimgui");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const CircBuf = @import("../datastruct/main.zig").CircBuf;
|
||||
const Surface = @import("../Surface.zig");
|
||||
|
||||
/// The stream handler for our inspector.
|
||||
pub const Stream = terminal.Stream(VTHandler);
|
||||
|
||||
/// VT event circular buffer.
|
||||
pub const VTEventRing = CircBuf(VTEvent, undefined);
|
||||
|
||||
/// VT event
|
||||
pub const VTEvent = struct {
|
||||
/// Sequence number, just monotonically increasing.
|
||||
seq: usize = 1,
|
||||
|
||||
/// Kind of event, for filtering
|
||||
kind: Kind,
|
||||
|
||||
/// The formatted string of the event. This is allocated. We format the
|
||||
/// event for now because there is so much data to copy if we wanted to
|
||||
/// store the raw event.
|
||||
str: [:0]const u8,
|
||||
|
||||
/// Various metadata at the time of the event (before processing).
|
||||
cursor: terminal.Screen.Cursor,
|
||||
scrolling_region: terminal.Terminal.ScrollingRegion,
|
||||
metadata: Metadata.Unmanaged = .{},
|
||||
|
||||
/// imgui selection state
|
||||
imgui_selected: bool = false,
|
||||
|
||||
const Kind = enum { print, execute, csi, esc, osc, dcs, apc };
|
||||
const Metadata = std.StringHashMap([:0]const u8);
|
||||
|
||||
/// Initialize the event information for the given parser action.
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
surface: *Surface,
|
||||
action: terminal.Parser.Action,
|
||||
) !VTEvent {
|
||||
var md = Metadata.init(alloc);
|
||||
errdefer md.deinit();
|
||||
var buf: std.Io.Writer.Allocating = .init(alloc);
|
||||
defer buf.deinit();
|
||||
try encodeAction(alloc, &buf.writer, &md, action);
|
||||
const str = try buf.toOwnedSliceSentinel(0);
|
||||
errdefer alloc.free(str);
|
||||
|
||||
const kind: Kind = switch (action) {
|
||||
.print => .print,
|
||||
.execute => .execute,
|
||||
.csi_dispatch => .csi,
|
||||
.esc_dispatch => .esc,
|
||||
.osc_dispatch => .osc,
|
||||
.dcs_hook, .dcs_put, .dcs_unhook => .dcs,
|
||||
.apc_start, .apc_put, .apc_end => .apc,
|
||||
};
|
||||
|
||||
const t = surface.renderer_state.terminal;
|
||||
|
||||
return .{
|
||||
.kind = kind,
|
||||
.str = str,
|
||||
.cursor = t.screens.active.cursor,
|
||||
.scrolling_region = t.scrolling_region,
|
||||
.metadata = md.unmanaged,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *VTEvent, alloc: Allocator) void {
|
||||
{
|
||||
var it = self.metadata.valueIterator();
|
||||
while (it.next()) |v| alloc.free(v.*);
|
||||
self.metadata.deinit(alloc);
|
||||
}
|
||||
|
||||
alloc.free(self.str);
|
||||
}
|
||||
|
||||
/// Returns true if the event passes the given filter.
|
||||
pub fn passFilter(
|
||||
self: *const VTEvent,
|
||||
filter: *const cimgui.c.ImGuiTextFilter,
|
||||
) bool {
|
||||
// Check our main string
|
||||
if (cimgui.c.ImGuiTextFilter_PassFilter(
|
||||
filter,
|
||||
self.str.ptr,
|
||||
null,
|
||||
)) return true;
|
||||
|
||||
// We also check all metadata keys and values
|
||||
var it = self.metadata.iterator();
|
||||
while (it.next()) |entry| {
|
||||
var buf: [256]u8 = undefined;
|
||||
const key = std.fmt.bufPrintZ(&buf, "{s}", .{entry.key_ptr.*}) catch continue;
|
||||
if (cimgui.c.ImGuiTextFilter_PassFilter(
|
||||
filter,
|
||||
key.ptr,
|
||||
null,
|
||||
)) return true;
|
||||
if (cimgui.c.ImGuiTextFilter_PassFilter(
|
||||
filter,
|
||||
entry.value_ptr.ptr,
|
||||
null,
|
||||
)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Encode a parser action as a string that we show in the logs.
|
||||
fn encodeAction(
|
||||
alloc: Allocator,
|
||||
writer: *std.Io.Writer,
|
||||
md: *Metadata,
|
||||
action: terminal.Parser.Action,
|
||||
) !void {
|
||||
switch (action) {
|
||||
.print => try encodePrint(writer, action),
|
||||
.execute => try encodeExecute(writer, action),
|
||||
.csi_dispatch => |v| try encodeCSI(writer, v),
|
||||
.esc_dispatch => |v| try encodeEsc(writer, v),
|
||||
.osc_dispatch => |v| try encodeOSC(alloc, writer, md, v),
|
||||
else => try writer.print("{f}", .{action}),
|
||||
}
|
||||
}
|
||||
|
||||
fn encodePrint(writer: *std.Io.Writer, action: terminal.Parser.Action) !void {
|
||||
const ch = action.print;
|
||||
try writer.print("'{u}' (U+{X})", .{ ch, ch });
|
||||
}
|
||||
|
||||
fn encodeExecute(writer: *std.Io.Writer, action: terminal.Parser.Action) !void {
|
||||
const ch = action.execute;
|
||||
switch (ch) {
|
||||
0x00 => try writer.writeAll("NUL"),
|
||||
0x01 => try writer.writeAll("SOH"),
|
||||
0x02 => try writer.writeAll("STX"),
|
||||
0x03 => try writer.writeAll("ETX"),
|
||||
0x04 => try writer.writeAll("EOT"),
|
||||
0x05 => try writer.writeAll("ENQ"),
|
||||
0x06 => try writer.writeAll("ACK"),
|
||||
0x07 => try writer.writeAll("BEL"),
|
||||
0x08 => try writer.writeAll("BS"),
|
||||
0x09 => try writer.writeAll("HT"),
|
||||
0x0A => try writer.writeAll("LF"),
|
||||
0x0B => try writer.writeAll("VT"),
|
||||
0x0C => try writer.writeAll("FF"),
|
||||
0x0D => try writer.writeAll("CR"),
|
||||
0x0E => try writer.writeAll("SO"),
|
||||
0x0F => try writer.writeAll("SI"),
|
||||
else => try writer.writeAll("?"),
|
||||
}
|
||||
try writer.print(" (0x{X})", .{ch});
|
||||
}
|
||||
|
||||
fn encodeCSI(writer: *std.Io.Writer, csi: terminal.Parser.Action.CSI) !void {
|
||||
for (csi.intermediates) |v| try writer.print("{c} ", .{v});
|
||||
for (csi.params, 0..) |v, i| {
|
||||
if (i != 0) try writer.writeByte(';');
|
||||
try writer.print("{d}", .{v});
|
||||
}
|
||||
if (csi.intermediates.len > 0 or csi.params.len > 0) try writer.writeByte(' ');
|
||||
try writer.writeByte(csi.final);
|
||||
}
|
||||
|
||||
fn encodeEsc(writer: *std.Io.Writer, esc: terminal.Parser.Action.ESC) !void {
|
||||
for (esc.intermediates) |v| try writer.print("{c} ", .{v});
|
||||
try writer.writeByte(esc.final);
|
||||
}
|
||||
|
||||
fn encodeOSC(
|
||||
alloc: Allocator,
|
||||
writer: *std.Io.Writer,
|
||||
md: *Metadata,
|
||||
osc: terminal.osc.Command,
|
||||
) !void {
|
||||
// The description is just the tag
|
||||
try writer.print("{s} ", .{@tagName(osc)});
|
||||
|
||||
// Add additional fields to metadata
|
||||
switch (osc) {
|
||||
inline else => |v, tag| if (tag == osc) {
|
||||
try encodeMetadata(alloc, md, v);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn encodeMetadata(
|
||||
alloc: Allocator,
|
||||
md: *Metadata,
|
||||
v: anytype,
|
||||
) !void {
|
||||
switch (@TypeOf(v)) {
|
||||
void => {},
|
||||
[]const u8,
|
||||
[:0]const u8,
|
||||
=> try md.put("data", try alloc.dupeZ(u8, v)),
|
||||
else => |T| switch (@typeInfo(T)) {
|
||||
.@"struct" => |info| inline for (info.fields) |field| {
|
||||
try encodeMetadataSingle(
|
||||
alloc,
|
||||
md,
|
||||
field.name,
|
||||
@field(v, field.name),
|
||||
);
|
||||
},
|
||||
|
||||
.@"union" => |info| {
|
||||
const Tag = info.tag_type orelse @compileError("Unions must have a tag");
|
||||
const tag_name = @tagName(@as(Tag, v));
|
||||
inline for (info.fields) |field| {
|
||||
if (std.mem.eql(u8, field.name, tag_name)) {
|
||||
if (field.type == void) {
|
||||
break try md.put("data", tag_name);
|
||||
} else {
|
||||
break try encodeMetadataSingle(alloc, md, tag_name, @field(v, field.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
else => {
|
||||
@compileLog(T);
|
||||
@compileError("unsupported type, see log");
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn encodeMetadataSingle(
|
||||
alloc: Allocator,
|
||||
md: *Metadata,
|
||||
key: []const u8,
|
||||
value: anytype,
|
||||
) !void {
|
||||
const Value = @TypeOf(value);
|
||||
const info = @typeInfo(Value);
|
||||
switch (info) {
|
||||
.optional => if (value) |unwrapped| {
|
||||
try encodeMetadataSingle(alloc, md, key, unwrapped);
|
||||
} else {
|
||||
try md.put(key, try alloc.dupeZ(u8, "(unset)"));
|
||||
},
|
||||
|
||||
.bool => try md.put(
|
||||
key,
|
||||
try alloc.dupeZ(u8, if (value) "true" else "false"),
|
||||
),
|
||||
|
||||
.@"enum" => try md.put(
|
||||
key,
|
||||
try alloc.dupeZ(u8, @tagName(value)),
|
||||
),
|
||||
|
||||
.@"union" => |u| {
|
||||
const Tag = u.tag_type orelse @compileError("Unions must have a tag");
|
||||
const tag_name = @tagName(@as(Tag, value));
|
||||
inline for (u.fields) |field| {
|
||||
if (std.mem.eql(u8, field.name, tag_name)) {
|
||||
const s = if (field.type == void)
|
||||
try alloc.dupeZ(u8, tag_name)
|
||||
else if (field.type == [:0]const u8 or field.type == []const u8)
|
||||
try std.fmt.allocPrintSentinel(alloc, "{s}={s}", .{
|
||||
tag_name,
|
||||
@field(value, field.name),
|
||||
}, 0)
|
||||
else
|
||||
try std.fmt.allocPrintSentinel(alloc, "{s}={}", .{
|
||||
tag_name,
|
||||
@field(value, field.name),
|
||||
}, 0);
|
||||
|
||||
try md.put(key, s);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
.@"struct" => try md.put(
|
||||
key,
|
||||
try alloc.dupeZ(u8, @typeName(Value)),
|
||||
),
|
||||
|
||||
else => switch (Value) {
|
||||
[]const u8,
|
||||
[:0]const u8,
|
||||
=> try md.put(key, try alloc.dupeZ(u8, value)),
|
||||
|
||||
else => |T| switch (@typeInfo(T)) {
|
||||
.int => try md.put(
|
||||
key,
|
||||
try std.fmt.allocPrintSentinel(alloc, "{}", .{value}, 0),
|
||||
),
|
||||
else => {
|
||||
@compileLog(T);
|
||||
@compileError("unsupported type, see log");
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Our VT stream handler.
|
||||
pub const VTHandler = struct {
|
||||
/// The surface that the inspector is attached to. We use this instead
|
||||
/// of the inspector because this is pointer-stable.
|
||||
surface: *Surface,
|
||||
|
||||
/// True if the handler is currently recording.
|
||||
active: bool = true,
|
||||
|
||||
/// Current sequence number
|
||||
current_seq: usize = 1,
|
||||
|
||||
/// Exclude certain actions by tag.
|
||||
filter_exclude: ActionTagSet = .initMany(&.{.print}),
|
||||
filter_text: cimgui.c.ImGuiTextFilter = .{},
|
||||
|
||||
const ActionTagSet = std.EnumSet(terminal.Parser.Action.Tag);
|
||||
|
||||
pub fn init(surface: *Surface) VTHandler {
|
||||
return .{
|
||||
.surface = surface,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *VTHandler) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn vt(
|
||||
self: *VTHandler,
|
||||
comptime action: Stream.Action.Tag,
|
||||
value: Stream.Action.Value(action),
|
||||
) !void {
|
||||
_ = self;
|
||||
_ = value;
|
||||
}
|
||||
|
||||
/// This is called with every single terminal action.
|
||||
pub fn handleManually(self: *VTHandler, action: terminal.Parser.Action) !bool {
|
||||
const insp = self.surface.inspector orelse return false;
|
||||
|
||||
// We always increment the sequence number, even if we're paused or
|
||||
// filter out the event. This helps show the user that there is a gap
|
||||
// between events and roughly how large that gap was.
|
||||
defer self.current_seq +%= 1;
|
||||
|
||||
// If we're pausing, then we ignore all events.
|
||||
if (!self.active) return true;
|
||||
|
||||
// We ignore certain action types that are too noisy.
|
||||
switch (action) {
|
||||
.dcs_put, .apc_put => return true,
|
||||
else => {},
|
||||
}
|
||||
|
||||
// If we requested a specific type to be ignored, ignore it.
|
||||
// We return true because we did "handle" it by ignoring it.
|
||||
if (self.filter_exclude.contains(std.meta.activeTag(action))) return true;
|
||||
|
||||
// Build our event
|
||||
const alloc = self.surface.alloc;
|
||||
var ev = try VTEvent.init(alloc, self.surface, action);
|
||||
ev.seq = self.current_seq;
|
||||
errdefer ev.deinit(alloc);
|
||||
|
||||
// Check if the event passes the filter
|
||||
if (!ev.passFilter(&self.filter_text)) {
|
||||
ev.deinit(alloc);
|
||||
return true;
|
||||
}
|
||||
|
||||
const max_capacity = 100;
|
||||
insp.vt_events.append(ev) catch |err| switch (err) {
|
||||
error.OutOfMemory => if (insp.vt_events.capacity() < max_capacity) {
|
||||
// We're out of memory, but we can allocate to our capacity.
|
||||
const new_capacity = @min(insp.vt_events.capacity() * 2, max_capacity);
|
||||
try insp.vt_events.resize(insp.surface.alloc, new_capacity);
|
||||
try insp.vt_events.append(ev);
|
||||
} else {
|
||||
var it = insp.vt_events.iterator(.forward);
|
||||
if (it.next()) |old_ev| old_ev.deinit(insp.surface.alloc);
|
||||
insp.vt_events.deleteOldest(1);
|
||||
try insp.vt_events.append(ev);
|
||||
},
|
||||
|
||||
else => return err,
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
227
src/inspector/widgets.zig
Normal file
227
src/inspector/widgets.zig
Normal file
@@ -0,0 +1,227 @@
|
||||
const cimgui = @import("dcimgui");
|
||||
|
||||
pub const page = @import("widgets/page.zig");
|
||||
pub const pagelist = @import("widgets/pagelist.zig");
|
||||
pub const key = @import("widgets/key.zig");
|
||||
pub const renderer = @import("widgets/renderer.zig");
|
||||
pub const screen = @import("widgets/screen.zig");
|
||||
pub const style = @import("widgets/style.zig");
|
||||
pub const surface = @import("widgets/surface.zig");
|
||||
pub const terminal = @import("widgets/terminal.zig");
|
||||
pub const termio = @import("widgets/termio.zig");
|
||||
|
||||
/// Draws a "(?)" disabled text marker that shows some help text
|
||||
/// on hover.
|
||||
pub fn helpMarker(text: [:0]const u8) void {
|
||||
cimgui.c.ImGui_TextDisabled("(?)");
|
||||
if (!cimgui.c.ImGui_BeginItemTooltip()) return;
|
||||
defer cimgui.c.ImGui_EndTooltip();
|
||||
|
||||
cimgui.c.ImGui_PushTextWrapPos(cimgui.c.ImGui_GetFontSize() * 35.0);
|
||||
defer cimgui.c.ImGui_PopTextWrapPos();
|
||||
|
||||
cimgui.c.ImGui_TextUnformatted(text.ptr);
|
||||
}
|
||||
|
||||
/// DetachableHeader allows rendering a collapsing header that can be
|
||||
/// detached into its own window.
|
||||
pub const DetachableHeader = struct {
|
||||
/// Set whether the window is detached.
|
||||
detached: bool = false,
|
||||
|
||||
/// If true, detaching will move the item into a docking position
|
||||
/// to the right.
|
||||
dock: bool = true,
|
||||
|
||||
// Internal state do not touch.
|
||||
window_first: bool = true,
|
||||
|
||||
pub fn windowEnd(self: *DetachableHeader) void {
|
||||
_ = self;
|
||||
|
||||
// If we started the window, we need to end it.
|
||||
cimgui.c.ImGui_End();
|
||||
}
|
||||
|
||||
/// Returns null if there is no window created (not detached).
|
||||
/// Otherwise returns whether the window is open.
|
||||
pub fn window(
|
||||
self: *DetachableHeader,
|
||||
label: [:0]const u8,
|
||||
) ?bool {
|
||||
// If we're not detached, we don't create a window.
|
||||
if (!self.detached) {
|
||||
self.window_first = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
// If this is our first time showing the window then we need to
|
||||
// setup docking. We only do this on the first time because we
|
||||
// don't want to reset a user's docking behavior later.
|
||||
if (self.window_first) dock: {
|
||||
self.window_first = false;
|
||||
if (!self.dock) break :dock;
|
||||
const dock_id = cimgui.c.ImGui_GetWindowDockID();
|
||||
if (dock_id == 0) break :dock;
|
||||
var dock_id_right: cimgui.c.ImGuiID = 0;
|
||||
var dock_id_left: cimgui.c.ImGuiID = 0;
|
||||
_ = cimgui.ImGui_DockBuilderSplitNode(
|
||||
dock_id,
|
||||
cimgui.c.ImGuiDir_Right,
|
||||
0.4,
|
||||
&dock_id_right,
|
||||
&dock_id_left,
|
||||
);
|
||||
cimgui.ImGui_DockBuilderDockWindow(label, dock_id_right);
|
||||
cimgui.ImGui_DockBuilderFinish(dock_id);
|
||||
}
|
||||
|
||||
return cimgui.c.ImGui_Begin(
|
||||
label,
|
||||
&self.detached,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn header(
|
||||
self: *DetachableHeader,
|
||||
label: [:0]const u8,
|
||||
) bool {
|
||||
// If we're detached, create a separate window.
|
||||
if (self.detached) return false;
|
||||
|
||||
// Make sure all headers have a unique ID in the stack. We only
|
||||
// need to do this for the header side because creating a window
|
||||
// automatically creates an ID.
|
||||
cimgui.c.ImGui_PushID(label);
|
||||
defer cimgui.c.ImGui_PopID();
|
||||
|
||||
// Create the collapsing header with the pop out button overlaid.
|
||||
cimgui.c.ImGui_SetNextItemAllowOverlap();
|
||||
const is_open = cimgui.c.ImGui_CollapsingHeader(
|
||||
label,
|
||||
cimgui.c.ImGuiTreeNodeFlags_None,
|
||||
);
|
||||
|
||||
// Place pop-out button inside the header bar
|
||||
const header_max = cimgui.c.ImGui_GetItemRectMax();
|
||||
const header_min = cimgui.c.ImGui_GetItemRectMin();
|
||||
const frame_height = cimgui.c.ImGui_GetFrameHeight();
|
||||
const button_size = frame_height - 4;
|
||||
const padding = 4;
|
||||
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_SetCursorScreenPos(.{
|
||||
.x = header_max.x - button_size - padding,
|
||||
.y = header_min.y + 2,
|
||||
});
|
||||
{
|
||||
cimgui.c.ImGui_PushStyleVarImVec2(
|
||||
cimgui.c.ImGuiStyleVar_FramePadding,
|
||||
.{ .x = 0, .y = 0 },
|
||||
);
|
||||
defer cimgui.c.ImGui_PopStyleVar();
|
||||
if (cimgui.c.ImGui_ButtonEx(
|
||||
">>##detach",
|
||||
.{ .x = button_size, .y = button_size },
|
||||
)) {
|
||||
self.detached = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_DelayShort)) {
|
||||
cimgui.c.ImGui_SetTooltip("Detach into separate window");
|
||||
}
|
||||
|
||||
return is_open;
|
||||
}
|
||||
};
|
||||
|
||||
pub const DetachableHeaderState = struct {
|
||||
show_window: bool = false,
|
||||
|
||||
/// Internal state. Don't touch.
|
||||
first_show: bool = false,
|
||||
};
|
||||
|
||||
/// Render a collapsing header that can be detached into its own window.
|
||||
/// When detached, renders as a separate window with a close button.
|
||||
/// When attached, renders as a collapsing header with a pop-out button.
|
||||
pub fn detachableHeader(
|
||||
label: [:0]const u8,
|
||||
state: *DetachableHeaderState,
|
||||
ctx: anytype,
|
||||
comptime contentFn: fn (@TypeOf(ctx)) void,
|
||||
) void {
|
||||
cimgui.c.ImGui_PushID(label);
|
||||
defer cimgui.c.ImGui_PopID();
|
||||
|
||||
if (state.show_window) {
|
||||
// On first show, dock this window to the right of the parent window's dock.
|
||||
// We only do this once so the user can freely reposition the window afterward
|
||||
// without it snapping back to the right on every frame.
|
||||
if (!state.first_show) {
|
||||
state.first_show = true;
|
||||
const current_dock_id = cimgui.c.ImGui_GetWindowDockID();
|
||||
if (current_dock_id != 0) {
|
||||
var dock_id_right: cimgui.c.ImGuiID = 0;
|
||||
var dock_id_left: cimgui.c.ImGuiID = 0;
|
||||
_ = cimgui.ImGui_DockBuilderSplitNode(
|
||||
current_dock_id,
|
||||
cimgui.c.ImGuiDir_Right,
|
||||
0.3,
|
||||
&dock_id_right,
|
||||
&dock_id_left,
|
||||
);
|
||||
cimgui.ImGui_DockBuilderDockWindow(label, dock_id_right);
|
||||
cimgui.ImGui_DockBuilderFinish(current_dock_id);
|
||||
}
|
||||
}
|
||||
|
||||
defer cimgui.c.ImGui_End();
|
||||
if (cimgui.c.ImGui_Begin(
|
||||
label,
|
||||
&state.show_window,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
)) contentFn(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset first_show when window is closed so next open docks again
|
||||
state.first_show = false;
|
||||
|
||||
cimgui.c.ImGui_SetNextItemAllowOverlap();
|
||||
const is_open = cimgui.c.ImGui_CollapsingHeader(
|
||||
label,
|
||||
cimgui.c.ImGuiTreeNodeFlags_None,
|
||||
);
|
||||
|
||||
// Place pop-out button inside the header bar
|
||||
const header_max = cimgui.c.ImGui_GetItemRectMax();
|
||||
const header_min = cimgui.c.ImGui_GetItemRectMin();
|
||||
const frame_height = cimgui.c.ImGui_GetFrameHeight();
|
||||
const button_size = frame_height - 4;
|
||||
const padding = 4;
|
||||
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_SetCursorScreenPos(.{
|
||||
.x = header_max.x - button_size - padding,
|
||||
.y = header_min.y + 2,
|
||||
});
|
||||
cimgui.c.ImGui_PushStyleVarImVec2(
|
||||
cimgui.c.ImGuiStyleVar_FramePadding,
|
||||
.{ .x = 0, .y = 0 },
|
||||
);
|
||||
if (cimgui.c.ImGui_ButtonEx(
|
||||
">>##detach",
|
||||
.{ .x = button_size, .y = button_size },
|
||||
)) {
|
||||
state.show_window = true;
|
||||
}
|
||||
cimgui.c.ImGui_PopStyleVar();
|
||||
if (cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_DelayShort)) {
|
||||
cimgui.c.ImGui_SetTooltip("Pop out into separate window");
|
||||
}
|
||||
|
||||
if (is_open) contentFn(ctx);
|
||||
}
|
||||
535
src/inspector/widgets/key.zig
Normal file
535
src/inspector/widgets/key.zig
Normal file
@@ -0,0 +1,535 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const input = @import("../../input.zig");
|
||||
const CircBuf = @import("../../datastruct/main.zig").CircBuf;
|
||||
const cimgui = @import("dcimgui");
|
||||
|
||||
/// Circular buffer of key events.
|
||||
pub const EventRing = CircBuf(Event, undefined);
|
||||
|
||||
/// Represents a recorded keyboard event.
|
||||
pub const Event = struct {
|
||||
/// The input event.
|
||||
event: input.KeyEvent,
|
||||
|
||||
/// The binding that was triggered as a result of this event.
|
||||
/// Multiple bindings are possible if they are chained.
|
||||
binding: []const input.Binding.Action = &.{},
|
||||
|
||||
/// The data sent to the pty as a result of this keyboard event.
|
||||
/// This is allocated using the inspector allocator.
|
||||
pty: []const u8 = "",
|
||||
|
||||
/// State for the inspector GUI. Do not set this unless you're the inspector.
|
||||
imgui_state: struct {
|
||||
selected: bool = false,
|
||||
} = .{},
|
||||
|
||||
pub fn init(alloc: Allocator, ev: input.KeyEvent) !Event {
|
||||
var copy = ev;
|
||||
copy.utf8 = "";
|
||||
if (ev.utf8.len > 0) copy.utf8 = try alloc.dupe(u8, ev.utf8);
|
||||
return .{ .event = copy };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Event, alloc: Allocator) void {
|
||||
alloc.free(self.binding);
|
||||
if (self.event.utf8.len > 0) alloc.free(self.event.utf8);
|
||||
if (self.pty.len > 0) alloc.free(self.pty);
|
||||
}
|
||||
|
||||
/// Returns a label that can be used for this event. This is null-terminated
|
||||
/// so it can be easily used with C APIs.
|
||||
pub fn label(self: *const Event, buf: []u8) ![:0]const u8 {
|
||||
var buf_stream = std.io.fixedBufferStream(buf);
|
||||
const writer = buf_stream.writer();
|
||||
|
||||
switch (self.event.action) {
|
||||
.press => try writer.writeAll("Press: "),
|
||||
.release => try writer.writeAll("Release: "),
|
||||
.repeat => try writer.writeAll("Repeat: "),
|
||||
}
|
||||
|
||||
if (self.event.mods.shift) try writer.writeAll("Shift+");
|
||||
if (self.event.mods.ctrl) try writer.writeAll("Ctrl+");
|
||||
if (self.event.mods.alt) try writer.writeAll("Alt+");
|
||||
if (self.event.mods.super) try writer.writeAll("Super+");
|
||||
|
||||
// Write our key. If we have an invalid key we attempt to write
|
||||
// the utf8 associated with it if we have it to handle non-ascii.
|
||||
try writer.writeAll(switch (self.event.key) {
|
||||
.unidentified => if (self.event.utf8.len > 0) self.event.utf8 else @tagName(self.event.key),
|
||||
else => @tagName(self.event.key),
|
||||
});
|
||||
|
||||
// Deadkey
|
||||
if (self.event.composing) try writer.writeAll(" (composing)");
|
||||
|
||||
// Null-terminator
|
||||
try writer.writeByte(0);
|
||||
return buf[0..(buf_stream.getWritten().len - 1) :0];
|
||||
}
|
||||
|
||||
/// Render this event in the inspector GUI.
|
||||
pub fn render(self: *const Event) void {
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"##event",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
if (self.binding.len > 0) {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Triggered Binding");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
|
||||
const height: f32 = height: {
|
||||
const item_count: f32 = @floatFromInt(@min(self.binding.len, 5));
|
||||
const padding = cimgui.c.ImGui_GetStyle().*.FramePadding.y * 2;
|
||||
break :height cimgui.c.ImGui_GetTextLineHeightWithSpacing() * item_count + padding;
|
||||
};
|
||||
if (cimgui.c.ImGui_BeginListBox("##bindings", .{ .x = 0, .y = height })) {
|
||||
defer cimgui.c.ImGui_EndListBox();
|
||||
for (self.binding) |action| {
|
||||
_ = cimgui.c.ImGui_SelectableEx(
|
||||
@tagName(action).ptr,
|
||||
false,
|
||||
cimgui.c.ImGuiSelectableFlags_None,
|
||||
.{ .x = 0, .y = 0 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pty: {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Encoding to Pty");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (self.pty.len == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("(no data)");
|
||||
break :pty;
|
||||
}
|
||||
|
||||
self.renderPty() catch {
|
||||
cimgui.c.ImGui_TextDisabled("(error rendering pty data)");
|
||||
break :pty;
|
||||
};
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Action");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(self.event.action).ptr);
|
||||
}
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Key");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(self.event.key).ptr);
|
||||
}
|
||||
if (!self.event.mods.empty()) {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Mods");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (self.event.mods.shift) cimgui.c.ImGui_Text("shift ");
|
||||
if (self.event.mods.ctrl) cimgui.c.ImGui_Text("ctrl ");
|
||||
if (self.event.mods.alt) cimgui.c.ImGui_Text("alt ");
|
||||
if (self.event.mods.super) cimgui.c.ImGui_Text("super ");
|
||||
}
|
||||
if (self.event.composing) {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Composing");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("true");
|
||||
}
|
||||
utf8: {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("UTF-8");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (self.event.utf8.len == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("(empty)");
|
||||
break :utf8;
|
||||
}
|
||||
|
||||
self.renderUtf8(self.event.utf8) catch {
|
||||
cimgui.c.ImGui_TextDisabled("(error rendering utf-8)");
|
||||
break :utf8;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn renderUtf8(self: *const Event, utf8: []const u8) !void {
|
||||
_ = self;
|
||||
|
||||
// Format the codepoint sequence
|
||||
var buf: [1024]u8 = undefined;
|
||||
var buf_stream = std.io.fixedBufferStream(&buf);
|
||||
const writer = buf_stream.writer();
|
||||
if (std.unicode.Utf8View.init(utf8)) |view| {
|
||||
var it = view.iterator();
|
||||
while (it.nextCodepoint()) |cp| {
|
||||
try writer.print("U+{X} ", .{cp});
|
||||
}
|
||||
} else |_| {
|
||||
try writer.writeAll("(invalid utf-8)");
|
||||
}
|
||||
try writer.writeByte(0);
|
||||
|
||||
// Render as a textbox
|
||||
_ = cimgui.c.ImGui_InputText(
|
||||
"##utf8",
|
||||
&buf,
|
||||
buf_stream.getWritten().len - 1,
|
||||
cimgui.c.ImGuiInputTextFlags_ReadOnly,
|
||||
);
|
||||
}
|
||||
|
||||
fn renderPty(self: *const Event) !void {
|
||||
// Format the codepoint sequence
|
||||
var buf: [1024]u8 = undefined;
|
||||
var buf_stream = std.io.fixedBufferStream(&buf);
|
||||
const writer = buf_stream.writer();
|
||||
|
||||
for (self.pty) |byte| {
|
||||
// Print ESC special because its so common
|
||||
if (byte == 0x1B) {
|
||||
try writer.writeAll("ESC ");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Print ASCII as-is
|
||||
if (byte > 0x20 and byte < 0x7F) {
|
||||
try writer.writeByte(byte);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Everything else as a hex byte
|
||||
try writer.print("0x{X} ", .{byte});
|
||||
}
|
||||
|
||||
try writer.writeByte(0);
|
||||
|
||||
// Render as a textbox
|
||||
_ = cimgui.c.ImGui_InputText(
|
||||
"##pty",
|
||||
&buf,
|
||||
buf_stream.getWritten().len - 1,
|
||||
cimgui.c.ImGuiInputTextFlags_ReadOnly,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
fn modsTooltip(
|
||||
mods: *const input.Mods,
|
||||
buf: []u8,
|
||||
) ![:0]const u8 {
|
||||
var stream = std.io.fixedBufferStream(buf);
|
||||
const writer = stream.writer();
|
||||
var first = true;
|
||||
if (mods.shift) {
|
||||
try writer.writeAll("Shift");
|
||||
first = false;
|
||||
}
|
||||
if (mods.ctrl) {
|
||||
if (!first) try writer.writeAll("+");
|
||||
try writer.writeAll("Ctrl");
|
||||
first = false;
|
||||
}
|
||||
if (mods.alt) {
|
||||
if (!first) try writer.writeAll("+");
|
||||
try writer.writeAll("Alt");
|
||||
first = false;
|
||||
}
|
||||
if (mods.super) {
|
||||
if (!first) try writer.writeAll("+");
|
||||
try writer.writeAll("Super");
|
||||
}
|
||||
try writer.writeByte(0);
|
||||
const written = stream.getWritten();
|
||||
return written[0 .. written.len - 1 :0];
|
||||
}
|
||||
|
||||
/// Keyboard event stream inspector widget.
|
||||
pub const Stream = struct {
|
||||
events: EventRing,
|
||||
|
||||
pub fn init(alloc: Allocator) !Stream {
|
||||
var events: EventRing = try .init(alloc, 2);
|
||||
errdefer events.deinit(alloc);
|
||||
return .{ .events = events };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Stream, alloc: Allocator) void {
|
||||
var it = self.events.iterator(.forward);
|
||||
while (it.next()) |v| v.deinit(alloc);
|
||||
self.events.deinit(alloc);
|
||||
}
|
||||
|
||||
pub fn draw(
|
||||
self: *Stream,
|
||||
open: bool,
|
||||
alloc: Allocator,
|
||||
) void {
|
||||
if (!open) return;
|
||||
|
||||
if (self.events.empty()) {
|
||||
cimgui.c.ImGui_Text("No recorded key events. Press a key with the " ++
|
||||
"terminal focused to record it.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cimgui.c.ImGui_Button("Clear")) {
|
||||
var it = self.events.iterator(.forward);
|
||||
while (it.next()) |v| v.deinit(alloc);
|
||||
self.events.clear();
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_Separator();
|
||||
|
||||
const table_flags = cimgui.c.ImGuiTableFlags_Borders |
|
||||
cimgui.c.ImGuiTableFlags_Resizable |
|
||||
cimgui.c.ImGuiTableFlags_ScrollY |
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit;
|
||||
|
||||
if (!cimgui.c.ImGui_BeginTable("table_key_events", 6, table_flags)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableSetupScrollFreeze(0, 1);
|
||||
cimgui.c.ImGui_TableSetupColumnEx("Action", cimgui.c.ImGuiTableColumnFlags_WidthFixed, 80, 0);
|
||||
cimgui.c.ImGui_TableSetupColumnEx("Key", cimgui.c.ImGuiTableColumnFlags_WidthFixed, 160, 0);
|
||||
cimgui.c.ImGui_TableSetupColumnEx("Mods", cimgui.c.ImGuiTableColumnFlags_WidthFixed, 150, 0);
|
||||
cimgui.c.ImGui_TableSetupColumnEx("UTF-8", cimgui.c.ImGuiTableColumnFlags_WidthFixed, 80, 0);
|
||||
cimgui.c.ImGui_TableSetupColumnEx("PTY Encoding", cimgui.c.ImGuiTableColumnFlags_WidthStretch, 0, 0);
|
||||
cimgui.c.ImGui_TableSetupColumnEx("Binding", cimgui.c.ImGuiTableColumnFlags_WidthStretch, 0, 0);
|
||||
cimgui.c.ImGui_TableHeadersRow();
|
||||
|
||||
var it = self.events.iterator(.reverse);
|
||||
while (it.next()) |ev| {
|
||||
cimgui.c.ImGui_PushIDPtr(ev);
|
||||
defer cimgui.c.ImGui_PopID();
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
const row_min_y = cimgui.c.ImGui_GetCursorScreenPos().y;
|
||||
|
||||
// Set row background color based on action
|
||||
cimgui.c.ImGui_TableSetBgColor(cimgui.c.ImGuiTableBgTarget_RowBg0, actionColor(ev.event.action), -1);
|
||||
|
||||
// Action column with colored text
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
const action_text_color: cimgui.c.ImVec4 = switch (ev.event.action) {
|
||||
.press => .{ .x = 0.4, .y = 1.0, .z = 0.4, .w = 1.0 }, // Green
|
||||
.release => .{ .x = 0.6, .y = 0.6, .z = 1.0, .w = 1.0 }, // Blue
|
||||
.repeat => .{ .x = 1.0, .y = 1.0, .z = 0.4, .w = 1.0 }, // Yellow
|
||||
};
|
||||
cimgui.c.ImGui_TextColored(action_text_color, "%s", @tagName(ev.event.action).ptr);
|
||||
|
||||
// Key column with consistent key coloring
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
const key_name = switch (ev.event.key) {
|
||||
.unidentified => if (ev.event.utf8.len > 0) ev.event.utf8 else @tagName(ev.event.key),
|
||||
else => @tagName(ev.event.key),
|
||||
};
|
||||
const key_rgba = keyColor(ev.event.key);
|
||||
const key_color: cimgui.c.ImVec4 = .{
|
||||
.x = @as(f32, @floatFromInt(key_rgba & 0xFF)) / 255.0,
|
||||
.y = @as(f32, @floatFromInt((key_rgba >> 8) & 0xFF)) / 255.0,
|
||||
.z = @as(f32, @floatFromInt((key_rgba >> 16) & 0xFF)) / 255.0,
|
||||
.w = 1.0,
|
||||
};
|
||||
cimgui.c.ImGui_TextColored(key_color, "%s", key_name.ptr);
|
||||
|
||||
// Composing indicator
|
||||
if (ev.event.composing) {
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.6, .z = 0.0, .w = 1.0 }, "*");
|
||||
if (cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None)) {
|
||||
cimgui.c.ImGui_SetTooltip("Composing (dead key)");
|
||||
}
|
||||
}
|
||||
|
||||
// Mods
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
mods: {
|
||||
if (ev.event.mods.empty()) {
|
||||
cimgui.c.ImGui_TextDisabled("-");
|
||||
break :mods;
|
||||
}
|
||||
|
||||
var any_hovered = false;
|
||||
if (ev.event.mods.shift) {
|
||||
_ = cimgui.c.ImGui_SmallButton("S");
|
||||
any_hovered = any_hovered or cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None);
|
||||
cimgui.c.ImGui_SameLine();
|
||||
}
|
||||
if (ev.event.mods.ctrl) {
|
||||
_ = cimgui.c.ImGui_SmallButton("C");
|
||||
any_hovered = any_hovered or cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None);
|
||||
cimgui.c.ImGui_SameLine();
|
||||
}
|
||||
if (ev.event.mods.alt) {
|
||||
_ = cimgui.c.ImGui_SmallButton("A");
|
||||
any_hovered = any_hovered or cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None);
|
||||
cimgui.c.ImGui_SameLine();
|
||||
}
|
||||
if (ev.event.mods.super) {
|
||||
_ = cimgui.c.ImGui_SmallButton("M");
|
||||
any_hovered = any_hovered or cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None);
|
||||
cimgui.c.ImGui_SameLine();
|
||||
}
|
||||
cimgui.c.ImGui_NewLine();
|
||||
|
||||
if (any_hovered) tooltip: {
|
||||
var tooltip_buf: [64]u8 = undefined;
|
||||
const tooltip = modsTooltip(
|
||||
&ev.event.mods,
|
||||
&tooltip_buf,
|
||||
) catch break :tooltip;
|
||||
cimgui.c.ImGui_SetTooltip("%s", tooltip.ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// UTF-8
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(3);
|
||||
if (ev.event.utf8.len == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("-");
|
||||
} else {
|
||||
var utf8_buf: [128]u8 = undefined;
|
||||
var utf8_stream = std.io.fixedBufferStream(&utf8_buf);
|
||||
const utf8_writer = utf8_stream.writer();
|
||||
if (std.unicode.Utf8View.init(ev.event.utf8)) |view| {
|
||||
var utf8_it = view.iterator();
|
||||
while (utf8_it.nextCodepoint()) |cp| {
|
||||
utf8_writer.print("U+{X} ", .{cp}) catch break;
|
||||
}
|
||||
} else |_| {
|
||||
utf8_writer.writeAll("?") catch {};
|
||||
}
|
||||
utf8_writer.writeByte(0) catch {};
|
||||
cimgui.c.ImGui_Text("%s", &utf8_buf);
|
||||
}
|
||||
|
||||
// PTY
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(4);
|
||||
if (ev.pty.len == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("-");
|
||||
} else {
|
||||
var pty_buf: [256]u8 = undefined;
|
||||
var pty_stream = std.io.fixedBufferStream(&pty_buf);
|
||||
const pty_writer = pty_stream.writer();
|
||||
for (ev.pty) |byte| {
|
||||
if (byte == 0x1B) {
|
||||
pty_writer.writeAll("ESC ") catch break;
|
||||
} else if (byte > 0x20 and byte < 0x7F) {
|
||||
pty_writer.writeByte(byte) catch break;
|
||||
} else {
|
||||
pty_writer.print("0x{X} ", .{byte}) catch break;
|
||||
}
|
||||
}
|
||||
pty_writer.writeByte(0) catch {};
|
||||
cimgui.c.ImGui_Text("%s", &pty_buf);
|
||||
}
|
||||
|
||||
// Binding
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(5);
|
||||
if (ev.binding.len == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("-");
|
||||
} else {
|
||||
var binding_buf: [256]u8 = undefined;
|
||||
var binding_stream = std.io.fixedBufferStream(&binding_buf);
|
||||
const binding_writer = binding_stream.writer();
|
||||
for (ev.binding, 0..) |action, i| {
|
||||
if (i > 0) binding_writer.writeAll(", ") catch break;
|
||||
binding_writer.writeAll(@tagName(action)) catch break;
|
||||
}
|
||||
binding_writer.writeByte(0) catch {};
|
||||
cimgui.c.ImGui_Text("%s", &binding_buf);
|
||||
}
|
||||
|
||||
// Row hover highlight
|
||||
const row_max_y = cimgui.c.ImGui_GetCursorScreenPos().y;
|
||||
const mouse_pos = cimgui.c.ImGui_GetMousePos();
|
||||
if (mouse_pos.y >= row_min_y and mouse_pos.y < row_max_y) {
|
||||
cimgui.c.ImGui_TableSetBgColor(cimgui.c.ImGuiTableBgTarget_RowBg1, 0x1AFFFFFF, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns row background color for an action (ABGR format for ImGui)
|
||||
fn actionColor(action: input.Action) u32 {
|
||||
return switch (action) {
|
||||
.press => 0x1A4A6F4A, // Muted sage green
|
||||
.release => 0x1A6A5A5A, // Muted slate gray
|
||||
.repeat => 0x1A4A5A6F, // Muted warm brown
|
||||
};
|
||||
}
|
||||
|
||||
/// Generate a consistent color for a key based on its enum value.
|
||||
/// Uses HSV color space with fixed saturation and value for pleasing colors.
|
||||
fn keyColor(key: input.Key) u32 {
|
||||
const key_int: u32 = @intCast(@intFromEnum(key));
|
||||
const hue: f32 = @as(f32, @floatFromInt(key_int *% 47)) / 256.0;
|
||||
return hsvToRgba(hue, 0.5, 0.9, 1.0);
|
||||
}
|
||||
|
||||
/// Convert HSV (hue 0-1, saturation 0-1, value 0-1) to RGBA u32.
|
||||
fn hsvToRgba(h: f32, s: f32, v: f32, a: f32) u32 {
|
||||
var r: f32 = undefined;
|
||||
var g: f32 = undefined;
|
||||
var b: f32 = undefined;
|
||||
|
||||
const i: u32 = @intFromFloat(h * 6.0);
|
||||
const f = h * 6.0 - @as(f32, @floatFromInt(i));
|
||||
const p = v * (1.0 - s);
|
||||
const q = v * (1.0 - f * s);
|
||||
const t = v * (1.0 - (1.0 - f) * s);
|
||||
|
||||
switch (i % 6) {
|
||||
0 => {
|
||||
r = v;
|
||||
g = t;
|
||||
b = p;
|
||||
},
|
||||
1 => {
|
||||
r = q;
|
||||
g = v;
|
||||
b = p;
|
||||
},
|
||||
2 => {
|
||||
r = p;
|
||||
g = v;
|
||||
b = t;
|
||||
},
|
||||
3 => {
|
||||
r = p;
|
||||
g = q;
|
||||
b = v;
|
||||
},
|
||||
4 => {
|
||||
r = t;
|
||||
g = p;
|
||||
b = v;
|
||||
},
|
||||
else => {
|
||||
r = v;
|
||||
g = p;
|
||||
b = q;
|
||||
},
|
||||
}
|
||||
|
||||
const ri: u32 = @intFromFloat(r * 255.0);
|
||||
const gi: u32 = @intFromFloat(g * 255.0);
|
||||
const bi: u32 = @intFromFloat(b * 255.0);
|
||||
const ai: u32 = @intFromFloat(a * 255.0);
|
||||
|
||||
return (ai << 24) | (bi << 16) | (gi << 8) | ri;
|
||||
}
|
||||
428
src/inspector/widgets/page.zig
Normal file
428
src/inspector/widgets/page.zig
Normal file
@@ -0,0 +1,428 @@
|
||||
const std = @import("std");
|
||||
const cimgui = @import("dcimgui");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const units = @import("../units.zig");
|
||||
const widgets = @import("../widgets.zig");
|
||||
|
||||
const PageList = terminal.PageList;
|
||||
const Page = terminal.Page;
|
||||
|
||||
pub fn inspector(page: *const terminal.Page) void {
|
||||
cimgui.c.ImGui_SeparatorText("Managed Memory");
|
||||
managedMemory(page);
|
||||
|
||||
cimgui.c.ImGui_SeparatorText("Styles");
|
||||
stylesList(page);
|
||||
|
||||
cimgui.c.ImGui_SeparatorText("Hyperlinks");
|
||||
hyperlinksList(page);
|
||||
|
||||
cimgui.c.ImGui_SeparatorText("Rows");
|
||||
rowsTable(page);
|
||||
}
|
||||
|
||||
/// Draw a tree node header with metadata about this page. Returns if
|
||||
/// the tree node is open or not. If it is open you must close it with
|
||||
/// TreePop.
|
||||
pub fn treeNode(state: struct {
|
||||
/// The page
|
||||
page: *const terminal.Page,
|
||||
/// The index of the page in a page list, used for headers.
|
||||
index: usize,
|
||||
/// The range of rows this page covers, inclusive.
|
||||
row_range: [2]usize,
|
||||
/// Whether this page is the active or viewport node.
|
||||
active: bool,
|
||||
viewport: bool,
|
||||
}) bool {
|
||||
// Setup our node.
|
||||
const open = open: {
|
||||
var label_buf: [160]u8 = undefined;
|
||||
const label = std.fmt.bufPrintZ(
|
||||
&label_buf,
|
||||
"Page {d}",
|
||||
.{state.index},
|
||||
) catch "Page";
|
||||
|
||||
const flags = cimgui.c.ImGuiTreeNodeFlags_AllowOverlap |
|
||||
cimgui.c.ImGuiTreeNodeFlags_SpanFullWidth |
|
||||
cimgui.c.ImGuiTreeNodeFlags_FramePadding;
|
||||
break :open cimgui.c.ImGui_TreeNodeEx(label.ptr, flags);
|
||||
};
|
||||
|
||||
// Move our cursor into the tree header so we can add extra info.
|
||||
const header_min = cimgui.c.ImGui_GetItemRectMin();
|
||||
const header_max = cimgui.c.ImGui_GetItemRectMax();
|
||||
const header_height = header_max.y - header_min.y;
|
||||
const text_line = cimgui.c.ImGui_GetTextLineHeight();
|
||||
const y_center = header_min.y + (header_height - text_line) * 0.5;
|
||||
cimgui.c.ImGui_SetCursorScreenPos(.{ .x = header_min.x + 170, .y = y_center });
|
||||
|
||||
// Metadata
|
||||
cimgui.c.ImGui_TextDisabled(
|
||||
"%dc x %dr",
|
||||
state.page.size.cols,
|
||||
state.page.size.rows,
|
||||
);
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_Text("rows %d..%d", state.row_range[0], state.row_range[1]);
|
||||
|
||||
// Labels
|
||||
if (state.active) {
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.9, .z = 0.4, .w = 1.0 }, "active");
|
||||
}
|
||||
if (state.viewport) {
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.8, .z = 1.0, .w = 1.0 }, "viewport");
|
||||
}
|
||||
if (state.page.isDirty()) {
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.4, .z = 0.4, .w = 1.0 }, "dirty");
|
||||
}
|
||||
|
||||
return open;
|
||||
}
|
||||
|
||||
pub fn managedMemory(page: *const Page) void {
|
||||
if (cimgui.c.ImGui_BeginTable(
|
||||
"##overview",
|
||||
3,
|
||||
cimgui.c.ImGuiTableFlags_BordersInnerV |
|
||||
cimgui.c.ImGuiTableFlags_RowBg |
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit,
|
||||
)) {
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Memory Size");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker(
|
||||
"Memory allocated for this page. Note the backing memory " ++
|
||||
"may be a larger allocation from which this page " ++
|
||||
"uses a portion.",
|
||||
);
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text(
|
||||
"%d KiB",
|
||||
units.toKibiBytes(page.memory.len),
|
||||
);
|
||||
}
|
||||
|
||||
if (cimgui.c.ImGui_BeginTable(
|
||||
"##managed",
|
||||
4,
|
||||
cimgui.c.ImGuiTableFlags_BordersInnerV |
|
||||
cimgui.c.ImGuiTableFlags_RowBg |
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit,
|
||||
)) {
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableSetupColumn("Resource", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Used", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Capacity", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableHeadersRow();
|
||||
|
||||
const size = page.size;
|
||||
const cap = page.capacity;
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Columns");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Number of columns in the terminal grid.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", size.cols);
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(3);
|
||||
cimgui.c.ImGui_Text("%d", cap.cols);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Rows");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Number of rows in this page.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", size.rows);
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(3);
|
||||
cimgui.c.ImGui_Text("%d", cap.rows);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Styles");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Unique text styles (colors, attributes) currently in use.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", page.styles.count());
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(3);
|
||||
cimgui.c.ImGui_Text("%d", page.styles.layout.cap);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Graphemes");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Extended grapheme clusters for multi-codepoint characters.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", page.graphemeCount());
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(3);
|
||||
cimgui.c.ImGui_Text("%d", page.graphemeCapacity());
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Strings (bytes)");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("String storage for hyperlink URIs and other text data.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", page.string_alloc.usedBytes(page.memory));
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(3);
|
||||
cimgui.c.ImGui_Text("%d", page.string_alloc.capacityBytes());
|
||||
|
||||
const hyperlink_map = page.hyperlink_map.map(page.memory);
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Hyperlink Map");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Maps cell positions to hyperlink IDs.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", hyperlink_map.count());
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(3);
|
||||
cimgui.c.ImGui_Text("%d", hyperlink_map.capacity());
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Hyperlink IDs");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Unique hyperlink definitions (URI + optional ID).");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", page.hyperlink_set.count());
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(3);
|
||||
cimgui.c.ImGui_Text("%d", page.hyperlink_set.layout.cap);
|
||||
}
|
||||
}
|
||||
|
||||
fn rowsTable(page: *const terminal.Page) void {
|
||||
const visible_rows: usize = @min(page.size.rows, 12);
|
||||
const row_height: f32 = cimgui.c.ImGui_GetTextLineHeightWithSpacing();
|
||||
const child_height: f32 = row_height * (@as(f32, @floatFromInt(visible_rows)) + 2.0);
|
||||
|
||||
// Child window so scrolling is separate.
|
||||
// This defer first is not a bug, EndChild always needs to be called.
|
||||
defer cimgui.c.ImGui_EndChild();
|
||||
if (!cimgui.c.ImGui_BeginChild(
|
||||
"##page_rows",
|
||||
.{ .x = 0.0, .y = child_height },
|
||||
cimgui.c.ImGuiChildFlags_Borders,
|
||||
cimgui.c.ImGuiWindowFlags_None,
|
||||
)) return;
|
||||
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"##page_rows_table",
|
||||
10,
|
||||
cimgui.c.ImGuiTableFlags_BordersInnerV |
|
||||
cimgui.c.ImGuiTableFlags_RowBg |
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableSetupScrollFreeze(0, 1);
|
||||
cimgui.c.ImGui_TableSetupColumn("Row", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Text", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Dirty", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Wrap", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Cont", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Styled", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Grapheme", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Link", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Prompt", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Kitty", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableHeadersRow();
|
||||
|
||||
const rows = page.rows.ptr(page.memory)[0..page.size.rows];
|
||||
for (rows, 0..) |*row, row_index| {
|
||||
var text_cells: usize = 0;
|
||||
const cells = page.getCells(row);
|
||||
for (cells) |cell| {
|
||||
if (cell.hasText()) {
|
||||
text_cells += 1;
|
||||
}
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("%d", row_index);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (text_cells == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("0");
|
||||
} else {
|
||||
cimgui.c.ImGui_Text("%d", text_cells);
|
||||
}
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
flagCell(row.dirty);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(3);
|
||||
flagCell(row.wrap);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(4);
|
||||
flagCell(row.wrap_continuation);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(5);
|
||||
flagCell(row.styled);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(6);
|
||||
flagCell(row.grapheme);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(7);
|
||||
flagCell(row.hyperlink);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(8);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(row.semantic_prompt).ptr);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(9);
|
||||
flagCell(row.kitty_virtual_placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
fn stylesList(page: *const Page) void {
|
||||
const items = page.styles.items.ptr(page.memory)[0..page.styles.layout.cap];
|
||||
|
||||
var count: usize = 0;
|
||||
for (items, 0..) |item, index| {
|
||||
if (index == 0) continue;
|
||||
if (item.meta.ref == 0) continue;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("(no styles in use)");
|
||||
return;
|
||||
}
|
||||
|
||||
const visible_rows: usize = @min(count, 8);
|
||||
const row_height: f32 = cimgui.c.ImGui_GetTextLineHeightWithSpacing();
|
||||
const child_height: f32 = row_height * (@as(f32, @floatFromInt(visible_rows)) + 2.0);
|
||||
|
||||
defer cimgui.c.ImGui_EndChild();
|
||||
if (!cimgui.c.ImGui_BeginChild(
|
||||
"##page_styles",
|
||||
.{ .x = 0.0, .y = child_height },
|
||||
cimgui.c.ImGuiChildFlags_Borders,
|
||||
cimgui.c.ImGuiWindowFlags_None,
|
||||
)) return;
|
||||
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"##page_styles_table",
|
||||
3,
|
||||
cimgui.c.ImGuiTableFlags_BordersInnerV |
|
||||
cimgui.c.ImGuiTableFlags_RowBg |
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableSetupScrollFreeze(0, 1);
|
||||
cimgui.c.ImGui_TableSetupColumn("ID", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Refs", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Style", cimgui.c.ImGuiTableColumnFlags_WidthStretch);
|
||||
cimgui.c.ImGui_TableHeadersRow();
|
||||
|
||||
for (items, 0..) |item, index| {
|
||||
if (index == 0) continue;
|
||||
if (item.meta.ref == 0) continue;
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
cimgui.c.ImGui_PushIDInt(@intCast(index));
|
||||
defer cimgui.c.ImGui_PopID();
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("%d", index);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d", item.meta.ref);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
if (cimgui.c.ImGui_TreeNodeEx("Details", cimgui.c.ImGuiTreeNodeFlags_None)) {
|
||||
defer cimgui.c.ImGui_TreePop();
|
||||
widgets.style.table(item.value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hyperlinksList(page: *const Page) void {
|
||||
const items = page.hyperlink_set.items.ptr(page.memory)[0..page.hyperlink_set.layout.cap];
|
||||
|
||||
var count: usize = 0;
|
||||
for (items, 0..) |item, index| {
|
||||
if (index == 0) continue;
|
||||
if (item.meta.ref == 0) continue;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("(no hyperlinks in use)");
|
||||
return;
|
||||
}
|
||||
|
||||
const visible_rows: usize = @min(count, 8);
|
||||
const row_height: f32 = cimgui.c.ImGui_GetTextLineHeightWithSpacing();
|
||||
const child_height: f32 = row_height * (@as(f32, @floatFromInt(visible_rows)) + 2.0);
|
||||
|
||||
defer cimgui.c.ImGui_EndChild();
|
||||
if (!cimgui.c.ImGui_BeginChild(
|
||||
"##page_hyperlinks",
|
||||
.{ .x = 0.0, .y = child_height },
|
||||
cimgui.c.ImGuiChildFlags_Borders,
|
||||
cimgui.c.ImGuiWindowFlags_None,
|
||||
)) return;
|
||||
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"##page_hyperlinks_table",
|
||||
4,
|
||||
cimgui.c.ImGuiTableFlags_BordersInnerV |
|
||||
cimgui.c.ImGuiTableFlags_RowBg |
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableSetupScrollFreeze(0, 1);
|
||||
cimgui.c.ImGui_TableSetupColumn("ID", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Refs", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Explicit ID", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("URI", cimgui.c.ImGuiTableColumnFlags_WidthStretch);
|
||||
cimgui.c.ImGui_TableHeadersRow();
|
||||
|
||||
for (items, 0..) |item, index| {
|
||||
if (index == 0) continue;
|
||||
if (item.meta.ref == 0) continue;
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("%d", index);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d", item.meta.ref);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
switch (item.value.id) {
|
||||
.explicit => |slice| {
|
||||
const explicit_id = slice.slice(page.memory);
|
||||
cimgui.c.ImGui_Text("%.*s", explicit_id.len, explicit_id.ptr);
|
||||
},
|
||||
.implicit => cimgui.c.ImGui_TextDisabled("-"),
|
||||
}
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(3);
|
||||
const uri = item.value.uri.slice(page.memory);
|
||||
cimgui.c.ImGui_Text("%.*s", uri.len, uri.ptr);
|
||||
}
|
||||
}
|
||||
|
||||
fn flagCell(value: bool) void {
|
||||
if (value) {
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.9, .z = 0.4, .w = 1.0 }, "yes");
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("-");
|
||||
}
|
||||
}
|
||||
852
src/inspector/widgets/pagelist.zig
Normal file
852
src/inspector/widgets/pagelist.zig
Normal file
@@ -0,0 +1,852 @@
|
||||
const std = @import("std");
|
||||
const cimgui = @import("dcimgui");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const stylepkg = @import("../../terminal/style.zig");
|
||||
const widgets = @import("../widgets.zig");
|
||||
const units = @import("../units.zig");
|
||||
|
||||
const PageList = terminal.PageList;
|
||||
|
||||
/// PageList inspector widget.
|
||||
pub const Inspector = struct {
|
||||
pub const empty: Inspector = .{};
|
||||
|
||||
pub fn draw(_: *const Inspector, pages: *PageList) void {
|
||||
cimgui.c.ImGui_TextWrapped(
|
||||
"PageList manages the backing pages that hold scrollback and the active " ++
|
||||
"terminal grid. Each page is a contiguous memory buffer with its " ++
|
||||
"own rows, cells, style set, grapheme map, and hyperlink storage.",
|
||||
);
|
||||
|
||||
if (cimgui.c.ImGui_CollapsingHeader(
|
||||
"Overview",
|
||||
cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
|
||||
)) {
|
||||
summaryTable(pages);
|
||||
}
|
||||
|
||||
if (cimgui.c.ImGui_CollapsingHeader(
|
||||
"Scrollbar & Regions",
|
||||
cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
|
||||
)) {
|
||||
cimgui.c.ImGui_SeparatorText("Scrollbar");
|
||||
scrollbarInfo(pages);
|
||||
cimgui.c.ImGui_SeparatorText("Regions");
|
||||
regionsTable(pages);
|
||||
}
|
||||
|
||||
if (cimgui.c.ImGui_CollapsingHeader(
|
||||
"Tracked Pins",
|
||||
cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
|
||||
)) {
|
||||
trackedPinsTable(pages);
|
||||
}
|
||||
|
||||
if (cimgui.c.ImGui_CollapsingHeader(
|
||||
"Pages",
|
||||
cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
|
||||
)) {
|
||||
widgets.helpMarker(
|
||||
"Pages are shown most-recent first. Each page holds a grid of rows/cells " ++
|
||||
"plus metadata tables for styles, graphemes, strings, and hyperlinks.",
|
||||
);
|
||||
|
||||
const active_pin = pages.getTopLeft(.active);
|
||||
const viewport_pin = pages.getTopLeft(.viewport);
|
||||
|
||||
var row_offset = pages.total_rows;
|
||||
var index: usize = pages.totalPages();
|
||||
var node = pages.pages.last;
|
||||
while (node) |page_node| : (node = page_node.prev) {
|
||||
const page = &page_node.data;
|
||||
row_offset -= page.size.rows;
|
||||
index -= 1;
|
||||
|
||||
// We use our location as the ID so that even if reallocations
|
||||
// happen we remain open if we're open already.
|
||||
cimgui.c.ImGui_PushIDInt(@intCast(index));
|
||||
defer cimgui.c.ImGui_PopID();
|
||||
|
||||
// Open up the tree node.
|
||||
if (!widgets.page.treeNode(.{
|
||||
.page = page,
|
||||
.index = index,
|
||||
.row_range = .{ row_offset, row_offset + page.size.rows - 1 },
|
||||
.active = node == active_pin.node,
|
||||
.viewport = node == viewport_pin.node,
|
||||
})) continue;
|
||||
defer cimgui.c.ImGui_TreePop();
|
||||
widgets.page.inspector(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn summaryTable(pages: *const PageList) void {
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"pagelist_summary",
|
||||
3,
|
||||
cimgui.c.ImGuiTableFlags_BordersInnerV |
|
||||
cimgui.c.ImGuiTableFlags_RowBg |
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Active Grid");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Active viewport size in columns x rows.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%dc x %dr", pages.cols, pages.rows);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Pages");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Total number of pages in the linked list.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", pages.totalPages());
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Total Rows");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Total rows represented by scrollback + active area.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", pages.total_rows);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Page Bytes");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Total bytes allocated for active pages.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text(
|
||||
"%d KiB",
|
||||
units.toKibiBytes(pages.page_size),
|
||||
);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Max Size");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker(
|
||||
\\Maximum bytes before pages must be evicated. The total
|
||||
\\used bytes may be higher due to minimum individual page
|
||||
\\sizes but the next allocation that would exceed this limit
|
||||
\\will evict pages from the front of the list to free up space.
|
||||
);
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text(
|
||||
"%d KiB",
|
||||
units.toKibiBytes(pages.maxSize()),
|
||||
);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Viewport");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Current viewport anchoring mode.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(pages.viewport).ptr);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Tracked Pins");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Number of pins tracked for automatic updates.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", pages.countTrackedPins());
|
||||
}
|
||||
|
||||
fn scrollbarInfo(pages: *PageList) void {
|
||||
const scrollbar = pages.scrollbar();
|
||||
|
||||
// If we have a scrollbar, show it.
|
||||
if (scrollbar.total > 0) {
|
||||
var delta_row: isize = 0;
|
||||
scrollbarWidget(&scrollbar, &delta_row);
|
||||
if (delta_row != 0) {
|
||||
pages.scroll(.{ .delta_row = delta_row });
|
||||
}
|
||||
}
|
||||
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"scrollbar_info",
|
||||
3,
|
||||
cimgui.c.ImGuiTableFlags_BordersInnerV |
|
||||
cimgui.c.ImGuiTableFlags_RowBg |
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Total");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Total number of scrollable rows including scrollback and active area.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", scrollbar.total);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Offset");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Current scroll position as row offset from the top of scrollback.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", scrollbar.offset);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Length");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Number of rows visible in the viewport.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", scrollbar.len);
|
||||
}
|
||||
|
||||
fn regionsTable(pages: *PageList) void {
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"pagelist_regions",
|
||||
4,
|
||||
cimgui.c.ImGuiTableFlags_BordersInnerV |
|
||||
cimgui.c.ImGuiTableFlags_RowBg |
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableSetupColumn("Region", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Top-Left", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Bottom-Right", cimgui.c.ImGuiTableColumnFlags_WidthStretch);
|
||||
cimgui.c.ImGui_TableHeadersRow();
|
||||
|
||||
inline for (comptime std.meta.tags(terminal.point.Tag)) |tag| {
|
||||
regionRow(pages, tag);
|
||||
}
|
||||
}
|
||||
|
||||
fn regionRow(pages: *const PageList, comptime tag: terminal.point.Tag) void {
|
||||
const tl_pin = pages.getTopLeft(tag);
|
||||
const br_pin = pages.getBottomRight(tag);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(tag).ptr);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker(comptime regionHelpText(tag));
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
if (pages.pointFromPin(tag, tl_pin)) |pt| {
|
||||
const coord = pt.coord();
|
||||
cimgui.c.ImGui_Text("(%d, %d)", coord.x, coord.y);
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("(n/a)");
|
||||
}
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(3);
|
||||
if (br_pin) |br| {
|
||||
if (pages.pointFromPin(tag, br)) |pt| {
|
||||
const coord = pt.coord();
|
||||
cimgui.c.ImGui_Text("(%d, %d)", coord.x, coord.y);
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("(n/a)");
|
||||
}
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("(empty)");
|
||||
}
|
||||
}
|
||||
|
||||
fn regionHelpText(comptime tag: terminal.point.Tag) [:0]const u8 {
|
||||
return switch (tag) {
|
||||
.active => "The active area where a running program can jump the cursor " ++
|
||||
"and make changes. This is the 'editable' part of the screen. " ++
|
||||
"Bottom-right includes the full height of the screen, including " ++
|
||||
"rows that may not be written yet.",
|
||||
.viewport => "The visible viewport. If the user has scrolled, top-left changes. " ++
|
||||
"Bottom-right is the last written row from the top-left.",
|
||||
.screen => "Top-left is the furthest back in scrollback history. Bottom-right " ++
|
||||
"is the last written row. Unlike 'active', this only contains " ++
|
||||
"written rows.",
|
||||
.history => "Same top-left as 'screen' but bottom-right is the line just before " ++
|
||||
"the top of 'active'. Contains only the scrollback history.",
|
||||
};
|
||||
}
|
||||
|
||||
fn trackedPinsTable(pages: *const PageList) void {
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"tracked_pins",
|
||||
5,
|
||||
cimgui.c.ImGuiTableFlags_Borders |
|
||||
cimgui.c.ImGuiTableFlags_RowBg |
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableSetupColumn("Index", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Pin", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Context", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Dirty", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("State", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableHeadersRow();
|
||||
|
||||
const active_pin = pages.getTopLeft(.active);
|
||||
const viewport_pin = pages.getTopLeft(.viewport);
|
||||
|
||||
for (pages.trackedPins(), 0..) |tracked, idx| {
|
||||
const pin = tracked.*;
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("%d", idx);
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (pin.garbage) {
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.5, .z = 0.3, .w = 1.0 }, "(%d, %d)", pin.x, pin.y);
|
||||
} else {
|
||||
cimgui.c.ImGui_Text("(%d, %d)", pin.x, pin.y);
|
||||
}
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
if (pages.pointFromPin(.screen, pin)) |pt| {
|
||||
const coord = pt.coord();
|
||||
cimgui.c.ImGui_Text(
|
||||
"screen (%d, %d)",
|
||||
coord.x,
|
||||
coord.y,
|
||||
);
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("screen (out of range)");
|
||||
}
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(3);
|
||||
const dirty = pin.isDirty();
|
||||
if (dirty) {
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.4, .z = 0.4, .w = 1.0 }, "dirty");
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("clean");
|
||||
}
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(4);
|
||||
if (pin.eql(active_pin)) {
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.9, .z = 0.4, .w = 1.0 }, "active top");
|
||||
} else if (pin.eql(viewport_pin)) {
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.8, .z = 1.0, .w = 1.0 }, "viewport top");
|
||||
} else if (pin.garbage) {
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.5, .z = 0.3, .w = 1.0 }, "garbage");
|
||||
} else if (tracked == pages.viewport_pin) {
|
||||
cimgui.c.ImGui_Text("viewport pin");
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("tracked");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scrollbarWidget(
|
||||
scrollbar: *const PageList.Scrollbar,
|
||||
delta_row: *isize,
|
||||
) void {
|
||||
delta_row.* = 0;
|
||||
|
||||
const avail_width = cimgui.c.ImGui_GetContentRegionAvail().x;
|
||||
const bar_height: f32 = cimgui.c.ImGui_GetFrameHeight();
|
||||
const cursor_pos = cimgui.c.ImGui_GetCursorScreenPos();
|
||||
|
||||
const total_f: f32 = @floatFromInt(scrollbar.total);
|
||||
const offset_f: f32 = @floatFromInt(scrollbar.offset);
|
||||
const len_f: f32 = @floatFromInt(scrollbar.len);
|
||||
|
||||
const grab_start = (offset_f / total_f) * avail_width;
|
||||
const grab_width = @max((len_f / total_f) * avail_width, 4.0);
|
||||
|
||||
const draw_list = cimgui.c.ImGui_GetWindowDrawList();
|
||||
const bg_color = cimgui.c.ImGui_GetColorU32(cimgui.c.ImGuiCol_ScrollbarBg);
|
||||
const grab_color = cimgui.c.ImGui_GetColorU32(cimgui.c.ImGuiCol_ScrollbarGrab);
|
||||
|
||||
const bg_min: cimgui.c.ImVec2 = cursor_pos;
|
||||
const bg_max: cimgui.c.ImVec2 = .{ .x = cursor_pos.x + avail_width, .y = cursor_pos.y + bar_height };
|
||||
cimgui.c.ImDrawList_AddRectFilledEx(
|
||||
draw_list,
|
||||
bg_min,
|
||||
bg_max,
|
||||
bg_color,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
|
||||
const grab_min: cimgui.c.ImVec2 = .{
|
||||
.x = cursor_pos.x + grab_start,
|
||||
.y = cursor_pos.y,
|
||||
};
|
||||
const grab_max: cimgui.c.ImVec2 = .{
|
||||
.x = cursor_pos.x + grab_start + grab_width,
|
||||
.y = cursor_pos.y + bar_height,
|
||||
};
|
||||
cimgui.c.ImDrawList_AddRectFilledEx(
|
||||
draw_list,
|
||||
grab_min,
|
||||
grab_max,
|
||||
grab_color,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
_ = cimgui.c.ImGui_InvisibleButton(
|
||||
"scrollbar_drag",
|
||||
.{ .x = avail_width, .y = bar_height },
|
||||
0,
|
||||
);
|
||||
if (cimgui.c.ImGui_IsItemActive()) {
|
||||
const drag_delta = cimgui.c.ImGui_GetMouseDragDelta(
|
||||
cimgui.c.ImGuiMouseButton_Left,
|
||||
0.0,
|
||||
);
|
||||
if (drag_delta.x != 0) {
|
||||
const row_delta = (drag_delta.x / avail_width) * total_f;
|
||||
delta_row.* = @intFromFloat(row_delta);
|
||||
cimgui.c.ImGui_ResetMouseDragDelta();
|
||||
}
|
||||
}
|
||||
|
||||
if (cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_DelayShort)) {
|
||||
cimgui.c.ImGui_SetTooltip(
|
||||
"offset=%d len=%d total=%d",
|
||||
scrollbar.offset,
|
||||
scrollbar.len,
|
||||
scrollbar.total,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Grid inspector widget for choosing and inspecting a specific cell.
|
||||
pub const CellChooser = struct {
|
||||
lookup_region: terminal.point.Tag,
|
||||
lookup_coord: terminal.point.Coordinate,
|
||||
cell_info: CellInfo,
|
||||
|
||||
pub const empty: CellChooser = .{
|
||||
.lookup_region = .viewport,
|
||||
.lookup_coord = .{ .x = 0, .y = 0 },
|
||||
.cell_info = .empty,
|
||||
};
|
||||
|
||||
pub fn draw(
|
||||
self: *CellChooser,
|
||||
pages: *const PageList,
|
||||
) void {
|
||||
cimgui.c.ImGui_TextWrapped(
|
||||
"Inspect a cell by choosing a coordinate space and entering the X/Y position. " ++
|
||||
"The inspector resolves the point into the page list and displays the cell contents.",
|
||||
);
|
||||
|
||||
cimgui.c.ImGui_SeparatorText("Cell Inspector");
|
||||
|
||||
const region_max = maxCoord(pages, self.lookup_region);
|
||||
if (region_max) |coord| {
|
||||
self.lookup_coord.x = @min(self.lookup_coord.x, coord.x);
|
||||
self.lookup_coord.y = @min(self.lookup_coord.y, coord.y);
|
||||
} else {
|
||||
self.lookup_coord = .{ .x = 0, .y = 0 };
|
||||
}
|
||||
|
||||
{
|
||||
const disabled = region_max == null;
|
||||
cimgui.c.ImGui_BeginDisabled(disabled);
|
||||
defer cimgui.c.ImGui_EndDisabled();
|
||||
|
||||
const preview = @tagName(self.lookup_region);
|
||||
const combo_width = comptime blk: {
|
||||
var max_len: usize = 0;
|
||||
for (std.meta.tags(terminal.point.Tag)) |tag| {
|
||||
max_len = @max(max_len, @tagName(tag).len);
|
||||
}
|
||||
break :blk max_len + 4;
|
||||
};
|
||||
cimgui.c.ImGui_SetNextItemWidth(cimgui.c.ImGui_CalcTextSize("X" ** combo_width).x);
|
||||
if (cimgui.c.ImGui_BeginCombo(
|
||||
"##grid_region",
|
||||
preview.ptr,
|
||||
cimgui.c.ImGuiComboFlags_HeightSmall,
|
||||
)) {
|
||||
inline for (comptime std.meta.tags(terminal.point.Tag)) |tag| {
|
||||
const selected = tag == self.lookup_region;
|
||||
if (cimgui.c.ImGui_SelectableEx(
|
||||
@tagName(tag).ptr,
|
||||
selected,
|
||||
cimgui.c.ImGuiSelectableFlags_None,
|
||||
.{ .x = 0, .y = 0 },
|
||||
)) {
|
||||
self.lookup_region = tag;
|
||||
}
|
||||
if (selected) cimgui.c.ImGui_SetItemDefaultFocus();
|
||||
}
|
||||
cimgui.c.ImGui_EndCombo();
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_SameLine();
|
||||
|
||||
const width = cimgui.c.ImGui_CalcTextSize("00000").x;
|
||||
var x_value: terminal.size.CellCountInt = self.lookup_coord.x;
|
||||
var y_value: u32 = self.lookup_coord.y;
|
||||
var changed = false;
|
||||
|
||||
cimgui.c.ImGui_AlignTextToFramePadding();
|
||||
cimgui.c.ImGui_Text("x:");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_SetNextItemWidth(width);
|
||||
if (cimgui.c.ImGui_InputScalar(
|
||||
"##grid_x",
|
||||
cimgui.c.ImGuiDataType_U16,
|
||||
&x_value,
|
||||
)) changed = true;
|
||||
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_AlignTextToFramePadding();
|
||||
cimgui.c.ImGui_Text("y:");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_SetNextItemWidth(width);
|
||||
if (cimgui.c.ImGui_InputScalar(
|
||||
"##grid_y",
|
||||
cimgui.c.ImGuiDataType_U32,
|
||||
&y_value,
|
||||
)) changed = true;
|
||||
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("Choose the coordinate space and X/Y position (0-indexed).");
|
||||
|
||||
if (changed) {
|
||||
if (region_max) |coord| {
|
||||
self.lookup_coord.x = @min(x_value, coord.x);
|
||||
self.lookup_coord.y = @min(y_value, coord.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (region_max) |coord| {
|
||||
cimgui.c.ImGui_TextDisabled(
|
||||
"Range: x 0..%d, y 0..%d",
|
||||
coord.x,
|
||||
coord.y,
|
||||
);
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("(region has no rows)");
|
||||
return;
|
||||
}
|
||||
|
||||
const pt = switch (self.lookup_region) {
|
||||
.active => terminal.Point{ .active = self.lookup_coord },
|
||||
.viewport => terminal.Point{ .viewport = self.lookup_coord },
|
||||
.screen => terminal.Point{ .screen = self.lookup_coord },
|
||||
.history => terminal.Point{ .history = self.lookup_coord },
|
||||
};
|
||||
|
||||
const cell = pages.getCell(pt) orelse {
|
||||
cimgui.c.ImGui_TextDisabled("(cell out of range)");
|
||||
return;
|
||||
};
|
||||
|
||||
self.cell_info.draw(cell, pt);
|
||||
|
||||
if (cell.cell.style_id != stylepkg.default_id) {
|
||||
cimgui.c.ImGui_SeparatorText("Style");
|
||||
const style = cell.node.data.styles.get(
|
||||
cell.node.data.memory,
|
||||
cell.cell.style_id,
|
||||
).*;
|
||||
widgets.style.table(style, null);
|
||||
}
|
||||
|
||||
if (cell.cell.hyperlink) {
|
||||
cimgui.c.ImGui_SeparatorText("Hyperlink");
|
||||
hyperlinkTable(cell);
|
||||
}
|
||||
|
||||
if (cell.cell.hasGrapheme()) {
|
||||
cimgui.c.ImGui_SeparatorText("Grapheme");
|
||||
graphemeTable(cell);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn maxCoord(
|
||||
pages: *const PageList,
|
||||
tag: terminal.point.Tag,
|
||||
) ?terminal.point.Coordinate {
|
||||
const br_pin = pages.getBottomRight(tag) orelse return null;
|
||||
const br_point = pages.pointFromPin(tag, br_pin) orelse return null;
|
||||
return br_point.coord();
|
||||
}
|
||||
|
||||
fn hyperlinkTable(cell: PageList.Cell) void {
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"cell_hyperlink",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
const page = &cell.node.data;
|
||||
const link_id = page.lookupHyperlink(cell.cell) orelse {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Status");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_TextDisabled("(missing link data)");
|
||||
return;
|
||||
};
|
||||
|
||||
const entry = page.hyperlink_set.get(page.memory, link_id);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("ID");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
switch (entry.id) {
|
||||
.implicit => |value| cimgui.c.ImGui_Text("implicit %d", value),
|
||||
.explicit => |slice| {
|
||||
const id = slice.slice(page.memory);
|
||||
if (id.len == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("(empty)");
|
||||
} else {
|
||||
cimgui.c.ImGui_Text("%.*s", id.len, id.ptr);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("URI");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
const uri = entry.uri.slice(page.memory);
|
||||
if (uri.len == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("(empty)");
|
||||
} else {
|
||||
cimgui.c.ImGui_Text("%.*s", uri.len, uri.ptr);
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Ref Count");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
const refs = page.hyperlink_set.refCount(page.memory, link_id);
|
||||
cimgui.c.ImGui_Text("%d", refs);
|
||||
}
|
||||
|
||||
fn graphemeTable(cell: PageList.Cell) void {
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"cell_grapheme",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
const page = &cell.node.data;
|
||||
const cps = page.lookupGrapheme(cell.cell) orelse {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Status");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_TextDisabled("(missing grapheme data)");
|
||||
return;
|
||||
};
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Extra Codepoints");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (cps.len == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("(none)");
|
||||
return;
|
||||
}
|
||||
|
||||
var buf: [96]u8 = undefined;
|
||||
if (cimgui.c.ImGui_BeginListBox("##grapheme_list", .{ .x = 0, .y = 0 })) {
|
||||
defer cimgui.c.ImGui_EndListBox();
|
||||
for (cps) |cp| {
|
||||
const label = std.fmt.bufPrintZ(&buf, "U+{X}", .{cp}) catch "U+?";
|
||||
_ = cimgui.c.ImGui_SelectableEx(
|
||||
label.ptr,
|
||||
false,
|
||||
cimgui.c.ImGuiSelectableFlags_None,
|
||||
.{ .x = 0, .y = 0 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cell inspector widget.
|
||||
pub const CellInfo = struct {
|
||||
pub const empty: CellInfo = .{};
|
||||
|
||||
pub fn draw(
|
||||
_: *const CellInfo,
|
||||
cell: PageList.Cell,
|
||||
point: terminal.Point,
|
||||
) void {
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"cell_info",
|
||||
3,
|
||||
cimgui.c.ImGuiTableFlags_BordersInnerV |
|
||||
cimgui.c.ImGuiTableFlags_RowBg |
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Grid Position");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("The cell's X/Y coordinates in the selected region.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
const coord = point.coord();
|
||||
cimgui.c.ImGui_Text("(%d, %d)", coord.x, coord.y);
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Page Location");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Row and column indices within the backing page.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("row=%d col=%d", cell.row_idx, cell.col_idx);
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Content");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Content tag describing how the cell data is stored.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(cell.cell.content_tag).ptr);
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Codepoint");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Primary Unicode codepoint for the cell.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
const cp = cell.cell.codepoint();
|
||||
if (cp == 0) {
|
||||
cimgui.c.ImGui_TextDisabled("(empty)");
|
||||
} else {
|
||||
cimgui.c.ImGui_Text("U+%04X", @as(u32, cp));
|
||||
}
|
||||
}
|
||||
|
||||
if (cell.cell.hasGrapheme()) {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Grapheme");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Extra codepoints that combine with the primary codepoint to form the grapheme cluster.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
if (cimgui.c.ImGui_BeginListBox("##cell_grapheme", .{ .x = 0, .y = 0 })) {
|
||||
defer cimgui.c.ImGui_EndListBox();
|
||||
if (cell.node.data.lookupGrapheme(cell.cell)) |cps| {
|
||||
var buf: [96]u8 = undefined;
|
||||
for (cps) |cp| {
|
||||
const label = std.fmt.bufPrintZ(&buf, "U+{X}", .{cp}) catch "U+?";
|
||||
_ = cimgui.c.ImGui_SelectableEx(
|
||||
label.ptr,
|
||||
false,
|
||||
cimgui.c.ImGuiSelectableFlags_None,
|
||||
.{ .x = 0, .y = 0 },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_ = cimgui.c.ImGui_SelectableEx(
|
||||
"(missing)",
|
||||
false,
|
||||
cimgui.c.ImGuiSelectableFlags_None,
|
||||
.{ .x = 0, .y = 0 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Width Property");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Character width property (narrow, wide, spacer, etc.).");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(cell.cell.wide).ptr);
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Row Flags");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Flags set on the row containing this cell.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
const row = cell.row;
|
||||
if (row.wrap or row.wrap_continuation or row.grapheme or row.styled or row.hyperlink) {
|
||||
if (row.wrap) {
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.8, .z = 1.0, .w = 1.0 }, "wrap");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
}
|
||||
if (row.wrap_continuation) {
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.8, .z = 1.0, .w = 1.0 }, "cont");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
}
|
||||
if (row.grapheme) {
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 0.9, .y = 0.7, .z = 0.3, .w = 1.0 }, "grapheme");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
}
|
||||
if (row.styled) {
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 0.7, .y = 0.9, .z = 0.5, .w = 1.0 }, "styled");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
}
|
||||
if (row.hyperlink) {
|
||||
cimgui.c.ImGui_TextColored(.{ .x = 0.8, .y = 0.6, .z = 1.0, .w = 1.0 }, "link");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
}
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("(none)");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Style ID");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Internal style reference ID for this cell.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_Text("%d", cell.cell.style_id);
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Style");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("Resolved style for the cell (colors, attributes, etc.).");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
if (cell.cell.style_id == stylepkg.default_id) {
|
||||
cimgui.c.ImGui_TextDisabled("(default)");
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("(see below)");
|
||||
}
|
||||
}
|
||||
|
||||
if (cell.cell.hyperlink) {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Hyperlink");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
widgets.helpMarker("OSC8 hyperlink ID associated with this cell.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
|
||||
const link_id = cell.node.data.lookupHyperlink(cell.cell) orelse 0;
|
||||
cimgui.c.ImGui_Text("id=%d", link_id);
|
||||
}
|
||||
}
|
||||
};
|
||||
123
src/inspector/widgets/renderer.zig
Normal file
123
src/inspector/widgets/renderer.zig
Normal file
@@ -0,0 +1,123 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cimgui = @import("dcimgui");
|
||||
const widgets = @import("../widgets.zig");
|
||||
const renderer = @import("../../renderer.zig");
|
||||
|
||||
const log = std.log.scoped(.inspector_renderer);
|
||||
|
||||
/// Renderer information inspector widget.
|
||||
pub const Info = struct {
|
||||
features: std.AutoArrayHashMapUnmanaged(
|
||||
std.meta.Tag(renderer.Overlay.Feature),
|
||||
renderer.Overlay.Feature,
|
||||
),
|
||||
|
||||
pub const empty: Info = .{
|
||||
.features = .empty,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *Info, alloc: Allocator) void {
|
||||
self.features.deinit(alloc);
|
||||
}
|
||||
|
||||
/// Grab the features into a new allocated slice. This is used by
|
||||
pub fn overlayFeatures(
|
||||
self: *const Info,
|
||||
alloc: Allocator,
|
||||
) Allocator.Error![]renderer.Overlay.Feature {
|
||||
// The features from our internal state.
|
||||
const features = self.features.values();
|
||||
|
||||
// For now we do a dumb copy since the features have no managed
|
||||
// memory.
|
||||
const result = try alloc.dupe(
|
||||
renderer.Overlay.Feature,
|
||||
features,
|
||||
);
|
||||
errdefer alloc.free(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Draw the renderer info window.
|
||||
pub fn draw(
|
||||
self: *Info,
|
||||
alloc: Allocator,
|
||||
open: bool,
|
||||
) void {
|
||||
if (!open) return;
|
||||
|
||||
cimgui.c.ImGui_SetNextItemOpen(true, cimgui.c.ImGuiCond_Once);
|
||||
if (!cimgui.c.ImGui_CollapsingHeader("Overlays", cimgui.c.ImGuiTreeNodeFlags_None)) return;
|
||||
|
||||
cimgui.c.ImGui_SeparatorText("Hyperlinks");
|
||||
self.overlayHyperlinks(alloc);
|
||||
cimgui.c.ImGui_SeparatorText("Semantic Prompts");
|
||||
self.overlaySemanticPrompts(alloc);
|
||||
}
|
||||
|
||||
fn overlayHyperlinks(self: *Info, alloc: Allocator) void {
|
||||
var hyperlinks: bool = self.features.contains(.highlight_hyperlinks);
|
||||
_ = cimgui.c.ImGui_Checkbox("Overlay Hyperlinks", &hyperlinks);
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("When enabled, highlights OSC8 hyperlinks.");
|
||||
|
||||
if (!hyperlinks) {
|
||||
_ = self.features.swapRemove(.highlight_hyperlinks);
|
||||
} else {
|
||||
self.features.put(
|
||||
alloc,
|
||||
.highlight_hyperlinks,
|
||||
.highlight_hyperlinks,
|
||||
) catch log.warn("error enabling hyperlink overlay feature", .{});
|
||||
}
|
||||
}
|
||||
|
||||
fn overlaySemanticPrompts(self: *Info, alloc: Allocator) void {
|
||||
var semantic_prompts: bool = self.features.contains(.semantic_prompts);
|
||||
_ = cimgui.c.ImGui_Checkbox("Overlay Semantic Prompts", &semantic_prompts);
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("When enabled, highlights OSC 133 semantic prompts.");
|
||||
|
||||
// Handle the checkbox results
|
||||
if (!semantic_prompts) {
|
||||
_ = self.features.swapRemove(.semantic_prompts);
|
||||
} else {
|
||||
self.features.put(
|
||||
alloc,
|
||||
.semantic_prompts,
|
||||
.semantic_prompts,
|
||||
) catch log.warn("error enabling semantic prompt overlay feature", .{});
|
||||
}
|
||||
|
||||
// Help
|
||||
cimgui.c.ImGui_Indent();
|
||||
defer cimgui.c.ImGui_Unindent();
|
||||
|
||||
cimgui.c.ImGui_TextDisabled("Colors:");
|
||||
|
||||
const prompt_rgb = renderer.Overlay.Color.semantic_prompt.rgb();
|
||||
const input_rgb = renderer.Overlay.Color.semantic_input.rgb();
|
||||
const prompt_col: cimgui.c.ImVec4 = .{
|
||||
.x = @as(f32, @floatFromInt(prompt_rgb.r)) / 255.0,
|
||||
.y = @as(f32, @floatFromInt(prompt_rgb.g)) / 255.0,
|
||||
.z = @as(f32, @floatFromInt(prompt_rgb.b)) / 255.0,
|
||||
.w = 1.0,
|
||||
};
|
||||
const input_col: cimgui.c.ImVec4 = .{
|
||||
.x = @as(f32, @floatFromInt(input_rgb.r)) / 255.0,
|
||||
.y = @as(f32, @floatFromInt(input_rgb.g)) / 255.0,
|
||||
.z = @as(f32, @floatFromInt(input_rgb.b)) / 255.0,
|
||||
.w = 1.0,
|
||||
};
|
||||
|
||||
_ = cimgui.c.ImGui_ColorButton("##prompt_color", prompt_col, cimgui.c.ImGuiColorEditFlags_NoTooltip);
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_Text("Prompt");
|
||||
|
||||
_ = cimgui.c.ImGui_ColorButton("##input_color", input_col, cimgui.c.ImGuiColorEditFlags_NoTooltip);
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_Text("Input");
|
||||
}
|
||||
};
|
||||
369
src/inspector/widgets/screen.zig
Normal file
369
src/inspector/widgets/screen.zig
Normal file
@@ -0,0 +1,369 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = @import("../../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cimgui = @import("dcimgui");
|
||||
const widgets = @import("../widgets.zig");
|
||||
const units = @import("../units.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const stylepkg = @import("../../terminal/style.zig");
|
||||
|
||||
/// Window names for the screen dockspace.
|
||||
const window_info = "Info";
|
||||
const window_cell = "Cell";
|
||||
const window_pagelist = "PageList";
|
||||
|
||||
/// Screen information inspector widget.
|
||||
pub const Info = struct {
|
||||
pagelist: widgets.pagelist.Inspector,
|
||||
cell_chooser: widgets.pagelist.CellChooser,
|
||||
|
||||
pub const empty: Info = .{
|
||||
.pagelist = .empty,
|
||||
.cell_chooser = .empty,
|
||||
};
|
||||
|
||||
/// Draw the screen info contents.
|
||||
pub fn draw(self: *Info, open: bool, data: struct {
|
||||
/// The screen that we're inspecting.
|
||||
screen: *terminal.Screen,
|
||||
|
||||
/// Which screen key we're viewing.
|
||||
key: terminal.ScreenSet.Key,
|
||||
|
||||
/// Which screen is active (primary or alternate).
|
||||
active_key: terminal.ScreenSet.Key,
|
||||
|
||||
/// Whether xterm modify other keys mode 2 is enabled.
|
||||
modify_other_keys_2: bool,
|
||||
|
||||
/// Color palette for cursor color resolution.
|
||||
color_palette: *const terminal.color.DynamicPalette,
|
||||
}) void {
|
||||
// Create the dockspace for this screen
|
||||
const dockspace_id = cimgui.c.ImGui_GetID("Screen Dockspace");
|
||||
_ = createDockSpace(dockspace_id);
|
||||
|
||||
const screen = data.screen;
|
||||
|
||||
// Info window
|
||||
info: {
|
||||
defer cimgui.c.ImGui_End();
|
||||
if (!cimgui.c.ImGui_Begin(
|
||||
window_info,
|
||||
null,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
)) break :info;
|
||||
|
||||
if (cimgui.c.ImGui_CollapsingHeader(
|
||||
"Cursor",
|
||||
cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
|
||||
)) {
|
||||
cursorTable(&screen.cursor);
|
||||
cimgui.c.ImGui_Separator();
|
||||
cursorStyle(
|
||||
&screen.cursor,
|
||||
&data.color_palette.current,
|
||||
);
|
||||
}
|
||||
|
||||
if (cimgui.c.ImGui_CollapsingHeader(
|
||||
"Keyboard",
|
||||
cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
|
||||
)) keyboardTable(
|
||||
screen,
|
||||
data.modify_other_keys_2,
|
||||
);
|
||||
|
||||
if (cimgui.c.ImGui_CollapsingHeader(
|
||||
"Kitty Graphics",
|
||||
cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
|
||||
)) kittyGraphicsTable(&screen.kitty_images);
|
||||
|
||||
if (cimgui.c.ImGui_CollapsingHeader(
|
||||
"Other Screen State",
|
||||
cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
|
||||
)) internalStateTable(screen);
|
||||
}
|
||||
|
||||
// Cell window
|
||||
cell: {
|
||||
defer cimgui.c.ImGui_End();
|
||||
if (!cimgui.c.ImGui_Begin(
|
||||
window_cell,
|
||||
null,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
)) break :cell;
|
||||
self.cell_chooser.draw(&screen.pages);
|
||||
}
|
||||
|
||||
// PageList window
|
||||
pagelist: {
|
||||
defer cimgui.c.ImGui_End();
|
||||
if (!cimgui.c.ImGui_Begin(
|
||||
window_pagelist,
|
||||
null,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
)) break :pagelist;
|
||||
self.pagelist.draw(&screen.pages);
|
||||
}
|
||||
|
||||
// The remainder is the open state
|
||||
if (!open) return;
|
||||
|
||||
// Show warning if viewing an inactive screen
|
||||
if (data.key != data.active_key) {
|
||||
cimgui.c.ImGui_TextColored(
|
||||
.{ .x = 1.0, .y = 0.8, .z = 0.0, .w = 1.0 },
|
||||
"⚠ Viewing inactive screen",
|
||||
);
|
||||
cimgui.c.ImGui_Separator();
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the dock space for the screen inspector. This creates
|
||||
/// a dedicated dock space for the screen inspector windows. But they
|
||||
/// can of course be undocked and moved around as desired.
|
||||
fn createDockSpace(dockspace_id: cimgui.c.ImGuiID) bool {
|
||||
// Check if we need to set up the dockspace
|
||||
const setup = cimgui.ImGui_DockBuilderGetNode(dockspace_id) == null;
|
||||
|
||||
if (setup) {
|
||||
// Register our dockspace node
|
||||
assert(cimgui.ImGui_DockBuilderAddNodeEx(
|
||||
dockspace_id,
|
||||
cimgui.ImGuiDockNodeFlagsPrivate.DockSpace,
|
||||
) == dockspace_id);
|
||||
|
||||
// Dock windows into the space
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_info, dockspace_id);
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_cell, dockspace_id);
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_pagelist, dockspace_id);
|
||||
cimgui.ImGui_DockBuilderFinish(dockspace_id);
|
||||
}
|
||||
|
||||
// Create the dockspace
|
||||
assert(cimgui.c.ImGui_DockSpaceEx(
|
||||
dockspace_id,
|
||||
.{ .x = 0, .y = 0 },
|
||||
cimgui.c.ImGuiDockNodeFlags_None,
|
||||
null,
|
||||
) == dockspace_id);
|
||||
return setup;
|
||||
}
|
||||
};
|
||||
|
||||
/// Render cursor state with a table of cursor-specific fields.
|
||||
pub fn cursorTable(
|
||||
cursor: *const terminal.Screen.Cursor,
|
||||
) void {
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"table_cursor",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Position (x, y)");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The current cursor position in the terminal grid (0-indexed).");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("(%d, %d)", cursor.x, cursor.y);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Hyperlink");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The active OSC8 hyperlink for newly printed characters.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (cursor.hyperlink) |link| {
|
||||
cimgui.c.ImGui_Text("%.*s", link.uri.len, link.uri.ptr);
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("(none)");
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Pending Wrap");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The 'last column flag' (LCF). If set, the next character will force a soft-wrap to the next line.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
var value: bool = cursor.pending_wrap;
|
||||
_ = cimgui.c.ImGui_Checkbox("##pending_wrap", &value);
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Protected");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("If enabled, new characters will have the protected attribute set, preventing erasure by certain sequences.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
var value: bool = cursor.protected;
|
||||
_ = cimgui.c.ImGui_Checkbox("##protected", &value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Render cursor style information using the shared style table.
|
||||
pub fn cursorStyle(cursor: *const terminal.Screen.Cursor, palette: ?*const terminal.color.Palette) void {
|
||||
widgets.style.table(cursor.style, palette);
|
||||
}
|
||||
|
||||
/// Render keyboard information with a table.
|
||||
fn keyboardTable(
|
||||
screen: *const terminal.Screen,
|
||||
modify_other_keys_2: bool,
|
||||
) void {
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"table_keyboard",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
const kitty_flags = screen.kitty_keyboard.current();
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Mode");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
const mode = if (kitty_flags.int() != 0) "kitty" else "legacy";
|
||||
cimgui.c.ImGui_Text("%s", mode.ptr);
|
||||
}
|
||||
}
|
||||
|
||||
if (kitty_flags.int() != 0) {
|
||||
const Flags = @TypeOf(kitty_flags);
|
||||
inline for (@typeInfo(Flags).@"struct".fields) |field| {
|
||||
{
|
||||
const value = @field(kitty_flags, field.name);
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
const field_name = std.fmt.comptimePrint("{s}", .{field.name});
|
||||
cimgui.c.ImGui_Text("%s", field_name.ptr);
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"%s",
|
||||
if (value) "true".ptr else "false".ptr,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Xterm modify keys");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"%s",
|
||||
if (modify_other_keys_2) "true".ptr else "false".ptr,
|
||||
);
|
||||
}
|
||||
}
|
||||
} // keyboard mode info
|
||||
}
|
||||
|
||||
/// Render kitty graphics information table.
|
||||
pub fn kittyGraphicsTable(
|
||||
kitty_images: *const terminal.kitty.graphics.ImageStorage,
|
||||
) void {
|
||||
if (!kitty_images.enabled()) {
|
||||
cimgui.c.ImGui_TextDisabled("(Kitty graphics are disabled)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"##kitty_graphics",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Memory Usage");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d bytes (%d KiB)", kitty_images.total_bytes, units.toKibiBytes(kitty_images.total_bytes));
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Memory Limit");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d bytes (%d KiB)", kitty_images.total_limit, units.toKibiBytes(kitty_images.total_limit));
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Image Count");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d", kitty_images.images.count());
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Placement Count");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d", kitty_images.placements.count());
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Image Loading");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%s", if (kitty_images.loading != null) "true".ptr else "false".ptr);
|
||||
}
|
||||
|
||||
/// Render internal terminal state table.
|
||||
pub fn internalStateTable(
|
||||
screen: *const terminal.Screen,
|
||||
) void {
|
||||
const pages = &screen.pages;
|
||||
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"##terminal_state",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Memory Usage");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d bytes (%d KiB)", pages.page_size, units.toKibiBytes(pages.page_size));
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Memory Limit");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%d bytes (%d KiB)", pages.maxSize(), units.toKibiBytes(pages.maxSize()));
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Viewport Location");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(pages.viewport).ptr);
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Semantic Content");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("Whether semantic prompt markers (OSC 133) have been seen.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
var value: bool = screen.flags.semantic_content;
|
||||
_ = cimgui.c.ImGui_Checkbox("##semantic_content", &value);
|
||||
}
|
||||
}
|
||||
125
src/inspector/widgets/style.zig
Normal file
125
src/inspector/widgets/style.zig
Normal file
@@ -0,0 +1,125 @@
|
||||
const std = @import("std");
|
||||
const cimgui = @import("dcimgui");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const widgets = @import("../widgets.zig");
|
||||
|
||||
/// Render a style as a table.
|
||||
pub fn table(
|
||||
st: terminal.Style,
|
||||
palette: ?*const terminal.color.Palette,
|
||||
) void {
|
||||
{
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"style",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Foreground");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The foreground (text) color");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
color("fg", st.fg_color, palette);
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Background");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The background (cell) color");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
color("bg", st.bg_color, palette);
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Underline");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The underline color, if underlines are enabled.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
color("underline", st.underline_color, palette);
|
||||
}
|
||||
|
||||
const style_flags = .{
|
||||
.{ "bold", "Text will be rendered with bold weight." },
|
||||
.{ "italic", "Text will be rendered in italic style." },
|
||||
.{ "faint", "Text will be rendered with reduced intensity." },
|
||||
.{ "blink", "Text will blink." },
|
||||
.{ "inverse", "Foreground and background colors are swapped." },
|
||||
.{ "invisible", "Text will be invisible (hidden)." },
|
||||
.{ "strikethrough", "Text will have a line through it." },
|
||||
};
|
||||
inline for (style_flags) |entry| entry: {
|
||||
const style = entry[0];
|
||||
const help = entry[1];
|
||||
if (!@field(st.flags, style)) break :entry;
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text(style.ptr);
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker(help);
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("true");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_TextDisabled("(Any styles not shown are not currently set)");
|
||||
}
|
||||
|
||||
/// Render a style color.
|
||||
pub fn color(
|
||||
id: [:0]const u8,
|
||||
c: terminal.Style.Color,
|
||||
palette: ?*const terminal.color.Palette,
|
||||
) void {
|
||||
cimgui.c.ImGui_PushID(id);
|
||||
defer cimgui.c.ImGui_PopID();
|
||||
|
||||
switch (c) {
|
||||
.none => cimgui.c.ImGui_Text("default"),
|
||||
|
||||
.palette => |idx| {
|
||||
cimgui.c.ImGui_Text("Palette %d", idx);
|
||||
if (palette) |p| {
|
||||
const rgb = p[idx];
|
||||
var data: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.g)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.b)) / 255,
|
||||
};
|
||||
_ = cimgui.c.ImGui_ColorEdit3(
|
||||
"color_fg",
|
||||
&data,
|
||||
cimgui.c.ImGuiColorEditFlags_DisplayHex |
|
||||
cimgui.c.ImGuiColorEditFlags_NoPicker |
|
||||
cimgui.c.ImGuiColorEditFlags_NoLabel,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
.rgb => |rgb| {
|
||||
var data: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.g)) / 255,
|
||||
@as(f32, @floatFromInt(rgb.b)) / 255,
|
||||
};
|
||||
_ = cimgui.c.ImGui_ColorEdit3(
|
||||
"color_fg",
|
||||
&data,
|
||||
cimgui.c.ImGuiColorEditFlags_DisplayHex |
|
||||
cimgui.c.ImGuiColorEditFlags_NoPicker |
|
||||
cimgui.c.ImGuiColorEditFlags_NoLabel,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
503
src/inspector/widgets/surface.zig
Normal file
503
src/inspector/widgets/surface.zig
Normal file
@@ -0,0 +1,503 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = @import("../../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cimgui = @import("dcimgui");
|
||||
const inspector = @import("../main.zig");
|
||||
const widgets = @import("../widgets.zig");
|
||||
const input = @import("../../input.zig");
|
||||
const renderer = @import("../../renderer.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const Surface = @import("../../Surface.zig");
|
||||
|
||||
/// This is discovered via the hardcoded string in the ImGui demo window.
|
||||
const window_imgui_demo = "Dear ImGui Demo";
|
||||
const window_keyboard = "Keyboard";
|
||||
const window_terminal = "Terminal";
|
||||
const window_surface = "Surface";
|
||||
const window_termio = "Terminal IO";
|
||||
const window_renderer = "Renderer";
|
||||
|
||||
pub const Inspector = struct {
|
||||
/// Internal GUI state
|
||||
surface_info: Info,
|
||||
key_stream: widgets.key.Stream,
|
||||
terminal_info: widgets.terminal.Info,
|
||||
vt_stream: widgets.termio.Stream,
|
||||
renderer_info: widgets.renderer.Info,
|
||||
show_demo_window: bool,
|
||||
|
||||
pub fn init(alloc: Allocator) !Inspector {
|
||||
return .{
|
||||
.surface_info = .empty,
|
||||
.key_stream = try .init(alloc),
|
||||
.terminal_info = .empty,
|
||||
.vt_stream = try .init(alloc),
|
||||
.renderer_info = .empty,
|
||||
.show_demo_window = true,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Inspector, alloc: Allocator) void {
|
||||
self.key_stream.deinit(alloc);
|
||||
self.vt_stream.deinit(alloc);
|
||||
self.renderer_info.deinit(alloc);
|
||||
}
|
||||
|
||||
pub fn draw(
|
||||
self: *Inspector,
|
||||
surface: *const Surface,
|
||||
mouse: Mouse,
|
||||
) void {
|
||||
// Create our dockspace first. If we had to setup our dockspace,
|
||||
// then it is a first render.
|
||||
const dockspace_id = cimgui.c.ImGui_GetID("Main Dockspace");
|
||||
const first_render = createDockSpace(dockspace_id);
|
||||
|
||||
// Draw everything that requires the terminal state mutex.
|
||||
{
|
||||
surface.renderer_state.mutex.lock();
|
||||
defer surface.renderer_state.mutex.unlock();
|
||||
const t = surface.renderer_state.terminal;
|
||||
|
||||
// Terminal info window
|
||||
{
|
||||
const open = cimgui.c.ImGui_Begin(
|
||||
window_terminal,
|
||||
null,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
);
|
||||
defer cimgui.c.ImGui_End();
|
||||
self.terminal_info.draw(open, t);
|
||||
}
|
||||
|
||||
// Surface info window
|
||||
{
|
||||
const open = cimgui.c.ImGui_Begin(
|
||||
window_surface,
|
||||
null,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
);
|
||||
defer cimgui.c.ImGui_End();
|
||||
self.surface_info.draw(
|
||||
open,
|
||||
surface,
|
||||
mouse,
|
||||
);
|
||||
}
|
||||
|
||||
// Keyboard info window
|
||||
{
|
||||
const open = cimgui.c.ImGui_Begin(
|
||||
window_keyboard,
|
||||
null,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
);
|
||||
defer cimgui.c.ImGui_End();
|
||||
self.key_stream.draw(
|
||||
open,
|
||||
surface.alloc,
|
||||
);
|
||||
}
|
||||
|
||||
// Terminal IO window
|
||||
{
|
||||
const open = cimgui.c.ImGui_Begin(
|
||||
window_termio,
|
||||
null,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
);
|
||||
defer cimgui.c.ImGui_End();
|
||||
if (open) {
|
||||
self.vt_stream.draw(
|
||||
surface.alloc,
|
||||
&t.colors.palette.current,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Renderer info window
|
||||
{
|
||||
const open = cimgui.c.ImGui_Begin(
|
||||
window_renderer,
|
||||
null,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
);
|
||||
defer cimgui.c.ImGui_End();
|
||||
self.renderer_info.draw(
|
||||
surface.alloc,
|
||||
open,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// In debug we show the ImGui demo window so we can easily view
|
||||
// available widgets and such.
|
||||
if (comptime builtin.mode == .Debug) {
|
||||
if (self.show_demo_window) {
|
||||
cimgui.c.ImGui_ShowDemoWindow(&self.show_demo_window);
|
||||
}
|
||||
}
|
||||
|
||||
if (first_render) {
|
||||
// On first render, setup our initial focus state. We only
|
||||
// do this on first render so that we can let the user change
|
||||
// focus afterward without it snapping back.
|
||||
cimgui.c.ImGui_SetWindowFocusStr(window_terminal);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the global dock space for the inspector. A dock space
|
||||
/// is a special area where windows can be docked into. The global
|
||||
/// dock space fills the entire main viewport.
|
||||
///
|
||||
/// Returns true if this was the first time the dock space was created.
|
||||
fn createDockSpace(dockspace_id: cimgui.c.ImGuiID) bool {
|
||||
const viewport: *cimgui.c.ImGuiViewport = cimgui.c.ImGui_GetMainViewport();
|
||||
|
||||
// Initial Docking setup
|
||||
const setup = cimgui.ImGui_DockBuilderGetNode(dockspace_id) == null;
|
||||
if (setup) {
|
||||
// Register our dockspace node
|
||||
assert(cimgui.ImGui_DockBuilderAddNodeEx(
|
||||
dockspace_id,
|
||||
cimgui.ImGuiDockNodeFlagsPrivate.DockSpace,
|
||||
) == dockspace_id);
|
||||
|
||||
// Ensure it is the full size of the viewport
|
||||
cimgui.ImGui_DockBuilderSetNodeSize(
|
||||
dockspace_id,
|
||||
viewport.Size,
|
||||
);
|
||||
|
||||
// We only initialize one central docking point now but
|
||||
// this is the point we'd pre-split and so on for the initial
|
||||
// layout.
|
||||
const dock_id_main: cimgui.c.ImGuiID = dockspace_id;
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_imgui_demo, dock_id_main);
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_terminal, dock_id_main);
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_surface, dock_id_main);
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_keyboard, dock_id_main);
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_termio, dock_id_main);
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_renderer, dock_id_main);
|
||||
cimgui.ImGui_DockBuilderFinish(dockspace_id);
|
||||
}
|
||||
|
||||
// Put the dockspace over the viewport.
|
||||
assert(cimgui.c.ImGui_DockSpaceOverViewportEx(
|
||||
dockspace_id,
|
||||
viewport,
|
||||
cimgui.c.ImGuiDockNodeFlags_PassthruCentralNode,
|
||||
null,
|
||||
) == dockspace_id);
|
||||
return setup;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Mouse = struct {
|
||||
/// Last hovered x/y
|
||||
last_xpos: f64 = 0,
|
||||
last_ypos: f64 = 0,
|
||||
|
||||
// Last hovered screen point
|
||||
last_point: ?terminal.Pin = null,
|
||||
};
|
||||
|
||||
/// Surface information inspector widget.
|
||||
pub const Info = struct {
|
||||
pub const empty: Info = .{};
|
||||
|
||||
/// Draw the surface info window.
|
||||
pub fn draw(
|
||||
self: *Info,
|
||||
open: bool,
|
||||
surface: *const Surface,
|
||||
mouse: Mouse,
|
||||
) void {
|
||||
_ = self;
|
||||
if (!open) return;
|
||||
|
||||
if (cimgui.c.ImGui_CollapsingHeader(
|
||||
"Help",
|
||||
cimgui.c.ImGuiTreeNodeFlags_None,
|
||||
)) {
|
||||
cimgui.c.ImGui_TextWrapped(
|
||||
"This window displays information about the surface (window). " ++
|
||||
"A surface is the graphical area that displays the terminal " ++
|
||||
"content. It includes dimensions, font sizing, and mouse state " ++
|
||||
"information specific to this window instance.",
|
||||
);
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_SeparatorText("Dimensions");
|
||||
dimensionsTable(surface);
|
||||
|
||||
cimgui.c.ImGui_SeparatorText("Font");
|
||||
fontTable(surface);
|
||||
|
||||
cimgui.c.ImGui_SeparatorText("Mouse");
|
||||
mouseTable(surface, mouse);
|
||||
}
|
||||
};
|
||||
|
||||
fn dimensionsTable(surface: *const Surface) void {
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"table_size",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
// Screen Size
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Screen Size");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"%dpx x %dpx",
|
||||
surface.size.screen.width,
|
||||
surface.size.screen.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Grid Size
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Grid Size");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
const grid_size = surface.size.grid();
|
||||
cimgui.c.ImGui_Text(
|
||||
"%dc x %dr",
|
||||
grid_size.columns,
|
||||
grid_size.rows,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cell Size
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Cell Size");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"%dpx x %dpx",
|
||||
surface.size.cell.width,
|
||||
surface.size.cell.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Padding
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Window Padding");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"T=%d B=%d L=%d R=%d px",
|
||||
surface.size.padding.top,
|
||||
surface.size.padding.bottom,
|
||||
surface.size.padding.left,
|
||||
surface.size.padding.right,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fontTable(surface: *const Surface) void {
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"table_font",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Size (Points)");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"%.2f pt",
|
||||
surface.font_size.points,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Size (Pixels)");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"%.2f px",
|
||||
surface.font_size.pixels(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mouseTable(
|
||||
surface: *const Surface,
|
||||
mouse: Mouse,
|
||||
) void {
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"table_mouse",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
const surface_mouse = &surface.mouse;
|
||||
const t = surface.renderer_state.terminal;
|
||||
|
||||
{
|
||||
const hover_point: terminal.point.Coordinate = pt: {
|
||||
const p = mouse.last_point orelse break :pt .{};
|
||||
const pt = t.screens.active.pages.pointFromPin(
|
||||
.active,
|
||||
p,
|
||||
) orelse break :pt .{};
|
||||
break :pt pt.coord();
|
||||
};
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Hover Grid");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"row=%d, col=%d",
|
||||
hover_point.y,
|
||||
hover_point.x,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const coord: renderer.Coordinate.Terminal = (renderer.Coordinate{
|
||||
.surface = .{
|
||||
.x = mouse.last_xpos,
|
||||
.y = mouse.last_ypos,
|
||||
},
|
||||
}).convert(.terminal, surface.size).terminal;
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Hover Point");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"(%dpx, %dpx)",
|
||||
@as(i64, @intFromFloat(coord.x)),
|
||||
@as(i64, @intFromFloat(coord.y)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const any_click = for (surface_mouse.click_state) |state| {
|
||||
if (state == .press) break true;
|
||||
} else false;
|
||||
|
||||
click: {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Click State");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (!any_click) {
|
||||
cimgui.c.ImGui_Text("none");
|
||||
break :click;
|
||||
}
|
||||
|
||||
for (surface_mouse.click_state, 0..) |state, i| {
|
||||
if (state != .press) continue;
|
||||
const button: input.MouseButton = @enumFromInt(i);
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_Text("%s", (switch (button) {
|
||||
.unknown => "?",
|
||||
.left => "L",
|
||||
.middle => "M",
|
||||
.right => "R",
|
||||
.four => "{4}",
|
||||
.five => "{5}",
|
||||
.six => "{6}",
|
||||
.seven => "{7}",
|
||||
.eight => "{8}",
|
||||
.nine => "{9}",
|
||||
.ten => "{10}",
|
||||
.eleven => "{11}",
|
||||
}).ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const left_click_point: terminal.point.Coordinate = pt: {
|
||||
const p = surface_mouse.left_click_pin orelse break :pt .{};
|
||||
const pt = t.screens.active.pages.pointFromPin(
|
||||
.active,
|
||||
p.*,
|
||||
) orelse break :pt .{};
|
||||
break :pt pt.coord();
|
||||
};
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Click Grid");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"row=%d, col=%d",
|
||||
left_click_point.y,
|
||||
left_click_point.x,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Click Point");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"(%dpx, %dpx)",
|
||||
@as(u32, @intFromFloat(surface_mouse.left_click_xpos)),
|
||||
@as(u32, @intFromFloat(surface_mouse.left_click_ypos)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
726
src/inspector/widgets/terminal.zig
Normal file
726
src/inspector/widgets/terminal.zig
Normal file
@@ -0,0 +1,726 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = @import("../../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cimgui = @import("dcimgui");
|
||||
const widgets = @import("../widgets.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const modes = terminal.modes;
|
||||
const Terminal = terminal.Terminal;
|
||||
|
||||
/// Terminal information inspector widget.
|
||||
pub const Info = struct {
|
||||
/// True if we're showing the 256-color palette window.
|
||||
show_palette: bool,
|
||||
|
||||
/// The various detachable headers.
|
||||
misc_header: widgets.DetachableHeader,
|
||||
layout_header: widgets.DetachableHeader,
|
||||
mouse_header: widgets.DetachableHeader,
|
||||
color_header: widgets.DetachableHeader,
|
||||
modes_header: widgets.DetachableHeader,
|
||||
|
||||
/// Screen detail windows for each screen key.
|
||||
screens: ScreenMap,
|
||||
|
||||
pub const empty: Info = .{
|
||||
.show_palette = false,
|
||||
.misc_header = .{},
|
||||
.layout_header = .{},
|
||||
.mouse_header = .{},
|
||||
.color_header = .{},
|
||||
.modes_header = .{},
|
||||
.screens = .{},
|
||||
};
|
||||
|
||||
/// Draw the terminal info window.
|
||||
pub fn draw(
|
||||
self: *Info,
|
||||
open: bool,
|
||||
t: *Terminal,
|
||||
) void {
|
||||
// Draw our open state if we're open.
|
||||
if (open) self.drawOpen(t);
|
||||
|
||||
// Draw our detached state that draws regardless of if
|
||||
// we're open or not.
|
||||
if (self.misc_header.window("Terminal Misc")) |visible| {
|
||||
defer self.misc_header.windowEnd();
|
||||
if (visible) miscTable(t);
|
||||
}
|
||||
if (self.layout_header.window("Terminal Layout")) |visible| {
|
||||
defer self.layout_header.windowEnd();
|
||||
if (visible) layoutTable(t);
|
||||
}
|
||||
if (self.mouse_header.window("Terminal Mouse")) |visible| {
|
||||
defer self.mouse_header.windowEnd();
|
||||
if (visible) mouseTable(t);
|
||||
}
|
||||
if (self.color_header.window("Terminal Color")) |visible| {
|
||||
defer self.color_header.windowEnd();
|
||||
if (visible) colorTable(t, &self.show_palette);
|
||||
}
|
||||
if (self.modes_header.window("Terminal Modes")) |visible| {
|
||||
defer self.modes_header.windowEnd();
|
||||
if (visible) modesTable(t);
|
||||
}
|
||||
|
||||
// Palette pop-out window
|
||||
if (self.show_palette) {
|
||||
defer cimgui.c.ImGui_End();
|
||||
if (cimgui.c.ImGui_Begin(
|
||||
"256-Color Palette",
|
||||
&self.show_palette,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
)) {
|
||||
palette("palette", &t.colors.palette.current);
|
||||
}
|
||||
}
|
||||
|
||||
// Screen pop-out windows
|
||||
var it = self.screens.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const screen = t.screens.get(entry.key) orelse {
|
||||
// Could happen if we opened up a window for a screen
|
||||
// and that screen was subsequently deinitialized. In
|
||||
// this case, hide the window.
|
||||
self.screens.remove(entry.key);
|
||||
continue;
|
||||
};
|
||||
|
||||
var title_buf: [128]u8 = undefined;
|
||||
const title = std.fmt.bufPrintZ(
|
||||
&title_buf,
|
||||
"Screen: {t}",
|
||||
.{entry.key},
|
||||
) catch "Screen";
|
||||
|
||||
// Setup our next window so it has some size to it.
|
||||
const viewport = cimgui.c.ImGui_GetMainViewport();
|
||||
cimgui.c.ImGui_SetNextWindowSize(
|
||||
.{
|
||||
.x = @min(400, viewport.*.Size.x),
|
||||
.y = @min(300, viewport.*.Size.y),
|
||||
},
|
||||
cimgui.c.ImGuiCond_FirstUseEver,
|
||||
);
|
||||
|
||||
var screen_open: bool = true;
|
||||
defer cimgui.c.ImGui_End();
|
||||
const screen_draw = cimgui.c.ImGui_Begin(
|
||||
title,
|
||||
&screen_open,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
);
|
||||
entry.value.draw(screen_draw, .{
|
||||
.screen = screen,
|
||||
.key = entry.key,
|
||||
.active_key = t.screens.active_key,
|
||||
.modify_other_keys_2 = t.flags.modify_other_keys_2,
|
||||
.color_palette = &t.colors.palette,
|
||||
});
|
||||
|
||||
// If the window was closed, remove it from our map so future
|
||||
// renders don't draw it.
|
||||
if (!screen_open) self.screens.remove(entry.key);
|
||||
}
|
||||
}
|
||||
|
||||
fn drawOpen(self: *Info, t: *Terminal) void {
|
||||
// Show our screens up top.
|
||||
screensTable(t, &self.screens);
|
||||
|
||||
if (self.misc_header.header("Misc")) miscTable(t);
|
||||
if (self.layout_header.header("Layout")) layoutTable(t);
|
||||
if (self.mouse_header.header("Mouse")) mouseTable(t);
|
||||
if (self.color_header.header("Color")) colorTable(t, &self.show_palette);
|
||||
if (self.modes_header.header("Modes")) modesTable(t);
|
||||
}
|
||||
};
|
||||
|
||||
pub const ScreenMap = std.EnumMap(
|
||||
terminal.ScreenSet.Key,
|
||||
widgets.screen.Info,
|
||||
);
|
||||
|
||||
/// Render the table of possible screens with various actions.
|
||||
fn screensTable(
|
||||
t: *Terminal,
|
||||
map: *ScreenMap,
|
||||
) void {
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"screens",
|
||||
3,
|
||||
cimgui.c.ImGuiTableFlags_Borders |
|
||||
cimgui.c.ImGuiTableFlags_RowBg |
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableSetupColumn("Screen", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("Status", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
cimgui.c.ImGui_TableSetupColumn("", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
|
||||
|
||||
// Custom header row to include help marker before "Screen"
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRowEx(cimgui.c.ImGuiTableRowFlags_Headers, 0.0);
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_PushStyleVarImVec2(cimgui.c.ImGuiStyleVar_FramePadding, .{ .x = 0, .y = 0 });
|
||||
widgets.helpMarker(
|
||||
"A terminal can have multiple screens, only one of which is active at " ++
|
||||
"a time. Each screen has its own grid, contents, and other state. " ++
|
||||
"This section allows you to inspect the different screens managed by " ++
|
||||
"the terminal.",
|
||||
);
|
||||
cimgui.c.ImGui_PopStyleVar();
|
||||
cimgui.c.ImGui_SameLineEx(0.0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x);
|
||||
cimgui.c.ImGui_TableHeader("Screen");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_TableHeader("Status");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_TableHeader("");
|
||||
}
|
||||
}
|
||||
|
||||
for (std.meta.tags(terminal.ScreenSet.Key)) |key| {
|
||||
const is_initialized = t.screens.get(key) != null;
|
||||
const is_active = t.screens.active_key == key;
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(key).ptr);
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (is_active) {
|
||||
cimgui.c.ImGui_TextColored(
|
||||
.{ .x = 0.4, .y = 1.0, .z = 0.4, .w = 1.0 },
|
||||
"active",
|
||||
);
|
||||
} else if (is_initialized) {
|
||||
cimgui.c.ImGui_TextColored(
|
||||
.{ .x = 0.6, .y = 0.6, .z = 0.6, .w = 1.0 },
|
||||
"initialized",
|
||||
);
|
||||
} else {
|
||||
cimgui.c.ImGui_TextColored(
|
||||
.{ .x = 0.4, .y = 0.4, .z = 0.4, .w = 1.0 },
|
||||
"(not initialized)",
|
||||
);
|
||||
}
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
cimgui.c.ImGui_PushIDInt(@intFromEnum(key));
|
||||
defer cimgui.c.ImGui_PopID();
|
||||
cimgui.c.ImGui_BeginDisabled(!is_initialized);
|
||||
defer cimgui.c.ImGui_EndDisabled();
|
||||
if (cimgui.c.ImGui_Button("View")) {
|
||||
if (!map.contains(key)) {
|
||||
map.put(key, .empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Table of miscellaneous terminal information.
|
||||
fn miscTable(t: *Terminal) void {
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"table_misc",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Working Directory");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The current working directory reported by the shell.");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (t.pwd.items.len > 0) {
|
||||
cimgui.c.ImGui_Text(
|
||||
"%.*s",
|
||||
t.pwd.items.len,
|
||||
t.pwd.items.ptr,
|
||||
);
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("(none)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Focused");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("Whether the terminal itself is currently focused.");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
var value: bool = t.flags.focused;
|
||||
_ = cimgui.c.ImGui_Checkbox("##focused", &value);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Previous Char");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The previously printed character, used only for the REP sequence.");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (t.previous_char) |c| {
|
||||
cimgui.c.ImGui_Text("U+%04X", @as(u32, c));
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("(none)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Table of terminal layout information.
|
||||
fn layoutTable(t: *Terminal) void {
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"table_layout",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Grid");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The size of the terminal grid in columns and rows.");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"%dc x %dr",
|
||||
t.cols,
|
||||
t.rows,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Pixels");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The size of the terminal grid in pixels.");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"%dw x %dh",
|
||||
t.width_px,
|
||||
t.height_px,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Scroll Region");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The scrolling region boundaries (top, bottom, left, right).");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_PushItemWidth(cimgui.c.ImGui_CalcTextSize("00000").x);
|
||||
defer cimgui.c.ImGui_PopItemWidth();
|
||||
|
||||
var override = t.scrolling_region;
|
||||
var changed = false;
|
||||
|
||||
cimgui.c.ImGui_AlignTextToFramePadding();
|
||||
cimgui.c.ImGui_Text("T:");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
if (cimgui.c.ImGui_InputScalar(
|
||||
"##scroll_top",
|
||||
cimgui.c.ImGuiDataType_U16,
|
||||
&override.top,
|
||||
)) {
|
||||
override.top = @min(override.top, t.rows -| 1);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_Text("B:");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
if (cimgui.c.ImGui_InputScalar(
|
||||
"##scroll_bottom",
|
||||
cimgui.c.ImGuiDataType_U16,
|
||||
&override.bottom,
|
||||
)) {
|
||||
override.bottom = @min(override.bottom, t.rows -| 1);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_Text("L:");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
if (cimgui.c.ImGui_InputScalar(
|
||||
"##scroll_left",
|
||||
cimgui.c.ImGuiDataType_U16,
|
||||
&override.left,
|
||||
)) {
|
||||
override.left = @min(override.left, t.cols -| 1);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_Text("R:");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
if (cimgui.c.ImGui_InputScalar(
|
||||
"##scroll_right",
|
||||
cimgui.c.ImGuiDataType_U16,
|
||||
&override.right,
|
||||
)) {
|
||||
override.right = @min(override.right, t.cols -| 1);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed and
|
||||
override.top < override.bottom and
|
||||
override.left < override.right)
|
||||
{
|
||||
t.scrolling_region = override;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Table of mouse-related terminal information.
|
||||
fn mouseTable(t: *Terminal) void {
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"table_mouse",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Event Mode");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The mouse event reporting mode set by the application.");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(t.flags.mouse_event).ptr);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Format");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The mouse event encoding format.");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(t.flags.mouse_format).ptr);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Shape");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The current mouse cursor shape set by the application.");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text("%s", @tagName(t.mouse_shape).ptr);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Shift Capture");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("XTSHIFTESCAPE state for capturing shift in mouse protocol.");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (t.flags.mouse_shift_capture == .null) {
|
||||
cimgui.c.ImGui_TextDisabled("(unset)");
|
||||
} else {
|
||||
cimgui.c.ImGui_Text("%s", @tagName(t.flags.mouse_shift_capture).ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Table of color-related terminal information.
|
||||
fn colorTable(
|
||||
t: *Terminal,
|
||||
show_palette: *bool,
|
||||
) void {
|
||||
cimgui.c.ImGui_TextWrapped(
|
||||
"Color state for the terminal. Note these colors only apply " ++
|
||||
"to the palette and unstyled colors. Many modern terminal " ++
|
||||
"applications use direct RGB colors which are not reflected here.",
|
||||
);
|
||||
cimgui.c.ImGui_Separator();
|
||||
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"table_color",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Background");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("Unstyled cell background color.");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
_ = dynamicRGB(
|
||||
"bg_color",
|
||||
&t.colors.background,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Foreground");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("Unstyled cell foreground color.");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
_ = dynamicRGB(
|
||||
"fg_color",
|
||||
&t.colors.foreground,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Cursor");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("Cursor coloring set by escape sequences.");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
_ = dynamicRGB(
|
||||
"cursor_color",
|
||||
&t.colors.cursor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Palette");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("The 256-color palette.");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
if (cimgui.c.ImGui_Button("View")) {
|
||||
show_palette.* = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Table of terminal modes.
|
||||
fn modesTable(t: *Terminal) void {
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"table_modes",
|
||||
3,
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit |
|
||||
cimgui.c.ImGuiTableFlags_RowBg,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableSetupColumn("", cimgui.c.ImGuiTableColumnFlags_NoResize);
|
||||
cimgui.c.ImGui_TableSetupColumn("Number", cimgui.c.ImGuiTableColumnFlags_PreferSortAscending);
|
||||
cimgui.c.ImGui_TableSetupColumn("Name", cimgui.c.ImGuiTableColumnFlags_WidthStretch);
|
||||
cimgui.c.ImGui_TableHeadersRow();
|
||||
}
|
||||
|
||||
inline for (@typeInfo(terminal.Mode).@"enum".fields) |field| {
|
||||
@setEvalBranchQuota(6000);
|
||||
const tag: modes.ModeTag = @bitCast(@as(modes.ModeTag.Backing, field.value));
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
cimgui.c.ImGui_PushIDInt(@intCast(field.value));
|
||||
defer cimgui.c.ImGui_PopID();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
var value: bool = t.modes.get(@field(terminal.Mode, field.name));
|
||||
_ = cimgui.c.ImGui_Checkbox("##checkbox", &value);
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"%s%d",
|
||||
if (tag.ansi) "" else "?",
|
||||
@as(u32, @intCast(tag.value)),
|
||||
);
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
|
||||
const name = std.fmt.comptimePrint("{s}", .{field.name});
|
||||
cimgui.c.ImGui_Text("%s", name.ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a DynamicRGB color.
|
||||
fn dynamicRGB(
|
||||
label: [:0]const u8,
|
||||
rgb: *terminal.color.DynamicRGB,
|
||||
) bool {
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
label,
|
||||
if (rgb.override != null) 2 else 1,
|
||||
cimgui.c.ImGuiTableFlags_SizingFixedFit,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
if (rgb.override != null) cimgui.c.ImGui_TableSetupColumn(
|
||||
"##label",
|
||||
cimgui.c.ImGuiTableColumnFlags_WidthFixed,
|
||||
);
|
||||
cimgui.c.ImGui_TableSetupColumn(
|
||||
"##value",
|
||||
cimgui.c.ImGuiTableColumnFlags_WidthStretch,
|
||||
);
|
||||
|
||||
if (rgb.override) |c| {
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("override:");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("Overridden color set by escape sequences.");
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
var col = [3]f32{
|
||||
@as(f32, @floatFromInt(c.r)) / 255.0,
|
||||
@as(f32, @floatFromInt(c.g)) / 255.0,
|
||||
@as(f32, @floatFromInt(c.b)) / 255.0,
|
||||
};
|
||||
_ = cimgui.c.ImGui_ColorEdit3(
|
||||
"##override",
|
||||
&col,
|
||||
cimgui.c.ImGuiColorEditFlags_None,
|
||||
);
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
if (rgb.default) |c| {
|
||||
if (rgb.override != null) {
|
||||
cimgui.c.ImGui_Text("default:");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("Default color from configuration.");
|
||||
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
}
|
||||
|
||||
var col = [3]f32{
|
||||
@as(f32, @floatFromInt(c.r)) / 255.0,
|
||||
@as(f32, @floatFromInt(c.g)) / 255.0,
|
||||
@as(f32, @floatFromInt(c.b)) / 255.0,
|
||||
};
|
||||
_ = cimgui.c.ImGui_ColorEdit3(
|
||||
"##default",
|
||||
&col,
|
||||
cimgui.c.ImGuiColorEditFlags_None,
|
||||
);
|
||||
} else {
|
||||
cimgui.c.ImGui_TextDisabled("(unset)");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Render a color palette as a 16x16 grid of color buttons.
|
||||
fn palette(
|
||||
label: [:0]const u8,
|
||||
pal: *const terminal.color.Palette,
|
||||
) void {
|
||||
cimgui.c.ImGui_PushID(label);
|
||||
defer cimgui.c.ImGui_PopID();
|
||||
|
||||
for (0..16) |row| {
|
||||
for (0..16) |col| {
|
||||
const idx = row * 16 + col;
|
||||
const rgb = pal[idx];
|
||||
var col_arr = [3]f32{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255.0,
|
||||
@as(f32, @floatFromInt(rgb.g)) / 255.0,
|
||||
@as(f32, @floatFromInt(rgb.b)) / 255.0,
|
||||
};
|
||||
|
||||
if (col > 0) cimgui.c.ImGui_SameLine();
|
||||
|
||||
cimgui.c.ImGui_PushIDInt(@intCast(idx));
|
||||
_ = cimgui.c.ImGui_ColorEdit3(
|
||||
"##color",
|
||||
&col_arr,
|
||||
cimgui.c.ImGuiColorEditFlags_NoInputs,
|
||||
);
|
||||
if (cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_DelayShort)) {
|
||||
cimgui.c.ImGui_SetTooltip(
|
||||
"%d: #%02X%02X%02X",
|
||||
idx,
|
||||
rgb.r,
|
||||
rgb.g,
|
||||
rgb.b,
|
||||
);
|
||||
}
|
||||
cimgui.c.ImGui_PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
811
src/inspector/widgets/termio.zig
Normal file
811
src/inspector/widgets/termio.zig
Normal file
@@ -0,0 +1,811 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const cimgui = @import("dcimgui");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const CircBuf = @import("../../datastruct/main.zig").CircBuf;
|
||||
const Surface = @import("../../Surface.zig");
|
||||
const screen = @import("screen.zig");
|
||||
|
||||
/// VT event stream inspector widget.
|
||||
pub const Stream = struct {
|
||||
events: VTEvent.Ring,
|
||||
parser_stream: VTHandler.Stream,
|
||||
|
||||
/// The currently selected event sequence number for keyboard navigation
|
||||
selected_event_seq: ?u32 = null,
|
||||
|
||||
/// Flag indicating whether we need to scroll to the selected item
|
||||
need_scroll_to_selected: bool = false,
|
||||
|
||||
/// Flag indicating whether the selection was made by keyboard
|
||||
is_keyboard_selection: bool = false,
|
||||
|
||||
pub fn init(alloc: Allocator) !Stream {
|
||||
var events: VTEvent.Ring = try .init(alloc, 2);
|
||||
errdefer events.deinit(alloc);
|
||||
|
||||
var handler: VTHandler = .init;
|
||||
errdefer handler.deinit();
|
||||
|
||||
return .{
|
||||
.events = events,
|
||||
.parser_stream = .initAlloc(alloc, handler),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Stream, alloc: Allocator) void {
|
||||
var it = self.events.iterator(.forward);
|
||||
while (it.next()) |v| v.deinit(alloc);
|
||||
self.events.deinit(alloc);
|
||||
|
||||
self.parser_stream.deinit();
|
||||
}
|
||||
|
||||
pub fn recordPtyRead(
|
||||
self: *Stream,
|
||||
alloc: Allocator,
|
||||
t: *terminal.Terminal,
|
||||
data: []const u8,
|
||||
) !void {
|
||||
self.parser_stream.handler.state = .{
|
||||
.alloc = alloc,
|
||||
.terminal = t,
|
||||
.events = &self.events,
|
||||
};
|
||||
defer self.parser_stream.handler.state = null;
|
||||
try self.parser_stream.nextSlice(data);
|
||||
}
|
||||
|
||||
pub fn draw(
|
||||
self: *Stream,
|
||||
alloc: Allocator,
|
||||
palette: *const terminal.color.Palette,
|
||||
) void {
|
||||
const events = &self.events;
|
||||
const handler = &self.parser_stream.handler;
|
||||
const popup_filter = "Filter";
|
||||
|
||||
// Controls
|
||||
{
|
||||
const pause_play: [:0]const u8 = if (!handler.paused)
|
||||
"Pause##pause_play"
|
||||
else
|
||||
"Resume##pause_play";
|
||||
if (cimgui.c.ImGui_Button(pause_play.ptr)) {
|
||||
handler.paused = !handler.paused;
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_SameLineEx(0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x);
|
||||
if (cimgui.c.ImGui_Button("Filter")) {
|
||||
cimgui.c.ImGui_OpenPopup(
|
||||
popup_filter,
|
||||
cimgui.c.ImGuiPopupFlags_None,
|
||||
);
|
||||
}
|
||||
|
||||
if (!events.empty()) {
|
||||
cimgui.c.ImGui_SameLineEx(0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x);
|
||||
if (cimgui.c.ImGui_Button("Clear")) {
|
||||
var it = events.iterator(.forward);
|
||||
while (it.next()) |v| v.deinit(alloc);
|
||||
events.clear();
|
||||
|
||||
handler.current_seq = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Events Table
|
||||
if (events.empty()) {
|
||||
cimgui.c.ImGui_Text("Waiting for events...");
|
||||
} else {
|
||||
// TODO: Eventually
|
||||
// eventTable(events);
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_Separator();
|
||||
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"table_vt_events",
|
||||
3,
|
||||
cimgui.c.ImGuiTableFlags_RowBg |
|
||||
cimgui.c.ImGuiTableFlags_Borders,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableSetupColumn(
|
||||
"Seq",
|
||||
cimgui.c.ImGuiTableColumnFlags_WidthFixed,
|
||||
);
|
||||
cimgui.c.ImGui_TableSetupColumn(
|
||||
"Kind",
|
||||
cimgui.c.ImGuiTableColumnFlags_WidthFixed,
|
||||
);
|
||||
cimgui.c.ImGui_TableSetupColumn(
|
||||
"Description",
|
||||
cimgui.c.ImGuiTableColumnFlags_WidthStretch,
|
||||
);
|
||||
|
||||
// Handle keyboard navigation when window is focused
|
||||
if (cimgui.c.ImGui_IsWindowFocused(cimgui.c.ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
const key_pressed = getKeyAction();
|
||||
|
||||
switch (key_pressed) {
|
||||
.none => {},
|
||||
.up, .down => {
|
||||
// If no event is selected, select the first/last event based on direction
|
||||
if (self.selected_event_seq == null) {
|
||||
if (!events.empty()) {
|
||||
var it = events.iterator(if (key_pressed == .up) .forward else .reverse);
|
||||
if (it.next()) |ev| {
|
||||
self.selected_event_seq = @as(u32, @intCast(ev.seq));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Find next/previous event based on current selection
|
||||
var it = events.iterator(.reverse);
|
||||
switch (key_pressed) {
|
||||
.down => {
|
||||
var found = false;
|
||||
while (it.next()) |ev| {
|
||||
if (found) {
|
||||
self.selected_event_seq = @as(u32, @intCast(ev.seq));
|
||||
break;
|
||||
}
|
||||
if (ev.seq == self.selected_event_seq.?) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
.up => {
|
||||
var prev_ev: ?*const VTEvent = null;
|
||||
while (it.next()) |ev| {
|
||||
if (ev.seq == self.selected_event_seq.?) {
|
||||
if (prev_ev) |prev| {
|
||||
self.selected_event_seq = @as(u32, @intCast(prev.seq));
|
||||
break;
|
||||
}
|
||||
}
|
||||
prev_ev = ev;
|
||||
}
|
||||
},
|
||||
.none => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
// Mark that we need to scroll to the newly selected item
|
||||
self.need_scroll_to_selected = true;
|
||||
self.is_keyboard_selection = true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var it = events.iterator(.reverse);
|
||||
while (it.next()) |ev| {
|
||||
// Need to push an ID so that our selectable is unique.
|
||||
cimgui.c.ImGui_PushIDPtr(ev);
|
||||
defer cimgui.c.ImGui_PopID();
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableNextColumn();
|
||||
|
||||
// Store the previous selection state to detect changes
|
||||
const was_selected = ev.imgui_selected;
|
||||
|
||||
// Update selection state based on keyboard navigation
|
||||
if (self.selected_event_seq) |seq| {
|
||||
ev.imgui_selected = (@as(u32, @intCast(ev.seq)) == seq);
|
||||
}
|
||||
|
||||
// Handle selectable widget
|
||||
if (cimgui.c.ImGui_SelectableBoolPtr(
|
||||
"##select",
|
||||
&ev.imgui_selected,
|
||||
cimgui.c.ImGuiSelectableFlags_SpanAllColumns,
|
||||
)) {
|
||||
// If selection state changed, update keyboard navigation state
|
||||
if (ev.imgui_selected != was_selected) {
|
||||
self.selected_event_seq = if (ev.imgui_selected)
|
||||
@as(u32, @intCast(ev.seq))
|
||||
else
|
||||
null;
|
||||
self.is_keyboard_selection = false;
|
||||
}
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_Text("%d", ev.seq);
|
||||
_ = cimgui.c.ImGui_TableNextColumn();
|
||||
cimgui.c.ImGui_Text("%s", @tagName(ev.kind).ptr);
|
||||
_ = cimgui.c.ImGui_TableNextColumn();
|
||||
cimgui.c.ImGui_Text("%s", ev.raw_description.ptr);
|
||||
|
||||
// If the event is selected, we render info about it. For now
|
||||
// we put this in the last column because that's the widest and
|
||||
// imgui has no way to make a column span.
|
||||
if (ev.imgui_selected) {
|
||||
{
|
||||
screen.cursorTable(&ev.cursor);
|
||||
screen.cursorStyle(&ev.cursor, palette);
|
||||
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"details",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Scroll Region");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
cimgui.c.ImGui_Text(
|
||||
"T=%d B=%d L=%d R=%d",
|
||||
ev.scrolling_region.top,
|
||||
ev.scrolling_region.bottom,
|
||||
ev.scrolling_region.left,
|
||||
ev.scrolling_region.right,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var md_it = ev.metadata.iterator();
|
||||
while (md_it.next()) |entry| {
|
||||
var buf: [256]u8 = undefined;
|
||||
const key = std.fmt.bufPrintZ(&buf, "{s}", .{entry.key_ptr.*}) catch
|
||||
"<internal error>";
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableNextColumn();
|
||||
cimgui.c.ImGui_Text("%s", key.ptr);
|
||||
_ = cimgui.c.ImGui_TableNextColumn();
|
||||
cimgui.c.ImGui_Text("%s", entry.value_ptr.ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// If this is the selected event and scrolling is needed, scroll to it
|
||||
if (self.need_scroll_to_selected and self.is_keyboard_selection) {
|
||||
cimgui.c.ImGui_SetScrollHereY(0.5);
|
||||
self.need_scroll_to_selected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // table
|
||||
|
||||
if (cimgui.c.ImGui_BeginPopupModal(
|
||||
popup_filter,
|
||||
null,
|
||||
cimgui.c.ImGuiWindowFlags_AlwaysAutoResize,
|
||||
)) {
|
||||
defer cimgui.c.ImGui_EndPopup();
|
||||
|
||||
cimgui.c.ImGui_Text("Changed filter settings will only affect future events.");
|
||||
|
||||
cimgui.c.ImGui_Separator();
|
||||
|
||||
{
|
||||
_ = cimgui.c.ImGui_BeginTable(
|
||||
"table_filter_kind",
|
||||
3,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
);
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
inline for (@typeInfo(terminal.Parser.Action.Tag).@"enum".fields) |field| {
|
||||
const tag = @field(terminal.Parser.Action.Tag, field.name);
|
||||
if (tag == .apc_put or tag == .dcs_put) continue;
|
||||
|
||||
_ = cimgui.c.ImGui_TableNextColumn();
|
||||
var value = !handler.filter_exclude.contains(tag);
|
||||
if (cimgui.c.ImGui_Checkbox(@tagName(tag).ptr, &value)) {
|
||||
if (value) {
|
||||
handler.filter_exclude.remove(tag);
|
||||
} else {
|
||||
handler.filter_exclude.insert(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // Filter kind table
|
||||
|
||||
cimgui.c.ImGui_Separator();
|
||||
|
||||
cimgui.c.ImGui_Text(
|
||||
"Filter by string. Empty displays all, \"abc\" finds lines\n" ++
|
||||
"containing \"abc\", \"abc,xyz\" finds lines containing \"abc\"\n" ++
|
||||
"or \"xyz\", \"-abc\" excludes lines containing \"abc\".",
|
||||
);
|
||||
_ = cimgui.c.ImGuiTextFilter_Draw(
|
||||
&handler.filter_text,
|
||||
"##filter_text",
|
||||
0,
|
||||
);
|
||||
|
||||
cimgui.c.ImGui_Separator();
|
||||
if (cimgui.c.ImGui_Button("Close")) {
|
||||
cimgui.c.ImGui_CloseCurrentPopup();
|
||||
}
|
||||
} // filter popup
|
||||
}
|
||||
};
|
||||
|
||||
/// Helper function to check keyboard state and determine navigation action.
|
||||
fn getKeyAction() KeyAction {
|
||||
const keys = .{
|
||||
.{ .key = cimgui.c.ImGuiKey_J, .action = KeyAction.down },
|
||||
.{ .key = cimgui.c.ImGuiKey_DownArrow, .action = KeyAction.down },
|
||||
.{ .key = cimgui.c.ImGuiKey_K, .action = KeyAction.up },
|
||||
.{ .key = cimgui.c.ImGuiKey_UpArrow, .action = KeyAction.up },
|
||||
};
|
||||
|
||||
inline for (keys) |k| {
|
||||
if (cimgui.c.ImGui_IsKeyPressed(k.key)) {
|
||||
return k.action;
|
||||
}
|
||||
}
|
||||
return .none;
|
||||
}
|
||||
|
||||
pub fn eventTable(events: *const VTEvent.Ring) void {
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"events",
|
||||
3,
|
||||
cimgui.c.ImGuiTableFlags_RowBg |
|
||||
cimgui.c.ImGuiTableFlags_Borders,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
cimgui.c.ImGui_TableSetupColumn(
|
||||
"Seq",
|
||||
cimgui.c.ImGuiTableColumnFlags_WidthFixed,
|
||||
);
|
||||
cimgui.c.ImGui_TableSetupColumn(
|
||||
"Kind",
|
||||
cimgui.c.ImGuiTableColumnFlags_WidthFixed,
|
||||
);
|
||||
cimgui.c.ImGui_TableSetupColumn(
|
||||
"Description",
|
||||
cimgui.c.ImGuiTableColumnFlags_WidthStretch,
|
||||
);
|
||||
|
||||
var it = events.iterator(.reverse);
|
||||
while (it.next()) |ev| {
|
||||
// Need to push an ID so that our selectable is unique.
|
||||
cimgui.c.ImGui_PushIDPtr(ev);
|
||||
defer cimgui.c.ImGui_PopID();
|
||||
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableNextColumn();
|
||||
|
||||
cimgui.c.ImGui_SameLine();
|
||||
cimgui.c.ImGui_Text("%d", ev.seq);
|
||||
_ = cimgui.c.ImGui_TableNextColumn();
|
||||
cimgui.c.ImGui_Text("%s", @tagName(ev.kind).ptr);
|
||||
_ = cimgui.c.ImGui_TableNextColumn();
|
||||
cimgui.c.ImGui_Text("%s", ev.raw_description.ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// VT event. This isn't public because this is just how we store internal
|
||||
/// events.
|
||||
const VTEvent = struct {
|
||||
/// The arena that all allocated memory for this event is stored.
|
||||
arena_state: ArenaAllocator.State,
|
||||
|
||||
/// Sequence number, just monotonically increasing and wrapping if
|
||||
/// it ever overflows. It gives us a nice way to visualize progress.
|
||||
seq: usize = 1,
|
||||
|
||||
/// Kind of event, for filtering
|
||||
kind: Kind,
|
||||
|
||||
/// The description of the raw event in a more human-friendly format.
|
||||
/// For example for control sequences this is the full sequence but
|
||||
/// control characters are replaced with human-readable names, e.g.
|
||||
/// 0x07 (bell) becomes BEL.
|
||||
raw_description: [:0]const u8,
|
||||
|
||||
/// Various metadata at the time of the event (before processing).
|
||||
cursor: terminal.Screen.Cursor,
|
||||
scrolling_region: terminal.Terminal.ScrollingRegion,
|
||||
metadata: Metadata.Unmanaged = .{},
|
||||
|
||||
/// imgui selection state
|
||||
imgui_selected: bool = false,
|
||||
|
||||
const Kind = enum { print, execute, csi, esc, osc, dcs, apc };
|
||||
const Metadata = std.StringHashMap([:0]const u8);
|
||||
|
||||
/// Circular buffer of VT events.
|
||||
pub const Ring = CircBuf(VTEvent, undefined);
|
||||
|
||||
/// Initialize the event information for the given parser action.
|
||||
pub fn init(
|
||||
alloc_gpa: Allocator,
|
||||
t: *const terminal.Terminal,
|
||||
action: terminal.Parser.Action,
|
||||
) !VTEvent {
|
||||
var arena: ArenaAllocator = .init(alloc_gpa);
|
||||
errdefer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var md = Metadata.init(alloc);
|
||||
var buf: std.Io.Writer.Allocating = .init(alloc);
|
||||
try encodeAction(alloc, &buf.writer, &md, action);
|
||||
const desc = try buf.toOwnedSliceSentinel(0);
|
||||
|
||||
const kind: Kind = switch (action) {
|
||||
.print => .print,
|
||||
.execute => .execute,
|
||||
.csi_dispatch => .csi,
|
||||
.esc_dispatch => .esc,
|
||||
.osc_dispatch => .osc,
|
||||
.dcs_hook, .dcs_put, .dcs_unhook => .dcs,
|
||||
.apc_start, .apc_put, .apc_end => .apc,
|
||||
};
|
||||
|
||||
return .{
|
||||
.arena_state = arena.state,
|
||||
.kind = kind,
|
||||
.raw_description = desc,
|
||||
.cursor = t.screens.active.cursor,
|
||||
.scrolling_region = t.scrolling_region,
|
||||
.metadata = md.unmanaged,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *VTEvent, alloc_gpa: Allocator) void {
|
||||
var arena = self.arena_state.promote(alloc_gpa);
|
||||
arena.deinit();
|
||||
}
|
||||
|
||||
/// Returns true if the event passes the given filter.
|
||||
pub fn passFilter(
|
||||
self: *const VTEvent,
|
||||
filter: *const cimgui.c.ImGuiTextFilter,
|
||||
) bool {
|
||||
// Check our main string
|
||||
if (cimgui.c.ImGuiTextFilter_PassFilter(
|
||||
filter,
|
||||
self.raw_description.ptr,
|
||||
null,
|
||||
)) return true;
|
||||
|
||||
// We also check all metadata keys and values
|
||||
var it = self.metadata.iterator();
|
||||
while (it.next()) |entry| {
|
||||
var buf: [256]u8 = undefined;
|
||||
const key = std.fmt.bufPrintZ(&buf, "{s}", .{entry.key_ptr.*}) catch continue;
|
||||
if (cimgui.c.ImGuiTextFilter_PassFilter(
|
||||
filter,
|
||||
key.ptr,
|
||||
null,
|
||||
)) return true;
|
||||
if (cimgui.c.ImGuiTextFilter_PassFilter(
|
||||
filter,
|
||||
entry.value_ptr.ptr,
|
||||
null,
|
||||
)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Encode a parser action as a string that we show in the logs.
|
||||
fn encodeAction(
|
||||
alloc: Allocator,
|
||||
writer: *std.Io.Writer,
|
||||
md: *Metadata,
|
||||
action: terminal.Parser.Action,
|
||||
) !void {
|
||||
switch (action) {
|
||||
.print => try encodePrint(writer, action),
|
||||
.execute => try encodeExecute(writer, action),
|
||||
.csi_dispatch => |v| try encodeCSI(writer, v),
|
||||
.esc_dispatch => |v| try encodeEsc(writer, v),
|
||||
.osc_dispatch => |v| try encodeOSC(alloc, writer, md, v),
|
||||
else => try writer.print("{f}", .{action}),
|
||||
}
|
||||
}
|
||||
|
||||
fn encodePrint(writer: *std.Io.Writer, action: terminal.Parser.Action) !void {
|
||||
const ch = action.print;
|
||||
try writer.print("'{u}' (U+{X})", .{ ch, ch });
|
||||
}
|
||||
|
||||
fn encodeExecute(writer: *std.Io.Writer, action: terminal.Parser.Action) !void {
|
||||
const ch = action.execute;
|
||||
switch (ch) {
|
||||
0x00 => try writer.writeAll("NUL"),
|
||||
0x01 => try writer.writeAll("SOH"),
|
||||
0x02 => try writer.writeAll("STX"),
|
||||
0x03 => try writer.writeAll("ETX"),
|
||||
0x04 => try writer.writeAll("EOT"),
|
||||
0x05 => try writer.writeAll("ENQ"),
|
||||
0x06 => try writer.writeAll("ACK"),
|
||||
0x07 => try writer.writeAll("BEL"),
|
||||
0x08 => try writer.writeAll("BS"),
|
||||
0x09 => try writer.writeAll("HT"),
|
||||
0x0A => try writer.writeAll("LF"),
|
||||
0x0B => try writer.writeAll("VT"),
|
||||
0x0C => try writer.writeAll("FF"),
|
||||
0x0D => try writer.writeAll("CR"),
|
||||
0x0E => try writer.writeAll("SO"),
|
||||
0x0F => try writer.writeAll("SI"),
|
||||
else => try writer.writeAll("?"),
|
||||
}
|
||||
try writer.print(" (0x{X})", .{ch});
|
||||
}
|
||||
|
||||
fn encodeCSI(writer: *std.Io.Writer, csi: terminal.Parser.Action.CSI) !void {
|
||||
for (csi.intermediates) |v| try writer.print("{c} ", .{v});
|
||||
for (csi.params, 0..) |v, i| {
|
||||
if (i != 0) try writer.writeByte(';');
|
||||
try writer.print("{d}", .{v});
|
||||
}
|
||||
if (csi.intermediates.len > 0 or csi.params.len > 0) try writer.writeByte(' ');
|
||||
try writer.writeByte(csi.final);
|
||||
}
|
||||
|
||||
fn encodeEsc(writer: *std.Io.Writer, esc: terminal.Parser.Action.ESC) !void {
|
||||
for (esc.intermediates) |v| try writer.print("{c} ", .{v});
|
||||
try writer.writeByte(esc.final);
|
||||
}
|
||||
|
||||
fn encodeOSC(
|
||||
alloc: Allocator,
|
||||
writer: *std.Io.Writer,
|
||||
md: *Metadata,
|
||||
osc: terminal.osc.Command,
|
||||
) !void {
|
||||
// The description is just the tag
|
||||
try writer.print("{s} ", .{@tagName(osc)});
|
||||
|
||||
// Add additional fields to metadata
|
||||
switch (osc) {
|
||||
inline else => |v, tag| if (tag == osc) {
|
||||
try encodeMetadata(alloc, md, v);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn encodeMetadata(
|
||||
alloc: Allocator,
|
||||
md: *Metadata,
|
||||
v: anytype,
|
||||
) !void {
|
||||
switch (@TypeOf(v)) {
|
||||
void => {},
|
||||
[]const u8,
|
||||
[:0]const u8,
|
||||
=> try md.put("data", try alloc.dupeZ(u8, v)),
|
||||
else => |T| switch (@typeInfo(T)) {
|
||||
.@"struct" => |info| inline for (info.fields) |field| {
|
||||
try encodeMetadataSingle(
|
||||
alloc,
|
||||
md,
|
||||
field.name,
|
||||
@field(v, field.name),
|
||||
);
|
||||
},
|
||||
|
||||
.@"union" => |info| {
|
||||
const Tag = info.tag_type orelse @compileError("Unions must have a tag");
|
||||
const tag_name = @tagName(@as(Tag, v));
|
||||
inline for (info.fields) |field| {
|
||||
if (std.mem.eql(u8, field.name, tag_name)) {
|
||||
if (field.type == void) {
|
||||
break try md.put("data", tag_name);
|
||||
} else {
|
||||
break try encodeMetadataSingle(alloc, md, tag_name, @field(v, field.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
else => {
|
||||
@compileLog(T);
|
||||
@compileError("unsupported type, see log");
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn encodeMetadataSingle(
|
||||
alloc: Allocator,
|
||||
md: *Metadata,
|
||||
key: []const u8,
|
||||
value: anytype,
|
||||
) !void {
|
||||
const Value = @TypeOf(value);
|
||||
const info = @typeInfo(Value);
|
||||
switch (info) {
|
||||
.optional => if (value) |unwrapped| {
|
||||
try encodeMetadataSingle(alloc, md, key, unwrapped);
|
||||
} else {
|
||||
try md.put(key, try alloc.dupeZ(u8, "(unset)"));
|
||||
},
|
||||
|
||||
.bool => try md.put(
|
||||
key,
|
||||
try alloc.dupeZ(u8, if (value) "true" else "false"),
|
||||
),
|
||||
|
||||
.@"enum" => try md.put(
|
||||
key,
|
||||
try alloc.dupeZ(u8, @tagName(value)),
|
||||
),
|
||||
|
||||
.@"union" => |u| {
|
||||
const Tag = u.tag_type orelse @compileError("Unions must have a tag");
|
||||
const tag_name = @tagName(@as(Tag, value));
|
||||
inline for (u.fields) |field| {
|
||||
if (std.mem.eql(u8, field.name, tag_name)) {
|
||||
const s = if (field.type == void)
|
||||
try alloc.dupeZ(u8, tag_name)
|
||||
else if (field.type == [:0]const u8 or field.type == []const u8)
|
||||
try std.fmt.allocPrintSentinel(alloc, "{s}={s}", .{
|
||||
tag_name,
|
||||
@field(value, field.name),
|
||||
}, 0)
|
||||
else
|
||||
try std.fmt.allocPrintSentinel(alloc, "{s}={}", .{
|
||||
tag_name,
|
||||
@field(value, field.name),
|
||||
}, 0);
|
||||
|
||||
try md.put(key, s);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
.@"struct" => try md.put(
|
||||
key,
|
||||
try alloc.dupeZ(u8, @typeName(Value)),
|
||||
),
|
||||
|
||||
else => switch (Value) {
|
||||
[]const u8,
|
||||
[:0]const u8,
|
||||
=> try md.put(key, try alloc.dupeZ(u8, value)),
|
||||
|
||||
else => |T| switch (@typeInfo(T)) {
|
||||
.int => try md.put(
|
||||
key,
|
||||
try std.fmt.allocPrintSentinel(alloc, "{}", .{value}, 0),
|
||||
),
|
||||
else => {
|
||||
@compileLog(T);
|
||||
@compileError("unsupported type, see log");
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Our VT stream handler for the Stream widget. This isn't public
|
||||
/// because there is no reason to use this directly.
|
||||
const VTHandler = struct {
|
||||
/// The capture state, must be set before use. If null, then
|
||||
/// events are dropped.
|
||||
state: ?State,
|
||||
|
||||
/// True to pause this artificially.
|
||||
paused: bool,
|
||||
|
||||
/// Current sequence number
|
||||
current_seq: usize,
|
||||
|
||||
/// Exclude certain actions by tag.
|
||||
filter_exclude: ActionTagSet,
|
||||
filter_text: cimgui.c.ImGuiTextFilter,
|
||||
|
||||
const Stream = terminal.Stream(VTHandler);
|
||||
|
||||
pub const ActionTagSet = std.EnumSet(terminal.Parser.Action.Tag);
|
||||
|
||||
pub const State = struct {
|
||||
/// The allocator to use for the events.
|
||||
alloc: Allocator,
|
||||
|
||||
/// The terminal state at the time of the event.
|
||||
terminal: *const terminal.Terminal,
|
||||
|
||||
/// The event ring to write events to.
|
||||
events: *VTEvent.Ring,
|
||||
};
|
||||
|
||||
pub const init: VTHandler = .{
|
||||
.state = null,
|
||||
.paused = false,
|
||||
.current_seq = 1,
|
||||
.filter_exclude = .initMany(&.{.print}),
|
||||
.filter_text = .{},
|
||||
};
|
||||
|
||||
pub fn deinit(self: *VTHandler) void {
|
||||
// Required for the parser stream interface
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn vt(
|
||||
self: *VTHandler,
|
||||
comptime action: VTHandler.Stream.Action.Tag,
|
||||
value: VTHandler.Stream.Action.Value(action),
|
||||
) !void {
|
||||
_ = self;
|
||||
_ = value;
|
||||
}
|
||||
|
||||
/// This is called with every single terminal action.
|
||||
pub fn vtRaw(self: *VTHandler, action: terminal.Parser.Action) !bool {
|
||||
const state: *State = if (self.state) |*s| s else return true;
|
||||
const alloc = state.alloc;
|
||||
const vt_events = state.events;
|
||||
|
||||
// We always increment the sequence number, even if we're paused or
|
||||
// filter out the event. This helps show the user that there is a gap
|
||||
// between events and roughly how large that gap was.
|
||||
defer self.current_seq +%= 1;
|
||||
|
||||
// If we're manually paused, we ignore all events.
|
||||
if (self.paused) return true;
|
||||
|
||||
// We ignore certain action types that are too noisy.
|
||||
switch (action) {
|
||||
.dcs_put, .apc_put => return true,
|
||||
else => {},
|
||||
}
|
||||
|
||||
// If we requested a specific type to be ignored, ignore it.
|
||||
// We return true because we did "handle" it by ignoring it.
|
||||
if (self.filter_exclude.contains(std.meta.activeTag(action))) return true;
|
||||
|
||||
// Build our event
|
||||
var ev: VTEvent = try .init(
|
||||
alloc,
|
||||
state.terminal,
|
||||
action,
|
||||
);
|
||||
ev.seq = self.current_seq;
|
||||
errdefer ev.deinit(alloc);
|
||||
|
||||
// Check if the event passes the filter
|
||||
if (!ev.passFilter(&self.filter_text)) {
|
||||
ev.deinit(alloc);
|
||||
return true;
|
||||
}
|
||||
|
||||
const max_capacity = 100;
|
||||
vt_events.append(ev) catch |err| switch (err) {
|
||||
error.OutOfMemory => if (vt_events.capacity() < max_capacity) {
|
||||
// We're out of memory, but we can allocate to our capacity.
|
||||
const new_capacity = @min(vt_events.capacity() * 2, max_capacity);
|
||||
try vt_events.resize(alloc, new_capacity);
|
||||
try vt_events.append(ev);
|
||||
} else {
|
||||
var it = vt_events.iterator(.forward);
|
||||
if (it.next()) |old_ev| old_ev.deinit(alloc);
|
||||
vt_events.deleteOldest(1);
|
||||
try vt_events.append(ev);
|
||||
},
|
||||
|
||||
else => return err,
|
||||
};
|
||||
|
||||
// Do NOT skip it, because we want to record more information
|
||||
// about this event.
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/// Enum representing keyboard navigation actions
|
||||
const KeyAction = enum {
|
||||
down,
|
||||
none,
|
||||
up,
|
||||
};
|
||||
@@ -19,6 +19,7 @@ pub const Metal = @import("renderer/Metal.zig");
|
||||
pub const OpenGL = @import("renderer/OpenGL.zig");
|
||||
pub const WebGL = @import("renderer/WebGL.zig");
|
||||
pub const Options = @import("renderer/Options.zig");
|
||||
pub const Overlay = @import("renderer/Overlay.zig");
|
||||
pub const Thread = @import("renderer/Thread.zig");
|
||||
pub const State = @import("renderer/State.zig");
|
||||
pub const CursorStyle = cursor.Style;
|
||||
|
||||
415
src/renderer/Overlay.zig
Normal file
415
src/renderer/Overlay.zig
Normal file
@@ -0,0 +1,415 @@
|
||||
/// The debug overlay that can be drawn on top of the terminal
|
||||
/// during the rendering process.
|
||||
///
|
||||
/// This is implemented by doing all the drawing on the CPU via z2d,
|
||||
/// since the debug overlay isn't that common, z2d is pretty fast, and
|
||||
/// it simplifies our implementation quite a bit by not relying on us
|
||||
/// having a bunch of shaders that we have to write per-platform.
|
||||
///
|
||||
/// Initialize the overlay, apply features with `applyFeatures`, then
|
||||
/// get the resulting image with `pendingImage` to upload to the GPU.
|
||||
/// This works in concert with `renderer.image.State` to simplify. Draw
|
||||
/// it on the GPU as an image composited on top of the terminal output.
|
||||
const Overlay = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const z2d = @import("z2d");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const size = @import("size.zig");
|
||||
const Size = size.Size;
|
||||
const CellSize = size.CellSize;
|
||||
const Image = @import("image.zig").Image;
|
||||
|
||||
const log = std.log.scoped(.renderer_overlay);
|
||||
|
||||
/// The colors we use for overlays.
|
||||
pub const Color = enum {
|
||||
hyperlink, // light blue
|
||||
semantic_prompt, // orange/gold
|
||||
semantic_input, // cyan
|
||||
|
||||
pub fn rgb(self: Color) z2d.pixel.RGB {
|
||||
return switch (self) {
|
||||
.hyperlink => .{ .r = 180, .g = 180, .b = 255 },
|
||||
.semantic_prompt => .{ .r = 255, .g = 200, .b = 64 },
|
||||
.semantic_input => .{ .r = 64, .g = 200, .b = 255 },
|
||||
};
|
||||
}
|
||||
|
||||
/// The fill color for rectangles.
|
||||
pub fn rectFill(self: Color) z2d.Pixel {
|
||||
return self.alphaPixel(96);
|
||||
}
|
||||
|
||||
/// The border color for rectangles.
|
||||
pub fn rectBorder(self: Color) z2d.Pixel {
|
||||
return self.alphaPixel(200);
|
||||
}
|
||||
|
||||
/// The raw RGB as a pixel.
|
||||
pub fn pixel(self: Color) z2d.Pixel {
|
||||
return self.rgb().asPixel();
|
||||
}
|
||||
|
||||
fn alphaPixel(self: Color, alpha: u8) z2d.Pixel {
|
||||
var rgba: z2d.pixel.RGBA = .fromPixel(self.pixel());
|
||||
rgba.a = alpha;
|
||||
return rgba.multiply().asPixel();
|
||||
}
|
||||
};
|
||||
|
||||
/// The surface we're drawing our overlay to.
|
||||
surface: z2d.Surface,
|
||||
|
||||
/// Cell size information so we can map grid coordinates to pixels.
|
||||
cell_size: CellSize,
|
||||
|
||||
/// The set of available features and their configuration.
|
||||
pub const Feature = union(enum) {
|
||||
highlight_hyperlinks,
|
||||
semantic_prompts,
|
||||
};
|
||||
|
||||
pub const InitError = Allocator.Error || error{
|
||||
// The terminal dimensions are invalid to support an overlay.
|
||||
// Either too small or too big.
|
||||
InvalidDimensions,
|
||||
};
|
||||
|
||||
/// Initialize a new, blank overlay.
|
||||
pub fn init(alloc: Allocator, sz: Size) InitError!Overlay {
|
||||
// Our surface does NOT need to take into account padding because
|
||||
// we render the overlay using the image subsystem and shaders which
|
||||
// already take that into account.
|
||||
const term_size = sz.terminal();
|
||||
var sfc = z2d.Surface.initPixel(
|
||||
.{ .rgba = .{ .r = 0, .g = 0, .b = 0, .a = 0 } },
|
||||
alloc,
|
||||
std.math.cast(i32, term_size.width) orelse
|
||||
return error.InvalidDimensions,
|
||||
std.math.cast(i32, term_size.height) orelse
|
||||
return error.InvalidDimensions,
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.InvalidWidth, error.InvalidHeight => return error.InvalidDimensions,
|
||||
};
|
||||
errdefer sfc.deinit(alloc);
|
||||
|
||||
return .{
|
||||
.surface = sfc,
|
||||
.cell_size = sz.cell,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Overlay, alloc: Allocator) void {
|
||||
self.surface.deinit(alloc);
|
||||
}
|
||||
|
||||
/// Returns a pending image that can be used to copy, convert, upload, etc.
|
||||
pub fn pendingImage(self: *const Overlay) Image.Pending {
|
||||
return .{
|
||||
.width = @intCast(self.surface.getWidth()),
|
||||
.height = @intCast(self.surface.getHeight()),
|
||||
.pixel_format = .rgba,
|
||||
.data = @ptrCast(self.surface.image_surface_rgba.buf.ptr),
|
||||
};
|
||||
}
|
||||
|
||||
/// Clear the overlay.
|
||||
pub fn reset(self: *Overlay) void {
|
||||
self.surface.paintPixel(.{ .rgba = .{
|
||||
.r = 0,
|
||||
.g = 0,
|
||||
.b = 0,
|
||||
.a = 0,
|
||||
} });
|
||||
}
|
||||
|
||||
/// Apply the given features to this overlay. This will draw on top of
|
||||
/// any pre-existing content in the overlay.
|
||||
pub fn applyFeatures(
|
||||
self: *Overlay,
|
||||
alloc: Allocator,
|
||||
state: *const terminal.RenderState,
|
||||
features: []const Feature,
|
||||
) void {
|
||||
for (features) |f| switch (f) {
|
||||
.highlight_hyperlinks => self.highlightHyperlinks(
|
||||
alloc,
|
||||
state,
|
||||
),
|
||||
.semantic_prompts => self.highlightSemanticPrompts(
|
||||
alloc,
|
||||
state,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Add rectangles around contiguous hyperlinks in the render state.
|
||||
///
|
||||
/// Note: this currently doesn't take into account unique hyperlink IDs
|
||||
/// because the render state doesn't contain this. This will be added
|
||||
/// later.
|
||||
fn highlightHyperlinks(
|
||||
self: *Overlay,
|
||||
alloc: Allocator,
|
||||
state: *const terminal.RenderState,
|
||||
) void {
|
||||
const border_color = Color.hyperlink.rectBorder();
|
||||
const fill_color = Color.hyperlink.rectFill();
|
||||
|
||||
const row_slice = state.row_data.slice();
|
||||
const row_raw = row_slice.items(.raw);
|
||||
const row_cells = row_slice.items(.cells);
|
||||
for (row_raw, row_cells, 0..) |row, cells, y| {
|
||||
if (!row.hyperlink) continue;
|
||||
|
||||
const cells_slice = cells.slice();
|
||||
const raw_cells = cells_slice.items(.raw);
|
||||
|
||||
var x: usize = 0;
|
||||
while (x < raw_cells.len) {
|
||||
// Skip cells without hyperlinks
|
||||
if (!raw_cells[x].hyperlink) {
|
||||
x += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found start of a hyperlink run
|
||||
const start_x = x;
|
||||
|
||||
// Find end of contiguous hyperlink cells
|
||||
while (x < raw_cells.len and raw_cells[x].hyperlink) x += 1;
|
||||
const end_x = x;
|
||||
|
||||
self.highlightGridRect(
|
||||
alloc,
|
||||
start_x,
|
||||
y,
|
||||
end_x - start_x,
|
||||
1,
|
||||
border_color,
|
||||
fill_color,
|
||||
) catch |err| {
|
||||
std.log.warn("Error drawing hyperlink border: {}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn highlightSemanticPrompts(
|
||||
self: *Overlay,
|
||||
alloc: Allocator,
|
||||
state: *const terminal.RenderState,
|
||||
) void {
|
||||
const row_slice = state.row_data.slice();
|
||||
const row_raw = row_slice.items(.raw);
|
||||
const row_cells = row_slice.items(.cells);
|
||||
|
||||
// Highlight the row-level semantic prompt bars. The prompts are easy
|
||||
// because they're part of the row metadata.
|
||||
{
|
||||
const prompt_border = Color.semantic_prompt.rectBorder();
|
||||
const prompt_fill = Color.semantic_prompt.rectFill();
|
||||
|
||||
var y: usize = 0;
|
||||
while (y < row_raw.len) {
|
||||
// If its not a semantic prompt row, skip it.
|
||||
if (row_raw[y].semantic_prompt == .none) {
|
||||
y += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the full length of the semantic prompt row by connecting
|
||||
// all continuations.
|
||||
const start_y = y;
|
||||
y += 1;
|
||||
while (y < row_raw.len and
|
||||
row_raw[y].semantic_prompt == .prompt_continuation)
|
||||
{
|
||||
y += 1;
|
||||
}
|
||||
const end_y = y; // Exclusive
|
||||
|
||||
const bar_width = @min(@as(usize, 5), self.cell_size.width);
|
||||
self.highlightPixelRect(
|
||||
alloc,
|
||||
0,
|
||||
start_y,
|
||||
bar_width,
|
||||
end_y - start_y,
|
||||
prompt_border,
|
||||
prompt_fill,
|
||||
) catch |err| {
|
||||
log.warn("Error drawing semantic prompt bar: {}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight contiguous semantic cells within rows.
|
||||
for (row_cells, 0..) |cells, y| {
|
||||
const cells_slice = cells.slice();
|
||||
const raw_cells = cells_slice.items(.raw);
|
||||
|
||||
var x: usize = 0;
|
||||
while (x < raw_cells.len) {
|
||||
const cell = raw_cells[x];
|
||||
const content = cell.semantic_content;
|
||||
const start_x = x;
|
||||
|
||||
// We skip output because its just the rest of the non-prompt
|
||||
// parts and it makes the overlay too noisy.
|
||||
if (cell.semantic_content == .output) {
|
||||
x += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the end of this content.
|
||||
x += 1;
|
||||
while (x < raw_cells.len) {
|
||||
const next = raw_cells[x];
|
||||
if (next.semantic_content != content) break;
|
||||
x += 1;
|
||||
}
|
||||
|
||||
const color: Color = switch (content) {
|
||||
.prompt => .semantic_prompt,
|
||||
.input => .semantic_input,
|
||||
.output => unreachable,
|
||||
};
|
||||
|
||||
self.highlightGridRect(
|
||||
alloc,
|
||||
start_x,
|
||||
y,
|
||||
x - start_x,
|
||||
1,
|
||||
color.rectBorder(),
|
||||
color.rectFill(),
|
||||
) catch |err| {
|
||||
log.warn("Error drawing semantic content highlight: {}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a rectangle for highlighting a grid region. x/y/width/height
|
||||
/// are all in grid cells.
|
||||
fn highlightGridRect(
|
||||
self: *Overlay,
|
||||
alloc: Allocator,
|
||||
x: usize,
|
||||
y: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
border_color: z2d.Pixel,
|
||||
fill_color: z2d.Pixel,
|
||||
) !void {
|
||||
// All math below uses checked arithmetic to avoid overflows. The
|
||||
// inputs aren't trusted and the path this is in isn't hot enough
|
||||
// to wrarrant unsafe optimizations.
|
||||
|
||||
// Calculate our width/height in pixels.
|
||||
const px_width = std.math.cast(i32, try std.math.mul(
|
||||
usize,
|
||||
width,
|
||||
self.cell_size.width,
|
||||
)) orelse return error.Overflow;
|
||||
const px_height = std.math.cast(i32, try std.math.mul(
|
||||
usize,
|
||||
height,
|
||||
self.cell_size.height,
|
||||
)) orelse return error.Overflow;
|
||||
|
||||
// Calculate pixel coordinates
|
||||
const start_x: f64 = @floatFromInt(std.math.cast(i32, try std.math.mul(
|
||||
usize,
|
||||
x,
|
||||
self.cell_size.width,
|
||||
)) orelse return error.Overflow);
|
||||
const start_y: f64 = @floatFromInt(std.math.cast(i32, try std.math.mul(
|
||||
usize,
|
||||
y,
|
||||
self.cell_size.height,
|
||||
)) orelse return error.Overflow);
|
||||
const end_x: f64 = start_x + @as(f64, @floatFromInt(px_width));
|
||||
const end_y: f64 = start_y + @as(f64, @floatFromInt(px_height));
|
||||
|
||||
// Grab our context to draw
|
||||
var ctx: z2d.Context = .init(alloc, &self.surface);
|
||||
defer ctx.deinit();
|
||||
|
||||
// Don't need AA because we use sharp edges
|
||||
ctx.setAntiAliasingMode(.none);
|
||||
// Can use hairline since we have 1px borders
|
||||
ctx.setHairline(true);
|
||||
|
||||
// Draw rectangle path
|
||||
try ctx.moveTo(start_x, start_y);
|
||||
try ctx.lineTo(end_x, start_y);
|
||||
try ctx.lineTo(end_x, end_y);
|
||||
try ctx.lineTo(start_x, end_y);
|
||||
try ctx.closePath();
|
||||
|
||||
// Fill
|
||||
ctx.setSourceToPixel(fill_color);
|
||||
try ctx.fill();
|
||||
|
||||
// Border
|
||||
ctx.setLineWidth(1);
|
||||
ctx.setSourceToPixel(border_color);
|
||||
try ctx.stroke();
|
||||
}
|
||||
|
||||
/// Creates a rectangle for highlighting a region. x/y are grid cells and
|
||||
/// width/height are pixels.
|
||||
fn highlightPixelRect(
|
||||
self: *Overlay,
|
||||
alloc: Allocator,
|
||||
x: usize,
|
||||
y: usize,
|
||||
width_px: usize,
|
||||
height: usize,
|
||||
border_color: z2d.Pixel,
|
||||
fill_color: z2d.Pixel,
|
||||
) !void {
|
||||
const px_width = std.math.cast(i32, width_px) orelse return error.Overflow;
|
||||
const px_height = std.math.cast(i32, try std.math.mul(
|
||||
usize,
|
||||
height,
|
||||
self.cell_size.height,
|
||||
)) orelse return error.Overflow;
|
||||
|
||||
const start_x: f64 = @floatFromInt(std.math.cast(i32, try std.math.mul(
|
||||
usize,
|
||||
x,
|
||||
self.cell_size.width,
|
||||
)) orelse return error.Overflow);
|
||||
const start_y: f64 = @floatFromInt(std.math.cast(i32, try std.math.mul(
|
||||
usize,
|
||||
y,
|
||||
self.cell_size.height,
|
||||
)) orelse return error.Overflow);
|
||||
const end_x: f64 = start_x + @as(f64, @floatFromInt(px_width));
|
||||
const end_y: f64 = start_y + @as(f64, @floatFromInt(px_height));
|
||||
|
||||
var ctx: z2d.Context = .init(alloc, &self.surface);
|
||||
defer ctx.deinit();
|
||||
|
||||
ctx.setAntiAliasingMode(.none);
|
||||
ctx.setHairline(true);
|
||||
|
||||
try ctx.moveTo(start_x, start_y);
|
||||
try ctx.lineTo(end_x, start_y);
|
||||
try ctx.lineTo(end_x, end_y);
|
||||
try ctx.lineTo(start_x, end_y);
|
||||
try ctx.closePath();
|
||||
|
||||
ctx.setSourceToPixel(fill_color);
|
||||
try ctx.fill();
|
||||
|
||||
ctx.setLineWidth(1);
|
||||
ctx.setSourceToPixel(border_color);
|
||||
try ctx.stroke();
|
||||
}
|
||||
@@ -294,9 +294,6 @@ fn setQosClass(self: *const Thread) void {
|
||||
|
||||
fn syncDrawTimer(self: *Thread) void {
|
||||
skip: {
|
||||
// If we have an inspector, we always run the draw timer.
|
||||
if (self.flags.has_inspector) break :skip;
|
||||
|
||||
// If our renderer supports animations and has them, then we
|
||||
// always have a draw timer.
|
||||
if (@hasDecl(rendererpkg.Renderer, "hasAnimations") and
|
||||
@@ -479,9 +476,6 @@ fn drainMailbox(self: *Thread) !void {
|
||||
|
||||
.inspector => |v| {
|
||||
self.flags.has_inspector = v;
|
||||
// Reset our draw timer state, which might change due
|
||||
// to the inspector change.
|
||||
self.syncDrawTimer();
|
||||
},
|
||||
|
||||
.macos_display_id => |v| {
|
||||
@@ -614,11 +608,6 @@ fn renderCallback(
|
||||
return .disarm;
|
||||
};
|
||||
|
||||
// If we have an inspector, let the app know we want to rerender that.
|
||||
if (t.flags.has_inspector) {
|
||||
_ = t.app_mailbox.push(.{ .redraw_inspector = t.surface }, .{ .instant = {} });
|
||||
}
|
||||
|
||||
// Update our frame data
|
||||
t.renderer.updateFrame(
|
||||
t.state,
|
||||
|
||||
@@ -17,10 +17,9 @@ const noMinContrast = cellpkg.noMinContrast;
|
||||
const constraintWidth = cellpkg.constraintWidth;
|
||||
const isCovering = cellpkg.isCovering;
|
||||
const rowNeverExtendBg = @import("row.zig").neverExtendBg;
|
||||
const Overlay = @import("Overlay.zig");
|
||||
const imagepkg = @import("image.zig");
|
||||
const Image = imagepkg.Image;
|
||||
const ImageMap = imagepkg.ImageMap;
|
||||
const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement);
|
||||
const ImageState = imagepkg.State;
|
||||
const shadertoy = @import("shadertoy.zig");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
@@ -169,11 +168,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
font_shaper_cache: font.ShaperCache,
|
||||
|
||||
/// The images that we may render.
|
||||
images: ImageMap = .{},
|
||||
image_placements: ImagePlacementList = .{},
|
||||
image_bg_end: u32 = 0,
|
||||
image_text_end: u32 = 0,
|
||||
image_virtual: bool = false,
|
||||
images: ImageState = .empty,
|
||||
|
||||
/// Background image, if we have one.
|
||||
bg_image: ?imagepkg.Image = null,
|
||||
@@ -227,6 +222,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
/// a large screen.
|
||||
terminal_state_frame_count: usize = 0,
|
||||
|
||||
/// Our overlay state, if any.
|
||||
overlay: ?Overlay = null,
|
||||
|
||||
const HighlightTag = enum(u8) {
|
||||
search_match,
|
||||
search_match_selected,
|
||||
@@ -787,6 +785,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
if (self.overlay) |*overlay| overlay.deinit(self.alloc);
|
||||
self.terminal_state.deinit(self.alloc);
|
||||
if (self.search_selected_match) |*m| m.arena.deinit();
|
||||
if (self.search_matches) |*m| m.arena.deinit();
|
||||
@@ -806,12 +805,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
|
||||
self.config.deinit();
|
||||
|
||||
{
|
||||
var it = self.images.iterator();
|
||||
while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc);
|
||||
self.images.deinit(self.alloc);
|
||||
}
|
||||
self.image_placements.deinit(self.alloc);
|
||||
self.images.deinit(self.alloc);
|
||||
|
||||
if (self.bg_image) |img| img.deinit(self.alloc);
|
||||
|
||||
@@ -1118,6 +1112,16 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
state: *renderer.State,
|
||||
cursor_blink_visible: bool,
|
||||
) Allocator.Error!void {
|
||||
// const start = std.time.Instant.now() catch unreachable;
|
||||
// const start_micro = std.time.microTimestamp();
|
||||
// defer {
|
||||
// const end = std.time.Instant.now() catch unreachable;
|
||||
// log.warn(
|
||||
// "[updateFrame time] start_micro={} duration={}ns",
|
||||
// .{ start_micro, end.since(start) / std.time.ns_per_us },
|
||||
// );
|
||||
// }
|
||||
|
||||
// We fully deinit and reset the terminal state every so often
|
||||
// so that a particularly large terminal state doesn't cause
|
||||
// the renderer to hold on to retained memory.
|
||||
@@ -1141,6 +1145,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
mouse: renderer.State.Mouse,
|
||||
preedit: ?renderer.State.Preedit,
|
||||
scrollbar: terminal.Scrollbar,
|
||||
overlay_features: []const Overlay.Feature,
|
||||
};
|
||||
|
||||
// Update all our data as tightly as possible within the mutex.
|
||||
@@ -1190,10 +1195,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
// If we have any virtual references, we must also rebuild our
|
||||
// kitty state on every frame because any cell change can move
|
||||
// an image.
|
||||
if (state.terminal.screens.active.kitty_images.dirty or
|
||||
self.image_virtual)
|
||||
{
|
||||
self.prepKittyGraphics(state.terminal);
|
||||
if (self.images.kittyRequiresUpdate(state.terminal)) {
|
||||
self.images.kittyUpdate(
|
||||
self.alloc,
|
||||
state.terminal,
|
||||
.{
|
||||
.width = self.grid_metrics.cell_width,
|
||||
.height = self.grid_metrics.cell_height,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Get our OSC8 links we're hovering if we have a mouse.
|
||||
@@ -1215,11 +1225,20 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
};
|
||||
};
|
||||
|
||||
const overlay_features: []const Overlay.Feature = overlay: {
|
||||
const insp = state.inspector orelse break :overlay &.{};
|
||||
const renderer_info = insp.rendererInfo();
|
||||
break :overlay renderer_info.overlayFeatures(
|
||||
arena_alloc,
|
||||
) catch &.{};
|
||||
};
|
||||
|
||||
break :critical .{
|
||||
.links = links,
|
||||
.mouse = state.mouse,
|
||||
.preedit = preedit,
|
||||
.scrollbar = scrollbar,
|
||||
.overlay_features = overlay_features,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1288,6 +1307,17 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
// Reset our dirty state after updating.
|
||||
defer self.terminal_state.dirty = .false;
|
||||
|
||||
// Rebuild the overlay image if we have one. We can do this
|
||||
// outside of any critical areas.
|
||||
self.rebuildOverlay(
|
||||
critical.overlay_features,
|
||||
) catch |err| {
|
||||
log.warn(
|
||||
"error rebuilding overlay surface err={}",
|
||||
.{err},
|
||||
);
|
||||
};
|
||||
|
||||
// Acquire the draw mutex for all remaining state updates.
|
||||
{
|
||||
self.draw_mutex.lock();
|
||||
@@ -1337,6 +1367,16 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
else => {},
|
||||
};
|
||||
|
||||
// Prepare our overlay image for upload (or unload). This
|
||||
// has to use our general allocator since it modifies
|
||||
// state that survives frames.
|
||||
self.images.overlayUpdate(
|
||||
self.alloc,
|
||||
self.overlay,
|
||||
) catch |err| {
|
||||
log.warn("error updating overlay images err={}", .{err});
|
||||
};
|
||||
|
||||
// Update custom shader uniforms that depend on terminal state.
|
||||
self.updateCustomShaderUniformsFromState();
|
||||
}
|
||||
@@ -1354,6 +1394,16 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
self: *Self,
|
||||
sync: bool,
|
||||
) !void {
|
||||
// const start = std.time.Instant.now() catch unreachable;
|
||||
// const start_micro = std.time.microTimestamp();
|
||||
// defer {
|
||||
// const end = std.time.Instant.now() catch unreachable;
|
||||
// log.warn(
|
||||
// "[drawFrame time] start_micro={} duration={}ns",
|
||||
// .{ start_micro, end.since(start) / std.time.ns_per_us },
|
||||
// );
|
||||
// }
|
||||
|
||||
// We hold a the draw mutex to prevent changes to any
|
||||
// data we access while we're in the middle of drawing.
|
||||
self.draw_mutex.lock();
|
||||
@@ -1460,7 +1510,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
}
|
||||
|
||||
// Upload images to the GPU as necessary.
|
||||
try self.uploadKittyImages();
|
||||
_ = self.images.upload(self.alloc, &self.api);
|
||||
|
||||
// Upload the background image to the GPU as necessary.
|
||||
try self.uploadBackgroundImage();
|
||||
@@ -1542,9 +1592,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
|
||||
// Then we draw any kitty images that need
|
||||
// to be behind text AND cell backgrounds.
|
||||
try self.drawImagePlacements(
|
||||
self.images.draw(
|
||||
&self.api,
|
||||
self.shaders.pipelines.image,
|
||||
&pass,
|
||||
self.image_placements.items[0..self.image_bg_end],
|
||||
.kitty_below_bg,
|
||||
);
|
||||
|
||||
// Then we draw any opaque cell backgrounds.
|
||||
@@ -1556,9 +1608,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
});
|
||||
|
||||
// Kitty images between cell backgrounds and text.
|
||||
try self.drawImagePlacements(
|
||||
self.images.draw(
|
||||
&self.api,
|
||||
self.shaders.pipelines.image,
|
||||
&pass,
|
||||
self.image_placements.items[self.image_bg_end..self.image_text_end],
|
||||
.kitty_below_text,
|
||||
);
|
||||
|
||||
// Text.
|
||||
@@ -1581,9 +1635,20 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
});
|
||||
|
||||
// Kitty images in front of text.
|
||||
try self.drawImagePlacements(
|
||||
self.images.draw(
|
||||
&self.api,
|
||||
self.shaders.pipelines.image,
|
||||
&pass,
|
||||
self.image_placements.items[self.image_text_end..],
|
||||
.kitty_above_text,
|
||||
);
|
||||
|
||||
// Debug overlay. We do this before any custom shader state
|
||||
// because our debug overlay is aligned with the grid.
|
||||
if (self.overlay != null) self.images.draw(
|
||||
&self.api,
|
||||
self.shaders.pipelines.image,
|
||||
&pass,
|
||||
.overlay,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1639,426 +1704,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
self.swap_chain.releaseFrame();
|
||||
}
|
||||
|
||||
fn drawImagePlacements(
|
||||
self: *Self,
|
||||
pass: *RenderPass,
|
||||
placements: []const imagepkg.Placement,
|
||||
) !void {
|
||||
if (placements.len == 0) return;
|
||||
|
||||
for (placements) |p| {
|
||||
|
||||
// Look up the image
|
||||
const image = self.images.get(p.image_id) orelse {
|
||||
log.warn("image not found for placement image_id={}", .{p.image_id});
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get the texture
|
||||
const texture = switch (image.image) {
|
||||
.ready,
|
||||
.unload_ready,
|
||||
=> |t| t,
|
||||
else => {
|
||||
log.warn("image not ready for placement image_id={}", .{p.image_id});
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
// Create our vertex buffer, which is always exactly one item.
|
||||
// future(mitchellh): we can group rendering multiple instances of a single image
|
||||
var buf = try Buffer(shaderpkg.Image).initFill(
|
||||
self.api.imageBufferOptions(),
|
||||
&.{.{
|
||||
.grid_pos = .{
|
||||
@as(f32, @floatFromInt(p.x)),
|
||||
@as(f32, @floatFromInt(p.y)),
|
||||
},
|
||||
|
||||
.cell_offset = .{
|
||||
@as(f32, @floatFromInt(p.cell_offset_x)),
|
||||
@as(f32, @floatFromInt(p.cell_offset_y)),
|
||||
},
|
||||
|
||||
.source_rect = .{
|
||||
@as(f32, @floatFromInt(p.source_x)),
|
||||
@as(f32, @floatFromInt(p.source_y)),
|
||||
@as(f32, @floatFromInt(p.source_width)),
|
||||
@as(f32, @floatFromInt(p.source_height)),
|
||||
},
|
||||
|
||||
.dest_size = .{
|
||||
@as(f32, @floatFromInt(p.width)),
|
||||
@as(f32, @floatFromInt(p.height)),
|
||||
},
|
||||
}},
|
||||
);
|
||||
defer buf.deinit();
|
||||
|
||||
pass.step(.{
|
||||
.pipeline = self.shaders.pipelines.image,
|
||||
.buffers = &.{buf.buffer},
|
||||
.textures = &.{texture},
|
||||
.draw = .{
|
||||
.type = .triangle_strip,
|
||||
.vertex_count = 4,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// This goes through the Kitty graphic placements and accumulates the
|
||||
/// placements we need to render on our viewport.
|
||||
fn prepKittyGraphics(
|
||||
self: *Self,
|
||||
t: *terminal.Terminal,
|
||||
) void {
|
||||
self.draw_mutex.lock();
|
||||
defer self.draw_mutex.unlock();
|
||||
|
||||
const storage = &t.screens.active.kitty_images;
|
||||
defer storage.dirty = false;
|
||||
|
||||
// We always clear our previous placements no matter what because
|
||||
// we rebuild them from scratch.
|
||||
self.image_placements.clearRetainingCapacity();
|
||||
self.image_virtual = false;
|
||||
|
||||
// Go through our known images and if there are any that are no longer
|
||||
// in use then mark them to be freed.
|
||||
//
|
||||
// This never conflicts with the below because a placement can't
|
||||
// reference an image that doesn't exist.
|
||||
{
|
||||
var it = self.images.iterator();
|
||||
while (it.next()) |kv| {
|
||||
if (storage.imageById(kv.key_ptr.*) == null) {
|
||||
kv.value_ptr.image.markForUnload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The top-left and bottom-right corners of our viewport in screen
|
||||
// points. This lets us determine offsets and containment of placements.
|
||||
const top = t.screens.active.pages.getTopLeft(.viewport);
|
||||
const bot = t.screens.active.pages.getBottomRight(.viewport).?;
|
||||
const top_y = t.screens.active.pages.pointFromPin(.screen, top).?.screen.y;
|
||||
const bot_y = t.screens.active.pages.pointFromPin(.screen, bot).?.screen.y;
|
||||
|
||||
// Go through the placements and ensure the image is
|
||||
// on the GPU or else is ready to be sent to the GPU.
|
||||
var it = storage.placements.iterator();
|
||||
while (it.next()) |kv| {
|
||||
const p = kv.value_ptr;
|
||||
|
||||
// Special logic based on location
|
||||
switch (p.location) {
|
||||
.pin => {},
|
||||
.virtual => {
|
||||
// We need to mark virtual placements on our renderer so that
|
||||
// we know to rebuild in more scenarios since cell changes can
|
||||
// now trigger placement changes.
|
||||
self.image_virtual = true;
|
||||
|
||||
// We also continue out because virtual placements are
|
||||
// only triggered by the unicode placeholder, not by the
|
||||
// placement itself.
|
||||
continue;
|
||||
},
|
||||
}
|
||||
|
||||
// Get the image for the placement
|
||||
const image = storage.imageById(kv.key_ptr.image_id) orelse {
|
||||
log.warn(
|
||||
"missing image for placement, ignoring image_id={}",
|
||||
.{kv.key_ptr.image_id},
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
self.prepKittyPlacement(
|
||||
t,
|
||||
top_y,
|
||||
bot_y,
|
||||
&image,
|
||||
p,
|
||||
) catch |err| {
|
||||
// For errors we log and continue. We try to place
|
||||
// other placements even if one fails.
|
||||
log.warn("error preparing kitty placement err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
// If we have virtual placements then we need to scan for placeholders.
|
||||
if (self.image_virtual) {
|
||||
var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot);
|
||||
while (v_it.next()) |virtual_p| {
|
||||
self.prepKittyVirtualPlacement(
|
||||
t,
|
||||
&virtual_p,
|
||||
) catch |err| {
|
||||
// For errors we log and continue. We try to place
|
||||
// other placements even if one fails.
|
||||
log.warn("error preparing kitty placement err={}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the placements by their Z value.
|
||||
std.mem.sortUnstable(
|
||||
imagepkg.Placement,
|
||||
self.image_placements.items,
|
||||
{},
|
||||
struct {
|
||||
fn lessThan(
|
||||
ctx: void,
|
||||
lhs: imagepkg.Placement,
|
||||
rhs: imagepkg.Placement,
|
||||
) bool {
|
||||
_ = ctx;
|
||||
return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id);
|
||||
}
|
||||
}.lessThan,
|
||||
);
|
||||
|
||||
// Find our indices. The values are sorted by z so we can
|
||||
// find the first placement out of bounds to find the limits.
|
||||
var bg_end: ?u32 = null;
|
||||
var text_end: ?u32 = null;
|
||||
const bg_limit = std.math.minInt(i32) / 2;
|
||||
for (self.image_placements.items, 0..) |p, i| {
|
||||
if (bg_end == null and p.z >= bg_limit) {
|
||||
bg_end = @intCast(i);
|
||||
}
|
||||
if (text_end == null and p.z >= 0) {
|
||||
text_end = @intCast(i);
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't see any images with a z > the bg limit,
|
||||
// then our bg end is the end of our placement list.
|
||||
self.image_bg_end =
|
||||
bg_end orelse @intCast(self.image_placements.items.len);
|
||||
|
||||
// Same idea for the image_text_end.
|
||||
self.image_text_end =
|
||||
text_end orelse @intCast(self.image_placements.items.len);
|
||||
}
|
||||
|
||||
fn prepKittyVirtualPlacement(
|
||||
self: *Self,
|
||||
t: *terminal.Terminal,
|
||||
p: *const terminal.kitty.graphics.unicode.Placement,
|
||||
) PrepKittyImageError!void {
|
||||
const storage = &t.screens.active.kitty_images;
|
||||
const image = storage.imageById(p.image_id) orelse {
|
||||
log.warn(
|
||||
"missing image for virtual placement, ignoring image_id={}",
|
||||
.{p.image_id},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
const rp = p.renderPlacement(
|
||||
storage,
|
||||
&image,
|
||||
self.grid_metrics.cell_width,
|
||||
self.grid_metrics.cell_height,
|
||||
) catch |err| {
|
||||
log.warn("error rendering virtual placement err={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
// If our placement is zero sized then we don't do anything.
|
||||
if (rp.dest_width == 0 or rp.dest_height == 0) return;
|
||||
|
||||
const viewport: terminal.point.Point = t.screens.active.pages.pointFromPin(
|
||||
.viewport,
|
||||
rp.top_left,
|
||||
) orelse {
|
||||
// This is unreachable with virtual placements because we should
|
||||
// only ever be looking at virtual placements that are in our
|
||||
// viewport in the renderer and virtual placements only ever take
|
||||
// up one row.
|
||||
unreachable;
|
||||
};
|
||||
|
||||
// Prepare the image for the GPU and store the placement.
|
||||
try self.prepKittyImage(&image);
|
||||
try self.image_placements.append(self.alloc, .{
|
||||
.image_id = image.id,
|
||||
.x = @intCast(rp.top_left.x),
|
||||
.y = @intCast(viewport.viewport.y),
|
||||
.z = -1,
|
||||
.width = rp.dest_width,
|
||||
.height = rp.dest_height,
|
||||
.cell_offset_x = rp.offset_x,
|
||||
.cell_offset_y = rp.offset_y,
|
||||
.source_x = rp.source_x,
|
||||
.source_y = rp.source_y,
|
||||
.source_width = rp.source_width,
|
||||
.source_height = rp.source_height,
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the viewport-relative position for this
|
||||
/// placement and add it to the placements list.
|
||||
fn prepKittyPlacement(
|
||||
self: *Self,
|
||||
t: *terminal.Terminal,
|
||||
top_y: u32,
|
||||
bot_y: u32,
|
||||
image: *const terminal.kitty.graphics.Image,
|
||||
p: *const terminal.kitty.graphics.ImageStorage.Placement,
|
||||
) PrepKittyImageError!void {
|
||||
// Get the rect for the placement. If this placement doesn't have
|
||||
// a rect then its virtual or something so skip it.
|
||||
const rect = p.rect(image.*, t) orelse return;
|
||||
|
||||
// This is expensive but necessary.
|
||||
const img_top_y = t.screens.active.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
|
||||
const img_bot_y = t.screens.active.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y;
|
||||
|
||||
// If the selection isn't within our viewport then skip it.
|
||||
if (img_top_y > bot_y) return;
|
||||
if (img_bot_y < top_y) return;
|
||||
|
||||
// We need to prep this image for upload if it isn't in the
|
||||
// cache OR it is in the cache but the transmit time doesn't
|
||||
// match meaning this image is different.
|
||||
try self.prepKittyImage(image);
|
||||
|
||||
// Calculate the dimensions of our image, taking in to
|
||||
// account the rows / columns specified by the placement.
|
||||
const dest_size = p.calculatedSize(image.*, t);
|
||||
|
||||
// Calculate the source rectangle
|
||||
const source_x = @min(image.width, p.source_x);
|
||||
const source_y = @min(image.height, p.source_y);
|
||||
const source_width = if (p.source_width > 0)
|
||||
@min(image.width - source_x, p.source_width)
|
||||
else
|
||||
image.width;
|
||||
const source_height = if (p.source_height > 0)
|
||||
@min(image.height - source_y, p.source_height)
|
||||
else
|
||||
image.height;
|
||||
|
||||
// Get the viewport-relative Y position of the placement.
|
||||
const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y));
|
||||
|
||||
// Accumulate the placement
|
||||
if (dest_size.width > 0 and dest_size.height > 0) {
|
||||
try self.image_placements.append(self.alloc, .{
|
||||
.image_id = image.id,
|
||||
.x = @intCast(rect.top_left.x),
|
||||
.y = y_pos,
|
||||
.z = p.z,
|
||||
.width = dest_size.width,
|
||||
.height = dest_size.height,
|
||||
.cell_offset_x = p.x_offset,
|
||||
.cell_offset_y = p.y_offset,
|
||||
.source_x = source_x,
|
||||
.source_y = source_y,
|
||||
.source_width = source_width,
|
||||
.source_height = source_height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const PrepKittyImageError = error{
|
||||
OutOfMemory,
|
||||
ImageConversionError,
|
||||
};
|
||||
|
||||
/// Prepare the provided image for upload to the GPU by copying its
|
||||
/// data with our allocator and setting it to the pending state.
|
||||
fn prepKittyImage(
|
||||
self: *Self,
|
||||
image: *const terminal.kitty.graphics.Image,
|
||||
) PrepKittyImageError!void {
|
||||
// If this image exists and its transmit time is the same we assume
|
||||
// it is the identical image so we don't need to send it to the GPU.
|
||||
const gop = try self.images.getOrPut(self.alloc, image.id);
|
||||
if (gop.found_existing and
|
||||
gop.value_ptr.transmit_time.order(image.transmit_time) == .eq)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy the data into the pending state.
|
||||
const data = if (self.alloc.dupe(
|
||||
u8,
|
||||
image.data,
|
||||
)) |v| v else |_| {
|
||||
if (!gop.found_existing) {
|
||||
// If this is a new entry we can just remove it since it
|
||||
// was never sent to the GPU.
|
||||
_ = self.images.remove(image.id);
|
||||
} else {
|
||||
// If this was an existing entry, it is invalid and
|
||||
// we must unload it.
|
||||
gop.value_ptr.image.markForUnload();
|
||||
}
|
||||
|
||||
return error.OutOfMemory;
|
||||
};
|
||||
// Note: we don't need to errdefer free the data because it is
|
||||
// put into the map immediately below and our errdefer to
|
||||
// handle our map state will fix this up.
|
||||
|
||||
// Store it in the map
|
||||
const new_image: Image = .{
|
||||
.pending = .{
|
||||
.width = image.width,
|
||||
.height = image.height,
|
||||
.pixel_format = switch (image.format) {
|
||||
.gray => .gray,
|
||||
.gray_alpha => .gray_alpha,
|
||||
.rgb => .rgb,
|
||||
.rgba => .rgba,
|
||||
.png => unreachable, // should be decoded by now
|
||||
},
|
||||
.data = data.ptr,
|
||||
},
|
||||
};
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = .{
|
||||
.image = new_image,
|
||||
.transmit_time = undefined,
|
||||
};
|
||||
} else {
|
||||
gop.value_ptr.image.markForReplace(
|
||||
self.alloc,
|
||||
new_image,
|
||||
);
|
||||
}
|
||||
|
||||
// If any error happens, we unload the image and it is invalid.
|
||||
errdefer gop.value_ptr.image.markForUnload();
|
||||
|
||||
gop.value_ptr.image.prepForUpload(self.alloc) catch |err| {
|
||||
log.warn("error preparing kitty image for upload err={}", .{err});
|
||||
return error.ImageConversionError;
|
||||
};
|
||||
gop.value_ptr.transmit_time = image.transmit_time;
|
||||
}
|
||||
|
||||
/// Upload any images to the GPU that need to be uploaded,
|
||||
/// and remove any images that are no longer needed on the GPU.
|
||||
fn uploadKittyImages(self: *Self) !void {
|
||||
var image_it = self.images.iterator();
|
||||
while (image_it.next()) |kv| {
|
||||
const img = &kv.value_ptr.image;
|
||||
if (img.isUnloading()) {
|
||||
img.deinit(self.alloc);
|
||||
self.images.removeByPtr(kv.key_ptr);
|
||||
continue;
|
||||
}
|
||||
if (img.isPending()) try img.upload(self.alloc, &self.api);
|
||||
}
|
||||
}
|
||||
|
||||
/// Call this any time the background image path changes.
|
||||
///
|
||||
/// Caller must hold the draw mutex.
|
||||
@@ -2531,6 +2176,51 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the overlay as configured. Returns null if there is no
|
||||
/// overlay currently configured.
|
||||
fn rebuildOverlay(
|
||||
self: *Self,
|
||||
features: []const Overlay.Feature,
|
||||
) Overlay.InitError!void {
|
||||
// const start = std.time.Instant.now() catch unreachable;
|
||||
// const start_micro = std.time.microTimestamp();
|
||||
// defer {
|
||||
// const end = std.time.Instant.now() catch unreachable;
|
||||
// log.warn(
|
||||
// "[rebuildOverlay time] start_micro={} duration={}ns",
|
||||
// .{ start_micro, end.since(start) / std.time.ns_per_us },
|
||||
// );
|
||||
// }
|
||||
|
||||
const alloc = self.alloc;
|
||||
|
||||
// If we have no features enabled, don't build an overlay.
|
||||
// If we had a previous overlay, deallocate it.
|
||||
if (features.len == 0) {
|
||||
if (self.overlay) |*old| {
|
||||
old.deinit(alloc);
|
||||
self.overlay = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If we had a previous overlay, clear it. Otherwise, init.
|
||||
const overlay: *Overlay = if (self.overlay) |*v| overlay: {
|
||||
v.reset();
|
||||
break :overlay v;
|
||||
} else overlay: {
|
||||
const new: Overlay = try .init(alloc, self.size);
|
||||
self.overlay = new;
|
||||
break :overlay &self.overlay.?;
|
||||
};
|
||||
overlay.applyFeatures(
|
||||
alloc,
|
||||
&self.terminal_state,
|
||||
features,
|
||||
);
|
||||
}
|
||||
|
||||
const PreeditRange = struct {
|
||||
y: terminal.size.CellCountInt,
|
||||
x: [2]terminal.size.CellCountInt,
|
||||
|
||||
@@ -2,16 +2,629 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const wuffs = @import("wuffs");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
|
||||
const Renderer = @import("../renderer.zig").Renderer;
|
||||
const GraphicsAPI = Renderer.API;
|
||||
const Texture = GraphicsAPI.Texture;
|
||||
const CellSize = @import("size.zig").CellSize;
|
||||
const Overlay = @import("Overlay.zig");
|
||||
|
||||
const log = std.log.scoped(.renderer_image);
|
||||
|
||||
/// Generic image rendering state for the renderer. This stores all
|
||||
/// images and their placements and exposes only a limited public API
|
||||
/// for adding images and placements and drawing them.
|
||||
pub const State = struct {
|
||||
/// The full image state for the renderer that specifies what images
|
||||
/// need to be uploaded, pruned, etc.
|
||||
images: ImageMap,
|
||||
|
||||
/// The placements for the Kitty image protocol.
|
||||
kitty_placements: std.ArrayListUnmanaged(Placement),
|
||||
|
||||
/// The end index (exclusive) for placements that should be
|
||||
/// drawn below the background, below the text, etc.
|
||||
kitty_bg_end: u32,
|
||||
kitty_text_end: u32,
|
||||
|
||||
/// True if there are any virtual placements. This needs to be known
|
||||
/// because virtual placements need to be recalculated more often
|
||||
/// on frame builds and are generally more expensive to handle.
|
||||
kitty_virtual: bool,
|
||||
|
||||
/// Overlays
|
||||
overlay_placements: std.ArrayListUnmanaged(Placement),
|
||||
|
||||
pub const empty: State = .{
|
||||
.images = .empty,
|
||||
.kitty_placements = .empty,
|
||||
.kitty_bg_end = 0,
|
||||
.kitty_text_end = 0,
|
||||
.kitty_virtual = false,
|
||||
.overlay_placements = .empty,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *State, alloc: Allocator) void {
|
||||
{
|
||||
var it = self.images.iterator();
|
||||
while (it.next()) |kv| kv.value_ptr.image.deinit(alloc);
|
||||
self.images.deinit(alloc);
|
||||
}
|
||||
self.kitty_placements.deinit(alloc);
|
||||
self.overlay_placements.deinit(alloc);
|
||||
}
|
||||
|
||||
/// Upload any images to the GPU that need to be uploaded,
|
||||
/// and remove any images that are no longer needed on the GPU.
|
||||
///
|
||||
/// If any uploads fail, they are ignored. The return value
|
||||
/// can be used to detect if upload was a total success (true)
|
||||
/// or not (false).
|
||||
pub fn upload(
|
||||
self: *State,
|
||||
alloc: Allocator,
|
||||
api: *GraphicsAPI,
|
||||
) bool {
|
||||
var success: bool = true;
|
||||
var image_it = self.images.iterator();
|
||||
while (image_it.next()) |kv| {
|
||||
const img = &kv.value_ptr.image;
|
||||
if (img.isUnloading()) {
|
||||
img.deinit(alloc);
|
||||
self.images.removeByPtr(kv.key_ptr);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (img.isPending()) {
|
||||
img.upload(
|
||||
alloc,
|
||||
api,
|
||||
) catch |err| {
|
||||
log.warn("error uploading image to GPU err={}", .{err});
|
||||
success = false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
pub const DrawPlacements = enum {
|
||||
kitty_below_bg,
|
||||
kitty_below_text,
|
||||
kitty_above_text,
|
||||
overlay,
|
||||
};
|
||||
|
||||
/// Draw the given named set of placements.
|
||||
///
|
||||
/// Any placements that have non-uploaded images are ignored. Any
|
||||
/// graphics API errors during drawing are also ignored.
|
||||
pub fn draw(
|
||||
self: *State,
|
||||
api: *GraphicsAPI,
|
||||
pipeline: GraphicsAPI.Pipeline,
|
||||
pass: *GraphicsAPI.RenderPass,
|
||||
placement_type: DrawPlacements,
|
||||
) void {
|
||||
const placements: []const Placement = switch (placement_type) {
|
||||
.kitty_below_bg => self.kitty_placements.items[0..self.kitty_bg_end],
|
||||
.kitty_below_text => self.kitty_placements.items[self.kitty_bg_end..self.kitty_text_end],
|
||||
.kitty_above_text => self.kitty_placements.items[self.kitty_text_end..],
|
||||
.overlay => self.overlay_placements.items,
|
||||
};
|
||||
|
||||
for (placements) |p| {
|
||||
// Look up the image
|
||||
const image = self.images.get(p.image_id) orelse {
|
||||
log.warn("image not found for placement image_id={}", .{p.image_id});
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get the texture
|
||||
const texture = switch (image.image) {
|
||||
.ready,
|
||||
.unload_ready,
|
||||
=> |t| t,
|
||||
else => {
|
||||
log.warn("image not ready for placement image_id={}", .{p.image_id});
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
// Create our vertex buffer, which is always exactly one item.
|
||||
// future(mitchellh): we can group rendering multiple instances of a single image
|
||||
var buf = GraphicsAPI.Buffer(GraphicsAPI.shaders.Image).initFill(
|
||||
api.imageBufferOptions(),
|
||||
&.{.{
|
||||
.grid_pos = .{
|
||||
@as(f32, @floatFromInt(p.x)),
|
||||
@as(f32, @floatFromInt(p.y)),
|
||||
},
|
||||
|
||||
.cell_offset = .{
|
||||
@as(f32, @floatFromInt(p.cell_offset_x)),
|
||||
@as(f32, @floatFromInt(p.cell_offset_y)),
|
||||
},
|
||||
|
||||
.source_rect = .{
|
||||
@as(f32, @floatFromInt(p.source_x)),
|
||||
@as(f32, @floatFromInt(p.source_y)),
|
||||
@as(f32, @floatFromInt(p.source_width)),
|
||||
@as(f32, @floatFromInt(p.source_height)),
|
||||
},
|
||||
|
||||
.dest_size = .{
|
||||
@as(f32, @floatFromInt(p.width)),
|
||||
@as(f32, @floatFromInt(p.height)),
|
||||
},
|
||||
}},
|
||||
) catch |err| {
|
||||
log.warn("error creating image vertex buffer err={}", .{err});
|
||||
continue;
|
||||
};
|
||||
defer buf.deinit();
|
||||
|
||||
pass.step(.{
|
||||
.pipeline = pipeline,
|
||||
.buffers = &.{buf.buffer},
|
||||
.textures = &.{texture},
|
||||
.draw = .{
|
||||
.type = .triangle_strip,
|
||||
.vertex_count = 4,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Update our overlay state. Null value deletes any existing overlay.
|
||||
pub fn overlayUpdate(
|
||||
self: *State,
|
||||
alloc: Allocator,
|
||||
overlay_: ?Overlay,
|
||||
) !void {
|
||||
const overlay = overlay_ orelse {
|
||||
// If we don't have an overlay, remove any existing one.
|
||||
if (self.images.getPtr(.overlay)) |data| {
|
||||
data.image.markForUnload();
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
// For transmit time we always just use the current time
|
||||
// and overwrite the overlay.
|
||||
const transmit_time = try std.time.Instant.now();
|
||||
|
||||
// Ensure we have space for our overlay placement. Do this before
|
||||
// we upload our image so we don't have to deal with cleaning
|
||||
// that up.
|
||||
self.overlay_placements.clearRetainingCapacity();
|
||||
try self.overlay_placements.ensureUnusedCapacity(alloc, 1);
|
||||
|
||||
// Setup our image.
|
||||
const pending = overlay.pendingImage();
|
||||
try self.prepImage(
|
||||
alloc,
|
||||
.overlay,
|
||||
transmit_time,
|
||||
pending,
|
||||
);
|
||||
errdefer comptime unreachable;
|
||||
|
||||
// Setup our placement
|
||||
self.overlay_placements.appendAssumeCapacity(.{
|
||||
.image_id = .overlay,
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.z = 0,
|
||||
.width = pending.width,
|
||||
.height = pending.height,
|
||||
.cell_offset_x = 0,
|
||||
.cell_offset_y = 0,
|
||||
.source_x = 0,
|
||||
.source_y = 0,
|
||||
.source_width = pending.width,
|
||||
.source_height = pending.height,
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns true if the Kitty graphics state requires an update based
|
||||
/// on the terminal state and our internal state.
|
||||
///
|
||||
/// This does not read/write state used by drawing.
|
||||
pub fn kittyRequiresUpdate(
|
||||
self: *const State,
|
||||
t: *const terminal.Terminal,
|
||||
) bool {
|
||||
// If the terminal kitty image state is dirty, we must update.
|
||||
if (t.screens.active.kitty_images.dirty) return true;
|
||||
|
||||
// If we have any virtual references, we must also rebuild our
|
||||
// kitty state on every frame because any cell change can move
|
||||
// an image. If the virtual placements were removed, this will
|
||||
// be set to false on the next update.
|
||||
if (self.kitty_virtual) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Update the Kitty graphics state from the terminal.
|
||||
///
|
||||
/// This reads/writes state used by drawing.
|
||||
pub fn kittyUpdate(
|
||||
self: *State,
|
||||
alloc: Allocator,
|
||||
t: *const terminal.Terminal,
|
||||
cell_size: CellSize,
|
||||
) void {
|
||||
const storage = &t.screens.active.kitty_images;
|
||||
defer storage.dirty = false;
|
||||
|
||||
// We always clear our previous placements no matter what because
|
||||
// we rebuild them from scratch.
|
||||
self.kitty_placements.clearRetainingCapacity();
|
||||
self.kitty_virtual = false;
|
||||
|
||||
// Go through our known images and if there are any that are no longer
|
||||
// in use then mark them to be freed.
|
||||
//
|
||||
// This never conflicts with the below because a placement can't
|
||||
// reference an image that doesn't exist.
|
||||
{
|
||||
var it = self.images.iterator();
|
||||
while (it.next()) |kv| {
|
||||
switch (kv.key_ptr.*) {
|
||||
// We're only looking at Kitty images
|
||||
.kitty => |id| if (storage.imageById(id) == null) {
|
||||
kv.value_ptr.image.markForUnload();
|
||||
},
|
||||
|
||||
.overlay => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The top-left and bottom-right corners of our viewport in screen
|
||||
// points. This lets us determine offsets and containment of placements.
|
||||
const top = t.screens.active.pages.getTopLeft(.viewport);
|
||||
const bot = t.screens.active.pages.getBottomRight(.viewport).?;
|
||||
const top_y = t.screens.active.pages.pointFromPin(.screen, top).?.screen.y;
|
||||
const bot_y = t.screens.active.pages.pointFromPin(.screen, bot).?.screen.y;
|
||||
|
||||
// Go through the placements and ensure the image is
|
||||
// on the GPU or else is ready to be sent to the GPU.
|
||||
var it = storage.placements.iterator();
|
||||
while (it.next()) |kv| {
|
||||
const p = kv.value_ptr;
|
||||
|
||||
// Special logic based on location
|
||||
switch (p.location) {
|
||||
.pin => {},
|
||||
.virtual => {
|
||||
// We need to mark virtual placements on our renderer so that
|
||||
// we know to rebuild in more scenarios since cell changes can
|
||||
// now trigger placement changes.
|
||||
self.kitty_virtual = true;
|
||||
|
||||
// We also continue out because virtual placements are
|
||||
// only triggered by the unicode placeholder, not by the
|
||||
// placement itself.
|
||||
continue;
|
||||
},
|
||||
}
|
||||
|
||||
// Get the image for the placement
|
||||
const image = storage.imageById(kv.key_ptr.image_id) orelse {
|
||||
log.warn(
|
||||
"missing image for placement, ignoring image_id={}",
|
||||
.{kv.key_ptr.image_id},
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
self.prepKittyPlacement(
|
||||
alloc,
|
||||
t,
|
||||
top_y,
|
||||
bot_y,
|
||||
&image,
|
||||
p,
|
||||
) catch |err| {
|
||||
// For errors we log and continue. We try to place
|
||||
// other placements even if one fails.
|
||||
log.warn("error preparing kitty placement err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
// If we have virtual placements then we need to scan for placeholders.
|
||||
if (self.kitty_virtual) {
|
||||
var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot);
|
||||
while (v_it.next()) |virtual_p| {
|
||||
self.prepKittyVirtualPlacement(
|
||||
alloc,
|
||||
t,
|
||||
&virtual_p,
|
||||
cell_size,
|
||||
) catch |err| {
|
||||
// For errors we log and continue. We try to place
|
||||
// other placements even if one fails.
|
||||
log.warn("error preparing kitty placement err={}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the placements by their Z value.
|
||||
std.mem.sortUnstable(
|
||||
Placement,
|
||||
self.kitty_placements.items,
|
||||
{},
|
||||
struct {
|
||||
fn lessThan(
|
||||
ctx: void,
|
||||
lhs: Placement,
|
||||
rhs: Placement,
|
||||
) bool {
|
||||
_ = ctx;
|
||||
return lhs.z < rhs.z or
|
||||
(lhs.z == rhs.z and lhs.image_id.zLessThan(rhs.image_id));
|
||||
}
|
||||
}.lessThan,
|
||||
);
|
||||
|
||||
// Find our indices. The values are sorted by z so we can
|
||||
// find the first placement out of bounds to find the limits.
|
||||
const bg_limit = std.math.minInt(i32) / 2;
|
||||
var bg_end: ?u32 = null;
|
||||
var text_end: ?u32 = null;
|
||||
for (self.kitty_placements.items, 0..) |p, i| {
|
||||
if (bg_end == null and p.z >= bg_limit) bg_end = @intCast(i);
|
||||
if (text_end == null and p.z >= 0) text_end = @intCast(i);
|
||||
}
|
||||
|
||||
// If we didn't see any images with a z > the bg limit,
|
||||
// then our bg end is the end of our placement list.
|
||||
self.kitty_bg_end =
|
||||
bg_end orelse @intCast(self.kitty_placements.items.len);
|
||||
// Same idea for the image_text_end.
|
||||
self.kitty_text_end =
|
||||
text_end orelse @intCast(self.kitty_placements.items.len);
|
||||
}
|
||||
|
||||
const PrepImageError = error{
|
||||
OutOfMemory,
|
||||
ImageConversionError,
|
||||
};
|
||||
|
||||
/// Get the viewport-relative position for this
|
||||
/// placement and add it to the placements list.
|
||||
fn prepKittyPlacement(
|
||||
self: *State,
|
||||
alloc: Allocator,
|
||||
t: *const terminal.Terminal,
|
||||
top_y: u32,
|
||||
bot_y: u32,
|
||||
image: *const terminal.kitty.graphics.Image,
|
||||
p: *const terminal.kitty.graphics.ImageStorage.Placement,
|
||||
) PrepImageError!void {
|
||||
// Get the rect for the placement. If this placement doesn't have
|
||||
// a rect then its virtual or something so skip it.
|
||||
const rect = p.rect(image.*, t) orelse return;
|
||||
|
||||
// This is expensive but necessary.
|
||||
const img_top_y = t.screens.active.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
|
||||
const img_bot_y = t.screens.active.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y;
|
||||
|
||||
// If the selection isn't within our viewport then skip it.
|
||||
if (img_top_y > bot_y) return;
|
||||
if (img_bot_y < top_y) return;
|
||||
|
||||
// We need to prep this image for upload if it isn't in the
|
||||
// cache OR it is in the cache but the transmit time doesn't
|
||||
// match meaning this image is different.
|
||||
try self.prepKittyImage(alloc, image);
|
||||
|
||||
// Calculate the dimensions of our image, taking in to
|
||||
// account the rows / columns specified by the placement.
|
||||
const dest_size = p.calculatedSize(image.*, t);
|
||||
|
||||
// Calculate the source rectangle
|
||||
const source_x = @min(image.width, p.source_x);
|
||||
const source_y = @min(image.height, p.source_y);
|
||||
const source_width = if (p.source_width > 0)
|
||||
@min(image.width - source_x, p.source_width)
|
||||
else
|
||||
image.width;
|
||||
const source_height = if (p.source_height > 0)
|
||||
@min(image.height - source_y, p.source_height)
|
||||
else
|
||||
image.height;
|
||||
|
||||
// Get the viewport-relative Y position of the placement.
|
||||
const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y));
|
||||
|
||||
// Accumulate the placement
|
||||
if (dest_size.width > 0 and dest_size.height > 0) {
|
||||
try self.kitty_placements.append(alloc, .{
|
||||
.image_id = .{ .kitty = image.id },
|
||||
.x = @intCast(rect.top_left.x),
|
||||
.y = y_pos,
|
||||
.z = p.z,
|
||||
.width = dest_size.width,
|
||||
.height = dest_size.height,
|
||||
.cell_offset_x = p.x_offset,
|
||||
.cell_offset_y = p.y_offset,
|
||||
.source_x = source_x,
|
||||
.source_y = source_y,
|
||||
.source_width = source_width,
|
||||
.source_height = source_height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn prepKittyVirtualPlacement(
|
||||
self: *State,
|
||||
alloc: Allocator,
|
||||
t: *const terminal.Terminal,
|
||||
p: *const terminal.kitty.graphics.unicode.Placement,
|
||||
cell_size: CellSize,
|
||||
) PrepImageError!void {
|
||||
const storage = &t.screens.active.kitty_images;
|
||||
const image = storage.imageById(p.image_id) orelse {
|
||||
log.warn(
|
||||
"missing image for virtual placement, ignoring image_id={}",
|
||||
.{p.image_id},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
const rp = p.renderPlacement(
|
||||
storage,
|
||||
&image,
|
||||
cell_size.width,
|
||||
cell_size.height,
|
||||
) catch |err| {
|
||||
log.warn("error rendering virtual placement err={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
// If our placement is zero sized then we don't do anything.
|
||||
if (rp.dest_width == 0 or rp.dest_height == 0) return;
|
||||
|
||||
const viewport: terminal.point.Point = t.screens.active.pages.pointFromPin(
|
||||
.viewport,
|
||||
rp.top_left,
|
||||
) orelse {
|
||||
// This is unreachable with virtual placements because we should
|
||||
// only ever be looking at virtual placements that are in our
|
||||
// viewport in the renderer and virtual placements only ever take
|
||||
// up one row.
|
||||
unreachable;
|
||||
};
|
||||
|
||||
// Prepare the image for the GPU and store the placement.
|
||||
try self.prepKittyImage(alloc, &image);
|
||||
try self.kitty_placements.append(alloc, .{
|
||||
.image_id = .{ .kitty = image.id },
|
||||
.x = @intCast(rp.top_left.x),
|
||||
.y = @intCast(viewport.viewport.y),
|
||||
.z = -1,
|
||||
.width = rp.dest_width,
|
||||
.height = rp.dest_height,
|
||||
.cell_offset_x = rp.offset_x,
|
||||
.cell_offset_y = rp.offset_y,
|
||||
.source_x = rp.source_x,
|
||||
.source_y = rp.source_y,
|
||||
.source_width = rp.source_width,
|
||||
.source_height = rp.source_height,
|
||||
});
|
||||
}
|
||||
|
||||
/// Prepare an image for upload to the GPU.
|
||||
fn prepImage(
|
||||
self: *State,
|
||||
alloc: Allocator,
|
||||
id: Id,
|
||||
transmit_time: std.time.Instant,
|
||||
pending: Image.Pending,
|
||||
) PrepImageError!void {
|
||||
// If this image exists and its transmit time is the same we assume
|
||||
// it is the identical image so we don't need to send it to the GPU.
|
||||
const gop = try self.images.getOrPut(alloc, id);
|
||||
if (gop.found_existing and
|
||||
gop.value_ptr.transmit_time.order(transmit_time) == .eq)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy the data so we own it.
|
||||
const data = if (alloc.dupe(
|
||||
u8,
|
||||
pending.dataSlice(),
|
||||
)) |v| v else |_| {
|
||||
if (!gop.found_existing) {
|
||||
// If this is a new entry we can just remove it since it
|
||||
// was never sent to the GPU.
|
||||
_ = self.images.remove(id);
|
||||
} else {
|
||||
// If this was an existing entry, it is invalid and
|
||||
// we must unload it.
|
||||
gop.value_ptr.image.markForUnload();
|
||||
}
|
||||
|
||||
return error.OutOfMemory;
|
||||
};
|
||||
// Note: we don't need to errdefer free the data because it is
|
||||
// put into the map immediately below and our errdefer to
|
||||
// handle our map state will fix this up.
|
||||
|
||||
// Store it in the map
|
||||
const new_image: Image = .{
|
||||
.pending = .{
|
||||
.width = pending.width,
|
||||
.height = pending.height,
|
||||
.pixel_format = pending.pixel_format,
|
||||
.data = data.ptr,
|
||||
},
|
||||
};
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = .{
|
||||
.image = new_image,
|
||||
.transmit_time = undefined,
|
||||
};
|
||||
} else {
|
||||
gop.value_ptr.image.markForReplace(
|
||||
alloc,
|
||||
new_image,
|
||||
);
|
||||
}
|
||||
|
||||
// If any error happens, we unload the image and it is invalid.
|
||||
errdefer gop.value_ptr.image.markForUnload();
|
||||
|
||||
gop.value_ptr.image.prepForUpload(alloc) catch |err| {
|
||||
log.warn("error preparing image for upload err={}", .{err});
|
||||
return error.ImageConversionError;
|
||||
};
|
||||
gop.value_ptr.transmit_time = transmit_time;
|
||||
}
|
||||
|
||||
/// Prepare the provided Kitty image for upload to the GPU by copying its
|
||||
/// data with our allocator and setting it to the pending state.
|
||||
fn prepKittyImage(
|
||||
self: *State,
|
||||
alloc: Allocator,
|
||||
image: *const terminal.kitty.graphics.Image,
|
||||
) PrepImageError!void {
|
||||
try self.prepImage(
|
||||
alloc,
|
||||
.{ .kitty = image.id },
|
||||
image.transmit_time,
|
||||
.{
|
||||
.width = image.width,
|
||||
.height = image.height,
|
||||
.pixel_format = switch (image.format) {
|
||||
.gray => .gray,
|
||||
.gray_alpha => .gray_alpha,
|
||||
.rgb => .rgb,
|
||||
.rgba => .rgba,
|
||||
.png => unreachable, // should be decoded by now
|
||||
},
|
||||
|
||||
// constCasts are always gross but this one is safe is because
|
||||
// the data is only read from here and copied into its own
|
||||
// buffer.
|
||||
.data = @constCast(image.data.ptr),
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/// Represents a single image placement on the grid.
|
||||
/// A placement is a request to render an instance of an image.
|
||||
pub const Placement = struct {
|
||||
/// The image being rendered. This MUST be in the image map.
|
||||
image_id: u32,
|
||||
image_id: Id,
|
||||
|
||||
/// The grid x/y where this placement is located.
|
||||
x: i32,
|
||||
@@ -34,8 +647,46 @@ pub const Placement = struct {
|
||||
source_height: u32,
|
||||
};
|
||||
|
||||
/// Image identifier used to store and lookup images.
|
||||
///
|
||||
/// This is tagged by different image types to make it easier to
|
||||
/// store different kinds of images in the same map without having
|
||||
/// to worry about ID collisions.
|
||||
pub const Id = union(enum) {
|
||||
/// Image sent to the terminal state via the kitty graphics protocol.
|
||||
/// The value is the ID assigned by the terminal.
|
||||
kitty: u32,
|
||||
|
||||
/// Debug overlay. This is always composited down to a single
|
||||
/// image for now. In the future we can support layers here if we want.
|
||||
overlay,
|
||||
|
||||
/// Z-ordering tie-breaker for images with the same z value.
|
||||
pub fn zLessThan(lhs: Id, rhs: Id) bool {
|
||||
// If our tags aren't the same, we sort by tag.
|
||||
if (std.meta.activeTag(lhs) != std.meta.activeTag(rhs)) {
|
||||
return switch (lhs) {
|
||||
// Kitty images always sort before (lower z) non-kitty images.
|
||||
.kitty => true,
|
||||
|
||||
.overlay => false,
|
||||
};
|
||||
}
|
||||
|
||||
switch (lhs) {
|
||||
.kitty => |lhs_id| {
|
||||
const rhs_id = rhs.kitty;
|
||||
return lhs_id < rhs_id;
|
||||
},
|
||||
|
||||
// No sensical ordering
|
||||
.overlay => return false,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// The map used for storing images.
|
||||
pub const ImageMap = std.AutoHashMapUnmanaged(u32, struct {
|
||||
pub const ImageMap = std.AutoHashMapUnmanaged(Id, struct {
|
||||
image: Image,
|
||||
transmit_time: std.time.Instant,
|
||||
});
|
||||
@@ -221,27 +872,33 @@ pub const Image = union(enum) {
|
||||
try self.convert(alloc);
|
||||
}
|
||||
|
||||
/// Upload the pending image to the GPU and
|
||||
/// change the state of this image to ready.
|
||||
/// Upload the pending image to the GPU and change the state of this
|
||||
/// image to ready.
|
||||
pub fn upload(
|
||||
self: *Image,
|
||||
alloc: Allocator,
|
||||
api: *const GraphicsAPI,
|
||||
) !void {
|
||||
) (wuffs.Error || error{
|
||||
/// Texture creation failed, usually a GPU memory issue.
|
||||
UploadFailed,
|
||||
})!void {
|
||||
assert(self.isPending());
|
||||
|
||||
// No error recover is required after this call because it just
|
||||
// converts in place and is idempotent.
|
||||
try self.prepForUpload(alloc);
|
||||
|
||||
// Get our pending info
|
||||
const p = self.getPending().?;
|
||||
|
||||
// Create our texture
|
||||
const texture = try Texture.init(
|
||||
const texture = Texture.init(
|
||||
api.imageTextureOptions(.rgba, true),
|
||||
@intCast(p.width),
|
||||
@intCast(p.height),
|
||||
p.dataSlice(),
|
||||
);
|
||||
) catch return error.UploadFailed;
|
||||
errdefer comptime unreachable;
|
||||
|
||||
// Uploaded. We can now clear our data and change our state.
|
||||
//
|
||||
|
||||
@@ -16,8 +16,8 @@ pub fn neverExtendBg(
|
||||
// because prompts often contain special formatting (such as
|
||||
// powerline) that looks bad when extended.
|
||||
switch (row.semantic_prompt) {
|
||||
.prompt, .prompt_continuation, .input => return true,
|
||||
.unknown, .command => {},
|
||||
.prompt, .prompt_continuation => return true,
|
||||
.none => {},
|
||||
}
|
||||
|
||||
for (0.., cells) |x, *cell| {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# We need to be in interactive mode to proceed.
|
||||
if [[ "$-" != *i* ]] ; then builtin return; fi
|
||||
if [[ "$-" != *i* ]]; then builtin return; fi
|
||||
|
||||
# When automatic shell integration is active, we were started in POSIX
|
||||
# mode and need to manually recreate the bash startup sequence.
|
||||
@@ -49,7 +49,10 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
|
||||
if [[ $__ghostty_bash_flags != *"--noprofile"* ]]; then
|
||||
[ -r /etc/profile ] && builtin source "/etc/profile"
|
||||
for __ghostty_rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
|
||||
[ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile"; break; }
|
||||
[ -r "$__ghostty_rcfile" ] && {
|
||||
builtin source "$__ghostty_rcfile"
|
||||
break
|
||||
}
|
||||
done
|
||||
fi
|
||||
else
|
||||
@@ -61,7 +64,10 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
|
||||
# Void Linux uses /etc/bash/bashrc
|
||||
# Nixos uses /etc/bashrc
|
||||
for __ghostty_rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do
|
||||
[ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile"; break; }
|
||||
[ -r "$__ghostty_rcfile" ] && {
|
||||
builtin source "$__ghostty_rcfile"
|
||||
break
|
||||
}
|
||||
done
|
||||
if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi
|
||||
[ -r "$GHOSTTY_BASH_RCFILE" ] && builtin source "$GHOSTTY_BASH_RCFILE"
|
||||
@@ -101,9 +107,9 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then
|
||||
fi
|
||||
done
|
||||
if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then
|
||||
builtin command sudo "$@";
|
||||
builtin command sudo "$@"
|
||||
else
|
||||
builtin command sudo --preserve-env=TERMINFO "$@";
|
||||
builtin command sudo --preserve-env=TERMINFO "$@"
|
||||
fi
|
||||
}
|
||||
fi
|
||||
@@ -127,8 +133,8 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then
|
||||
|
||||
while IFS=' ' read -r ssh_key ssh_value; do
|
||||
case "$ssh_key" in
|
||||
user) ssh_user="$ssh_value" ;;
|
||||
hostname) ssh_hostname="$ssh_value" ;;
|
||||
user) ssh_user="$ssh_value" ;;
|
||||
hostname) ssh_hostname="$ssh_value" ;;
|
||||
esac
|
||||
[[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break
|
||||
done < <(builtin command ssh -G "$@" 2>/dev/null)
|
||||
@@ -187,66 +193,71 @@ _ghostty_executing=""
|
||||
_ghostty_last_reported_cwd=""
|
||||
|
||||
function __ghostty_precmd() {
|
||||
local ret="$?"
|
||||
if test "$_ghostty_executing" != "0"; then
|
||||
_GHOSTTY_SAVE_PS1="$PS1"
|
||||
_GHOSTTY_SAVE_PS2="$PS2"
|
||||
local ret="$?"
|
||||
if test "$_ghostty_executing" != "0"; then
|
||||
_GHOSTTY_SAVE_PS1="$PS1"
|
||||
_GHOSTTY_SAVE_PS2="$PS2"
|
||||
|
||||
# Marks
|
||||
PS1=$PS1'\[\e]133;B\a\]'
|
||||
PS2=$PS2'\[\e]133;B\a\]'
|
||||
# Marks. We need to do fresh line (A) at the beginning of the prompt
|
||||
# since if the cursor is not at the beginning of a line, the terminal
|
||||
# will emit a newline.
|
||||
PS1='\[\e]133;A;redraw=last\a\]'$PS1'\[\e]133;B\a\]'
|
||||
PS2='\[\e]133;A;k=s\a\]'$PS2'\[\e]133;B\a\]'
|
||||
|
||||
# bash doesn't redraw the leading lines in a multiline prompt so
|
||||
# mark the last line as a secondary prompt (k=s) to prevent the
|
||||
# preceding lines from being erased by ghostty after a resize.
|
||||
if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then
|
||||
PS1=$PS1'\[\e]133;A;k=s\a\]'
|
||||
fi
|
||||
|
||||
# Cursor
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
|
||||
[[ "$PS1" != *'\[\e[5 q\]'* ]] && PS1=$PS1'\[\e[5 q\]' # input
|
||||
[[ "$PS0" != *'\[\e[0 q\]'* ]] && PS0=$PS0'\[\e[0 q\]' # reset
|
||||
fi
|
||||
|
||||
# Title (working directory)
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
PS1=$PS1'\[\e]2;\w\a\]'
|
||||
fi
|
||||
# Bash doesn't redraw the leading lines in a multiline prompt so
|
||||
# we mark the start of each line (after each newline) as a secondary
|
||||
# prompt. This correctly handles multiline prompts by setting the first
|
||||
# to primary and the subsequent lines to secondary.
|
||||
if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then
|
||||
builtin local __ghostty_mark=$'\\[\\e]133;A;k=s\\a\\]'
|
||||
PS1="${PS1//$'\n'/$'\n'$__ghostty_mark}"
|
||||
PS1="${PS1//\\n/\\n$__ghostty_mark}"
|
||||
fi
|
||||
|
||||
if test "$_ghostty_executing" != ""; then
|
||||
# End of current command. Report its status.
|
||||
builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID"
|
||||
# Cursor
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
|
||||
[[ "$PS1" != *'\[\e[5 q\]'* ]] && PS1=$PS1'\[\e[5 q\]' # input
|
||||
[[ "$PS0" != *'\[\e[0 q\]'* ]] && PS0=$PS0'\[\e[0 q\]' # reset
|
||||
fi
|
||||
|
||||
# unfortunately bash provides no hooks to detect cwd changes
|
||||
# in particular this means cwd reporting will not happen for a
|
||||
# command like cd /test && cat. PS0 is evaluated before cd is run.
|
||||
if [[ "$_ghostty_last_reported_cwd" != "$PWD" ]]; then
|
||||
_ghostty_last_reported_cwd="$PWD"
|
||||
builtin printf "\e]7;kitty-shell-cwd://%s%s\a" "$HOSTNAME" "$PWD"
|
||||
# Title (working directory)
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
PS1=$PS1'\[\e]2;\w\a\]'
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fresh line and start of prompt.
|
||||
builtin printf "\e]133;A;aid=%s\a" "$BASHPID"
|
||||
_ghostty_executing=0
|
||||
if test "$_ghostty_executing" != ""; then
|
||||
# End of current command. Report its status.
|
||||
builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID"
|
||||
fi
|
||||
|
||||
# unfortunately bash provides no hooks to detect cwd changes
|
||||
# in particular this means cwd reporting will not happen for a
|
||||
# command like cd /test && cat. PS0 is evaluated before cd is run.
|
||||
if [[ "$_ghostty_last_reported_cwd" != "$PWD" ]]; then
|
||||
_ghostty_last_reported_cwd="$PWD"
|
||||
builtin printf "\e]7;kitty-shell-cwd://%s%s\a" "$HOSTNAME" "$PWD"
|
||||
fi
|
||||
|
||||
# Fresh line and start of prompt.
|
||||
builtin printf "\e]133;A;redraw=last;aid=%s\a" "$BASHPID"
|
||||
_ghostty_executing=0
|
||||
}
|
||||
|
||||
function __ghostty_preexec() {
|
||||
builtin local cmd="$1"
|
||||
builtin local cmd="$1"
|
||||
|
||||
PS1="$_GHOSTTY_SAVE_PS1"
|
||||
PS2="$_GHOSTTY_SAVE_PS2"
|
||||
PS1="$_GHOSTTY_SAVE_PS1"
|
||||
PS2="$_GHOSTTY_SAVE_PS2"
|
||||
|
||||
# Title (current command)
|
||||
if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]}"
|
||||
fi
|
||||
# Title (current command)
|
||||
if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]/}"
|
||||
fi
|
||||
|
||||
# End of input, start of output.
|
||||
builtin printf "\e]133;C;\a"
|
||||
_ghostty_executing=1
|
||||
# End of input, start of output.
|
||||
builtin printf "\e]133;C;\a"
|
||||
_ghostty_executing=1
|
||||
}
|
||||
|
||||
preexec_functions+=(__ghostty_preexec)
|
||||
|
||||
@@ -132,6 +132,7 @@ _ghostty_deferred_init() {
|
||||
# asynchronously from a `zle -F` handler might still remove our
|
||||
# marks. Oh well.
|
||||
builtin local mark2=$'%{\e]133;A;k=s\a%}'
|
||||
builtin local markB=$'%{\e]133;B\a%}'
|
||||
# Add marks conditionally to avoid a situation where we have
|
||||
# several marks in place. These conditions can have false
|
||||
# positives and false negatives though.
|
||||
@@ -139,8 +140,17 @@ _ghostty_deferred_init() {
|
||||
# - False positive (with prompt_percent): PS1="%(?.$mark1.)"
|
||||
# - False negative (with prompt_subst): PS1='$mark1'
|
||||
[[ $PS1 == *$mark1* ]] || PS1=${mark1}${PS1}
|
||||
[[ $PS1 == *$markB* ]] || PS1=${PS1}${markB}
|
||||
# Handle multiline prompts by marking continuation lines as
|
||||
# secondary by replacing newlines with being prefixed
|
||||
# with k=s
|
||||
if [[ $PS1 == *$'\n'* ]]; then
|
||||
PS1=${PS1//$'\n'/$'\n'${mark2}}
|
||||
fi
|
||||
|
||||
# PS2 mark is needed when clearing the prompt on resize
|
||||
[[ $PS2 == *$mark2* ]] || PS2=${mark2}${PS2}
|
||||
[[ $PS2 == *$markB* ]] || PS2=${PS2}${markB}
|
||||
(( _ghostty_state = 2 ))
|
||||
else
|
||||
# If our precmd hook is not the last, we cannot rely on prompt
|
||||
@@ -179,7 +189,10 @@ _ghostty_deferred_init() {
|
||||
# top. We cannot force prompt_subst on the user though, so we would
|
||||
# still need this code for the no_prompt_subst case.
|
||||
PS1=${PS1//$'%{\e]133;A\a%}'}
|
||||
PS1=${PS1//$'%{\e]133;A;k=s\a%}'}
|
||||
PS1=${PS1//$'%{\e]133;B\a%}'}
|
||||
PS2=${PS2//$'%{\e]133;A;k=s\a%}'}
|
||||
PS2=${PS2//$'%{\e]133;B\a%}'}
|
||||
|
||||
# This will work incorrectly in the presence of a preexec hook that
|
||||
# prints. For example, if MichaelAquilina/zsh-you-should-use installs
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ const charsets = @import("charsets.zig");
|
||||
const csi = @import("csi.zig");
|
||||
const hyperlink = @import("hyperlink.zig");
|
||||
const kitty = @import("kitty.zig");
|
||||
const osc = @import("osc.zig");
|
||||
const point = @import("point.zig");
|
||||
const sgr = @import("sgr.zig");
|
||||
const Tabstops = @import("Tabstops.zig");
|
||||
@@ -79,11 +80,10 @@ mouse_shape: mouse_shape_pkg.MouseShape = .text,
|
||||
|
||||
/// These are just a packed set of flags we may set on the terminal.
|
||||
flags: packed struct {
|
||||
// This isn't a mode, this is set by OSC 133 using the "A" event.
|
||||
// If this is true, it tells us that the shell supports redrawing
|
||||
// the prompt and that when we resize, if the cursor is at a prompt,
|
||||
// then we should clear the screen below and allow the shell to redraw.
|
||||
shell_redraws_prompt: bool = false,
|
||||
// This supports a Kitty extension where programs using semantic
|
||||
// prompts (OSC133) can annotate their new prompts with `redraw=0` to
|
||||
// disable clearing the prompt on resize.
|
||||
shell_redraws_prompt: osc.semantic_prompt.Redraw = .true,
|
||||
|
||||
// This is set via ESC[4;2m. Any other modify key mode just sets
|
||||
// this to false and we act in mode 1 by default.
|
||||
@@ -778,6 +778,7 @@ fn printCell(
|
||||
.style_id = self.screens.active.cursor.style_id,
|
||||
.wide = wide,
|
||||
.protected = self.screens.active.cursor.protected,
|
||||
.semantic_content = self.screens.active.cursor.semantic_content,
|
||||
};
|
||||
|
||||
if (style_changed) {
|
||||
@@ -820,22 +821,35 @@ fn printWrap(self: *Terminal) !void {
|
||||
// We only mark that we soft-wrapped if we're at the edge of our
|
||||
// full screen. We don't mark the row as wrapped if we're in the
|
||||
// middle due to a right margin.
|
||||
const mark_wrap = self.screens.active.cursor.x == self.cols - 1;
|
||||
if (mark_wrap) self.screens.active.cursor.page_row.wrap = true;
|
||||
const cursor: *Screen.Cursor = &self.screens.active.cursor;
|
||||
const mark_wrap = cursor.x == self.cols - 1;
|
||||
if (mark_wrap) cursor.page_row.wrap = true;
|
||||
|
||||
// Get the old semantic prompt so we can extend it to the next
|
||||
// line. We need to do this before we index() because we may
|
||||
// modify memory.
|
||||
const old_prompt = self.screens.active.cursor.page_row.semantic_prompt;
|
||||
const old_semantic = cursor.semantic_content;
|
||||
const old_semantic_clear = cursor.semantic_content_clear_eol;
|
||||
|
||||
// Move to the next line
|
||||
try self.index();
|
||||
self.screens.active.cursorHorizontalAbsolute(self.scrolling_region.left);
|
||||
|
||||
// Our pointer should never move
|
||||
assert(cursor == &self.screens.active.cursor);
|
||||
|
||||
// We always reset our semantic prompt state
|
||||
cursor.semantic_content = old_semantic;
|
||||
cursor.semantic_content_clear_eol = old_semantic_clear;
|
||||
switch (old_semantic) {
|
||||
.output, .input => {},
|
||||
.prompt => cursor.page_row.semantic_prompt = .prompt_continuation,
|
||||
}
|
||||
|
||||
if (mark_wrap) {
|
||||
// New line must inherit semantic prompt of the old line
|
||||
self.screens.active.cursor.page_row.semantic_prompt = old_prompt;
|
||||
self.screens.active.cursor.page_row.wrap_continuation = true;
|
||||
const row = self.screens.active.cursor.page_row;
|
||||
// Always mark the row as a continuation
|
||||
row.wrap_continuation = true;
|
||||
}
|
||||
|
||||
// Assure that our screen is consistent
|
||||
@@ -1124,6 +1138,127 @@ pub fn setProtectedMode(self: *Terminal, mode: ansi.ProtectedMode) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a semantic prompt command.
|
||||
///
|
||||
/// If there is an error, we do our best to get the terminal into
|
||||
/// some coherent state, since callers typically can't handle errors
|
||||
/// (since they're sending sequences via the pty).
|
||||
pub fn semanticPrompt(
|
||||
self: *Terminal,
|
||||
cmd: osc.Command.SemanticPrompt,
|
||||
) !void {
|
||||
switch (cmd.action) {
|
||||
.fresh_line => try self.semanticPromptFreshLine(),
|
||||
|
||||
.fresh_line_new_prompt => {
|
||||
// "First do a fresh-line."
|
||||
try self.semanticPromptFreshLine();
|
||||
|
||||
// "Subsequent text (until a OSC "133;B" or OSC "133;I" command)
|
||||
// is a prompt string (as if followed by OSC 133;P;k=i\007)."
|
||||
self.screens.active.cursorSetSemanticContent(.{
|
||||
.prompt = cmd.readOption(.prompt_kind) orelse .initial,
|
||||
});
|
||||
|
||||
// This is a kitty-specific flag that notes that the shell
|
||||
// is NOT capable of redraw. Redraw defaults to true so this
|
||||
// usually just disables it, but either is possible.
|
||||
if (cmd.readOption(.redraw)) |v| {
|
||||
self.flags.shell_redraws_prompt = v;
|
||||
}
|
||||
|
||||
// The "aid" and "cl" options are also valid for this
|
||||
// command but we don't yet handle these in any meaningful way.
|
||||
},
|
||||
|
||||
.new_command => {
|
||||
// Same as OSC "133;A" but may first implicitly terminate a
|
||||
// previous command: if the options specify an aid and there
|
||||
// is an active (open) command with matching aid, finish the
|
||||
// innermost such command (as well as any other commands
|
||||
// nested more deeply). If no aid is specified, treat as an
|
||||
// aid whose value is the empty string.
|
||||
try self.semanticPrompt(.{
|
||||
.action = .fresh_line_new_prompt,
|
||||
.options_unvalidated = cmd.options_unvalidated,
|
||||
});
|
||||
},
|
||||
|
||||
.prompt_start => {
|
||||
// Explicit start of prompt. Optional after an A or N command.
|
||||
// The k (kind) option specifies the type of prompt:
|
||||
// regular primary prompt (k=i or default),
|
||||
// right-side prompts (k=r), or prompts for continuation lines (k=c or k=s).
|
||||
self.screens.active.cursorSetSemanticContent(.{
|
||||
.prompt = cmd.readOption(.prompt_kind) orelse .initial,
|
||||
});
|
||||
},
|
||||
|
||||
.end_prompt_start_input => {
|
||||
// End of prompt and start of user input, terminated by a OSC
|
||||
// "133;C" or another prompt (OSC "133;P").
|
||||
self.screens.active.cursorSetSemanticContent(.{
|
||||
.input = .clear_explicit,
|
||||
});
|
||||
},
|
||||
|
||||
.end_prompt_start_input_terminate_eol => {
|
||||
// End of prompt and start of user input, terminated by end-of-line.
|
||||
self.screens.active.cursorSetSemanticContent(.{
|
||||
.input = .clear_eol,
|
||||
});
|
||||
},
|
||||
|
||||
.end_input_start_output => {
|
||||
// "End of input, and start of output."
|
||||
self.screens.active.cursorSetSemanticContent(.output);
|
||||
|
||||
// If our current row is marked as a prompt and we're
|
||||
// at column zero then we assume we're un-prompting. This
|
||||
// is a heuristic to deal with fish, mostly. The issue that
|
||||
// fish brings up is that it has no PS2 equivalent and its
|
||||
// builtin OSC133 marking doesn't output continuation lines
|
||||
// as k=s. So, we assume when we get a newline with a prompt
|
||||
// cursor that the new line is also a prompt. But fish changes
|
||||
// to output on the newline. So if we're at col 0 we just assume
|
||||
// we're overwriting the prompt.
|
||||
if (self.screens.active.cursor.page_row.semantic_prompt != .none and
|
||||
self.screens.active.cursor.x == 0)
|
||||
{
|
||||
self.screens.active.cursor.page_row.semantic_prompt = .none;
|
||||
}
|
||||
},
|
||||
|
||||
.end_command => {
|
||||
// From a terminal state perspective, this doesn't really do
|
||||
// anything. Other terminals appear to do nothing here. I think
|
||||
// its reasonable at this point to reset our semantic content
|
||||
// state but the spec doesn't really say what to do.
|
||||
self.screens.active.cursorSetSemanticContent(.output);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// OSC 133;L
|
||||
fn semanticPromptFreshLine(self: *Terminal) !void {
|
||||
const left_margin = if (self.screens.active.cursor.x < self.scrolling_region.left)
|
||||
0
|
||||
else
|
||||
self.scrolling_region.left;
|
||||
|
||||
// Spec: "If the cursor is the initial column (left, assuming
|
||||
// left-to-right writing), do nothing" This specification is very under
|
||||
// specified. We are taking the liberty to assume that in a left/right
|
||||
// margin context, if the cursor is outside of the left margin, we treat
|
||||
// it as being at the left margin for the purposes of this command.
|
||||
// This is arbitrary. If someone has a better reasonable idea we can
|
||||
// apply it.
|
||||
if (self.screens.active.cursor.x == left_margin) return;
|
||||
|
||||
self.carriageReturn();
|
||||
try self.index();
|
||||
}
|
||||
|
||||
/// The semantic prompt type. This is used when tracking a line type and
|
||||
/// requires integration with the shell. By default, we mark a line as "none"
|
||||
/// meaning we don't know what type it is.
|
||||
@@ -1136,19 +1271,6 @@ pub const SemanticPrompt = enum {
|
||||
command,
|
||||
};
|
||||
|
||||
/// Mark the current semantic prompt information. Current escape sequences
|
||||
/// (OSC 133) only allow setting this for wherever the current active cursor
|
||||
/// is located.
|
||||
pub fn markSemanticPrompt(self: *Terminal, p: SemanticPrompt) void {
|
||||
//log.debug("semantic_prompt y={} p={}", .{ self.screens.active.cursor.y, p });
|
||||
self.screens.active.cursor.page_row.semantic_prompt = switch (p) {
|
||||
.prompt => .prompt,
|
||||
.prompt_continuation => .prompt_continuation,
|
||||
.input => .input,
|
||||
.command => .command,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if the cursor is currently at a prompt. Another way to look
|
||||
/// at this is it returns false if the shell is currently outputting something.
|
||||
/// This requires shell integration (semantic prompt integration).
|
||||
@@ -1158,29 +1280,15 @@ pub fn cursorIsAtPrompt(self: *Terminal) bool {
|
||||
// If we're on the secondary screen, we're never at a prompt.
|
||||
if (self.screens.active_key == .alternate) return false;
|
||||
|
||||
// Reverse through the active
|
||||
const start_x, const start_y = .{ self.screens.active.cursor.x, self.screens.active.cursor.y };
|
||||
defer self.screens.active.cursorAbsolute(start_x, start_y);
|
||||
// If our page row is a prompt then we're always at a prompt
|
||||
const cursor: *const Screen.Cursor = &self.screens.active.cursor;
|
||||
if (cursor.page_row.semantic_prompt != .none) return true;
|
||||
|
||||
for (0..start_y + 1) |i| {
|
||||
if (i > 0) self.screens.active.cursorUp(1);
|
||||
switch (self.screens.active.cursor.page_row.semantic_prompt) {
|
||||
// If we're at a prompt or input area, then we are at a prompt.
|
||||
.prompt,
|
||||
.prompt_continuation,
|
||||
.input,
|
||||
=> return true,
|
||||
|
||||
// If we have command output, then we're most certainly not
|
||||
// at a prompt.
|
||||
.command => return false,
|
||||
|
||||
// If we don't know, we keep searching.
|
||||
.unknown => {},
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
// Otherwise, determine our cursor state
|
||||
return switch (cursor.semantic_content) {
|
||||
.input, .prompt => true,
|
||||
.output => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Horizontal tab moves the cursor to the next tabstop, clearing
|
||||
@@ -1245,17 +1353,48 @@ pub fn tabReset(self: *Terminal) void {
|
||||
///
|
||||
/// This unsets the pending wrap state without wrapping.
|
||||
pub fn index(self: *Terminal) !void {
|
||||
const screen: *Screen = self.screens.active;
|
||||
|
||||
// Unset pending wrap state
|
||||
self.screens.active.cursor.pending_wrap = false;
|
||||
screen.cursor.pending_wrap = false;
|
||||
|
||||
// We handle our cursor semantic prompt state AFTER doing the
|
||||
// scrolling, because we may need to apply to new rows.
|
||||
defer if (screen.cursor.semantic_content != .output) {
|
||||
@branchHint(.unlikely);
|
||||
|
||||
// Always reset any semantic content clear-eol state.
|
||||
//
|
||||
// The specification is not clear what "end-of-line" means. If we
|
||||
// discover that there are more scenarios we should be unsetting
|
||||
// this we should document and test it.
|
||||
if (screen.cursor.semantic_content_clear_eol) {
|
||||
screen.cursor.semantic_content = .output;
|
||||
screen.cursor.semantic_content_clear_eol = false;
|
||||
} else {
|
||||
// If we aren't clearing our state at EOL and we're not output,
|
||||
// then we mark the new row as a prompt continuation. This is
|
||||
// to work around shells that don't send OSC 133 k=s sequences
|
||||
// for continuations.
|
||||
//
|
||||
// This can be a false positive if the shell changes content
|
||||
// type later and outputs something. We handle that in the
|
||||
// semanticPrompt function.
|
||||
screen.cursor.page_row.semantic_prompt = .prompt_continuation;
|
||||
}
|
||||
} else {
|
||||
// This should never be set in the output mode.
|
||||
assert(!screen.cursor.semantic_content_clear_eol);
|
||||
};
|
||||
|
||||
// Outside of the scroll region we move the cursor one line down.
|
||||
if (self.screens.active.cursor.y < self.scrolling_region.top or
|
||||
self.screens.active.cursor.y > self.scrolling_region.bottom)
|
||||
if (screen.cursor.y < self.scrolling_region.top or
|
||||
screen.cursor.y > self.scrolling_region.bottom)
|
||||
{
|
||||
// We only move down if we're not already at the bottom of
|
||||
// the screen.
|
||||
if (self.screens.active.cursor.y < self.rows - 1) {
|
||||
self.screens.active.cursorDown(1);
|
||||
if (screen.cursor.y < self.rows - 1) {
|
||||
screen.cursorDown(1);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -1264,13 +1403,13 @@ pub fn index(self: *Terminal) !void {
|
||||
// If the cursor is inside the scrolling region and on the bottom-most
|
||||
// line, then we scroll up. If our scrolling region is the full screen
|
||||
// we create scrollback.
|
||||
if (self.screens.active.cursor.y == self.scrolling_region.bottom and
|
||||
self.screens.active.cursor.x >= self.scrolling_region.left and
|
||||
self.screens.active.cursor.x <= self.scrolling_region.right)
|
||||
if (screen.cursor.y == self.scrolling_region.bottom and
|
||||
screen.cursor.x >= self.scrolling_region.left and
|
||||
screen.cursor.x <= self.scrolling_region.right)
|
||||
{
|
||||
if (comptime build_options.kitty_graphics) {
|
||||
// Scrolling dirties the images because it updates their placements pins.
|
||||
self.screens.active.kitty_images.dirty = true;
|
||||
screen.kitty_images.dirty = true;
|
||||
}
|
||||
|
||||
// If our scrolling region is at the top, we create scrollback.
|
||||
@@ -1278,7 +1417,7 @@ pub fn index(self: *Terminal) !void {
|
||||
self.scrolling_region.left == 0 and
|
||||
self.scrolling_region.right == self.cols - 1)
|
||||
{
|
||||
try self.screens.active.cursorScrollAbove();
|
||||
try screen.cursorScrollAbove();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1292,7 +1431,7 @@ pub fn index(self: *Terminal) !void {
|
||||
// However, scrollUp is WAY slower. We should optimize this
|
||||
// case to work in the eraseRowBounded codepath and remove
|
||||
// this check.
|
||||
!self.screens.active.blankCell().isZero())
|
||||
!screen.blankCell().isZero())
|
||||
{
|
||||
try self.scrollUp(1);
|
||||
return;
|
||||
@@ -1302,9 +1441,9 @@ pub fn index(self: *Terminal) !void {
|
||||
// scroll the contents of the scrolling region.
|
||||
|
||||
// Preserve old cursor just for assertions
|
||||
const old_cursor = self.screens.active.cursor;
|
||||
const old_cursor = screen.cursor;
|
||||
|
||||
try self.screens.active.pages.eraseRowBounded(
|
||||
try screen.pages.eraseRowBounded(
|
||||
.{ .active = .{ .y = self.scrolling_region.top } },
|
||||
self.scrolling_region.bottom - self.scrolling_region.top,
|
||||
);
|
||||
@@ -1313,26 +1452,26 @@ pub fn index(self: *Terminal) !void {
|
||||
// up by 1, so we need to move it back down. A `cursorReload`
|
||||
// would be better option but this is more efficient and this is
|
||||
// a super hot path so we do this instead.
|
||||
assert(self.screens.active.cursor.x == old_cursor.x);
|
||||
assert(self.screens.active.cursor.y == old_cursor.y);
|
||||
self.screens.active.cursor.y -= 1;
|
||||
self.screens.active.cursorDown(1);
|
||||
assert(screen.cursor.x == old_cursor.x);
|
||||
assert(screen.cursor.y == old_cursor.y);
|
||||
screen.cursor.y -= 1;
|
||||
screen.cursorDown(1);
|
||||
|
||||
// The operations above can prune our cursor style so we need to
|
||||
// update. This should never fail because the above can only FREE
|
||||
// memory.
|
||||
self.screens.active.manualStyleUpdate() catch |err| {
|
||||
screen.manualStyleUpdate() catch |err| {
|
||||
std.log.warn("deleteLines manualStyleUpdate err={}", .{err});
|
||||
self.screens.active.cursor.style = .{};
|
||||
self.screens.active.manualStyleUpdate() catch unreachable;
|
||||
screen.cursor.style = .{};
|
||||
screen.manualStyleUpdate() catch unreachable;
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Increase cursor by 1, maximum to bottom of scroll region
|
||||
if (self.screens.active.cursor.y < self.scrolling_region.bottom) {
|
||||
self.screens.active.cursorDown(1);
|
||||
if (screen.cursor.y < self.scrolling_region.bottom) {
|
||||
screen.cursorDown(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2314,15 +2453,11 @@ pub fn eraseDisplay(
|
||||
// If we're at a prompt or input area, then we are at a prompt.
|
||||
.prompt,
|
||||
.prompt_continuation,
|
||||
.input,
|
||||
=> break,
|
||||
|
||||
// If we have command output, then we're most certainly not
|
||||
// at a prompt.
|
||||
.command => break :at_prompt,
|
||||
|
||||
// If we don't know, we keep searching.
|
||||
.unknown => {},
|
||||
.none => break :at_prompt,
|
||||
}
|
||||
} else break :at_prompt;
|
||||
|
||||
@@ -2629,21 +2764,19 @@ pub fn resize(
|
||||
|
||||
// Resize primary screen, which supports reflow
|
||||
const primary = self.screens.get(.primary).?;
|
||||
if (self.screens.active_key == .primary and
|
||||
self.flags.shell_redraws_prompt)
|
||||
{
|
||||
primary.clearPrompt();
|
||||
}
|
||||
if (self.modes.get(.wraparound)) {
|
||||
try primary.resize(cols, rows);
|
||||
} else {
|
||||
try primary.resizeWithoutReflow(cols, rows);
|
||||
}
|
||||
try primary.resize(.{
|
||||
.cols = cols,
|
||||
.rows = rows,
|
||||
.reflow = self.modes.get(.wraparound),
|
||||
.prompt_redraw = self.flags.shell_redraws_prompt,
|
||||
});
|
||||
|
||||
// Alternate screen, if it exists, doesn't reflow
|
||||
if (self.screens.get(.alternate)) |alt| {
|
||||
try alt.resizeWithoutReflow(cols, rows);
|
||||
}
|
||||
if (self.screens.get(.alternate)) |alt| try alt.resize(.{
|
||||
.cols = cols,
|
||||
.rows = rows,
|
||||
.reflow = false,
|
||||
});
|
||||
|
||||
// Whenever we resize we just mark it as a screen clear
|
||||
self.flags.dirty.clear = true;
|
||||
@@ -4555,19 +4688,20 @@ test "Terminal: soft wrap with semantic prompt" {
|
||||
var t = try init(testing.allocator, .{ .cols = 3, .rows = 80 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
// Mark our prompt. Should not make anything dirty on its own.
|
||||
t.markSemanticPrompt(.prompt);
|
||||
// Mark our prompt.
|
||||
try t.semanticPrompt(.init(.prompt_start));
|
||||
// Should not make anything dirty on its own.
|
||||
try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
|
||||
|
||||
// Write and wrap
|
||||
for ("hello") |c| try t.print(c);
|
||||
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
||||
try testing.expectEqual(Row.SemanticPrompt.prompt, list_cell.row.semantic_prompt);
|
||||
try testing.expectEqual(.prompt, list_cell.row.semantic_prompt);
|
||||
}
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?;
|
||||
try testing.expectEqual(Row.SemanticPrompt.prompt, list_cell.row.semantic_prompt);
|
||||
try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11518,33 +11652,343 @@ test "Terminal: eraseDisplay complete preserves cursor" {
|
||||
try testing.expect(t.screens.active.cursor.style_id != style.default_id);
|
||||
}
|
||||
|
||||
test "Terminal: semantic prompt" {
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Prompt
|
||||
try t.semanticPrompt(.init(.fresh_line_new_prompt));
|
||||
for ("hello") |c| try t.print(c);
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 5), t.screens.active.cursor.x);
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = t.screens.active.cursor.x - 1,
|
||||
.y = t.screens.active.cursor.y,
|
||||
} }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(.prompt, cell.semantic_content);
|
||||
|
||||
const row = list_cell.row;
|
||||
try testing.expectEqual(.prompt, row.semantic_prompt);
|
||||
}
|
||||
|
||||
// Start input but end it on EOL
|
||||
try t.semanticPrompt(.init(.end_prompt_start_input_terminate_eol));
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
|
||||
// Write some output
|
||||
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
|
||||
for ("world") |c| try t.print(c);
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = t.screens.active.cursor.x - 1,
|
||||
.y = t.screens.active.cursor.y,
|
||||
} }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(.output, cell.semantic_content);
|
||||
|
||||
const row = list_cell.row;
|
||||
try testing.expectEqual(.none, row.semantic_prompt);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: semantic prompt continuations" {
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Prompt
|
||||
try t.semanticPrompt(.init(.fresh_line_new_prompt));
|
||||
for ("hello") |c| try t.print(c);
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 5), t.screens.active.cursor.x);
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = t.screens.active.cursor.x - 1,
|
||||
.y = t.screens.active.cursor.y,
|
||||
} }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(.prompt, cell.semantic_content);
|
||||
|
||||
const row = list_cell.row;
|
||||
try testing.expectEqual(.prompt, row.semantic_prompt);
|
||||
}
|
||||
|
||||
// Start input but end it on EOL
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.semanticPrompt(.{
|
||||
.action = .prompt_start,
|
||||
.options_unvalidated = "k=c",
|
||||
});
|
||||
|
||||
// Write some output
|
||||
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
|
||||
for ("world") |c| try t.print(c);
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = t.screens.active.cursor.x - 1,
|
||||
.y = t.screens.active.cursor.y,
|
||||
} }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(.prompt, cell.semantic_content);
|
||||
|
||||
const row = list_cell.row;
|
||||
try testing.expectEqual(.prompt_continuation, row.semantic_prompt);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: index in prompt mode marks new row as prompt continuation" {
|
||||
// This tests the Fish shell workaround: when in prompt mode and we get
|
||||
// a newline, assume the new row is a prompt continuation (since Fish
|
||||
// doesn't emit OSC133 k=s markers for continuation lines).
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Start a prompt
|
||||
try t.semanticPrompt(.init(.prompt_start));
|
||||
for ("hello") |c| try t.print(c);
|
||||
|
||||
// Verify first row is marked as prompt
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
} }).?;
|
||||
try testing.expectEqual(.prompt, list_cell.row.semantic_prompt);
|
||||
}
|
||||
|
||||
// Now do a linefeed while still in prompt mode
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
|
||||
// The new row should automatically be marked as prompt continuation
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 1,
|
||||
} }).?;
|
||||
try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt);
|
||||
}
|
||||
|
||||
// The cursor semantic content should still be prompt
|
||||
try testing.expectEqual(.prompt, t.screens.active.cursor.semantic_content);
|
||||
}
|
||||
|
||||
test "Terminal: index in input mode does not mark new row as prompt" {
|
||||
// Input mode should NOT trigger prompt continuation on newline
|
||||
// (only prompt mode does, not input mode)
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Start a prompt then switch to input
|
||||
try t.semanticPrompt(.init(.prompt_start));
|
||||
for ("$ ") |c| try t.print(c);
|
||||
try t.semanticPrompt(.init(.end_prompt_start_input));
|
||||
for ("echo \\") |c| try t.print(c);
|
||||
|
||||
// Linefeed while in input mode
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
|
||||
// The new row should be marked as prompt continuation
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 1,
|
||||
} }).?;
|
||||
try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt);
|
||||
}
|
||||
|
||||
// Our cursor should still be in input
|
||||
try testing.expectEqual(.input, t.screens.active.cursor.semantic_content);
|
||||
}
|
||||
|
||||
test "Terminal: index in output mode does not mark new row as prompt" {
|
||||
// Output mode should NOT trigger prompt continuation
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Complete prompt cycle: prompt -> input -> output
|
||||
try t.semanticPrompt(.init(.prompt_start));
|
||||
for ("$ ") |c| try t.print(c);
|
||||
try t.semanticPrompt(.init(.end_prompt_start_input));
|
||||
for ("ls") |c| try t.print(c);
|
||||
try t.semanticPrompt(.init(.end_input_start_output));
|
||||
|
||||
// Linefeed while in output mode
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
|
||||
// The new row should NOT be marked as a prompt
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 1,
|
||||
} }).?;
|
||||
try testing.expectEqual(.none, list_cell.row.semantic_prompt);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: OSC133C at x=0 on prompt row clears prompt mark" {
|
||||
// This tests the second Fish heuristic: when Fish emits a newline
|
||||
// then immediately sends OSC133C (start output) at column 0, we
|
||||
// should clear the prompt continuation mark we just set.
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Start a prompt
|
||||
try t.semanticPrompt(.init(.prompt_start));
|
||||
for ("$ echo \\") |c| try t.print(c);
|
||||
|
||||
// Simulate Fish behavior: newline first (which marks next row as prompt)
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
|
||||
// Verify the new row is marked as prompt continuation
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 1,
|
||||
} }).?;
|
||||
try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt);
|
||||
}
|
||||
|
||||
// Now Fish sends OSC133C at column 0 (cursor is still at x=0)
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
|
||||
try t.semanticPrompt(.init(.end_input_start_output));
|
||||
|
||||
// The prompt continuation should be cleared
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 1,
|
||||
} }).?;
|
||||
try testing.expectEqual(.none, list_cell.row.semantic_prompt);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: OSC133C at x>0 on prompt row does not clear prompt mark" {
|
||||
// If we're not at column 0, we shouldn't clear the prompt mark
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Start a prompt on a row
|
||||
try t.semanticPrompt(.init(.prompt_start));
|
||||
for ("$ ") |c| try t.print(c);
|
||||
|
||||
// Move to a new line and mark it as prompt continuation manually
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.semanticPrompt(.{
|
||||
.action = .prompt_start,
|
||||
.options_unvalidated = "k=c",
|
||||
});
|
||||
for ("> ") |c| try t.print(c);
|
||||
|
||||
// Verify the row is marked as prompt continuation
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 1,
|
||||
} }).?;
|
||||
try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt);
|
||||
}
|
||||
|
||||
// Now send OSC133C but cursor is NOT at column 0
|
||||
try testing.expect(t.screens.active.cursor.x > 0);
|
||||
try t.semanticPrompt(.init(.end_input_start_output));
|
||||
|
||||
// The prompt continuation should NOT be cleared (we're not at x=0)
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 1,
|
||||
} }).?;
|
||||
try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: multiple newlines in prompt mode marks all rows" {
|
||||
// Multiple newlines should each mark their row as prompt continuation
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Start a prompt
|
||||
try t.semanticPrompt(.init(.prompt_start));
|
||||
for ("line1") |c| try t.print(c);
|
||||
|
||||
// Multiple newlines
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
for ("line2") |c| try t.print(c);
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
for ("line3") |c| try t.print(c);
|
||||
|
||||
// First row should be prompt
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
} }).?;
|
||||
try testing.expectEqual(.prompt, list_cell.row.semantic_prompt);
|
||||
}
|
||||
|
||||
// Second and third rows should be prompt continuation
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 1,
|
||||
} }).?;
|
||||
try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt);
|
||||
}
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 2,
|
||||
} }).?;
|
||||
try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: cursorIsAtPrompt" {
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 3, .rows = 2 });
|
||||
var t = try init(alloc, .{ .cols = 10, .rows = 3 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
try testing.expect(!t.cursorIsAtPrompt());
|
||||
t.markSemanticPrompt(.prompt);
|
||||
try t.semanticPrompt(.init(.prompt_start));
|
||||
try testing.expect(t.cursorIsAtPrompt());
|
||||
for ("$ ") |c| try t.print(c);
|
||||
|
||||
// Input is also a prompt
|
||||
t.markSemanticPrompt(.input);
|
||||
try testing.expect(t.cursorIsAtPrompt());
|
||||
|
||||
// Newline -- we expect we're still at a prompt if we received
|
||||
// prompt stuff before.
|
||||
try t.linefeed();
|
||||
try t.semanticPrompt(.init(.end_prompt_start_input));
|
||||
try testing.expect(t.cursorIsAtPrompt());
|
||||
for ("ls") |c| try t.print(c);
|
||||
|
||||
// But once we say we're starting output, we're not a prompt
|
||||
t.markSemanticPrompt(.command);
|
||||
try testing.expect(!t.cursorIsAtPrompt());
|
||||
// (cursor is not at x=0, so the Fish heuristic doesn't trigger)
|
||||
try t.semanticPrompt(.init(.end_input_start_output));
|
||||
// Still a prompt because this line has a prompt
|
||||
try testing.expect(t.cursorIsAtPrompt());
|
||||
try t.linefeed();
|
||||
try testing.expect(!t.cursorIsAtPrompt());
|
||||
|
||||
// Until we know we're at a prompt again
|
||||
try t.linefeed();
|
||||
t.markSemanticPrompt(.prompt);
|
||||
try t.semanticPrompt(.init(.prompt_start));
|
||||
try testing.expect(t.cursorIsAtPrompt());
|
||||
}
|
||||
|
||||
@@ -11554,13 +11998,13 @@ test "Terminal: cursorIsAtPrompt alternate screen" {
|
||||
defer t.deinit(alloc);
|
||||
|
||||
try testing.expect(!t.cursorIsAtPrompt());
|
||||
t.markSemanticPrompt(.prompt);
|
||||
try t.semanticPrompt(.init(.prompt_start));
|
||||
try testing.expect(t.cursorIsAtPrompt());
|
||||
|
||||
// Secondary screen is never a prompt
|
||||
try t.switchScreenMode(.@"1049", true);
|
||||
try testing.expect(!t.cursorIsAtPrompt());
|
||||
t.markSemanticPrompt(.prompt);
|
||||
try t.semanticPrompt(.init(.prompt_start));
|
||||
try testing.expect(!t.cursorIsAtPrompt());
|
||||
}
|
||||
|
||||
@@ -11570,6 +12014,7 @@ test "Terminal: fullReset with a non-empty pen" {
|
||||
|
||||
try t.setAttribute(.{ .direct_color_fg = .{ .r = 0xFF, .g = 0, .b = 0x7F } });
|
||||
try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0x7F } });
|
||||
t.screens.active.cursor.semantic_content = .input;
|
||||
t.fullReset();
|
||||
|
||||
{
|
||||
@@ -11582,6 +12027,7 @@ test "Terminal: fullReset with a non-empty pen" {
|
||||
}
|
||||
|
||||
try testing.expectEqual(@as(style.Id, 0), t.screens.active.cursor.style_id);
|
||||
try testing.expectEqual(.output, t.screens.active.cursor.semantic_content);
|
||||
}
|
||||
|
||||
test "Terminal: fullReset hyperlink" {
|
||||
|
||||
@@ -147,6 +147,20 @@ pub fn BitmapAllocator(comptime chunk_size: comptime_int) type {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the total capacity in bytes.
|
||||
pub fn capacityBytes(self: Self) usize {
|
||||
return self.bitmap_count * bitmap_bit_size * chunk_size;
|
||||
}
|
||||
|
||||
/// Returns the number of bytes currently in use.
|
||||
pub fn usedBytes(self: Self, base: anytype) usize {
|
||||
const bitmaps = self.bitmap.ptr(base);
|
||||
var free_chunks: usize = 0;
|
||||
for (bitmaps[0..self.bitmap_count]) |bitmap| free_chunks += @popCount(bitmap);
|
||||
const total_chunks = self.bitmap_count * bitmap_bit_size;
|
||||
return (total_chunks - free_chunks) * chunk_size;
|
||||
}
|
||||
|
||||
/// For testing only.
|
||||
fn isAllocated(self: *Self, base: anytype, slice: anytype) bool {
|
||||
comptime assert(@import("builtin").is_test);
|
||||
|
||||
@@ -17,6 +17,7 @@ const parsers = @import("osc/parsers.zig");
|
||||
const encoding = @import("osc/encoding.zig");
|
||||
|
||||
pub const color = parsers.color;
|
||||
pub const semantic_prompt = parsers.semantic_prompt;
|
||||
|
||||
const log = std.log.scoped(.osc);
|
||||
|
||||
|
||||
@@ -47,11 +47,10 @@ pub const Option = enum {
|
||||
cl,
|
||||
prompt_kind,
|
||||
err,
|
||||
|
||||
// https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||
// Kitty supports a "redraw" option for prompt_start. I can't find
|
||||
// this documented anywhere but can see in the code that this is used
|
||||
// by shell environments to tell the terminal that the shell will NOT
|
||||
// redraw the prompt so we should attempt to resize it.
|
||||
// Kitty supports a "redraw" option for prompt_start. This is extended
|
||||
// by Ghostty with the "last" option. See Redraw the type for more details.
|
||||
redraw,
|
||||
|
||||
// Use a special key instead of arrow keys to move the cursor on
|
||||
@@ -81,7 +80,7 @@ pub const Option = enum {
|
||||
.cl => Click,
|
||||
.prompt_kind => PromptKind,
|
||||
.err => []const u8,
|
||||
.redraw => bool,
|
||||
.redraw => Redraw,
|
||||
.special_key => bool,
|
||||
.click_events => bool,
|
||||
.exit_code => i32,
|
||||
@@ -169,7 +168,15 @@ pub const Option = enum {
|
||||
.cl => std.meta.stringToEnum(Click, value),
|
||||
.prompt_kind => if (value.len == 1) PromptKind.init(value[0]) else null,
|
||||
.err => value,
|
||||
.redraw, .special_key, .click_events => if (value.len == 1) switch (value[0]) {
|
||||
.redraw => if (std.mem.eql(u8, value, "0"))
|
||||
.false
|
||||
else if (std.mem.eql(u8, value, "1"))
|
||||
.true
|
||||
else if (std.mem.eql(u8, value, "last"))
|
||||
.last
|
||||
else
|
||||
null,
|
||||
.special_key, .click_events => if (value.len == 1) switch (value[0]) {
|
||||
'0' => false,
|
||||
'1' => true,
|
||||
else => null,
|
||||
@@ -208,6 +215,29 @@ pub const PromptKind = enum {
|
||||
}
|
||||
};
|
||||
|
||||
/// The values for the `redraw` extension to OSC133. This was
|
||||
/// started by Kitty[1] and extended by Ghostty (the "last" option).
|
||||
///
|
||||
/// [1]: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||
pub const Redraw = enum(u2) {
|
||||
/// The shell supports redrawing the full prompt and all continuations.
|
||||
/// This is the default value, it does not need to be explicitly set
|
||||
/// unless it is to reset a prior other value.
|
||||
true,
|
||||
|
||||
/// The shell does NOT support redrawing. In this case, Ghostty will NOT
|
||||
/// clear any prompt lines on resize.
|
||||
false,
|
||||
|
||||
/// The shell supports redrawing only the LAST line of the prompt.
|
||||
/// Ghostty will only clear the last line of the prompt on resize.
|
||||
///
|
||||
/// This is specifically introduced because Bash only redraws the last
|
||||
/// line. It is literally the only shell that does this and it does this
|
||||
/// because its bad and they should feel bad. Don't be like Bash.
|
||||
last,
|
||||
};
|
||||
|
||||
/// Parse OSC 133, semantic prompts
|
||||
pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand {
|
||||
const writer = parser.writer orelse {
|
||||
@@ -513,7 +543,7 @@ test "OSC 133: fresh_line_new_prompt with redraw=0" {
|
||||
const cmd = p.end(null).?.*;
|
||||
try testing.expect(cmd == .semantic_prompt);
|
||||
try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
|
||||
try testing.expect(cmd.semantic_prompt.readOption(.redraw).? == false);
|
||||
try testing.expect(cmd.semantic_prompt.readOption(.redraw).? == .false);
|
||||
}
|
||||
|
||||
test "OSC 133: fresh_line_new_prompt with redraw=1" {
|
||||
@@ -527,7 +557,7 @@ test "OSC 133: fresh_line_new_prompt with redraw=1" {
|
||||
const cmd = p.end(null).?.*;
|
||||
try testing.expect(cmd == .semantic_prompt);
|
||||
try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
|
||||
try testing.expect(cmd.semantic_prompt.readOption(.redraw).? == true);
|
||||
try testing.expect(cmd.semantic_prompt.readOption(.redraw).? == .true);
|
||||
}
|
||||
|
||||
test "OSC 133: fresh_line_new_prompt with invalid redraw" {
|
||||
@@ -870,8 +900,9 @@ test "Option.read err" {
|
||||
|
||||
test "Option.read redraw" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(Option.redraw.read("redraw=1").? == true);
|
||||
try testing.expect(Option.redraw.read("redraw=0").? == false);
|
||||
try testing.expect(Option.redraw.read("redraw=1").? == .true);
|
||||
try testing.expect(Option.redraw.read("redraw=0").? == .false);
|
||||
try testing.expect(Option.redraw.read("redraw=last").? == .last);
|
||||
try testing.expect(Option.redraw.read("redraw=2") == null);
|
||||
try testing.expect(Option.redraw.read("redraw=10") == null);
|
||||
try testing.expect(Option.redraw.read("redraw=") == null);
|
||||
|
||||
@@ -1898,9 +1898,16 @@ pub const Row = packed struct(u64) {
|
||||
/// false negatives. This is used to optimize hyperlink operations.
|
||||
hyperlink: bool = false,
|
||||
|
||||
/// The semantic prompt type for this row as specified by the
|
||||
/// running program, or "unknown" if it was never set.
|
||||
semantic_prompt: SemanticPrompt = .unknown,
|
||||
/// The semantic prompt state for this row.
|
||||
///
|
||||
/// This is ONLY meant to note if there are ANY cells in this
|
||||
/// row that are part of a prompt. This is an optimization for more
|
||||
/// efficiently implementing jump-to-prompt operations.
|
||||
///
|
||||
/// This may contain false positives but never false negatives. If
|
||||
/// this is set, you should still check individual cells to see if they
|
||||
/// have prompt semantics.
|
||||
semantic_prompt: SemanticPrompt = .none,
|
||||
|
||||
/// True if this row contains a virtual placeholder for the Kitty
|
||||
/// graphics protocol. (U+10EEEE)
|
||||
@@ -1922,29 +1929,22 @@ pub const Row = packed struct(u64) {
|
||||
/// screen.
|
||||
dirty: bool = false,
|
||||
|
||||
_padding: u22 = 0,
|
||||
_padding: u23 = 0,
|
||||
|
||||
/// Semantic prompt type.
|
||||
pub const SemanticPrompt = enum(u3) {
|
||||
/// Unknown, the running application didn't tell us for this line.
|
||||
unknown = 0,
|
||||
|
||||
/// This is a prompt line, meaning it only contains the shell prompt.
|
||||
/// For poorly behaving shells, this may also be the input.
|
||||
/// The semantic prompt state of the row. See `semantic_prompt`.
|
||||
pub const SemanticPrompt = enum(u2) {
|
||||
/// No prompt cells in this row.
|
||||
none = 0,
|
||||
/// Prompt cells exist in this row and this is a primary prompt
|
||||
/// line. A primary prompt line is one that is not a continuation
|
||||
/// and is the beginning of a prompt.
|
||||
prompt = 1,
|
||||
/// Prompt cells exist in this row that had k=c set (continuation)
|
||||
/// line. This is used as a way to detect when a line should
|
||||
/// be considered part of some prior prompt. If no prior prompt
|
||||
/// is found, the last (most historical) prompt continuation line is
|
||||
/// considered the prompt.
|
||||
prompt_continuation = 2,
|
||||
|
||||
/// This line contains the input area. We don't currently track
|
||||
/// where this actually is in the line, so we just assume it is somewhere.
|
||||
input = 3,
|
||||
|
||||
/// This line is the start of command output.
|
||||
command = 4,
|
||||
|
||||
/// True if this is a prompt or input line.
|
||||
pub fn promptOrInput(self: SemanticPrompt) bool {
|
||||
return self == .prompt or self == .prompt_continuation or self == .input;
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns true if this row has any managed memory outside of the
|
||||
@@ -1994,7 +1994,12 @@ pub const Cell = packed struct(u64) {
|
||||
/// the hyperlink_set to get the actual hyperlink data.
|
||||
hyperlink: bool = false,
|
||||
|
||||
_padding: u18 = 0,
|
||||
/// The semantic type of the content of this cell. This is used
|
||||
/// by the semantic prompt (OSC 133) set of sequences to understand
|
||||
/// boundary points for content.
|
||||
semantic_content: SemanticContent = .output,
|
||||
|
||||
_padding: u16 = 0,
|
||||
|
||||
pub const ContentTag = enum(u2) {
|
||||
/// A single codepoint, could be zero to be empty cell.
|
||||
@@ -2033,6 +2038,19 @@ pub const Cell = packed struct(u64) {
|
||||
spacer_head = 3,
|
||||
};
|
||||
|
||||
pub const SemanticContent = enum(u2) {
|
||||
/// Regular output content, such as command output.
|
||||
output = 0,
|
||||
|
||||
/// Content that is part of user input, such as the command
|
||||
/// to execute at a prompt.
|
||||
input = 1,
|
||||
|
||||
/// Content that is part of prompt emitted by the interactive
|
||||
/// application, such as "user@host >"
|
||||
prompt = 2,
|
||||
};
|
||||
|
||||
/// Helper to make a cell that just has a codepoint.
|
||||
pub fn init(cp: u21) Cell {
|
||||
// We have to use this bitCast here to ensure that our memory is
|
||||
@@ -2166,6 +2184,10 @@ test "Cell is zero by default" {
|
||||
const cell = Cell.init(0);
|
||||
const cell_int: u64 = @bitCast(cell);
|
||||
try std.testing.expectEqual(@as(u64, 0), cell_int);
|
||||
|
||||
// The zero value should be output type for semantic content.
|
||||
// This is very important for our assumptions elsewhere.
|
||||
try std.testing.expectEqual(Cell.SemanticContent.output, cell.semantic_content);
|
||||
}
|
||||
|
||||
test "Page capacity adjust cols down" {
|
||||
|
||||
@@ -707,19 +707,21 @@ pub fn Stream(comptime Handler: type) type {
|
||||
const action = action_opt orelse continue;
|
||||
if (comptime debug) log.info("action: {f}", .{action});
|
||||
|
||||
// If this handler handles everything manually then we do nothing
|
||||
// if it can be processed.
|
||||
if (@hasDecl(T, "handleManually")) {
|
||||
const processed = self.handler.handleManually(action) catch |err| err: {
|
||||
// A handler can expose this to get the raw action before
|
||||
// it is further parsed. If this returns `true` then we skip
|
||||
// processing ourselves.
|
||||
if (@hasDecl(T, "vtRaw")) {
|
||||
const skip = self.handler.vtRaw(action) catch |err| err: {
|
||||
log.warn("error handling action manually err={} action={f}", .{
|
||||
err,
|
||||
action,
|
||||
});
|
||||
|
||||
break :err false;
|
||||
// Always skip erroneous actions because we can't
|
||||
// be sure...
|
||||
break :err true;
|
||||
};
|
||||
|
||||
if (processed) continue;
|
||||
if (skip) continue;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
|
||||
@@ -153,7 +153,7 @@ pub const Handler = struct {
|
||||
.full_reset => self.terminal.fullReset(),
|
||||
.start_hyperlink => try self.terminal.screens.active.startHyperlink(value.uri, value.id),
|
||||
.end_hyperlink => self.terminal.screens.active.endHyperlink(),
|
||||
.semantic_prompt => self.semanticPrompt(value),
|
||||
.semantic_prompt => try self.terminal.semanticPrompt(value),
|
||||
.mouse_shape => self.terminal.mouse_shape = value,
|
||||
.color_operation => try self.colorOperation(value.op, &value.requests),
|
||||
.kitty_color_report => try self.kittyColorOperation(value),
|
||||
@@ -209,42 +209,6 @@ pub const Handler = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn semanticPrompt(
|
||||
self: *Handler,
|
||||
cmd: Action.SemanticPrompt,
|
||||
) void {
|
||||
switch (cmd.action) {
|
||||
.fresh_line_new_prompt => {
|
||||
const kind = cmd.readOption(.prompt_kind) orelse .initial;
|
||||
switch (kind) {
|
||||
.initial, .right => {
|
||||
self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt;
|
||||
if (cmd.readOption(.redraw)) |redraw| {
|
||||
self.terminal.flags.shell_redraws_prompt = redraw;
|
||||
}
|
||||
},
|
||||
.continuation, .secondary => {
|
||||
self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt_continuation;
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
.end_prompt_start_input => self.terminal.markSemanticPrompt(.input),
|
||||
.end_input_start_output => self.terminal.markSemanticPrompt(.command),
|
||||
.end_command => self.terminal.screens.active.cursor.page_row.semantic_prompt = .input,
|
||||
|
||||
// All of these commands weren't previously handled by our
|
||||
// semantic prompt code. I am PR-ing the parser separate from the
|
||||
// handling so we just ignore these like we did before, even
|
||||
// though we should handle them eventually.
|
||||
.end_prompt_start_input_terminate_eol,
|
||||
.fresh_line,
|
||||
.new_command,
|
||||
.prompt_start,
|
||||
=> {},
|
||||
}
|
||||
}
|
||||
|
||||
fn setMode(self: *Handler, mode: modes.Mode, enabled: bool) !void {
|
||||
// Set the mode on the terminal
|
||||
self.terminal.modes.set(mode, enabled);
|
||||
@@ -905,3 +869,122 @@ test "palette dirty flag set on color change" {
|
||||
try s.nextSlice("\x1b]21;1=rgb:00/ff/00\x1b\\");
|
||||
try testing.expect(t.flags.dirty.palette);
|
||||
}
|
||||
|
||||
test "semantic prompt fresh line" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
try s.nextSlice("Hello");
|
||||
try s.nextSlice("\x1b]133;L\x07");
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
|
||||
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y);
|
||||
}
|
||||
|
||||
test "semantic prompt fresh line new prompt" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Write some text and then send OSC 133;A (fresh_line_new_prompt)
|
||||
try s.nextSlice("Hello");
|
||||
try s.nextSlice("\x1b]133;A\x07");
|
||||
|
||||
// Should do a fresh line (carriage return + index)
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
|
||||
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y);
|
||||
|
||||
// Should set cursor semantic_content to prompt
|
||||
try testing.expectEqual(.prompt, t.screens.active.cursor.semantic_content);
|
||||
|
||||
// Test with redraw option
|
||||
try s.nextSlice("prompt$ ");
|
||||
try s.nextSlice("\x1b]133;A;redraw=1\x07");
|
||||
try testing.expect(t.flags.shell_redraws_prompt == .true);
|
||||
}
|
||||
|
||||
test "semantic prompt end of input, then start output" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Write some text and then send OSC 133;A (fresh_line_new_prompt)
|
||||
try s.nextSlice("Hello");
|
||||
try s.nextSlice("\x1b]133;A\x07");
|
||||
try s.nextSlice("prompt$ ");
|
||||
try s.nextSlice("\x1b]133;B\x07");
|
||||
try testing.expectEqual(.input, t.screens.active.cursor.semantic_content);
|
||||
try s.nextSlice("\x1b]133;C\x07");
|
||||
try testing.expectEqual(.output, t.screens.active.cursor.semantic_content);
|
||||
}
|
||||
|
||||
test "semantic prompt prompt_start" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Write some text
|
||||
try s.nextSlice("Hello");
|
||||
|
||||
// OSC 133;P marks the start of a prompt (without fresh line behavior)
|
||||
try s.nextSlice("\x1b]133;P\x07");
|
||||
try testing.expectEqual(.prompt, t.screens.active.cursor.semantic_content);
|
||||
try testing.expectEqual(@as(usize, 5), t.screens.active.cursor.x);
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
|
||||
}
|
||||
|
||||
test "semantic prompt new_command" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Write some text
|
||||
try s.nextSlice("Hello");
|
||||
try s.nextSlice("\x1b]133;N\x07");
|
||||
|
||||
// Should behave like fresh_line_new_prompt - cursor moves to column 0
|
||||
// on next line since we had content
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
|
||||
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y);
|
||||
try testing.expectEqual(.prompt, t.screens.active.cursor.semantic_content);
|
||||
}
|
||||
|
||||
test "semantic prompt new_command at column zero" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// OSC 133;N when already at column 0 should stay on same line
|
||||
try s.nextSlice("\x1b]133;N\x07");
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
|
||||
try testing.expectEqual(.prompt, t.screens.active.cursor.semantic_content);
|
||||
}
|
||||
|
||||
test "semantic prompt end_prompt_start_input_terminate_eol clears on linefeed" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Set input terminated by EOL
|
||||
try s.nextSlice("\x1b]133;I\x07");
|
||||
try testing.expectEqual(.input, t.screens.active.cursor.semantic_content);
|
||||
|
||||
// Linefeed should reset semantic content to output
|
||||
try s.nextSlice("\n");
|
||||
try testing.expectEqual(.output, t.screens.active.cursor.semantic_content);
|
||||
}
|
||||
|
||||
@@ -610,8 +610,9 @@ pub fn clearScreen(self: *Termio, td: *ThreadData, history: bool) !void {
|
||||
// send a FF (0x0C) to the shell so that it can repaint the screen.
|
||||
// Mark the current row as a not a prompt so we can properly
|
||||
// clear the full screen in the next eraseDisplay call.
|
||||
self.terminal.markSemanticPrompt(.command);
|
||||
assert(!self.terminal.cursorIsAtPrompt());
|
||||
// TODO: fix this
|
||||
// self.terminal.markSemanticPrompt(.command);
|
||||
// assert(!self.terminal.cursorIsAtPrompt());
|
||||
self.terminal.eraseDisplay(.complete, false);
|
||||
}
|
||||
|
||||
@@ -694,7 +695,11 @@ fn processOutputLocked(self: *Termio, buf: []const u8) void {
|
||||
// below but at least users only pay for it if they're using the inspector.
|
||||
if (self.renderer_state.inspector) |insp| {
|
||||
for (buf, 0..) |byte, i| {
|
||||
insp.recordPtyRead(buf[i .. i + 1]) catch |err| {
|
||||
insp.recordPtyRead(
|
||||
self.alloc,
|
||||
&self.terminal,
|
||||
buf[i .. i + 1],
|
||||
) catch |err| {
|
||||
log.err("error recording pty read in inspector err={}", .{err});
|
||||
};
|
||||
|
||||
|
||||
@@ -320,7 +320,7 @@ pub const StreamHandler = struct {
|
||||
.progress_report => self.progressReport(value),
|
||||
.start_hyperlink => try self.startHyperlink(value.uri, value.id),
|
||||
.clipboard_contents => try self.clipboardContents(value.kind, value.data),
|
||||
.semantic_prompt => self.semanticPrompt(value),
|
||||
.semantic_prompt => try self.semanticPrompt(value),
|
||||
.mouse_shape => try self.setMouseShape(value),
|
||||
.configure_charset => self.configureCharset(value.slot, value.charset),
|
||||
.set_attribute => {
|
||||
@@ -1069,28 +1069,12 @@ pub const StreamHandler = struct {
|
||||
fn semanticPrompt(
|
||||
self: *StreamHandler,
|
||||
cmd: Stream.Action.SemanticPrompt,
|
||||
) void {
|
||||
) !void {
|
||||
switch (cmd.action) {
|
||||
.fresh_line_new_prompt => {
|
||||
const kind = cmd.readOption(.prompt_kind) orelse .initial;
|
||||
switch (kind) {
|
||||
.initial, .right => {
|
||||
self.terminal.markSemanticPrompt(.prompt);
|
||||
if (cmd.readOption(.redraw)) |redraw| {
|
||||
self.terminal.flags.shell_redraws_prompt = redraw;
|
||||
}
|
||||
},
|
||||
.continuation, .secondary => {
|
||||
self.terminal.markSemanticPrompt(.prompt_continuation);
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
.end_prompt_start_input => self.terminal.markSemanticPrompt(.input),
|
||||
.end_input_start_output => {
|
||||
self.terminal.markSemanticPrompt(.command);
|
||||
self.surfaceMessageWriter(.start_command);
|
||||
},
|
||||
|
||||
.end_command => {
|
||||
// The specification seems to not specify the type but
|
||||
// other terminals accept 32-bits, but exit codes are really
|
||||
@@ -1103,16 +1087,19 @@ pub const StreamHandler = struct {
|
||||
self.surfaceMessageWriter(.{ .stop_command = code });
|
||||
},
|
||||
|
||||
// All of these commands weren't previously handled by our
|
||||
// semantic prompt code. I am PR-ing the parser separate from the
|
||||
// handling so we just ignore these like we did before, even
|
||||
// though we should handle them eventually.
|
||||
// Handled by Terminal, no special handling by us
|
||||
.end_prompt_start_input,
|
||||
.end_prompt_start_input_terminate_eol,
|
||||
.fresh_line,
|
||||
.fresh_line_new_prompt,
|
||||
.new_command,
|
||||
.prompt_start,
|
||||
=> {},
|
||||
}
|
||||
|
||||
// We do this last so failures are still processed correctly
|
||||
// above.
|
||||
try self.terminal.semanticPrompt(cmd);
|
||||
}
|
||||
|
||||
fn reportPwd(self: *StreamHandler, url: []const u8) !void {
|
||||
|
||||
Reference in New Issue
Block a user