mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-17 21:12:39 +00:00
Merge branch 'main' into localize-nautilus-script
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
root = true
|
||||
|
||||
[*.{sh,bash,elv}]
|
||||
[*.{sh,bash,elv,nu}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
|
||||
4
.github/scripts/check-translations.sh
vendored
4
.github/scripts/check-translations.sh
vendored
@@ -1,4 +1,6 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
old_pot=$(mktemp)
|
||||
cp po/com.mitchellh.ghostty.pot "$old_pot"
|
||||
|
||||
4
.github/workflows/milestone.yml
vendored
4
.github/workflows/milestone.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
name: Milestone Update
|
||||
steps:
|
||||
- name: Set Milestone for PR
|
||||
uses: hustcer/milestone-action@dcd6c3742acc1846929c054251c64cccd555a00d # v3.0
|
||||
uses: hustcer/milestone-action@ebed8d5daafd855a600d7e665c1b130f06d24130 # v3.1
|
||||
if: github.event.pull_request.merged == true
|
||||
with:
|
||||
action: bind-pr # `bind-pr` is the default action
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
# Bind milestone to closed issue that has a merged PR fix
|
||||
- name: Set Milestone for Issue
|
||||
uses: hustcer/milestone-action@dcd6c3742acc1846929c054251c64cccd555a00d # v3.0
|
||||
uses: hustcer/milestone-action@ebed8d5daafd855a600d7e665c1b130f06d24130 # v3.1
|
||||
if: github.event.issue.state == 'closed'
|
||||
with:
|
||||
action: bind-issue
|
||||
|
||||
6
.github/workflows/nix.yml
vendored
6
.github/workflows/nix.yml
vendored
@@ -39,9 +39,9 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
14
.github/workflows/release-tag.yml
vendored
14
.github/workflows/release-tag.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
@@ -80,10 +80,10 @@ jobs:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -132,12 +132,12 @@ jobs:
|
||||
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -306,7 +306,7 @@ jobs:
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Download macOS Artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
|
||||
55
.github/workflows/release-tip.yml
vendored
55
.github/workflows/release-tip.yml
vendored
@@ -4,7 +4,11 @@ on:
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
|
||||
workflow_dispatch: {}
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr:
|
||||
type: number
|
||||
required: false
|
||||
|
||||
name: Release Tip
|
||||
|
||||
@@ -29,14 +33,14 @@ jobs:
|
||||
commit: ${{ steps.extract_build_info.outputs.commit }}
|
||||
commit_long: ${{ steps.extract_build_info.outputs.commit_long }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -66,7 +70,7 @@ jobs:
|
||||
needs: [setup, build-macos]
|
||||
if: needs.setup.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Tip Tag
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
@@ -81,7 +85,7 @@ jobs:
|
||||
env:
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install sentry-cli
|
||||
run: |
|
||||
@@ -104,7 +108,7 @@ jobs:
|
||||
env:
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install sentry-cli
|
||||
run: |
|
||||
@@ -127,7 +131,7 @@ jobs:
|
||||
env:
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install sentry-cli
|
||||
run: |
|
||||
@@ -159,9 +163,9 @@ jobs:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -169,7 +173,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -217,7 +221,7 @@ jobs:
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
@@ -226,7 +230,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -424,12 +428,21 @@ jobs:
|
||||
source-dir: blob
|
||||
destination-dir: ./
|
||||
|
||||
- name: Echo Release URLs
|
||||
- name: Show and Save Release URLs
|
||||
run: |
|
||||
echo "Release URLs:"
|
||||
echo " App Bundle: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal.zip"
|
||||
echo " Debug Symbols: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip"
|
||||
echo " DMG: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/Ghostty.dmg"
|
||||
cat << EOF | tee release-urls.txt
|
||||
Release URLs:
|
||||
App Bundle: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal.zip
|
||||
Debug Symbols: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip
|
||||
DMG: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/Ghostty.dmg
|
||||
EOF
|
||||
|
||||
- name: Upload Release URLs
|
||||
uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 # v6.0
|
||||
with:
|
||||
name: release-urls-${{ inputs.pr || '0' }}
|
||||
path: release-urls.txt
|
||||
retention-days: 2
|
||||
|
||||
build-macos-debug-slow:
|
||||
needs: [setup]
|
||||
@@ -451,7 +464,7 @@ jobs:
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
@@ -460,7 +473,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -635,7 +648,7 @@ jobs:
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
@@ -644,7 +657,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
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@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
|
||||
172
.github/workflows/test.yml
vendored
172
.github/workflows/test.yml
vendored
@@ -74,10 +74,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -117,10 +117,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -150,10 +150,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -184,10 +184,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -228,10 +228,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -241,7 +241,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -264,10 +264,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -277,7 +277,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -293,10 +293,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -306,7 +306,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -326,10 +326,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -339,7 +339,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -372,10 +372,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -385,7 +385,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -410,7 +410,7 @@ jobs:
|
||||
needs: [build-dist, build-snap]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Trigger Snap workflow
|
||||
run: |
|
||||
@@ -428,7 +428,7 @@ jobs:
|
||||
needs: [build-dist, build-flatpak]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Trigger Flatpak workflow
|
||||
run: |
|
||||
@@ -445,13 +445,13 @@ jobs:
|
||||
needs: test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -488,13 +488,13 @@ jobs:
|
||||
needs: test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -511,11 +511,11 @@ jobs:
|
||||
|
||||
- name: Test All
|
||||
run: |
|
||||
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=freetype
|
||||
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext_freetype
|
||||
|
||||
- name: Build All
|
||||
run: |
|
||||
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=freetype
|
||||
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_freetype
|
||||
|
||||
build-windows:
|
||||
runs-on: windows-2022
|
||||
@@ -525,7 +525,7 @@ jobs:
|
||||
needs: test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
# This could be from a script if we wanted to but inlining here for now
|
||||
# in one place.
|
||||
@@ -596,7 +596,7 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Get required Zig version
|
||||
id: zig
|
||||
@@ -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@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -614,7 +614,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -643,10 +643,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -656,7 +656,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -691,10 +691,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -704,7 +704,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -726,10 +726,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -739,7 +739,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -753,13 +753,13 @@ jobs:
|
||||
needs: test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -790,10 +790,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -803,7 +803,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -820,9 +820,9 @@ jobs:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -830,7 +830,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -850,9 +850,9 @@ jobs:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -860,7 +860,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -879,9 +879,9 @@ jobs:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -889,7 +889,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -906,9 +906,9 @@ jobs:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -916,7 +916,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -933,9 +933,9 @@ jobs:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -943,7 +943,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -960,9 +960,9 @@ jobs:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -970,7 +970,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -992,9 +992,9 @@ jobs:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -1002,7 +1002,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1019,9 +1019,9 @@ jobs:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -1029,7 +1029,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1053,10 +1053,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -1066,7 +1066,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -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
|
||||
@@ -1098,7 +1098,7 @@ jobs:
|
||||
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: dist
|
||||
file: dist/src/build/docker/debian/Dockerfile
|
||||
@@ -1115,10 +1115,10 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -1128,7 +1128,7 @@ jobs:
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1154,7 +1154,7 @@ jobs:
|
||||
# timeout-minutes: 10
|
||||
# steps:
|
||||
# - name: Checkout Ghostty
|
||||
# uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
# uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
#
|
||||
# - name: Start SSH
|
||||
# run: |
|
||||
|
||||
8
.github/workflows/update-colorschemes.yml
vendored
8
.github/workflows/update-colorschemes.yml
vendored
@@ -17,12 +17,12 @@ jobs:
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
run: nix build .#ghostty
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
title: Update iTerm2 colorschemes
|
||||
base: main
|
||||
|
||||
69
AI_POLICY.md
Normal file
69
AI_POLICY.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# AI Usage Policy
|
||||
|
||||
The Ghostty project has strict rules for AI usage:
|
||||
|
||||
- **All AI usage in any form must be disclosed.** You must state
|
||||
the tool you used (e.g. Claude Code, Cursor, Amp) along with
|
||||
the extent that the work was AI-assisted.
|
||||
|
||||
- **Pull requests created in any way by AI can only be for accepted issues.**
|
||||
Drive-by pull requests that do not reference an accepted issue will be
|
||||
closed. If AI isn't disclosed but a maintainer suspects its use, the
|
||||
PR will be closed. If you want to share code for a non-accepted issue,
|
||||
open a discussion or attach it to an existing discussion.
|
||||
|
||||
- **Pull requests created by AI must have been fully verified with
|
||||
human use.** AI must not create hypothetically correct code that
|
||||
hasn't been tested. Importantly, you must not allow AI to write
|
||||
code for platforms or environments you don't have access to manually
|
||||
test on.
|
||||
|
||||
- **Issues and discussions can use AI assistance but must have a full
|
||||
human-in-the-loop.** This means that any content generated with AI
|
||||
must have been reviewed _and edited_ by a human before submission.
|
||||
AI is very good at being overly verbose and including noise that
|
||||
distracts from the main point. Humans must do their research and
|
||||
trim this down.
|
||||
|
||||
- **No AI-generated media is allowed (art, images, videos, audio, etc.).**
|
||||
Text and code are the only acceptable AI-generated content, per the
|
||||
other rules in this policy.
|
||||
|
||||
- **Bad AI drivers will be banned and ridiculed in public.** You've
|
||||
been warned. We love to help junior developers learn and grow, but
|
||||
if you're interested in that then don't use AI, and we'll help you.
|
||||
I'm sorry that bad AI drivers have ruined this for you.
|
||||
|
||||
These rules apply only to outside contributions to Ghostty. Maintainers
|
||||
are exempt from these rules and may use AI tools at their discretion;
|
||||
they've proven themselves trustworthy to apply good judgment.
|
||||
|
||||
## There are Humans Here
|
||||
|
||||
Please remember that Ghostty is maintained by humans.
|
||||
|
||||
Every discussion, issue, and pull request is read and reviewed by
|
||||
humans (and sometimes machines, too). It is a boundary point at which
|
||||
people interact with each other and the work done. It is rude and
|
||||
disrespectful to approach this boundary with low-effort, unqualified
|
||||
work, since it puts the burden of validation on the maintainer.
|
||||
|
||||
In a perfect world, AI would produce high-quality, accurate work
|
||||
every time. But today, that reality depends on the driver of the AI.
|
||||
And today, most drivers of AI are just not good enough. So, until either
|
||||
the people get better, the AI gets better, or both, we have to have
|
||||
strict rules to protect maintainers.
|
||||
|
||||
## AI is Welcome Here
|
||||
|
||||
Ghostty is written with plenty of AI assistance, and many maintainers embrace
|
||||
AI tools as a productive tool in their workflow. As a project, we welcome
|
||||
AI as a tool!
|
||||
|
||||
**Our reason for the strict AI policy is not due to an anti-AI stance**, but
|
||||
instead due to the number of highly unqualified people using AI. It's the
|
||||
people, not the tools, that are the problem.
|
||||
|
||||
I include this section to be transparent about the project's usage about
|
||||
AI for people who may disagree with it, and to address the misconception
|
||||
that this policy is anti-AI in nature.
|
||||
@@ -185,6 +185,7 @@
|
||||
/po/he_IL.UTF-8.po @ghostty-org/he_IL
|
||||
/po/it_IT.UTF-8.po @ghostty-org/it_IT
|
||||
/po/lt_LT.UTF-8.po @ghostty-org/lt_LT
|
||||
/po/lv_LV.UTF-8.po @ghostty-org/lv_LV
|
||||
/po/zh_TW.UTF-8.po @ghostty-org/zh_TW
|
||||
/po/hr_HR.UTF-8.po @ghostty-org/hr_HR
|
||||
|
||||
|
||||
@@ -13,91 +13,10 @@ it, please check out our ["Developing Ghostty"](HACKING.md) document as well.
|
||||
> time to fixing bugs, maintaining features, and reviewing code, I do kindly
|
||||
> ask you spend a few minutes reading this document. Thank you. ❤️
|
||||
|
||||
## AI Assistance Notice
|
||||
## AI Usage
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> The Ghostty project allows AI-**assisted** _code contributions_, which
|
||||
> must be properly disclosed in the pull request.
|
||||
|
||||
If you are using any kind of AI assistance while contributing to Ghostty,
|
||||
**this must be disclosed in the pull request**, along with the extent to
|
||||
which AI assistance was used (e.g. docs only vs. code generation).
|
||||
|
||||
The submitter must have also tested the pull request on all impacted
|
||||
platforms, and it's **highly discouraged** to code for an unfamiliar platform
|
||||
with AI assistance alone: if you only have a macOS machine, do **not** ask AI
|
||||
to write the equivalent GTK code, and vice versa — someone else with more
|
||||
expertise will eventually get to it and do it for you.
|
||||
|
||||
> [!WARNING]
|
||||
> **Note that AI _assistance_ does not equal AI _generation_**. We require
|
||||
> a significant amount of human accountability, involvement and interaction
|
||||
> even within AI-assisted contributions. Contributors are required to be able
|
||||
> to understand the AI-assisted output, reason with it and answer critical
|
||||
> questions about it. Should a PR see no visible human accountability and
|
||||
> involvement, or it is so broken that it requires significant rework to be
|
||||
> acceptable, **we reserve the right to close it without hesitation**.
|
||||
|
||||
**In addition, we currently restrict AI assistance to code changes only.**
|
||||
No AI-generated media, e.g. artwork, icons, videos and other assets is
|
||||
allowed, as it goes against the methodology and ethos behind Ghostty.
|
||||
While AI-assisted code can help with productive prototyping, creative
|
||||
inspiration and even automated bugfinding, we have currently found zero
|
||||
benefit to AI-generated assets. Instead, we are far more interested and
|
||||
invested in funding professional work done by human designers and artists.
|
||||
If you intend to submit AI-generated assets to Ghostty, sorry,
|
||||
we are not interested.
|
||||
|
||||
Likewise, all community interactions, including all comments on issues and
|
||||
discussions and all PR titles and descriptions **must be composed by a human**.
|
||||
Community moderators and Ghostty maintainers reserve the right to mark
|
||||
AI-generated responses as spam or disruptive content, and ban users who have
|
||||
been repeatedly caught relying entirely on LLMs during interactions.
|
||||
|
||||
> [!NOTE]
|
||||
> If your English isn't the best and you are currently relying on an LLM to
|
||||
> translate your responses, don't fret — usually we maintainers will be able
|
||||
> to understand your messages well enough. We'd like to encourage real humans
|
||||
> to interact with each other more, and the positive impact of genuine,
|
||||
> responsive yet imperfect human interaction more than makes up for any
|
||||
> language barrier.
|
||||
>
|
||||
> Please write your responses yourself, to the best of your ability.
|
||||
> If you do feel the need to polish your sentences, however, please use
|
||||
> dedicated translation software rather than an LLM.
|
||||
>
|
||||
> We greatly appreciate it. Thank you. ❤️
|
||||
|
||||
Minor exceptions to this policy include trivial AI-generated tab completion
|
||||
functionality, as it usually does not impact the quality of the code and
|
||||
do not need to be disclosed, and commit titles and messages, which are often
|
||||
generated by AI coding agents.
|
||||
|
||||
An example disclosure:
|
||||
|
||||
> This PR was written primarily by Claude Code.
|
||||
|
||||
Or a more detailed disclosure:
|
||||
|
||||
> I consulted ChatGPT to understand the codebase but the solution
|
||||
> was fully authored manually by myself.
|
||||
|
||||
An example of a **problematic** disclosure (not having tested all platforms):
|
||||
|
||||
> I used Amp to code both macOS and GTK UIs, but I have not yet tested
|
||||
> the GTK UI as I don't have a Linux setup.
|
||||
|
||||
Failure to disclose this is first and foremost rude to the human operators
|
||||
on the other end of the pull request, but it also makes it difficult to
|
||||
determine how much scrutiny to apply to the contribution.
|
||||
|
||||
In a perfect world, AI assistance would produce equal or higher quality
|
||||
work than any human. That isn't the world we live in today, and in most cases
|
||||
it's generating slop. I say this despite being a fan of and using them
|
||||
successfully myself (with heavy supervision)!
|
||||
|
||||
Please be respectful to maintainers and disclose AI assistance.
|
||||
The Ghostty project has strict rules for AI usage. Please see
|
||||
the [AI Usage Policy](AI_POLICY.md). **This is very important.**
|
||||
|
||||
## Quick Guide
|
||||
|
||||
@@ -451,7 +370,7 @@ requirements for macOS are detailed below.
|
||||
### Linux
|
||||
|
||||
1. Check out the Ghostty source and change to the directory.
|
||||
2. Run `nix run .#check.<system>.<test-name>.driver`. `<system>` should be
|
||||
2. Run `nix run .#checks.<system>.<test-name>.driver`. `<system>` should be
|
||||
`x86_64-linux` or `aarch64-linux` (even on macOS, this launches a Linux
|
||||
VM, not a macOS one). `<test-name>` should be one of the tests defined in
|
||||
`nix/tests.nix`. The test will build and then launch. Depending on the speed
|
||||
|
||||
@@ -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-20260202-151632-49169e9.tgz",
|
||||
.hash = "N-V-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z",
|
||||
.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-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z": {
|
||||
"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-20260202-151632-49169e9.tgz",
|
||||
"hash": "sha256-xN+3iQaN3uIJ/BzkgFxLojgHqeuz1htNcVjcjWR7Qjg="
|
||||
},
|
||||
"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-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z";
|
||||
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-20260202-151632-49169e9.tgz";
|
||||
hash = "sha256-xN+3iQaN3uIJ/BzkgFxLojgHqeuz1htNcVjcjWR7Qjg=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
||||
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-20260202-151632-49169e9.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
|
||||
|
||||
14
flake.lock
generated
14
flake.lock
generated
@@ -41,11 +41,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1768068402,
|
||||
"narHash": "sha256-bAXnnJZKJiF7Xr6eNW6+PhBf1lg2P1aFUO9+xgWkXfA=",
|
||||
"lastModified": 1770586272,
|
||||
"narHash": "sha256-Ucci8mu8QfxwzyfER2DQDbvW9t1BnTUJhBmY7ybralo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "8bc5473b6bc2b6e1529a9c4040411e1199c43b4c",
|
||||
"rev": "b1f916ba052341edc1f80d4b2399f1092a4873ca",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -56,11 +56,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1768032153,
|
||||
"narHash": "sha256-zvxtwlM8ZlulmZKyYCQAPpkm5dngSEnnHjmjV7Teloc=",
|
||||
"rev": "3146c6aa9995e7351a398e17470e15305e6e18ff",
|
||||
"lastModified": 1770537093,
|
||||
"narHash": "sha256-XV30uo8tXuxdzuV8l3sojmlPRLd/8tpMsOp4lNzLGUo=",
|
||||
"rev": "fef9403a3e4d31b0a23f0bacebbec52c248fbb51",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre925418.3146c6aa9995/nixexprs.tar.xz"
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre942631.fef9403a3e4d/nixexprs.tar.xz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
|
||||
171
flake.nix
171
flake.nix
@@ -49,92 +49,101 @@
|
||||
zon2nix,
|
||||
home-manager,
|
||||
...
|
||||
}:
|
||||
builtins.foldl' nixpkgs.lib.recursiveUpdate {} (
|
||||
builtins.map (
|
||||
system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
devShells.${system}.default = pkgs.callPackage ./nix/devShell.nix {
|
||||
zig = zig.packages.${system}."0.15.2";
|
||||
wraptest = pkgs.callPackage ./nix/pkgs/wraptest.nix {};
|
||||
zon2nix = zon2nix;
|
||||
}: let
|
||||
inherit (nixpkgs) lib legacyPackages;
|
||||
|
||||
python3 = pkgs.python3.override {
|
||||
self = pkgs.python3;
|
||||
packageOverrides = pyfinal: pyprev: {
|
||||
blessed = pyfinal.callPackage ./nix/pkgs/blessed.nix {};
|
||||
ucs-detect = pyfinal.callPackage ./nix/pkgs/ucs-detect.nix {};
|
||||
};
|
||||
};
|
||||
# Our supported systems are the same supported systems as the Zig binaries.
|
||||
platforms = lib.attrNames zig.packages;
|
||||
|
||||
# It's not always possible to build Ghostty with Nix for each system,
|
||||
# one such example being macOS due to missing Swift 6 and xcodebuild
|
||||
# support in the Nix ecosystem. Therefore for things like package outputs
|
||||
# we need to limit the attributes we expose.
|
||||
buildablePlatforms = lib.filter (p: !(lib.systems.elaborate p).isDarwin) platforms;
|
||||
|
||||
forAllPlatforms = f: lib.genAttrs platforms (s: f legacyPackages.${s});
|
||||
forBuildablePlatforms = f: lib.genAttrs buildablePlatforms (s: f legacyPackages.${s});
|
||||
|
||||
mkPkgArgs = optimize: {
|
||||
inherit optimize;
|
||||
revision = self.shortRev or self.dirtyShortRev or "dirty";
|
||||
};
|
||||
in {
|
||||
devShells = forAllPlatforms (pkgs: {
|
||||
default = pkgs.callPackage ./nix/devShell.nix {
|
||||
zig = zig.packages.${pkgs.stdenv.hostPlatform.system}."0.15.2";
|
||||
wraptest = pkgs.callPackage ./nix/pkgs/wraptest.nix {};
|
||||
zon2nix = zon2nix;
|
||||
|
||||
python3 = pkgs.python3.override {
|
||||
self = pkgs.python3;
|
||||
packageOverrides = pyfinal: pyprev: {
|
||||
blessed = pyfinal.callPackage ./nix/pkgs/blessed.nix {};
|
||||
ucs-detect = pyfinal.callPackage ./nix/pkgs/ucs-detect.nix {};
|
||||
};
|
||||
|
||||
packages.${system} = let
|
||||
mkArgs = optimize: {
|
||||
inherit optimize;
|
||||
|
||||
revision = self.shortRev or self.dirtyShortRev or "dirty";
|
||||
};
|
||||
in rec {
|
||||
deps = pkgs.callPackage ./build.zig.zon.nix {};
|
||||
ghostty-debug = pkgs.callPackage ./nix/package.nix (mkArgs "Debug");
|
||||
ghostty-releasesafe = pkgs.callPackage ./nix/package.nix (mkArgs "ReleaseSafe");
|
||||
ghostty-releasefast = pkgs.callPackage ./nix/package.nix (mkArgs "ReleaseFast");
|
||||
|
||||
ghostty = ghostty-releasefast;
|
||||
default = ghostty;
|
||||
};
|
||||
|
||||
formatter.${system} = pkgs.alejandra;
|
||||
|
||||
checks.${system} = import ./nix/tests.nix {
|
||||
inherit home-manager nixpkgs self system;
|
||||
};
|
||||
|
||||
apps.${system} = let
|
||||
runVM = (
|
||||
module: let
|
||||
vm = import ./nix/vm/create.nix {
|
||||
inherit system module nixpkgs;
|
||||
overlay = self.overlays.debug;
|
||||
};
|
||||
program = pkgs.writeShellScript "run-ghostty-vm" ''
|
||||
SHARED_DIR=$(pwd)
|
||||
export SHARED_DIR
|
||||
|
||||
${pkgs.lib.getExe vm.config.system.build.vm} "$@"
|
||||
'';
|
||||
in {
|
||||
type = "app";
|
||||
program = "${program}";
|
||||
meta = {
|
||||
description = "start a vm from ${toString module}";
|
||||
};
|
||||
}
|
||||
);
|
||||
in {
|
||||
wayland-cinnamon = runVM ./nix/vm/wayland-cinnamon.nix;
|
||||
wayland-gnome = runVM ./nix/vm/wayland-gnome.nix;
|
||||
wayland-plasma6 = runVM ./nix/vm/wayland-plasma6.nix;
|
||||
x11-cinnamon = runVM ./nix/vm/x11-cinnamon.nix;
|
||||
x11-plasma6 = runVM ./nix/vm/x11-plasma6.nix;
|
||||
x11-xfce = runVM ./nix/vm/x11-xfce.nix;
|
||||
};
|
||||
}
|
||||
# Our supported systems are the same supported systems as the Zig binaries.
|
||||
) (builtins.attrNames zig.packages)
|
||||
)
|
||||
// {
|
||||
overlays = {
|
||||
default = self.overlays.releasefast;
|
||||
releasefast = final: prev: {
|
||||
ghostty = self.packages.${prev.stdenv.hostPlatform.system}.ghostty-releasefast;
|
||||
};
|
||||
debug = final: prev: {
|
||||
ghostty = self.packages.${prev.stdenv.hostPlatform.system}.ghostty-debug;
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
packages =
|
||||
forAllPlatforms (pkgs: {
|
||||
# Deps are needed for environmental setup on macOS
|
||||
deps = pkgs.callPackage ./build.zig.zon.nix {};
|
||||
})
|
||||
// forBuildablePlatforms (pkgs: rec {
|
||||
ghostty-debug = pkgs.callPackage ./nix/package.nix (mkPkgArgs "Debug");
|
||||
ghostty-releasesafe = pkgs.callPackage ./nix/package.nix (mkPkgArgs "ReleaseSafe");
|
||||
ghostty-releasefast = pkgs.callPackage ./nix/package.nix (mkPkgArgs "ReleaseFast");
|
||||
|
||||
ghostty = ghostty-releasefast;
|
||||
default = ghostty;
|
||||
});
|
||||
|
||||
formatter = forAllPlatforms (pkgs: pkgs.alejandra);
|
||||
|
||||
apps = forBuildablePlatforms (pkgs: let
|
||||
runVM = module: let
|
||||
vm = import ./nix/vm/create.nix {
|
||||
inherit (pkgs.stdenv.hostPlatform) system;
|
||||
inherit module nixpkgs;
|
||||
overlay = self.overlays.debug;
|
||||
};
|
||||
program = pkgs.writeShellScript "run-ghostty-vm" ''
|
||||
SHARED_DIR=$(pwd)
|
||||
export SHARED_DIR
|
||||
|
||||
${pkgs.lib.getExe vm.config.system.build.vm} "$@"
|
||||
'';
|
||||
in {
|
||||
type = "app";
|
||||
program = "${program}";
|
||||
meta.description = "start a vm from ${toString module}";
|
||||
};
|
||||
in {
|
||||
wayland-cinnamon = runVM ./nix/vm/wayland-cinnamon.nix;
|
||||
wayland-gnome = runVM ./nix/vm/wayland-gnome.nix;
|
||||
wayland-plasma6 = runVM ./nix/vm/wayland-plasma6.nix;
|
||||
x11-cinnamon = runVM ./nix/vm/x11-cinnamon.nix;
|
||||
x11-plasma6 = runVM ./nix/vm/x11-plasma6.nix;
|
||||
x11-xfce = runVM ./nix/vm/x11-xfce.nix;
|
||||
});
|
||||
|
||||
checks = forAllPlatforms (pkgs:
|
||||
import ./nix/tests.nix {
|
||||
inherit home-manager nixpkgs self;
|
||||
inherit (pkgs.stdenv.hostPlatform) system;
|
||||
});
|
||||
|
||||
overlays = {
|
||||
default = self.overlays.releasefast;
|
||||
releasefast = final: prev: {
|
||||
ghostty = final.callPackage ./nix/package.nix (mkPkgArgs "ReleaseFast");
|
||||
};
|
||||
debug = final: prev: {
|
||||
ghostty = final.callPackage ./nix/package.nix (mkPkgArgs "Debug");
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nixConfig = {
|
||||
extra-substituters = ["https://ghostty.cachix.org"];
|
||||
|
||||
@@ -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-20260202-151632-49169e9.tgz",
|
||||
"dest": "vendor/p/N-V-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z",
|
||||
"sha256": "c4dfb789068ddee209fc1ce4805c4ba23807a9ebb3d61b4d7158dc8d647b4238"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
||||
@@ -63,24 +63,26 @@ typedef enum {
|
||||
GHOSTTY_OSC_COMMAND_INVALID = 0,
|
||||
GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1,
|
||||
GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2,
|
||||
GHOSTTY_OSC_COMMAND_PROMPT_START = 3,
|
||||
GHOSTTY_OSC_COMMAND_PROMPT_END = 4,
|
||||
GHOSTTY_OSC_COMMAND_END_OF_INPUT = 5,
|
||||
GHOSTTY_OSC_COMMAND_END_OF_COMMAND = 6,
|
||||
GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 7,
|
||||
GHOSTTY_OSC_COMMAND_REPORT_PWD = 8,
|
||||
GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 9,
|
||||
GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 10,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 11,
|
||||
GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 12,
|
||||
GHOSTTY_OSC_COMMAND_HYPERLINK_START = 13,
|
||||
GHOSTTY_OSC_COMMAND_HYPERLINK_END = 14,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 15,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 16,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 17,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 18,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 19,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 20,
|
||||
GHOSTTY_OSC_COMMAND_SEMANTIC_PROMPT = 3,
|
||||
GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 4,
|
||||
GHOSTTY_OSC_COMMAND_REPORT_PWD = 5,
|
||||
GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 6,
|
||||
GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 7,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 8,
|
||||
GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 9,
|
||||
GHOSTTY_OSC_COMMAND_HYPERLINK_START = 10,
|
||||
GHOSTTY_OSC_COMMAND_HYPERLINK_END = 11,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 12,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 13,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 14,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 15,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 16,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 17,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_RUN_PROCESS = 18,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_OUTPUT_ENVIRONMENT_VARIABLE = 19,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22,
|
||||
} GhosttyOscCommandType;
|
||||
|
||||
/**
|
||||
|
||||
@@ -623,6 +623,7 @@
|
||||
INFOPLIST_KEY_NSLocationUsageDescription = "A program running within Ghostty would like to access your location information.";
|
||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program running within Ghostty would like to use your microphone.";
|
||||
INFOPLIST_KEY_NSAudioCaptureUsageDescription = "A program running within Ghostty would like to access your system's audio.";
|
||||
INFOPLIST_KEY_NSMotionUsageDescription = "A program running within Ghostty would like to access motion data.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program running within Ghostty would like to access your Photo Library.";
|
||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
|
||||
|
||||
@@ -476,7 +476,7 @@ class AppDelegate: NSObject,
|
||||
// profile/rc files for the shell, which is super important on macOS
|
||||
// due to things like Homebrew. Instead, we set the command to
|
||||
// `<filename>; exit` which is what Terminal and iTerm2 do.
|
||||
config.initialInput = "\(filename); exit\n"
|
||||
config.initialInput = "\(filename.shellQuoted()); exit\n"
|
||||
|
||||
// For commands executed directly, we want to ensure we wait after exit
|
||||
// because in most cases scripts don't block on exit and we don't want
|
||||
|
||||
@@ -68,7 +68,7 @@ struct NewTerminalIntent: AppIntent {
|
||||
// We don't run command as "command" and instead use "initialInput" so
|
||||
// that we can get all the login scripts to setup things like PATH.
|
||||
if let command {
|
||||
config.initialInput = "\(command); exit\n"
|
||||
config.initialInput = "\(command.shellQuoted()); exit\n"
|
||||
}
|
||||
|
||||
// If we were given a working directory then open that directory
|
||||
|
||||
@@ -120,14 +120,16 @@ struct TerminalCommandPaletteView: View {
|
||||
/// Custom commands from the command-palette-entry configuration.
|
||||
private var terminalOptions: [CommandOption] {
|
||||
guard let appDelegate = NSApp.delegate as? AppDelegate else { return [] }
|
||||
return appDelegate.ghostty.config.commandPaletteEntries.map { c in
|
||||
CommandOption(
|
||||
title: c.title,
|
||||
description: c.description
|
||||
) {
|
||||
onAction(c.action)
|
||||
return appDelegate.ghostty.config.commandPaletteEntries
|
||||
.filter(\.isSupported)
|
||||
.map { c in
|
||||
CommandOption(
|
||||
title: c.title,
|
||||
description: c.description
|
||||
) {
|
||||
onAction(c.action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Commands for jumping to other terminal surfaces.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24506"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -17,7 +17,7 @@
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="0.0" y="0.0" width="800" height="600"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1661"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1512" height="949"/>
|
||||
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24506"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -17,7 +17,7 @@
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="0.0" y="0.0" width="800" height="600"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1661"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1512" height="949"/>
|
||||
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
|
||||
@@ -508,13 +508,13 @@ extension Ghostty {
|
||||
return gotoWindow(app, target: target, direction: action.action.goto_window)
|
||||
|
||||
case GHOSTTY_ACTION_RESIZE_SPLIT:
|
||||
resizeSplit(app, target: target, resize: action.action.resize_split)
|
||||
return resizeSplit(app, target: target, resize: action.action.resize_split)
|
||||
|
||||
case GHOSTTY_ACTION_EQUALIZE_SPLITS:
|
||||
equalizeSplits(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM:
|
||||
toggleSplitZoom(app, target: target)
|
||||
return toggleSplitZoom(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_INSPECTOR:
|
||||
controlInspector(app, target: target, mode: action.action.inspector)
|
||||
@@ -1247,16 +1247,21 @@ extension Ghostty {
|
||||
private static func resizeSplit(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
resize: ghostty_action_resize_split_s) {
|
||||
resize: ghostty_action_resize_split_s) -> Bool {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("resize split does nothing with an app target")
|
||||
return
|
||||
return false
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
guard let resizeDirection = SplitResizeDirection.from(direction: resize.direction) else { return }
|
||||
guard let surface = target.target.surface else { return false }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
||||
guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { return false }
|
||||
|
||||
// If the window has no splits, the action is not performable
|
||||
guard controller.surfaceTree.isSplit else { return false }
|
||||
|
||||
guard let resizeDirection = SplitResizeDirection.from(direction: resize.direction) else { return false }
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didResizeSplit,
|
||||
object: surfaceView,
|
||||
@@ -1265,9 +1270,11 @@ extension Ghostty {
|
||||
Notification.ResizeSplitAmountKey: resize.amount,
|
||||
]
|
||||
)
|
||||
return true
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1295,23 +1302,30 @@ extension Ghostty {
|
||||
|
||||
private static func toggleSplitZoom(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s) {
|
||||
target: ghostty_target_s) -> Bool {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("toggle split zoom does nothing with an app target")
|
||||
return
|
||||
return false
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
guard let surface = target.target.surface else { return false }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
||||
guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { return false }
|
||||
|
||||
// If the window has no splits, the action is not performable
|
||||
guard controller.surfaceTree.isSplit else { return false }
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didToggleSplitZoom,
|
||||
object: surfaceView
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1321,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)
|
||||
|
||||
}
|
||||
|
||||
@@ -269,16 +267,10 @@ extension Ghostty {
|
||||
// Builds up the "input.ScrollMods" bitmask
|
||||
var mods: Int32 = 0
|
||||
|
||||
var x = event.scrollingDeltaX
|
||||
var y = event.scrollingDeltaY
|
||||
let x = event.scrollingDeltaX
|
||||
let y = event.scrollingDeltaY
|
||||
if event.hasPreciseScrollingDeltas {
|
||||
mods = 1
|
||||
|
||||
// We do a 2x speed multiplier. This is subjective, it "feels" better to me.
|
||||
x *= 2;
|
||||
y *= 2;
|
||||
|
||||
// TODO(mitchellh): do we have to scale the x/y here by window scale factor?
|
||||
}
|
||||
|
||||
// Determine our momentum value
|
||||
@@ -303,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) {
|
||||
@@ -342,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
|
||||
@@ -412,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) {
|
||||
@@ -441,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)
|
||||
|
||||
@@ -190,7 +190,12 @@ extension Ghostty {
|
||||
SurfaceSearchOverlay(
|
||||
surfaceView: surfaceView,
|
||||
searchState: searchState,
|
||||
onClose: { surfaceView.searchState = nil }
|
||||
onClose: {
|
||||
#if canImport(AppKit)
|
||||
Ghostty.moveFocus(to: surfaceView)
|
||||
#endif
|
||||
surfaceView.searchState = nil
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -432,7 +437,6 @@ extension Ghostty {
|
||||
#if canImport(AppKit)
|
||||
.onExitCommand {
|
||||
if searchState.needle.isEmpty {
|
||||
Ghostty.moveFocus(to: surfaceView)
|
||||
onClose()
|
||||
} else {
|
||||
Ghostty.moveFocus(to: surfaceView)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,4 +26,12 @@ extension String {
|
||||
return self
|
||||
}
|
||||
#endif
|
||||
|
||||
private static let shellUnsafe = /[^\w@%+=:,.\/-]/
|
||||
|
||||
/// Returns a shell-escaped version of the string, like Python's shlex.quote.
|
||||
func shellQuoted() -> String {
|
||||
guard self.isEmpty || self.contains(Self.shellUnsafe) else { return self };
|
||||
return "'" + self.replacingOccurrences(of: "'", with: #"'"'"'"#) + "'"
|
||||
}
|
||||
}
|
||||
|
||||
19
macos/Tests/Helpers/Extensions/StringExtensionTests.swift
Normal file
19
macos/Tests/Helpers/Extensions/StringExtensionTests.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
import Testing
|
||||
@testable import Ghostty
|
||||
|
||||
struct StringExtensionTests {
|
||||
@Test(arguments: [
|
||||
("", "''"),
|
||||
("filename", "filename"),
|
||||
("abcABC123@%_-+=:,./", "abcABC123@%_-+=:,./"),
|
||||
("file name", "'file name'"),
|
||||
("file$name", "'file$name'"),
|
||||
("file!name", "'file!name'"),
|
||||
("file\\name", "'file\\name'"),
|
||||
("it's", "'it'\"'\"'s'"),
|
||||
("file$'name'", "'file$'\"'\"'name'\"'\"''"),
|
||||
])
|
||||
func shellQuoted(input: String, expected: String) {
|
||||
#expect(input.shellQuoted() == expected)
|
||||
}
|
||||
}
|
||||
@@ -35,10 +35,10 @@
|
||||
pkgs.libadwaita
|
||||
]
|
||||
++ lib.optionals (stdenv.hostPlatform.isLinux && enableX11) [
|
||||
pkgs.xorg.libX11
|
||||
pkgs.xorg.libXcursor
|
||||
pkgs.xorg.libXi
|
||||
pkgs.xorg.libXrandr
|
||||
pkgs.libx11
|
||||
pkgs.libxcursor
|
||||
pkgs.libxi
|
||||
pkgs.libxrandr
|
||||
]
|
||||
++ lib.optionals (stdenv.hostPlatform.isLinux && enableWayland) [
|
||||
pkgs.gtk4-layer-shell
|
||||
|
||||
@@ -77,7 +77,6 @@ in
|
||||
|
||||
buildInputs = buildInputs;
|
||||
|
||||
dontConfigure = true;
|
||||
dontStrip = !strip;
|
||||
|
||||
GI_TYPELIB_PATH = gi_typelib_path;
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
};
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
extraOptionOverrides = {
|
||||
StrictHostKeyChecking = "accept-new";
|
||||
UserKnownHostsFile = "/dev/null";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -238,6 +238,12 @@ pub const Buffer = struct {
|
||||
pub fn guessSegmentProperties(self: Buffer) void {
|
||||
c.hb_buffer_guess_segment_properties(self.handle);
|
||||
}
|
||||
|
||||
/// Sets the cluster level of a buffer. The `ClusterLevel` dictates one
|
||||
/// aspect of how HarfBuzz will treat non-base characters during shaping.
|
||||
pub fn setClusterLevel(self: Buffer, level: ClusterLevel) void {
|
||||
c.hb_buffer_set_cluster_level(self.handle, @intFromEnum(level));
|
||||
}
|
||||
};
|
||||
|
||||
/// The type of hb_buffer_t contents.
|
||||
@@ -252,6 +258,40 @@ pub const ContentType = enum(u2) {
|
||||
glyphs = c.HB_BUFFER_CONTENT_TYPE_GLYPHS,
|
||||
};
|
||||
|
||||
/// Data type for holding HarfBuzz's clustering behavior options. The cluster
|
||||
/// level dictates one aspect of how HarfBuzz will treat non-base characters
|
||||
/// during shaping.
|
||||
pub const ClusterLevel = enum(u2) {
|
||||
/// In `monotone_graphemes`, non-base characters are merged into the
|
||||
/// cluster of the base character that precedes them. There is also cluster
|
||||
/// merging every time the clusters will otherwise become non-monotone.
|
||||
/// This is the default cluster level.
|
||||
monotone_graphemes = c.HB_BUFFER_CLUSTER_LEVEL_MONOTONE_GRAPHEMES,
|
||||
|
||||
/// In `monotone_characters`, non-base characters are initially assigned
|
||||
/// their own cluster values, which are not merged into preceding base
|
||||
/// clusters. This allows HarfBuzz to perform additional operations like
|
||||
/// reorder sequences of adjacent marks. The output is still monotone, but
|
||||
/// the cluster values are more granular.
|
||||
monotone_characters = c.HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS,
|
||||
|
||||
/// In `characters`, non-base characters are assigned their own cluster
|
||||
/// values, which are not merged into preceding base clusters. Moreover,
|
||||
/// the cluster values are not merged into monotone order. This is the most
|
||||
/// granular cluster level, and it is useful for clients that need to know
|
||||
/// the exact cluster values of each character, but is harder to use for
|
||||
/// clients, since clusters might appear in any order.
|
||||
characters = c.HB_BUFFER_CLUSTER_LEVEL_CHARACTERS,
|
||||
|
||||
/// In `graphemes`, non-base characters are merged into the cluster of the
|
||||
/// base character that precedes them. This is similar to the Unicode
|
||||
/// Grapheme Cluster algorithm, but it is not exactly the same. The output
|
||||
/// is not forced to be monotone. This is useful for clients that want to
|
||||
/// use HarfBuzz as a cheap implementation of the Unicode Grapheme Cluster
|
||||
/// algorithm.
|
||||
graphemes = c.HB_BUFFER_CLUSTER_LEVEL_GRAPHEMES,
|
||||
};
|
||||
|
||||
/// The hb_glyph_info_t is the structure that holds information about the
|
||||
/// glyphs and their relation to input text.
|
||||
pub const GlyphInfo = extern struct {
|
||||
|
||||
@@ -13,6 +13,7 @@ pub const coretext = @import("coretext.zig");
|
||||
pub const MemoryMode = blob.MemoryMode;
|
||||
pub const Blob = blob.Blob;
|
||||
pub const Buffer = buffer.Buffer;
|
||||
pub const GlyphPosition = buffer.GlyphPosition;
|
||||
pub const Direction = common.Direction;
|
||||
pub const Script = common.Script;
|
||||
pub const Language = common.Language;
|
||||
|
||||
339
po/lv_LV.UTF-8.po
Normal file
339
po/lv_LV.UTF-8.po
Normal file
@@ -0,0 +1,339 @@
|
||||
# Latvian translations for com.mitchellh.ghostty package.
|
||||
# Copyright (C) 2026 "Mitchell Hashimoto, Ghostty contributors"
|
||||
# This file is distributed under the same license as the com.mitchellh.ghostty package.
|
||||
# Ēriks Remess <eriks@remess.lv>, 2026.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2026-02-05 10:23+0800\n"
|
||||
"PO-Revision-Date: 2026-02-09 03:24+0200\n"
|
||||
"Last-Translator: Ēriks Remess <eriks@remess.lv>\n"
|
||||
"Language-Team: Latvian\n"
|
||||
"Language: LV\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n!=0 ? 1 : 2);\n"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12
|
||||
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12
|
||||
#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197
|
||||
#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201
|
||||
msgid "Authorize Clipboard Access"
|
||||
msgstr "Atļaut piekļuvi starpliktuvei"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17
|
||||
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17
|
||||
msgid "Deny"
|
||||
msgstr "Noraidīt"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18
|
||||
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18
|
||||
msgid "Allow"
|
||||
msgstr "Atļaut"
|
||||
|
||||
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92
|
||||
msgid "Remember choice for this split"
|
||||
msgstr "Atcerēties izvēli šim sadalījumam"
|
||||
|
||||
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93
|
||||
msgid "Reload configuration to show this prompt again"
|
||||
msgstr "Pārlādējiet konfigurāciju, lai šo uzvedni rādītu atkal"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7
|
||||
#: src/apprt/gtk/ui/1.5/surface-title-dialog.blp:9
|
||||
msgid "Cancel"
|
||||
msgstr "Atcelt"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8
|
||||
#: src/apprt/gtk/ui/1.2/search-overlay.blp:85
|
||||
#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17
|
||||
msgid "Close"
|
||||
msgstr "Aizvērt"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
|
||||
msgid "Configuration Errors"
|
||||
msgstr "Konfigurācijas kļūdas"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7
|
||||
msgid ""
|
||||
"One or more configuration errors were found. Please review the errors below, "
|
||||
"and either reload your configuration or ignore these errors."
|
||||
msgstr ""
|
||||
"Tika atrasta viena vai vairākas konfigurācijas kļūdas. Lūdzu, pārskatiet "
|
||||
"zemāk redzamās kļūdas un pārlādējiet konfigurāciju vai ignorējiet tās."
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10
|
||||
msgid "Ignore"
|
||||
msgstr "Ignorēt"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:293
|
||||
msgid "Reload Configuration"
|
||||
msgstr "Pārlādēt konfigurāciju"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/debug-warning.blp:7
|
||||
#: src/apprt/gtk/ui/1.3/debug-warning.blp:6
|
||||
msgid ""
|
||||
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
|
||||
msgstr "⚠️ Jūs izmantojat Ghostty atkļūdošanas būvējumu! Veiktspēja būs zemāka."
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/inspector-window.blp:5
|
||||
msgid "Ghostty: Terminal Inspector"
|
||||
msgstr "Ghostty: Termināļa inspektors"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/search-overlay.blp:29
|
||||
msgid "Find…"
|
||||
msgstr "Meklēt…"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/search-overlay.blp:64
|
||||
msgid "Previous Match"
|
||||
msgstr "Iepriekšējā atbilstība"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/search-overlay.blp:74
|
||||
msgid "Next Match"
|
||||
msgstr "Nākamā atbilstība"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:6
|
||||
msgid "Oh, no."
|
||||
msgstr "Ak, nē."
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:7
|
||||
msgid "Unable to acquire an OpenGL context for rendering."
|
||||
msgstr "Neizdevās iegūt OpenGL kontekstu attēlošanai."
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:97
|
||||
msgid ""
|
||||
"This terminal is in read-only mode. You can still view, select, and scroll "
|
||||
"through the content, but no input events will be sent to the running "
|
||||
"application."
|
||||
msgstr ""
|
||||
"Šis terminālis ir tikai lasīšanas režīmā. Jūs joprojām varat skatīt, atlasīt "
|
||||
"un ritināt saturu, taču ievade netiks sūtīta palaistajai lietotnei."
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:107
|
||||
msgid "Read-only"
|
||||
msgstr "Tikai lasīšanai"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:198
|
||||
msgid "Copy"
|
||||
msgstr "Kopēt"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:203
|
||||
msgid "Paste"
|
||||
msgstr "Ielīmēt"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:270
|
||||
msgid "Notify on Next Command Finish"
|
||||
msgstr "Paziņot, kad nākamā komanda būs izpildīta"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:266
|
||||
msgid "Clear"
|
||||
msgstr "Notīrīt"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:271
|
||||
msgid "Reset"
|
||||
msgstr "Atiestatīt"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:235
|
||||
msgid "Split"
|
||||
msgstr "Sadalīt"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:238
|
||||
msgid "Change Title…"
|
||||
msgstr "Mainīt virsrakstu…"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:175
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:243
|
||||
msgid "Split Up"
|
||||
msgstr "Sadalīt uz augšu"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:180
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:248
|
||||
msgid "Split Down"
|
||||
msgstr "Sadalīt uz leju"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:185
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:253
|
||||
msgid "Split Left"
|
||||
msgstr "Sadalīt pa kreisi"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:190
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:258
|
||||
msgid "Split Right"
|
||||
msgstr "Sadalīt pa labi"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:322
|
||||
msgid "Tab"
|
||||
msgstr "Cilne"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:57
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:222
|
||||
msgid "New Tab"
|
||||
msgstr "Jauna cilne"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:227
|
||||
msgid "Close Tab"
|
||||
msgstr "Aizvērt cilni"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:337
|
||||
msgid "Window"
|
||||
msgstr "Logs"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:340 src/apprt/gtk/ui/1.5/window.blp:210
|
||||
msgid "New Window"
|
||||
msgstr "Jauns logs"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:215
|
||||
msgid "Close Window"
|
||||
msgstr "Aizvērt logu"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:353
|
||||
msgid "Config"
|
||||
msgstr "Konfigurācija"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:356 src/apprt/gtk/ui/1.5/window.blp:288
|
||||
msgid "Open Configuration"
|
||||
msgstr "Atvērt konfigurāciju"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/surface-title-dialog.blp:5
|
||||
msgid "Change Terminal Title"
|
||||
msgstr "Mainīt termināļa virsrakstu"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/surface-title-dialog.blp:6
|
||||
msgid "Leave blank to restore the default title."
|
||||
msgstr "Atstāj tukšu, lai atjaunotu noklusēto virsrakstu."
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/surface-title-dialog.blp:10
|
||||
msgid "OK"
|
||||
msgstr "Labi"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108
|
||||
msgid "New Split"
|
||||
msgstr "Jauns sadalījums"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126
|
||||
msgid "View Open Tabs"
|
||||
msgstr "Skatīt atvērtās cilnes"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140
|
||||
msgid "Main Menu"
|
||||
msgstr "Galvenā izvēlne"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:278
|
||||
msgid "Command Palette"
|
||||
msgstr "Komandu palete"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:283
|
||||
msgid "Terminal Inspector"
|
||||
msgstr "Termināļa inspektors"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:300 src/apprt/gtk/class/window.zig:1714
|
||||
msgid "About Ghostty"
|
||||
msgstr "Par Ghostty"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:305
|
||||
msgid "Quit"
|
||||
msgstr "Iziet"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/command-palette.blp:17
|
||||
msgid "Execute a command…"
|
||||
msgstr "Izpildīt komandu…"
|
||||
|
||||
#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198
|
||||
msgid ""
|
||||
"An application is attempting to write to the clipboard. The current "
|
||||
"clipboard contents are shown below."
|
||||
msgstr ""
|
||||
"Lietotne mēģina rakstīt starpliktuvē. Zemāk ir redzams pašreizējais "
|
||||
"starpliktuves saturs."
|
||||
|
||||
#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202
|
||||
msgid ""
|
||||
"An application is attempting to read from the clipboard. The current "
|
||||
"clipboard contents are shown below."
|
||||
msgstr ""
|
||||
"Lietotne mēģina lasīt no starpliktuves. Zemāk ir redzams pašreizējais "
|
||||
"starpliktuves saturs."
|
||||
|
||||
#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205
|
||||
msgid "Warning: Potentially Unsafe Paste"
|
||||
msgstr "Brīdinājums: potenciāli nedroša ielīmēšana"
|
||||
|
||||
#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206
|
||||
msgid ""
|
||||
"Pasting this text into the terminal may be dangerous as it looks like some "
|
||||
"commands may be executed."
|
||||
msgstr ""
|
||||
"Šī teksta ielīmēšana terminālī var būt bīstama, jo izskatās, ka var tikt "
|
||||
"izpildītas komandas."
|
||||
|
||||
#: src/apprt/gtk/class/close_confirmation_dialog.zig:184
|
||||
msgid "Quit Ghostty?"
|
||||
msgstr "Iziet no Ghostty?"
|
||||
|
||||
#: src/apprt/gtk/class/close_confirmation_dialog.zig:185
|
||||
msgid "Close Tab?"
|
||||
msgstr "Aizvērt cilni?"
|
||||
|
||||
#: src/apprt/gtk/class/close_confirmation_dialog.zig:186
|
||||
msgid "Close Window?"
|
||||
msgstr "Aizvērt logu?"
|
||||
|
||||
#: src/apprt/gtk/class/close_confirmation_dialog.zig:187
|
||||
msgid "Close Split?"
|
||||
msgstr "Aizvērt sadalījumu?"
|
||||
|
||||
#: src/apprt/gtk/class/close_confirmation_dialog.zig:193
|
||||
msgid "All terminal sessions will be terminated."
|
||||
msgstr "Visas termināļa sesijas tiks pārtrauktas."
|
||||
|
||||
#: src/apprt/gtk/class/close_confirmation_dialog.zig:194
|
||||
msgid "All terminal sessions in this tab will be terminated."
|
||||
msgstr "Visas termināļa sesijas šajā cilnē tiks pārtrauktas."
|
||||
|
||||
#: src/apprt/gtk/class/close_confirmation_dialog.zig:195
|
||||
msgid "All terminal sessions in this window will be terminated."
|
||||
msgstr "Visas termināļa sesijas šajā logā tiks pārtrauktas."
|
||||
|
||||
#: src/apprt/gtk/class/close_confirmation_dialog.zig:196
|
||||
msgid "The currently running process in this split will be terminated."
|
||||
msgstr "Pašlaik palaistais process šajā sadalījumā tiks pārtraukts."
|
||||
|
||||
#: src/apprt/gtk/class/surface.zig:1108
|
||||
msgid "Command Finished"
|
||||
msgstr "Komanda izpildīta"
|
||||
|
||||
#: src/apprt/gtk/class/surface.zig:1109
|
||||
msgid "Command Succeeded"
|
||||
msgstr "Komanda izdevās"
|
||||
|
||||
#: src/apprt/gtk/class/surface.zig:1110
|
||||
msgid "Command Failed"
|
||||
msgstr "Komanda neizdevās"
|
||||
|
||||
#: src/apprt/gtk/class/surface_child_exited.zig:109
|
||||
msgid "Command succeeded"
|
||||
msgstr "Komanda izdevās"
|
||||
|
||||
#: src/apprt/gtk/class/surface_child_exited.zig:113
|
||||
msgid "Command failed"
|
||||
msgstr "Komanda neizdevās"
|
||||
|
||||
#: src/apprt/gtk/class/window.zig:1001
|
||||
msgid "Reloaded the configuration"
|
||||
msgstr "Konfigurācija pārlādēta"
|
||||
|
||||
#: src/apprt/gtk/class/window.zig:1553
|
||||
msgid "Copied to clipboard"
|
||||
msgstr "Nokopēts starpliktuvē"
|
||||
|
||||
#: src/apprt/gtk/class/window.zig:1555
|
||||
msgid "Cleared clipboard"
|
||||
msgstr "Starpliktuve notīrīta"
|
||||
|
||||
#: src/apprt/gtk/class/window.zig:1695
|
||||
msgid "Ghostty Developers"
|
||||
msgstr "Ghostty izstrādātāji"
|
||||
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,
|
||||
|
||||
228
src/Surface.zig
228
src/Surface.zig
@@ -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
|
||||
@@ -3995,6 +3969,15 @@ pub fn mouseButtonCallback(
|
||||
log.warn("error processing links err={}", .{err});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle prompt clicking. If we released our mouse on a prompt
|
||||
// and we support some kind of click events, then we need to
|
||||
// move to it.
|
||||
if (self.maybePromptClick()) |handled| {
|
||||
if (handled) return true;
|
||||
} else |err| {
|
||||
log.warn("error processing prompt click err={}", .{err});
|
||||
}
|
||||
}
|
||||
|
||||
// Report mouse events if enabled
|
||||
@@ -4036,25 +4019,6 @@ pub fn mouseButtonCallback(
|
||||
}
|
||||
}
|
||||
|
||||
// For left button click release we check if we are moving our cursor.
|
||||
if (button == .left and
|
||||
action == .release and
|
||||
mods.alt)
|
||||
click_move: {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// If we have a selection then we do not do click to move because
|
||||
// it means that we moved our cursor while pressing the mouse button.
|
||||
if (self.io.terminal.screens.active.selection != null) break :click_move;
|
||||
|
||||
// Moving always resets the click count so that we don't highlight.
|
||||
self.mouse.left_click_count = 0;
|
||||
const pin = self.mouse.left_click_pin orelse break :click_move;
|
||||
try self.clickMoveCursor(pin.*);
|
||||
return true;
|
||||
}
|
||||
|
||||
// For left button clicks we always record some information for
|
||||
// selection/highlighting purposes.
|
||||
if (button == .left and action == .press) click: {
|
||||
@@ -4214,10 +4178,6 @@ pub fn mouseButtonCallback(
|
||||
.y = pt_viewport.y,
|
||||
},
|
||||
}) orelse {
|
||||
// Weird... our viewport x/y that we just converted isn't
|
||||
// found in our pages. This is probably a bug but we don't
|
||||
// want to crash in releases because its harmless. So, we
|
||||
// only assert in debug mode.
|
||||
if (comptime std.debug.runtime_safety) unreachable;
|
||||
break :sel;
|
||||
};
|
||||
@@ -4304,58 +4264,118 @@ pub fn mouseButtonCallback(
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Performs the "click-to-move" logic to move the cursor to the given
|
||||
/// screen point if possible. This works by converting the path to the
|
||||
/// given point into a series of arrow key inputs.
|
||||
fn clickMoveCursor(self: *Surface, to: terminal.Pin) !void {
|
||||
// If click-to-move is disabled then we're done.
|
||||
if (!self.config.cursor_click_to_move) return;
|
||||
fn maybePromptClick(self: *Surface) !bool {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
const t: *terminal.Terminal = self.renderer_state.terminal;
|
||||
const screen: *terminal.Screen = t.screens.active;
|
||||
|
||||
const t = &self.io.terminal;
|
||||
// If our screen doesn't handle any prompt clicks, then we never
|
||||
// do anything.
|
||||
if (screen.semantic_prompt.click == .none) return false;
|
||||
|
||||
// Click to move cursor only works on the primary screen where prompts
|
||||
// exist. This means that alt screen multiplexers like tmux will not
|
||||
// support this feature. It is just too messy.
|
||||
if (t.screens.active_key != .primary) return;
|
||||
// If our cursor isn't currently at a prompt then we don't handle
|
||||
// prompt clicks because we can't move if we're not in a prompt!
|
||||
if (!t.cursorIsAtPrompt()) return false;
|
||||
|
||||
// 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 we have a selection currently, then releasing the mouse
|
||||
// completes the selection and we don't do prompt moving. I don't
|
||||
// love this logic, I think it should be generalized to "if the
|
||||
// mouse release was on a different cell than the mouse press" but
|
||||
// our mouse state at the time of writing this doesn't support that.
|
||||
if (screen.selection != null) return false;
|
||||
|
||||
// Get our path
|
||||
const from = t.screens.active.cursor.page_pin.*;
|
||||
const path = t.screens.active.promptPath(from, to);
|
||||
log.debug("click-to-move-cursor from={} to={} path={}", .{ from, to, path });
|
||||
|
||||
// If we aren't moving at all, fast path out of here.
|
||||
if (path.x == 0 and path.y == 0) return;
|
||||
|
||||
// Convert our path to arrow key inputs. Yes, that is how this works.
|
||||
// Yes, that is pretty sad. Yes, this could backfire in various ways.
|
||||
// But its the best we can do.
|
||||
|
||||
// We do Y first because it prevents any weird wrap behavior.
|
||||
if (path.y != 0) {
|
||||
const arrow = if (path.y < 0) arrow: {
|
||||
break :arrow if (t.modes.get(.cursor_keys)) "\x1bOA" else "\x1b[A";
|
||||
} else arrow: {
|
||||
break :arrow if (t.modes.get(.cursor_keys)) "\x1bOB" else "\x1b[B";
|
||||
// Get the pin for our mouse click.
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
const pos_vp = self.posToViewport(pos.x, pos.y);
|
||||
const click_pin: terminal.Pin = pin: {
|
||||
const pin = screen.pages.pin(.{
|
||||
.viewport = .{
|
||||
.x = pos_vp.x,
|
||||
.y = pos_vp.y,
|
||||
},
|
||||
}) orelse {
|
||||
// See mouseButtonCallback for explanation
|
||||
if (comptime std.debug.runtime_safety) unreachable;
|
||||
return false;
|
||||
};
|
||||
for (0..@abs(path.y)) |_| {
|
||||
self.queueIo(.{ .write_stable = arrow }, .locked);
|
||||
}
|
||||
}
|
||||
if (path.x != 0) {
|
||||
const arrow = if (path.x < 0) arrow: {
|
||||
break :arrow if (t.modes.get(.cursor_keys)) "\x1bOD" else "\x1b[D";
|
||||
} else arrow: {
|
||||
break :arrow if (t.modes.get(.cursor_keys)) "\x1bOC" else "\x1b[C";
|
||||
|
||||
break :pin pin;
|
||||
};
|
||||
|
||||
// Get our cursor's most current prompt.
|
||||
const prompt_pin: terminal.Pin = prompt_pin: {
|
||||
var it = screen.cursor.page_pin.promptIterator(
|
||||
.left_up,
|
||||
null,
|
||||
);
|
||||
break :prompt_pin it.next() orelse {
|
||||
// This shouldn't be possible because we asserted we're at
|
||||
// a prompt above, so we MUST find some prompt in a left_up search.
|
||||
log.warn("cursor is at prompt but no prompt found", .{});
|
||||
if (comptime std.debug.runtime_safety) unreachable;
|
||||
return false;
|
||||
};
|
||||
for (0..@abs(path.x)) |_| {
|
||||
self.queueIo(.{ .write_stable = arrow }, .locked);
|
||||
}
|
||||
};
|
||||
|
||||
// If our mouse click is before the prompt, we don't move.
|
||||
// We DO ALLOW clicks AFTER the prompt, specifically with Kitty's
|
||||
// click_events=1 since we rely on the shell to validate out of
|
||||
// bounds clicks. This matches Kitty's logic as best I can tell.
|
||||
if (click_pin.before(prompt_pin)) return false;
|
||||
|
||||
// At this point we've established:
|
||||
// - Screen supports prompt clicks
|
||||
// - Cursor is at a prompt
|
||||
// - Click is at or below our prompt
|
||||
switch (screen.semantic_prompt.click) {
|
||||
// Guarded at the start of this function
|
||||
.none => unreachable,
|
||||
|
||||
.click_events => {
|
||||
// For the event, we always send a left-click press event.
|
||||
// This matches what Kitty sends.
|
||||
var data: termio.Message.WriteReq.Small.Array = undefined;
|
||||
const resp = try std.fmt.bufPrint(
|
||||
&data,
|
||||
"\x1B[<0;{d};{d}M",
|
||||
.{ pos_vp.x + 1, pos_vp.y + 1 },
|
||||
);
|
||||
|
||||
// Not that noisy since this only happens on prompt clicks.
|
||||
log.debug(
|
||||
"sending click_events=1 event=ESC{s}",
|
||||
.{resp[1..]},
|
||||
);
|
||||
|
||||
// Ask our IO thread to write the data
|
||||
self.queueIo(.{ .write_small = .{
|
||||
.data = data,
|
||||
.len = @intCast(resp.len),
|
||||
} }, .locked);
|
||||
},
|
||||
|
||||
.cl => {
|
||||
const left_arrow = if (t.modes.get(.cursor_keys)) "\x1bOD" else "\x1b[D";
|
||||
const right_arrow = if (t.modes.get(.cursor_keys)) "\x1bOC" else "\x1b[C";
|
||||
|
||||
const move = screen.promptClickMove(click_pin);
|
||||
for (0..move.left) |_| {
|
||||
self.queueIo(
|
||||
.{ .write_stable = left_arrow },
|
||||
.locked,
|
||||
);
|
||||
}
|
||||
for (0..move.right) |_| {
|
||||
self.queueIo(
|
||||
.{ .write_stable = right_arrow },
|
||||
.locked,
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const Link = struct {
|
||||
|
||||
@@ -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
|
||||
@@ -1133,15 +1133,18 @@ pub const Inspector = struct {
|
||||
yoff: f64,
|
||||
mods: input.ScrollMods,
|
||||
) void {
|
||||
_ = mods;
|
||||
|
||||
self.queueRender();
|
||||
cimgui.c.ImGui_SetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
|
||||
|
||||
// For precision scrolling (trackpads), the values are in pixels which
|
||||
// scroll way too fast. Scale them down to approximate discrete wheel
|
||||
// notches. imgui expects 1.0 to scroll ~5 lines of text.
|
||||
const scale: f64 = if (mods.precision) 0.1 else 1.0;
|
||||
cimgui.c.ImGuiIO_AddMouseWheelEvent(
|
||||
io,
|
||||
@floatCast(xoff),
|
||||
@floatCast(yoff),
|
||||
@floatCast(xoff * scale),
|
||||
@floatCast(yoff * scale),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1202,10 +1205,11 @@ pub const Inspector = struct {
|
||||
// Determine our delta time
|
||||
const now = try std.time.Instant.now();
|
||||
io.DeltaTime = if (self.instant) |prev| delta: {
|
||||
const since_ns = now.since(prev);
|
||||
const since_s: f32 = @floatFromInt(since_ns / std.time.ns_per_s);
|
||||
const since_ns: f64 = @floatFromInt(now.since(prev));
|
||||
const ns_per_s: f64 = @floatFromInt(std.time.ns_per_s);
|
||||
const since_s: f32 = @floatCast(since_ns / ns_per_s);
|
||||
break :delta @max(0.00001, since_s);
|
||||
} else (1 / 60);
|
||||
} else (1.0 / 60.0);
|
||||
self.instant = now;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -213,6 +213,11 @@ pub const Application = extern struct {
|
||||
/// Providers for loading custom stylesheets defined by user
|
||||
custom_css_providers: std.ArrayListUnmanaged(*gtk.CssProvider) = .empty,
|
||||
|
||||
/// A copy of the LANG environment variable that was provided to Ghostty
|
||||
/// by the system. If this is null, the LANG environment variable did
|
||||
/// not exist in Ghostty's environment variable.
|
||||
saved_language: ?[:0]const u8 = null,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
@@ -249,15 +254,6 @@ pub const Application = extern struct {
|
||||
gtk_version.logVersion();
|
||||
adw_version.logVersion();
|
||||
|
||||
// Set gettext global domain to be our app so that our unqualified
|
||||
// translations map to our translations.
|
||||
internal_os.i18n.initGlobalDomain() catch |err| {
|
||||
// Failures shuldn't stop application startup. Our app may
|
||||
// not translate correctly but it should still work. In the
|
||||
// future we may want to add this to the GUI to show.
|
||||
log.warn("i18n initialization failed error={}", .{err});
|
||||
};
|
||||
|
||||
// Load our configuration.
|
||||
var config = CoreConfig.load(alloc) catch |err| err: {
|
||||
// If we fail to load the configuration, then we should log
|
||||
@@ -275,6 +271,27 @@ pub const Application = extern struct {
|
||||
};
|
||||
defer config.deinit();
|
||||
|
||||
const saved_language: ?[:0]const u8 = saved_language: {
|
||||
const old_language = old_language: {
|
||||
const result = (internal_os.getenv(alloc, "LANG") catch break :old_language null) orelse break :old_language null;
|
||||
defer result.deinit(alloc);
|
||||
break :old_language alloc.dupeZ(u8, result.value) catch break :old_language null;
|
||||
};
|
||||
|
||||
if (config.language) |language| _ = internal_os.setenv("LANG", language);
|
||||
|
||||
break :saved_language old_language;
|
||||
};
|
||||
|
||||
// Set gettext global domain to be our app so that our unqualified
|
||||
// translations map to our translations.
|
||||
internal_os.i18n.initGlobalDomain() catch |err| {
|
||||
// Failures shuldn't stop application startup. Our app may
|
||||
// not translate correctly but it should still work. In the
|
||||
// future we may want to add this to the GUI to show.
|
||||
log.warn("i18n initialization failed error={}", .{err});
|
||||
};
|
||||
|
||||
// Setup our GTK init env vars
|
||||
setGtkEnv(&config) catch |err| switch (err) {
|
||||
error.NoSpaceLeft => {
|
||||
@@ -374,7 +391,7 @@ pub const Application = extern struct {
|
||||
// Setup our private state. More setup is done in the init
|
||||
// callback that GObject calls, but we can't pass this data through
|
||||
// to there (and we don't need it there directly) so this is here.
|
||||
const priv = self.private();
|
||||
const priv: *Private = self.private();
|
||||
priv.* = .{
|
||||
.rt_app = rt_app,
|
||||
.core_app = core_app,
|
||||
@@ -383,6 +400,7 @@ pub const Application = extern struct {
|
||||
.css_provider = css_provider,
|
||||
.custom_css_providers = .empty,
|
||||
.global_shortcuts = gobject.ext.newInstance(GlobalShortcuts, .{}),
|
||||
.saved_language = saved_language,
|
||||
};
|
||||
|
||||
// Signals
|
||||
@@ -415,11 +433,12 @@ pub const Application = extern struct {
|
||||
/// ensures that our memory is cleaned up properly.
|
||||
pub fn deinit(self: *Self) void {
|
||||
const alloc = self.allocator();
|
||||
const priv = self.private();
|
||||
const priv: *Private = self.private();
|
||||
priv.config.unref();
|
||||
priv.winproto.deinit(alloc);
|
||||
priv.global_shortcuts.unref();
|
||||
if (priv.transient_cgroup_base) |base| alloc.free(base);
|
||||
if (priv.saved_language) |language| alloc.free(language);
|
||||
if (gdk.Display.getDefault()) |display| {
|
||||
gtk.StyleContext.removeProviderForDisplay(
|
||||
display,
|
||||
@@ -445,6 +464,12 @@ pub const Application = extern struct {
|
||||
return self.private().core_app.alloc;
|
||||
}
|
||||
|
||||
/// Get the original language that Ghostty was launched with. This returns a
|
||||
/// pointer to internal memory so it must be copied by callers.
|
||||
pub fn savedLanguage(self: *Self) ?[:0]const u8 {
|
||||
return self.private().saved_language;
|
||||
}
|
||||
|
||||
/// Run the application. This is a replacement for `gio.Application.run`
|
||||
/// because we want more tight control over our event loop so we can
|
||||
/// integrate it with libghostty.
|
||||
@@ -733,6 +758,7 @@ pub const Application = extern struct {
|
||||
.toggle_split_zoom => return Action.toggleSplitZoom(target),
|
||||
.show_on_screen_keyboard => return Action.showOnScreenKeyboard(target),
|
||||
.command_finished => return Action.commandFinished(target, value),
|
||||
.readonly => return Action.setReadonly(target, value),
|
||||
|
||||
.start_search => Action.startSearch(target, value),
|
||||
.end_search => Action.endSearch(target),
|
||||
@@ -753,7 +779,6 @@ pub const Application = extern struct {
|
||||
.check_for_updates,
|
||||
.undo,
|
||||
.redo,
|
||||
.readonly,
|
||||
=> {
|
||||
log.warn("unimplemented action={}", .{action});
|
||||
return false;
|
||||
@@ -2400,10 +2425,14 @@ const Action = struct {
|
||||
SplitTree,
|
||||
surface.as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn("surface is not in a split tree, ignoring goto_split", .{});
|
||||
log.warn("surface is not in a split tree, ignoring resize_split", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
// If the tree has no splits (only one leaf), this action is not performable.
|
||||
// This allows the key event to pass through to the terminal.
|
||||
if (!tree.getIsSplit()) return false;
|
||||
|
||||
return tree.resize(
|
||||
switch (value.direction) {
|
||||
.up => .up,
|
||||
@@ -2550,6 +2579,18 @@ const Action = struct {
|
||||
.surface => |core| {
|
||||
// TODO: pass surface ID when we have that
|
||||
const surface = core.rt_surface.surface;
|
||||
const tree = ext.getAncestor(
|
||||
SplitTree,
|
||||
surface.as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn("surface is not in a split tree, ignoring toggle_split_zoom", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
// If the tree has no splits (only one leaf), this action is not performable.
|
||||
// This allows the key event to pass through to the terminal.
|
||||
if (!tree.getIsSplit()) return false;
|
||||
|
||||
return surface.as(gtk.Widget).activateAction("split-tree.zoom", null) != 0;
|
||||
},
|
||||
}
|
||||
@@ -2662,6 +2703,15 @@ const Action = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setReadonly(target: apprt.Target, value: apprt.Action.Value(.readonly)) bool {
|
||||
switch (target) {
|
||||
.app => return false,
|
||||
.surface => |surface| {
|
||||
return surface.rt_surface.gobj().setReadonly(value);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keySequence(target: apprt.Target, value: apprt.Action.Value(.key_sequence)) bool {
|
||||
switch (target) {
|
||||
.app => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -131,21 +137,17 @@ pub const ImguiWidget = extern struct {
|
||||
|
||||
/// Initialize the frame. Expects that the context is already current.
|
||||
fn newFrame(self: *Self) void {
|
||||
// If we can't determine the time since the last frame we default to
|
||||
// 1/60th of a second.
|
||||
const default_delta_time = 1 / 60;
|
||||
|
||||
const priv = self.private();
|
||||
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
|
||||
|
||||
// Determine our delta time
|
||||
const now = std.time.Instant.now() catch unreachable;
|
||||
io.DeltaTime = if (priv.instant) |prev| delta: {
|
||||
const since_ns = now.since(prev);
|
||||
const since_s: f32 = @floatFromInt(since_ns / std.time.ns_per_s);
|
||||
const since_ns: f64 = @floatFromInt(now.since(prev));
|
||||
const ns_per_s: f64 = @floatFromInt(std.time.ns_per_s);
|
||||
const since_s: f32 = @floatCast(since_ns / ns_per_s);
|
||||
break :delta @max(0.00001, since_s);
|
||||
} else default_delta_time;
|
||||
} else (1.0 / 60.0);
|
||||
|
||||
priv.instant = now;
|
||||
}
|
||||
@@ -235,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();
|
||||
}
|
||||
@@ -269,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.
|
||||
@@ -415,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);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
|
||||
@@ -561,7 +561,7 @@ pub const SplitTree = extern struct {
|
||||
));
|
||||
}
|
||||
|
||||
fn getIsSplit(self: *Self) bool {
|
||||
pub fn getIsSplit(self: *Self) bool {
|
||||
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
|
||||
if (tree.isEmpty()) return false;
|
||||
|
||||
|
||||
@@ -400,6 +400,25 @@ pub const Surface = extern struct {
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const readonly = struct {
|
||||
pub const name = "readonly";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.default = false,
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.getter = getReadonly,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
pub const signals = struct {
|
||||
@@ -678,6 +697,13 @@ pub const Surface = extern struct {
|
||||
/// Whether primary paste (middle-click paste) is enabled.
|
||||
gtk_enable_primary_paste: bool = true,
|
||||
|
||||
/// How much pending horizontal scroll do we have?
|
||||
pending_horizontal_scroll: f64 = 0.0,
|
||||
|
||||
/// Timer to reset the amount of horizontal scroll if the user
|
||||
/// stops scrolling.
|
||||
pending_horizontal_scroll_reset: ?c_uint = null,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
@@ -1106,6 +1132,20 @@ pub const Surface = extern struct {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Get the readonly state from the core surface.
|
||||
pub fn getReadonly(self: *Self) bool {
|
||||
const priv: *Private = self.private();
|
||||
const surface = priv.core_surface orelse return false;
|
||||
return surface.readonly;
|
||||
}
|
||||
|
||||
/// Notify anyone interested that the readonly status has changed.
|
||||
pub fn setReadonly(self: *Self, _: apprt.Action.Value(.readonly)) bool {
|
||||
self.as(gobject.Object).notifyByPspec(properties.readonly.impl.param_spec);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Key press event (press or release).
|
||||
///
|
||||
/// At a high level, we want to construct an `input.KeyEvent` and
|
||||
@@ -1555,10 +1595,17 @@ pub const Surface = extern struct {
|
||||
}
|
||||
|
||||
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
|
||||
const alloc = Application.default().allocator();
|
||||
const app = Application.default();
|
||||
const alloc = app.allocator();
|
||||
var env = try internal_os.getEnvMap(alloc);
|
||||
errdefer env.deinit();
|
||||
|
||||
if (app.savedLanguage()) |language| {
|
||||
try env.put("LANG", language);
|
||||
} else {
|
||||
env.remove("LANG");
|
||||
}
|
||||
|
||||
// Don't leak these GTK environment variables to child processes.
|
||||
env.remove("GDK_DEBUG");
|
||||
env.remove("GDK_DISABLE");
|
||||
@@ -1847,6 +1894,13 @@ pub const Surface = extern struct {
|
||||
priv.idle_rechild = null;
|
||||
}
|
||||
|
||||
if (priv.pending_horizontal_scroll_reset) |v| {
|
||||
if (glib.Source.remove(v) == 0) {
|
||||
log.warn("unable to remove pending horizontal scroll reset source", .{});
|
||||
}
|
||||
priv.pending_horizontal_scroll_reset = null;
|
||||
}
|
||||
|
||||
// This works around a GTK double-free bug where if you bind
|
||||
// to a top-level template child, it frees twice if the widget is
|
||||
// also the root child of the template. By unsetting the child here,
|
||||
@@ -2846,27 +2900,27 @@ pub const Surface = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn ecMouseScrollPrecisionBegin(
|
||||
fn ecMouseScrollVerticalPrecisionBegin(
|
||||
_: *gtk.EventControllerScroll,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
self.private().precision_scroll = true;
|
||||
}
|
||||
|
||||
fn ecMouseScrollPrecisionEnd(
|
||||
fn ecMouseScrollVerticalPrecisionEnd(
|
||||
_: *gtk.EventControllerScroll,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
self.private().precision_scroll = false;
|
||||
}
|
||||
|
||||
fn ecMouseScroll(
|
||||
fn ecMouseScrollVertical(
|
||||
_: *gtk.EventControllerScroll,
|
||||
x: f64,
|
||||
y: f64,
|
||||
self: *Self,
|
||||
) callconv(.c) c_int {
|
||||
const priv = self.private();
|
||||
const priv: *Private = self.private();
|
||||
const surface = priv.core_surface orelse return 0;
|
||||
|
||||
// Multiply precision scrolls by 10 to get a better response from
|
||||
@@ -2893,6 +2947,57 @@ pub const Surface = extern struct {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn ecMouseScrollHorizontal(
|
||||
ec: *gtk.EventControllerScroll,
|
||||
x: f64,
|
||||
_: f64,
|
||||
self: *Self,
|
||||
) callconv(.c) c_int {
|
||||
const priv: *Private = self.private();
|
||||
|
||||
switch (ec.getUnit()) {
|
||||
.surface => {},
|
||||
.wheel => return @intFromBool(false),
|
||||
else => return @intFromBool(false),
|
||||
}
|
||||
|
||||
priv.pending_horizontal_scroll += x;
|
||||
|
||||
if (@abs(priv.pending_horizontal_scroll) < 120) {
|
||||
if (priv.pending_horizontal_scroll_reset) |v| {
|
||||
_ = glib.Source.remove(v);
|
||||
priv.pending_horizontal_scroll_reset = null;
|
||||
}
|
||||
priv.pending_horizontal_scroll_reset = glib.timeoutAdd(500, ecMouseScrollHorizontalReset, self);
|
||||
return @intFromBool(true);
|
||||
}
|
||||
|
||||
_ = self.as(gtk.Widget).activateAction(
|
||||
if (priv.pending_horizontal_scroll < 0.0)
|
||||
"tab.next-page"
|
||||
else
|
||||
"tab.previous-page",
|
||||
null,
|
||||
);
|
||||
|
||||
if (priv.pending_horizontal_scroll_reset) |v| {
|
||||
_ = glib.Source.remove(v);
|
||||
priv.pending_horizontal_scroll_reset = null;
|
||||
}
|
||||
|
||||
priv.pending_horizontal_scroll = 0.0;
|
||||
|
||||
return @intFromBool(true);
|
||||
}
|
||||
|
||||
fn ecMouseScrollHorizontalReset(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *Self = @ptrCast(@alignCast(ud orelse return @intFromBool(glib.SOURCE_REMOVE)));
|
||||
const priv: *Private = self.private();
|
||||
priv.pending_horizontal_scroll = 0.0;
|
||||
priv.pending_horizontal_scroll_reset = null;
|
||||
return @intFromBool(glib.SOURCE_REMOVE);
|
||||
}
|
||||
|
||||
fn imPreeditStart(
|
||||
_: *gtk.IMMulticontext,
|
||||
self: *Self,
|
||||
@@ -3431,9 +3536,10 @@ pub const Surface = extern struct {
|
||||
class.bindTemplateCallback("mouse_up", &gcMouseUp);
|
||||
class.bindTemplateCallback("mouse_motion", &ecMouseMotion);
|
||||
class.bindTemplateCallback("mouse_leave", &ecMouseLeave);
|
||||
class.bindTemplateCallback("scroll", &ecMouseScroll);
|
||||
class.bindTemplateCallback("scroll_begin", &ecMouseScrollPrecisionBegin);
|
||||
class.bindTemplateCallback("scroll_end", &ecMouseScrollPrecisionEnd);
|
||||
class.bindTemplateCallback("scroll_vertical", &ecMouseScrollVertical);
|
||||
class.bindTemplateCallback("scroll_vertical_begin", &ecMouseScrollVerticalPrecisionBegin);
|
||||
class.bindTemplateCallback("scroll_vertical_end", &ecMouseScrollVerticalPrecisionEnd);
|
||||
class.bindTemplateCallback("scroll_horizontal", &ecMouseScrollHorizontal);
|
||||
class.bindTemplateCallback("drop", &dtDrop);
|
||||
class.bindTemplateCallback("gl_realize", &glareaRealize);
|
||||
class.bindTemplateCallback("gl_unrealize", &glareaUnrealize);
|
||||
@@ -3480,6 +3586,7 @@ pub const Surface = extern struct {
|
||||
properties.@"title-override".impl,
|
||||
properties.zoom.impl,
|
||||
properties.@"is-split".impl,
|
||||
properties.readonly.impl,
|
||||
|
||||
// For Gtk.Scrollable
|
||||
properties.hadjustment.impl,
|
||||
|
||||
@@ -202,6 +202,8 @@ pub const Tab = extern struct {
|
||||
const actions = [_]ext.actions.Action(Self){
|
||||
.init("close", actionClose, s_param_type),
|
||||
.init("ring-bell", actionRingBell, null),
|
||||
.init("next-page", actionNextPage, null),
|
||||
.init("previous-page", actionPreviousPage, null),
|
||||
};
|
||||
|
||||
_ = ext.actions.addAsGroup(Self, self, "tab", &actions);
|
||||
@@ -235,12 +237,17 @@ pub const Tab = extern struct {
|
||||
return tree.getNeedsConfirmQuit();
|
||||
}
|
||||
|
||||
/// Get the tab page holding this tab, if any.
|
||||
fn getTabPage(self: *Self) ?*adw.TabPage {
|
||||
const tab_view = ext.getAncestor(
|
||||
/// Get the tab view holding this tab, if any.
|
||||
fn getTabView(self: *Self) ?*adw.TabView {
|
||||
return ext.getAncestor(
|
||||
adw.TabView,
|
||||
self.as(gtk.Widget),
|
||||
) orelse return null;
|
||||
);
|
||||
}
|
||||
|
||||
/// Get the tab page holding this tab, if any.
|
||||
fn getTabPage(self: *Self) ?*adw.TabPage {
|
||||
const tab_view = self.getTabView() orelse return null;
|
||||
return tab_view.getPage(self.as(gtk.Widget));
|
||||
}
|
||||
|
||||
@@ -325,11 +332,7 @@ pub const Tab = extern struct {
|
||||
var str: ?[*:0]const u8 = null;
|
||||
param.get("&s", &str);
|
||||
|
||||
const tab_view = ext.getAncestor(
|
||||
adw.TabView,
|
||||
self.as(gtk.Widget),
|
||||
) orelse return;
|
||||
|
||||
const tab_view = self.getTabView() orelse return;
|
||||
const page = tab_view.getPage(self.as(gtk.Widget));
|
||||
|
||||
const mode = std.meta.stringToEnum(
|
||||
@@ -372,6 +375,26 @@ pub const Tab = extern struct {
|
||||
page.setNeedsAttention(@intFromBool(true));
|
||||
}
|
||||
|
||||
/// Select the next tab page.
|
||||
fn actionNextPage(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const tab_view = self.getTabView() orelse return;
|
||||
_ = tab_view.selectNextPage();
|
||||
}
|
||||
|
||||
/// Select the previous tab page.
|
||||
fn actionPreviousPage(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const tab_view = self.getTabView() orelse return;
|
||||
_ = tab_view.selectPreviousPage();
|
||||
}
|
||||
|
||||
fn closureComputedTitle(
|
||||
_: *Self,
|
||||
config_: ?*Config,
|
||||
|
||||
@@ -134,6 +134,16 @@ label.resize-overlay {
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.surface .readonly_overlay {
|
||||
/* Should be the equivalent of the following SwiftUI color: */
|
||||
/* Color(hue: 0.08, saturation: 0.5, brightness: 0.8) */
|
||||
color: hsl(25 50 75);
|
||||
padding: 8px 8px 8px 8px;
|
||||
margin: 8px 8px 8px 8px;
|
||||
border-radius: 6px 6px 6px 6px;
|
||||
outline-style: solid;
|
||||
outline-width: 1px;
|
||||
}
|
||||
/*
|
||||
* Command Palette
|
||||
*/
|
||||
|
||||
@@ -53,10 +53,15 @@ Overlay terminal_page {
|
||||
}
|
||||
|
||||
EventControllerScroll {
|
||||
scroll => $scroll();
|
||||
scroll-begin => $scroll_begin();
|
||||
scroll-end => $scroll_end();
|
||||
flags: both_axes;
|
||||
scroll => $scroll_vertical();
|
||||
scroll-begin => $scroll_vertical_begin();
|
||||
scroll-end => $scroll_vertical_end();
|
||||
flags: vertical;
|
||||
}
|
||||
|
||||
EventControllerScroll {
|
||||
scroll => $scroll_horizontal();
|
||||
flags: horizontal;
|
||||
}
|
||||
|
||||
EventControllerMotion {
|
||||
@@ -71,6 +76,39 @@ Overlay terminal_page {
|
||||
}
|
||||
};
|
||||
|
||||
[overlay]
|
||||
Revealer {
|
||||
reveal-child: bind template.readonly;
|
||||
transition-type: crossfade;
|
||||
transition-duration: 500;
|
||||
// Revealers take up the full size, we need this to not capture events.
|
||||
can-focus: false;
|
||||
can-target: false;
|
||||
focusable: false;
|
||||
|
||||
Box readonly_overlay {
|
||||
styles [
|
||||
"readonly_overlay",
|
||||
]
|
||||
|
||||
// TODO: the tooltip doesn't actually work, but keep it here for now so
|
||||
// that we can get the tooltip text translated.
|
||||
has-tooltip: true;
|
||||
tooltip-text: _("This terminal is in read-only mode. You can still view, select, and scroll through the content, but no input events will be sent to the running application.");
|
||||
halign: end;
|
||||
valign: start;
|
||||
spacing: 6;
|
||||
|
||||
Image {
|
||||
icon-name: "changes-prevent-symbolic";
|
||||
}
|
||||
|
||||
Label {
|
||||
label: _("Read-only");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
ProgressBar progress_bar_overlay {
|
||||
styles [
|
||||
|
||||
@@ -19,14 +19,22 @@ branch: []const u8,
|
||||
pub fn detect(b: *std.Build) !Version {
|
||||
// Execute a bunch of git commands to determine the automatic version.
|
||||
var code: u8 = 0;
|
||||
const branch: []const u8 = b.runAllowFail(
|
||||
&[_][]const u8{ "git", "-C", b.build_root.path orelse ".", "rev-parse", "--abbrev-ref", "HEAD" },
|
||||
&code,
|
||||
.Ignore,
|
||||
) catch |err| switch (err) {
|
||||
error.FileNotFound => return error.GitNotFound,
|
||||
error.ExitCodeFailure => return error.GitNotRepository,
|
||||
else => return err,
|
||||
const branch: []const u8 = b: {
|
||||
const tmp: []u8 = b.runAllowFail(
|
||||
&[_][]const u8{ "git", "-C", b.build_root.path orelse ".", "rev-parse", "--abbrev-ref", "HEAD" },
|
||||
&code,
|
||||
.Ignore,
|
||||
) catch |err| switch (err) {
|
||||
error.FileNotFound => return error.GitNotFound,
|
||||
error.ExitCodeFailure => return error.GitNotRepository,
|
||||
else => return err,
|
||||
};
|
||||
// Replace any '/' with '-' as including slashes will mess up building
|
||||
// the dist tarball - the tarball uses the branch as part of the
|
||||
// name and including slashes means that the tarball will end up in
|
||||
// subdirectories instead of where it's supposed to be.
|
||||
std.mem.replaceScalar(u8, tmp, '/', '-');
|
||||
break :b tmp;
|
||||
};
|
||||
|
||||
const short_hash = short_hash: {
|
||||
|
||||
@@ -94,6 +94,27 @@ pub const compatibility = std.StaticStringMap(
|
||||
.{ "macos-dock-drop-behavior", compatMacOSDockDropBehavior },
|
||||
});
|
||||
|
||||
/// Set Ghostty's graphical user interface language to a language other than the
|
||||
/// system default language. The language must be fully specified, including the
|
||||
/// encoding. For example:
|
||||
///
|
||||
/// language = de_DE.UTF-8
|
||||
///
|
||||
/// will force the strings in Ghostty's graphical user interface to be in German
|
||||
/// rather than the system default.
|
||||
///
|
||||
/// This will not affect the language used by programs run _within_ Ghostty.
|
||||
/// Those will continue to use the default system language. There are also many
|
||||
/// non-GUI elements in Ghostty that are not translated - this setting will have
|
||||
/// no effect on those.
|
||||
///
|
||||
/// Warning: This setting cannot be reloaded at runtime. To change the language
|
||||
/// you must fully restart Ghostty.
|
||||
///
|
||||
/// GTK only.
|
||||
/// Available since 1.3.0.
|
||||
language: ?[:0]const u8 = null,
|
||||
|
||||
/// The font families to use.
|
||||
///
|
||||
/// You can generate the list of valid values using the CLI:
|
||||
|
||||
@@ -26,7 +26,7 @@ pub const regex =
|
||||
"(?:" ++ url_schemes ++
|
||||
\\)(?:
|
||||
++ ipv6_url_pattern ++
|
||||
\\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?<![,.])|(?:\.\.\/|\.\/|\/)(?:(?=[\w\-.~:\/?#@!$&*+,;=%]*\.)[\w\-.~:\/?#@!$&*+,;=%]+(?: [\w\-.~:\/?#@!$&*+,;=%]*[\/.])*(?: +(?= *$))?|(?![\w\-.~:\/?#@!$&*+,;=%]*\.)[\w\-.~:\/?#@!$&*+,;=%]+(?: [\w\-.~:\/?#@!$&*+,;=%]+)*(?: +(?= *$))?)
|
||||
\\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?<![,.])|(?:\.\.\/|\.\/|(?<!\w)\/)(?:(?=[\w\-.~:\/?#@!$&*+,;=%]*\.)[\w\-.~:\/?#@!$&*+,;=%]+(?: [\w\-.~:\/?#@!$&*+,;=%]*[\/.])*(?: +(?= *$))?|(?![\w\-.~:\/?#@!$&*+,;=%]*\.)[\w\-.~:\/?#@!$&*+,;=%]+(?: [\w\-.~:\/?#@!$&*+,;=%]+)*(?: +(?= *$))?)|[\w][\w\-.]*\/(?=[\w\-.~:\/?#@!$&*+,;=%]*\.)[\w\-.~:\/?#@!$&*+,;=%]+(?: [\w\-.~:\/?#@!$&*+,;=%]*[\/.])*(?: +(?= *$))?
|
||||
;
|
||||
const url_schemes =
|
||||
\\https?://|mailto:|ftp://|file:|ssh:|git://|ssh://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:
|
||||
@@ -270,6 +270,27 @@ test "url regex" {
|
||||
.input = "/tmp/test folder/file.txt",
|
||||
.expect = "/tmp/test folder/file.txt",
|
||||
},
|
||||
// Bare relative file paths (no ./ or ../ prefix)
|
||||
.{
|
||||
.input = "src/config/url.zig",
|
||||
.expect = "src/config/url.zig",
|
||||
},
|
||||
.{
|
||||
.input = "app/folder/file.rb:1",
|
||||
.expect = "app/folder/file.rb:1",
|
||||
},
|
||||
.{
|
||||
.input = "modified: src/config/url.zig",
|
||||
.expect = "src/config/url.zig",
|
||||
},
|
||||
.{
|
||||
.input = "lib/ghostty/terminal.zig:42:10",
|
||||
.expect = "lib/ghostty/terminal.zig:42:10",
|
||||
},
|
||||
.{
|
||||
.input = "some-pkg/src/file.txt more text",
|
||||
.expect = "some-pkg/src/file.txt",
|
||||
},
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
@@ -284,4 +305,17 @@ test "url regex" {
|
||||
const match = case.input[@intCast(reg.starts()[0])..@intCast(reg.ends()[0])];
|
||||
try testing.expectEqualStrings(case.expect, match);
|
||||
}
|
||||
|
||||
// Bare relative paths without any dot should not match as file paths
|
||||
const no_match_cases = [_][]const u8{
|
||||
"input/output",
|
||||
"foo/bar",
|
||||
};
|
||||
for (no_match_cases) |input| {
|
||||
var result = re.search(input, .{});
|
||||
if (result) |*reg| {
|
||||
reg.deinit();
|
||||
return error.TestUnexpectedResult;
|
||||
} else |_| {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +170,19 @@ pub fn SplitTree(comptime V: type) type {
|
||||
return self.nodes.len == 0;
|
||||
}
|
||||
|
||||
/// Returns true if this tree has more than one split (i.e., the root
|
||||
/// is a split node). This is useful for determining if actions like
|
||||
/// resize_split or toggle_split_zoom are performable.
|
||||
pub fn isSplit(self: *const Self) bool {
|
||||
// An empty tree is not split.
|
||||
if (self.isEmpty()) return false;
|
||||
// The root node is at index 0. If it's a split, we have multiple splits.
|
||||
return switch (self.nodes[0]) {
|
||||
.split => true,
|
||||
.leaf => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// An iterator over all the views in the tree.
|
||||
pub fn iterator(
|
||||
self: *const Self,
|
||||
@@ -760,9 +773,9 @@ pub fn SplitTree(comptime V: type) type {
|
||||
/// Resize the nearest split matching the layout by the given ratio.
|
||||
/// Positive is right and down.
|
||||
///
|
||||
/// The ratio is a value between 0 and 1 representing the percentage
|
||||
/// to move the divider in the given direction. The percentage is
|
||||
/// of the entire grid size, not just the specific split size.
|
||||
/// The ratio is a signed delta representing the percentage to move
|
||||
/// the divider. The percentage is of the entire grid size, not just
|
||||
/// the specific split size.
|
||||
/// We use the entire grid size because that's what Ghostty's
|
||||
/// `resize_split` keybind does, because it maps to a general human
|
||||
/// understanding of moving a split relative to the entire window
|
||||
@@ -781,7 +794,7 @@ pub fn SplitTree(comptime V: type) type {
|
||||
layout: Split.Layout,
|
||||
ratio: f16,
|
||||
) Allocator.Error!Self {
|
||||
assert(ratio >= 0 and ratio <= 1);
|
||||
assert(ratio >= -1 and ratio <= 1);
|
||||
assert(!std.math.isNan(ratio));
|
||||
assert(!std.math.isInf(ratio));
|
||||
|
||||
@@ -1326,6 +1339,36 @@ const TestView = struct {
|
||||
}
|
||||
};
|
||||
|
||||
test "SplitTree: isSplit" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Empty tree should not be split
|
||||
var empty: TestTree = .empty;
|
||||
defer empty.deinit();
|
||||
try testing.expect(!empty.isSplit());
|
||||
|
||||
// Single node tree should not be split
|
||||
var v1: TestView = .{ .label = "A" };
|
||||
var single: TestTree = try TestTree.init(alloc, &v1);
|
||||
defer single.deinit();
|
||||
try testing.expect(!single.isSplit());
|
||||
|
||||
// Split tree should be split
|
||||
var v2: TestView = .{ .label = "B" };
|
||||
var tree2: TestTree = try TestTree.init(alloc, &v2);
|
||||
defer tree2.deinit();
|
||||
var split = try single.split(
|
||||
alloc,
|
||||
.root,
|
||||
.right,
|
||||
0.5,
|
||||
&tree2,
|
||||
);
|
||||
defer split.deinit();
|
||||
try testing.expect(split.isSplit());
|
||||
}
|
||||
|
||||
test "SplitTree: empty tree" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
@@ -2007,6 +2050,32 @@ test "SplitTree: resize" {
|
||||
\\
|
||||
);
|
||||
}
|
||||
|
||||
// Resize the other direction (negative ratio)
|
||||
{
|
||||
var resized = try split.resize(
|
||||
alloc,
|
||||
at: {
|
||||
var it = split.iterator();
|
||||
break :at while (it.next()) |entry| {
|
||||
if (std.mem.eql(u8, entry.view.label, "B")) {
|
||||
break entry.handle;
|
||||
}
|
||||
} else return error.NotFound;
|
||||
},
|
||||
.horizontal, // resize left
|
||||
-0.25,
|
||||
);
|
||||
defer resized.deinit();
|
||||
const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(resized, .formatDiagram)});
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqualStrings(str,
|
||||
\\+---++-------------+
|
||||
\\| A || B |
|
||||
\\+---++-------------+
|
||||
\\
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "SplitTree: clone empty tree" {
|
||||
|
||||
@@ -20,6 +20,7 @@ const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const testing = std.testing;
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
const tripwire = @import("../tripwire.zig");
|
||||
|
||||
const log = std.log.scoped(.atlas);
|
||||
|
||||
@@ -91,7 +92,15 @@ pub const Region = extern struct {
|
||||
/// TODO: figure out optimal prealloc based on real world usage
|
||||
const node_prealloc: usize = 64;
|
||||
|
||||
pub const init_tw = tripwire.module(enum {
|
||||
alloc_data,
|
||||
alloc_nodes,
|
||||
}, init);
|
||||
|
||||
pub fn init(alloc: Allocator, size: u32, format: Format) Allocator.Error!Atlas {
|
||||
const tw = init_tw;
|
||||
|
||||
try tw.check(.alloc_data);
|
||||
var result = Atlas{
|
||||
.data = try alloc.alloc(u8, size * size * format.depth()),
|
||||
.size = size,
|
||||
@@ -101,6 +110,7 @@ pub fn init(alloc: Allocator, size: u32, format: Format) Allocator.Error!Atlas {
|
||||
errdefer result.deinit(alloc);
|
||||
|
||||
// Prealloc some nodes.
|
||||
try tw.check(.alloc_nodes);
|
||||
result.nodes = try .initCapacity(alloc, node_prealloc);
|
||||
|
||||
// This sets up our initial state
|
||||
@@ -115,6 +125,10 @@ pub fn deinit(self: *Atlas, alloc: Allocator) void {
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
pub const reserve_tw = tripwire.module(enum {
|
||||
insert_node,
|
||||
}, reserve);
|
||||
|
||||
/// Reserve a region within the atlas with the given width and height.
|
||||
///
|
||||
/// May allocate to add a new rectangle into the internal list of rectangles.
|
||||
@@ -125,6 +139,8 @@ pub fn reserve(
|
||||
width: u32,
|
||||
height: u32,
|
||||
) (Allocator.Error || Error)!Region {
|
||||
const tw = reserve_tw;
|
||||
|
||||
// x, y are populated within :best_idx below
|
||||
var region: Region = .{ .x = 0, .y = 0, .width = width, .height = height };
|
||||
|
||||
@@ -162,11 +178,13 @@ pub fn reserve(
|
||||
};
|
||||
|
||||
// Insert our new node for this rectangle at the exact best index
|
||||
try tw.check(.insert_node);
|
||||
try self.nodes.insert(alloc, best_idx, .{
|
||||
.x = region.x,
|
||||
.y = region.y + height,
|
||||
.width = width,
|
||||
});
|
||||
errdefer comptime unreachable;
|
||||
|
||||
// Optimize our rectangles
|
||||
var i: usize = best_idx + 1;
|
||||
@@ -287,15 +305,24 @@ pub fn setFromLarger(
|
||||
_ = self.modified.fetchAdd(1, .monotonic);
|
||||
}
|
||||
|
||||
pub const grow_tw = tripwire.module(enum {
|
||||
ensure_node_capacity,
|
||||
alloc_data,
|
||||
}, grow);
|
||||
|
||||
// Grow the texture to the new size, preserving all previously written data.
|
||||
pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void {
|
||||
const tw = grow_tw;
|
||||
|
||||
assert(size_new >= self.size);
|
||||
if (size_new == self.size) return;
|
||||
|
||||
// We reserve space ahead of time for the new node, so that we
|
||||
// won't have to handle any errors after allocating our new data.
|
||||
try tw.check(.ensure_node_capacity);
|
||||
try self.nodes.ensureUnusedCapacity(alloc, 1);
|
||||
|
||||
try tw.check(.alloc_data);
|
||||
const data_new = try alloc.alloc(
|
||||
u8,
|
||||
size_new * size_new * self.format.depth(),
|
||||
@@ -355,7 +382,7 @@ pub fn clear(self: *Atlas) void {
|
||||
/// swapped because PPM expects RGB. This would be
|
||||
/// easy enough to fix so next time someone needs
|
||||
/// to debug a color atlas they should fix it.
|
||||
pub fn dump(self: Atlas, writer: *std.Io.Writer) !void {
|
||||
pub fn dump(self: Atlas, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||
try writer.print(
|
||||
\\P{c}
|
||||
\\{d} {d}
|
||||
@@ -795,3 +822,68 @@ test "grow OOM" {
|
||||
try testing.expectEqual(@as(u8, 3), atlas.data[9]);
|
||||
try testing.expectEqual(@as(u8, 4), atlas.data[10]);
|
||||
}
|
||||
|
||||
test "init error" {
|
||||
// Test every failure point in `init` and ensure that we don't
|
||||
// leak memory (testing.allocator verifies) since we're exiting early.
|
||||
for (std.meta.tags(init_tw.FailPoint)) |tag| {
|
||||
const tw = init_tw;
|
||||
defer tw.end(.reset) catch unreachable;
|
||||
tw.errorAlways(tag, error.OutOfMemory);
|
||||
try testing.expectError(
|
||||
error.OutOfMemory,
|
||||
init(testing.allocator, 32, .grayscale),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "reserve error" {
|
||||
// Test every failure point in `reserve` and ensure that we don't
|
||||
// leak memory (testing.allocator verifies) since we're exiting early.
|
||||
for (std.meta.tags(reserve_tw.FailPoint)) |tag| {
|
||||
const tw = reserve_tw;
|
||||
defer tw.end(.reset) catch unreachable;
|
||||
|
||||
var atlas = try init(testing.allocator, 32, .grayscale);
|
||||
defer atlas.deinit(testing.allocator);
|
||||
|
||||
tw.errorAlways(tag, error.OutOfMemory);
|
||||
try testing.expectError(
|
||||
error.OutOfMemory,
|
||||
atlas.reserve(testing.allocator, 2, 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "grow error" {
|
||||
// Test every failure point in `grow` and ensure that we don't
|
||||
// leak memory (testing.allocator verifies) since we're exiting early.
|
||||
for (std.meta.tags(grow_tw.FailPoint)) |tag| {
|
||||
const tw = grow_tw;
|
||||
defer tw.end(.reset) catch unreachable;
|
||||
|
||||
var atlas = try init(testing.allocator, 4, .grayscale);
|
||||
defer atlas.deinit(testing.allocator);
|
||||
|
||||
// Write some data to verify it's preserved after failed grow
|
||||
const reg = try atlas.reserve(testing.allocator, 2, 2);
|
||||
atlas.set(reg, &[_]u8{ 1, 2, 3, 4 });
|
||||
|
||||
const old_modified = atlas.modified.load(.monotonic);
|
||||
const old_resized = atlas.resized.load(.monotonic);
|
||||
|
||||
tw.errorAlways(tag, error.OutOfMemory);
|
||||
try testing.expectError(
|
||||
error.OutOfMemory,
|
||||
atlas.grow(testing.allocator, atlas.size + 1),
|
||||
);
|
||||
|
||||
// Verify atlas state is unchanged after failed grow
|
||||
try testing.expectEqual(old_modified, atlas.modified.load(.monotonic));
|
||||
try testing.expectEqual(old_resized, atlas.resized.load(.monotonic));
|
||||
try testing.expectEqual(@as(u8, 1), atlas.data[5]);
|
||||
try testing.expectEqual(@as(u8, 2), atlas.data[6]);
|
||||
try testing.expectEqual(@as(u8, 3), atlas.data[9]);
|
||||
try testing.expectEqual(@as(u8, 4), atlas.data[10]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ const SharedGrid = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const tripwire = @import("../tripwire.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const renderer = @import("../renderer.zig");
|
||||
const font = @import("main.zig");
|
||||
@@ -61,6 +62,12 @@ metrics: Metrics,
|
||||
/// to review call sites to ensure they are using the lock correctly.
|
||||
lock: std.Thread.RwLock,
|
||||
|
||||
pub const init_tw = tripwire.module(enum {
|
||||
codepoints_capacity,
|
||||
glyphs_capacity,
|
||||
reload_metrics,
|
||||
}, init);
|
||||
|
||||
/// Initialize the grid.
|
||||
///
|
||||
/// The resolver must have a collection that supports deferred loading
|
||||
@@ -74,6 +81,8 @@ pub fn init(
|
||||
alloc: Allocator,
|
||||
resolver: CodepointResolver,
|
||||
) !SharedGrid {
|
||||
const tw = init_tw;
|
||||
|
||||
// We need to support loading options since we use the size data
|
||||
assert(resolver.collection.load_options != null);
|
||||
|
||||
@@ -92,10 +101,15 @@ pub fn init(
|
||||
|
||||
// We set an initial capacity that can fit a good number of characters.
|
||||
// This number was picked empirically based on my own terminal usage.
|
||||
try tw.check(.codepoints_capacity);
|
||||
try result.codepoints.ensureTotalCapacity(alloc, 128);
|
||||
errdefer result.codepoints.deinit(alloc);
|
||||
try tw.check(.glyphs_capacity);
|
||||
try result.glyphs.ensureTotalCapacity(alloc, 128);
|
||||
errdefer result.glyphs.deinit(alloc);
|
||||
|
||||
// Initialize our metrics.
|
||||
try tw.check(.reload_metrics);
|
||||
try result.reloadMetrics();
|
||||
|
||||
return result;
|
||||
@@ -232,6 +246,10 @@ pub fn renderCodepoint(
|
||||
return try self.renderGlyph(alloc, index, glyph_index, opts);
|
||||
}
|
||||
|
||||
pub const renderGlyph_tw = tripwire.module(enum {
|
||||
get_presentation,
|
||||
}, renderGlyph);
|
||||
|
||||
/// Render a glyph index. This automatically determines the correct texture
|
||||
/// atlas to use and caches the result.
|
||||
pub fn renderGlyph(
|
||||
@@ -241,6 +259,8 @@ pub fn renderGlyph(
|
||||
glyph_index: u32,
|
||||
opts: RenderOptions,
|
||||
) !Render {
|
||||
const tw = renderGlyph_tw;
|
||||
|
||||
const key: GlyphKey = .{ .index = index, .glyph = glyph_index, .opts = opts };
|
||||
|
||||
// Fast path: the cache has the value. This is almost always true and
|
||||
@@ -257,8 +277,10 @@ pub fn renderGlyph(
|
||||
|
||||
const gop = try self.glyphs.getOrPut(alloc, key);
|
||||
if (gop.found_existing) return gop.value_ptr.*;
|
||||
errdefer self.glyphs.removeByPtr(gop.key_ptr);
|
||||
|
||||
// Get the presentation to determine what atlas to use
|
||||
try tw.check(.get_presentation);
|
||||
const p = try self.resolver.getPresentation(index, glyph_index);
|
||||
const atlas: *font.Atlas = switch (p) {
|
||||
.text => &self.atlas_grayscale,
|
||||
@@ -426,3 +448,93 @@ test getIndex {
|
||||
try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx);
|
||||
}
|
||||
}
|
||||
|
||||
test "renderGlyph error after cache insert rolls back cache entry" {
|
||||
// This test verifies that when renderGlyph fails after inserting a cache
|
||||
// entry (via getOrPut), the errdefer properly removes the entry, preventing
|
||||
// corrupted/uninitialized data from remaining in the cache.
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var grid = try testGrid(.normal, alloc, lib);
|
||||
defer grid.deinit(alloc);
|
||||
|
||||
// Get the font index for 'A'
|
||||
const idx = (try grid.getIndex(alloc, 'A', .regular, null)).?;
|
||||
|
||||
// Get the glyph index for 'A'
|
||||
const glyph_index = glyph_index: {
|
||||
grid.lock.lockShared();
|
||||
defer grid.lock.unlockShared();
|
||||
const face = try grid.resolver.collection.getFace(idx);
|
||||
break :glyph_index face.glyphIndex('A').?;
|
||||
};
|
||||
|
||||
const render_opts: RenderOptions = .{ .grid_metrics = grid.metrics };
|
||||
const key: GlyphKey = .{ .index = idx, .glyph = glyph_index, .opts = render_opts };
|
||||
|
||||
// Verify the cache is empty for this glyph
|
||||
try testing.expect(grid.glyphs.get(key) == null);
|
||||
|
||||
// Set up tripwire to fail after cache insert.
|
||||
// We use OutOfMemory as it's a valid error in the renderGlyph error set.
|
||||
const tw = renderGlyph_tw;
|
||||
defer tw.end(.reset) catch {};
|
||||
tw.errorAlways(.get_presentation, error.OutOfMemory);
|
||||
|
||||
// This should fail due to the tripwire
|
||||
try testing.expectError(
|
||||
error.OutOfMemory,
|
||||
grid.renderGlyph(alloc, idx, glyph_index, render_opts),
|
||||
);
|
||||
|
||||
// The errdefer should have removed the cache entry, leaving the cache clean.
|
||||
// Without the errdefer fix, this would contain garbage/uninitialized data.
|
||||
try testing.expect(grid.glyphs.get(key) == null);
|
||||
}
|
||||
|
||||
test "init error" {
|
||||
// Test every failure point in `init` and ensure that we don't
|
||||
// leak memory (testing.allocator verifies) since we're exiting early.
|
||||
//
|
||||
// BUG: Currently this test will fail because init() is missing errdefer
|
||||
// cleanup for codepoints and glyphs when late operations fail
|
||||
// (ensureTotalCapacity, reloadMetrics).
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
for (std.meta.tags(init_tw.FailPoint)) |tag| {
|
||||
const tw = init_tw;
|
||||
defer tw.end(.reset) catch unreachable;
|
||||
tw.errorAlways(tag, error.OutOfMemory);
|
||||
|
||||
// Create a resolver for testing - we need to set up a minimal one.
|
||||
// The caller is responsible for cleaning up the resolver if init fails.
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var c = Collection.init();
|
||||
c.load_options = .{ .library = lib };
|
||||
_ = try c.add(alloc, try .init(
|
||||
lib,
|
||||
font.embedded.regular,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
), .{
|
||||
.style = .regular,
|
||||
.fallback = false,
|
||||
.size_adjustment = .none,
|
||||
});
|
||||
|
||||
var resolver: CodepointResolver = .{ .collection = c };
|
||||
defer resolver.deinit(alloc); // Caller cleans up on init failure
|
||||
|
||||
try testing.expectError(
|
||||
error.OutOfMemory,
|
||||
init(alloc, resolver),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator;
|
||||
const harfbuzz = @import("harfbuzz");
|
||||
const font = @import("../main.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const unicode = @import("../../unicode/main.zig");
|
||||
const Feature = font.shape.Feature;
|
||||
const FeatureList = font.shape.FeatureList;
|
||||
const default_features = font.shape.default_features;
|
||||
@@ -19,7 +20,7 @@ const log = std.log.scoped(.font_shaper);
|
||||
|
||||
/// Shaper that uses Harfbuzz.
|
||||
pub const Shaper = struct {
|
||||
/// The allocated used for the feature list and cell buf.
|
||||
/// The allocated used for the feature list, cell buf, and codepoints.
|
||||
alloc: Allocator,
|
||||
|
||||
/// The buffer used for text shaping. We reuse it across multiple shaping
|
||||
@@ -32,8 +33,29 @@ pub const Shaper = struct {
|
||||
/// The features to use for shaping.
|
||||
hb_feats: []harfbuzz.Feature,
|
||||
|
||||
/// The codepoints added to the buffer before shaping. We need to keep
|
||||
/// these separately because after shaping, HarfBuzz replaces codepoints
|
||||
/// with glyph indices in the buffer.
|
||||
codepoints: std.ArrayListUnmanaged(Codepoint) = .{},
|
||||
|
||||
const Codepoint = struct {
|
||||
cluster: u32,
|
||||
codepoint: u32,
|
||||
};
|
||||
|
||||
const CellBuf = std.ArrayListUnmanaged(font.shape.Cell);
|
||||
|
||||
const RunOffset = struct {
|
||||
cluster: u32 = 0,
|
||||
x: i32 = 0,
|
||||
y: i32 = 0,
|
||||
};
|
||||
|
||||
const CellOffset = struct {
|
||||
cluster: u32 = 0,
|
||||
x: i32 = 0,
|
||||
};
|
||||
|
||||
/// The cell_buf argument is the buffer to use for storing shaped results.
|
||||
/// This should be at least the number of columns in the terminal.
|
||||
pub fn init(alloc: Allocator, opts: font.shape.Options) !Shaper {
|
||||
@@ -74,6 +96,7 @@ pub const Shaper = struct {
|
||||
self.hb_buf.destroy();
|
||||
self.cell_buf.deinit(self.alloc);
|
||||
self.alloc.free(self.hb_feats);
|
||||
self.codepoints.deinit(self.alloc);
|
||||
}
|
||||
|
||||
pub fn endFrame(self: *const Shaper) void {
|
||||
@@ -135,33 +158,97 @@ pub const Shaper = struct {
|
||||
// If it isn't true, I'd like to catch it and learn more.
|
||||
assert(info.len == pos.len);
|
||||
|
||||
// This keeps track of the current offsets within a single cell.
|
||||
var cell_offset: struct {
|
||||
cluster: u32 = 0,
|
||||
x: i32 = 0,
|
||||
y: i32 = 0,
|
||||
} = .{};
|
||||
// This keeps track of the current x and y offsets (sum of advances)
|
||||
// and the furthest cluster we've seen so far (max).
|
||||
var run_offset: RunOffset = .{};
|
||||
|
||||
// This keeps track of the cell starting x and cluster.
|
||||
var cell_offset: CellOffset = .{};
|
||||
|
||||
// Convert all our info/pos to cells and set it.
|
||||
self.cell_buf.clearRetainingCapacity();
|
||||
for (info, pos) |info_v, pos_v| {
|
||||
// If our cluster changed then we've moved to a new cell.
|
||||
if (info_v.cluster != cell_offset.cluster) cell_offset = .{
|
||||
.cluster = info_v.cluster,
|
||||
};
|
||||
// info_v.cluster is the index into our codepoints array. We use it
|
||||
// to get the original cluster.
|
||||
const index = info_v.cluster;
|
||||
// Our cluster is also our cell X position. If the cluster changes
|
||||
// then we need to reset our current cell offsets.
|
||||
const cluster = self.codepoints.items[index].cluster;
|
||||
if (cell_offset.cluster != cluster) {
|
||||
const is_after_glyph_from_current_or_next_clusters =
|
||||
cluster <= run_offset.cluster;
|
||||
|
||||
try self.cell_buf.append(self.alloc, .{
|
||||
.x = @intCast(info_v.cluster),
|
||||
.x_offset = @intCast(cell_offset.x),
|
||||
.y_offset = @intCast(cell_offset.y),
|
||||
.glyph_index = info_v.codepoint,
|
||||
});
|
||||
const is_first_codepoint_in_cluster = blk: {
|
||||
var i = index;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
const codepoint = self.codepoints.items[i];
|
||||
break :blk codepoint.cluster != cluster;
|
||||
} else break :blk true;
|
||||
};
|
||||
|
||||
// We need to reset the `cell_offset` at the start of a new
|
||||
// cluster, but we do that conditionally if the codepoint
|
||||
// `is_first_codepoint_in_cluster` and the cluster is not
|
||||
// `is_after_glyph_from_current_or_next_clusters`, which is
|
||||
// a heuristic to detect ligatures and avoid positioning
|
||||
// glyphs that mark ligatures incorrectly. The idea is that
|
||||
// if the first codepoint in a cluster doesn't appear in
|
||||
// the stream, it's very likely that it combined with
|
||||
// codepoints from a previous cluster into a ligature.
|
||||
// Then, the subsequent codepoints are very likely marking
|
||||
// glyphs that are placed relative to that ligature, so if
|
||||
// we were to reset the `cell_offset` to align it with the
|
||||
// grid, the positions would be off. The
|
||||
// `!is_after_glyph_from_current_or_next_clusters` check is
|
||||
// needed in case these marking glyphs come from a later
|
||||
// cluster but are rendered first (see the Chakma and
|
||||
// Bengali tests). In that case when we get to the
|
||||
// codepoint that `is_first_codepoint_in_cluster`, but in a
|
||||
// cluster that
|
||||
// `is_after_glyph_from_current_or_next_clusters`, we don't
|
||||
// want to reset to the grid and cause the positions to be
|
||||
// off. (Note that we could go back and align the cells to
|
||||
// the grid starting from the one from the cluster that
|
||||
// rendered out of order, but that is more complicated so
|
||||
// we don't do that for now. Also, it's TBD if there are
|
||||
// exceptions to this heuristic for detecting ligatures,
|
||||
// but using the logging below seems to show it works
|
||||
// well.)
|
||||
if (is_first_codepoint_in_cluster and
|
||||
!is_after_glyph_from_current_or_next_clusters)
|
||||
{
|
||||
cell_offset = .{
|
||||
.cluster = cluster,
|
||||
.x = run_offset.x,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Under both FreeType and CoreText the harfbuzz scale is
|
||||
// in 26.6 fixed point units, so we round to the nearest
|
||||
// whole value here.
|
||||
cell_offset.x += (pos_v.x_advance + 0b100_000) >> 6;
|
||||
cell_offset.y += (pos_v.y_advance + 0b100_000) >> 6;
|
||||
const x_offset = run_offset.x - cell_offset.x + ((pos_v.x_offset + 0b100_000) >> 6);
|
||||
const y_offset = run_offset.y + ((pos_v.y_offset + 0b100_000) >> 6);
|
||||
|
||||
// For debugging positions, turn this on:
|
||||
//try self.debugPositions(run_offset, cell_offset, pos_v, index);
|
||||
|
||||
try self.cell_buf.append(self.alloc, .{
|
||||
.x = @intCast(cell_offset.cluster),
|
||||
.x_offset = @intCast(x_offset),
|
||||
.y_offset = @intCast(y_offset),
|
||||
.glyph_index = info_v.codepoint,
|
||||
});
|
||||
|
||||
// Add our advances to keep track of our run offsets.
|
||||
// Advances apply to the NEXT cell.
|
||||
// Under both FreeType and CoreText the harfbuzz scale is
|
||||
// in 26.6 fixed point units, so we round to the nearest
|
||||
// whole value here.
|
||||
run_offset.x += (pos_v.x_advance + 0b100_000) >> 6;
|
||||
run_offset.y += (pos_v.y_advance + 0b100_000) >> 6;
|
||||
run_offset.cluster = @max(run_offset.cluster, cluster);
|
||||
|
||||
// const i = self.cell_buf.items.len - 1;
|
||||
// log.warn("i={} info={} pos={} cell={}", .{ i, info_v, pos_v, self.cell_buf.items[i] });
|
||||
@@ -180,6 +267,13 @@ pub const Shaper = struct {
|
||||
self.shaper.hb_buf.reset();
|
||||
self.shaper.hb_buf.setContentType(.unicode);
|
||||
|
||||
// We set the cluster level to `characters` to give us the most
|
||||
// granularity, matching the CoreText shaper, and allowing us
|
||||
// to use our same ligature detection heuristics.
|
||||
self.shaper.hb_buf.setClusterLevel(.characters);
|
||||
|
||||
self.shaper.codepoints.clearRetainingCapacity();
|
||||
|
||||
// We don't support RTL text because RTL in terminals is messy.
|
||||
// Its something we want to improve. For now, we force LTR because
|
||||
// our renderers assume a strictly increasing X value.
|
||||
@@ -188,13 +282,156 @@ pub const Shaper = struct {
|
||||
|
||||
pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void {
|
||||
// log.warn("cluster={} cp={x}", .{ cluster, cp });
|
||||
self.shaper.hb_buf.add(cp, cluster);
|
||||
// We pass the index into codepoints as the cluster value to HarfBuzz.
|
||||
// After shaping, we use info.cluster to get back the index, which
|
||||
// lets us look up the original cluster value from codepoints.
|
||||
const index: u32 = @intCast(self.shaper.codepoints.items.len);
|
||||
self.shaper.hb_buf.add(cp, index);
|
||||
try self.shaper.codepoints.append(self.shaper.alloc, .{
|
||||
.cluster = cluster,
|
||||
.codepoint = cp,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn finalize(self: RunIteratorHook) void {
|
||||
self.shaper.hb_buf.guessSegmentProperties();
|
||||
}
|
||||
};
|
||||
|
||||
fn debugPositions(
|
||||
self: *Shaper,
|
||||
run_offset: RunOffset,
|
||||
cell_offset: CellOffset,
|
||||
pos_v: harfbuzz.GlyphPosition,
|
||||
index: u32,
|
||||
) !void {
|
||||
const x_offset = run_offset.x - cell_offset.x + ((pos_v.x_offset + 0b100_000) >> 6);
|
||||
const y_offset = run_offset.y + ((pos_v.y_offset + 0b100_000) >> 6);
|
||||
const advance_x_offset = run_offset.x - cell_offset.x;
|
||||
const advance_y_offset = run_offset.y;
|
||||
const x_offset_diff = x_offset - advance_x_offset;
|
||||
const y_offset_diff = y_offset - advance_y_offset;
|
||||
const positions_differ = @abs(x_offset_diff) > 0 or @abs(y_offset_diff) > 0;
|
||||
const y_offset_differs = run_offset.y != 0;
|
||||
const cluster = self.codepoints.items[index].cluster;
|
||||
const cluster_differs = cluster != cell_offset.cluster;
|
||||
|
||||
// To debug every loop, flip this to true:
|
||||
const extra_debugging = false;
|
||||
|
||||
const is_previous_codepoint_prepend = if (cluster_differs or
|
||||
extra_debugging)
|
||||
blk: {
|
||||
var i = index;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
const codepoint = self.codepoints.items[i];
|
||||
break :blk unicode.table.get(@intCast(codepoint.codepoint)).grapheme_boundary_class == .prepend;
|
||||
}
|
||||
break :blk false;
|
||||
} else false;
|
||||
|
||||
const formatted_cps: ?[]u8 = if (positions_differ or
|
||||
y_offset_differs or
|
||||
cluster_differs or
|
||||
extra_debugging)
|
||||
blk: {
|
||||
var allocating = std.Io.Writer.Allocating.init(self.alloc);
|
||||
defer allocating.deinit();
|
||||
const writer = &allocating.writer;
|
||||
const codepoints = self.codepoints.items;
|
||||
var last_cluster: ?u32 = null;
|
||||
for (codepoints, 0..) |cp, i| {
|
||||
if (@as(i32, @intCast(cp.cluster)) >= @as(i32, @intCast(cell_offset.cluster)) - 1 and
|
||||
cp.cluster <= cluster + 1)
|
||||
{
|
||||
if (last_cluster) |last| {
|
||||
if (cp.cluster != last) {
|
||||
try writer.writeAll(" ");
|
||||
}
|
||||
}
|
||||
if (i == index) {
|
||||
try writer.writeAll("▸");
|
||||
}
|
||||
// Using Python syntax for easier debugging
|
||||
if (cp.codepoint > 0xFFFF) {
|
||||
try writer.print("\\U{x:0>8}", .{cp.codepoint});
|
||||
} else {
|
||||
try writer.print("\\u{x:0>4}", .{cp.codepoint});
|
||||
}
|
||||
last_cluster = cp.cluster;
|
||||
}
|
||||
}
|
||||
try writer.writeAll(" → ");
|
||||
for (codepoints) |cp| {
|
||||
if (@as(i32, @intCast(cp.cluster)) >= @as(i32, @intCast(cell_offset.cluster)) - 1 and
|
||||
cp.cluster <= cluster + 1)
|
||||
{
|
||||
try writer.print("{u}", .{@as(u21, @intCast(cp.codepoint))});
|
||||
}
|
||||
}
|
||||
break :blk try allocating.toOwnedSlice();
|
||||
} else null;
|
||||
defer if (formatted_cps) |cps| self.alloc.free(cps);
|
||||
|
||||
if (extra_debugging) {
|
||||
log.warn("extra debugging of positions index={d} cell_offset.cluster={d} cluster={d} run_offset.cluster={d} diff={d} pos=({d},{d}) run_offset=({d},{d}) cell_offset.x={d} is_prev_prepend={} cps = {s}", .{
|
||||
index,
|
||||
cell_offset.cluster,
|
||||
cluster,
|
||||
run_offset.cluster,
|
||||
@as(isize, @intCast(cluster)) - @as(isize, @intCast(cell_offset.cluster)),
|
||||
x_offset,
|
||||
y_offset,
|
||||
run_offset.x,
|
||||
run_offset.y,
|
||||
cell_offset.x,
|
||||
is_previous_codepoint_prepend,
|
||||
formatted_cps.?,
|
||||
});
|
||||
}
|
||||
|
||||
if (positions_differ) {
|
||||
log.warn("position differs from advance: cluster={d} pos=({d},{d}) adv=({d},{d}) diff=({d},{d}) cps = {s}", .{
|
||||
cluster,
|
||||
x_offset,
|
||||
y_offset,
|
||||
advance_x_offset,
|
||||
advance_y_offset,
|
||||
x_offset_diff,
|
||||
y_offset_diff,
|
||||
formatted_cps.?,
|
||||
});
|
||||
}
|
||||
|
||||
if (y_offset_differs) {
|
||||
log.warn("run_offset.y differs from zero: cluster={d} pos=({d},{d}) run_offset=({d},{d}) cell_offset.x={d} cps = {s}", .{
|
||||
cluster,
|
||||
x_offset,
|
||||
y_offset,
|
||||
run_offset.x,
|
||||
run_offset.y,
|
||||
cell_offset.x,
|
||||
formatted_cps.?,
|
||||
});
|
||||
}
|
||||
|
||||
if (cluster_differs) {
|
||||
log.warn("cell_offset.cluster differs from cluster (potential ligature detected) cell_offset.cluster={d} cluster={d} run_offset.cluster={d} diff={d} pos=({d},{d}) run_offset=({d},{d}) cell_offset.x={d} is_prev_prepend={} cps = {s}", .{
|
||||
cell_offset.cluster,
|
||||
cluster,
|
||||
run_offset.cluster,
|
||||
@as(isize, @intCast(cluster)) - @as(isize, @intCast(cell_offset.cluster)),
|
||||
x_offset,
|
||||
y_offset,
|
||||
run_offset.x,
|
||||
run_offset.y,
|
||||
cell_offset.x,
|
||||
is_previous_codepoint_prepend,
|
||||
formatted_cps.?,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "run iterator" {
|
||||
@@ -737,7 +974,7 @@ test "shape with empty cells in between" {
|
||||
}
|
||||
}
|
||||
|
||||
test "shape Chinese characters" {
|
||||
test "shape Combining characters" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@@ -786,6 +1023,443 @@ test "shape Chinese characters" {
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
|
||||
// This test exists because the string it uses causes HarfBuzz to output a
|
||||
// non-monotonic run with our cluster level set to `characters`, which we need
|
||||
// to handle by tracking the max cluster for the run.
|
||||
test "shape Devanagari string" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// We need a font that supports devanagari for this to work, if we can't
|
||||
// find Arial Unicode MS, which is a system font on macOS, we just skip
|
||||
// the test.
|
||||
var testdata = testShaperWithDiscoveredFont(
|
||||
alloc,
|
||||
"Arial Unicode MS",
|
||||
) catch return error.SkipZigTest;
|
||||
defer testdata.deinit();
|
||||
|
||||
// Make a screen with some data
|
||||
var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Disable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, false);
|
||||
|
||||
var s = t.vtStream();
|
||||
defer s.deinit();
|
||||
try s.nextSlice("अपार्टमेंट");
|
||||
|
||||
var state: terminal.RenderState = .empty;
|
||||
defer state.deinit(alloc);
|
||||
try state.update(alloc, &t);
|
||||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.cells = state.row_data.get(0).cells.slice(),
|
||||
});
|
||||
|
||||
const run = try it.next(alloc);
|
||||
try testing.expect(run != null);
|
||||
const cells = try shaper.shape(run.?);
|
||||
|
||||
try testing.expectEqual(@as(usize, 8), cells.len);
|
||||
try testing.expectEqual(@as(u16, 0), cells[0].x);
|
||||
try testing.expectEqual(@as(u16, 1), cells[1].x);
|
||||
try testing.expectEqual(@as(u16, 2), cells[2].x);
|
||||
try testing.expectEqual(@as(u16, 4), cells[3].x);
|
||||
try testing.expectEqual(@as(u16, 4), cells[4].x);
|
||||
try testing.expectEqual(@as(u16, 5), cells[5].x);
|
||||
try testing.expectEqual(@as(u16, 5), cells[6].x);
|
||||
try testing.expectEqual(@as(u16, 6), cells[7].x);
|
||||
|
||||
try testing.expect(try it.next(alloc) == null);
|
||||
}
|
||||
|
||||
test "shape Tai Tham vowels (position differs from advance)" {
|
||||
// Note that while this test was necessary for CoreText, the old logic was
|
||||
// working for HarfBuzz. Still we keep it to ensure it has the correct
|
||||
// behavior.
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// We need a font that supports Tai Tham for this to work, if we can't find
|
||||
// Noto Sans Tai Tham, which is a system font on macOS, we just skip the
|
||||
// test.
|
||||
var testdata = testShaperWithDiscoveredFont(
|
||||
alloc,
|
||||
"Noto Sans Tai Tham",
|
||||
) catch return error.SkipZigTest;
|
||||
defer testdata.deinit();
|
||||
|
||||
var buf: [32]u8 = undefined;
|
||||
var buf_idx: usize = 0;
|
||||
buf_idx += try std.unicode.utf8Encode(0x1a2F, buf[buf_idx..]); // ᨯ
|
||||
buf_idx += try std.unicode.utf8Encode(0x1a70, buf[buf_idx..]); // ᩰ
|
||||
|
||||
// Make a screen with some data
|
||||
var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
var s = t.vtStream();
|
||||
defer s.deinit();
|
||||
try s.nextSlice(buf[0..buf_idx]);
|
||||
|
||||
var state: terminal.RenderState = .empty;
|
||||
defer state.deinit(alloc);
|
||||
try state.update(alloc, &t);
|
||||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.cells = state.row_data.get(0).cells.slice(),
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
||||
const cells = try shaper.shape(run);
|
||||
try testing.expectEqual(@as(usize, 2), cells.len);
|
||||
try testing.expectEqual(@as(u16, 0), cells[0].x);
|
||||
try testing.expectEqual(@as(u16, 0), cells[1].x);
|
||||
|
||||
// The first glyph renders in the next cell. We expect the x_offset
|
||||
// to equal the cell width. However, with FreeType the cell_width is
|
||||
// computed from ASCII glyphs, and Noto Sans Tai Tham only has the
|
||||
// space character in ASCII (with a 3px advance), so the cell_width
|
||||
// metric doesn't match the actual Tai Tham glyph positioning.
|
||||
const expected_x_offset: i16 = if (comptime font.options.backend.hasFreetype()) 7 else @intCast(run.grid.metrics.cell_width);
|
||||
try testing.expectEqual(expected_x_offset, cells[0].x_offset);
|
||||
try testing.expectEqual(@as(i16, 0), cells[1].x_offset);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
|
||||
test "shape Tibetan characters" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// We need a font that has multiple glyphs for this codepoint to reproduce
|
||||
// the old broken behavior, and Noto Serif Tibetan is one of them. It's not
|
||||
// a default Mac font, and if we can't find it we just skip the test.
|
||||
var testdata = testShaperWithDiscoveredFont(
|
||||
alloc,
|
||||
"Noto Serif Tibetan",
|
||||
) catch return error.SkipZigTest;
|
||||
defer testdata.deinit();
|
||||
|
||||
var buf: [32]u8 = undefined;
|
||||
var buf_idx: usize = 0;
|
||||
buf_idx += try std.unicode.utf8Encode(0x0f00, buf[buf_idx..]); // ༀ
|
||||
|
||||
// Make a screen with some data
|
||||
var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
var s = t.vtStream();
|
||||
defer s.deinit();
|
||||
try s.nextSlice(buf[0..buf_idx]);
|
||||
|
||||
var state: terminal.RenderState = .empty;
|
||||
defer state.deinit(alloc);
|
||||
try state.update(alloc, &t);
|
||||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.cells = state.row_data.get(0).cells.slice(),
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
||||
const cells = try shaper.shape(run);
|
||||
try testing.expectEqual(@as(usize, 2), cells.len);
|
||||
try testing.expectEqual(@as(u16, 0), cells[0].x);
|
||||
try testing.expectEqual(@as(u16, 0), cells[1].x);
|
||||
|
||||
// The second glyph renders at the correct location
|
||||
try testing.expect(cells[1].x_offset < 2);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
|
||||
test "shape Tai Tham letters (run_offset.y differs from zero)" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// We need a font that supports Tai Tham for this to work, if we can't find
|
||||
// Noto Sans Tai Tham, which is a system font on macOS, we just skip the
|
||||
// test.
|
||||
var testdata = testShaperWithDiscoveredFont(
|
||||
alloc,
|
||||
"Noto Sans Tai Tham",
|
||||
) catch return error.SkipZigTest;
|
||||
defer testdata.deinit();
|
||||
|
||||
var buf: [32]u8 = undefined;
|
||||
var buf_idx: usize = 0;
|
||||
|
||||
// First grapheme cluster:
|
||||
buf_idx += try std.unicode.utf8Encode(0x1a49, buf[buf_idx..]); // HA
|
||||
buf_idx += try std.unicode.utf8Encode(0x1a60, buf[buf_idx..]); // SAKOT
|
||||
// Second grapheme cluster, combining with the first in a ligature:
|
||||
buf_idx += try std.unicode.utf8Encode(0x1a3f, buf[buf_idx..]); // YA
|
||||
buf_idx += try std.unicode.utf8Encode(0x1a69, buf[buf_idx..]); // U
|
||||
|
||||
// Make a screen with some data
|
||||
var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
var s = t.vtStream();
|
||||
defer s.deinit();
|
||||
try s.nextSlice(buf[0..buf_idx]);
|
||||
|
||||
var state: terminal.RenderState = .empty;
|
||||
defer state.deinit(alloc);
|
||||
try state.update(alloc, &t);
|
||||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.cells = state.row_data.get(0).cells.slice(),
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
||||
const cells = try shaper.shape(run);
|
||||
try testing.expectEqual(@as(usize, 3), cells.len);
|
||||
try testing.expectEqual(@as(u16, 0), cells[0].x);
|
||||
try testing.expectEqual(@as(u16, 0), cells[1].x);
|
||||
try testing.expectEqual(@as(u16, 0), cells[2].x); // U from second grapheme
|
||||
|
||||
// The U glyph renders at a y below zero
|
||||
try testing.expectEqual(@as(i16, -3), cells[2].y_offset);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
|
||||
test "shape Javanese ligatures" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// We need a font that supports Javanese for this to work, if we can't find
|
||||
// Noto Sans Javanese Regular, which is a system font on macOS, we just
|
||||
// skip the test.
|
||||
var testdata = testShaperWithDiscoveredFont(
|
||||
alloc,
|
||||
"Noto Sans Javanese",
|
||||
) catch return error.SkipZigTest;
|
||||
defer testdata.deinit();
|
||||
|
||||
var buf: [32]u8 = undefined;
|
||||
var buf_idx: usize = 0;
|
||||
|
||||
// First grapheme cluster:
|
||||
buf_idx += try std.unicode.utf8Encode(0xa9a4, buf[buf_idx..]); // NA
|
||||
buf_idx += try std.unicode.utf8Encode(0xa9c0, buf[buf_idx..]); // PANGKON
|
||||
// Second grapheme cluster, combining with the first in a ligature:
|
||||
buf_idx += try std.unicode.utf8Encode(0xa9b2, buf[buf_idx..]); // HA
|
||||
buf_idx += try std.unicode.utf8Encode(0xa9b8, buf[buf_idx..]); // Vowel sign SUKU
|
||||
|
||||
// Make a screen with some data
|
||||
var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
var s = t.vtStream();
|
||||
defer s.deinit();
|
||||
try s.nextSlice(buf[0..buf_idx]);
|
||||
|
||||
var state: terminal.RenderState = .empty;
|
||||
defer state.deinit(alloc);
|
||||
try state.update(alloc, &t);
|
||||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.cells = state.row_data.get(0).cells.slice(),
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
||||
const cells = try shaper.shape(run);
|
||||
const cell_width = run.grid.metrics.cell_width;
|
||||
try testing.expectEqual(@as(usize, 3), cells.len);
|
||||
try testing.expectEqual(@as(u16, 0), cells[0].x);
|
||||
try testing.expectEqual(@as(u16, 0), cells[1].x);
|
||||
try testing.expectEqual(@as(u16, 0), cells[2].x);
|
||||
|
||||
// The vowel sign SUKU renders with correct x_offset
|
||||
try testing.expect(cells[2].x_offset > 3 * cell_width);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
|
||||
test "shape Chakma vowel sign with ligature (vowel sign renders first)" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// We need a font that supports Chakma for this to work, if we can't find
|
||||
// Noto Sans Chakma Regular, which is a system font on macOS, we just skip
|
||||
// the test.
|
||||
var testdata = testShaperWithDiscoveredFont(
|
||||
alloc,
|
||||
"Noto Sans Chakma",
|
||||
) catch return error.SkipZigTest;
|
||||
defer testdata.deinit();
|
||||
|
||||
var buf: [32]u8 = undefined;
|
||||
var buf_idx: usize = 0;
|
||||
|
||||
// First grapheme cluster:
|
||||
buf_idx += try std.unicode.utf8Encode(0x1111d, buf[buf_idx..]); // BAA
|
||||
// Second grapheme cluster:
|
||||
buf_idx += try std.unicode.utf8Encode(0x11116, buf[buf_idx..]); // TAA
|
||||
buf_idx += try std.unicode.utf8Encode(0x11133, buf[buf_idx..]); // Virama
|
||||
// Third grapheme cluster, combining with the second in a ligature:
|
||||
buf_idx += try std.unicode.utf8Encode(0x11120, buf[buf_idx..]); // YYAA
|
||||
buf_idx += try std.unicode.utf8Encode(0x1112c, buf[buf_idx..]); // Vowel Sign U
|
||||
|
||||
// Make a screen with some data
|
||||
var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
var s = t.vtStream();
|
||||
defer s.deinit();
|
||||
try s.nextSlice(buf[0..buf_idx]);
|
||||
|
||||
var state: terminal.RenderState = .empty;
|
||||
defer state.deinit(alloc);
|
||||
try state.update(alloc, &t);
|
||||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.cells = state.row_data.get(0).cells.slice(),
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
||||
const cells = try shaper.shape(run);
|
||||
try testing.expectEqual(@as(usize, 4), cells.len);
|
||||
try testing.expectEqual(@as(u16, 0), cells[0].x);
|
||||
// See the giant "We need to reset the `cell_offset`" comment, but here
|
||||
// we should technically have the rest of these be `x` of 1, but that
|
||||
// would require going back in the stream to adjust past cells, and
|
||||
// we don't take on that complexity.
|
||||
try testing.expectEqual(@as(u16, 0), cells[1].x);
|
||||
try testing.expectEqual(@as(u16, 0), cells[2].x);
|
||||
try testing.expectEqual(@as(u16, 0), cells[3].x);
|
||||
|
||||
// The vowel sign U renders before the TAA:
|
||||
try testing.expect(cells[1].x_offset < cells[2].x_offset);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
|
||||
test "shape Bengali ligatures with out of order vowels" {
|
||||
// Whereas this test in CoreText had everything shaping into one giant
|
||||
// ligature, HarfBuzz splits it into a few clusters. It still looks okay
|
||||
// (see #10332).
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// We need a font that supports Bengali for this to work, if we can't find
|
||||
// Arial Unicode MS, which is a system font on macOS, we just skip the
|
||||
// test.
|
||||
var testdata = testShaperWithDiscoveredFont(
|
||||
alloc,
|
||||
"Arial Unicode MS",
|
||||
) catch return error.SkipZigTest;
|
||||
defer testdata.deinit();
|
||||
|
||||
var buf: [32]u8 = undefined;
|
||||
var buf_idx: usize = 0;
|
||||
|
||||
// First grapheme cluster:
|
||||
buf_idx += try std.unicode.utf8Encode(0x09b0, buf[buf_idx..]); // RA
|
||||
buf_idx += try std.unicode.utf8Encode(0x09be, buf[buf_idx..]); // Vowel sign AA
|
||||
// Second grapheme cluster:
|
||||
buf_idx += try std.unicode.utf8Encode(0x09b7, buf[buf_idx..]); // SSA
|
||||
buf_idx += try std.unicode.utf8Encode(0x09cd, buf[buf_idx..]); // Virama
|
||||
// Third grapheme cluster:
|
||||
buf_idx += try std.unicode.utf8Encode(0x099f, buf[buf_idx..]); // TTA
|
||||
buf_idx += try std.unicode.utf8Encode(0x09cd, buf[buf_idx..]); // Virama
|
||||
// Fourth grapheme cluster:
|
||||
buf_idx += try std.unicode.utf8Encode(0x09b0, buf[buf_idx..]); // RA
|
||||
buf_idx += try std.unicode.utf8Encode(0x09c7, buf[buf_idx..]); // Vowel sign E
|
||||
|
||||
// Make a screen with some data
|
||||
var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
var s = t.vtStream();
|
||||
defer s.deinit();
|
||||
try s.nextSlice(buf[0..buf_idx]);
|
||||
|
||||
var state: terminal.RenderState = .empty;
|
||||
defer state.deinit(alloc);
|
||||
try state.update(alloc, &t);
|
||||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.cells = state.row_data.get(0).cells.slice(),
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
||||
const cells = try shaper.shape(run);
|
||||
try testing.expectEqual(@as(usize, 8), cells.len);
|
||||
try testing.expectEqual(@as(u16, 0), cells[0].x);
|
||||
try testing.expectEqual(@as(u16, 0), cells[1].x);
|
||||
|
||||
// Whereas CoreText puts everything all into the first cell (see the
|
||||
// corresponding test), HarfBuzz splits into two clusters.
|
||||
try testing.expectEqual(@as(u16, 1), cells[2].x);
|
||||
try testing.expectEqual(@as(u16, 1), cells[3].x);
|
||||
try testing.expectEqual(@as(u16, 1), cells[4].x);
|
||||
try testing.expectEqual(@as(u16, 1), cells[5].x);
|
||||
try testing.expectEqual(@as(u16, 1), cells[6].x);
|
||||
try testing.expectEqual(@as(u16, 1), cells[7].x);
|
||||
|
||||
// The vowel sign E renders before the SSA:
|
||||
try testing.expect(cells[2].x_offset < cells[3].x_offset);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
|
||||
test "shape box glyphs" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
@@ -1432,3 +2106,58 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
|
||||
.lib = lib,
|
||||
};
|
||||
}
|
||||
|
||||
fn testShaperWithDiscoveredFont(alloc: Allocator, font_req: [:0]const u8) !TestShaper {
|
||||
if (font.Discover == void) return error.SkipZigTest;
|
||||
var lib = try Library.init(alloc);
|
||||
errdefer lib.deinit();
|
||||
|
||||
var c = Collection.init();
|
||||
c.load_options = .{ .library = lib };
|
||||
|
||||
// Discover and add our font to the collection.
|
||||
{
|
||||
var disco = font.Discover.init();
|
||||
defer disco.deinit();
|
||||
var disco_it = try disco.discover(alloc, .{
|
||||
.family = font_req,
|
||||
.size = 12,
|
||||
.monospace = false,
|
||||
});
|
||||
defer disco_it.deinit();
|
||||
var face: font.DeferredFace = (try disco_it.next()) orelse return error.FontNotFound;
|
||||
errdefer face.deinit();
|
||||
|
||||
// Check which font was discovered - skip if it doesn't match the request
|
||||
var name_buf: [256]u8 = undefined;
|
||||
const face_name = face.name(&name_buf) catch "(unknown)";
|
||||
if (std.mem.indexOf(u8, face_name, font_req) == null) {
|
||||
return error.SkipZigTest;
|
||||
}
|
||||
|
||||
_ = try c.add(
|
||||
alloc,
|
||||
try face.load(lib, .{ .size = .{ .points = 12 } }),
|
||||
.{
|
||||
.style = .regular,
|
||||
.fallback = false,
|
||||
.size_adjustment = .none,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const grid_ptr = try alloc.create(SharedGrid);
|
||||
errdefer alloc.destroy(grid_ptr);
|
||||
grid_ptr.* = try .init(alloc, .{ .collection = c });
|
||||
errdefer grid_ptr.*.deinit(alloc);
|
||||
|
||||
var shaper = try Shaper.init(alloc, .{});
|
||||
errdefer shaper.deinit();
|
||||
|
||||
return TestShaper{
|
||||
.alloc = alloc,
|
||||
.shaper = shaper,
|
||||
.grid = grid_ptr,
|
||||
.lib = lib,
|
||||
};
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
};
|
||||
400
src/inspector/widgets/screen.zig
Normal file
400
src/inspector/widgets/screen.zig
Normal file
@@ -0,0 +1,400 @@
|
||||
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(
|
||||
"Semantic Prompt",
|
||||
cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
|
||||
)) semanticPromptTable(&screen.semantic_prompt);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// Render semantic prompt state table.
|
||||
pub fn semanticPromptTable(
|
||||
semantic_prompt: *const terminal.Screen.SemanticPrompt,
|
||||
) void {
|
||||
if (!cimgui.c.ImGui_BeginTable(
|
||||
"##semantic_prompt",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
)) return;
|
||||
defer cimgui.c.ImGui_EndTable();
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Seen");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("Whether any semantic prompt markers (OSC 133) have been seen in this screen.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
var value: bool = semantic_prompt.seen;
|
||||
_ = cimgui.c.ImGui_Checkbox("##seen", &value);
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.ImGui_TableNextRow();
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
|
||||
cimgui.c.ImGui_Text("Click Handling");
|
||||
cimgui.c.ImGui_SameLine();
|
||||
widgets.helpMarker("How click events are handled in prompts. Set via 'cl' or 'click_events' options in OSC 133.");
|
||||
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
|
||||
switch (semantic_prompt.click) {
|
||||
.none => cimgui.c.ImGui_TextDisabled("(none)"),
|
||||
.click_events => cimgui.c.ImGui_Text("click_events"),
|
||||
.cl => |cl| cimgui.c.ImGui_Text("cl=%s", @tagName(cl).ptr),
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
};
|
||||
@@ -190,6 +190,7 @@ test {
|
||||
_ = @import("surface_mouse.zig");
|
||||
|
||||
// Libraries
|
||||
_ = @import("tripwire.zig");
|
||||
_ = @import("benchmark/main.zig");
|
||||
_ = @import("crash/main.zig");
|
||||
_ = @import("datastruct/main.zig");
|
||||
|
||||
@@ -53,4 +53,5 @@ pub const locales = [_][:0]const u8{
|
||||
"zh_TW.UTF-8",
|
||||
"hr_HR.UTF-8",
|
||||
"lt_LT.UTF-8",
|
||||
"lv_LV.UTF-8",
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -254,7 +254,7 @@ fn threadMain_(self: *Thread) !void {
|
||||
);
|
||||
|
||||
// Start the draw timer
|
||||
self.startDrawTimer();
|
||||
self.syncDrawTimer();
|
||||
|
||||
// Run
|
||||
log.debug("starting renderer thread", .{});
|
||||
@@ -292,11 +292,28 @@ fn setQosClass(self: *const Thread) void {
|
||||
}
|
||||
}
|
||||
|
||||
fn startDrawTimer(self: *Thread) void {
|
||||
// If our renderer doesn't support animations then we never run this.
|
||||
if (!@hasDecl(rendererpkg.Renderer, "hasAnimations")) return;
|
||||
if (!self.renderer.hasAnimations()) return;
|
||||
if (self.config.custom_shader_animation == .false) return;
|
||||
fn syncDrawTimer(self: *Thread) void {
|
||||
skip: {
|
||||
// If our renderer supports animations and has them, then we
|
||||
// can apply draw timer based on custom shader animation configuration.
|
||||
if (@hasDecl(rendererpkg.Renderer, "hasAnimations") and
|
||||
self.renderer.hasAnimations())
|
||||
{
|
||||
// If our config says to always animate, we do so.
|
||||
switch (self.config.custom_shader_animation) {
|
||||
// Always animate
|
||||
.always => break :skip,
|
||||
// Only when focused
|
||||
.true => if (self.flags.focused) break :skip,
|
||||
// Never animate
|
||||
.false => {},
|
||||
}
|
||||
}
|
||||
|
||||
// We're skipping the draw timer. Stop it on the next iteration.
|
||||
self.draw_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set our active state so it knows we're running. We set this before
|
||||
// even checking the active state in case we have a pending shutdown.
|
||||
@@ -316,11 +333,6 @@ fn startDrawTimer(self: *Thread) void {
|
||||
);
|
||||
}
|
||||
|
||||
fn stopDrawTimer(self: *Thread) void {
|
||||
// This will stop the draw on the next iteration.
|
||||
self.draw_active = false;
|
||||
}
|
||||
|
||||
/// Drain the mailbox.
|
||||
fn drainMailbox(self: *Thread) !void {
|
||||
// There's probably a more elegant way to do this...
|
||||
@@ -377,12 +389,10 @@ fn drainMailbox(self: *Thread) !void {
|
||||
// Set it on the renderer
|
||||
try self.renderer.setFocus(v);
|
||||
|
||||
if (!v) {
|
||||
if (self.config.custom_shader_animation != .always) {
|
||||
// Stop the draw timer
|
||||
self.stopDrawTimer();
|
||||
}
|
||||
// We always resync our draw timer (may disable it)
|
||||
self.syncDrawTimer();
|
||||
|
||||
if (!v) {
|
||||
// If we're not focused, then we stop the cursor blink
|
||||
if (self.cursor_c.state() == .active and
|
||||
self.cursor_c_cancel.state() == .dead)
|
||||
@@ -397,9 +407,6 @@ fn drainMailbox(self: *Thread) !void {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Start the draw timer
|
||||
self.startDrawTimer();
|
||||
|
||||
// If we're focused, we immediately show the cursor again
|
||||
// and then restart the timer.
|
||||
if (self.cursor_c.state() != .active) {
|
||||
@@ -446,8 +453,7 @@ fn drainMailbox(self: *Thread) !void {
|
||||
|
||||
// Stop and start the draw timer to capture the new
|
||||
// hasAnimations value.
|
||||
self.stopDrawTimer();
|
||||
self.startDrawTimer();
|
||||
self.syncDrawTimer();
|
||||
},
|
||||
|
||||
.search_viewport_matches => |v| {
|
||||
@@ -466,7 +472,9 @@ fn drainMailbox(self: *Thread) !void {
|
||||
self.renderer.search_matches_dirty = true;
|
||||
},
|
||||
|
||||
.inspector => |v| self.flags.has_inspector = v,
|
||||
.inspector => |v| {
|
||||
self.flags.has_inspector = v;
|
||||
},
|
||||
|
||||
.macos_display_id => |v| {
|
||||
if (@hasDecl(rendererpkg.Renderer, "setMacOSDisplayID")) {
|
||||
@@ -598,11 +606,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,76 @@ 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 = overlay: {
|
||||
if (self.overlay) |*v| existing: {
|
||||
// Verify that our overlay size matches our screen
|
||||
// size as we know it now. If not, deinit and reinit.
|
||||
// Note: these intCasts are always safe because z2d
|
||||
// stores as i32 but we always init with a u32.
|
||||
const width: u32 = @intCast(v.surface.getWidth());
|
||||
const height: u32 = @intCast(v.surface.getHeight());
|
||||
const term_size = self.size.terminal();
|
||||
if (width != term_size.width or
|
||||
height != term_size.height) break :existing;
|
||||
|
||||
// We also depend on cell size.
|
||||
if (v.cell_size.width != self.size.cell.width or
|
||||
v.cell_size.height != self.size.cell.height) break :existing;
|
||||
|
||||
// Everything matches, so we can just reset the surface
|
||||
// and redraw.
|
||||
v.reset();
|
||||
break :overlay v;
|
||||
}
|
||||
|
||||
// If we reached this point we want to reset our overlay.
|
||||
if (self.overlay) |*v| {
|
||||
v.deinit(alloc);
|
||||
self.overlay = null;
|
||||
}
|
||||
|
||||
assert(self.overlay == null);
|
||||
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| {
|
||||
|
||||
@@ -44,7 +44,7 @@ Elvish, on startup, searches for paths defined in `XDG_DATA_DIRS`
|
||||
variable for `./elvish/lib/*.elv` files and imports them. They are thus
|
||||
made available for use as modules by way of `use <filename>`.
|
||||
|
||||
Ghostty launches Elvish, passing the environment with `XDG_DATA_DIRS`prepended
|
||||
Ghostty launches Elvish, passing the environment with `XDG_DATA_DIRS` prepended
|
||||
with `$GHOSTTY_RESOURCES_DIR/src/shell-integration`. It contains
|
||||
`./elvish/lib/ghostty-integration.elv`. The user can then import it
|
||||
by `use ghostty-integration` every time after shell startup or
|
||||
@@ -57,7 +57,7 @@ of your `rc.elv` file:
|
||||
|
||||
```elvish
|
||||
if (eq $E:TERM "xterm-ghostty") {
|
||||
use ghostty-integration
|
||||
try { use ghostty-integration } catch { }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -84,7 +84,8 @@ Nushell's vendor autoload mechanism. Ghostty then automatically imports
|
||||
the module using the `-e "use ghostty *"` flag when starting Nushell.
|
||||
|
||||
Nushell provides many shell features itself, such as `title` and `cursor`,
|
||||
so our integration focuses on Ghostty-specific features like `sudo`.
|
||||
so our integration focuses on Ghostty-specific features like `sudo`,
|
||||
`ssh-env`, and `ssh-terminfo`.
|
||||
|
||||
The shell integration is automatically enabled when running Nushell in Ghostty,
|
||||
but you can also load it manually is shell integration is disabled:
|
||||
|
||||
@@ -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)
|
||||
@@ -178,76 +184,121 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then
|
||||
}
|
||||
fi
|
||||
|
||||
# Import bash-preexec, safe to do multiple times
|
||||
builtin source "$(dirname -- "${BASH_SOURCE[0]}")/bash-preexec.sh"
|
||||
|
||||
# This is set to 1 when we're executing a command so that we don't
|
||||
# send prompt marks multiple times.
|
||||
_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;cl=line\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;cl=line;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)
|
||||
precmd_functions+=(__ghostty_precmd)
|
||||
if (( BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4) )); then
|
||||
__ghostty_preexec_hook() {
|
||||
builtin local cmd
|
||||
cmd=$(LC_ALL=C HISTTIMEFORMAT='' builtin history 1)
|
||||
cmd="${cmd#*[[:digit:]][* ] }" # remove leading history number
|
||||
[[ -n "$cmd" ]] && __ghostty_preexec "$cmd"
|
||||
}
|
||||
|
||||
# Use function substitution in 5.3+. Otherwise, use command substitution.
|
||||
# Any output (including escape sequences) goes to the terminal.
|
||||
if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3) )); then
|
||||
# shellcheck disable=SC2016
|
||||
builtin readonly __ghostty_ps0='${ __ghostty_preexec_hook; }'
|
||||
else
|
||||
# shellcheck disable=SC2016
|
||||
builtin readonly __ghostty_ps0='$(__ghostty_preexec_hook >/dev/tty)'
|
||||
fi
|
||||
|
||||
__ghostty_hook() {
|
||||
builtin local ret=$?
|
||||
__ghostty_precmd "$ret"
|
||||
PS0=$__ghostty_ps0
|
||||
}
|
||||
|
||||
# Append our hook to PROMPT_COMMAND, preserving its existing type.
|
||||
if [[ ";${PROMPT_COMMAND[*]:-};" != *";__ghostty_hook;"* ]]; then
|
||||
if [[ -z "${PROMPT_COMMAND[*]}" ]]; then
|
||||
if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then
|
||||
PROMPT_COMMAND=(__ghostty_hook)
|
||||
else
|
||||
# shellcheck disable=SC2178
|
||||
PROMPT_COMMAND="__ghostty_hook"
|
||||
fi
|
||||
elif [[ $(builtin declare -p PROMPT_COMMAND 2>/dev/null) == "declare -a "* ]]; then
|
||||
PROMPT_COMMAND+=(__ghostty_hook)
|
||||
else
|
||||
# shellcheck disable=SC2179
|
||||
PROMPT_COMMAND+="; __ghostty_hook"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
builtin source "$(dirname -- "${BASH_SOURCE[0]}")/bash-preexec.sh"
|
||||
preexec_functions+=(__ghostty_preexec)
|
||||
precmd_functions+=(__ghostty_precmd)
|
||||
fi
|
||||
|
||||
@@ -1,42 +1,12 @@
|
||||
{
|
||||
fn restore-xdg-dirs {
|
||||
use str
|
||||
var integration-dir = $E:GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
var xdg-dirs = [(str:split ':' $E:XDG_DATA_DIRS)]
|
||||
var len = (count $xdg-dirs)
|
||||
use platform
|
||||
use str
|
||||
|
||||
var index = $nil
|
||||
range $len | each {|dir-index|
|
||||
if (eq $xdg-dirs[$dir-index] $integration-dir) {
|
||||
set index = $dir-index
|
||||
break
|
||||
}
|
||||
}
|
||||
if (eq $nil $index) { return } # will appear as an error
|
||||
|
||||
if (== 0 $index) {
|
||||
set xdg-dirs = $xdg-dirs[1..]
|
||||
} elif (== (- $len 1) $index) {
|
||||
set xdg-dirs = $xdg-dirs[0..(- $len 1)]
|
||||
} else {
|
||||
# no builtin function for this : )
|
||||
set xdg-dirs = [ (take $index $xdg-dirs) (drop (+ 1 $index) $xdg-dirs) ]
|
||||
}
|
||||
|
||||
if (== 0 (count $xdg-dirs)) {
|
||||
unset-env XDG_DATA_DIRS
|
||||
} else {
|
||||
set-env XDG_DATA_DIRS (str:join ':' $xdg-dirs)
|
||||
}
|
||||
# Clean up XDG_DATA_DIRS by removing GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
if (and (has-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR) (has-env XDG_DATA_DIRS)) {
|
||||
set-env XDG_DATA_DIRS (str:replace $E:GHOSTTY_SHELL_INTEGRATION_XDG_DIR":" "" $E:XDG_DATA_DIRS)
|
||||
unset-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
}
|
||||
if (and (has-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR) (has-env XDG_DATA_DIRS)) {
|
||||
restore-xdg-dirs
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
use str
|
||||
|
||||
# List of enabled shell integration features
|
||||
var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)]
|
||||
@@ -77,24 +47,22 @@
|
||||
printf "\e]133;D;"$exit-status"\a"
|
||||
}
|
||||
|
||||
fn report-pwd {
|
||||
use platform
|
||||
printf "\e]7;kitty-shell-cwd://%s%s\a" (platform:hostname) $pwd
|
||||
}
|
||||
|
||||
fn sudo-with-terminfo {|@args|
|
||||
var sudoedit = $false
|
||||
for arg $args {
|
||||
use str
|
||||
if (str:has-prefix $arg -) {
|
||||
if (has-value [e -edit] $arg[1..]) {
|
||||
if (str:has-prefix $arg --) {
|
||||
if (eq $arg --edit) {
|
||||
set sudoedit = $true
|
||||
break
|
||||
}
|
||||
continue
|
||||
} elif (str:has-prefix $arg -) {
|
||||
if (str:contains (str:trim-prefix $arg -) e) {
|
||||
set sudoedit = $true
|
||||
break
|
||||
}
|
||||
} elif (not (str:contains $arg =)) {
|
||||
break
|
||||
}
|
||||
|
||||
if (not (has-value $arg =)) { break }
|
||||
}
|
||||
|
||||
if (not $sudoedit) { set args = [ --preserve-env=TERMINFO $@args ] }
|
||||
@@ -180,16 +148,12 @@
|
||||
|
||||
defer {
|
||||
mark-prompt-start
|
||||
report-pwd
|
||||
}
|
||||
|
||||
set edit:before-readline = (conj $edit:before-readline $mark-prompt-start~)
|
||||
set edit:after-readline = (conj $edit:after-readline $mark-output-start~)
|
||||
set edit:after-command = (conj $edit:after-command $mark-output-end~)
|
||||
|
||||
if (has-value $features title) {
|
||||
set after-chdir = (conj $after-chdir {|_| report-pwd })
|
||||
}
|
||||
if (has-value $features cursor) {
|
||||
fn beam { printf "\e[5 q" }
|
||||
fn block { printf "\e[0 q" }
|
||||
@@ -207,4 +171,9 @@
|
||||
if (and (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-) (has-external ssh)) {
|
||||
edit:add-var ssh~ $ssh-integration~
|
||||
}
|
||||
|
||||
# Report changes to the current directory.
|
||||
fn report-pwd { printf "\e]7;kitty-shell-cwd://%s%s\a" (platform:hostname) $pwd }
|
||||
set after-chdir = (conj $after-chdir {|_| report-pwd })
|
||||
report-pwd
|
||||
}
|
||||
|
||||
@@ -51,6 +51,27 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
|
||||
set --local features (string split , $GHOSTTY_SHELL_FEATURES)
|
||||
|
||||
# Parse the fish version for feature detection.
|
||||
# Default to 0.0 if version is unavailable or malformed.
|
||||
set -l fish_major 0
|
||||
set -l fish_minor 0
|
||||
if set -q version[1]
|
||||
set -l fish_ver (string match -r '(\d+)\.(\d+)' -- $version[1])
|
||||
if set -q fish_ver[2]; and test -n "$fish_ver[2]"
|
||||
set fish_major "$fish_ver[2]"
|
||||
end
|
||||
if set -q fish_ver[3]; and test -n "$fish_ver[3]"
|
||||
set fish_minor "$fish_ver[3]"
|
||||
end
|
||||
end
|
||||
|
||||
# Our OSC133A (prompt start) sequence. If we're using Fish >= 4.1
|
||||
# then it supports click_events so we enable that.
|
||||
set -g __ghostty_prompt_start_mark "\e]133;A\a"
|
||||
if test "$fish_major" -gt 4; or test "$fish_major" -eq 4 -a "$fish_minor" -ge 1
|
||||
set -g __ghostty_prompt_start_mark "\e]133;A;click_events=1\a"
|
||||
end
|
||||
|
||||
if contains cursor $features
|
||||
# Change the cursor to a beam on prompt.
|
||||
function __ghostty_set_cursor_beam --on-event fish_prompt -d "Set cursor shape"
|
||||
@@ -72,14 +93,14 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
|
||||
# When using sudo shell integration feature, ensure $TERMINFO is set
|
||||
# and `sudo` is not already a function or alias
|
||||
if contains sudo $features; and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x")
|
||||
if contains sudo $features; and test -n "$TERMINFO"; and test file = (type -t sudo 2> /dev/null; or echo "x")
|
||||
# Wrap `sudo` command to ensure Ghostty terminfo is preserved
|
||||
function sudo -d "Wrap sudo to preserve terminfo"
|
||||
set --function sudo_has_sudoedit_flags "no"
|
||||
set --function sudo_has_sudoedit_flags no
|
||||
for arg in $argv
|
||||
# Check if argument is '-e' or '--edit' (sudoedit flags)
|
||||
if string match -q -- "-e" "$arg"; or string match -q -- "--edit" "$arg"
|
||||
set --function sudo_has_sudoedit_flags "yes"
|
||||
if string match -q -- -e "$arg"; or string match -q -- --edit "$arg"
|
||||
set --function sudo_has_sudoedit_flags yes
|
||||
break
|
||||
end
|
||||
# Check if argument is neither an option nor a key-value pair
|
||||
@@ -87,7 +108,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
break
|
||||
end
|
||||
end
|
||||
if test "$sudo_has_sudoedit_flags" = "yes"
|
||||
if test "$sudo_has_sudoedit_flags" = yes
|
||||
command sudo $argv
|
||||
else
|
||||
command sudo --preserve-env=TERMINFO $argv
|
||||
@@ -100,7 +121,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
if contains ssh-env $features; or contains ssh-terminfo $features
|
||||
function ssh --wraps=ssh --description "SSH wrapper with Ghostty integration"
|
||||
set -l features (string split ',' -- "$GHOSTTY_SHELL_FEATURES")
|
||||
set -l ssh_term "xterm-256color"
|
||||
set -l ssh_term xterm-256color
|
||||
set -l ssh_opts
|
||||
|
||||
# Configure environment variables for remote session
|
||||
@@ -134,7 +155,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
|
||||
# Check if terminfo is already cached
|
||||
if test -x "$GHOSTTY_BIN_DIR/ghostty"; and "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1
|
||||
set ssh_term "xterm-ghostty"
|
||||
set ssh_term xterm-ghostty
|
||||
else if command -q infocmp
|
||||
set -l ssh_terminfo
|
||||
set -l ssh_cpath_dir
|
||||
@@ -154,7 +175,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1
|
||||
' 2>/dev/null
|
||||
set ssh_term "xterm-ghostty"
|
||||
set ssh_term xterm-ghostty
|
||||
set -a ssh_opts -o "ControlPath=$ssh_cpath"
|
||||
|
||||
# Cache successful installation
|
||||
@@ -179,14 +200,14 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
end
|
||||
|
||||
# Setup prompt marking
|
||||
function __ghostty_mark_prompt_start --on-event fish_prompt --on-event fish_cancel --on-event fish_posterror
|
||||
function __ghostty_mark_prompt_start --on-event fish_prompt --on-event fish_posterror
|
||||
# If we never got the output end event, then we need to send it now.
|
||||
if test "$__ghostty_prompt_state" != prompt-start
|
||||
echo -en "\e]133;D\a"
|
||||
end
|
||||
|
||||
set --global __ghostty_prompt_state prompt-start
|
||||
echo -en "\e]133;A\a"
|
||||
echo -en $__ghostty_prompt_start_mark
|
||||
end
|
||||
|
||||
function __ghostty_mark_output_start --on-event fish_preexec
|
||||
|
||||
@@ -4,22 +4,97 @@ export module ghostty {
|
||||
$feature in ($env.GHOSTTY_SHELL_FEATURES | default "" | split row ',')
|
||||
}
|
||||
|
||||
# Wrap `ssh` with Ghostty TERMINFO support
|
||||
export def --wrapped ssh [...args] {
|
||||
mut ssh_env = {}
|
||||
mut ssh_opts = []
|
||||
|
||||
# `ssh-env`: use xterm-256color and propagate COLORTERM/TERM_PROGRAM vars
|
||||
if (has_feature "ssh-env") {
|
||||
$ssh_env.TERM = "xterm-256color"
|
||||
$ssh_opts = [
|
||||
"-o" "SetEnv COLORTERM=truecolor"
|
||||
"-o" "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION"
|
||||
]
|
||||
}
|
||||
|
||||
# `ssh-terminfo`: auto-install xterm-ghostty terminfo on remote hosts
|
||||
if (has_feature "ssh-terminfo") {
|
||||
let ghostty = ($env.GHOSTTY_BIN_DIR? | default "") | path join "ghostty"
|
||||
|
||||
let ssh_cfg = ^ssh -G ...$args
|
||||
| lines
|
||||
| parse "{key} {value}"
|
||||
| where key in ["user" "hostname"]
|
||||
| select key value
|
||||
| transpose -rd
|
||||
| default {user: $env.USER hostname: "localhost"}
|
||||
let ssh_id = $"($ssh_cfg.user)@($ssh_cfg.hostname)"
|
||||
|
||||
if (^$ghostty "+ssh-cache" $"--host=($ssh_id)" | complete | $in.exit_code == 0) {
|
||||
$ssh_env.TERM = "xterm-ghostty"
|
||||
} else {
|
||||
$ssh_env.TERM = "xterm-256color"
|
||||
|
||||
let terminfo = try {
|
||||
^infocmp -0 -x xterm-ghostty
|
||||
} catch {
|
||||
print -e "infocmp failed, using xterm-256color"
|
||||
}
|
||||
|
||||
if ($terminfo | is-not-empty) {
|
||||
print $"Setting up xterm-ghostty terminfo on ($ssh_cfg.hostname)..."
|
||||
|
||||
let ctrl_path = (
|
||||
mktemp -td $"ghostty-ssh-($ssh_cfg.user).XXXXXX"
|
||||
| path join "socket"
|
||||
)
|
||||
|
||||
let remote_args = $ssh_opts ++ [
|
||||
"-o" "ControlMaster=yes"
|
||||
"-o" $"ControlPath=($ctrl_path)"
|
||||
"-o" "ControlPersist=60s"
|
||||
] ++ $args
|
||||
|
||||
$terminfo | ^ssh ...$remote_args '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1'
|
||||
| complete
|
||||
| if $in.exit_code == 0 {
|
||||
^$ghostty "+ssh-cache" $"--add=($ssh_id)" e>| print -e
|
||||
$ssh_env.TERM = "xterm-ghostty"
|
||||
$ssh_opts = ($ssh_opts ++ ["-o" $"ControlPath=($ctrl_path)"])
|
||||
} else {
|
||||
print -e "terminfo install failed, using xterm-256color"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ssh_args = $ssh_opts ++ $args
|
||||
with-env $ssh_env {
|
||||
^ssh ...$ssh_args
|
||||
}
|
||||
}
|
||||
|
||||
# Wrap `sudo` to preserve Ghostty's TERMINFO environment variable
|
||||
export def --wrapped sudo [
|
||||
...args # Arguments to pass to `sudo`
|
||||
] {
|
||||
export def --wrapped sudo [...args] {
|
||||
mut sudo_args = $args
|
||||
|
||||
if (has_feature "sudo") {
|
||||
# Extract just the sudo options (before the command)
|
||||
let sudo_options = ($args | take until {|arg|
|
||||
not (($arg | str starts-with "-") or ($arg | str contains "="))
|
||||
})
|
||||
|
||||
# Prepend TERMINFO preservation flag if not using sudoedit
|
||||
if (not ("-e" in $sudo_options or "--edit" in $sudo_options)) {
|
||||
$sudo_args = ($args | prepend "--preserve-env=TERMINFO")
|
||||
# Extract just the sudo options (before the command)
|
||||
let sudo_options = (
|
||||
$args | take until {|arg|
|
||||
not (($arg | str starts-with "-") or ($arg | str contains "="))
|
||||
}
|
||||
)
|
||||
|
||||
# Prepend TERMINFO preservation flag if not using sudoedit
|
||||
if (not ("-e" in $sudo_options or "--edit" in $sudo_options)) {
|
||||
$sudo_args = ($args | prepend "--preserve-env=TERMINFO")
|
||||
}
|
||||
}
|
||||
|
||||
^sudo ...$sudo_args
|
||||
|
||||
@@ -121,7 +121,7 @@ _ghostty_deferred_init() {
|
||||
fi
|
||||
fi
|
||||
|
||||
builtin local mark1=$'%{\e]133;A\a%}'
|
||||
builtin local mark1=$'%{\e]133;A;cl=line\a%}'
|
||||
if [[ -o prompt_percent ]]; then
|
||||
builtin typeset -g precmd_functions
|
||||
if [[ ${precmd_functions[-1]} == _ghostty_precmd ]]; then
|
||||
@@ -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
@@ -32,7 +32,7 @@ all: std.EnumMap(Key, *Screen),
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
opts: Screen.Options,
|
||||
) !ScreenSet {
|
||||
) Allocator.Error!ScreenSet {
|
||||
// We need to initialize our initial primary screen
|
||||
const screen = try alloc.create(Screen);
|
||||
errdefer alloc.destroy(screen);
|
||||
@@ -64,7 +64,7 @@ pub fn getInit(
|
||||
alloc: Allocator,
|
||||
key: Key,
|
||||
opts: Screen.Options,
|
||||
) !*Screen {
|
||||
) Allocator.Error!*Screen {
|
||||
if (self.get(key)) |screen| return screen;
|
||||
const screen = try alloc.create(Screen);
|
||||
errdefer alloc.destroy(screen);
|
||||
|
||||
@@ -3,6 +3,7 @@ const Selection = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const page = @import("page.zig");
|
||||
const point = @import("point.zig");
|
||||
const PageList = @import("PageList.zig");
|
||||
@@ -126,7 +127,7 @@ pub fn tracked(self: *const Selection) bool {
|
||||
|
||||
/// Convert this selection a tracked selection. It is asserted this is
|
||||
/// an untracked selection. The tracked selection is returned.
|
||||
pub fn track(self: *const Selection, s: *Screen) !Selection {
|
||||
pub fn track(self: *const Selection, s: *Screen) Allocator.Error!Selection {
|
||||
assert(!self.tracked());
|
||||
|
||||
// Track our pins
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
const Tabstops = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const tripwire = @import("../tripwire.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const testing = std.testing;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
@@ -58,7 +59,11 @@ inline fn index(col: usize) usize {
|
||||
return @mod(col, unit_bits);
|
||||
}
|
||||
|
||||
pub fn init(alloc: Allocator, cols: usize, interval: usize) !Tabstops {
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
cols: usize,
|
||||
interval: usize,
|
||||
) Allocator.Error!Tabstops {
|
||||
var res: Tabstops = .{};
|
||||
try res.resize(alloc, cols);
|
||||
res.reset(interval);
|
||||
@@ -114,21 +119,36 @@ pub fn get(self: Tabstops, col: usize) bool {
|
||||
return unit & mask == mask;
|
||||
}
|
||||
|
||||
const resize_tw = tripwire.module(enum {
|
||||
dynamic_alloc,
|
||||
}, resize);
|
||||
|
||||
/// Resize this to support up to cols columns.
|
||||
// TODO: needs interval to set new tabstops
|
||||
pub fn resize(self: *Tabstops, alloc: Allocator, cols: usize) !void {
|
||||
// Set our new value
|
||||
self.cols = cols;
|
||||
pub fn resize(
|
||||
self: *Tabstops,
|
||||
alloc: Allocator,
|
||||
cols: usize,
|
||||
) Allocator.Error!void {
|
||||
const tw = resize_tw;
|
||||
|
||||
// Do nothing if it fits.
|
||||
if (cols <= prealloc_columns) return;
|
||||
if (cols <= prealloc_columns) {
|
||||
self.cols = cols;
|
||||
return;
|
||||
}
|
||||
|
||||
// What we need in the dynamic size
|
||||
const size = cols - prealloc_columns;
|
||||
if (size < self.dynamic_stops.len) return;
|
||||
if (size < self.dynamic_stops.len) {
|
||||
self.cols = cols;
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: we can probably try to realloc here but I'm not sure it matters.
|
||||
try tw.check(.dynamic_alloc);
|
||||
const new = try alloc.alloc(Unit, size);
|
||||
errdefer comptime unreachable;
|
||||
@memset(new, 0);
|
||||
if (self.dynamic_stops.len > 0) {
|
||||
fastmem.copy(Unit, new, self.dynamic_stops);
|
||||
@@ -136,6 +156,7 @@ pub fn resize(self: *Tabstops, alloc: Allocator, cols: usize) !void {
|
||||
}
|
||||
|
||||
self.dynamic_stops = new;
|
||||
self.cols = cols;
|
||||
}
|
||||
|
||||
/// Return the maximum number of columns this can support currently.
|
||||
@@ -230,3 +251,21 @@ test "Tabstops: count on 80" {
|
||||
|
||||
try testing.expectEqual(@as(usize, 9), count);
|
||||
}
|
||||
|
||||
test "Tabstops: resize alloc failure preserves state" {
|
||||
// This test verifies that if resize() fails during allocation,
|
||||
// the original cols value is preserved (not corrupted).
|
||||
var t: Tabstops = try init(testing.allocator, 80, 8);
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
const original_cols = t.cols;
|
||||
|
||||
// Trigger allocation failure when resizing beyond prealloc
|
||||
resize_tw.errorAlways(.dynamic_alloc, error.OutOfMemory);
|
||||
const result = t.resize(testing.allocator, prealloc_columns * 2);
|
||||
try testing.expectError(error.OutOfMemory, result);
|
||||
try resize_tw.end(.reset);
|
||||
|
||||
// cols should be unchanged after failed resize
|
||||
try testing.expectEqual(original_cols, t.cols);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -168,7 +168,7 @@ pub const Name = enum(u8) {
|
||||
}
|
||||
|
||||
/// Default colors for tagged values.
|
||||
pub fn default(self: Name) !RGB {
|
||||
pub fn default(self: Name) error{NoDefaultValue}!RGB {
|
||||
return switch (self) {
|
||||
.black => RGB{ .r = 0x1D, .g = 0x1F, .b = 0x21 },
|
||||
.red => RGB{ .r = 0xCC, .g = 0x66, .b = 0x66 },
|
||||
@@ -355,7 +355,7 @@ pub const RGB = packed struct(u24) {
|
||||
/// Parse a color from a floating point intensity value.
|
||||
///
|
||||
/// The value should be between 0.0 and 1.0, inclusive.
|
||||
fn fromIntensity(value: []const u8) !u8 {
|
||||
fn fromIntensity(value: []const u8) error{InvalidFormat}!u8 {
|
||||
const i = std.fmt.parseFloat(f64, value) catch {
|
||||
@branchHint(.cold);
|
||||
return error.InvalidFormat;
|
||||
@@ -372,7 +372,7 @@ pub const RGB = packed struct(u24) {
|
||||
///
|
||||
/// The string can contain 1, 2, 3, or 4 characters and represents the color
|
||||
/// value scaled in 4, 8, 12, or 16 bits, respectively.
|
||||
fn fromHex(value: []const u8) !u8 {
|
||||
fn fromHex(value: []const u8) error{InvalidFormat}!u8 {
|
||||
if (value.len == 0 or value.len > 4) {
|
||||
@branchHint(.cold);
|
||||
return error.InvalidFormat;
|
||||
@@ -414,7 +414,7 @@ pub const RGB = packed struct(u24) {
|
||||
/// where `r`, `g`, and `b` are a single hexadecimal digit.
|
||||
/// These specify a color with 4, 8, 12, and 16 bits of precision
|
||||
/// per color channel.
|
||||
pub fn parse(value: []const u8) !RGB {
|
||||
pub fn parse(value: []const u8) error{InvalidFormat}!RGB {
|
||||
if (value.len == 0) {
|
||||
@branchHint(.cold);
|
||||
return error.InvalidFormat;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -41,74 +42,8 @@ pub const Command = union(Key) {
|
||||
/// in the log.
|
||||
change_window_icon: [:0]const u8,
|
||||
|
||||
/// First do a fresh-line. Then start a new command, and enter prompt mode:
|
||||
/// 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). Note: I've noticed
|
||||
/// not all shells will send the prompt end code.
|
||||
prompt_start: struct {
|
||||
/// "aid" is an optional "application identifier" that helps disambiguate
|
||||
/// nested shell sessions. It can be anything but is usually a process ID.
|
||||
aid: ?[:0]const u8 = null,
|
||||
/// "kind" tells us which kind of semantic prompt sequence this is:
|
||||
/// - primary: normal, left-aligned first-line prompt (initial, default)
|
||||
/// - continuation: an editable continuation line
|
||||
/// - secondary: a non-editable continuation line
|
||||
/// - right: a right-aligned prompt that may need adjustment during reflow
|
||||
kind: enum { primary, continuation, secondary, right } = .primary,
|
||||
/// If true, the shell will not redraw the prompt on resize so don't erase it.
|
||||
/// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||
redraw: bool = true,
|
||||
/// Use a special key instead of arrow keys to move the cursor on
|
||||
/// mouse click. Useful if arrow keys have side-effets like triggering
|
||||
/// auto-complete. The shell integration script should bind the special
|
||||
/// key as needed.
|
||||
/// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||
special_key: bool = false,
|
||||
/// If true, the shell is capable of handling mouse click events.
|
||||
/// Ghostty will then send a click event to the shell when the user
|
||||
/// clicks somewhere in the prompt. The shell can then move the cursor
|
||||
/// to that position or perform some other appropriate action. If false,
|
||||
/// Ghostty may generate a number of fake key events to move the cursor
|
||||
/// which is not very robust.
|
||||
/// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||
click_events: bool = false,
|
||||
},
|
||||
|
||||
/// End of prompt and start of user input, terminated by a OSC "133;C"
|
||||
/// or another prompt (OSC "133;P").
|
||||
prompt_end: void,
|
||||
|
||||
/// The OSC "133;C" command can be used to explicitly end
|
||||
/// the input area and begin the output area. However, some applications
|
||||
/// don't provide a convenient way to emit that command.
|
||||
/// That is why we also specify an implicit way to end the input area
|
||||
/// at the end of the line. In the case of multiple input lines: If the
|
||||
/// cursor is on a fresh (empty) line and we see either OSC "133;P" or
|
||||
/// OSC "133;I" then this is the start of a continuation input line.
|
||||
/// If we see anything else, it is the start of the output area (or end
|
||||
/// of command).
|
||||
end_of_input: struct {
|
||||
/// The command line that the user entered.
|
||||
/// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||
cmdline: ?[:0]const u8 = null,
|
||||
},
|
||||
|
||||
/// End of current command.
|
||||
///
|
||||
/// The exit-code need not be specified if there are no options,
|
||||
/// or if the command was cancelled (no OSC "133;C"), such as by typing
|
||||
/// an interrupt/cancel character (typically ctrl-C) during line-editing.
|
||||
/// Otherwise, it must be an integer code, where 0 means the command
|
||||
/// succeeded, and other values indicate failure. In additing to the
|
||||
/// exit-code there may be an err= option, which non-legacy terminals
|
||||
/// should give precedence to. The err=_value_ option is more general:
|
||||
/// an empty string is success, and any non-empty value (which need not
|
||||
/// be an integer) is an error code. So to indicate success both ways you
|
||||
/// could send OSC "133;D;0;err=\007", though `OSC "133;D;0\007" is shorter.
|
||||
end_of_command: struct {
|
||||
exit_code: ?u8 = null,
|
||||
// TODO: err option
|
||||
},
|
||||
/// Semantic prompt command: https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md
|
||||
semantic_prompt: SemanticPrompt,
|
||||
|
||||
/// Set or get clipboard contents. If data is null, then the current
|
||||
/// clipboard contents are sent to the pty. If data is set, this
|
||||
@@ -193,9 +128,33 @@ pub const Command = union(Key) {
|
||||
/// ConEmu GUI macro (OSC 9;6)
|
||||
conemu_guimacro: [:0]const u8,
|
||||
|
||||
/// ConEmu run process (OSC 9;7)
|
||||
conemu_run_process: [:0]const u8,
|
||||
|
||||
/// ConEmu output environment variable (OSC 9;8)
|
||||
conemu_output_environment_variable: [:0]const u8,
|
||||
|
||||
/// ConEmu XTerm keyboard and output emulation (OSC 9;10)
|
||||
/// https://conemu.github.io/en/TerminalModes.html
|
||||
conemu_xterm_emulation: struct {
|
||||
/// null => do not change
|
||||
/// false => turn off
|
||||
/// true => turn on
|
||||
keyboard: ?bool,
|
||||
/// null => do not change
|
||||
/// false => turn off
|
||||
/// true => turn on
|
||||
output: ?bool,
|
||||
},
|
||||
|
||||
/// ConEmu comment (OSC 9;11)
|
||||
conemu_comment: [:0]const u8,
|
||||
|
||||
/// Kitty text sizing protocol (OSC 66)
|
||||
kitty_text_sizing: parsers.kitty_text_sizing.OSC,
|
||||
|
||||
pub const SemanticPrompt = parsers.semantic_prompt.Command;
|
||||
|
||||
pub const Key = LibEnum(
|
||||
if (build_options.c_abi) .c else .zig,
|
||||
// NOTE: Order matters, see LibEnum documentation.
|
||||
@@ -203,10 +162,7 @@ pub const Command = union(Key) {
|
||||
"invalid",
|
||||
"change_window_title",
|
||||
"change_window_icon",
|
||||
"prompt_start",
|
||||
"prompt_end",
|
||||
"end_of_input",
|
||||
"end_of_command",
|
||||
"semantic_prompt",
|
||||
"clipboard_contents",
|
||||
"report_pwd",
|
||||
"mouse_shape",
|
||||
@@ -221,6 +177,10 @@ pub const Command = union(Key) {
|
||||
"conemu_progress_report",
|
||||
"conemu_wait_input",
|
||||
"conemu_guimacro",
|
||||
"conemu_run_process",
|
||||
"conemu_output_environment_variable",
|
||||
"conemu_xterm_emulation",
|
||||
"conemu_comment",
|
||||
"kitty_text_sizing",
|
||||
},
|
||||
);
|
||||
@@ -380,6 +340,7 @@ pub const Parser = struct {
|
||||
@"119",
|
||||
@"133",
|
||||
@"777",
|
||||
@"1337",
|
||||
};
|
||||
|
||||
pub fn init(alloc: ?Allocator) Parser {
|
||||
@@ -424,20 +385,21 @@ pub const Parser = struct {
|
||||
.clipboard_contents,
|
||||
.color_operation,
|
||||
.conemu_change_tab_title,
|
||||
.conemu_comment,
|
||||
.conemu_guimacro,
|
||||
.conemu_output_environment_variable,
|
||||
.conemu_progress_report,
|
||||
.conemu_run_process,
|
||||
.conemu_show_message_box,
|
||||
.conemu_sleep,
|
||||
.conemu_wait_input,
|
||||
.end_of_command,
|
||||
.end_of_input,
|
||||
.conemu_xterm_emulation,
|
||||
.hyperlink_end,
|
||||
.hyperlink_start,
|
||||
.invalid,
|
||||
.mouse_shape,
|
||||
.prompt_end,
|
||||
.prompt_start,
|
||||
.report_pwd,
|
||||
.semantic_prompt,
|
||||
.show_desktop_notification,
|
||||
.kitty_text_sizing,
|
||||
=> {},
|
||||
@@ -633,8 +595,20 @@ pub const Parser = struct {
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.@"0",
|
||||
.@"133",
|
||||
=> switch (c) {
|
||||
';' => self.writeToFixed(),
|
||||
'7' => self.state = .@"1337",
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.@"1337",
|
||||
=> switch (c) {
|
||||
';' => self.writeToFixed(),
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.@"0",
|
||||
.@"22",
|
||||
.@"777",
|
||||
.@"8",
|
||||
@@ -711,6 +685,8 @@ pub const Parser = struct {
|
||||
.@"133" => parsers.semantic_prompt.parse(self, terminator_ch),
|
||||
|
||||
.@"777" => parsers.rxvt_extension.parse(self, terminator_ch),
|
||||
|
||||
.@"1337" => parsers.iterm2.parse(self, terminator_ch),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user