diff --git a/.gitattributes b/.gitattributes
index eef19e09b9..e6f3c2789b 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,6 @@
+build.zig.zon.nix linguist-generated=true
+build.zig.zon.txt linguist-generated=true
+build.zig.zon2json-lock linguist-generated=true
vendor/** linguist-vendored
website/** linguist-documentation
pkg/breakpad/vendor/** linguist-vendored
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
index d5ee328e50..ec55f2dffd 100644
--- a/.github/workflows/nix.yml
+++ b/.github/workflows/nix.yml
@@ -1,6 +1,31 @@
on: [push, pull_request]
name: Nix
jobs:
+ required:
+ name: "Required Checks: Nix"
+ runs-on: namespace-profile-ghostty-sm
+ needs:
+ - check-zig-cache-hash
+ steps:
+ - id: status
+ name: Determine status
+ run: |
+ results=$(tr -d '\n' <<< '${{ toJSON(needs.*.result) }}')
+ if ! grep -q -v -E '(failure|cancelled)' <<< "$results"; then
+ result="failed"
+ else
+ result="success"
+ fi
+ {
+ echo "result=${result}"
+ echo "results=${results}"
+ } | tee -a "$GITHUB_OUTPUT"
+ - if: always() && steps.status.outputs.result != 'success'
+ name: Check for failed status
+ run: |
+ echo "One or more required build workflows failed: ${{ steps.status.outputs.results }}"
+ exit 1
+
check-zig-cache-hash:
if: github.repository == 'ghostty-org/ghostty'
runs-on: namespace-profile-ghostty-sm
@@ -25,5 +50,5 @@ jobs:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
useDaemon: false # sometimes fails on short jobs
- - name: Check Zig cache hash
- run: nix develop -c ./nix/build-support/check-zig-cache-hash.sh
+ - name: Check Zig cache
+ run: nix develop -c ./nix/build-support/check-zig-cache.sh
diff --git a/.github/workflows/publish-tag.yml b/.github/workflows/publish-tag.yml
new file mode 100644
index 0000000000..4589821400
--- /dev/null
+++ b/.github/workflows/publish-tag.yml
@@ -0,0 +1,74 @@
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version to deploy (format: vX.Y.Z)"
+ required: true
+
+name: Publish Tagged Release
+
+# We must only run one release workflow at a time to prevent corrupting
+# our release artifacts.
+concurrency:
+ group: ${{ github.workflow }}
+ cancel-in-progress: false
+
+jobs:
+ setup:
+ runs-on: namespace-profile-ghostty-sm
+ outputs:
+ version: ${{ steps.extract_version.outputs.version }}
+ steps:
+ - name: Validate Version Input
+ run: |
+ if [[ ! "${{ github.event.inputs.version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "Error: Version must follow the format vX.Y.Z (e.g., v1.0.0)."
+ exit 1
+ fi
+
+ echo "Version is valid: ${{ github.event.inputs.version }}"
+
+ - name: Exract the Version
+ id: extract_version
+ run: |
+ VERSION=${{ github.event.inputs.version }}
+ VERSION=${VERSION#v}
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+
+ upload:
+ needs: [setup]
+ runs-on: namespace-profile-ghostty-sm
+ env:
+ GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
+ steps:
+ - name: Validate Release Files
+ run: |
+ BASE="https://release.files.ghostty.org/${GHOSTTY_VERSION}"
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-${GHOSTTY_VERSION}.tar.gz" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-${GHOSTTY_VERSION}.tar.gz.minisig" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-source.tar.gz" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-source.tar.gz.minisig" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-macos-universal.zip" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-macos-universal-dsym.zip" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/Ghostty.dmg" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/appcast-staged.xml" | grep -q "^200$" || exit 1
+
+ - name: Download Staged Appcast
+ run: |
+ curl -L https://release.files.ghostty.org/${GHOSTTY_VERSION}/appcast-staged.xml > appcast-staged.xml
+ mv appcast-staged.xml appcast.xml
+
+ - name: Upload Appcast
+ run: |
+ rm -rf blob
+ mkdir blob
+ mv appcast.xml blob/appcast.xml
+ - name: Upload Appcast to R2
+ uses: ryand56/r2-upload-action@latest
+ with:
+ r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
+ r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
+ r2-secret-access-key: ${{ secrets.CF_R2_RELEASE_SECRET_KEY }}
+ r2-bucket: ghostty-release
+ source-dir: blob
+ destination-dir: ./
diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml
index e647c97203..ced4979977 100644
--- a/.github/workflows/release-pr.yml
+++ b/.github/workflows/release-pr.yml
@@ -68,7 +68,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
- SPARKLE_VERSION: 2.6.3
+ SPARKLE_VERSION: 2.6.4
run: |
mkdir -p .action/sparkle
cd .action/sparkle
diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml
index cf94bf23ef..1789576a8c 100644
--- a/.github/workflows/release-tag.yml
+++ b/.github/workflows/release-tag.yml
@@ -7,6 +7,7 @@ on:
upload:
description: "Upload final artifacts to R2"
default: false
+
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
@@ -135,7 +136,7 @@ jobs:
- name: Setup Sparkle
env:
- SPARKLE_VERSION: 2.6.3
+ SPARKLE_VERSION: 2.6.4
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -297,7 +298,7 @@ jobs:
- name: Setup Sparkle
env:
- SPARKLE_VERSION: 2.6.3
+ SPARKLE_VERSION: 2.6.4
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -367,6 +368,7 @@ jobs:
mv ghostty-macos-universal.zip blob/${GHOSTTY_VERSION}/ghostty-macos-universal.zip
mv ghostty-macos-universal-dsym.zip blob/${GHOSTTY_VERSION}/ghostty-macos-universal-dsym.zip
mv Ghostty.dmg blob/${GHOSTTY_VERSION}/Ghostty.dmg
+ mv appcast.xml blob/${GHOSTTY_VERSION}/appcast-staged.xml
- name: Upload to R2
uses: ryand56/r2-upload-action@latest
with:
@@ -376,18 +378,3 @@ jobs:
r2-bucket: ghostty-release
source-dir: blob
destination-dir: ./
-
- - name: Prep Appcast
- run: |
- rm -rf blob
- mkdir blob
- mv appcast.xml blob/appcast.xml
- - name: Upload Appcast to R2
- uses: ryand56/r2-upload-action@latest
- with:
- r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
- r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
- r2-secret-access-key: ${{ secrets.CF_R2_RELEASE_SECRET_KEY }}
- r2-bucket: ghostty-release
- source-dir: blob
- destination-dir: ./
diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml
index e239dda4f3..a031f3ad14 100644
--- a/.github/workflows/release-tip.yml
+++ b/.github/workflows/release-tip.yml
@@ -164,7 +164,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
- SPARKLE_VERSION: 2.6.3
+ SPARKLE_VERSION: 2.6.4
run: |
mkdir -p .action/sparkle
cd .action/sparkle
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 81d58a1efe..e64b2c4d85 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -6,6 +6,47 @@ on:
name: Test
jobs:
+ required:
+ name: "Required Checks: Test"
+ runs-on: namespace-profile-ghostty-sm
+ needs:
+ - build
+ - build-bench
+ - build-linux-libghostty
+ - build-nix
+ - build-snap
+ - build-macos
+ - build-macos-matrix
+ - build-windows
+ - test
+ - test-gtk
+ - test-sentry-linux
+ - test-macos
+ - prettier
+ - alejandra
+ - typos
+ - test-pkg-linux
+ - test-debian-12
+ steps:
+ - id: status
+ name: Determine status
+ run: |
+ results=$(tr -d '\n' <<< '${{ toJSON(needs.*.result) }}')
+ if ! grep -q -v -E '(failure|cancelled)' <<< "$results"; then
+ result="failed"
+ else
+ result="success"
+ fi
+ {
+ echo "result=${result}"
+ echo "results=${results}"
+ } | tee -a "$GITHUB_OUTPUT"
+ - if: always() && steps.status.outputs.result != 'success'
+ name: Check for failed status
+ run: |
+ echo "One or more required build workflows failed: ${{ steps.status.outputs.results }}"
+ exit 1
+
build:
strategy:
fail-fast: false
@@ -163,10 +204,14 @@ jobs:
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app
+ - name: get the Zig deps
+ id: deps
+ run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT
+
# GhosttyKit is the framework that is built from Zig for our native
# Mac app to access.
- name: Build GhosttyKit
- run: nix develop -c zig build
+ run: nix develop -c zig build --system ${{ steps.deps.outputs.deps }}
# The native app is built with native XCode tooling. This also does
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
@@ -199,35 +244,65 @@ jobs:
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app
+ - name: get the Zig deps
+ id: deps
+ run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT
+
- name: Test All
run: |
# OpenGL
- nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=freetype
- nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext
- nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_freetype
- nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_harfbuzz
- nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_noshape
+ nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=freetype
+ nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext
+ nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_freetype
+ nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_harfbuzz
+ nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_noshape
# Metal
- nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=freetype
- nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext
- nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_freetype
- nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_harfbuzz
- nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_noshape
+ nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=freetype
+ nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext
+ nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_freetype
+ nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_harfbuzz
+ nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_noshape
- name: Build All
run: |
- nix develop -c zig build -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=freetype
- nix develop -c zig build -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext
- nix develop -c zig build -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_freetype
- nix develop -c zig build -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_harfbuzz
- nix develop -c zig build -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_noshape
-
- nix develop -c zig build -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=freetype
- nix develop -c zig build -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext
- nix develop -c zig build -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_freetype
- nix develop -c zig build -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_harfbuzz
- nix develop -c zig build -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_noshape
+ nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=freetype
+ nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext
+ nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_freetype
+ nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_harfbuzz
+ nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_noshape
+
+ nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=freetype
+ nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext
+ nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_freetype
+ nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_harfbuzz
+ nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_noshape
+
+ build-snap:
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ [namespace-profile-ghostty-snap, namespace-profile-ghostty-snap-arm64]
+ runs-on: ${{ matrix.os }}
+ needs: test
+ env:
+ ZIG_LOCAL_CACHE_DIR: /zig/local-cache
+ ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ fetch-tags: true
+ - name: Setup Cache
+ uses: namespacelabs/nscloud-cache-action@v1.2.0
+ with:
+ path: |
+ /nix
+ /zig
+ - run: sudo apt install -y udev
+ - run: sudo systemctl start systemd-udevd
+ - uses: snapcore/action-build@v1
build-windows:
runs-on: windows-2022
@@ -247,10 +322,10 @@ jobs:
run: |
# Get the zig version from build.zig so that it only needs to be updated
$fileContent = Get-Content -Path "build.zig" -Raw
- $pattern = 'const required_zig = "(.*?)";'
+ $pattern = 'buildpkg\.requireZig\("(.*?)"\);'
$zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value
- Write-Output $version
$version = "zig-windows-x86_64-$zigVersion"
+ Write-Output $version
$uri = "https://ziglang.org/download/$zigVersion/$version.zip"
Invoke-WebRequest -Uri "$uri" -OutFile ".\zig-windows.zip"
Expand-Archive -Path ".\zig-windows.zip" -DestinationPath ".\" -Force
@@ -327,7 +402,7 @@ jobs:
run: nix develop -c zig build -Dapp-runtime=none test
- name: Test GTK Build
- run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=true -Demit-docs
+ run: nix develop -c zig build -Dapp-runtime=gtk -Demit-docs
- name: Test GLFW Build
run: nix develop -c zig build -Dapp-runtime=glfw
@@ -340,9 +415,9 @@ jobs:
strategy:
fail-fast: false
matrix:
- adwaita: ["true", "false"]
x11: ["true", "false"]
- name: GTK adwaita=${{ matrix.adwaita }} x11=${{ matrix.x11 }}
+ wayland: ["true", "false"]
+ name: GTK x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
runs-on: namespace-profile-ghostty-sm
needs: test
env:
@@ -373,8 +448,8 @@ jobs:
nix develop -c \
zig build \
-Dapp-runtime=gtk \
- -Dgtk-adwaita=${{ matrix.adwaita }} \
- -Dgtk-x11=${{ matrix.x11 }}
+ -Dgtk-x11=${{ matrix.x11 }} \
+ -Dgtk-wayland=${{ matrix.wayland }}
test-sentry-linux:
strategy:
@@ -430,8 +505,12 @@ jobs:
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app
+ - name: get the Zig deps
+ id: deps
+ run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT
+
- name: test
- run: nix develop -c zig build test
+ run: nix develop -c zig build test --system ${{ steps.deps.outputs.deps }}
prettier:
if: github.repository == 'ghostty-org/ghostty'
@@ -548,3 +627,26 @@ jobs:
- name: Test ${{ matrix.pkg }} Build
run: |
nix develop -c sh -c "cd pkg/${{ matrix.pkg }} ; zig build test"
+
+ test-debian-12:
+ name: Test build on Debian 12
+ runs-on: namespace-profile-ghostty-sm
+ needs: test
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install and configure Namespace CLI
+ uses: namespacelabs/nscloud-setup@v0
+
+ - name: Configure Namespace powered Buildx
+ uses: namespacelabs/nscloud-setup-buildx-action@v0
+
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: src/build/docker/debian/Dockerfile
+ build-args: |
+ DISTRO_VERSION=12
+ ZIG_VERSION=0.13.0
diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml
index 569ef6765e..8a6c77ea5f 100644
--- a/.github/workflows/update-colorschemes.yml
+++ b/.github/workflows/update-colorschemes.yml
@@ -48,14 +48,14 @@ jobs:
run: |
# Only proceed if build.zig.zon has changed
if ! git diff --exit-code build.zig.zon; then
- nix develop -c ./nix/build-support/check-zig-cache-hash.sh --update
- nix develop -c ./nix/build-support/check-zig-cache-hash.sh
+ nix develop -c ./nix/build-support/check-zig-cache.sh --update
+ nix develop -c ./nix/build-support/check-zig-cache.sh
fi
# Verify the build still works. We choose an arbitrary build type
# as a canary instead of testing all build types.
- name: Test Build
- run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=true
+ run: nix build .#ghostty
- name: Create pull request
uses: peter-evans/create-pull-request@v7
@@ -66,7 +66,7 @@ jobs:
commit-message: "deps: Update iTerm2 color schemes"
add-paths: |
build.zig.zon
- nix/zigCacheHash.nix
+ build.zig.zon.nix
body: |
Upstream revision: https://github.com/mbadolato/iTerm2-Color-Schemes/tree/${{ steps.zig_fetch.outputs.upstream_rev }}
labels: dependencies
diff --git a/.gitignore b/.gitignore
index 0e301f8c41..db8457e1f8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@ test/cases/**/*.actual.png
glad.zip
/Box_test.ppm
/Box_test_diff.ppm
+/ghostty.qcow2
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000000..835244ebc5
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,149 @@
+# This file documents the subsystem maintainers of the Ghostty project
+# along with the responsibilities of a maintainer and how one can become
+# a maintainer.
+#
+# Ghostty follows a subsystem maintainer model where distinguished
+# contributors (with mutual agreement) are designated as maintainers of a
+# specific subset of the project. A subsystem maintainer has more privileges
+# and authority over a specific part of the project than a regular
+# contributor and deference is given to them when making decisions about
+# their subsystem.
+#
+# Ultimately Ghostty has a BDFL (Benevolent Dictator For Life) model
+# currently with @mitchellh as the BDFL. The BDFL has the final say in all
+# decisions and may override a maintainer's decision if necessary. I like to
+# say its a BDFLFN (Benevolent Dictator For Life "For Now") model because
+# long term I'd like to see the project be more community driven. But for
+# now, early in its life, we're going with this model.
+#
+# ## Privileges
+#
+# - Authority to approve or reject pull requests in their subsystem.
+# - Authority to moderate issues and discussions in their subsystem.
+# - Authority to make roadmap and design decisions about their subsystem
+# with input only from other subsystem maintainers.
+#
+# In all scenarios, the BDFL doesn't need to be consulted for decisions
+# but may revert or override decisions if necessary. The expectation is
+# that maintainers will be trusted to make the right decisions for their
+# subsystem and this will be rare.
+#
+# ## Responsibilities
+#
+# Subsystem maintainership is a voluntary role and maintainers are not
+# expected to dedicate any amount of time to the project. However, if a
+# maintainer is inactive for a long period of time, they may be removed from
+# the maintainers list to avoid bitrot or outdated information.
+#
+# Maintainers are expected to be exemplary members of the community and
+# should be respectful, helpful, and professional in all interactions.
+# This is both in regards to the community at large as well as other
+# subsystem maintainers as well as @mitchellh.
+#
+# As technical leaders, maintainers are expected to be mindful about
+# breaking changes, performance, user impact, and other technical
+# considerations in their subsystem. They should be considerate of large
+# changes and should be able to justify their decisions.
+#
+# Notably, maintainers have NO OBLIGATION to review pull requests or issues
+# in their subsystem. They have full discretion to review or not review
+# anything they want. This isn't a job! It is a role of trust and authority
+# and the expectation is that maintainers will use their best judgement.
+#
+# ## Becoming a Maintainer
+#
+# Maintainer candidates are noticed and proposed by the community. Anyone
+# may propose themselves or someone else as a maintainer. The BDFL along
+# with existing maintainers will discuss and decide.
+#
+# Generally, we want to see consistent high quality contributions to a
+# specific subsystem before considering someone as a maintainer. There isn't
+# an exact number of contributions or time period required but generally
+# we're looking for an order of a dozen or more contributions over a period of
+# months, at least.
+#
+# # Subsystem List
+#
+# The subsystems don't fully cover the entirety of the Ghostty project but
+# are created organically as experts in certain areas emerge. If you feel
+# you are an expert in a certain area and would like to be a maintainer,
+# please reach out to @mitchellh on Discord.
+#
+# (Alphabetical order)
+#
+# - @ghostty-org/font - All things font related including discovery,
+# rasterization, shaping, coloring, etc.
+#
+# - @ghostty-org/gtk - Anything GTK-related in the project, primarily
+# the GTK apprt. Also includes X11/Wayland integrations and general
+# Linux support.
+#
+# - @ghostty-org/macos - The Ghostty macOS app and any macOS-specific
+# features, configurations, etc.
+#
+# - @ghostty-org/renderer - Ghostty rendering subsystem, including the
+# rendering abstractions as well as specific renderers like OpenGL
+# and Metal.
+#
+# - @ghostty-org/shell - Ghostty shell integration, including shell
+# completions, shell detection, and any other shell interactions.
+#
+# - @ghostty-org/terminal - The terminal emulator subsystem, including
+# subprocess management and pty handling, escape sequence parsing,
+# key encoding, etc.
+#
+# ## Outside of Ghostty
+#
+# Other "subsystems" exist outside of Ghostty and will not be represented
+# in this CODEOWNERS file:
+#
+# - @ghostty-org/discord-bot - Maintainers of the Ghostty Discord bot.
+#
+# - @ghostty-org/website - Maintainers of the Ghostty website.
+
+# Font
+/src/font/ @ghostty-org/font
+/pkg/fontconfig/ @ghostty-org/font
+/pkg/freetype/ @ghostty-org/font
+/pkg/harfbuzz/ @ghostty-org/font
+
+# GTK
+/src/apprt/gtk/ @ghostty-org/gtk
+/src/os/cgroup.zig @ghostty-org/gtk
+/src/os/flatpak.zig @ghostty-org/gtk
+/dist/linux/ @ghostty-org/gtk
+
+# macOS
+#
+# This includes libghostty because the macOS apprt is built on top of
+# libghostty and often requires or is impacted by changes to libghostty.
+# macOS subsystem maintainers are expected to only work on libghostty
+# insofar as it impacts the macOS apprt.
+/include/ghostty.h @ghostty-org/macos
+/src/apprt/embedded.zig @ghostty-org/macos
+/src/os/cf_release_thread.zig @ghostty-org/macos
+/src/os/macos.zig @ghostty-org/macos
+/macos/ @ghostty-org/macos
+/dist/macos/ @ghostty-org/macos
+/pkg/apple-sdk/ @ghostty-org/macos
+/pkg/macos/ @ghostty-org/macos
+
+# Renderer
+/src/renderer.zig @ghostty-org/renderer
+/src/renderer/ @ghostty-org/renderer
+/pkg/glslang/ @ghostty-org/renderer
+/pkg/opengl/ @ghostty-org/renderer
+/pkg/spirv-cross/ @ghostty-org/renderer
+/pkg/wuffs/ @ghostty-org/renderer
+
+# Shell
+/src/shell-integration/ @ghostty-org/shell
+/src/termio/shell-integration.zig @ghostty-org/shell
+
+# Terminal
+/src/simd/ @ghostty-org/terminal
+/src/terminal/ @ghostty-org/terminal
+/src/terminfo/ @ghostty-org/terminal
+/src/unicode/ @ghostty-org/terminal
+/src/Surface.zig @ghostty-org/terminal
+/src/surface_mouse.zig @ghostty-org/terminal
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index af3c30be7d..e4d148df87 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -77,3 +77,183 @@ pull request will be accepted with a high degree of certainty.
> **Pull requests are NOT a place to discuss feature design.** Please do
> not open a WIP pull request to discuss a feature. Instead, use a discussion
> and link to your branch.
+
+# Developer Guide
+
+> [!NOTE]
+>
+> **The remainder of this file is dedicated to developers actively
+> working on Ghostty.** If you're a user reporting an issue, you can
+> ignore the rest of this document.
+
+## Input Stack Testing
+
+The input stack is the part of the codebase that starts with a
+key event and ends with text encoding being sent to the pty (it
+does not include _rendering_ the text, which is part of the
+font or rendering stack).
+
+If you modify any part of the input stack, you must manually verify
+all the following input cases work properly. We unfortunately do
+not automate this in any way, but if we can do that one day that'd
+save a LOT of grief and time.
+
+Note: this list may not be exhaustive, I'm still working on it.
+
+### Linux IME
+
+IME (Input Method Editors) are a common source of bugs in the input stack,
+especially on Linux since there are multiple different IME systems
+interacting with different windowing systems and application frameworks
+all written by different organizations.
+
+The following matrix should be tested to ensure that all IME input works
+properly:
+
+1. Wayland, X11
+2. ibus, fcitx, none
+3. Dead key input (e.g. Spanish), CJK (e.g. Japanese), Emoji, Unicode Hex
+4. ibus versions: 1.5.29, 1.5.30, 1.5.31 (each exhibit slightly different behaviors)
+
+> [!NOTE]
+>
+> This is a **work in progress**. I'm still working on this list and it
+> is not complete. As I find more test cases, I will add them here.
+
+#### Dead Key Input
+
+Set your keyboard layout to "Spanish" (or another layout that uses dead keys).
+
+1. Launch Ghostty
+2. Press `'`
+3. Press `a`
+4. Verify that `á` is displayed
+
+Note that the dead key may or may not show a preedit state visually.
+For ibus and fcitx it does but for the "none" case it does not. Importantly,
+the text should be correct when it is sent to the pty.
+
+We should also test canceling dead key input:
+
+1. Launch Ghostty
+2. Press `'`
+3. Press escape
+4. Press `a`
+5. Verify that `a` is displayed (no diacritic)
+
+#### CJK Input
+
+Configure fcitx or ibus with a keyboard layout like Japanese or Mozc. The
+exact layout doesn't matter.
+
+1. Launch Ghostty
+2. Press `Ctrl+Shift` to switch to "Hiragana"
+3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
+4. Press `Enter`
+5. Verify that `こん` is displayed in the terminal.
+
+We should also test switching input methods while preedit is active, which
+should commit the text:
+
+1. Launch Ghostty
+2. Press `Ctrl+Shift` to switch to "Hiragana"
+3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
+4. Press `Ctrl+Shift` to switch to another layout (any)
+5. Verify that `こん` is displayed in the terminal as committed text.
+
+## Nix Virtual Machines
+
+Several Nix virtual machine definitions are provided by the project for testing
+and developing Ghostty against multiple different Linux desktop environments.
+
+Running these requires a working Nix installation, either Nix on your
+favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further
+requirements for macOS are detailed below.
+
+VMs should only be run on your local desktop and then powered off when not in
+use, which will discard any changes to the VM.
+
+The VM definitions provide minimal software "out of the box" but additional
+software can be installed by using standard Nix mechanisms like `nix run nixpkgs#`.
+
+### Linux
+
+1. Check out the Ghostty source and change to the directory.
+2. Run `nix run .#`. `` can be any of the VMs defined in the
+ `nix/vm` directory (without the `.nix` suffix) excluding any file prefixed
+ with `common` or `create`.
+3. The VM will build and then launch. Depending on the speed of your system, this
+ can take a while, but eventually you should get a new VM window.
+4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending
+ on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be
+ writable by the VM user, so be careful!
+
+### macOS
+
+1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin`
+ config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your
+ configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/)
+ blog post for more information about the Linux builder and how to tune the performance.
+2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions
+ above to launch a VM.
+
+### Custom VMs
+
+To easily create a custom VM without modifying the Ghostty source, create a new
+directory, then create a file called `flake.nix` with the following text in the
+new directory.
+
+```
+{
+ inputs = {
+ nixpkgs.url = "nixpkgs/nixpkgs-unstable";
+ ghostty.url = "github:ghostty-org/ghostty";
+ };
+ outputs = {
+ nixpkgs,
+ ghostty,
+ ...
+ }: {
+ nixosConfigurations.custom-vm = ghostty.create-gnome-vm {
+ nixpkgs = nixpkgs;
+ system = "x86_64-linux";
+ overlay = ghostty.overlays.releasefast;
+ # module = ./configuration.nix # also works
+ module = {pkgs, ...}: {
+ environment.systemPackages = [
+ pkgs.btop
+ ];
+ };
+ };
+ };
+}
+```
+
+The custom VM can then be run with a command like this:
+
+```
+nix run .#nixosConfigurations.custom-vm.config.system.build.vm
+```
+
+A file named `ghostty.qcow2` will be created that is used to persist any changes
+made in the VM. To "reset" the VM to default delete the file and it will be
+recreated the next time you run the VM.
+
+### Contributing new VM definitions
+
+#### VM Acceptance Criteria
+
+We welcome the contribution of new VM definitions, as long as they meet the following criteria:
+
+1. The should be different enough from existing VM definitions that they represent a distinct
+ user (and developer) experience.
+2. There's a significant Ghostty user population that uses a similar environment.
+3. The VMs can be built using only packages from the current stable NixOS release.
+
+#### VM Definition Criteria
+
+1. VMs should be as minimal as possible so that they build and launch quickly.
+ Additional software can be added at runtime with a command like `nix run nixpkgs#`.
+2. VMs should not expose any services to the network, or run any remote access
+ software like SSH daemons, VNC or RDP.
+3. VMs should auto-login using the "ghostty" user.
diff --git a/PACKAGING.md b/PACKAGING.md
index 82c7c56739..6a4c01b6f5 100644
--- a/PACKAGING.md
+++ b/PACKAGING.md
@@ -23,13 +23,6 @@ https://release.files.ghostty.org/VERSION/ghostty-VERSION.tar.gz
https://release.files.ghostty.org/VERSION/ghostty-VERSION.tar.gz.minisig
```
-> [!NOTE]
->
-> **Version 1.0.0 the filename is `ghostty-source.tar.gz`.** Future
-> versions will use the `ghostty-VERSION.tar.gz` format since it is more
-> typical for source tarballs. But for version 1.0.0, the filename is
-> `ghostty-source.tar.gz`.
-
Signature files are signed with
[minisign](https://jedisct1.github.io/minisign/)
using the following public key:
@@ -88,6 +81,13 @@ for system packages which separate a build and install step, since the
install step can then be done with a `mv` or `cp` command (from `/tmp/ghostty`
to wherever the package manager expects it).
+> [!NOTE]
+>
+> **Version 1.1.1 and 1.1.2 are missing `fetch-zig-cache.sh`.** This was
+> an oversight on the release process. You can use the script from version
+> 1.1.0 to fetch the Zig cache for these versions. Future versions will
+> restore the script.
+
### Build Options
Ghostty uses the Zig build system. You can see all available build options by
diff --git a/build.zig b/build.zig
index 1364745cec..38d2bca6dd 100644
--- a/build.zig
+++ b/build.zig
@@ -3,21 +3,7 @@ const builtin = @import("builtin");
const buildpkg = @import("src/build/main.zig");
comptime {
- // This is the required Zig version for building this project. We allow
- // any patch version but the major and minor must match exactly.
- const required_zig = "0.13.0";
-
- // Fail compilation if the current Zig version doesn't meet requirements.
- const current_vsn = builtin.zig_version;
- const required_vsn = std.SemanticVersion.parse(required_zig) catch unreachable;
- if (current_vsn.major != required_vsn.major or
- current_vsn.minor != required_vsn.minor)
- {
- @compileError(std.fmt.comptimePrint(
- "Your Zig version v{} does not meet the required build version of v{}",
- .{ current_vsn, required_vsn },
- ));
- }
+ buildpkg.requireZig("0.13.0");
}
pub fn build(b: *std.Build) !void {
diff --git a/build.zig.zon b/build.zig.zon
index 518022486f..cc617cf513 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -1,32 +1,39 @@
.{
.name = "ghostty",
- .version = "1.0.2",
+ .version = "1.1.3",
.paths = .{""},
.dependencies = .{
// Zig libs
+
.libxev = .{
- .url = "https://github.com/mitchellh/libxev/archive/db6a52bafadf00360e675fefa7926e8e6c0e9931.tar.gz",
- .hash = "12206029de146b685739f69b10a6f08baee86b3d0a5f9a659fa2b2b66c9602078bbf",
+ // mitchellh/libxev
+ .url = "https://deps.files.ghostty.org/libxev-1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c.tar.gz",
+ .hash = "1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c",
},
.mach_glfw = .{
- .url = "https://github.com/mitchellh/mach-glfw/archive/37c2995f31abcf7e8378fba68ddcf4a3faa02de0.tar.gz",
+ // mitchellh/mach-glfw
+ .url = "https://deps.files.ghostty.org/mach_glfw-12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62.tar.gz",
.hash = "12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62",
.lazy = true,
},
.vaxis = .{
- .url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",
- .hash = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f",
+ // rockorager/libvaxis
+ .url = "git+https://github.com/rockorager/libvaxis#2237a7059eae99e9f132dd5acd1555e49d6c7d93",
+ .hash = "1220f5aec880d4f430cc1597ede88f1530da69e39a4986080e976b0c7b919c2ebfeb",
},
.z2d = .{
- .url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
+ // vancluever/z2d
+ .url = "https://deps.files.ghostty.org/z2d-12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a.tar.gz",
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a",
},
.zig_objc = .{
- .url = "https://github.com/mitchellh/zig-objc/archive/9b8ba849b0f58fe207ecd6ab7c147af55b17556e.tar.gz",
+ // mitchellh/zig-objc
+ .url = "https://deps.files.ghostty.org/zig_objc-1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634.tar.gz",
.hash = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634",
},
.zig_js = .{
- .url = "https://github.com/mitchellh/zig-js/archive/d0b8b0a57c52fbc89f9d9fecba75ca29da7dd7d1.tar.gz",
+ // mitchellh/zig-js
+ .url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
.hash = "12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc",
},
.ziglyph = .{
@@ -34,13 +41,20 @@
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
},
.zig_wayland = .{
- .url = "https://codeberg.org/ifreund/zig-wayland/archive/a5e2e9b6a6d7fba638ace4d4b24a3b576a02685b.tar.gz",
- .hash = "1220d41b23ae70e93355bb29dac1c07aa6aeb92427a2dffc4375e94b4de18111248c",
+ // codeberg ifreund/zig-wayland
+ .url = "https://deps.files.ghostty.org/zig-wayland-fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz",
+ .hash = "12209ca054cb1919fa276e328967f10b253f7537c4136eb48f3332b0f7cf661cad38",
},
.zf = .{
- .url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd",
+ // natecraddock/zf
+ .url = "https://deps.files.ghostty.org/zf-1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8.tar.gz",
.hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8",
},
+ .gobject = .{
+ // ianprime0509/zig-gobject
+ .url = "https://deps.files.ghostty.org/gobject-12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d.tar.zst",
+ .hash = "12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d",
+ },
// C libs
.cimgui = .{ .path = "./pkg/cimgui" },
@@ -72,15 +86,15 @@
.hash = "12201a57c6ce0001aa034fa80fba3e1cd2253c560a45748f4f4dd21ff23b491cddef",
},
.plasma_wayland_protocols = .{
- .url = "git+https://github.com/KDE/plasma-wayland-protocols?ref=main#db525e8f9da548cffa2ac77618dd0fbe7f511b86",
+ .url = "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz",
.hash = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566",
},
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
- .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4762ad5bd6d3906e28babdc2bda8a967d63a63be.tar.gz",
- .hash = "1220a263b22113273d01bd33e3c06b8119cb2f63b4e5d414a85d88e3aa95bb68a2de",
+ .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/efb1bb1843500a751eb30afa58fe48a6bec8952c.tar.gz",
+ .hash = "1220a1dbe41bc69aacf75026a7158812198ea265fb9cac64dcb91cd31f3b1b8c1f92",
},
},
}
diff --git a/build.zig.zon.nix b/build.zig.zon.nix
new file mode 100644
index 0000000000..e1eecdd3ea
--- /dev/null
+++ b/build.zig.zon.nix
@@ -0,0 +1,390 @@
+# generated by zon2nix (https://github.com/Cloudef/zig2nix)
+{
+ lib,
+ linkFarm,
+ fetchurl,
+ fetchgit,
+ runCommandLocal,
+ zig,
+ name ? "zig-packages",
+}:
+with builtins;
+with lib; let
+ unpackZigArtifact = {
+ name,
+ artifact,
+ }:
+ runCommandLocal name
+ {
+ nativeBuildInputs = [zig];
+ }
+ ''
+ hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})"
+ mv "$TMPDIR/p/$hash" "$out"
+ chmod 755 "$out"
+ '';
+
+ fetchZig = {
+ name,
+ url,
+ hash,
+ }: let
+ artifact = fetchurl {inherit url hash;};
+ in
+ unpackZigArtifact {inherit name artifact;};
+
+ fetchGitZig = {
+ name,
+ url,
+ hash,
+ }: let
+ parts = splitString "#" url;
+ url_base = elemAt parts 0;
+ url_without_query = elemAt (splitString "?" url_base) 0;
+ rev_base = elemAt parts 1;
+ rev =
+ if match "^[a-fA-F0-9]{40}$" rev_base != null
+ then rev_base
+ else "refs/heads/${rev_base}";
+ in
+ fetchgit {
+ inherit name rev hash;
+ url = url_without_query;
+ deepClone = false;
+ };
+
+ fetchZigArtifact = {
+ name,
+ url,
+ hash,
+ }: let
+ parts = splitString "://" url;
+ proto = elemAt parts 0;
+ path = elemAt parts 1;
+ fetcher = {
+ "git+http" = fetchGitZig {
+ inherit name hash;
+ url = "http://${path}";
+ };
+ "git+https" = fetchGitZig {
+ inherit name hash;
+ url = "https://${path}";
+ };
+ http = fetchZig {
+ inherit name hash;
+ url = "http://${path}";
+ };
+ https = fetchZig {
+ inherit name hash;
+ url = "https://${path}";
+ };
+ };
+ in
+ fetcher.${proto};
+in
+ linkFarm name [
+ {
+ name = "1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c";
+ path = fetchZigArtifact {
+ name = "libxev";
+ url = "https://deps.files.ghostty.org/libxev-1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c.tar.gz";
+ hash = "sha256-VHP90NTytIZ8UZsYRKOOxN490/I6yv6ec40sP8y5MJ8=";
+ };
+ }
+ {
+ name = "12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62";
+ path = fetchZigArtifact {
+ name = "mach_glfw";
+ url = "https://deps.files.ghostty.org/mach_glfw-12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62.tar.gz";
+ hash = "sha256-HhXIvWUS8/CHWY4VXPG2ZEo+we8XOn3o5rYJCQ1n8Nk=";
+ };
+ }
+ {
+ name = "1220736fa4ba211162c7a0e46cc8fe04d95921927688bff64ab5da7420d098a7272d";
+ path = fetchZigArtifact {
+ name = "glfw";
+ url = "https://github.com/mitchellh/glfw/archive/b552c6ec47326b94015feddb36058ea567b87159.tar.gz";
+ hash = "sha256-IeBVAOQmtyFqVxzuXPek1onuPwIamcOyYtxqKpPEQjU=";
+ };
+ }
+ {
+ name = "12202adbfecdad671d585c9a5bfcbd5cdf821726779430047742ce1bf94ad67d19cb";
+ path = fetchZigArtifact {
+ name = "xcode_frameworks";
+ url = "https://github.com/mitchellh/xcode-frameworks/archive/69801c154c39d7ae6129ea1ba8fe1afe00585fc8.tar.gz";
+ hash = "sha256-mP/I2coL57UJm/3+4Q8sPAgQwk8V4zM+S4VBBTrX2To=";
+ };
+ }
+ {
+ name = "122004bfd4c519dadfb8e6281a42fc34fd1aa15aea654ea8a492839046f9894fa2cf";
+ path = fetchZigArtifact {
+ name = "vulkan_headers";
+ url = "https://github.com/mitchellh/vulkan-headers/archive/04c8a0389d5a0236a96312988017cd4ce27d8041.tar.gz";
+ hash = "sha256-K+zrRudgHFukOM6En1StRYRMNYkeRk+qHTXvrXaG+FU=";
+ };
+ }
+ {
+ name = "1220b3164434d2ec9db146a40bf3a30f490590d68fa8529776a3138074f0da2c11ca";
+ path = fetchZigArtifact {
+ name = "wayland_headers";
+ url = "https://github.com/mitchellh/wayland-headers/archive/5f991515a29f994d87b908115a2ab0b899474bd1.tar.gz";
+ hash = "sha256-uFilLZinKkZt6RdVTV3lUmJpzpswDdFva22FvwU/XQI=";
+ };
+ }
+ {
+ name = "122089c326186c84aa2fd034b16abc38f3ebf4862d9ae106dc1847ac44f557b36465";
+ path = fetchZigArtifact {
+ name = "x11_headers";
+ url = "https://github.com/mitchellh/x11-headers/archive/2ffbd62d82ff73ec929dd8de802bc95effa0ef88.tar.gz";
+ hash = "sha256-EhV2bmTY/OMYN1wEul35gD0hQgS/Al262jO3pVr0O+c=";
+ };
+ }
+ {
+ name = "1220f5aec880d4f430cc1597ede88f1530da69e39a4986080e976b0c7b919c2ebfeb";
+ path = fetchZigArtifact {
+ name = "vaxis";
+ url = "git+https://github.com/rockorager/libvaxis#2237a7059eae99e9f132dd5acd1555e49d6c7d93";
+ hash = "sha256-ZzLNJOsXzyBhkdUhbET30RoU2T9xKYsBUQz2NAjK/G8=";
+ };
+ }
+ {
+ name = "1220dd654ef941fc76fd96f9ec6adadf83f69b9887a0d3f4ee5ac0a1a3e11be35cf5";
+ path = fetchZigArtifact {
+ name = "zigimg";
+ url = "git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e";
+ hash = "sha256-oLf3YH3yeg4ikVO/GahMCDRMTU31AHkfSnF4rt7xTKo=";
+ };
+ }
+ {
+ name = "122055beff332830a391e9895c044d33b15ea21063779557024b46169fb1984c6e40";
+ path = fetchZigArtifact {
+ name = "zg";
+ url = "https://codeberg.org/atman/zg/archive/v0.13.2.tar.gz";
+ hash = "sha256-2x9hT7bYq9KJYWLVOf21a+QvTG/F7HWT+YK15IMRzNY=";
+ };
+ }
+ {
+ name = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a";
+ path = fetchZigArtifact {
+ name = "z2d";
+ url = "https://deps.files.ghostty.org/z2d-12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a.tar.gz";
+ hash = "sha256-P0UJ54RO/vVyDa+UkBl+QEOjzoMMEFSOTexQP/uBXfc=";
+ };
+ }
+ {
+ name = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634";
+ path = fetchZigArtifact {
+ name = "zig_objc";
+ url = "https://deps.files.ghostty.org/zig_objc-1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634.tar.gz";
+ hash = "sha256-H+HIbh2T23uzrsg9/1/vl9Ir1HCAa2pzeTx6zktJH9Q=";
+ };
+ }
+ {
+ name = "12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc";
+ path = fetchZigArtifact {
+ name = "zig_js";
+ url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz";
+ hash = "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0=";
+ };
+ }
+ {
+ name = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25";
+ path = fetchZigArtifact {
+ name = "ziglyph";
+ url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz";
+ hash = "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k=";
+ };
+ }
+ {
+ name = "12209ca054cb1919fa276e328967f10b253f7537c4136eb48f3332b0f7cf661cad38";
+ path = fetchZigArtifact {
+ name = "zig_wayland";
+ url = "https://deps.files.ghostty.org/zig-wayland-fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz";
+ hash = "sha256-RtAystqK/GRYIquTK1KfD7rRSCrfuzAvCD1Z9DE1ldc=";
+ };
+ }
+ {
+ name = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8";
+ path = fetchZigArtifact {
+ name = "zf";
+ url = "https://deps.files.ghostty.org/zf-1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8.tar.gz";
+ hash = "sha256-/oLryY3VQfjbtQi+UP+n6FJTVA/YxIetjO+6Ovrh6/E=";
+ };
+ }
+ {
+ name = "1220c72c1697dd9008461ead702997a15d8a1c5810247f02e7983b9f74c6c6e4c087";
+ path = fetchZigArtifact {
+ name = "vaxis";
+ url = "git+https://github.com/rockorager/libvaxis/?ref=main#dc0a228a5544988d4a920cfb40be9cd28db41423";
+ hash = "sha256-QWN4jOrA91KlbqmeEHHJ4HTnCC9nmfxt8DHUXJpAzLI=";
+ };
+ }
+ {
+ name = "12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d";
+ path = fetchZigArtifact {
+ name = "gobject";
+ url = "https://deps.files.ghostty.org/gobject-12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d.tar.zst";
+ hash = "sha256-UU97kNv/bZzQPKz1djhEDLapLguvfBpFfWVb6FthtcI=";
+ };
+ }
+ {
+ name = "12202cdac858abc52413a6c6711d5026d2d3c8e13f95ca2c327eade0736298bb021f";
+ path = fetchZigArtifact {
+ name = "wayland";
+ url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz";
+ hash = "sha256-6kGR1o5DdnflHzqs3ieCmBAUTpMdOXoyfcYDXiw5xQ0=";
+ };
+ }
+ {
+ name = "12201a57c6ce0001aa034fa80fba3e1cd2253c560a45748f4f4dd21ff23b491cddef";
+ path = fetchZigArtifact {
+ name = "wayland_protocols";
+ url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz";
+ hash = "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg=";
+ };
+ }
+ {
+ name = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566";
+ path = fetchZigArtifact {
+ name = "plasma_wayland_protocols";
+ url = "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz";
+ hash = "sha256-XFi6IUrNjmvKNCbcCLAixGqN2Zeymhs+KLrfccIN9EE=";
+ };
+ }
+ {
+ name = "1220a1dbe41bc69aacf75026a7158812198ea265fb9cac64dcb91cd31f3b1b8c1f92";
+ path = fetchZigArtifact {
+ name = "iterm2_themes";
+ url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/efb1bb1843500a751eb30afa58fe48a6bec8952c.tar.gz";
+ hash = "sha256-ZdVc1mmLwF45PZiqL/j/l7MO2O6hZ11lqIToGFdHiEU=";
+ };
+ }
+ {
+ name = "1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402";
+ path = fetchZigArtifact {
+ name = "imgui";
+ url = "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz";
+ hash = "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=";
+ };
+ }
+ {
+ name = "1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d";
+ path = fetchZigArtifact {
+ name = "freetype";
+ url = "https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz";
+ hash = "sha256-QnIB9dUVFnDQXB9bRb713aHy592XHvVPD+qqf/0quQw=";
+ };
+ }
+ {
+ name = "1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66";
+ path = fetchZigArtifact {
+ name = "libpng";
+ url = "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz";
+ hash = "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo=";
+ };
+ }
+ {
+ name = "1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb";
+ path = fetchZigArtifact {
+ name = "zlib";
+ url = "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz";
+ hash = "sha256-F+iIY/NgBnKrSRgvIXKBtvxNPHYr3jYZNeQ2qVIU0Fw=";
+ };
+ }
+ {
+ name = "12201149afb3326c56c05bb0a577f54f76ac20deece63aa2f5cd6ff31a4fa4fcb3b7";
+ path = fetchZigArtifact {
+ name = "fontconfig";
+ url = "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz";
+ hash = "sha256-O6LdkhWHGKzsXKrxpxYEO1qgVcJ7CB2RSvPMtA3OilU=";
+ };
+ }
+ {
+ name = "122032442d95c3b428ae8e526017fad881e7dc78eab4d558e9a58a80bfbd65a64f7d";
+ path = fetchZigArtifact {
+ name = "libxml2";
+ url = "https://github.com/GNOME/libxml2/archive/refs/tags/v2.11.5.tar.gz";
+ hash = "sha256-bCgFni4+60K1tLFkieORamNGwQladP7jvGXNxdiaYhU=";
+ };
+ }
+ {
+ name = "1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122";
+ path = fetchZigArtifact {
+ name = "harfbuzz";
+ url = "https://deps.files.ghostty.org/harfbuzz-1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122.tar.gz";
+ hash = "sha256-nxygiYE7BZRK0c6MfgGCEwJtNdybq0gKIeuHaDg5ZVY=";
+ };
+ }
+ {
+ name = "12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b";
+ path = fetchZigArtifact {
+ name = "highway";
+ url = "https://deps.files.ghostty.org/highway-12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b.tar.gz";
+ hash = "sha256-NUqLRTm1iOcLmOxwhEJz4/J0EwLEw3e8xOgbPRhm98k=";
+ };
+ }
+ {
+ name = "1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb";
+ path = fetchZigArtifact {
+ name = "oniguruma";
+ url = "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz";
+ hash = "sha256-ABqhIC54RI9MC/GkjHblVodrNvFtks4yB+zP1h2Z8qA=";
+ };
+ }
+ {
+ name = "1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e";
+ path = fetchZigArtifact {
+ name = "sentry";
+ url = "https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz";
+ hash = "sha256-KsZJfMjWGo0xCT5HrduMmyxFsWsHBbszSoNbZCPDGN8=";
+ };
+ }
+ {
+ name = "12207fd37bb8251919c112dcdd8f616a491857b34a451f7e4486490077206dc2a1ea";
+ path = fetchZigArtifact {
+ name = "breakpad";
+ url = "https://github.com/getsentry/breakpad/archive/b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz";
+ hash = "sha256-bMqYlD0amQdmzvYQd8Ca/1k4Bj/heh7+EijlQSttatk=";
+ };
+ }
+ {
+ name = "1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641";
+ path = fetchZigArtifact {
+ name = "utfcpp";
+ url = "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz";
+ hash = "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=";
+ };
+ }
+ {
+ name = "122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd";
+ path = fetchZigArtifact {
+ name = "wuffs";
+ url = "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz";
+ hash = "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=";
+ };
+ }
+ {
+ name = "12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806";
+ path = fetchZigArtifact {
+ name = "pixels";
+ url = "https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz";
+ hash = "sha256-Veg7FtCRCCUCvxSb9FfzH0IJLFmCZQ4/+657SIcb8Ro=";
+ };
+ }
+ {
+ name = "12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1";
+ path = fetchZigArtifact {
+ name = "glslang";
+ url = "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz";
+ hash = "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U=";
+ };
+ }
+ {
+ name = "1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da";
+ path = fetchZigArtifact {
+ name = "spirv_cross";
+ url = "https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz";
+ hash = "sha256-tStvz8Ref6abHwahNiwVVHNETizAmZVVaxVsU7pmV+M=";
+ };
+ }
+ ]
diff --git a/build.zig.zon.txt b/build.zig.zon.txt
new file mode 100644
index 0000000000..297226f2f1
--- /dev/null
+++ b/build.zig.zon.txt
@@ -0,0 +1,38 @@
+git+https://github.com/rockorager/libvaxis#2237a7059eae99e9f132dd5acd1555e49d6c7d93
+git+https://github.com/rockorager/libvaxis/?ref=main#dc0a228a5544988d4a920cfb40be9cd28db41423
+git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e
+https://codeberg.org/atman/zg/archive/v0.13.2.tar.gz
+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/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz
+https://deps.files.ghostty.org/gobject-12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d.tar.zst
+https://deps.files.ghostty.org/harfbuzz-1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122.tar.gz
+https://deps.files.ghostty.org/highway-12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b.tar.gz
+https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz
+https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz
+https://deps.files.ghostty.org/libxev-1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c.tar.gz
+https://deps.files.ghostty.org/mach_glfw-12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62.tar.gz
+https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz
+https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz
+https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz
+https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz
+https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz
+https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz
+https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz
+https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz
+https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz
+https://deps.files.ghostty.org/z2d-12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a.tar.gz
+https://deps.files.ghostty.org/zf-1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8.tar.gz
+https://deps.files.ghostty.org/zig-wayland-fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz
+https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz
+https://deps.files.ghostty.org/zig_objc-1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634.tar.gz
+https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz
+https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
+https://github.com/GNOME/libxml2/archive/refs/tags/v2.11.5.tar.gz
+https://github.com/getsentry/breakpad/archive/b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz
+https://github.com/mbadolato/iTerm2-Color-Schemes/archive/db227d159adc265818f2e898da0f70ef8d7b580e.tar.gz
+https://github.com/mitchellh/glfw/archive/b552c6ec47326b94015feddb36058ea567b87159.tar.gz
+https://github.com/mitchellh/vulkan-headers/archive/04c8a0389d5a0236a96312988017cd4ce27d8041.tar.gz
+https://github.com/mitchellh/wayland-headers/archive/5f991515a29f994d87b908115a2ab0b899474bd1.tar.gz
+https://github.com/mitchellh/x11-headers/archive/2ffbd62d82ff73ec929dd8de802bc95effa0ef88.tar.gz
+https://github.com/mitchellh/xcode-frameworks/archive/69801c154c39d7ae6129ea1ba8fe1afe00585fc8.tar.gz
diff --git a/build.zig.zon2json-lock b/build.zig.zon2json-lock
new file mode 100644
index 0000000000..26ccc690a3
--- /dev/null
+++ b/build.zig.zon2json-lock
@@ -0,0 +1,192 @@
+{
+ "1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c": {
+ "name": "libxev",
+ "url": "https://deps.files.ghostty.org/libxev-1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c.tar.gz",
+ "hash": "sha256-VHP90NTytIZ8UZsYRKOOxN490/I6yv6ec40sP8y5MJ8="
+ },
+ "12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62": {
+ "name": "mach_glfw",
+ "url": "https://deps.files.ghostty.org/mach_glfw-12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62.tar.gz",
+ "hash": "sha256-HhXIvWUS8/CHWY4VXPG2ZEo+we8XOn3o5rYJCQ1n8Nk="
+ },
+ "1220736fa4ba211162c7a0e46cc8fe04d95921927688bff64ab5da7420d098a7272d": {
+ "name": "glfw",
+ "url": "https://github.com/mitchellh/glfw/archive/b552c6ec47326b94015feddb36058ea567b87159.tar.gz",
+ "hash": "sha256-IeBVAOQmtyFqVxzuXPek1onuPwIamcOyYtxqKpPEQjU="
+ },
+ "12202adbfecdad671d585c9a5bfcbd5cdf821726779430047742ce1bf94ad67d19cb": {
+ "name": "xcode_frameworks",
+ "url": "https://github.com/mitchellh/xcode-frameworks/archive/69801c154c39d7ae6129ea1ba8fe1afe00585fc8.tar.gz",
+ "hash": "sha256-mP/I2coL57UJm/3+4Q8sPAgQwk8V4zM+S4VBBTrX2To="
+ },
+ "122004bfd4c519dadfb8e6281a42fc34fd1aa15aea654ea8a492839046f9894fa2cf": {
+ "name": "vulkan_headers",
+ "url": "https://github.com/mitchellh/vulkan-headers/archive/04c8a0389d5a0236a96312988017cd4ce27d8041.tar.gz",
+ "hash": "sha256-K+zrRudgHFukOM6En1StRYRMNYkeRk+qHTXvrXaG+FU="
+ },
+ "1220b3164434d2ec9db146a40bf3a30f490590d68fa8529776a3138074f0da2c11ca": {
+ "name": "wayland_headers",
+ "url": "https://github.com/mitchellh/wayland-headers/archive/5f991515a29f994d87b908115a2ab0b899474bd1.tar.gz",
+ "hash": "sha256-uFilLZinKkZt6RdVTV3lUmJpzpswDdFva22FvwU/XQI="
+ },
+ "122089c326186c84aa2fd034b16abc38f3ebf4862d9ae106dc1847ac44f557b36465": {
+ "name": "x11_headers",
+ "url": "https://github.com/mitchellh/x11-headers/archive/2ffbd62d82ff73ec929dd8de802bc95effa0ef88.tar.gz",
+ "hash": "sha256-EhV2bmTY/OMYN1wEul35gD0hQgS/Al262jO3pVr0O+c="
+ },
+ "1220f5aec880d4f430cc1597ede88f1530da69e39a4986080e976b0c7b919c2ebfeb": {
+ "name": "vaxis",
+ "url": "git+https://github.com/rockorager/libvaxis#2237a7059eae99e9f132dd5acd1555e49d6c7d93",
+ "hash": "sha256-ZzLNJOsXzyBhkdUhbET30RoU2T9xKYsBUQz2NAjK/G8="
+ },
+ "1220dd654ef941fc76fd96f9ec6adadf83f69b9887a0d3f4ee5ac0a1a3e11be35cf5": {
+ "name": "zigimg",
+ "url": "git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e",
+ "hash": "sha256-oLf3YH3yeg4ikVO/GahMCDRMTU31AHkfSnF4rt7xTKo="
+ },
+ "122055beff332830a391e9895c044d33b15ea21063779557024b46169fb1984c6e40": {
+ "name": "zg",
+ "url": "https://codeberg.org/atman/zg/archive/v0.13.2.tar.gz",
+ "hash": "sha256-2x9hT7bYq9KJYWLVOf21a+QvTG/F7HWT+YK15IMRzNY="
+ },
+ "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a": {
+ "name": "z2d",
+ "url": "https://deps.files.ghostty.org/z2d-12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a.tar.gz",
+ "hash": "sha256-P0UJ54RO/vVyDa+UkBl+QEOjzoMMEFSOTexQP/uBXfc="
+ },
+ "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634": {
+ "name": "zig_objc",
+ "url": "https://deps.files.ghostty.org/zig_objc-1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634.tar.gz",
+ "hash": "sha256-H+HIbh2T23uzrsg9/1/vl9Ir1HCAa2pzeTx6zktJH9Q="
+ },
+ "12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc": {
+ "name": "zig_js",
+ "url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
+ "hash": "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="
+ },
+ "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25": {
+ "name": "ziglyph",
+ "url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
+ "hash": "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k="
+ },
+ "12209ca054cb1919fa276e328967f10b253f7537c4136eb48f3332b0f7cf661cad38": {
+ "name": "zig_wayland",
+ "url": "https://deps.files.ghostty.org/zig-wayland-fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz",
+ "hash": "sha256-RtAystqK/GRYIquTK1KfD7rRSCrfuzAvCD1Z9DE1ldc="
+ },
+ "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8": {
+ "name": "zf",
+ "url": "https://deps.files.ghostty.org/zf-1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8.tar.gz",
+ "hash": "sha256-/oLryY3VQfjbtQi+UP+n6FJTVA/YxIetjO+6Ovrh6/E="
+ },
+ "1220c72c1697dd9008461ead702997a15d8a1c5810247f02e7983b9f74c6c6e4c087": {
+ "name": "vaxis",
+ "url": "git+https://github.com/rockorager/libvaxis/?ref=main#dc0a228a5544988d4a920cfb40be9cd28db41423",
+ "hash": "sha256-QWN4jOrA91KlbqmeEHHJ4HTnCC9nmfxt8DHUXJpAzLI="
+ },
+ "12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d": {
+ "name": "gobject",
+ "url": "https://deps.files.ghostty.org/gobject-12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d.tar.zst",
+ "hash": "sha256-UU97kNv/bZzQPKz1djhEDLapLguvfBpFfWVb6FthtcI="
+ },
+ "12202cdac858abc52413a6c6711d5026d2d3c8e13f95ca2c327eade0736298bb021f": {
+ "name": "wayland",
+ "url": "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz",
+ "hash": "sha256-6kGR1o5DdnflHzqs3ieCmBAUTpMdOXoyfcYDXiw5xQ0="
+ },
+ "12201a57c6ce0001aa034fa80fba3e1cd2253c560a45748f4f4dd21ff23b491cddef": {
+ "name": "wayland_protocols",
+ "url": "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz",
+ "hash": "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg="
+ },
+ "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566": {
+ "name": "plasma_wayland_protocols",
+ "url": "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz",
+ "hash": "sha256-XFi6IUrNjmvKNCbcCLAixGqN2Zeymhs+KLrfccIN9EE="
+ },
+ "12203d2647e5daf36a9c85b969e03f422540786ce9ea624eb4c26d204fe1f46218f3": {
+ "name": "iterm2_themes",
+ "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/db227d159adc265818f2e898da0f70ef8d7b580e.tar.gz",
+ "hash": "sha256-Iyf7U4rpvNkPX4AOEbYSYGte5+SjRwsWD2luOn1Hz8U="
+ },
+ "1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402": {
+ "name": "imgui",
+ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
+ "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
+ },
+ "1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d": {
+ "name": "freetype",
+ "url": "https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz",
+ "hash": "sha256-QnIB9dUVFnDQXB9bRb713aHy592XHvVPD+qqf/0quQw="
+ },
+ "1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66": {
+ "name": "libpng",
+ "url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
+ "hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="
+ },
+ "1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb": {
+ "name": "zlib",
+ "url": "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz",
+ "hash": "sha256-F+iIY/NgBnKrSRgvIXKBtvxNPHYr3jYZNeQ2qVIU0Fw="
+ },
+ "12201149afb3326c56c05bb0a577f54f76ac20deece63aa2f5cd6ff31a4fa4fcb3b7": {
+ "name": "fontconfig",
+ "url": "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz",
+ "hash": "sha256-O6LdkhWHGKzsXKrxpxYEO1qgVcJ7CB2RSvPMtA3OilU="
+ },
+ "122032442d95c3b428ae8e526017fad881e7dc78eab4d558e9a58a80bfbd65a64f7d": {
+ "name": "libxml2",
+ "url": "https://github.com/GNOME/libxml2/archive/refs/tags/v2.11.5.tar.gz",
+ "hash": "sha256-bCgFni4+60K1tLFkieORamNGwQladP7jvGXNxdiaYhU="
+ },
+ "1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122": {
+ "name": "harfbuzz",
+ "url": "https://deps.files.ghostty.org/harfbuzz-1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122.tar.gz",
+ "hash": "sha256-nxygiYE7BZRK0c6MfgGCEwJtNdybq0gKIeuHaDg5ZVY="
+ },
+ "12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b": {
+ "name": "highway",
+ "url": "https://deps.files.ghostty.org/highway-12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b.tar.gz",
+ "hash": "sha256-NUqLRTm1iOcLmOxwhEJz4/J0EwLEw3e8xOgbPRhm98k="
+ },
+ "1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb": {
+ "name": "oniguruma",
+ "url": "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz",
+ "hash": "sha256-ABqhIC54RI9MC/GkjHblVodrNvFtks4yB+zP1h2Z8qA="
+ },
+ "1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e": {
+ "name": "sentry",
+ "url": "https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz",
+ "hash": "sha256-KsZJfMjWGo0xCT5HrduMmyxFsWsHBbszSoNbZCPDGN8="
+ },
+ "12207fd37bb8251919c112dcdd8f616a491857b34a451f7e4486490077206dc2a1ea": {
+ "name": "breakpad",
+ "url": "https://github.com/getsentry/breakpad/archive/b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz",
+ "hash": "sha256-bMqYlD0amQdmzvYQd8Ca/1k4Bj/heh7+EijlQSttatk="
+ },
+ "1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641": {
+ "name": "utfcpp",
+ "url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz",
+ "hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8="
+ },
+ "122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd": {
+ "name": "wuffs",
+ "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
+ "hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
+ },
+ "12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806": {
+ "name": "pixels",
+ "url": "https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz",
+ "hash": "sha256-Veg7FtCRCCUCvxSb9FfzH0IJLFmCZQ4/+657SIcb8Ro="
+ },
+ "12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1": {
+ "name": "glslang",
+ "url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
+ "hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="
+ },
+ "1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da": {
+ "name": "spirv_cross",
+ "url": "https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz",
+ "hash": "sha256-tStvz8Ref6abHwahNiwVVHNETizAmZVVaxVsU7pmV+M="
+ }
+}
diff --git a/dist/linux/app.desktop b/dist/linux/app.desktop
index 6fc43d4708..6e464ea87c 100644
--- a/dist/linux/app.desktop
+++ b/dist/linux/app.desktop
@@ -7,6 +7,7 @@ Icon=com.mitchellh.ghostty
Categories=System;TerminalEmulator;
Keywords=terminal;tty;pty;
StartupNotify=true
+StartupWMClass=com.mitchellh.ghostty
Terminal=false
Actions=new-window;
X-GNOME-UsesNotifications=true
diff --git a/dist/linux/ghostty_dolphin.desktop b/dist/linux/ghostty_dolphin.desktop
old mode 100644
new mode 100755
diff --git a/dist/linux/ghostty_nautilus.py b/dist/linux/ghostty_nautilus.py
new file mode 100644
index 0000000000..42c3976428
--- /dev/null
+++ b/dist/linux/ghostty_nautilus.py
@@ -0,0 +1,97 @@
+# Adapted from wezterm: https://github.com/wez/wezterm/blob/main/assets/wezterm-nautilus.py
+# original copyright notice:
+#
+# Copyright (C) 2022 Sebastian Wiesner
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from os.path import isdir
+from gi import require_version
+from gi.repository import Nautilus, GObject, Gio, GLib
+
+
+class OpenInGhosttyAction(GObject.GObject, Nautilus.MenuProvider):
+ def __init__(self):
+ super().__init__()
+ session = Gio.bus_get_sync(Gio.BusType.SESSION, None)
+ self._systemd = None
+ # Check if the this system runs under systemd, per sd_booted(3)
+ if isdir('/run/systemd/system/'):
+ self._systemd = Gio.DBusProxy.new_sync(session,
+ Gio.DBusProxyFlags.NONE,
+ None,
+ "org.freedesktop.systemd1",
+ "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager", None)
+
+ def _open_terminal(self, path):
+ cmd = ['ghostty', f'--working-directory={path}', '--gtk-single-instance=false']
+ child = Gio.Subprocess.new(cmd, Gio.SubprocessFlags.NONE)
+ if self._systemd:
+ # Move new terminal into a dedicated systemd scope to make systemd
+ # track the terminal separately; in particular this makes systemd
+ # keep a separate CPU and memory account for the terminal which in turn
+ # ensures that oomd doesn't take nautilus down if a process in
+ # ghostty consumes a lot of memory.
+ pid = int(child.get_identifier())
+ props = [("PIDs", GLib.Variant('au', [pid])),
+ ('CollectMode', GLib.Variant('s', 'inactive-or-failed'))]
+ name = 'app-nautilus-com.mitchellh.ghostty-{}.scope'.format(pid)
+ args = GLib.Variant('(ssa(sv)a(sa(sv)))', (name, 'fail', props, []))
+ self._systemd.call_sync('StartTransientUnit', args,
+ Gio.DBusCallFlags.NO_AUTO_START, 500, None)
+
+ def _menu_item_activated(self, _menu, paths):
+ for path in paths:
+ self._open_terminal(path)
+
+ def _make_item(self, name, paths):
+ item = Nautilus.MenuItem(name=name, label='Open in Ghostty',
+ icon='com.mitchellh.ghostty')
+ item.connect('activate', self._menu_item_activated, paths)
+ return item
+
+ def _paths_to_open(self, files):
+ paths = []
+ for file in files:
+ location = file.get_location() if file.is_directory() else file.get_parent_location()
+ path = location.get_path()
+ if path and path not in paths:
+ paths.append(path)
+ if 10 < len(paths):
+ # Let's not open anything if the user selected a lot of directories,
+ # to avoid accidentally spamming their desktop with dozends of
+ # new windows or tabs. Ten is a totally arbitrary limit :)
+ return []
+ else:
+ return paths
+
+ def get_file_items(self, *args):
+ # Nautilus 3.0 API passes args (window, files), 4.0 API just passes files
+ files = args[0] if len(args) == 1 else args[1]
+ paths = self._paths_to_open(files)
+ if paths:
+ return [self._make_item(name='GhosttyNautilus::open_in_ghostty', paths=paths)]
+ else:
+ return []
+
+ def get_background_items(self, *args):
+ # Nautilus 3.0 API passes args (window, file), 4.0 API just passes file
+ file = args[0] if len(args) == 1 else args[1]
+ paths = self._paths_to_open([file])
+ if paths:
+ return [self._make_item(name='GhosttyNautilus::open_folder_in_ghostty', paths=paths)]
+ else:
+ return []
diff --git a/dist/macos/update_appcast_tag.py b/dist/macos/update_appcast_tag.py
index 4ef526019d..2cb20dd5d5 100644
--- a/dist/macos/update_appcast_tag.py
+++ b/dist/macos/update_appcast_tag.py
@@ -21,6 +21,7 @@
now = datetime.now(timezone.utc)
version = os.environ["GHOSTTY_VERSION"]
+version_dash = version.replace('.', '-')
build = os.environ["GHOSTTY_BUILD"]
commit = os.environ["GHOSTTY_COMMIT"]
commit_long = os.environ["GHOSTTY_COMMIT_LONG"]
@@ -82,6 +83,8 @@
elem.text = f"{version}"
elem = ET.SubElement(item, "sparkle:minimumSystemVersion")
elem.text = "13.0.0"
+elem = ET.SubElement(item, "sparkle:fullReleaseNotesLink")
+elem.text = f"https://ghostty.org/docs/install/release-notes/{version_dash}"
elem = ET.SubElement(item, "description")
elem.text = f"""
Ghostty v{version}
@@ -91,8 +94,8 @@
We don't currently generate release notes for auto-updates.
-You can view the complete changelog and release notes on
-the Ghostty website.
+You can view the complete changelog and release notes
+at ghostty.org/docs/install/release-notes/{version_dash}.
"""
elem = ET.SubElement(item, "enclosure")
diff --git a/flake.lock b/flake.lock
index bf678156b7..7905635b3b 100644
--- a/flake.lock
+++ b/flake.lock
@@ -3,11 +3,11 @@
"flake-compat": {
"flake": false,
"locked": {
- "lastModified": 1696426674,
- "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
+ "lastModified": 1733328505,
+ "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra",
"repo": "flake-compat",
- "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
+ "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github"
},
"original": {
@@ -21,11 +21,11 @@
"systems": "systems"
},
"locked": {
- "lastModified": 1705309234,
- "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
@@ -36,11 +36,11 @@
},
"nixpkgs-stable": {
"locked": {
- "lastModified": 1733423277,
- "narHash": "sha256-TxabjxEgkNbCGFRHgM/b9yZWlBj60gUOUnRT/wbVQR8=",
+ "lastModified": 1738255539,
+ "narHash": "sha256-hP2eOqhIO/OILW+3moNWO4GtdJFYCqAe9yJZgvlCoDQ=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "e36963a147267afc055f7cf65225958633e536bf",
+ "rev": "c3511a3b53b482aa7547c9d1626fd7310c1de1c5",
"type": "github"
},
"original": {
@@ -52,11 +52,11 @@
},
"nixpkgs-unstable": {
"locked": {
- "lastModified": 1733229606,
- "narHash": "sha256-FLYY5M0rpa5C2QAE3CKLYAM6TwbKicdRK6qNrSHlNrE=",
+ "lastModified": 1738136902,
+ "narHash": "sha256-pUvLijVGARw4u793APze3j6mU1Zwdtz7hGkGGkD87qw=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "566e53c2ad750c84f6d31f9ccb9d00f823165550",
+ "rev": "9a5db3142ce450045840cc8d832b13b8a2018e0c",
"type": "github"
},
"original": {
@@ -69,9 +69,11 @@
"root": {
"inputs": {
"flake-compat": "flake-compat",
+ "flake-utils": "flake-utils",
"nixpkgs-stable": "nixpkgs-stable",
"nixpkgs-unstable": "nixpkgs-unstable",
- "zig": "zig"
+ "zig": "zig",
+ "zig2nix": "zig2nix"
}
},
"systems": {
@@ -92,17 +94,19 @@
"zig": {
"inputs": {
"flake-compat": [],
- "flake-utils": "flake-utils",
+ "flake-utils": [
+ "flake-utils"
+ ],
"nixpkgs": [
"nixpkgs-stable"
]
},
"locked": {
- "lastModified": 1717848532,
- "narHash": "sha256-d+xIUvSTreHl8pAmU1fnmkfDTGQYCn2Rb/zOwByxS2M=",
+ "lastModified": 1738239110,
+ "narHash": "sha256-Y5i9mQ++dyIQr+zEPNy+KIbc5wjPmfllBrag3cHZgcE=",
"owner": "mitchellh",
"repo": "zig-overlay",
- "rev": "02fc5cc555fc14fda40c42d7c3250efa43812b43",
+ "rev": "1a8fb6f3a04724519436355564b95fce5e272504",
"type": "github"
},
"original": {
@@ -110,6 +114,30 @@
"repo": "zig-overlay",
"type": "github"
}
+ },
+ "zig2nix": {
+ "inputs": {
+ "flake-utils": [
+ "flake-utils"
+ ],
+ "nixpkgs": [
+ "nixpkgs-stable"
+ ]
+ },
+ "locked": {
+ "lastModified": 1738263917,
+ "narHash": "sha256-j/3fwe2pEOquHabP/puljOKwAZFjIE9gXZqA91sC48M=",
+ "owner": "jcollie",
+ "repo": "zig2nix",
+ "rev": "c311d8e77a6ee0d995f40a6e10a89a3a4ab04f9a",
+ "type": "github"
+ },
+ "original": {
+ "owner": "jcollie",
+ "ref": "c311d8e77a6ee0d995f40a6e10a89a3a4ab04f9a",
+ "repo": "zig2nix",
+ "type": "github"
+ }
}
},
"root": "root",
diff --git a/flake.nix b/flake.nix
index 83d4af4144..df0eeb759e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -8,6 +8,7 @@
# glibc versions used by our dependencies from Nix are compatible with the
# system glibc that the user is building for.
nixpkgs-stable.url = "github:nixos/nixpkgs/release-24.11";
+ flake-utils.url = "github:numtide/flake-utils";
# Used for shell.nix
flake-compat = {
@@ -19,9 +20,18 @@
url = "github:mitchellh/zig-overlay";
inputs = {
nixpkgs.follows = "nixpkgs-stable";
+ flake-utils.follows = "flake-utils";
flake-compat.follows = "";
};
};
+
+ zig2nix = {
+ url = "github:jcollie/zig2nix?ref=c311d8e77a6ee0d995f40a6e10a89a3a4ab04f9a";
+ inputs = {
+ nixpkgs.follows = "nixpkgs-stable";
+ flake-utils.follows = "flake-utils";
+ };
+ };
};
outputs = {
@@ -29,40 +39,86 @@
nixpkgs-unstable,
nixpkgs-stable,
zig,
+ zig2nix,
...
}:
- builtins.foldl' nixpkgs-stable.lib.recursiveUpdate {} (builtins.map (system: let
- pkgs-stable = nixpkgs-stable.legacyPackages.${system};
- pkgs-unstable = nixpkgs-unstable.legacyPackages.${system};
- in {
- devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix {
- zig = zig.packages.${system}."0.13.0";
- wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {};
- };
+ builtins.foldl' nixpkgs-stable.lib.recursiveUpdate {} (
+ builtins.map (
+ system: let
+ pkgs-stable = nixpkgs-stable.legacyPackages.${system};
+ pkgs-unstable = nixpkgs-unstable.legacyPackages.${system};
+ in {
+ devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix {
+ zig = zig.packages.${system}."0.13.0";
+ wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {};
+ zig2nix = zig2nix;
+ };
- packages.${system} = let
- mkArgs = optimize: {
- inherit optimize;
+ packages.${system} = let
+ mkArgs = optimize: {
+ inherit optimize;
- revision = self.shortRev or self.dirtyShortRev or "dirty";
- };
- in rec {
- ghostty-debug = pkgs-stable.callPackage ./nix/package.nix (mkArgs "Debug");
- ghostty-releasesafe = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseSafe");
- ghostty-releasefast = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseFast");
+ revision = self.shortRev or self.dirtyShortRev or "dirty";
+ };
+ in rec {
+ deps = pkgs-stable.callPackage ./build.zig.zon.nix {};
+ ghostty-debug = pkgs-stable.callPackage ./nix/package.nix (mkArgs "Debug");
+ ghostty-releasesafe = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseSafe");
+ ghostty-releasefast = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseFast");
- ghostty = ghostty-releasefast;
- default = ghostty;
- };
+ ghostty = ghostty-releasefast;
+ default = ghostty;
+ };
+
+ formatter.${system} = pkgs-stable.alejandra;
- formatter.${system} = pkgs-stable.alejandra;
+ apps.${system} = let
+ runVM = (
+ module: let
+ vm = import ./nix/vm/create.nix {
+ inherit system module;
+ nixpkgs = nixpkgs-stable;
+ overlay = self.overlays.debug;
+ };
+ program = pkgs-stable.writeShellScript "run-ghostty-vm" ''
+ SHARED_DIR=$(pwd)
+ export SHARED_DIR
- # Our supported systems are the same supported systems as the Zig binaries.
- }) (builtins.attrNames zig.packages))
+ ${pkgs-stable.lib.getExe vm.config.system.build.vm} "$@"
+ '';
+ in {
+ type = "app";
+ program = "${program}";
+ }
+ );
+ 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-gnome = runVM ./nix/vm/x11-gnome.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 = final: prev: {
- ghostty = self.packages.${prev.system}.default;
+ overlays = {
+ default = self.overlays.releasefast;
+ releasefast = final: prev: {
+ ghostty = self.packages.${prev.system}.ghostty-releasefast;
+ };
+ debug = final: prev: {
+ ghostty = self.packages.${prev.system}.ghostty-debug;
+ };
};
+ create-vm = import ./nix/vm/create.nix;
+ create-cinnamon-vm = import ./nix/vm/create-cinnamon.nix;
+ create-gnome-vm = import ./nix/vm/create-gnome.nix;
+ create-plasma6-vm = import ./nix/vm/create-plasma6.nix;
+ create-xfce-vm = import ./nix/vm/create-xfce.nix;
};
nixConfig = {
diff --git a/include/ghostty.h b/include/ghostty.h
index 0e444a2fab..86de4266df 100644
--- a/include/ghostty.h
+++ b/include/ghostty.h
@@ -159,7 +159,7 @@ typedef enum {
GHOSTTY_KEY_EQUAL,
GHOSTTY_KEY_LEFT_BRACKET, // [
GHOSTTY_KEY_RIGHT_BRACKET, // ]
- GHOSTTY_KEY_BACKSLASH, // /
+ GHOSTTY_KEY_BACKSLASH, // \
// control
GHOSTTY_KEY_UP,
@@ -412,6 +412,7 @@ typedef enum {
GHOSTTY_FULLSCREEN_NATIVE,
GHOSTTY_FULLSCREEN_NON_NATIVE,
GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU,
+ GHOSTTY_FULLSCREEN_NON_NATIVE_PADDED_NOTCH,
} ghostty_action_fullscreen_e;
// apprt.action.SecureInput
@@ -562,8 +563,10 @@ typedef enum {
GHOSTTY_ACTION_QUIT,
GHOSTTY_ACTION_NEW_WINDOW,
GHOSTTY_ACTION_NEW_TAB,
+ GHOSTTY_ACTION_CLOSE_TAB,
GHOSTTY_ACTION_NEW_SPLIT,
GHOSTTY_ACTION_CLOSE_ALL_WINDOWS,
+ GHOSTTY_ACTION_TOGGLE_MAXIMIZE,
GHOSTTY_ACTION_TOGGLE_FULLSCREEN,
GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW,
GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
@@ -583,6 +586,7 @@ typedef enum {
GHOSTTY_ACTION_RENDER_INSPECTOR,
GHOSTTY_ACTION_DESKTOP_NOTIFICATION,
GHOSTTY_ACTION_SET_TITLE,
+ GHOSTTY_ACTION_PROMPT_TITLE,
GHOSTTY_ACTION_PWD,
GHOSTTY_ACTION_MOUSE_SHAPE,
GHOSTTY_ACTION_MOUSE_VISIBILITY,
@@ -642,7 +646,7 @@ typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
ghostty_clipboard_e,
bool);
typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
-typedef void (*ghostty_runtime_action_cb)(ghostty_app_t,
+typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t,
ghostty_target_s,
ghostty_action_s);
diff --git a/macos/Assets.xcassets/Alternate Icons/BlueprintImage.imageset/Contents.json b/macos/Assets.xcassets/Alternate Icons/BlueprintImage.imageset/Contents.json
new file mode 100644
index 0000000000..1c1b9b47eb
--- /dev/null
+++ b/macos/Assets.xcassets/Alternate Icons/BlueprintImage.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "macOS-AppIcon-1024px.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/macos/Assets.xcassets/Alternate Icons/BlueprintImage.imageset/macOS-AppIcon-1024px.png b/macos/Assets.xcassets/Alternate Icons/BlueprintImage.imageset/macOS-AppIcon-1024px.png
new file mode 100644
index 0000000000..ffba7d94dc
Binary files /dev/null and b/macos/Assets.xcassets/Alternate Icons/BlueprintImage.imageset/macOS-AppIcon-1024px.png differ
diff --git a/macos/Assets.xcassets/Alternate Icons/ChalkboardImage.imageset/Contents.json b/macos/Assets.xcassets/Alternate Icons/ChalkboardImage.imageset/Contents.json
new file mode 100644
index 0000000000..1c1b9b47eb
--- /dev/null
+++ b/macos/Assets.xcassets/Alternate Icons/ChalkboardImage.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "macOS-AppIcon-1024px.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/macos/Assets.xcassets/Alternate Icons/ChalkboardImage.imageset/macOS-AppIcon-1024px.png b/macos/Assets.xcassets/Alternate Icons/ChalkboardImage.imageset/macOS-AppIcon-1024px.png
new file mode 100644
index 0000000000..eeedb72031
Binary files /dev/null and b/macos/Assets.xcassets/Alternate Icons/ChalkboardImage.imageset/macOS-AppIcon-1024px.png differ
diff --git a/macos/Assets.xcassets/Alternate Icons/Contents.json b/macos/Assets.xcassets/Alternate Icons/Contents.json
new file mode 100644
index 0000000000..73c00596a7
--- /dev/null
+++ b/macos/Assets.xcassets/Alternate Icons/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/macos/Assets.xcassets/Alternate Icons/GlassImage.imageset/Contents.json b/macos/Assets.xcassets/Alternate Icons/GlassImage.imageset/Contents.json
new file mode 100644
index 0000000000..1c1b9b47eb
--- /dev/null
+++ b/macos/Assets.xcassets/Alternate Icons/GlassImage.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "macOS-AppIcon-1024px.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/macos/Assets.xcassets/Alternate Icons/GlassImage.imageset/macOS-AppIcon-1024px.png b/macos/Assets.xcassets/Alternate Icons/GlassImage.imageset/macOS-AppIcon-1024px.png
new file mode 100644
index 0000000000..99d704e27b
Binary files /dev/null and b/macos/Assets.xcassets/Alternate Icons/GlassImage.imageset/macOS-AppIcon-1024px.png differ
diff --git a/macos/Assets.xcassets/Alternate Icons/HolographicImage.imageset/Contents.json b/macos/Assets.xcassets/Alternate Icons/HolographicImage.imageset/Contents.json
new file mode 100644
index 0000000000..1c1b9b47eb
--- /dev/null
+++ b/macos/Assets.xcassets/Alternate Icons/HolographicImage.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "macOS-AppIcon-1024px.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/macos/Assets.xcassets/Alternate Icons/HolographicImage.imageset/macOS-AppIcon-1024px.png b/macos/Assets.xcassets/Alternate Icons/HolographicImage.imageset/macOS-AppIcon-1024px.png
new file mode 100644
index 0000000000..b31c9e9736
Binary files /dev/null and b/macos/Assets.xcassets/Alternate Icons/HolographicImage.imageset/macOS-AppIcon-1024px.png differ
diff --git a/macos/Assets.xcassets/Alternate Icons/MicrochipImage.imageset/Contents.json b/macos/Assets.xcassets/Alternate Icons/MicrochipImage.imageset/Contents.json
new file mode 100644
index 0000000000..1c1b9b47eb
--- /dev/null
+++ b/macos/Assets.xcassets/Alternate Icons/MicrochipImage.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "macOS-AppIcon-1024px.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/macos/Assets.xcassets/Alternate Icons/MicrochipImage.imageset/macOS-AppIcon-1024px.png b/macos/Assets.xcassets/Alternate Icons/MicrochipImage.imageset/macOS-AppIcon-1024px.png
new file mode 100644
index 0000000000..add488d362
Binary files /dev/null and b/macos/Assets.xcassets/Alternate Icons/MicrochipImage.imageset/macOS-AppIcon-1024px.png differ
diff --git a/macos/Assets.xcassets/Alternate Icons/PaperImage.imageset/Contents.json b/macos/Assets.xcassets/Alternate Icons/PaperImage.imageset/Contents.json
new file mode 100644
index 0000000000..1c1b9b47eb
--- /dev/null
+++ b/macos/Assets.xcassets/Alternate Icons/PaperImage.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "macOS-AppIcon-1024px.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/macos/Assets.xcassets/Alternate Icons/PaperImage.imageset/macOS-AppIcon-1024px.png b/macos/Assets.xcassets/Alternate Icons/PaperImage.imageset/macOS-AppIcon-1024px.png
new file mode 100644
index 0000000000..fad8dc70b6
Binary files /dev/null and b/macos/Assets.xcassets/Alternate Icons/PaperImage.imageset/macOS-AppIcon-1024px.png differ
diff --git a/macos/Assets.xcassets/Alternate Icons/RetroImage.imageset/Contents.json b/macos/Assets.xcassets/Alternate Icons/RetroImage.imageset/Contents.json
new file mode 100644
index 0000000000..1c1b9b47eb
--- /dev/null
+++ b/macos/Assets.xcassets/Alternate Icons/RetroImage.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "macOS-AppIcon-1024px.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/macos/Assets.xcassets/Alternate Icons/RetroImage.imageset/macOS-AppIcon-1024px.png b/macos/Assets.xcassets/Alternate Icons/RetroImage.imageset/macOS-AppIcon-1024px.png
new file mode 100644
index 0000000000..02619e860e
Binary files /dev/null and b/macos/Assets.xcassets/Alternate Icons/RetroImage.imageset/macOS-AppIcon-1024px.png differ
diff --git a/macos/Assets.xcassets/Alternate Icons/XrayImage.imageset/Contents.json b/macos/Assets.xcassets/Alternate Icons/XrayImage.imageset/Contents.json
new file mode 100644
index 0000000000..1c1b9b47eb
--- /dev/null
+++ b/macos/Assets.xcassets/Alternate Icons/XrayImage.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "macOS-AppIcon-1024px.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/macos/Assets.xcassets/Alternate Icons/XrayImage.imageset/macOS-AppIcon-1024px.png b/macos/Assets.xcassets/Alternate Icons/XrayImage.imageset/macOS-AppIcon-1024px.png
new file mode 100644
index 0000000000..9e74a967c2
Binary files /dev/null and b/macos/Assets.xcassets/Alternate Icons/XrayImage.imageset/macOS-AppIcon-1024px.png differ
diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj
index fded209113..0c68da5346 100644
--- a/macos/Ghostty.xcodeproj/project.pbxproj
+++ b/macos/Ghostty.xcodeproj/project.pbxproj
@@ -69,9 +69,13 @@
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; };
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
+ A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3C92D4445E20033CF96 /* Dock.swift */; };
+ A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */; };
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; };
+ A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */; };
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
+ A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; };
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; };
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
@@ -102,6 +106,7 @@
C159E89D2B69A2EF00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; };
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EA62B738B9900404083 /* NSView+Extension.swift */; };
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EE82B76CBFC00404083 /* VibrantLayer.m */; };
+ CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */; };
FC5218FA2D10FFCE004C93E0 /* zsh in Resources */ = {isa = PBXBuildFile; fileRef = FC5218F92D10FFC7004C93E0 /* zsh */; };
FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */ = {isa = PBXBuildFile; fileRef = FC9ABA9B2D0F538D0020D4C8 /* bash-completion */; };
/* End PBXBuildFile section */
@@ -161,11 +166,15 @@
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; };
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = ""; };
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; };
+ A5A2A3C92D4445E20033CF96 /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = ""; };
+ A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Extension.swift"; sourceTree = ""; };
A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = ""; };
+ A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastWindowPosition.swift; sourceTree = ""; };
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; };
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = ""; };
+ A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; };
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = ""; };
A5CBD0572C9F30860017A1AE /* Cursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = ""; };
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QuickTerminal.xib; sourceTree = ""; };
@@ -198,6 +207,7 @@
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VibrantLayer.h; sourceTree = ""; };
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VibrantLayer.m; sourceTree = ""; };
C1F26EEA2B76CC2400404083 /* ghostty-bridging-header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ghostty-bridging-header.h"; sourceTree = ""; };
+ CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalSpaceBehavior.swift; sourceTree = ""; };
FC5218F92D10FFC7004C93E0 /* zsh */ = {isa = PBXFileReference; lastKnownFileType = folder; name = zsh; path = "../zig-out/share/zsh"; sourceTree = ""; };
FC9ABA9B2D0F538D0020D4C8 /* bash-completion */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "bash-completion"; path = "../zig-out/share/bash-completion"; sourceTree = ""; };
/* End PBXFileReference section */
@@ -262,11 +272,13 @@
A534263D2A7DCBB000EBB7A2 /* Helpers */ = {
isa = PBXGroup;
children = (
+ A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */,
A5A6F7292CC41B8700B232A5 /* Xcode.swift */,
A5CEAFFE29C2410700646FDA /* Backport.swift */,
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
+ A5A2A3C92D4445E20033CF96 /* Dock.swift */,
A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */,
A59630962AEE163600D64628 /* HostingWindow.swift */,
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */,
@@ -274,12 +286,14 @@
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
+ A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */,
A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */,
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
A5985CD62C320C4500C57AD3 /* String+Extension.swift */,
A5CC36142C9CDA03004D6760 /* View+Extension.swift */,
+ A5CA378D2D31D6C100931030 /* Weak.swift */,
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */,
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */,
A5CEAFDA29B8005900646FDA /* SplitView */,
@@ -448,6 +462,7 @@
children = (
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */,
A5CBD05D2CA0C5E70017A1AE /* QuickTerminalController.swift */,
+ CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */,
A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */,
A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */,
A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */,
@@ -611,11 +626,13 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */,
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
+ CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
@@ -628,6 +645,7 @@
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
+ A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */,
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
@@ -643,12 +661,14 @@
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */,
A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */,
A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */,
+ A5CA378E2D31D6C300931030 /* Weak.swift in Sources */,
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
+ A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */,
A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */,
A5FEB3002ABB69450068369E /* main.swift in Sources */,
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
diff --git a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index d380910a8b..5ace476e08 100644
--- a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
- "revision" : "b456fd404954a9e13f55aa0c88cd5a40b8399638",
- "version" : "2.6.3"
+ "revision" : "0ef1ee0220239b3776f433314515fd849025673f",
+ "version" : "2.6.4"
}
}
],
diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift
index e3518cd2b5..12f8f8c31b 100644
--- a/macos/Sources/App/macOS/AppDelegate.swift
+++ b/macos/Sources/App/macOS/AppDelegate.swift
@@ -30,15 +30,18 @@ class AppDelegate: NSObject,
@IBOutlet private var menuSplitRight: NSMenuItem?
@IBOutlet private var menuSplitDown: NSMenuItem?
@IBOutlet private var menuClose: NSMenuItem?
+ @IBOutlet private var menuCloseTab: NSMenuItem?
@IBOutlet private var menuCloseWindow: NSMenuItem?
@IBOutlet private var menuCloseAllWindows: NSMenuItem?
@IBOutlet private var menuCopy: NSMenuItem?
@IBOutlet private var menuPaste: NSMenuItem?
+ @IBOutlet private var menuPasteSelection: NSMenuItem?
@IBOutlet private var menuSelectAll: NSMenuItem?
@IBOutlet private var menuToggleVisibility: NSMenuItem?
@IBOutlet private var menuToggleFullScreen: NSMenuItem?
+ @IBOutlet private var menuBringAllToFront: NSMenuItem?
@IBOutlet private var menuZoomSplit: NSMenuItem?
@IBOutlet private var menuPreviousSplit: NSMenuItem?
@IBOutlet private var menuNextSplit: NSMenuItem?
@@ -50,6 +53,7 @@ class AppDelegate: NSObject,
@IBOutlet private var menuIncreaseFontSize: NSMenuItem?
@IBOutlet private var menuDecreaseFontSize: NSMenuItem?
@IBOutlet private var menuResetFontSize: NSMenuItem?
+ @IBOutlet private var menuChangeTitle: NSMenuItem?
@IBOutlet private var menuQuickTerminal: NSMenuItem?
@IBOutlet private var menuTerminalInspector: NSMenuItem?
@@ -90,10 +94,8 @@ class AppDelegate: NSObject,
return ProcessInfo.processInfo.systemUptime - applicationLaunchTime
}
- /// Tracks whether the application is currently visible. This can be gamed, i.e. if a user manually
- /// brings each window one by one to the front. But at worst its off by one set of toggles and this
- /// makes our logic very easy.
- private var isVisible: Bool = true
+ /// Tracks the windows that we hid for toggleVisibility.
+ private var hiddenState: ToggleVisibilityState? = nil
/// The observer for the app appearance.
private var appearanceObserver: NSKeyValueObservation? = nil
@@ -217,15 +219,20 @@ class AppDelegate: NSObject,
}
func applicationDidBecomeActive(_ notification: Notification) {
- guard !applicationHasBecomeActive else { return }
- applicationHasBecomeActive = true
-
- // Let's launch our first window. We only do this if we have no other windows. It
- // is possible to have other windows in a few scenarios:
- // - if we're opening a URL since `application(_:openFile:)` is called before this.
- // - if we're restoring from persisted state
- if terminalManager.windows.count == 0 && derivedConfig.initialWindow {
- terminalManager.newWindow()
+ // If we're back manually then clear the hidden state because macOS handles it.
+ self.hiddenState = nil
+
+ // First launch stuff
+ if (!applicationHasBecomeActive) {
+ applicationHasBecomeActive = true
+
+ // Let's launch our first window. We only do this if we have no other windows. It
+ // is possible to have other windows in a few scenarios:
+ // - if we're opening a URL since `application(_:openFile:)` is called before this.
+ // - if we're restoring from persisted state
+ if terminalManager.windows.count == 0 && derivedConfig.initialWindow {
+ terminalManager.newWindow()
+ }
}
}
@@ -240,7 +247,13 @@ class AppDelegate: NSObject,
// This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't
// quite work with SwiftUI because windows are retained on close. So instead we check
// if there are any that are visible. I'm guessing this breaks under certain scenarios.
- if (windows.allSatisfy { !$0.isVisible }) { return .terminateNow }
+ //
+ // NOTE(mitchellh): I don't think we need this check at all anymore. I'm keeping it
+ // here because I don't want to remove it in a patch release cycle but we should
+ // target removing it soon.
+ if (self.quickController == nil && windows.allSatisfy { !$0.isVisible }) {
+ return .terminateNow
+ }
// If the user is shutting down, restarting, or logging out, we don't confirm quit.
why: if let event = NSAppleEventManager.shared().currentAppleEvent {
@@ -346,6 +359,7 @@ class AppDelegate: NSObject,
syncMenuShortcut(config, action: "new_window", menuItem: self.menuNewWindow)
syncMenuShortcut(config, action: "new_tab", menuItem: self.menuNewTab)
syncMenuShortcut(config, action: "close_surface", menuItem: self.menuClose)
+ syncMenuShortcut(config, action: "close_tab", menuItem: self.menuCloseTab)
syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow)
syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows)
syncMenuShortcut(config, action: "new_split:right", menuItem: self.menuSplitRight)
@@ -353,6 +367,7 @@ class AppDelegate: NSObject,
syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy)
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
+ syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection)
syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll)
syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
@@ -371,6 +386,7 @@ class AppDelegate: NSObject,
syncMenuShortcut(config, action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize)
+ syncMenuShortcut(config, action: "change_title_prompt", menuItem: self.menuChangeTitle)
syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility)
syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector)
@@ -424,7 +440,7 @@ class AppDelegate: NSObject,
// If we have a main window then we don't process any of the keys
// because we let it capture and propagate.
guard NSApp.mainWindow == nil else { return event }
-
+
// If this event as-is would result in a key binding then we send it.
if let app = ghostty.app,
ghostty_app_key_is_binding(
@@ -440,26 +456,26 @@ class AppDelegate: NSObject,
return nil
}
}
-
+
// If this event would be handled by our menu then we do nothing.
if let mainMenu = NSApp.mainMenu,
mainMenu.performKeyEquivalent(with: event) {
return nil
}
-
+
// If we reach this point then we try to process the key event
// through the Ghostty key mechanism.
-
+
// Ghostty must be loaded
guard let ghostty = self.ghostty.app else { return event }
-
+
// Build our event input and call ghostty
if (ghostty_app_key(ghostty, event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS))) {
// The key was used so we want to stop it from going to our Mac app
Ghostty.logger.debug("local key event handled event=\(event)")
return nil
}
-
+
return event
}
@@ -517,6 +533,15 @@ class AppDelegate: NSObject,
// AppKit mutex on the appearance.
DispatchQueue.main.async { self.syncAppearance(config: config) }
+ // Decide whether to hide/unhide app from dock and app switcher
+ switch (config.macosHidden) {
+ case .never:
+ NSApp.setActivationPolicy(.regular)
+
+ case .always:
+ NSApp.setActivationPolicy(.accessory)
+ }
+
// If we have configuration errors, we need to show them.
let c = ConfigurationErrorsController.sharedInstance
c.errors = config.errors
@@ -550,6 +575,30 @@ class AppDelegate: NSObject,
self.appIcon = nil
break
+ case .blueprint:
+ self.appIcon = NSImage(named: "BlueprintImage")!
+
+ case .chalkboard:
+ self.appIcon = NSImage(named: "ChalkboardImage")!
+
+ case .glass:
+ self.appIcon = NSImage(named: "GlassImage")!
+
+ case .holographic:
+ self.appIcon = NSImage(named: "HolographicImage")!
+
+ case .microchip:
+ self.appIcon = NSImage(named: "MicrochipImage")!
+
+ case .paper:
+ self.appIcon = NSImage(named: "PaperImage")!
+
+ case .retro:
+ self.appIcon = NSImage(named: "RetroImage")!
+
+ case .xray:
+ self.appIcon = NSImage(named: "XrayImage")!
+
case .customStyle:
guard let ghostColor = config.macosIconGhostColor else { break }
guard let screenColors = config.macosIconScreenColor else { break }
@@ -702,21 +751,35 @@ class AppDelegate: NSObject,
/// Toggles visibility of all Ghosty Terminal windows. When hidden, activates Ghostty as the frontmost application
@IBAction func toggleVisibility(_ sender: Any) {
- // We only care about terminal windows.
- for window in NSApp.windows.filter({ $0.windowController is BaseTerminalController }) {
- if isVisible {
- window.orderOut(nil)
- } else {
- window.makeKeyAndOrderFront(nil)
- }
+ // If we have focus, then we hide all windows.
+ if NSApp.isActive {
+ // Toggle visibility doesn't do anything if the focused window is native
+ // fullscreen. This is only relevant if Ghostty is active.
+ guard let keyWindow = NSApp.keyWindow,
+ !keyWindow.styleMask.contains(.fullScreen) else { return }
+
+ // Keep track of our hidden state to restore properly
+ self.hiddenState = .init()
+ NSApp.hide(nil)
+ return
}
- // After bringing them all to front we make sure our app is active too.
- if !isVisible {
+ // If we're not active, we want to become active
+ NSApp.activate(ignoringOtherApps: true)
+
+ // Bring all windows to the front. Note: we don't use NSApp.unhide because
+ // that will unhide ALL hidden windows. We want to only bring forward the
+ // ones that we hid.
+ hiddenState?.restore()
+ hiddenState = nil
+ }
+
+ @IBAction func bringAllToFront(_ sender: Any) {
+ if !NSApp.isActive {
NSApp.activate(ignoringOtherApps: true)
}
-
- isVisible.toggle()
+
+ NSApplication.shared.arrangeInFront(sender)
}
private struct DerivedConfig {
@@ -736,4 +799,33 @@ class AppDelegate: NSObject,
self.quickTerminalPosition = config.quickTerminalPosition
}
}
+
+ private struct ToggleVisibilityState {
+ let hiddenWindows: [Weak]
+ let keyWindow: Weak?
+
+ init() {
+ // We need to know the key window so that we can bring focus back to the
+ // right window if it was hidden.
+ self.keyWindow = if let keyWindow = NSApp.keyWindow {
+ .init(keyWindow)
+ } else {
+ nil
+ }
+
+ // We need to keep track of the windows that were visible because we only
+ // want to bring back these windows if we remove the toggle.
+ //
+ // We also ignore fullscreen windows because they don't hide anyways.
+ self.hiddenWindows = NSApp.windows.filter {
+ $0.isVisible &&
+ !$0.styleMask.contains(.fullScreen)
+ }.map { Weak($0) }
+ }
+
+ func restore() {
+ hiddenWindows.forEach { $0.value?.orderFrontRegardless() }
+ keyWindow?.value?.makeKey()
+ }
+ }
}
diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib
index 7a8e0d894b..05364212fe 100644
--- a/macos/Sources/App/macOS/MainMenu.xib
+++ b/macos/Sources/App/macOS/MainMenu.xib
@@ -1,8 +1,8 @@
-
+
-
+
@@ -14,9 +14,12 @@
+
+
+
@@ -31,6 +34,7 @@
+
@@ -154,6 +158,12 @@
+
+
+
+
-
+
+
diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
index 47ee2dfd95..8079358063 100644
--- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
+++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
@@ -3,6 +3,12 @@ import Cocoa
import SwiftUI
import GhosttyKit
+// This is a Apple's private function that we need to call to get the active space.
+@_silgen_name("CGSGetActiveSpace")
+func CGSGetActiveSpace(_ cid: Int) -> size_t
+@_silgen_name("CGSMainConnectionID")
+func CGSMainConnectionID() -> Int
+
/// Controller for the "quick" terminal.
class QuickTerminalController: BaseTerminalController {
override var windowNibName: NSNib.Name? { "QuickTerminal" }
@@ -18,6 +24,12 @@ class QuickTerminalController: BaseTerminalController {
/// application to the front.
private var previousApp: NSRunningApplication? = nil
+ // The active space when the quick terminal was last shown.
+ private var previousActiveSpace: size_t = 0
+
+ /// Non-nil if we have hidden dock state.
+ private var hiddenDock: HiddenDock? = nil
+
/// The configuration derived from the Ghostty config so we don't need to rely on references.
private var derivedConfig: DerivedConfig
@@ -32,6 +44,11 @@ class QuickTerminalController: BaseTerminalController {
// Setup our notifications for behaviors
let center = NotificationCenter.default
+ center.addObserver(
+ self,
+ selector: #selector(applicationWillTerminate(_:)),
+ name: NSApplication.willTerminateNotification,
+ object: nil)
center.addObserver(
self,
selector: #selector(onToggleFullscreen),
@@ -52,6 +69,9 @@ class QuickTerminalController: BaseTerminalController {
// Remove all of our notificationcenter subscriptions
let center = NotificationCenter.default
center.removeObserver(self)
+
+ // Make sure we restore our hidden dock
+ hiddenDock = nil
}
// MARK: NSWindowController
@@ -87,6 +107,17 @@ class QuickTerminalController: BaseTerminalController {
// MARK: NSWindowDelegate
+ override func windowDidBecomeKey(_ notification: Notification) {
+ super.windowDidBecomeKey(notification)
+
+ // If we're not visible we don't care to run the logic below. It only
+ // applies if we can be seen.
+ guard visible else { return }
+
+ // Re-hide the dock if we were hiding it before.
+ hiddenDock?.hide()
+ }
+
override func windowDidResignKey(_ notification: Notification) {
super.windowDidResignKey(notification)
@@ -107,8 +138,32 @@ class QuickTerminalController: BaseTerminalController {
self.previousApp = nil
}
- if (derivedConfig.quickTerminalAutoHide) {
- animateOut()
+ // Regardless of autohide, we always want to bring the dock back
+ // when we lose focus.
+ hiddenDock?.restore()
+
+ if derivedConfig.quickTerminalAutoHide {
+ switch derivedConfig.quickTerminalSpaceBehavior {
+ case .remain:
+ // If we lose focus on the active space, then we can animate out
+ animateOut()
+
+ case .move:
+ let currentActiveSpace = CGSGetActiveSpace(CGSMainConnectionID())
+ if previousActiveSpace == currentActiveSpace {
+ // We haven't moved spaces. We lost focus to another app on the
+ // current space. Animate out.
+ animateOut()
+ } else {
+ // We've moved to a different space. Bring the quick terminal back
+ // into view.
+ DispatchQueue.main.async {
+ self.window?.makeKeyAndOrderFront(nil)
+ }
+
+ self.previousActiveSpace = currentActiveSpace
+ }
+ }
}
}
@@ -163,6 +218,9 @@ class QuickTerminalController: BaseTerminalController {
}
}
+ // Set previous active space
+ self.previousActiveSpace = CGSGetActiveSpace(CGSMainConnectionID())
+
// Animate the window in
animateWindowIn(window: window, from: position)
@@ -198,8 +256,29 @@ class QuickTerminalController: BaseTerminalController {
// Move our window off screen to the top
position.setInitial(in: window, on: screen)
+ // We need to set our window level to a high value. In testing, only
+ // popUpMenu and above do what we want. This gets it above the menu bar
+ // and lets us render off screen.
+ window.level = .popUpMenu
+
// Move it to the visible position since animation requires this
- window.makeKeyAndOrderFront(nil)
+ DispatchQueue.main.async {
+ window.makeKeyAndOrderFront(nil)
+ }
+
+ // If our dock position would conflict with our target location then
+ // we autohide the dock.
+ if position.conflictsWithDock(on: screen) {
+ if (hiddenDock == nil) {
+ hiddenDock = .init()
+ }
+
+ hiddenDock?.hide()
+ } else {
+ // Ensure we don't have any hidden dock if we don't conflict.
+ // The deinit will restore.
+ hiddenDock = nil
+ }
// Run the animation that moves our window into the proper place and makes
// it visible.
@@ -211,8 +290,16 @@ class QuickTerminalController: BaseTerminalController {
// There is a very minor delay here so waiting at least an event loop tick
// keeps us safe from the view not being on the window.
DispatchQueue.main.async {
- // If we canceled our animation in we do nothing
- guard self.visible else { return }
+ // If we canceled our animation clean up some state.
+ guard self.visible else {
+ self.hiddenDock = nil
+ return
+ }
+
+ // After animating in, we reset the window level to a value that
+ // is above other windows but not as high as popUpMenu. This allows
+ // things like IME dropdowns to appear properly.
+ window.level = .floating
// Now that the window is visible, sync our appearance. This function
// requires the window is visible.
@@ -276,6 +363,17 @@ class QuickTerminalController: BaseTerminalController {
}
private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) {
+ // If we hid the dock then we unhide it.
+ hiddenDock = nil
+
+ // If the window isn't on our active space then we don't animate, we just
+ // hide it.
+ if !window.isOnActiveSpace {
+ self.previousApp = nil
+ window.orderOut(self)
+ return
+ }
+
// We always animate out to whatever screen the window is actually on.
guard let screen = window.screen ?? NSScreen.main else { return }
@@ -297,6 +395,11 @@ class QuickTerminalController: BaseTerminalController {
}
}
+ // We need to set our window level to a high value. In testing, only
+ // popUpMenu and above do what we want. This gets it above the menu bar
+ // and lets us render off screen.
+ window.level = .popUpMenu
+
NSAnimationContext.runAnimationGroup({ context in
context.duration = derivedConfig.quickTerminalAnimationDuration
context.timingFunction = .init(name: .easeIn)
@@ -311,23 +414,13 @@ class QuickTerminalController: BaseTerminalController {
private func syncAppearance() {
guard let window else { return }
+ // Change the collection behavior of the window depending on the configuration.
+ window.collectionBehavior = derivedConfig.quickTerminalSpaceBehavior.collectionBehavior
+
// If our window is not visible, then no need to sync the appearance yet.
// Some APIs such as window blur have no effect unless the window is visible.
guard window.isVisible else { return }
- // Terminals typically operate in sRGB color space and macOS defaults
- // to "native" which is typically P3. There is a lot more resources
- // covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
- // Ghostty defaults to sRGB but this can be overridden.
- switch (self.derivedConfig.windowColorspace) {
- case "display-p3":
- window.colorSpace = .displayP3
- case "srgb":
- fallthrough
- default:
- window.colorSpace = .sRGB
- }
-
// If we have window transparency then set it transparent. Otherwise set it opaque.
if (self.derivedConfig.backgroundOpacity < 1) {
window.isOpaque = false
@@ -368,6 +461,13 @@ class QuickTerminalController: BaseTerminalController {
// MARK: Notifications
+ @objc private func applicationWillTerminate(_ notification: Notification) {
+ // If the application is going to terminate we want to make sure we
+ // restore any global dock state. I think deinit should be called which
+ // would call this anyways but I can't be sure so I will do this too.
+ hiddenDock = nil
+ }
+
@objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard target == self.focusedSurface else { return }
@@ -396,14 +496,14 @@ class QuickTerminalController: BaseTerminalController {
let quickTerminalScreen: QuickTerminalScreen
let quickTerminalAnimationDuration: Double
let quickTerminalAutoHide: Bool
- let windowColorspace: String
+ let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior
let backgroundOpacity: Double
init() {
self.quickTerminalScreen = .main
self.quickTerminalAnimationDuration = 0.2
self.quickTerminalAutoHide = true
- self.windowColorspace = ""
+ self.quickTerminalSpaceBehavior = .move
self.backgroundOpacity = 1.0
}
@@ -411,10 +511,39 @@ class QuickTerminalController: BaseTerminalController {
self.quickTerminalScreen = config.quickTerminalScreen
self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration
self.quickTerminalAutoHide = config.quickTerminalAutoHide
- self.windowColorspace = config.windowColorspace
+ self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior
self.backgroundOpacity = config.backgroundOpacity
}
}
+
+ /// Hides the dock globally (not just NSApp). This is only used if the quick terminal is
+ /// in a conflicting position with the dock.
+ private class HiddenDock {
+ let previousAutoHide: Bool
+ private var hidden: Bool = false
+
+ init() {
+ previousAutoHide = Dock.autoHideEnabled
+ }
+
+ deinit {
+ restore()
+ }
+
+ func hide() {
+ guard !hidden else { return }
+ NSApp.acquirePresentationOption(.autoHideDock)
+ Dock.autoHideEnabled = true
+ hidden = true
+ }
+
+ func restore() {
+ guard hidden else { return }
+ NSApp.releasePresentationOption(.autoHideDock)
+ Dock.autoHideEnabled = previousAutoHide
+ hidden = false
+ }
+ }
}
extension Notification.Name {
diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
index 0acbfec1b2..7ba124a309 100644
--- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
+++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
@@ -69,7 +69,7 @@ enum QuickTerminalPosition : String {
finalSize.width = screen.frame.width
case .left, .right:
- finalSize.height = screen.frame.height
+ finalSize.height = screen.visibleFrame.height
case .center:
finalSize.width = screen.frame.width / 2
@@ -118,4 +118,22 @@ enum QuickTerminalPosition : String {
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
}
}
+
+ func conflictsWithDock(on screen: NSScreen) -> Bool {
+ // Screen must have a dock for it to conflict
+ guard screen.hasDock else { return false }
+
+ // Get the dock orientation for this screen
+ guard let orientation = Dock.orientation else { return false }
+
+ // Depending on the orientation of the dock, we conflict if our quick terminal
+ // would potentially "hit" the dock. In the future we should probably consider
+ // the frame of the quick terminal.
+ return switch (orientation) {
+ case .top: self == .top || self == .left || self == .right
+ case .bottom: self == .bottom || self == .left || self == .right
+ case .left: self == .top || self == .bottom
+ case .right: self == .top || self == .bottom
+ }
+ }
}
diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift
new file mode 100644
index 0000000000..0561aaa188
--- /dev/null
+++ b/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift
@@ -0,0 +1,36 @@
+import Foundation
+import Cocoa
+
+enum QuickTerminalSpaceBehavior {
+ case remain
+ case move
+
+ init?(fromGhosttyConfig string: String) {
+ switch (string) {
+ case "move":
+ self = .move
+
+ case "remain":
+ self = .remain
+
+ default:
+ return nil
+ }
+ }
+
+ var collectionBehavior: NSWindow.CollectionBehavior {
+ let commonBehavior: [NSWindow.CollectionBehavior] = [
+ .ignoresCycle,
+ .fullScreenAuxiliary
+ ]
+
+ switch (self) {
+ case .move:
+ // We want this to move the window to the active space.
+ return NSWindow.CollectionBehavior([.canJoinAllSpaces] + commonBehavior)
+ case .remain:
+ // We want this to remain the window in the current space.
+ return NSWindow.CollectionBehavior([.moveToActiveSpace] + commonBehavior)
+ }
+ }
+}
diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift
index ed3a7f781c..005808a23d 100644
--- a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift
+++ b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift
@@ -1,6 +1,6 @@
import Cocoa
-class QuickTerminalWindow: NSWindow {
+class QuickTerminalWindow: NSPanel {
// Both of these must be true for windows without decorations to be able to
// still become key/main and receive events.
override var canBecomeKey: Bool { return true }
@@ -26,22 +26,7 @@ class QuickTerminalWindow: NSWindow {
// window remains resizable.
self.styleMask.remove(.titled)
- // We need to set our window level to a high value. In testing, only
- // popUpMenu and above do what we want. This gets it above the menu bar
- // and lets us render off screen.
- self.level = .popUpMenu
-
- // This plus the level above was what was needed for the animation to work,
- // because it gets the window off screen properly. Plus we add some fields
- // we just want the behavior of.
- self.collectionBehavior = [
- // We want this to be part of every space because it is a singleton.
- .canJoinAllSpaces,
-
- // We don't want to be part of command-tilde
- .ignoresCycle,
-
- // We never support fullscreen
- .fullScreenNone]
+ // We don't want to activate the owning app when quick terminal is triggered.
+ self.styleMask.insert(.nonactivatingPanel)
}
}
diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift
index 393c6ef4dd..bace20f052 100644
--- a/macos/Sources/Features/Terminal/BaseTerminalController.swift
+++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift
@@ -389,9 +389,9 @@ class BaseTerminalController: NSWindowController,
}
switch (request) {
- case .osc_52_write:
+ case let .osc_52_write(pasteboard):
guard case .confirm = action else { break }
- let pb = NSPasteboard.general
+ let pb = pasteboard ?? NSPasteboard.general
pb.declareTypes([.string], owner: nil)
pb.setString(cc.contents, forType: .string)
case .osc_52_read, .paste:
@@ -452,6 +452,7 @@ class BaseTerminalController: NSWindowController,
self.alert = nil
switch (response) {
case .alertFirstButtonReturn:
+ alert.window.orderOut(nil)
window.close()
default:
diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift
index 2da498e3ac..8118103d67 100644
--- a/macos/Sources/Features/Terminal/TerminalController.swift
+++ b/macos/Sources/Features/Terminal/TerminalController.swift
@@ -22,7 +22,7 @@ class TerminalController: BaseTerminalController {
private var restorable: Bool = true
/// The configuration derived from the Ghostty config so we don't need to rely on references.
- private var derivedConfig: DerivedConfig
+ private(set) var derivedConfig: DerivedConfig
/// The notification cancellable for focused surface property changes.
private var surfaceAppearanceCancellables: Set = []
@@ -60,6 +60,11 @@ class TerminalController: BaseTerminalController {
selector: #selector(onGotoTab),
name: Ghostty.Notification.ghosttyGotoTab,
object: nil)
+ center.addObserver(
+ self,
+ selector: #selector(onCloseTab),
+ name: .ghosttyCloseTab,
+ object: nil)
center.addObserver(
self,
selector: #selector(ghosttyConfigDidChange(_:)),
@@ -278,9 +283,12 @@ class TerminalController: BaseTerminalController {
private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) {
guard let window else { return }
- // If we don't have both an X and Y we center.
+ // If we don't have an X/Y then we try to use the previously saved window pos.
guard let x, let y else {
- window.center()
+ if (!LastWindowPosition.shared.restore(window)) {
+ window.center()
+ }
+
return
}
@@ -310,28 +318,28 @@ class TerminalController: BaseTerminalController {
window.styleMask = [
// We need `titled` in the mask to get the normal window frame
.titled,
-
+
// Full size content view so we can extend
// content in to the hidden titlebar's area
- .fullSizeContentView,
-
- .resizable,
+ .fullSizeContentView,
+
+ .resizable,
.closable,
.miniaturizable,
]
-
+
// Hide the title
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
-
+
// Hide the traffic lights (window control buttons)
window.standardWindowButton(.closeButton)?.isHidden = true
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true
-
+
// Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar.
window.tabbingMode = .disallowed
-
+
// Nuke it from orbit -- hide the titlebar container entirely, just in case. There are
// some operations that appear to bring back the titlebar visibility so this ensures
// it is gone forever.
@@ -340,7 +348,7 @@ class TerminalController: BaseTerminalController {
titleBarContainer.isHidden = true
}
}
-
+
override func windowDidLoad() {
super.windowDidLoad()
guard let window = window as? TerminalWindow else { return }
@@ -361,33 +369,31 @@ class TerminalController: BaseTerminalController {
// If window decorations are disabled, remove our title
if (!config.windowDecorations) { window.styleMask.remove(.titled) }
- // Terminals typically operate in sRGB color space and macOS defaults
- // to "native" which is typically P3. There is a lot more resources
- // covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
- // Ghostty defaults to sRGB but this can be overridden.
- switch (config.windowColorspace) {
- case "display-p3":
- window.colorSpace = .displayP3
- case "srgb":
- fallthrough
- default:
- window.colorSpace = .sRGB
- }
-
// If we have only a single surface (no splits) and that surface requested
// an initial size then we set it here now.
if case let .leaf(leaf) = surfaceTree {
if let initialSize = leaf.surface.initialSize,
let screen = window.screen ?? NSScreen.main {
- // Setup our frame. We need to first subtract the views frame so that we can
- // just get the chrome frame so that we only affect the surface view size.
+ // Get the current frame of the window
var frame = window.frame
- frame.size.width -= leaf.surface.frame.size.width
- frame.size.height -= leaf.surface.frame.size.height
- frame.size.width += min(initialSize.width, screen.frame.width)
- frame.size.height += min(initialSize.height, screen.frame.height)
- // We have no tabs and we are not a split, so set the initial size of the window.
+ // Calculate the chrome size (window size minus view size)
+ let chromeWidth = frame.size.width - leaf.surface.frame.size.width
+ let chromeHeight = frame.size.height - leaf.surface.frame.size.height
+
+ // Calculate the new width and height, clamping to the screen's size
+ let newWidth = min(initialSize.width + chromeWidth, screen.visibleFrame.width)
+ let newHeight = min(initialSize.height + chromeHeight, screen.visibleFrame.height)
+
+ // Update the frame size while keeping the window's position intact
+ frame.size.width = newWidth
+ frame.size.height = newHeight
+
+ // Ensure the window doesn't go outside the screen boundaries
+ frame.origin.x = max(screen.frame.origin.x, min(frame.origin.x, screen.frame.maxX - newWidth))
+ frame.origin.y = max(screen.frame.origin.y, min(frame.origin.y, screen.frame.maxY - newHeight))
+
+ // Set the updated frame to the window
window.setFrame(frame, display: true)
}
}
@@ -487,6 +493,20 @@ class TerminalController: BaseTerminalController {
override func windowDidMove(_ notification: Notification) {
super.windowDidMove(notification)
self.fixTabBar()
+
+ // Whenever we move save our last position for the next start.
+ if let window {
+ LastWindowPosition.shared.save(window)
+ }
+ }
+
+ func windowDidBecomeMain(_ notification: Notification) {
+ // Whenever we get focused, use that as our last window position for
+ // restart. This differs from Terminal.app but matches iTerm2 behavior
+ // and I think its sensible.
+ if let window {
+ LastWindowPosition.shared.save(window)
+ }
}
// Called when the window will be encoded. We handle the data encoding here in the
@@ -508,7 +528,50 @@ class TerminalController: BaseTerminalController {
ghostty.newTab(surface: surface)
}
- @IBAction override func closeWindow(_ sender: Any) {
+ private func confirmClose(
+ window: NSWindow,
+ messageText: String,
+ informativeText: String,
+ completion: @escaping () -> Void
+ ) {
+ // If we need confirmation by any, show one confirmation for all windows
+ // in the tab group.
+ let alert = NSAlert()
+ alert.messageText = messageText
+ alert.informativeText = informativeText
+ alert.addButton(withTitle: "Close")
+ alert.addButton(withTitle: "Cancel")
+ alert.alertStyle = .warning
+ alert.beginSheetModal(for: window) { response in
+ if response == .alertFirstButtonReturn {
+ completion()
+ }
+ }
+ }
+
+ @IBAction func closeTab(_ sender: Any?) {
+ guard let window = window else { return }
+ guard window.tabGroup != nil else {
+ // No tabs, no tab group, just perform a normal close.
+ window.performClose(sender)
+ return
+ }
+
+ if surfaceTree?.needsConfirmQuit() ?? false {
+ confirmClose(
+ window: window,
+ messageText: "Close Tab?",
+ informativeText: "The terminal still has a running process. If you close the tab the process will be killed."
+ ) {
+ window.close()
+ }
+ return
+ }
+
+ window.close()
+ }
+
+ @IBAction override func closeWindow(_ sender: Any?) {
guard let window = window else { return }
guard let tabGroup = window.tabGroup else {
// No tabs, no tab group, just perform a normal close.
@@ -523,47 +586,34 @@ class TerminalController: BaseTerminalController {
}
// Check if any windows require close confirmation.
- var needsConfirm: Bool = false
- for tabWindow in tabGroup.windows {
- guard let c = tabWindow.windowController as? TerminalController else { continue }
- if (c.surfaceTree?.needsConfirmQuit() ?? false) {
- needsConfirm = true
- break
+ let needsConfirm = tabGroup.windows.contains { tabWindow in
+ guard let controller = tabWindow.windowController as? TerminalController else {
+ return false
}
+ return controller.surfaceTree?.needsConfirmQuit() ?? false
}
// If none need confirmation then we can just close all the windows.
- if (!needsConfirm) {
- for tabWindow in tabGroup.windows {
- tabWindow.close()
- }
-
+ if !needsConfirm {
+ tabGroup.windows.forEach { $0.close() }
return
}
- // If we need confirmation by any, show one confirmation for all windows
- // in the tab group.
- let alert = NSAlert()
- alert.messageText = "Close Window?"
- alert.informativeText = "All terminal sessions in this window will be terminated."
- alert.addButton(withTitle: "Close Window")
- alert.addButton(withTitle: "Cancel")
- alert.alertStyle = .warning
- alert.beginSheetModal(for: window, completionHandler: { response in
- if (response == .alertFirstButtonReturn) {
- for tabWindow in tabGroup.windows {
- tabWindow.close()
- }
- }
- })
+ confirmClose(
+ window: window,
+ messageText: "Close Window?",
+ informativeText: "All terminal sessions in this window will be terminated."
+ ) {
+ tabGroup.windows.forEach { $0.close() }
+ }
}
- @IBAction func toggleGhosttyFullScreen(_ sender: Any) {
+ @IBAction func toggleGhosttyFullScreen(_ sender: Any?) {
guard let surface = focusedSurface?.surface else { return }
ghostty.toggleFullscreen(surface: surface)
}
- @IBAction func toggleTerminalInspector(_ sender: Any) {
+ @IBAction func toggleTerminalInspector(_ sender: Any?) {
guard let surface = focusedSurface?.surface else { return }
ghostty.toggleTerminalInspector(surface: surface)
}
@@ -659,13 +709,21 @@ class TerminalController: BaseTerminalController {
// If our index is the same we do nothing
guard finalIndex != selectedIndex else { return }
- // Get our parent
- let parent = tabbedWindows[finalIndex]
+ // Get our target window
+ let targetWindow = tabbedWindows[finalIndex]
+
+ // Begin a group of window operations to minimize visual updates
+ NSAnimationContext.beginGrouping()
+ NSAnimationContext.current.duration = 0
- // Move our current selected window to the proper index
+ // Remove and re-add the window in the correct position
tabGroup.removeWindow(selectedWindow)
- parent.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above)
- selectedWindow.makeKeyAndOrderFront(nil)
+ targetWindow.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above)
+
+ // Ensure our window remains selected
+ selectedWindow.makeKey()
+
+ NSAnimationContext.endGrouping()
}
@objc private func onGotoTab(notification: SwiftUI.Notification) {
@@ -720,6 +778,12 @@ class TerminalController: BaseTerminalController {
targetWindow.makeKeyAndOrderFront(nil)
}
+ @objc private func onCloseTab(notification: SwiftUI.Notification) {
+ guard let target = notification.object as? Ghostty.SurfaceView else { return }
+ guard surfaceTree?.contains(view: target) ?? false else { return }
+ closeTab(self)
+ }
+
@objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard target == self.focusedSurface else { return }
@@ -737,7 +801,7 @@ class TerminalController: BaseTerminalController {
toggleFullscreen(mode: fullscreenMode)
}
- private struct DerivedConfig {
+ struct DerivedConfig {
let backgroundColor: Color
let macosTitlebarStyle: String
diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift
index 42e35b90eb..2db29aec91 100644
--- a/macos/Sources/Features/Terminal/TerminalManager.swift
+++ b/macos/Sources/Features/Terminal/TerminalManager.swift
@@ -86,7 +86,7 @@ class TerminalManager {
// fullscreen for the logic later in this method.
c.toggleFullscreen(mode: .native)
- case .nonNative, .nonNativeVisibleMenu:
+ case .nonNative, .nonNativeVisibleMenu, .nonNativePaddedNotch:
// If we're non-native then we have to do it on a later loop
// so that the content view is setup.
DispatchQueue.main.async {
diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift
index 15b5048750..3d4165e915 100644
--- a/macos/Sources/Features/Terminal/TerminalView.swift
+++ b/macos/Sources/Features/Terminal/TerminalView.swift
@@ -10,7 +10,7 @@ protocol TerminalViewDelegate: AnyObject {
/// The title of the terminal should change.
func titleDidChange(to: String)
-
+
/// The URL of the pwd should change.
func pwdDidChange(to: URL?)
@@ -56,15 +56,10 @@ struct TerminalView: View {
// The title for our window
private var title: String {
- var title = "👻"
-
- if let surfaceTitle = surfaceTitle {
- if (surfaceTitle.count > 0) {
- title = surfaceTitle
- }
+ if let surfaceTitle, !surfaceTitle.isEmpty {
+ return surfaceTitle
}
-
- return title
+ return "👻"
}
// The pwd of the focused surface as a URL
@@ -72,7 +67,7 @@ struct TerminalView: View {
guard let surfacePwd, surfacePwd != "" else { return nil }
return URL(fileURLWithPath: surfacePwd)
}
-
+
var body: some View {
switch ghostty.readiness {
case .loading:
diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift
index 0eb8daeeb3..9d29c193f5 100644
--- a/macos/Sources/Features/Terminal/TerminalWindow.swift
+++ b/macos/Sources/Features/Terminal/TerminalWindow.swift
@@ -115,6 +115,21 @@ class TerminalWindow: NSWindow {
}
}
+ // We override this so that with the hidden titlebar style the titlebar
+ // area is not draggable.
+ override var contentLayoutRect: CGRect {
+ var rect = super.contentLayoutRect
+
+ // If we are using a hidden titlebar style, the content layout is the
+ // full frame making it so that it is not draggable.
+ if let controller = windowController as? TerminalController,
+ controller.derivedConfig.macosTitlebarStyle == "hidden" {
+ rect.origin.y = 0
+ rect.size.height = self.frame.height
+ }
+ return rect
+ }
+
// The window theme configuration from Ghostty. This is used to control some
// behaviors that don't look quite right in certain situations.
var windowTheme: TerminalWindowTheme?
diff --git a/macos/Sources/Ghostty/FullscreenMode+Extension.swift b/macos/Sources/Ghostty/FullscreenMode+Extension.swift
index fffd8e84b6..0c0bba908e 100644
--- a/macos/Sources/Ghostty/FullscreenMode+Extension.swift
+++ b/macos/Sources/Ghostty/FullscreenMode+Extension.swift
@@ -13,6 +13,9 @@ extension FullscreenMode {
case GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU:
.nonNativeVisibleMenu
+ case GHOSTTY_FULLSCREEN_NON_NATIVE_PADDED_NOTCH:
+ .nonNativePaddedNotch
+
default:
nil
}
diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift
index ed140dcd53..ba249e3d11 100644
--- a/macos/Sources/Ghostty/Ghostty.App.swift
+++ b/macos/Sources/Ghostty/Ghostty.App.swift
@@ -62,7 +62,7 @@ extension Ghostty {
// uses to interface with the application runtime environment.
var runtime_cfg = ghostty_runtime_config_s(
userdata: Unmanaged.passUnretained(self).toOpaque(),
- supports_selection_clipboard: false,
+ supports_selection_clipboard: true,
wakeup_cb: { userdata in App.wakeup(userdata) },
action_cb: { app, target, action in App.action(app!, target: target, action: action) },
read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) },
@@ -257,7 +257,7 @@ extension Ghostty {
// MARK: Ghostty Callbacks (iOS)
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {}
- static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) {}
+ static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) -> Bool { return false }
static func readClipboard(
_ userdata: UnsafeMutableRawPointer?,
location: ghostty_clipboard_e,
@@ -320,13 +320,13 @@ extension Ghostty {
let surfaceView = self.surfaceUserdata(from: userdata)
guard let surface = surfaceView.surface else { return }
- // We only support the standard clipboard
- if (location != GHOSTTY_CLIPBOARD_STANDARD) {
+ // Get our pasteboard
+ guard let pasteboard = NSPasteboard.ghostty(location) else {
return completeClipboardRequest(surface, data: "", state: state)
}
// Get our string
- let str = NSPasteboard.general.getOpinionatedStringContents() ?? ""
+ let str = pasteboard.getOpinionatedStringContents() ?? ""
completeClipboardRequest(surface, data: str, state: state)
}
@@ -364,14 +364,12 @@ extension Ghostty {
static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?, location: ghostty_clipboard_e, confirm: Bool) {
let surface = self.surfaceUserdata(from: userdata)
- // We only support the standard clipboard
- if (location != GHOSTTY_CLIPBOARD_STANDARD) { return }
+ guard let pasteboard = NSPasteboard.ghostty(location) else { return }
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
if !confirm {
- let pb = NSPasteboard.general
- pb.declareTypes([.string], owner: nil)
- pb.setString(valueStr, forType: .string)
+ pasteboard.declareTypes([.string], owner: nil)
+ pasteboard.setString(valueStr, forType: .string)
return
}
@@ -380,7 +378,7 @@ extension Ghostty {
object: surface,
userInfo: [
Notification.ConfirmClipboardStrKey: valueStr,
- Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write,
+ Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write(pasteboard),
]
)
}
@@ -425,7 +423,7 @@ extension Ghostty {
// MARK: Actions (macOS)
- static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) {
+ static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) -> Bool {
// Make sure it a target we understand so all our action handlers can assert
switch (target.tag) {
case GHOSTTY_TARGET_APP, GHOSTTY_TARGET_SURFACE:
@@ -433,7 +431,7 @@ extension Ghostty {
default:
Ghostty.logger.warning("unknown action target=\(target.tag.rawValue)")
- return
+ return false
}
// Action dispatch
@@ -450,17 +448,20 @@ extension Ghostty {
case GHOSTTY_ACTION_NEW_SPLIT:
newSplit(app, target: target, direction: action.action.new_split)
+ case GHOSTTY_ACTION_CLOSE_TAB:
+ closeTab(app, target: target)
+
case GHOSTTY_ACTION_TOGGLE_FULLSCREEN:
toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen)
case GHOSTTY_ACTION_MOVE_TAB:
- moveTab(app, target: target, move: action.action.move_tab)
+ return moveTab(app, target: target, move: action.action.move_tab)
case GHOSTTY_ACTION_GOTO_TAB:
- gotoTab(app, target: target, tab: action.action.goto_tab)
+ return gotoTab(app, target: target, tab: action.action.goto_tab)
case GHOSTTY_ACTION_GOTO_SPLIT:
- gotoSplit(app, target: target, direction: action.action.goto_split)
+ return gotoSplit(app, target: target, direction: action.action.goto_split)
case GHOSTTY_ACTION_RESIZE_SPLIT:
resizeSplit(app, target: target, resize: action.action.resize_split)
@@ -483,6 +484,9 @@ extension Ghostty {
case GHOSTTY_ACTION_SET_TITLE:
setTitle(app, target: target, v: action.action.set_title)
+ case GHOSTTY_ACTION_PROMPT_TITLE:
+ return promptTitle(app, target: target)
+
case GHOSTTY_ACTION_PWD:
pwdChanged(app, target: target, v: action.action.pwd)
@@ -540,10 +544,15 @@ extension Ghostty {
fallthrough
case GHOSTTY_ACTION_QUIT_TIMER:
Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)")
-
+ return false
default:
Ghostty.logger.warning("unknown action action=\(action.tag.rawValue)")
+ return false
}
+
+ // If we reached here then we assume performed since all unknown actions
+ // are captured in the switch and return false.
+ return true
}
private static func quit(_ app: ghostty_app_t) {
@@ -653,6 +662,27 @@ extension Ghostty {
}
}
+ private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s) {
+ switch (target.tag) {
+ case GHOSTTY_TARGET_APP:
+ Ghostty.logger.warning("close tab does nothing with an app target")
+ return
+
+ case GHOSTTY_TARGET_SURFACE:
+ guard let surface = target.target.surface else { return }
+ guard let surfaceView = self.surfaceView(from: surface) else { return }
+
+ NotificationCenter.default.post(
+ name: .ghosttyCloseTab,
+ object: surfaceView
+ )
+
+
+ default:
+ assertionFailure()
+ }
+ }
+
private static func toggleFullscreen(
_ app: ghostty_app_t,
target: ghostty_target_s,
@@ -694,15 +724,19 @@ extension Ghostty {
private static func moveTab(
_ app: ghostty_app_t,
target: ghostty_target_s,
- move: ghostty_action_move_tab_s) {
+ move: ghostty_action_move_tab_s) -> Bool {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("move tab 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 }
+
+ // See gotoTab for notes on this check.
+ guard (surfaceView.window?.tabGroup?.windows.count ?? 0) > 1 else { return false }
+
NotificationCenter.default.post(
name: .ghosttyMoveTab,
object: surfaceView,
@@ -714,20 +748,27 @@ extension Ghostty {
default:
assertionFailure()
}
+
+ return true
}
private static func gotoTab(
_ app: ghostty_app_t,
target: ghostty_target_s,
- tab: ghostty_action_goto_tab_e) {
+ tab: ghostty_action_goto_tab_e) -> Bool {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("goto tab 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 }
+
+ // Similar to goto_split (see comment there) about our performability,
+ // we should make this more accurate later.
+ guard (surfaceView.window?.tabGroup?.windows.count ?? 0) > 1 else { return false }
+
NotificationCenter.default.post(
name: Notification.ghosttyGotoTab,
object: surfaceView,
@@ -739,20 +780,31 @@ extension Ghostty {
default:
assertionFailure()
}
+
+ return true
}
private static func gotoSplit(
_ app: ghostty_app_t,
target: ghostty_target_s,
- direction: ghostty_action_goto_split_e) {
+ direction: ghostty_action_goto_split_e) -> Bool {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("goto 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 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 }
+
+ // For now, we return false if the window has no splits and we return
+ // true if the window has ANY splits. This isn't strictly correct because
+ // we should only be returning true if we actually performed the action,
+ // but this handles the most common case of caring about goto_split performability
+ // which is the no-split case.
+ guard controller.surfaceTree?.isSplit ?? false else { return false }
+
NotificationCenter.default.post(
name: Notification.ghosttyFocusSplit,
object: surfaceView,
@@ -764,6 +816,8 @@ extension Ghostty {
default:
assertionFailure()
}
+
+ return true
}
private static func resizeSplit(
@@ -956,6 +1010,26 @@ extension Ghostty {
}
}
+ private static func promptTitle(
+ _ app: ghostty_app_t,
+ target: ghostty_target_s) -> Bool {
+ switch (target.tag) {
+ case GHOSTTY_TARGET_APP:
+ Ghostty.logger.warning("set title prompt does nothing with an app target")
+ return false
+
+ case GHOSTTY_TARGET_SURFACE:
+ guard let surface = target.target.surface else { return false }
+ guard let surfaceView = self.surfaceView(from: surface) else { return false }
+ surfaceView.promptTitle()
+
+ default:
+ assertionFailure()
+ }
+
+ return true
+ }
+
private static func pwdChanged(
_ app: ghostty_app_t,
target: ghostty_target_s,
diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift
index ed9364914d..ec23632f75 100644
--- a/macos/Sources/Ghostty/Ghostty.Config.swift
+++ b/macos/Sources/Ghostty/Ghostty.Config.swift
@@ -132,15 +132,6 @@ extension Ghostty {
return v
}
- var windowColorspace: String {
- guard let config = self.config else { return "" }
- var v: UnsafePointer? = nil
- let key = "window-colorspace"
- guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
- guard let ptr = v else { return "" }
- return String(cString: ptr)
- }
-
var windowSaveState: String {
guard let config = self.config else { return "" }
var v: UnsafePointer? = nil
@@ -174,11 +165,14 @@ extension Ghostty {
}
var windowDecorations: Bool {
- guard let config = self.config else { return true }
- var v = false;
+ let defaultValue = true
+ guard let config = self.config else { return defaultValue }
+ var v: UnsafePointer? = nil
let key = "window-decoration"
- _ = ghostty_config_get(config, &v, key, UInt(key.count))
- return v;
+ guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
+ guard let ptr = v else { return defaultValue }
+ let str = String(cString: ptr)
+ return WindowDecoration(rawValue: str)?.enabled() ?? defaultValue
}
var windowTheme: String? {
@@ -222,6 +216,8 @@ extension Ghostty {
.nonNative
case "visible-menu":
.nonNativeVisibleMenu
+ case "padded-notch":
+ .nonNativePaddedNotch
default:
defaultValue
}
@@ -306,6 +302,16 @@ extension Ghostty {
return buffer.map { .init(ghostty: $0) }
}
+ var macosHidden: MacHidden {
+ guard let config = self.config else { return .never }
+ var v: UnsafePointer? = nil
+ let key = "macos-hidden"
+ guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .never }
+ guard let ptr = v else { return .never }
+ let str = String(cString: ptr)
+ return MacHidden(rawValue: str) ?? .never
+ }
+
var focusFollowsMouse : Bool {
guard let config = self.config else { return false }
var v = false;
@@ -345,7 +351,7 @@ extension Ghostty {
var backgroundBlurRadius: Int {
guard let config = self.config else { return 1 }
var v: Int = 0
- let key = "background-blur-radius"
+ let key = "background-blur"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v;
}
@@ -431,6 +437,16 @@ extension Ghostty {
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v
}
+
+ var quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior {
+ guard let config = self.config else { return .move }
+ var v: UnsafePointer? = nil
+ let key = "quick-terminal-space-behavior"
+ guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .move }
+ guard let ptr = v else { return .move }
+ let str = String(cString: ptr)
+ return QuickTerminalSpaceBehavior(fromGhosttyConfig: str) ?? .move
+ }
#endif
var resizeOverlay: ResizeOverlay {
@@ -510,6 +526,11 @@ extension Ghostty.Config {
case download
}
+ enum MacHidden : String {
+ case never
+ case always
+ }
+
enum ResizeOverlay : String {
case always
case never
@@ -553,4 +574,18 @@ extension Ghostty.Config {
}
}
}
+
+ enum WindowDecoration: String {
+ case none
+ case client
+ case server
+ case auto
+
+ func enabled() -> Bool {
+ switch self {
+ case .client, .server, .auto: return true
+ case .none: return false
+ }
+ }
+ }
}
diff --git a/macos/Sources/Ghostty/Ghostty.SplitNode.swift b/macos/Sources/Ghostty/Ghostty.SplitNode.swift
index 899825d37b..95c019b1f9 100644
--- a/macos/Sources/Ghostty/Ghostty.SplitNode.swift
+++ b/macos/Sources/Ghostty/Ghostty.SplitNode.swift
@@ -38,6 +38,15 @@ extension Ghostty {
}
}
+ /// Returns true if the tree is split.
+ var isSplit: Bool {
+ return if case .leaf = self {
+ false
+ } else {
+ true
+ }
+ }
+
func topLeft() -> SurfaceView {
switch (self) {
case .leaf(let leaf):
@@ -120,14 +129,7 @@ extension Ghostty {
/// Returns true if the split tree contains the given view.
func contains(view: SurfaceView) -> Bool {
- switch (self) {
- case .leaf(let leaf):
- return leaf.surface == view
-
- case .split(let container):
- return container.topLeft.contains(view: view) ||
- container.bottomRight.contains(view: view)
- }
+ return leaf(for: view) != nil
}
/// Find a surface view by UUID.
@@ -164,6 +166,22 @@ extension Ghostty {
}
}
+ /// Return the node for the given view if its in the tree.
+ func leaf(for view: SurfaceView) -> Leaf? {
+ switch (self) {
+ case .leaf(let leaf):
+ if leaf.surface == view {
+ return leaf
+ } else {
+ return nil
+ }
+
+ case .split(let container):
+ return container.topLeft.leaf(for: view) ??
+ container.bottomRight.leaf(for: view)
+ }
+ }
+
// MARK: - Sequence
func makeIterator() -> IndexingIterator<[Leaf]> {
diff --git a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift
index cc3bef1492..cec1782459 100644
--- a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift
+++ b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift
@@ -205,6 +205,7 @@ extension Ghostty {
alert.beginSheetModal(for: window, completionHandler: { response in
switch (response) {
case .alertFirstButtonReturn:
+ alert.window.orderOut(nil)
node = nil
default:
diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift
index d091002123..18ef3f3a73 100644
--- a/macos/Sources/Ghostty/Package.swift
+++ b/macos/Sources/Ghostty/Package.swift
@@ -159,7 +159,7 @@ extension Ghostty {
case osc_52_read
/// An application is attempting to write to the clipboard using OSC 52
- case osc_52_write
+ case osc_52_write(OSPasteboard?)
/// The text to show in the clipboard confirmation prompt for a given request type
func text() -> String {
@@ -188,7 +188,7 @@ extension Ghostty {
case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ:
return .osc_52_read
case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE:
- return .osc_52_write
+ return .osc_52_write(nil)
default:
return nil
}
@@ -198,6 +198,14 @@ extension Ghostty {
/// macos-icon
enum MacOSIcon: String {
case official
+ case blueprint
+ case chalkboard
+ case glass
+ case holographic
+ case microchip
+ case paper
+ case retro
+ case xray
case customStyle = "custom-style"
}
@@ -236,6 +244,9 @@ extension Notification.Name {
/// Goto tab. Has tab index in the userinfo.
static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab")
static let GhosttyMoveTabKey = ghosttyMoveTab.rawValue
+
+ /// Close tab
+ static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab")
}
// NOTE: I am moving all of these to Notification.Name extensions over time. This
diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift
index 4abf87c7fa..beae503314 100644
--- a/macos/Sources/Ghostty/SurfaceView.swift
+++ b/macos/Sources/Ghostty/SurfaceView.swift
@@ -92,22 +92,6 @@ extension Ghostty {
windowFocus = false
}
}
- .onDrop(of: [.fileURL], isTargeted: nil) { providers in
- providers.forEach { provider in
- _ = provider.loadObject(ofClass: URL.self) { url, _ in
- guard let url = url else { return }
- let path = Shell.escape(url.path)
- DispatchQueue.main.async {
- surfaceView.insertText(
- path,
- replacementRange: NSMakeRange(0, 0)
- )
- }
- }
- }
-
- return true
- }
#endif
// If our geo size changed then we show the resize overlay as configured.
diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift
index cf4357a8ca..daa2ccfde5 100644
--- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift
+++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift
@@ -1,3 +1,4 @@
+import AppKit
import SwiftUI
import CoreText
import UserNotifications
@@ -12,7 +13,14 @@ extension Ghostty {
// The current title of the surface as defined by the pty. This can be
// changed with escape codes. This is public because the callbacks go
// to the app level and it is set from there.
- @Published private(set) var title: String = "👻"
+ @Published private(set) var title: String = "" {
+ didSet {
+ if !title.isEmpty {
+ titleFallbackTimer?.invalidate()
+ titleFallbackTimer = nil
+ }
+ }
+ }
// The current pwd of the surface as defined by the pty. This can be
// changed with escape codes.
@@ -113,6 +121,14 @@ extension Ghostty {
// A small delay that is introduced before a title change to avoid flickers
private var titleChangeTimer: Timer?
+ // A timer to fallback to ghost emoji if no title is set within the grace period
+ private var titleFallbackTimer: Timer?
+
+ // This is the title from the terminal. This is nil if we're currently using
+ // the terminal title as the main title property. If the title is set manually
+ // by the user, this is set to the prior value (which may be empty, but non-nil).
+ private var titleFromTerminal: String?
+
/// Event monitor (see individual events for why)
private var eventMonitor: Any? = nil
@@ -139,6 +155,13 @@ extension Ghostty {
// can do SOMETHING.
super.init(frame: NSMakeRect(0, 0, 800, 600))
+ // Set a timer to show the ghost emoji after 500ms if no title is set
+ titleFallbackTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
+ if let self = self, self.title.isEmpty {
+ self.title = "👻"
+ }
+ }
+
// Before we initialize the surface we want to register our notifications
// so there is no window where we can't receive them.
let center = NotificationCenter.default
@@ -213,6 +236,9 @@ extension Ghostty {
ghostty_surface_set_color_scheme(surface, scheme)
}
+
+ // The UTTypes that can be dragged onto this view.
+ registerForDraggedTypes(Array(Self.dropTypes))
}
required init?(coder: NSCoder) {
@@ -359,6 +385,45 @@ extension Ghostty {
NSCursor.setHiddenUntilMouseMoves(!visible)
}
+ /// Set the title by prompting the user.
+ func promptTitle() {
+ // Create an alert dialog
+ let alert = NSAlert()
+ alert.messageText = "Change Terminal Title"
+ alert.informativeText = "Leave blank to restore the default."
+ alert.alertStyle = .informational
+
+ // Add a text field to the alert
+ let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 250, height: 24))
+ textField.stringValue = title
+ alert.accessoryView = textField
+
+ // Add buttons
+ alert.addButton(withTitle: "OK")
+ alert.addButton(withTitle: "Cancel")
+
+ let response = alert.runModal()
+
+ // Check if the user clicked "OK"
+ if response == .alertFirstButtonReturn {
+ // Get the input text
+ let newTitle = textField.stringValue
+
+ if newTitle.isEmpty {
+ // Empty means that user wants the title to be set automatically
+ // We also need to reload the config for the "title" property to be
+ // used again by this tab.
+ let prevTitle = titleFromTerminal ?? "👻"
+ titleFromTerminal = nil
+ setTitle(prevTitle)
+ } else {
+ // Set the title and prevent it from being changed automatically
+ titleFromTerminal = title
+ title = newTitle
+ }
+ }
+ }
+
func setTitle(_ title: String) {
// This fixes an issue where very quick changes to the title could
// cause an unpleasant flickering. We set a timer so that we can
@@ -369,6 +434,11 @@ extension Ghostty {
withTimeInterval: 0.075,
repeats: false
) { [weak self] _ in
+ // Set the title if it wasn't manually set.
+ guard self?.titleFromTerminal == nil else {
+ self?.titleFromTerminal = title
+ return
+ }
self?.title = title
}
}
@@ -1096,6 +1166,8 @@ extension Ghostty {
menu.addItem(.separator())
menu.addItem(withTitle: "Reset Terminal", action: #selector(resetTerminal(_:)), keyEquivalent: "")
menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(toggleTerminalInspector(_:)), keyEquivalent: "")
+ menu.addItem(.separator())
+ menu.addItem(withTitle: "Change Title...", action: #selector(changeTitle(_:)), keyEquivalent: "")
return menu
}
@@ -1127,6 +1199,14 @@ extension Ghostty {
}
}
+ @IBAction func pasteSelection(_ sender: Any?) {
+ guard let surface = self.surface else { return }
+ let action = "paste_from_selection"
+ if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
+ AppDelegate.logger.warning("action failed action=\(action)")
+ }
+ }
+
@IBAction override func selectAll(_ sender: Any?) {
guard let surface = self.surface else { return }
let action = "select_all"
@@ -1160,6 +1240,10 @@ extension Ghostty {
AppDelegate.logger.warning("action failed action=\(action)")
}
}
+
+ @IBAction func changeTitle(_ sender: Any) {
+ promptTitle()
+ }
/// Show a user notification and associate it with this surface
func showUserNotification(title: String, body: String) {
@@ -1448,3 +1532,78 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor {
return true
}
}
+
+// MARK: NSMenuItemValidation
+
+extension Ghostty.SurfaceView: NSMenuItemValidation {
+ func validateMenuItem(_ item: NSMenuItem) -> Bool {
+ switch item.action {
+ case #selector(pasteSelection):
+ let pb = NSPasteboard.ghosttySelection
+ guard let str = pb.getOpinionatedStringContents() else { return false }
+ return !str.isEmpty
+
+ default:
+ return true
+ }
+ }
+}
+
+// MARK: NSDraggingDestination
+
+extension Ghostty.SurfaceView {
+ static let dropTypes: Set = [
+ .string,
+ .fileURL,
+ .URL
+ ]
+
+ override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation {
+ guard let types = sender.draggingPasteboard.types else { return [] }
+
+ // If the dragging object contains none of our types then we return none.
+ // This shouldn't happen because AppKit should guarantee that we only
+ // receive types we registered for but its good to check.
+ if Set(types).isDisjoint(with: Self.dropTypes) {
+ return []
+ }
+
+ // We use copy to get the proper icon
+ return .copy
+ }
+
+ override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool {
+ let pb = sender.draggingPasteboard
+
+ let content: String?
+ if let url = pb.string(forType: .URL) {
+ // URLs first, they get escaped as-is.
+ content = Ghostty.Shell.escape(url)
+ } else if let urls = pb.readObjects(forClasses: [NSURL.self]) as? [URL],
+ urls.count > 0 {
+ // File URLs next. They get escaped individually and then joined by a
+ // space if there are multiple.
+ content = urls
+ .map { Ghostty.Shell.escape($0.path) }
+ .joined(separator: " ")
+ } else if let str = pb.string(forType: .string) {
+ // Strings are not escaped because they may be copy/pasting a
+ // command they want to execute.
+ content = str
+ } else {
+ content = nil
+ }
+
+ if let content {
+ DispatchQueue.main.async {
+ self.insertText(
+ content,
+ replacementRange: NSMakeRange(0, 0)
+ )
+ }
+ return true
+ }
+
+ return false
+ }
+}
diff --git a/macos/Sources/Helpers/CrossKit.swift b/macos/Sources/Helpers/CrossKit.swift
index 5a69b45a37..690e811bb5 100644
--- a/macos/Sources/Helpers/CrossKit.swift
+++ b/macos/Sources/Helpers/CrossKit.swift
@@ -10,6 +10,7 @@ import AppKit
typealias OSView = NSView
typealias OSColor = NSColor
typealias OSSize = NSSize
+typealias OSPasteboard = NSPasteboard
protocol OSViewRepresentable: NSViewRepresentable where NSViewType == OSViewType {
associatedtype OSViewType: NSView
@@ -34,6 +35,7 @@ import UIKit
typealias OSView = UIView
typealias OSColor = UIColor
typealias OSSize = CGSize
+typealias OSPasteboard = UIPasteboard
protocol OSViewRepresentable: UIViewRepresentable {
associatedtype OSViewType: UIView
diff --git a/macos/Sources/Helpers/Dock.swift b/macos/Sources/Helpers/Dock.swift
new file mode 100644
index 0000000000..a71fcaa5b2
--- /dev/null
+++ b/macos/Sources/Helpers/Dock.swift
@@ -0,0 +1,38 @@
+import Cocoa
+
+// Private API to get Dock location
+@_silgen_name("CoreDockGetOrientationAndPinning")
+func CoreDockGetOrientationAndPinning(
+ _ outOrientation: UnsafeMutablePointer,
+ _ outPinning: UnsafeMutablePointer)
+
+// Private API to get the current Dock auto-hide state
+@_silgen_name("CoreDockGetAutoHideEnabled")
+func CoreDockGetAutoHideEnabled() -> Bool
+
+// Toggles the Dock's auto-hide state
+@_silgen_name("CoreDockSetAutoHideEnabled")
+func CoreDockSetAutoHideEnabled(_ flag: Bool)
+
+enum DockOrientation: Int {
+ case top = 1
+ case bottom = 2
+ case left = 3
+ case right = 4
+}
+
+class Dock {
+ /// Returns the orientation of the dock or nil if it can't be determined.
+ static var orientation: DockOrientation? {
+ var orientation: Int32 = 0
+ var pinning: Int32 = 0
+ CoreDockGetOrientationAndPinning(&orientation, &pinning)
+ return .init(rawValue: Int(orientation)) ?? nil
+ }
+
+ /// Set the dock autohide.
+ static var autoHideEnabled: Bool {
+ get { return CoreDockGetAutoHideEnabled() }
+ set { CoreDockSetAutoHideEnabled(newValue) }
+ }
+}
diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift
index a16f329f88..59865fc9eb 100644
--- a/macos/Sources/Helpers/Fullscreen.swift
+++ b/macos/Sources/Helpers/Fullscreen.swift
@@ -6,6 +6,7 @@ enum FullscreenMode {
case native
case nonNative
case nonNativeVisibleMenu
+ case nonNativePaddedNotch
/// Initializes the fullscreen style implementation for the mode. This will not toggle any
/// fullscreen properties. This may fail if the window isn't configured properly for a given
@@ -20,6 +21,9 @@ enum FullscreenMode {
case .nonNativeVisibleMenu:
return NonNativeFullscreenVisibleMenu(window)
+
+ case .nonNativePaddedNotch:
+ return NonNativeFullscreenPaddedNotch(window)
}
}
}
@@ -141,6 +145,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
struct Properties {
var hideMenu: Bool = true
+ var paddedNotch: Bool = false
}
private var savedState: SavedState?
@@ -278,6 +283,9 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
// put an #available check, but it was in a bug fix release so I think
// if a bug is reported to Ghostty we can just advise the user to
// update.
+ } else if (properties.paddedNotch) {
+ // We are hiding the menu, we may need to avoid the notch.
+ frame.size.height -= screen.safeAreaInsets.top
}
return frame
@@ -307,21 +315,21 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
// MARK: Dock
private func hideDock() {
- NSApp.presentationOptions.insert(.autoHideDock)
+ NSApp.acquirePresentationOption(.autoHideDock)
}
private func unhideDock() {
- NSApp.presentationOptions.remove(.autoHideDock)
+ NSApp.releasePresentationOption(.autoHideDock)
}
// MARK: Menu
func hideMenu() {
- NSApp.presentationOptions.insert(.autoHideMenuBar)
+ NSApp.acquirePresentationOption(.autoHideMenuBar)
}
func unhideMenu() {
- NSApp.presentationOptions.remove(.autoHideMenuBar)
+ NSApp.releasePresentationOption(.autoHideMenuBar)
}
/// The state that must be saved for non-native fullscreen to exit fullscreen.
@@ -349,3 +357,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
class NonNativeFullscreenVisibleMenu: NonNativeFullscreen {
override var properties: Properties { Properties(hideMenu: false) }
}
+
+class NonNativeFullscreenPaddedNotch: NonNativeFullscreen {
+ override var properties: Properties { Properties(paddedNotch: true) }
+}
diff --git a/macos/Sources/Helpers/LastWindowPosition.swift b/macos/Sources/Helpers/LastWindowPosition.swift
new file mode 100644
index 0000000000..a0dfa90dda
--- /dev/null
+++ b/macos/Sources/Helpers/LastWindowPosition.swift
@@ -0,0 +1,34 @@
+import Cocoa
+
+/// Manages the persistence and restoration of window positions across app launches.
+class LastWindowPosition {
+ static let shared = LastWindowPosition()
+
+ private let positionKey = "NSWindowLastPosition"
+
+ func save(_ window: NSWindow) {
+ let origin = window.frame.origin
+ let point = [origin.x, origin.y]
+ UserDefaults.standard.set(point, forKey: positionKey)
+ }
+
+ func restore(_ window: NSWindow) -> Bool {
+ guard let points = UserDefaults.standard.array(forKey: positionKey) as? [Double],
+ points.count == 2 else { return false }
+
+ let lastPosition = CGPoint(x: points[0], y: points[1])
+
+ guard let screen = window.screen ?? NSScreen.main else { return false }
+ let visibleFrame = screen.visibleFrame
+
+ var newFrame = window.frame
+ newFrame.origin = lastPosition
+ if !visibleFrame.contains(newFrame.origin) {
+ newFrame.origin.x = max(visibleFrame.minX, min(visibleFrame.maxX - newFrame.width, newFrame.origin.x))
+ newFrame.origin.y = max(visibleFrame.minY, min(visibleFrame.maxY - newFrame.height, newFrame.origin.y))
+ }
+
+ window.setFrame(newFrame, display: true)
+ return true
+ }
+}
diff --git a/macos/Sources/Helpers/NSApplication+Extension.swift b/macos/Sources/Helpers/NSApplication+Extension.swift
new file mode 100644
index 0000000000..0580cd5fc7
--- /dev/null
+++ b/macos/Sources/Helpers/NSApplication+Extension.swift
@@ -0,0 +1,31 @@
+import Cocoa
+
+extension NSApplication {
+ private static var presentationOptionCounts: [NSApplication.PresentationOptions.Element: UInt] = [:]
+
+ /// Add a presentation option to the application and main a reference count so that and equal
+ /// number of pops is required to disable it. This is useful so that multiple classes can affect global
+ /// app state without overriding others.
+ func acquirePresentationOption(_ option: NSApplication.PresentationOptions.Element) {
+ Self.presentationOptionCounts[option, default: 0] += 1
+ presentationOptions.insert(option)
+ }
+
+ /// See acquirePresentationOption
+ func releasePresentationOption(_ option: NSApplication.PresentationOptions.Element) {
+ guard let value = Self.presentationOptionCounts[option] else { return }
+ guard value > 0 else { return }
+ if (value == 1) {
+ presentationOptions.remove(option)
+ Self.presentationOptionCounts.removeValue(forKey: option)
+ } else {
+ Self.presentationOptionCounts[option] = value - 1
+ }
+ }
+}
+
+extension NSApplication.PresentationOptions.Element: @retroactive Hashable {
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(rawValue)
+ }
+}
diff --git a/macos/Sources/Helpers/NSPasteboard+Extension.swift b/macos/Sources/Helpers/NSPasteboard+Extension.swift
index b1755fea01..11815fbc87 100644
--- a/macos/Sources/Helpers/NSPasteboard+Extension.swift
+++ b/macos/Sources/Helpers/NSPasteboard+Extension.swift
@@ -1,17 +1,39 @@
import AppKit
+import GhosttyKit
extension NSPasteboard {
+ /// The pasteboard to used for Ghostty selection.
+ static var ghosttySelection: NSPasteboard = {
+ NSPasteboard(name: .init("com.mitchellh.ghostty.selection"))
+ }()
+
/// Gets the contents of the pasteboard as a string following a specific set of semantics.
/// Does these things in order:
- /// - Tries to get the absolute filesystem path of the file in the pasteboard if there is one.
+ /// - Tries to get the absolute filesystem path of the file in the pasteboard if there is one and ensures the file path is properly escaped.
/// - Tries to get any string from the pasteboard.
/// If all of the above fail, returns None.
func getOpinionatedStringContents() -> String? {
- if let file = self.string(forType: .fileURL) {
- if let path = NSURL(string: file)?.path {
- return path
- }
+ if let urls = readObjects(forClasses: [NSURL.self]) as? [URL],
+ urls.count > 0 {
+ return urls
+ .map { $0.isFileURL ? Ghostty.Shell.escape($0.path) : $0.absoluteString }
+ .joined(separator: " ")
}
+
return self.string(forType: .string)
}
+
+ /// The pasteboard for the Ghostty enum type.
+ static func ghostty(_ clipboard: ghostty_clipboard_e) -> NSPasteboard? {
+ switch (clipboard) {
+ case GHOSTTY_CLIPBOARD_STANDARD:
+ return Self.general
+
+ case GHOSTTY_CLIPBOARD_SELECTION:
+ return Self.ghosttySelection
+
+ default:
+ return nil
+ }
+ }
}
diff --git a/macos/Sources/Helpers/Weak.swift b/macos/Sources/Helpers/Weak.swift
new file mode 100644
index 0000000000..d5f784844f
--- /dev/null
+++ b/macos/Sources/Helpers/Weak.swift
@@ -0,0 +1,9 @@
+/// A wrapper that holds a weak reference to an object. This lets us create native containers
+/// of weak references.
+class Weak {
+ weak var value: T?
+
+ init(_ value: T) {
+ self.value = value
+ }
+}
diff --git a/nix/build-support/check-zig-cache-hash.sh b/nix/build-support/check-zig-cache-hash.sh
deleted file mode 100755
index 49ea29ffbf..0000000000
--- a/nix/build-support/check-zig-cache-hash.sh
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/usr/bin/env bash
-
-# Nothing in this script should fail.
-set -e
-
-CACHE_HASH_FILE="$(realpath "$(dirname "$0")/../zigCacheHash.nix")"
-
-help() {
- echo ""
- echo "To fix, please (manually) re-run the script from the repository root,"
- echo "commit, and push the update:"
- echo ""
- echo " ./nix/build-support/check-zig-cache-hash.sh --update"
- echo " git add nix/zigCacheHash.nix"
- echo " git commit -m \"nix: update Zig cache hash\""
- echo " git push"
- echo ""
-}
-
-if [ -f "${CACHE_HASH_FILE}" ]; then
- OLD_CACHE_HASH="$(nix eval --raw --file "${CACHE_HASH_FILE}")"
-elif [ "$1" != "--update" ]; then
- echo -e "\nERROR: Zig cache hash file missing."
- help
- exit 1
-fi
-
-ZIG_GLOBAL_CACHE_DIR="$(mktemp --directory --suffix nix-zig-cache)"
-export ZIG_GLOBAL_CACHE_DIR
-
-# This is not 100% necessary in CI but is helpful when running locally to keep
-# a local workstation clean.
-trap 'rm -rf "${ZIG_GLOBAL_CACHE_DIR}"' EXIT
-
-# Run Zig and download the cache to the temporary directory.
-
-sh ./nix/build-support/fetch-zig-cache.sh
-
-# Now, calculate the hash.
-ZIG_CACHE_HASH="sha256-$(nix-hash --type sha256 --to-base64 "$(nix-hash --type sha256 "${ZIG_GLOBAL_CACHE_DIR}")")"
-
-if [ "${OLD_CACHE_HASH}" == "${ZIG_CACHE_HASH}" ]; then
- echo -e "\nOK: Zig cache store hash unchanged."
- exit 0
-elif [ "$1" != "--update" ]; then
- echo -e "\nERROR: The Zig cache store hash has updated."
- echo ""
- echo " * Old hash: ${OLD_CACHE_HASH}"
- echo " * New hash: ${ZIG_CACHE_HASH}"
- help
- exit 1
-else
- echo -e "\nNew Zig cache store hash: ${ZIG_CACHE_HASH}"
-fi
-
-# Write out the cache file
-cat > "${CACHE_HASH_FILE}" < "$WORK_DIR/build.zig.zon.nix"
+alejandra --quiet "$WORK_DIR/build.zig.zon.nix"
+
+NEW_HASH=$(sha512sum "$WORK_DIR/build.zig.zon.nix" | awk '{print $1}')
+
+if [ "${OLD_HASH}" == "${NEW_HASH}" ]; then
+ echo -e "\nOK: build.zig.zon.nix unchanged."
+ exit 0
+elif [ "$1" != "--update" ]; then
+ echo -e "\nERROR: build.zig.zon.nix needs to be updated."
+ echo ""
+ echo " * Old hash: ${OLD_HASH}"
+ echo " * New hash: ${NEW_HASH}"
+ help
+ exit 1
+else
+ jq -r '.[] .url' "$BUILD_ZIG_ZON_LOCK" | sort > "$BUILD_ZIG_ZON_TXT"
+ mv "$WORK_DIR/build.zig.zon.nix" "$BUILD_ZIG_ZON_NIX"
+ echo -e "\nOK: build.zig.zon.nix updated."
+ exit 0
+fi
+
diff --git a/nix/build-support/fetch-zig-cache.sh b/nix/build-support/fetch-zig-cache.sh
index 56b94e35df..8ff00cad0d 100755
--- a/nix/build-support/fetch-zig-cache.sh
+++ b/nix/build-support/fetch-zig-cache.sh
@@ -1,32 +1,13 @@
#!/bin/sh
-set -e
-
-# Because Zig does not fetch recursive dependencies when you run `zig build
-# --fetch` (see https://github.com/ziglang/zig/issues/20976) we need to do some
-# extra work to fetch everything that we actually need to build without Internet
-# access (such as when building a Nix package).
-#
-# An example of this happening:
-#
-# error: builder for '/nix/store/cx8qcwrhjmjxik2547fw99v5j6np5san-ghostty-0.1.0.drv' failed with exit code 1;
-# la/build/tmp.xgHOheUF7V/p/12208cfdda4d5fdbc81b0c44b82e4d6dba2d4a86bff644a153e026fdfc80f8469133/build.zig.zon:7:20: error: unable to discover remote git server capabilities: TemporaryNameServerFailure
-# > .url = "git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e",
-# > ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-# > /build/tmp.xgHOheUF7V/p/12208cfdda4d5fdbc81b0c44b82e4d6dba2d4a86bff644a153e026fdfc80f8469133/build.zig.zon:16:20: error: unable to discover remote git server capabilities: TemporaryNameServerFailure
-# > .url = "git+https://github.com/mitchellh/libxev#f6a672a78436d8efee1aa847a43a900ad773618b",
-# > ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-# >
-# For full logs, run 'nix log /nix/store/cx8qcwrhjmjxik2547fw99v5j6np5san-ghostty-0.1.0.drv'.
-#
-# To update this script, add any failing URLs with a line like this:
-#
-# zig fetch
+# NOTE THIS IS A TEMPORARY SCRIPT TO SUPPORT PACKAGE MAINTAINERS.
#
-# Periodically old URLs may need to be cleaned out.
+# A future Zig version will hopefully fix the issue where
+# `zig build --fetch` doesn't fetch transitive dependencies[1]. When that
+# is resolved, we won't need any special machinery for the general use case
+# at all and packagers can just use `zig build --fetch`.
#
-# Hopefully when the Zig issue is fixed this script can be eliminated in favor
-# of a plain `zig build --fetch`.
+# [1]: https://github.com/ziglang/zig/issues/20976
if [ -z ${ZIG_GLOBAL_CACHE_DIR+x} ]
then
@@ -34,6 +15,13 @@ then
exit 1
fi
-zig build --fetch
-zig fetch git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e
-zig fetch git+https://github.com/mitchellh/libxev#f6a672a78436d8efee1aa847a43a900ad773618b
+# Go through each line of our build.zig.zon.txt and fetch it.
+SCRIPT_PATH="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
+ZON_TXT_FILE="$SCRIPT_PATH/../../build.zig.zon.txt"
+while IFS= read -r url; do
+ echo "Fetching: $url"
+ zig fetch "$url" >/dev/null 2>&1 || {
+ echo "Failed to fetch: $url" >&2
+ exit 1
+ }
+done < "$ZON_TXT_FILE"
diff --git a/nix/build-support/update-mirror.sh b/nix/build-support/update-mirror.sh
new file mode 100755
index 0000000000..35fd841e2e
--- /dev/null
+++ b/nix/build-support/update-mirror.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+#
+# This script generates a directory that can be uploaded to blob
+# storage to mirror our dependencies. The dependencies are unmodified
+# so their checksum and content hashes will match.
+
+set -e # Exit immediately if a command exits with a non-zero status
+
+SCRIPT_PATH="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
+INPUT_FILE="$SCRIPT_PATH/../../build.zig.zon2json-lock"
+OUTPUT_DIR="blob"
+
+# Ensure the output directory exists
+mkdir -p "$OUTPUT_DIR"
+
+# Use jq to iterate over the JSON and download files
+jq -r 'to_entries[] | "\(.key) \(.value.name) \(.value.url)"' "$INPUT_FILE" | while read -r key name url; do
+ # Skip URLs that don't start with http(s). They aren't necessary for
+ # our mirror.
+ if ! echo "$url" | grep -Eq "^https?://"; then
+ continue
+ fi
+
+ # Extract the file extension from the URL
+ extension=$(echo "$url" | grep -oE '\.[a-z0-9]+(\.[a-z0-9]+)?$')
+
+ filename="${name}-${key}${extension}"
+ echo "$url -> $filename"
+ curl -L -o "$OUTPUT_DIR/$filename" "$url"
+done
diff --git a/nix/devShell.nix b/nix/devShell.nix
index c52afb6c0c..66f2596566 100644
--- a/nix/devShell.nix
+++ b/nix/devShell.nix
@@ -14,6 +14,7 @@
python3,
qemu,
scdoc,
+ snapcraft,
valgrind,
#, vulkan-loader # unused
vttest,
@@ -30,7 +31,9 @@
glib,
glslang,
gtk4,
+ gobject-introspection,
libadwaita,
+ blueprint-compiler,
adwaita-icon-theme,
hicolor-icon-theme,
harfbuzz,
@@ -47,6 +50,7 @@
simdutf,
zlib,
alejandra,
+ jq,
minisign,
pandoc,
hyperfine,
@@ -54,6 +58,8 @@
wayland,
wayland-scanner,
wayland-protocols,
+ zig2nix,
+ system,
}: let
# See package.nix. Keep in sync.
rpathLibs =
@@ -83,6 +89,7 @@
libadwaita
gtk4
glib
+ gobject-introspection
wayland
];
in
@@ -92,6 +99,7 @@ in
packages =
[
# For builds
+ jq
llvmPackages_latest.llvm
minisign
ncurses
@@ -100,6 +108,7 @@ in
scdoc
zig
zip
+ zig2nix.packages.${system}.zon2nix
# For web and wasm stuff
nodejs
@@ -129,6 +138,7 @@ in
qemu
gdb
+ snapcraft
valgrind
wraptest
@@ -154,9 +164,11 @@ in
libXrandr
# Only needed for GTK builds
+ blueprint-compiler
libadwaita
gtk4
glib
+ gobject-introspection
wayland
wayland-scanner
wayland-protocols
diff --git a/nix/package.nix b/nix/package.nix
index 2f7825a562..45f189cf5b 100644
--- a/nix/package.nix
+++ b/nix/package.nix
@@ -2,6 +2,7 @@
lib,
stdenv,
bzip2,
+ callPackage,
expat,
fontconfig,
freetype,
@@ -12,7 +13,9 @@
libGL,
glib,
gtk4,
+ gobject-introspection,
libadwaita,
+ blueprint-compiler,
wrapGAppsHook4,
gsettings-desktop-schemas,
git,
@@ -40,82 +43,36 @@
# ultimately acted on and has made its way to a nixpkgs implementation, this
# can probably be removed in favor of that.
zig_hook = zig_0_13.hook.overrideAttrs {
- zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize}";
- };
-
- # We limit source like this to try and reduce the amount of rebuilds as possible
- # thus we only provide the source that is needed for the build
- #
- # NOTE: as of the current moment only linux files are provided,
- # since darwin support is not finished
- src = lib.fileset.toSource {
- root = ../.;
- fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
- lib.fileset.unions [
- ../dist/linux
- ../images
- ../include
- ../pkg
- ../src
- ../vendor
- ../build.zig
- ../build.zig.zon
- ./build-support/fetch-zig-cache.sh
- ]
- );
- };
-
- # This hash is the computation of the zigCache fixed-output derivation. This
- # allows us to use remote package dependencies without breaking the sandbox.
- #
- # This will need updating whenever dependencies get updated (e.g. changes are
- # made to zig.build.zon). If you see that the main build is trying to reach
- # out to the internet and failing, this is likely the cause. Change this
- # value back to lib.fakeHash, and re-run. The build failure should emit the
- # updated hash, which of course, should be validated before updating here.
- #
- # (It's also possible that you might see a hash mismatch - without the
- # network errors - if you don't have a previous instance of the cache
- # derivation in your store already. If so, just update the value as above.)
- zigCacheHash = import ./zigCacheHash.nix;
-
- zigCache = stdenv.mkDerivation {
- inherit src;
- name = "ghostty-cache";
- nativeBuildInputs = [
- git
- zig_hook
- ];
-
- dontConfigure = true;
- dontUseZigBuild = true;
- dontUseZigInstall = true;
- dontFixup = true;
-
- buildPhase = ''
- runHook preBuild
-
- sh ./nix/build-support/fetch-zig-cache.sh
-
- runHook postBuild
- '';
-
- installPhase = ''
- runHook preInstall
-
- cp -r --reflink=auto $ZIG_GLOBAL_CACHE_DIR $out
-
- runHook postInstall
- '';
-
- outputHashMode = "recursive";
- outputHash = zigCacheHash;
+ zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off";
};
in
stdenv.mkDerivation (finalAttrs: {
pname = "ghostty";
- version = "1.0.2";
- inherit src;
+ version = "1.1.3";
+
+ # We limit source like this to try and reduce the amount of rebuilds as possible
+ # thus we only provide the source that is needed for the build
+ #
+ # NOTE: as of the current moment only linux files are provided,
+ # since darwin support is not finished
+ src = lib.fileset.toSource {
+ root = ../.;
+ fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
+ lib.fileset.unions [
+ ../dist/linux
+ ../images
+ ../include
+ ../pkg
+ ../src
+ ../vendor
+ ../build.zig
+ ../build.zig.zon
+ ../build.zig.zon.nix
+ ]
+ );
+ };
+
+ deps = callPackage ../build.zig.zon.nix {name = "ghostty-cache-${finalAttrs.version}";};
nativeBuildInputs =
[
@@ -124,7 +81,9 @@ in
pandoc
pkg-config
zig_hook
+ gobject-introspection
wrapGAppsHook4
+ blueprint-compiler
]
++ lib.optionals enableWayland [
wayland-scanner
@@ -162,13 +121,13 @@ in
dontConfigure = true;
- zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString enableX11} -Dgtk-wayland=${lib.boolToString enableWayland}";
-
- preBuild = ''
- rm -rf $ZIG_GLOBAL_CACHE_DIR
- cp -r --reflink=auto ${zigCache} $ZIG_GLOBAL_CACHE_DIR
- chmod u+rwX -R $ZIG_GLOBAL_CACHE_DIR
- '';
+ zigBuildFlags = [
+ "--system"
+ "${finalAttrs.deps}"
+ "-Dversion-string=${finalAttrs.version}-${revision}-nix"
+ "-Dgtk-x11=${lib.boolToString enableX11}"
+ "-Dgtk-wayland=${lib.boolToString enableWayland}"
+ ];
outputs = [
"out"
@@ -202,7 +161,7 @@ in
'';
meta = {
- homepage = "https://github.com/ghostty-org/ghostty";
+ homepage = "https://ghostty.org";
license = lib.licenses.mit;
platforms = [
"x86_64-linux"
diff --git a/nix/vm/common-cinnamon.nix b/nix/vm/common-cinnamon.nix
new file mode 100644
index 0000000000..dabe5e7018
--- /dev/null
+++ b/nix/vm/common-cinnamon.nix
@@ -0,0 +1,18 @@
+{...}: {
+ imports = [
+ ./common.nix
+ ];
+
+ services.xserver = {
+ displayManager = {
+ lightdm = {
+ enable = true;
+ };
+ };
+ desktopManager = {
+ cinnamon = {
+ enable = true;
+ };
+ };
+ };
+}
diff --git a/nix/vm/common-gnome.nix b/nix/vm/common-gnome.nix
new file mode 100644
index 0000000000..0c2bef150c
--- /dev/null
+++ b/nix/vm/common-gnome.nix
@@ -0,0 +1,136 @@
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}: {
+ imports = [
+ ./common.nix
+ ];
+
+ services.xserver = {
+ displayManager = {
+ gdm = {
+ enable = true;
+ autoSuspend = false;
+ };
+ };
+ desktopManager = {
+ gnome = {
+ enable = true;
+ };
+ };
+ };
+
+ environment.systemPackages = [
+ pkgs.gnomeExtensions.no-overview
+ ];
+
+ environment.gnome.excludePackages = with pkgs; [
+ atomix
+ baobab
+ cheese
+ epiphany
+ evince
+ file-roller
+ geary
+ gnome-backgrounds
+ gnome-calculator
+ gnome-calendar
+ gnome-clocks
+ gnome-connections
+ gnome-contacts
+ gnome-disk-utility
+ gnome-extension-manager
+ gnome-logs
+ gnome-maps
+ gnome-music
+ gnome-photos
+ gnome-software
+ gnome-system-monitor
+ gnome-text-editor
+ gnome-themes-extra
+ gnome-tour
+ gnome-user-docs
+ gnome-weather
+ hitori
+ iagno
+ loupe
+ nautilus
+ orca
+ seahorse
+ simple-scan
+ snapshot
+ sushi
+ tali
+ totem
+ yelp
+ ];
+
+ programs.dconf = {
+ enable = true;
+ profiles.user.databases = [
+ {
+ settings = with lib.gvariant; {
+ "org/gnome/desktop/background" = {
+ picture-uri = "file://${pkgs.ghostty}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png";
+ picture-uri-dark = "file://${pkgs.ghostty}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png";
+ picture-options = "centered";
+ primary-color = "#000000000000";
+ secondary-color = "#000000000000";
+ };
+ "org/gnome/desktop/interface" = {
+ color-scheme = "prefer-dark";
+ };
+ "org/gnome/desktop/notifications" = {
+ show-in-lock-screen = false;
+ };
+ "org/gnome/desktop/screensaver" = {
+ lock-enabled = false;
+ picture-uri = "file://${pkgs.ghostty}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png";
+ picture-options = "centered";
+ primary-color = "#000000000000";
+ secondary-color = "#000000000000";
+ };
+ "org/gnome/desktop/session" = {
+ idle-delay = mkUint32 0;
+ };
+ "org/gnome/shell" = {
+ disable-user-extensions = false;
+ enabled-extensions = builtins.map (x: x.extensionUuid) (
+ lib.filter (p: p ? extensionUuid) config.environment.systemPackages
+ );
+ };
+ };
+ }
+ ];
+ };
+
+ programs.geary.enable = false;
+
+ services.gnome = {
+ gnome-browser-connector.enable = false;
+ gnome-initial-setup.enable = false;
+ gnome-online-accounts.enable = false;
+ gnome-remote-desktop.enable = false;
+ rygel.enable = false;
+ };
+
+ system.activationScripts = {
+ face = {
+ text = ''
+ mkdir -p /var/lib/AccountsService/{icons,users}
+
+ cp ${pkgs.ghostty}/share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png /var/lib/AccountsService/icons/ghostty
+
+ echo -e "[User]\nIcon=/var/lib/AccountsService/icons/ghostty\n" > /var/lib/AccountsService/users/ghostty
+
+ chown root:root /var/lib/AccountsService/users/ghostty
+ chmod 0600 /var/lib/AccountsService/users/ghostty
+
+ chown root:root /var/lib/AccountsService/icons/ghostty
+ chmod 0444 /var/lib/AccountsService/icons/ghostty
+ '';
+ };
+ };
+}
diff --git a/nix/vm/common-plasma6.nix b/nix/vm/common-plasma6.nix
new file mode 100644
index 0000000000..e5c9bd4d87
--- /dev/null
+++ b/nix/vm/common-plasma6.nix
@@ -0,0 +1,21 @@
+{...}: {
+ imports = [
+ ./common.nix
+ ];
+
+ services = {
+ displayManager = {
+ sddm = {
+ enable = true;
+ wayland = {
+ enable = true;
+ };
+ };
+ };
+ desktopManager = {
+ plasma6 = {
+ enable = true;
+ };
+ };
+ };
+}
diff --git a/nix/vm/common-xfce.nix b/nix/vm/common-xfce.nix
new file mode 100644
index 0000000000..12a20d8d86
--- /dev/null
+++ b/nix/vm/common-xfce.nix
@@ -0,0 +1,18 @@
+{...}: {
+ imports = [
+ ./common.nix
+ ];
+
+ services.xserver = {
+ displayManager = {
+ lightdm = {
+ enable = true;
+ };
+ };
+ desktopManager = {
+ xfce = {
+ enable = true;
+ };
+ };
+ };
+}
diff --git a/nix/vm/common.nix b/nix/vm/common.nix
new file mode 100644
index 0000000000..eefd7c1c03
--- /dev/null
+++ b/nix/vm/common.nix
@@ -0,0 +1,83 @@
+{pkgs, ...}: {
+ boot.loader.systemd-boot.enable = true;
+ boot.loader.efi.canTouchEfiVariables = true;
+
+ documentation.nixos.enable = false;
+
+ networking.hostName = "ghostty";
+ networking.domain = "mitchellh.com";
+
+ virtualisation.vmVariant = {
+ virtualisation.memorySize = 2048;
+ };
+
+ nix = {
+ settings = {
+ trusted-users = [
+ "root"
+ "ghostty"
+ ];
+ };
+ extraOptions = ''
+ experimental-features = nix-command flakes
+ '';
+ };
+
+ users.mutableUsers = false;
+
+ users.groups.ghostty = {};
+
+ users.users.ghostty = {
+ description = "Ghostty";
+ group = "ghostty";
+ extraGroups = ["wheel"];
+ isNormalUser = true;
+ initialPassword = "ghostty";
+ };
+
+ environment.etc = {
+ "xdg/autostart/com.mitchellh.ghostty.desktop" = {
+ source = "${pkgs.ghostty}/share/applications/com.mitchellh.ghostty.desktop";
+ };
+ };
+
+ environment.systemPackages = [
+ pkgs.kitty
+ pkgs.fish
+ pkgs.ghostty
+ pkgs.helix
+ pkgs.neovim
+ pkgs.xterm
+ pkgs.zsh
+ ];
+
+ security.polkit = {
+ enable = true;
+ };
+
+ services.dbus = {
+ enable = true;
+ };
+
+ services.displayManager = {
+ autoLogin = {
+ user = "ghostty";
+ };
+ };
+
+ services.libinput = {
+ enable = true;
+ };
+
+ services.qemuGuest = {
+ enable = true;
+ };
+
+ services.spice-vdagentd = {
+ enable = true;
+ };
+
+ services.xserver = {
+ enable = true;
+ };
+}
diff --git a/nix/vm/create-cinnamon.nix b/nix/vm/create-cinnamon.nix
new file mode 100644
index 0000000000..a9d9e44d77
--- /dev/null
+++ b/nix/vm/create-cinnamon.nix
@@ -0,0 +1,12 @@
+{
+ system,
+ nixpkgs,
+ overlay,
+ module,
+ uid ? 1000,
+ gid ? 1000,
+}:
+import ./create.nix {
+ inherit system nixpkgs overlay module uid gid;
+ common = ./common-cinnamon.nix;
+}
diff --git a/nix/vm/create-gnome.nix b/nix/vm/create-gnome.nix
new file mode 100644
index 0000000000..bcd31f2b63
--- /dev/null
+++ b/nix/vm/create-gnome.nix
@@ -0,0 +1,12 @@
+{
+ system,
+ nixpkgs,
+ overlay,
+ module,
+ uid ? 1000,
+ gid ? 1000,
+}:
+import ./create.nix {
+ inherit system nixpkgs overlay module uid gid;
+ common = ./common-gnome.nix;
+}
diff --git a/nix/vm/create-plasma6.nix b/nix/vm/create-plasma6.nix
new file mode 100644
index 0000000000..ede5371f34
--- /dev/null
+++ b/nix/vm/create-plasma6.nix
@@ -0,0 +1,12 @@
+{
+ system,
+ nixpkgs,
+ overlay,
+ module,
+ uid ? 1000,
+ gid ? 1000,
+}:
+import ./create.nix {
+ inherit system nixpkgs overlay module uid gid;
+ common = ./common-plasma6.nix;
+}
diff --git a/nix/vm/create-xfce.nix b/nix/vm/create-xfce.nix
new file mode 100644
index 0000000000..d1789472d4
--- /dev/null
+++ b/nix/vm/create-xfce.nix
@@ -0,0 +1,12 @@
+{
+ system,
+ nixpkgs,
+ overlay,
+ module,
+ uid ? 1000,
+ gid ? 1000,
+}:
+import ./create.nix {
+ inherit system nixpkgs overlay module uid gid;
+ common = ./common-xfce.nix;
+}
diff --git a/nix/vm/create.nix b/nix/vm/create.nix
new file mode 100644
index 0000000000..f8fe8500da
--- /dev/null
+++ b/nix/vm/create.nix
@@ -0,0 +1,42 @@
+{
+ system,
+ nixpkgs,
+ overlay,
+ module,
+ common ? ./common.nix,
+ uid ? 1000,
+ gid ? 1000,
+}: let
+ pkgs = import nixpkgs {
+ inherit system;
+ overlays = [
+ overlay
+ ];
+ };
+in
+ nixpkgs.lib.nixosSystem {
+ system = builtins.replaceStrings ["darwin"] ["linux"] system;
+ modules = [
+ {
+ virtualisation.vmVariant = {
+ virtualisation.host.pkgs = pkgs;
+ };
+
+ nixpkgs.overlays = [
+ overlay
+ ];
+
+ users.groups.ghostty = {
+ gid = gid;
+ };
+
+ users.users.ghostty = {
+ uid = uid;
+ };
+
+ system.stateVersion = nixpkgs.lib.trivial.release;
+ }
+ common
+ module
+ ];
+ }
diff --git a/nix/vm/wayland-cinnamon.nix b/nix/vm/wayland-cinnamon.nix
new file mode 100644
index 0000000000..531c882b64
--- /dev/null
+++ b/nix/vm/wayland-cinnamon.nix
@@ -0,0 +1,7 @@
+{...}: {
+ imports = [
+ ./common-cinnamon.nix
+ ];
+
+ services.displayManager.defaultSession = "cinnamon-wayland";
+}
diff --git a/nix/vm/wayland-gnome.nix b/nix/vm/wayland-gnome.nix
new file mode 100644
index 0000000000..eb277d5d1b
--- /dev/null
+++ b/nix/vm/wayland-gnome.nix
@@ -0,0 +1,9 @@
+{...}: {
+ imports = [
+ ./common-gnome.nix
+ ];
+
+ services.displayManager = {
+ defaultSession = "gnome";
+ };
+}
diff --git a/nix/vm/wayland-plasma6.nix b/nix/vm/wayland-plasma6.nix
new file mode 100644
index 0000000000..6e5a253b89
--- /dev/null
+++ b/nix/vm/wayland-plasma6.nix
@@ -0,0 +1,6 @@
+{...}: {
+ imports = [
+ ./common-plasma6.nix
+ ];
+ services.displayManager.defaultSession = "plasma";
+}
diff --git a/nix/vm/x11-cinnamon.nix b/nix/vm/x11-cinnamon.nix
new file mode 100644
index 0000000000..636f235a2c
--- /dev/null
+++ b/nix/vm/x11-cinnamon.nix
@@ -0,0 +1,7 @@
+{...}: {
+ imports = [
+ ./common-cinnamon.nix
+ ];
+
+ services.displayManager.defaultSession = "cinnamon";
+}
diff --git a/nix/vm/x11-gnome.nix b/nix/vm/x11-gnome.nix
new file mode 100644
index 0000000000..1994aea82f
--- /dev/null
+++ b/nix/vm/x11-gnome.nix
@@ -0,0 +1,9 @@
+{...}: {
+ imports = [
+ ./common-gnome.nix
+ ];
+
+ services.displayManager = {
+ defaultSession = "gnome-xorg";
+ };
+}
diff --git a/nix/vm/x11-plasma6.nix b/nix/vm/x11-plasma6.nix
new file mode 100644
index 0000000000..7818a80ca1
--- /dev/null
+++ b/nix/vm/x11-plasma6.nix
@@ -0,0 +1,6 @@
+{...}: {
+ imports = [
+ ./common-plasma6.nix
+ ];
+ services.displayManager.defaultSession = "plasmax11";
+}
diff --git a/nix/vm/x11-xfce.nix b/nix/vm/x11-xfce.nix
new file mode 100644
index 0000000000..71eb87f2fb
--- /dev/null
+++ b/nix/vm/x11-xfce.nix
@@ -0,0 +1,7 @@
+{...}: {
+ imports = [
+ ./common-xfce.nix
+ ];
+
+ services.displayManager.defaultSession = "xfce";
+}
diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix
index 3806c64c9d..12f855e5f1 100644
--- a/nix/zigCacheHash.nix
+++ b/nix/zigCacheHash.nix
@@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details.
-"sha256-PnfSy793kcVt85q47kWR0xkivXoMOZAAmuUyKO9vqAI="
+"sha256-S8kS+gO17dl9LJGKL1+kgDUre+vPTmdTvXzgc585Fl8="
diff --git a/pkg/cimgui/build.zig.zon b/pkg/cimgui/build.zig.zon
index 2f2c9cfa02..d5d90b1327 100644
--- a/pkg/cimgui/build.zig.zon
+++ b/pkg/cimgui/build.zig.zon
@@ -6,7 +6,8 @@
// This should be kept in sync with the submodule in the cimgui source
// code in ./vendor/ to be safe that they're compatible.
.imgui = .{
- .url = "https://github.com/ocornut/imgui/archive/e391fe2e66eb1c96b1624ae8444dc64c23146ef4.tar.gz",
+ // ocornut/imgui
+ .url = "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
.hash = "1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402",
},
diff --git a/pkg/freetype/build.zig.zon b/pkg/freetype/build.zig.zon
index 5c6538fd57..581666dd9d 100644
--- a/pkg/freetype/build.zig.zon
+++ b/pkg/freetype/build.zig.zon
@@ -3,8 +3,9 @@
.version = "2.13.2",
.paths = .{""},
.dependencies = .{
+ // freetype/freetype
.freetype = .{
- .url = "https://github.com/freetype/freetype/archive/refs/tags/VER-2-13-2.tar.gz",
+ .url = "https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz",
.hash = "1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d",
},
diff --git a/pkg/glslang/build.zig.zon b/pkg/glslang/build.zig.zon
index f80552d4fd..091276502c 100644
--- a/pkg/glslang/build.zig.zon
+++ b/pkg/glslang/build.zig.zon
@@ -3,8 +3,9 @@
.version = "14.2.0",
.paths = .{""},
.dependencies = .{
+ // KhronosGroup/glslang
.glslang = .{
- .url = "https://github.com/KhronosGroup/glslang/archive/refs/tags/14.2.0.tar.gz",
+ .url = "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
.hash = "12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1",
},
diff --git a/pkg/harfbuzz/build.zig.zon b/pkg/harfbuzz/build.zig.zon
index dc036f09d4..41f2b4e4f8 100644
--- a/pkg/harfbuzz/build.zig.zon
+++ b/pkg/harfbuzz/build.zig.zon
@@ -3,8 +3,9 @@
.version = "8.4.0",
.paths = .{""},
.dependencies = .{
+ // harfbuzz/harfbuzz
.harfbuzz = .{
- .url = "https://github.com/harfbuzz/harfbuzz/archive/refs/tags/8.4.0.tar.gz",
+ .url = "https://deps.files.ghostty.org/harfbuzz-1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122.tar.gz",
.hash = "1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122",
},
diff --git a/pkg/highway/build.zig.zon b/pkg/highway/build.zig.zon
index c8bc405f84..da631f93df 100644
--- a/pkg/highway/build.zig.zon
+++ b/pkg/highway/build.zig.zon
@@ -3,8 +3,9 @@
.version = "1.1.0",
.paths = .{""},
.dependencies = .{
+ // google/highway
.highway = .{
- .url = "https://github.com/google/highway/archive/refs/tags/1.1.0.tar.gz",
+ .url = "https://deps.files.ghostty.org/highway-12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b.tar.gz",
.hash = "12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b",
},
diff --git a/pkg/libpng/build.zig.zon b/pkg/libpng/build.zig.zon
index dc9bd3712b..be37d8c09d 100644
--- a/pkg/libpng/build.zig.zon
+++ b/pkg/libpng/build.zig.zon
@@ -3,8 +3,9 @@
.version = "1.6.43",
.paths = .{""},
.dependencies = .{
+ // glennrp/libpng
.libpng = .{
- .url = "https://github.com/glennrp/libpng/archive/refs/tags/v1.6.43.tar.gz",
+ .url = "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
.hash = "1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66",
},
diff --git a/pkg/macos/graphics/color_space.zig b/pkg/macos/graphics/color_space.zig
index 459f063029..16960591b2 100644
--- a/pkg/macos/graphics/color_space.zig
+++ b/pkg/macos/graphics/color_space.zig
@@ -18,9 +18,72 @@ pub const ColorSpace = opaque {
) orelse Allocator.Error.OutOfMemory;
}
+ pub fn createNamed(name: Name) Allocator.Error!*ColorSpace {
+ return @as(
+ ?*ColorSpace,
+ @ptrFromInt(@intFromPtr(c.CGColorSpaceCreateWithName(name.cfstring()))),
+ ) orelse Allocator.Error.OutOfMemory;
+ }
+
pub fn release(self: *ColorSpace) void {
c.CGColorSpaceRelease(@ptrCast(self));
}
+
+ pub const Name = enum {
+ /// This color space uses the DCI P3 primaries, a D65 white point, and
+ /// the sRGB transfer function.
+ displayP3,
+ /// The Display P3 color space with a linear transfer function and
+ /// extended-range values.
+ extendedLinearDisplayP3,
+ /// The sRGB colorimetry and non-linear transfer function are specified
+ /// in IEC 61966-2-1.
+ sRGB,
+ /// This color space has the same colorimetry as `sRGB`, but uses a
+ /// linear transfer function.
+ linearSRGB,
+ /// This color space has the same colorimetry as `sRGB`, but you can
+ /// encode component values below `0.0` and above `1.0`. Negative values
+ /// are encoded as the signed reflection of the original encoding
+ /// function, as shown in the formula below:
+ /// ```
+ /// extendedTransferFunction(x) = sign(x) * sRGBTransferFunction(abs(x))
+ /// ```
+ extendedSRGB,
+ /// This color space has the same colorimetry as `sRGB`; in addition,
+ /// you may encode component values below `0.0` and above `1.0`.
+ extendedLinearSRGB,
+ /// ...
+ genericGrayGamma2_2,
+ /// ...
+ linearGray,
+ /// This color space has the same colorimetry as `genericGrayGamma2_2`,
+ /// but you can encode component values below `0.0` and above `1.0`.
+ /// Negative values are encoded as the signed reflection of the
+ /// original encoding function, as shown in the formula below:
+ /// ```
+ /// extendedGrayTransferFunction(x) = sign(x) * gamma22Function(abs(x))
+ /// ```
+ extendedGray,
+ /// This color space has the same colorimetry as `linearGray`; in
+ /// addition, you may encode component values below `0.0` and above `1.0`.
+ extendedLinearGray,
+
+ fn cfstring(self: Name) c.CFStringRef {
+ return switch (self) {
+ .displayP3 => c.kCGColorSpaceDisplayP3,
+ .extendedLinearDisplayP3 => c.kCGColorSpaceExtendedLinearDisplayP3,
+ .sRGB => c.kCGColorSpaceSRGB,
+ .extendedSRGB => c.kCGColorSpaceExtendedSRGB,
+ .linearSRGB => c.kCGColorSpaceLinearSRGB,
+ .extendedLinearSRGB => c.kCGColorSpaceExtendedLinearSRGB,
+ .genericGrayGamma2_2 => c.kCGColorSpaceGenericGrayGamma2_2,
+ .extendedGray => c.kCGColorSpaceExtendedGray,
+ .linearGray => c.kCGColorSpaceLinearGray,
+ .extendedLinearGray => c.kCGColorSpaceExtendedLinearGray,
+ };
+ }
+ };
};
test {
diff --git a/pkg/oniguruma/build.zig.zon b/pkg/oniguruma/build.zig.zon
index 2120f77ae3..b1ba96dc8c 100644
--- a/pkg/oniguruma/build.zig.zon
+++ b/pkg/oniguruma/build.zig.zon
@@ -3,8 +3,9 @@
.version = "6.9.9",
.paths = .{""},
.dependencies = .{
+ // kkos/oniguruma
.oniguruma = .{
- .url = "https://github.com/kkos/oniguruma/archive/refs/tags/v6.9.9.tar.gz",
+ .url = "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz",
.hash = "1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb",
},
diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig
index 4cd1cf9f9b..a9fa5d4fe3 100644
--- a/pkg/opengl/Texture.zig
+++ b/pkg/opengl/Texture.zig
@@ -162,4 +162,26 @@ pub const Binding = struct {
data,
);
}
+
+ pub fn copySubImage2D(
+ b: Binding,
+ level: c.GLint,
+ xoffset: c.GLint,
+ yoffset: c.GLint,
+ x: c.GLint,
+ y: c.GLint,
+ width: c.GLsizei,
+ height: c.GLsizei,
+ ) !void {
+ glad.context.CopyTexSubImage2D.?(
+ @intFromEnum(b.target),
+ level,
+ xoffset,
+ yoffset,
+ x,
+ y,
+ width,
+ height
+ );
+ }
};
diff --git a/pkg/sentry/build.zig.zon b/pkg/sentry/build.zig.zon
index 385cd69d8c..8d1162cb42 100644
--- a/pkg/sentry/build.zig.zon
+++ b/pkg/sentry/build.zig.zon
@@ -3,8 +3,9 @@
.version = "0.7.8",
.paths = .{""},
.dependencies = .{
+ // getsentry/sentry-native
.sentry = .{
- .url = "https://github.com/getsentry/sentry-native/archive/refs/tags/0.7.8.tar.gz",
+ .url = "https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz",
.hash = "1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e",
},
diff --git a/pkg/spirv-cross/build.zig.zon b/pkg/spirv-cross/build.zig.zon
index 39fcac5294..dedf5e964a 100644
--- a/pkg/spirv-cross/build.zig.zon
+++ b/pkg/spirv-cross/build.zig.zon
@@ -3,8 +3,9 @@
.version = "13.1.1",
.paths = .{""},
.dependencies = .{
+ // KhronosGroup/SPIRV-Cross
.spirv_cross = .{
- .url = "https://github.com/KhronosGroup/SPIRV-Cross/archive/476f384eb7d9e48613c45179e502a15ab95b6b49.tar.gz",
+ .url = "https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz",
.hash = "1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da",
},
diff --git a/pkg/utfcpp/build.zig.zon b/pkg/utfcpp/build.zig.zon
index e56d77f193..795a027496 100644
--- a/pkg/utfcpp/build.zig.zon
+++ b/pkg/utfcpp/build.zig.zon
@@ -3,8 +3,9 @@
.version = "4.0.5",
.paths = .{""},
.dependencies = .{
+ // nemtrif/utfcpp
.utfcpp = .{
- .url = "https://github.com/nemtrif/utfcpp/archive/refs/tags/v4.0.5.tar.gz",
+ .url = "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz",
.hash = "1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641",
},
diff --git a/pkg/wuffs/build.zig.zon b/pkg/wuffs/build.zig.zon
index d84d6957e2..caa28f1c6e 100644
--- a/pkg/wuffs/build.zig.zon
+++ b/pkg/wuffs/build.zig.zon
@@ -2,13 +2,15 @@
.name = "wuffs",
.version = "0.0.0",
.dependencies = .{
+ // google/wuffs
.wuffs = .{
- .url = "https://github.com/google/wuffs/archive/refs/tags/v0.4.0-alpha.9.tar.gz",
+ .url = "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
.hash = "122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd",
},
+ // make-github-pseudonymous-again/pixels
.pixels = .{
- .url = "git+https://github.com/make-github-pseudonymous-again/pixels?ref=main#d843c2714d32e15b48b8d7eeb480295af537f877",
+ .url = "https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz",
.hash = "12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806",
},
diff --git a/pkg/wuffs/src/jpeg.zig b/pkg/wuffs/src/jpeg.zig
index 69628f582f..c07278eed3 100644
--- a/pkg/wuffs/src/jpeg.zig
+++ b/pkg/wuffs/src/jpeg.zig
@@ -55,7 +55,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
c.wuffs_base__pixel_config__set(
&image_config.pixcfg,
- c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
+ c.WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL,
c.WUFFS_BASE__PIXEL_SUBSAMPLING__NONE,
width,
height,
@@ -95,16 +95,6 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
try check(log, &status);
}
- var frame_config: c.wuffs_base__frame_config = undefined;
- {
- const status = c.wuffs_jpeg__decoder__decode_frame_config(
- decoder,
- &frame_config,
- &source_buffer,
- );
- try check(log, &status);
- }
-
{
const status = c.wuffs_jpeg__decoder__decode_frame(
decoder,
diff --git a/pkg/wuffs/src/png.zig b/pkg/wuffs/src/png.zig
index b85e4d7474..1f37bb375a 100644
--- a/pkg/wuffs/src/png.zig
+++ b/pkg/wuffs/src/png.zig
@@ -55,7 +55,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
c.wuffs_base__pixel_config__set(
&image_config.pixcfg,
- c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
+ c.WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL,
c.WUFFS_BASE__PIXEL_SUBSAMPLING__NONE,
width,
height,
@@ -95,16 +95,6 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
try check(log, &status);
}
- var frame_config: c.wuffs_base__frame_config = undefined;
- {
- const status = c.wuffs_png__decoder__decode_frame_config(
- decoder,
- &frame_config,
- &source_buffer,
- );
- try check(log, &status);
- }
-
{
const status = c.wuffs_png__decoder__decode_frame(
decoder,
diff --git a/pkg/zlib/build.zig.zon b/pkg/zlib/build.zig.zon
index 94aa184de7..43176352a0 100644
--- a/pkg/zlib/build.zig.zon
+++ b/pkg/zlib/build.zig.zon
@@ -3,8 +3,9 @@
.version = "1.3.1",
.paths = .{""},
.dependencies = .{
+ // madler/zlib
.zlib = .{
- .url = "https://github.com/madler/zlib/archive/refs/tags/v1.3.1.tar.gz",
+ .url = "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz",
.hash = "1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb",
},
diff --git a/snap/local/launcher b/snap/local/launcher
new file mode 100755
index 0000000000..11597f238e
--- /dev/null
+++ b/snap/local/launcher
@@ -0,0 +1,52 @@
+#!/bin/sh
+set -e
+
+# Set these to reasonable defaults if not already set
+if [ -z "$XDG_CONFIG_HOME" ]; then
+ export XDG_CONFIG_HOME="$SNAP_REAL_HOME/.config"
+fi
+
+if [ -z "$XDG_CACHE_HOME" ]; then
+ export XDG_CACHE_HOME="$SNAP_REAL_HOME/.cache"
+fi
+
+if [ -z "$XDG_DATA_HOME" ]; then
+ export XDG_DATA_HOME="$SNAP_REAL_HOME/.local/share"
+fi
+
+export HOME="$SNAP_REAL_HOME"
+
+if [ "$SNAP_ARCH" = "amd64" ]; then
+ ARCH="x86_64-linux-gnu"
+elif [ "$SNAP_ARCH" = "arm64" ]; then
+ ARCH="aarch64-linux-gnu"
+else
+ ARCH="$SNAP_ARCH-linux-gnu"
+fi
+
+export LD_LIBRARY_PATH=${SNAP}/usr/lib/${ARCH}:${SNAP}/usr/lib/${ARCH}/vdpau:${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}
+export LIBGL_DRIVERS_PATH=${LIBGL_DRIVERS_PATH:+$LIBGL_DRIVERS_PATH:}${SNAP}/usr/lib/${ARCH}/dri/
+export LIBVA_DRIVERS_PATH=${LIBVA_DRIVERS_PATH:+$LIBVA_DRIVERS_PATH:}${SNAP}/usr/lib/${ARCH}/dri/
+export __EGL_VENDOR_LIBRARY_DIRS=${__EGL_VENDOR_LIBRARY_DIRS:+$__EGL_VENDOR_LIBRARY_DIRS:}${SNAP}/usr/share/glvnd/egl_vendor.d
+export __EGL_EXTERNAL_PLATFORM_CONFIG_DIRS=${__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS:+$__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS:}${SNAP}/usr/share/egl/egl_external_platform.d
+export DRIRC_CONFIGDIR=${SNAP}/usr/share/drirc.d
+export VK_LAYER_PATH=${VK_LAYER_PATH:+$VK_LAYER_PATH:}${SNAP}/usr/share/vulkan/implicit_layer.d/:${SNAP}/usr/share/vulkan/explicit_layer.d/
+export XDG_DATA_DIRS=${XDG_DATA_DIRS:+$XDG_DATA_DIRS:}${SNAP}/usr/share
+export XLOCALEDIR="${SNAP}/usr/share/X11/locale"
+export GDK_PIXBUF_MODULE_FILE="$XDG_CACHE_HOME/gdk-pixbuf-loaders.cache"
+export GDK_PIXBUF_MODULEDIR="$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/2.10.0/loaders"
+export GTK_PATH="$SNAP/usr/lib/$ARCH/gtk-4.0"
+
+if [ "${__NV_PRIME_RENDER_OFFLOAD:-}" != 1 ]; then
+ # Prevent picking VA-API (Intel/AMD) over NVIDIA VDPAU
+ # https://download.nvidia.com/XFree86/Linux-x86_64/510.54/README/primerenderoffload.html#configureapplications
+ export LIBVA_DRIVERS_PATH
+fi
+
+# Unset all SNAP specific environment variables to keep them from leaking
+# into other snaps that might get executed from within the shell
+for var in $(printenv | grep SNAP_ | cut -d= -f1); do
+ unset $var
+done
+
+exec "$@"
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
new file mode 100644
index 0000000000..9ef2f5cc41
--- /dev/null
+++ b/snap/snapcraft.yaml
@@ -0,0 +1,139 @@
+name: ghostty
+base: core24
+summary: A terminal emulator
+description: |
+ Ghostty is a fast, feature-rich, and cross-platform terminal emulator that
+ uses platform-native UI and GPU acceleration.
+grade: stable
+confinement: classic
+contact: https://github.com/ghostty-org/ghostty/discussions
+issues: https://github.com/ghostty-org/ghostty/issues
+website: https://ghostty.org
+license: MIT
+icon: images/icons/icon_512.png
+adopt-info: ghostty
+
+platforms:
+ amd64:
+ arm64:
+
+apps:
+ ghostty:
+ command: bin/ghostty
+ command-chain: [bin/launcher]
+ completer: share/bash-completion/completions/ghostty.bash
+ desktop: share/applications/com.mitchellh.ghostty.desktop
+ #refresh-mode: ignore-running # Store rejects this, needs fix in review-tools
+ environment:
+ PATH: /snap/ghostty/current/bin:/snap/ghostty/current/usr/bin:$PATH
+ LC_ALL: C.UTF-8
+ GHOSTTY_RESOURCES_DIR: /snap/ghostty/current/share/ghostty
+
+parts:
+ launcher:
+ plugin: dump
+ source: snap/local
+ source-type: local
+ organize:
+ launcher: bin/
+
+ zig:
+ plugin: nil
+ build-packages:
+ - curl
+ override-pull: |
+ set -ex
+ case "$CRAFT_ARCH_BUILD_FOR" in
+ amd64) arch=x86_64 ;;
+ arm64) arch=aarch64 ;;
+ *) arch="" ;;
+ esac
+
+ rm -rf $CRAFT_PART_SRC/*
+
+ if [[ -n $arch ]]; then
+ curl -LO --retry-connrefused --retry 10 https://ziglang.org/download/0.13.0/zig-linux-$arch-0.13.0.tar.xz
+ else
+ echo "Unsupported arch"
+ exit 1
+ fi
+
+ tar xf zig-lin*xz
+ rm -f *xz
+ mv zig-linux*/* .
+ prime:
+ - -*
+
+ ghostty:
+ source: .
+ after: [zig]
+ plugin: nil
+ build-attributes: [enable-patchelf]
+ build-packages:
+ - libgtk-4-dev
+ - libadwaita-1-dev
+ - git
+ - patchelf
+ override-build: |
+ craftctl set version=$(git describe --abbrev=8)
+ $CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast
+ cp -rp zig-out/* $CRAFT_PART_INSTALL/
+ sed -i 's|Icon=com.mitchellh.ghostty|Icon=/snap/ghostty/current/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png|g' $CRAFT_PART_INSTALL/share/applications/com.mitchellh.ghostty.desktop
+
+ libs:
+ plugin: nil
+ build-attributes: [enable-patchelf]
+ stage-packages:
+ - libadwaita-1-0
+ - libglib2.0-0t64
+ - libgtk-4-1
+ - libgtk-4-media-gstreamer
+ - ibus-gtk4
+ - libpciaccess0
+ - libtinfo6
+ - libedit2
+ - libelf1t64
+ - libsensors5
+ - libllvm17
+ - libunistring5
+ - librsvg2-2
+ - on amd64:
+ [
+ i965-va-driver,
+ libdrm-intel1,
+ libdrm-nouveau2,
+ libdrm-amdgpu1,
+ libdrm-radeon1,
+ ]
+ stage:
+ # The libraries in dri need no-patchelf, so they come from the mesa-unpatched part
+ - -usr/lib/*/dri
+
+ mesa:
+ plugin: nil
+ build-attributes: [enable-patchelf]
+ stage-packages:
+ - libglu1-mesa
+ - libgl1-mesa-dri
+ - libegl-mesa0
+ - libegl1
+ - libglx-mesa0
+ - mesa-libgallium
+ stage:
+ # The libraries in dri need no-patchelf, so they come from the mesa-unpatched part
+ - usr/lib/*/*.so*
+ - usr/lib/*/dri/libdril_dri.so
+ - -usr/lib/*/libgallium*so
+ - -usr/lib/*/dri
+
+ mesa-gl1-dri:
+ plugin: nil
+ stage-packages:
+ - libgl1-mesa-dri
+ build-attributes: [no-patchelf]
+ stage:
+ # Only the libraries in dri need to not be patched, the rest come from the mesa part
+ # Otherwise snapcraft may strip the build ID and cause the driver to crash
+ - usr/lib/*/libgallium*so
+ - -usr/lib/*/dri/libdril_dri.so
+ - usr/lib/*/dri
diff --git a/src/App.zig b/src/App.zig
index a6b54db232..15859d1155 100644
--- a/src/App.zig
+++ b/src/App.zig
@@ -161,7 +161,7 @@ pub fn updateConfig(self: *App, rt_app: *apprt.App, config: *const Config) !void
const applied: *const configpkg.Config = if (applied_) |*c| c else config;
// Notify the apprt that the app has changed configuration.
- try rt_app.performAction(
+ _ = try rt_app.performAction(
.app,
.config_change,
.{ .config = applied },
@@ -180,7 +180,7 @@ pub fn addSurface(
// Since we have non-zero surfaces, we can cancel the quit timer.
// It is up to the apprt if there is a quit timer at all and if it
// should be canceled.
- rt_surface.app.performAction(
+ _ = rt_surface.app.performAction(
.app,
.quit_timer,
.stop,
@@ -214,7 +214,7 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void {
// If we have no surfaces, we can start the quit timer. It is up to the
// apprt to determine if this is necessary.
- if (self.surfaces.items.len == 0) rt_surface.app.performAction(
+ if (self.surfaces.items.len == 0) _ = rt_surface.app.performAction(
.app,
.quit_timer,
.start,
@@ -294,7 +294,7 @@ pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
break :target .app;
};
- try rt_app.performAction(
+ _ = try rt_app.performAction(
target,
.new_window,
{},
@@ -419,7 +419,7 @@ pub fn colorSchemeEvent(
// Request our configuration be reloaded because the new scheme may
// impact the colors of the app.
- try rt_app.performAction(
+ _ = try rt_app.performAction(
.app,
.reload_config,
.{ .soft = true },
@@ -437,13 +437,13 @@ pub fn performAction(
switch (action) {
.unbind => unreachable,
.ignore => {},
- .quit => try rt_app.performAction(.app, .quit, {}),
- .new_window => try self.newWindow(rt_app, .{ .parent = null }),
- .open_config => try rt_app.performAction(.app, .open_config, {}),
- .reload_config => try rt_app.performAction(.app, .reload_config, .{}),
- .close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}),
- .toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}),
- .toggle_visibility => try rt_app.performAction(.app, .toggle_visibility, {}),
+ .quit => _ = try rt_app.performAction(.app, .quit, {}),
+ .new_window => _ = try self.newWindow(rt_app, .{ .parent = null }),
+ .open_config => _ = try rt_app.performAction(.app, .open_config, {}),
+ .reload_config => _ = try rt_app.performAction(.app, .reload_config, .{}),
+ .close_all_windows => _ = try rt_app.performAction(.app, .close_all_windows, {}),
+ .toggle_quick_terminal => _ = try rt_app.performAction(.app, .toggle_quick_terminal, {}),
+ .toggle_visibility => _ = try rt_app.performAction(.app, .toggle_visibility, {}),
}
}
diff --git a/src/Surface.zig b/src/Surface.zig
index 06701b5362..14ddde294c 100644
--- a/src/Surface.zig
+++ b/src/Surface.zig
@@ -519,9 +519,19 @@ pub fn init(
// This separate block ({}) is important because our errdefers must
// be scoped here to be valid.
{
+ var env = rt_surface.defaultTermioEnv() catch |err| env: {
+ // If an error occurs, we don't want to block surface startup.
+ log.warn("error getting env map for surface err={}", .{err});
+ break :env internal_os.getEnvMap(alloc) catch
+ std.process.EnvMap.init(alloc);
+ };
+ errdefer env.deinit();
+
// Initialize our IO backend
var io_exec = try termio.Exec.init(alloc, .{
.command = command,
+ .env = env,
+ .env_override = config.env,
.shell_integration = config.@"shell-integration",
.shell_integration_features = config.@"shell-integration-features",
.working_directory = config.@"working-directory",
@@ -561,7 +571,7 @@ pub fn init(
errdefer self.io.deinit();
// Report initial cell size on surface creation
- try rt_app.performAction(
+ _ = try rt_app.performAction(
.{ .surface = self },
.cell_size,
.{ .width = size.cell.width, .height = size.cell.height },
@@ -573,7 +583,7 @@ pub fn init(
const min_window_width_cells: u32 = 10;
const min_window_height_cells: u32 = 4;
- try rt_app.performAction(
+ _ = try rt_app.performAction(
.{ .surface = self },
.size_limit,
.{
@@ -637,7 +647,7 @@ pub fn init(
size.padding.top +
size.padding.bottom;
- rt_app.performAction(
+ _ = rt_app.performAction(
.{ .surface = self },
.initial_size,
.{ .width = final_width, .height = final_height },
@@ -649,7 +659,7 @@ pub fn init(
}
if (config.title) |title| {
- try rt_app.performAction(
+ _ = try rt_app.performAction(
.{ .surface = self },
.set_title,
.{ .title = title },
@@ -670,7 +680,7 @@ pub fn init(
break :xdg;
};
defer alloc.free(title);
- try rt_app.performAction(
+ _ = try rt_app.performAction(
.{ .surface = self },
.set_title,
.{ .title = title },
@@ -823,7 +833,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
// We know that our title should end in 0.
const slice = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(v)), 0);
log.debug("changing title \"{s}\"", .{slice});
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.set_title,
.{ .title = slice },
@@ -859,7 +869,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.color_change => |change| {
// Notify our apprt, but don't send a mode 2031 DSR report
// because VT sequences were used to change the color.
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.color_change,
.{
@@ -878,7 +888,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.set_mouse_shape => |shape| {
log.debug("changing mouse shape: {}", .{shape});
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
shape,
@@ -910,7 +920,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
const str = try self.alloc.dupeZ(u8, w.slice());
defer self.alloc.free(str);
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.pwd,
.{ .pwd = str },
@@ -961,7 +971,7 @@ fn passwordInput(self: *Surface, v: bool) !void {
}
// Notify our apprt so it can do whatever it wants.
- self.rt_app.performAction(
+ _ = self.rt_app.performAction(
.{ .surface = self },
.secure_input,
if (v) .on else .off,
@@ -1041,13 +1051,16 @@ fn mouseRefreshLinks(
pos_vp: terminal.point.Coordinate,
over_link: bool,
) !void {
+ // If the position is outside our viewport, do nothing
+ if (pos.x < 0 or pos.y < 0) return;
+
self.mouse.link_point = pos_vp;
if (try self.linkAtPos(pos)) |link| {
self.renderer_state.mouse.point = pos_vp;
self.mouse.over_link = true;
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
.pointer,
@@ -1060,7 +1073,7 @@ fn mouseRefreshLinks(
.trim = false,
});
defer self.alloc.free(str);
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
.{ .url = str },
@@ -1074,7 +1087,7 @@ fn mouseRefreshLinks(
log.warn("failed to get URI for OSC8 hyperlink", .{});
break :link;
};
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
.{ .url = uri },
@@ -1084,12 +1097,12 @@ fn mouseRefreshLinks(
try self.queueRender();
} else if (over_link) {
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
self.io.terminal.mouse_shape,
);
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
.{ .url = "" },
@@ -1101,7 +1114,7 @@ fn mouseRefreshLinks(
/// Called when our renderer health state changes.
fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
log.warn("renderer health status change status={}", .{health});
- self.rt_app.performAction(
+ _ = self.rt_app.performAction(
.{ .surface = self },
.renderer_health,
health,
@@ -1113,7 +1126,7 @@ fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
/// This should be called anytime `config_conditional_state` changes
/// so that the apprt can reload the configuration.
fn notifyConfigConditionalState(self: *Surface) void {
- self.rt_app.performAction(
+ _ = self.rt_app.performAction(
.{ .surface = self },
.reload_config,
.{ .soft = true },
@@ -1193,14 +1206,14 @@ pub fn updateConfig(
// If we have a title set then we update our window to have the
// newly configured title.
- if (config.title) |title| try self.rt_app.performAction(
+ if (config.title) |title| _ = try self.rt_app.performAction(
.{ .surface = self },
.set_title,
.{ .title = title },
);
// Notify the window
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.config_change,
.{ .config = config },
@@ -1316,8 +1329,8 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 };
const x: f64 = x: {
- // Simple x * cell width gives the top-left corner
- var x: f64 = @floatFromInt(cursor.x * self.size.cell.width);
+ // Simple x * cell width gives the top-left corner, then add padding offset
+ var x: f64 = @floatFromInt(cursor.x * self.size.cell.width + self.size.padding.left);
// We want the midpoint
x += @as(f64, @floatFromInt(self.size.cell.width)) / 2;
@@ -1329,8 +1342,8 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
};
const y: f64 = y: {
- // Simple x * cell width gives the top-left corner
- var y: f64 = @floatFromInt(cursor.y * self.size.cell.height);
+ // Simple y * cell height gives the top-left corner, then add padding offset
+ var y: f64 = @floatFromInt(cursor.y * self.size.cell.height + self.size.padding.top);
// We want the bottom
y += @floatFromInt(self.size.cell.height);
@@ -1467,7 +1480,7 @@ fn setCellSize(self: *Surface, size: renderer.CellSize) !void {
self.io.queueMessage(.{ .resize = self.size }, .unlocked);
// Notify the window
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.cell_size,
.{ .width = size.width, .height = size.height },
@@ -1591,6 +1604,15 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
+ // We clear our selection when ANY OF:
+ // 1. We have an existing preedit
+ // 2. We have preedit text
+ if (self.renderer_state.preedit != null or
+ preedit_ != null)
+ {
+ self.setSelection(null) catch {};
+ }
+
// We always clear our prior preedit
if (self.renderer_state.preedit) |p| {
self.alloc.free(p.codepoints);
@@ -1754,12 +1776,12 @@ pub fn keyCallback(
};
} else if (self.io.terminal.flags.mouse_event != .none and !self.mouse.mods.shift) {
// If we have mouse reports on and we don't have shift pressed, we reset state
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
self.io.terminal.mouse_shape,
);
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
.{ .url = "" },
@@ -1777,7 +1799,7 @@ pub fn keyCallback(
.mods = self.mouse.mods,
.over_link = self.mouse.over_link,
.hidden = self.mouse.hidden,
- }).keyToMouseShape()) |shape| try self.rt_app.performAction(
+ }).keyToMouseShape()) |shape| _ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
shape,
@@ -1902,7 +1924,7 @@ fn maybeHandleBinding(
}
// Start or continue our key sequence
- self.rt_app.performAction(
+ _ = self.rt_app.performAction(
.{ .surface = self },
.key_sequence,
.{ .trigger = entry.key_ptr.* },
@@ -2011,7 +2033,7 @@ fn endKeySequence(
mem: KeySequenceMemory,
) void {
// Notify apprt key sequence ended
- self.rt_app.performAction(
+ _ = self.rt_app.performAction(
.{ .surface = self },
.key_sequence,
.end,
@@ -3203,26 +3225,9 @@ fn linkAtPos(
}));
defer strmap.deinit(self.alloc);
- // Go through each link and see if we clicked it
- for (self.config.links) |link| {
- switch (link.highlight) {
- .always, .hover => {},
- .always_mods, .hover_mods => |v| if (!v.equal(mouse_mods)) continue,
- }
-
- var it = strmap.searchIterator(link.regex);
- while (true) {
- var match = (try it.next()) orelse break;
- defer match.deinit();
- const sel = match.selection();
- if (!sel.contains(screen, mouse_pin)) continue;
- return .{ .action = link.action, .selection = sel };
- }
- }
-
- // this is the last chance to return any substr as clickable or not
- // by checking if any given str is path resolvable or not. below lines of code
- // will work only for file paths. this covers both relative and absolute paths.
+ // Check if any substr as clickable or not.
+ // If any given str is path resolvable or not. Below lines of code
+ // will work only for file paths. This covers both relative and absolute paths.
// some of the paths may not work it is based on the `std.fs.realpath`
const cwd = self.io.terminal.getPwd();
var split_str = std.mem.splitSequence(u8, strmap.string, " ");
@@ -3261,6 +3266,23 @@ fn linkAtPos(
}
}
+ // Go through each link and see if we clicked it
+ for (self.config.links) |link| {
+ switch (link.highlight) {
+ .always, .hover => {},
+ .always_mods, .hover_mods => |v| if (!v.equal(mouse_mods)) continue,
+ }
+
+ var it = strmap.searchIterator(link.regex);
+ while (true) {
+ var match = (try it.next()) orelse break;
+ defer match.deinit();
+ const sel = match.selection();
+ if (!sel.contains(screen, mouse_pin)) continue;
+ return .{ .action = link.action, .selection = sel };
+ }
+ }
+
return null;
}
@@ -3398,12 +3420,12 @@ pub fn cursorPosCallback(
self.mouse.link_point = null;
if (self.mouse.over_link) {
self.mouse.over_link = false;
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
self.io.terminal.mouse_shape,
);
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
.{ .url = "" },
@@ -3605,22 +3627,21 @@ fn dragLeftClickTriple(
const screen = &self.io.terminal.screen;
const click_pin = self.mouse.left_click_pin.?.*;
- // Get the word under our current point. If there isn't a word, do nothing.
- const word = screen.selectLine(.{ .pin = drag_pin }) orelse return;
+ // Get the line selection under our current drag point. If there isn't a
+ // line, do nothing.
+ const line = screen.selectLine(.{ .pin = drag_pin }) orelse return;
- // Get our selection to grow it. If we don't have a selection, start it now.
- // We may not have a selection if we started our dbl-click in an area
- // that had no data, then we dragged our mouse into an area with data.
- var sel = screen.selectLine(.{ .pin = click_pin }) orelse {
- try self.setSelection(word);
- return;
- };
+ // Get the selection under our click point. We first try to trim
+ // whitespace if we've selected a word. But if no word exists then
+ // we select the blank line.
+ const sel_ = screen.selectLine(.{ .pin = click_pin }) orelse
+ screen.selectLine(.{ .pin = click_pin, .whitespace = null });
- // Grow our selection
+ var sel = sel_ orelse return;
if (drag_pin.before(click_pin)) {
- sel.startPtr().* = word.start();
+ sel.startPtr().* = line.start();
} else {
- sel.endPtr().* = word.end();
+ sel.endPtr().* = line.end();
}
try self.setSelection(sel);
}
@@ -3830,7 +3851,7 @@ fn scrollToBottom(self: *Surface) !void {
fn hideMouse(self: *Surface) void {
if (self.mouse.hidden) return;
self.mouse.hidden = true;
- self.rt_app.performAction(
+ _ = self.rt_app.performAction(
.{ .surface = self },
.mouse_visibility,
.hidden,
@@ -3842,7 +3863,7 @@ fn hideMouse(self: *Surface) void {
fn showMouse(self: *Surface) void {
if (!self.mouse.hidden) return;
self.mouse.hidden = false;
- self.rt_app.performAction(
+ _ = self.rt_app.performAction(
.{ .surface = self },
.mouse_visibility,
.visible,
@@ -3987,6 +4008,33 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
return false;
},
+ .copy_url_to_clipboard => {
+ // If the mouse isn't over a link, nothing we can do.
+ if (!self.mouse.over_link) return false;
+
+ const pos = try self.rt_surface.getCursorPos();
+ if (try self.linkAtPos(pos)) |link_info| {
+ // Get the URL text from selection
+ const url_text = (self.io.terminal.screen.selectionString(self.alloc, .{
+ .sel = link_info.selection,
+ .trim = self.config.clipboard_trim_trailing_spaces,
+ })) catch |err| {
+ log.err("error reading url string err={}", .{err});
+ return false;
+ };
+ defer self.alloc.free(url_text);
+
+ self.rt_surface.setClipboardString(url_text, .standard, false) catch |err| {
+ log.err("error copying url to clipboard err={}", .{err});
+ return true;
+ };
+
+ return true;
+ }
+
+ return false;
+ },
+
.paste_from_clipboard => try self.startClipboardRequest(
.standard,
.{ .paste = {} },
@@ -4028,6 +4076,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
try self.setFontSize(size);
},
+ .prompt_surface_title => return try self.rt_app.performAction(
+ .{ .surface = self },
+ .prompt_title,
+ {},
+ ),
+
.clear_screen => {
// This is a duplicate of some of the logic in termio.clearScreen
// but we need to do this here so we can know the answer before
@@ -4106,17 +4160,23 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
v,
),
- .new_tab => try self.rt_app.performAction(
+ .new_tab => return try self.rt_app.performAction(
.{ .surface = self },
.new_tab,
{},
),
+ .close_tab => return try self.rt_app.performAction(
+ .{ .surface = self },
+ .close_tab,
+ {},
+ ),
+
inline .previous_tab,
.next_tab,
.last_tab,
.goto_tab,
- => |v, tag| try self.rt_app.performAction(
+ => |v, tag| return try self.rt_app.performAction(
.{ .surface = self },
.goto_tab,
switch (tag) {
@@ -4128,13 +4188,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
),
- .move_tab => |position| try self.rt_app.performAction(
+ .move_tab => |position| return try self.rt_app.performAction(
.{ .surface = self },
.move_tab,
.{ .amount = position },
),
- .new_split => |direction| try self.rt_app.performAction(
+ .new_split => |direction| return try self.rt_app.performAction(
.{ .surface = self },
.new_split,
switch (direction) {
@@ -4149,7 +4209,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
),
- .goto_split => |direction| try self.rt_app.performAction(
+ .goto_split => |direction| return try self.rt_app.performAction(
.{ .surface = self },
.goto_split,
switch (direction) {
@@ -4160,7 +4220,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
),
- .resize_split => |value| try self.rt_app.performAction(
+ .resize_split => |value| return try self.rt_app.performAction(
.{ .surface = self },
.resize_split,
.{
@@ -4174,41 +4234,48 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
),
- .equalize_splits => try self.rt_app.performAction(
+ .equalize_splits => return try self.rt_app.performAction(
.{ .surface = self },
.equalize_splits,
{},
),
- .toggle_split_zoom => try self.rt_app.performAction(
+ .toggle_split_zoom => return try self.rt_app.performAction(
.{ .surface = self },
.toggle_split_zoom,
{},
),
- .toggle_fullscreen => try self.rt_app.performAction(
+ .toggle_maximize => return try self.rt_app.performAction(
+ .{ .surface = self },
+ .toggle_maximize,
+ {},
+ ),
+
+ .toggle_fullscreen => return try self.rt_app.performAction(
.{ .surface = self },
.toggle_fullscreen,
switch (self.config.macos_non_native_fullscreen) {
.false => .native,
.true => .macos_non_native,
.@"visible-menu" => .macos_non_native_visible_menu,
+ .@"padded-notch" => .macos_non_native_padded_notch,
},
),
- .toggle_window_decorations => try self.rt_app.performAction(
+ .toggle_window_decorations => return try self.rt_app.performAction(
.{ .surface = self },
.toggle_window_decorations,
{},
),
- .toggle_tab_overview => try self.rt_app.performAction(
+ .toggle_tab_overview => return try self.rt_app.performAction(
.{ .surface = self },
.toggle_tab_overview,
{},
),
- .toggle_secure_input => try self.rt_app.performAction(
+ .toggle_secure_input => return try self.rt_app.performAction(
.{ .surface = self },
.secure_input,
.toggle,
@@ -4222,7 +4289,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
}
},
- .inspector => |mode| try self.rt_app.performAction(
+ .inspector => |mode| return try self.rt_app.performAction(
.{ .surface = self },
.inspector,
switch (mode) {
@@ -4311,6 +4378,7 @@ fn closingAction(action: input.Binding.Action) bool {
return switch (action) {
.close_surface,
.close_window,
+ .close_tab,
=> true,
else => false,
@@ -4668,7 +4736,7 @@ fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const
self.app.last_notification_time = now;
self.app.last_notification_digest = new_digest;
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.desktop_notification,
.{
@@ -4688,7 +4756,7 @@ fn crashThreadState(self: *Surface) crash.sentry.ThreadState {
/// Tell the surface to present itself to the user. This may involve raising the
/// window and switching tabs.
fn presentSurface(self: *Surface) !void {
- try self.rt_app.performAction(
+ _ = try self.rt_app.performAction(
.{ .surface = self },
.present_terminal,
{},
diff --git a/src/apprt/action.zig b/src/apprt/action.zig
index df30f7b7be..20b86707e1 100644
--- a/src/apprt/action.zig
+++ b/src/apprt/action.zig
@@ -82,6 +82,9 @@ pub const Action = union(Key) {
/// the tab should be opened in a new window.
new_tab,
+ /// Closes the tab belonging to the currently focused split.
+ close_tab,
+
/// Create a new split. The value determines the location of the split
/// relative to the target.
new_split: SplitDirection,
@@ -89,6 +92,9 @@ pub const Action = union(Key) {
/// Close all open windows.
close_all_windows,
+ /// Toggle maximized window state.
+ toggle_maximize,
+
/// Toggle fullscreen mode.
toggle_fullscreen: Fullscreen,
@@ -152,9 +158,13 @@ pub const Action = union(Key) {
/// Show a desktop notification.
desktop_notification: DesktopNotification,
- /// Set the title of the target.
+ /// Set the title of the target to the requested value.
set_title: SetTitle,
+ /// Set the title of the target to a prompted value. It is up to
+ /// the apprt to prompt.
+ prompt_title,
+
/// The current working directory has changed for the target terminal.
pwd: Pwd,
@@ -225,8 +235,10 @@ pub const Action = union(Key) {
quit,
new_window,
new_tab,
+ close_tab,
new_split,
close_all_windows,
+ toggle_maximize,
toggle_fullscreen,
toggle_tab_overview,
toggle_window_decorations,
@@ -246,6 +258,7 @@ pub const Action = union(Key) {
render_inspector,
desktop_notification,
set_title,
+ prompt_title,
pwd,
mouse_shape,
mouse_visibility,
@@ -377,6 +390,7 @@ pub const Fullscreen = enum(c_int) {
/// window. This is much faster to enter and exit than the native mode.
macos_non_native,
macos_non_native_visible_menu,
+ macos_non_native_padded_notch,
};
pub const SecureInput = enum(c_int) {
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
index 50d1e90e4b..18674bc385 100644
--- a/src/apprt/embedded.zig
+++ b/src/apprt/embedded.zig
@@ -12,6 +12,7 @@ const objc = @import("objc");
const apprt = @import("../apprt.zig");
const font = @import("../font/main.zig");
const input = @import("../input.zig");
+const internal_os = @import("../os/main.zig");
const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
const CoreApp = @import("../App.zig");
@@ -45,7 +46,7 @@ pub const App = struct {
wakeup: *const fn (AppUD) callconv(.C) void,
/// Callback called to handle an action.
- action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.C) void,
+ action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.C) bool,
/// Read the clipboard value. The return value must be preserved
/// by the host until the next call. If there is no valid clipboard
@@ -181,14 +182,9 @@ pub const App = struct {
if (strip) translate_mods.alt = false;
}
- // On macOS we strip ctrl because UCKeyTranslate
- // converts to the masked values (i.e. ctrl+c becomes 3)
- // and we don't want that behavior.
- //
- // We also strip super because its not used for translation
- // on macos and it results in a bad translation.
+ // We strip super on macOS because its not used for translation
+ // it results in a bad translation.
if (comptime builtin.target.isDarwin()) {
- translate_mods.ctrl = false;
translate_mods.super = false;
}
@@ -199,6 +195,11 @@ pub const App = struct {
// This logic only applies to macOS.
if (comptime builtin.os.tag != .macos) break :event_text event.text;
+ // If we're in a preedit state then we allow it through. This
+ // allows ctrl sequences that affect IME to work. For example,
+ // Ctrl+H deletes a character with Japanese input.
+ if (event.composing) break :event_text event.text;
+
// If the modifiers are ONLY "control" then we never process
// the event text because we want to do our own translation so
// we can handle ctrl+c, ctrl+z, etc.
@@ -223,6 +224,7 @@ pub const App = struct {
const result: input.Keymap.Translation = if (event_text) |text| .{
.text = text,
.composing = event.composing,
+ .mods = translate_mods,
} else try self.keymap.translate(
&buf,
switch (target) {
@@ -233,6 +235,14 @@ pub const App = struct {
translate_mods,
);
+ // TODO(mitchellh): I think we can get rid of the above keymap
+ // translation code completely and defer to AppKit/Swift
+ // (for macOS) for handling all translations. The translation
+ // within libghostty is an artifact of an earlier design and
+ // it is buggy (see #5558). We should move closer to a GTK-style
+ // model of tracking composing states and preedit in the apprt
+ // and not in libghostty.
+
// If this is a dead key, then we're composing a character and
// we need to set our proper preedit state if we're targeting a
// surface.
@@ -259,16 +269,12 @@ pub const App = struct {
// then we clear the text. We handle non-printables in the
// key encoder manual (such as tab, ctrl+c, etc.)
if (result.text.len == 1 and result.text[0] < 0x20) {
- break :translate .{ .composing = false, .text = "" };
+ break :translate .{};
}
}
break :translate result;
- } else .{ .composing = false, .text = "" };
-
- // UCKeyTranslate always consumes all mods, so if we have any output
- // then we've consumed our translate mods.
- const consumed_mods: input.Mods = if (result.text.len > 0) translate_mods else .{};
+ } else .{};
// We need to always do a translation with no modifiers at all in
// order to get the "unshifted_codepoint" for the key event.
@@ -340,7 +346,7 @@ pub const App = struct {
.key = key,
.physical_key = physical_key,
.mods = mods,
- .consumed_mods = consumed_mods,
+ .consumed_mods = result.mods,
.composing = result.composing,
.utf8 = result.text,
.unshifted_codepoint = unshifted_codepoint,
@@ -464,13 +470,14 @@ pub const App = struct {
surface.queueInspectorRender();
}
- /// Perform a given action.
+ /// Perform a given action. Returns `true` if the action was able to be
+ /// performed, `false` otherwise.
pub fn performAction(
self: *App,
target: apprt.Target,
comptime action: apprt.Action.Key,
value: apprt.Action.Value(action),
- ) !void {
+ ) !bool {
// Special case certain actions before they are sent to the
// embedded apprt.
self.performPreAction(target, action, value);
@@ -480,7 +487,7 @@ pub const App = struct {
action,
value,
});
- self.opts.action(
+ return self.opts.action(
self,
target.cval(),
@unionInit(apprt.Action, @tagName(action), value).cval(),
@@ -633,7 +640,7 @@ pub const Surface = struct {
.y = @floatCast(opts.scale_factor),
},
.size = .{ .width = 800, .height = 600 },
- .cursor_pos = .{ .x = 0, .y = 0 },
+ .cursor_pos = .{ .x = -1, .y = -1 },
.keymap_state = .{},
};
@@ -992,7 +999,7 @@ pub const Surface = struct {
}
fn queueInspectorRender(self: *Surface) void {
- self.app.performAction(
+ _ = self.app.performAction(
.{ .surface = &self.core_surface },
.render_inspector,
{},
@@ -1013,6 +1020,30 @@ pub const Surface = struct {
};
}
+ pub fn defaultTermioEnv(self: *const Surface) !std.process.EnvMap {
+ const alloc = self.app.core_app.alloc;
+ var env = try internal_os.getEnvMap(alloc);
+ errdefer env.deinit();
+
+ if (comptime builtin.target.isDarwin()) {
+ if (env.get("__XCODE_BUILT_PRODUCTS_DIR_PATHS") != null) {
+ env.remove("__XCODE_BUILT_PRODUCTS_DIR_PATHS");
+ env.remove("__XPC_DYLD_LIBRARY_PATH");
+ env.remove("DYLD_FRAMEWORK_PATH");
+ env.remove("DYLD_INSERT_LIBRARIES");
+ env.remove("DYLD_LIBRARY_PATH");
+ env.remove("LD_LIBRARY_PATH");
+ env.remove("SECURITYSESSIONID");
+ env.remove("XPC_SERVICE_NAME");
+ }
+
+ // Remove this so that running `ghostty` within Ghostty works.
+ env.remove("GHOSTTY_MAC_APP");
+ }
+
+ return env;
+ }
+
/// The cursor position from the host directly is in screen coordinates but
/// all our interface works in pixels.
fn cursorPosToPixels(self: *const Surface, pos: apprt.CursorPos) !apprt.CursorPos {
@@ -1419,7 +1450,7 @@ pub const CAPI = struct {
/// Open the configuration.
export fn ghostty_app_open_config(v: *App) void {
- v.performAction(.app, .open_config, {}) catch |err| {
+ _ = v.performAction(.app, .open_config, {}) catch |err| {
log.err("error reloading config err={}", .{err});
return;
};
@@ -1647,7 +1678,12 @@ pub const CAPI = struct {
event: KeyEvent,
) bool {
const core_event = surface.app.coreKeyEvent(
- .{ .surface = surface },
+ // Note: this "app" target here looks like a bug, but it is
+ // intentional. coreKeyEvent uses the target only as a way to
+ // trigger preedit callbacks for keymap translation and we don't
+ // want to trigger that here. See the todo item in coreKeyEvent
+ // for a long term solution to this and removing target altogether.
+ .app,
event.keyEvent(),
) catch |err| {
log.warn("error processing key event err={}", .{err});
@@ -1757,7 +1793,7 @@ pub const CAPI = struct {
/// Request that the surface split in the given direction.
export fn ghostty_surface_split(ptr: *Surface, direction: apprt.action.SplitDirection) void {
- ptr.app.performAction(
+ _ = ptr.app.performAction(
.{ .surface = &ptr.core_surface },
.new_split,
direction,
@@ -1772,7 +1808,7 @@ pub const CAPI = struct {
ptr: *Surface,
direction: apprt.action.GotoSplit,
) void {
- ptr.app.performAction(
+ _ = ptr.app.performAction(
.{ .surface = &ptr.core_surface },
.goto_split,
direction,
@@ -1791,7 +1827,7 @@ pub const CAPI = struct {
direction: apprt.action.ResizeSplit.Direction,
amount: u16,
) void {
- ptr.app.performAction(
+ _ = ptr.app.performAction(
.{ .surface = &ptr.core_surface },
.resize_split,
.{ .direction = direction, .amount = amount },
@@ -1803,7 +1839,7 @@ pub const CAPI = struct {
/// Equalize the size of all splits in the current window.
export fn ghostty_surface_split_equalize(ptr: *Surface) void {
- ptr.app.performAction(
+ _ = ptr.app.performAction(
.{ .surface = &ptr.core_surface },
.equalize_splits,
{},
@@ -1953,7 +1989,7 @@ pub const CAPI = struct {
_ = CGSSetWindowBackgroundBlurRadius(
CGSDefaultConnectionForThread(),
nswindow.msgSend(usize, objc.sel("windowNumber"), .{}),
- @intCast(config.@"background-blur-radius".cval()),
+ @intCast(config.@"background-blur".cval()),
);
}
diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig
index c91464068a..531269ee14 100644
--- a/src/apprt/glfw.zig
+++ b/src/apprt/glfw.zig
@@ -147,13 +147,14 @@ pub const App = struct {
glfw.postEmptyEvent();
}
- /// Perform a given action.
+ /// Perform a given action. Returns `true` if the action was able to be
+ /// performed, `false` otherwise.
pub fn performAction(
self: *App,
target: apprt.Target,
comptime action: apprt.Action.Key,
value: apprt.Action.Value(action),
- ) !void {
+ ) !bool {
switch (action) {
.quit => self.quit = true,
@@ -218,6 +219,7 @@ pub const App = struct {
.toggle_split_zoom,
.present_terminal,
.close_all_windows,
+ .close_tab,
.toggle_tab_overview,
.toggle_window_decorations,
.toggle_quick_terminal,
@@ -236,8 +238,15 @@ pub const App = struct {
.color_change,
.pwd,
.config_change,
- => log.info("unimplemented action={}", .{action}),
+ .toggle_maximize,
+ .prompt_title,
+ => {
+ log.info("unimplemented action={}", .{action});
+ return false;
+ },
}
+
+ return true;
}
/// Reload the configuration. This should return the new configuration.
@@ -872,6 +881,10 @@ pub const Surface = struct {
};
}
+ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap {
+ return try internal_os.getEnvMap(self.app.app.alloc);
+ }
+
fn sizeCallback(window: glfw.Window, width: i32, height: i32) void {
_ = width;
_ = height;
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index 38c019b3e5..227c36ec45 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -25,7 +25,6 @@ const Config = configpkg.Config;
const CoreApp = @import("../../App.zig");
const CoreSurface = @import("../../Surface.zig");
-const adwaita = @import("adwaita.zig");
const cgroup = @import("cgroup.zig");
const Surface = @import("Surface.zig");
const Window = @import("Window.zig");
@@ -36,8 +35,7 @@ const c = @import("c.zig").c;
const version = @import("version.zig");
const inspector = @import("inspector.zig");
const key = @import("key.zig");
-const x11 = @import("x11.zig");
-const wayland = @import("wayland.zig");
+const winproto = @import("winproto.zig");
const testing = std.testing;
const log = std.log.scoped(.gtk);
@@ -50,6 +48,9 @@ config: Config,
app: *c.GtkApplication,
ctx: *c.GMainContext,
+/// State and logic for the underlying windowing protocol.
+winproto: winproto.App,
+
/// True if the app was launched with single instance mode.
single_instance: bool,
@@ -71,11 +72,10 @@ clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
/// This is set to false when the main loop should exit.
running: bool = true,
-/// Xkb state (X11 only). Will be null on Wayland.
-x11_xkb: ?x11.Xkb = null,
-
-/// Wayland app state. Will be null on X11.
-wayland: ?wayland.AppState = null,
+/// If we should retry querying D-Bus for the color scheme with the deprecated
+/// Read method, instead of the recommended ReadOne method. This is kind of
+/// nasty to have as struct state but its just a byte...
+dbus_color_scheme_retry: bool = true,
/// The base path of the transient cgroup used to put all surfaces
/// into their own cgroup. This is only set if cgroups are enabled
@@ -108,41 +108,13 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
c.gtk_get_micro_version(),
});
- // Disabling Vulkan can improve startup times by hundreds of
- // milliseconds on some systems. We don't use Vulkan so we can just
- // disable it.
- if (version.runtimeAtLeast(4, 16, 0)) {
- // From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
- // For the remainder of "why" see the 4.14 comment below.
- _ = internal_os.setenv("GDK_DISABLE", "gles-api,vulkan");
- _ = internal_os.setenv("GDK_DEBUG", "opengl,gl-no-fractional");
- } else if (version.runtimeAtLeast(4, 14, 0)) {
- // We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
- // Older versions of GTK do not support these values so it is safe
- // to always set this. Forwards versions are uncertain so we'll have to
- // reassess...
- //
- // Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
- //
- // Specific details about values:
- // - "opengl" - output OpenGL debug information
- // - "gl-disable-gles" - disable GLES, Ghostty can't use GLES
- // - "vulkan-disable" - disable Vulkan, Ghostty can't use Vulkan
- // and initializing a Vulkan context was causing a longer delay
- // on some systems.
- _ = internal_os.setenv("GDK_DEBUG", "opengl,gl-disable-gles,vulkan-disable,gl-no-fractional");
- } else {
- // Versions prior to 4.14 are a bit of an unknown for Ghostty. It
- // is an environment that isn't tested well and we don't have a
- // good understanding of what we may need to do.
- _ = internal_os.setenv("GDK_DEBUG", "vulkan-disable");
- }
-
- if (version.runtimeAtLeast(4, 14, 0)) {
- // We need to export GSK_RENDERER to opengl because GTK uses ngl by
- // default after 4.14
- _ = internal_os.setenv("GSK_RENDERER", "opengl");
- }
+ // log the adwaita version
+ log.info("libadwaita version build={s} runtime={}.{}.{}", .{
+ c.ADW_VERSION_S,
+ c.adw_get_major_version(),
+ c.adw_get_minor_version(),
+ c.adw_get_micro_version(),
+ });
// Load our configuration
var config = try Config.load(core_app.alloc);
@@ -165,19 +137,121 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
}
}
- c.gtk_init();
- const display = c.gdk_display_get_default();
+ var gdk_debug: struct {
+ /// output OpenGL debug information
+ opengl: bool = false,
+ /// disable GLES, Ghostty can't use GLES
+ @"gl-disable-gles": bool = false,
+ @"gl-no-fractional": bool = false,
+ /// Disabling Vulkan can improve startup times by hundreds of
+ /// milliseconds on some systems. We don't use Vulkan so we can just
+ /// disable it.
+ @"vulkan-disable": bool = false,
+ } = .{
+ .opengl = config.@"gtk-opengl-debug",
+ };
- // If we're using libadwaita, log the version
- if (adwaita.enabled(&config)) {
- log.info("libadwaita version build={s} runtime={}.{}.{}", .{
- c.ADW_VERSION_S,
- c.adw_get_major_version(),
- c.adw_get_minor_version(),
- c.adw_get_micro_version(),
- });
+ var gdk_disable: struct {
+ @"gles-api": bool = false,
+ /// current gtk implementation for color management is not good enough.
+ /// see: https://bugs.kde.org/show_bug.cgi?id=495647
+ /// gtk issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6864
+ @"color-mgmt": bool = true,
+ /// Disabling Vulkan can improve startup times by hundreds of
+ /// milliseconds on some systems. We don't use Vulkan so we can just
+ /// disable it.
+ vulkan: bool = false,
+ } = .{};
+
+ environment: {
+ if (version.runtimeAtLeast(4, 18, 0)) {
+ gdk_disable.@"color-mgmt" = false;
+ }
+
+ if (version.runtimeAtLeast(4, 16, 0)) {
+ // From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
+ // For the remainder of "why" see the 4.14 comment below.
+ gdk_disable.@"gles-api" = true;
+ gdk_disable.vulkan = true;
+ gdk_debug.@"gl-no-fractional" = true;
+ break :environment;
+ }
+ if (version.runtimeAtLeast(4, 14, 0)) {
+ // We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
+ // Older versions of GTK do not support these values so it is safe
+ // to always set this. Forwards versions are uncertain so we'll have
+ // to reassess...
+ //
+ // Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
+ gdk_debug.@"gl-disable-gles" = true;
+ gdk_debug.@"gl-no-fractional" = true;
+ gdk_debug.@"vulkan-disable" = true;
+ break :environment;
+ }
+ // Versions prior to 4.14 are a bit of an unknown for Ghostty. It
+ // is an environment that isn't tested well and we don't have a
+ // good understanding of what we may need to do.
+ gdk_debug.@"vulkan-disable" = true;
+ }
+
+ {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ const writer = fmt.writer();
+ var first: bool = true;
+ inline for (@typeInfo(@TypeOf(gdk_debug)).Struct.fields) |field| {
+ if (@field(gdk_debug, field.name)) {
+ if (!first) try writer.writeAll(",");
+ try writer.writeAll(field.name);
+ first = false;
+ }
+ }
+ try writer.writeByte(0);
+ const value = fmt.getWritten();
+ log.warn("setting GDK_DEBUG={s}", .{value[0 .. value.len - 1]});
+ _ = internal_os.setenv("GDK_DEBUG", value[0 .. value.len - 1 :0]);
+ }
+
+ {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ const writer = fmt.writer();
+ var first: bool = true;
+ inline for (@typeInfo(@TypeOf(gdk_disable)).Struct.fields) |field| {
+ if (@field(gdk_disable, field.name)) {
+ if (!first) try writer.writeAll(",");
+ try writer.writeAll(field.name);
+ first = false;
+ }
+ }
+ try writer.writeByte(0);
+ const value = fmt.getWritten();
+ log.warn("setting GDK_DISABLE={s}", .{value[0 .. value.len - 1]});
+ _ = internal_os.setenv("GDK_DISABLE", value[0 .. value.len - 1 :0]);
}
+ if (version.runtimeAtLeast(4, 14, 0)) {
+ switch (config.@"gtk-gsk-renderer") {
+ .default => {},
+ else => |renderer| {
+ // Force the GSK renderer to a specific value. After GTK 4.14 the
+ // `ngl` renderer is used by default which causes artifacts when
+ // used with Ghostty so it should be avoided.
+ log.warn("setting GSK_RENDERER={s}", .{@tagName(renderer)});
+ _ = internal_os.setenv("GSK_RENDERER", @tagName(renderer));
+ },
+ }
+ }
+
+ c.adw_init();
+
+ const display: *c.GdkDisplay = c.gdk_display_get_default() orelse {
+ // I'm unsure of any scenario where this happens. Because we don't
+ // want to litter null checks everywhere, we just exit here.
+ log.warn("gdk display is null, exiting", .{});
+ std.posix.exit(1);
+ };
+
// The "none" cursor is used for hiding the cursor
const cursor_none = c.gdk_cursor_new_from_name("none", null);
errdefer if (cursor_none) |cursor| c.g_object_unref(cursor);
@@ -212,103 +286,38 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
};
// Create our GTK Application which encapsulates our process.
- const app: *c.GtkApplication = app: {
- log.debug("creating GTK application id={s} single-instance={} adwaita={}", .{
- app_id,
- single_instance,
- adwaita,
- });
-
- // If not libadwaita, create a standard GTK application.
- if ((comptime !adwaita.versionAtLeast(0, 0, 0)) or
- !adwaita.enabled(&config))
- {
- {
- const provider = c.gtk_css_provider_new();
- defer c.g_object_unref(provider);
- switch (config.@"window-theme") {
- .system, .light => {},
- .dark => {
- const settings = c.gtk_settings_get_default();
- c.g_object_set(@ptrCast(@alignCast(settings)), "gtk-application-prefer-dark-theme", true, @as([*c]const u8, null));
-
- c.gtk_css_provider_load_from_resource(
- provider,
- "/com/mitchellh/ghostty/style-dark.css",
- );
- c.gtk_style_context_add_provider_for_display(
- display,
- @ptrCast(provider),
- c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 2,
- );
- },
- .auto, .ghostty => {
- const lum = config.background.toTerminalRGB().perceivedLuminance();
- if (lum <= 0.5) {
- const settings = c.gtk_settings_get_default();
- c.g_object_set(@ptrCast(@alignCast(settings)), "gtk-application-prefer-dark-theme", true, @as([*c]const u8, null));
-
- c.gtk_css_provider_load_from_resource(
- provider,
- "/com/mitchellh/ghostty/style-dark.css",
- );
- c.gtk_style_context_add_provider_for_display(
- display,
- @ptrCast(provider),
- c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 2,
- );
- }
- },
- }
- }
-
- {
- const provider = c.gtk_css_provider_new();
- defer c.g_object_unref(provider);
-
- c.gtk_css_provider_load_from_resource(provider, "/com/mitchellh/ghostty/style.css");
- c.gtk_style_context_add_provider_for_display(
- display,
- @ptrCast(provider),
- c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1,
- );
- }
-
- break :app @as(?*c.GtkApplication, @ptrCast(c.gtk_application_new(
- app_id.ptr,
- app_flags,
- ))) orelse return error.GtkInitFailed;
- }
+ log.debug("creating GTK application id={s} single-instance={}", .{
+ app_id,
+ single_instance,
+ });
- // Use libadwaita if requested. Using an AdwApplication lets us use
- // Adwaita widgets and access things such as the color scheme.
- const adw_app = @as(?*c.AdwApplication, @ptrCast(c.adw_application_new(
- app_id.ptr,
- app_flags,
- ))) orelse return error.GtkInitFailed;
-
- const style_manager = c.adw_application_get_style_manager(adw_app);
- c.adw_style_manager_set_color_scheme(
- style_manager,
- switch (config.@"window-theme") {
- .auto, .ghostty => auto: {
- const lum = config.background.toTerminalRGB().perceivedLuminance();
- break :auto if (lum > 0.5)
- c.ADW_COLOR_SCHEME_PREFER_LIGHT
- else
- c.ADW_COLOR_SCHEME_PREFER_DARK;
- },
-
- .system => c.ADW_COLOR_SCHEME_PREFER_LIGHT,
- .dark => c.ADW_COLOR_SCHEME_FORCE_DARK,
- .light => c.ADW_COLOR_SCHEME_FORCE_LIGHT,
+ // Using an AdwApplication lets us use Adwaita widgets and access things
+ // such as the color scheme.
+ const adw_app = @as(?*c.AdwApplication, @ptrCast(c.adw_application_new(
+ app_id.ptr,
+ app_flags,
+ ))) orelse return error.GtkInitFailed;
+ errdefer c.g_object_unref(adw_app);
+
+ const style_manager = c.adw_application_get_style_manager(adw_app);
+ c.adw_style_manager_set_color_scheme(
+ style_manager,
+ switch (config.@"window-theme") {
+ .auto, .ghostty => auto: {
+ const lum = config.background.toTerminalRGB().perceivedLuminance();
+ break :auto if (lum > 0.5)
+ c.ADW_COLOR_SCHEME_PREFER_LIGHT
+ else
+ c.ADW_COLOR_SCHEME_PREFER_DARK;
},
- );
+ .system => c.ADW_COLOR_SCHEME_PREFER_LIGHT,
+ .dark => c.ADW_COLOR_SCHEME_FORCE_DARK,
+ .light => c.ADW_COLOR_SCHEME_FORCE_LIGHT,
+ },
+ );
- break :app @ptrCast(adw_app);
- };
- errdefer c.g_object_unref(app);
- const gapp = @as(*c.GApplication, @ptrCast(app));
+ const app: *c.GtkApplication = @ptrCast(adw_app);
+ const gapp: *c.GApplication = @ptrCast(app);
// force the resource path to a known value so that it doesn't depend on
// the app id and load in compiled resources
@@ -364,46 +373,15 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
return error.GtkApplicationRegisterFailed;
}
- // Perform all X11 initialization. This ultimately returns the X11
- // keyboard state but the block does more than that (i.e. setting up
- // WM_CLASS).
- const x11_xkb: ?x11.Xkb = x11_xkb: {
- if (comptime !build_options.x11) break :x11_xkb null;
- if (!x11.is_display(display)) break :x11_xkb null;
-
- // Set the X11 window class property (WM_CLASS) if are are on an X11
- // display.
- //
- // Note that we also set the program name here using g_set_prgname.
- // This is how the instance name field for WM_CLASS is derived when
- // calling gdk_x11_display_set_program_class; there does not seem to be
- // a way to set it directly. It does not look like this is being set by
- // our other app initialization routines currently, but since we're
- // currently deriving its value from x11-instance-name effectively, I
- // feel like gating it behind an X11 check is better intent.
- //
- // This makes the property show up like so when using xprop:
- //
- // WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty"
- //
- // Append "-debug" on both when using the debug build.
- //
- const prgname = if (config.@"x11-instance-name") |pn|
- pn
- else if (builtin.mode == .Debug)
- "ghostty-debug"
- else
- "ghostty";
- c.g_set_prgname(prgname);
- c.gdk_x11_display_set_program_class(display, app_id);
-
- // Set up Xkb
- break :x11_xkb try x11.Xkb.init(display);
- };
-
- // Initialize Wayland state
- var wl = wayland.AppState.init(display);
- if (wl) |*w| try w.register();
+ // Setup our windowing protocol logic
+ var winproto_app = try winproto.App.init(
+ core_app.alloc,
+ display,
+ app_id,
+ &config,
+ );
+ errdefer winproto_app.deinit(core_app.alloc);
+ log.debug("windowing protocol={s}", .{@tagName(winproto_app)});
// This just calls the `activate` signal but its part of the normal startup
// routine so we just call it, but only if the config allows it (this allows
@@ -429,8 +407,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
.config = config,
.ctx = ctx,
.cursor_none = cursor_none,
- .x11_xkb = x11_xkb,
- .wayland = wl,
+ .winproto = winproto_app,
.single_instance = single_instance,
// If we are NOT the primary instance, then we never want to run.
// This means that another instance of the GTK app is running and
@@ -458,31 +435,36 @@ pub fn terminate(self: *App) void {
}
self.custom_css_providers.deinit(self.core_app.alloc);
+ self.winproto.deinit(self.core_app.alloc);
+
self.config.deinit();
}
-/// Perform a given action.
+/// Perform a given action. Returns `true` if the action was able to be
+/// performed, `false` otherwise.
pub fn performAction(
self: *App,
target: apprt.Target,
comptime action: apprt.Action.Key,
value: apprt.Action.Value(action),
-) !void {
+) !bool {
switch (action) {
.quit => self.quit(),
.new_window => _ = try self.newWindow(switch (target) {
.app => null,
.surface => |v| v,
}),
+ .toggle_maximize => self.toggleMaximize(target),
.toggle_fullscreen => self.toggleFullscreen(target, value),
.new_tab => try self.newTab(target),
- .goto_tab => self.gotoTab(target, value),
+ .close_tab => try self.closeTab(target),
+ .goto_tab => return self.gotoTab(target, value),
.move_tab => self.moveTab(target, value),
.new_split => try self.newSplit(target, value),
.resize_split => self.resizeSplit(target, value),
.equalize_splits => self.equalizeSplits(target),
- .goto_split => self.gotoSplit(target, value),
+ .goto_split => return self.gotoSplit(target, value),
.open_config => try configpkg.edit.open(self.core_app.alloc),
.config_change => self.configChange(target, value.config),
.reload_config => try self.reloadConfig(target, value),
@@ -492,6 +474,7 @@ pub fn performAction(
.pwd => try self.setPwd(target, value),
.present_terminal => self.presentTerminal(target),
.initial_size => try self.setInitialSize(target, value),
+ .size_limit => try self.setSizeLimit(target, value),
.mouse_visibility => self.setMouseVisibility(target, value),
.mouse_shape => try self.setMouseShape(target, value),
.mouse_over_link => self.setMouseOverLink(target, value),
@@ -504,15 +487,22 @@ pub fn performAction(
.close_all_windows,
.toggle_quick_terminal,
.toggle_visibility,
- .size_limit,
.cell_size,
.secure_input,
.key_sequence,
.render_inspector,
.renderer_health,
.color_change,
- => log.warn("unimplemented action={}", .{action}),
+ .prompt_title,
+ => {
+ log.warn("unimplemented action={}", .{action});
+ return false;
+ },
}
+
+ // We can assume it was handled because all unknown/unimplemented actions
+ // are caught above.
+ return true;
}
fn newTab(_: *App, target: apprt.Target) !void {
@@ -532,24 +522,41 @@ fn newTab(_: *App, target: apprt.Target) !void {
}
}
-fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) void {
+fn closeTab(_: *App, target: apprt.Target) !void {
switch (target) {
.app => {},
+ .surface => |v| {
+ const tab = v.rt_surface.container.tab() orelse {
+ log.info(
+ "close_tab invalid for container={s}",
+ .{@tagName(v.rt_surface.container)},
+ );
+ return;
+ };
+
+ tab.closeWithConfirmation();
+ },
+ }
+}
+
+fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) bool {
+ switch (target) {
+ .app => return false,
.surface => |v| {
const window = v.rt_surface.container.window() orelse {
log.info(
"gotoTab invalid for container={s}",
.{@tagName(v.rt_surface.container)},
);
- return;
+ return false;
};
- switch (tab) {
+ return switch (tab) {
.previous => window.gotoPreviousTab(v.rt_surface),
.next => window.gotoNextTab(v.rt_surface),
.last => window.gotoLastTab(),
else => window.gotoTab(@intCast(@intFromEnum(tab))),
- }
+ };
},
}
}
@@ -603,18 +610,22 @@ fn gotoSplit(
_: *const App,
target: apprt.Target,
direction: apprt.action.GotoSplit,
-) void {
+) bool {
switch (target) {
- .app => {},
+ .app => return false,
.surface => |v| {
- const s = v.rt_surface.container.split() orelse return;
+ const s = v.rt_surface.container.split() orelse return false;
const map = s.directionMap(switch (v.rt_surface.container) {
.split_tl => .top_left,
.split_br => .bottom_right,
.none, .tab_ => unreachable,
});
- const surface_ = map.get(direction) orelse return;
- if (surface_) |surface| surface.grabFocus();
+ const surface_ = map.get(direction) orelse return false;
+ if (surface_) |surface| {
+ surface.grabFocus();
+ return true;
+ }
+ return false;
},
}
}
@@ -658,6 +669,22 @@ fn controlInspector(
surface.controlInspector(mode);
}
+fn toggleMaximize(_: *App, target: apprt.Target) void {
+ switch (target) {
+ .app => {},
+ .surface => |v| {
+ const window = v.rt_surface.container.window() orelse {
+ log.info(
+ "toggleMaximize invalid for container={s}",
+ .{@tagName(v.rt_surface.container)},
+ );
+ return;
+ };
+ window.toggleMaximize();
+ },
+ }
+}
+
fn toggleFullscreen(
_: *App,
target: apprt.Target,
@@ -805,6 +832,23 @@ fn setInitialSize(
}
}
+fn setSizeLimit(
+ _: *App,
+ target: apprt.Target,
+ value: apprt.action.SizeLimit,
+) !void {
+ switch (target) {
+ .app => {},
+ .surface => |v| try v.rt_surface.setSizeLimits(.{
+ .width = value.min_width,
+ .height = value.min_height,
+ }, if (value.max_width > 0) .{
+ .width = value.max_width,
+ .height = value.max_height,
+ } else null),
+ }
+}
+
fn showDesktopNotification(
self: *App,
target: apprt.Target,
@@ -847,9 +891,10 @@ fn configChange(
new_config: *const Config,
) void {
switch (target) {
- .surface => |surface| {
- if (surface.rt_surface.container.window()) |window| window.syncAppearance(new_config) catch |err| {
- log.warn("error syncing appearance changes to window err={}", .{err});
+ .surface => |surface| surface: {
+ const window = surface.rt_surface.container.window() orelse break :surface;
+ window.updateConfig(new_config) catch |err| {
+ log.warn("error updating config for window err={}", .{err});
};
},
@@ -868,11 +913,9 @@ fn configChange(
// App changes needs to show a toast that our configuration
// has reloaded.
- if (adwaita.enabled(&self.config)) {
- if (self.core_app.focusedSurface()) |core_surface| {
- const surface = core_surface.rt_surface;
- if (surface.container.window()) |window| window.onConfigReloaded();
- }
+ if (self.core_app.focusedSurface()) |core_surface| {
+ const surface = core_surface.rt_surface;
+ if (surface.container.window()) |window| window.onConfigReloaded();
}
},
}
@@ -1185,7 +1228,8 @@ pub fn run(self: *App) !void {
self.transient_cgroup_base = path;
} else log.debug("cgroup isolation disabled config={}", .{self.config.@"linux-cgroup"});
- // Setup our D-Bus connection for listening to settings changes.
+ // Setup our D-Bus connection for listening to settings changes,
+ // and asynchronously request the initial color scheme
self.initDbus();
// Setup our menu items
@@ -1193,9 +1237,6 @@ pub fn run(self: *App) !void {
self.initMenu();
self.initContextMenu();
- // Setup our initial color scheme
- self.colorSchemeEvent(self.getColorScheme());
-
// On startup, we want to check for configuration errors right away
// so we can show our error window. We also need to setup other initial
// state.
@@ -1243,6 +1284,22 @@ fn initDbus(self: *App) void {
self,
null,
);
+
+ // Request the initial color scheme asynchronously.
+ c.g_dbus_connection_call(
+ dbus,
+ "org.freedesktop.portal.Desktop",
+ "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.Settings",
+ "ReadOne",
+ c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
+ c.G_VARIANT_TYPE("(v)"),
+ c.G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ null,
+ dbusColorSchemeCallback,
+ self,
+ );
}
// This timeout function is started when no surfaces are open. It can be
@@ -1480,93 +1537,58 @@ fn gtkWindowIsActive(
core_app.focusEvent(false);
}
-/// Call a D-Bus method to determine the current color scheme. If there
-/// is any error at any point we'll log the error and return "light"
-pub fn getColorScheme(self: *App) apprt.ColorScheme {
- const dbus_connection = c.g_application_get_dbus_connection(@ptrCast(self.app));
+fn dbusColorSchemeCallback(
+ source_object: [*c]c.GObject,
+ res: ?*c.GAsyncResult,
+ ud: ?*anyopaque,
+) callconv(.C) void {
+ const self: *App = @ptrCast(@alignCast(ud.?));
+ const dbus: *c.GDBusConnection = @ptrCast(source_object);
var err: ?*c.GError = null;
defer if (err) |e| c.g_error_free(e);
- const value = c.g_dbus_connection_call_sync(
- dbus_connection,
- "org.freedesktop.portal.Desktop",
- "/org/freedesktop/portal/desktop",
- "org.freedesktop.portal.Settings",
- "ReadOne",
- c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
- c.G_VARIANT_TYPE("(v)"),
- c.G_DBUS_CALL_FLAGS_NONE,
- -1,
- null,
- &err,
- ) orelse {
- if (err) |e| {
- // If ReadOne is not yet implemented, fall back to deprecated "Read" method
- // Error code: GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod: No such method “ReadOne”
- if (e.code == 19) {
- return self.getColorSchemeDeprecated();
+ if (c.g_dbus_connection_call_finish(dbus, res, &err)) |value| {
+ if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(v)")) == 1) {
+ var inner: ?*c.GVariant = null;
+ c.g_variant_get(value, "(v)", &inner);
+ defer c.g_variant_unref(inner);
+ if (c.g_variant_is_of_type(inner, c.G_VARIANT_TYPE("u")) == 1) {
+ self.colorSchemeEvent(if (c.g_variant_get_uint32(inner) == 1)
+ .dark
+ else
+ .light);
+ return;
}
- // Otherwise, log the error and return .light
- log.err("unable to get current color scheme: {s}", .{e.message});
}
- return .light;
- };
- defer c.g_variant_unref(value);
-
- if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(v)")) == 1) {
- var inner: ?*c.GVariant = null;
- c.g_variant_get(value, "(v)", &inner);
- defer c.g_variant_unref(inner);
- if (c.g_variant_is_of_type(inner, c.G_VARIANT_TYPE("u")) == 1) {
- return if (c.g_variant_get_uint32(inner) == 1) .dark else .light;
+ } else if (err) |e| {
+ // If ReadOne is not yet implemented, fall back to deprecated "Read" method
+ // Error code: GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod: No such method “ReadOne”
+ if (self.dbus_color_scheme_retry and e.code == 19) {
+ self.dbus_color_scheme_retry = false;
+ c.g_dbus_connection_call(
+ dbus,
+ "org.freedesktop.portal.Desktop",
+ "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.Settings",
+ "Read",
+ c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
+ c.G_VARIANT_TYPE("(v)"),
+ c.G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ null,
+ dbusColorSchemeCallback,
+ self,
+ );
+ return;
}
- }
- return .light;
-}
-
-/// Call the deprecated D-Bus "Read" method to determine the current color scheme. If
-/// there is any error at any point we'll log the error and return "light"
-fn getColorSchemeDeprecated(self: *App) apprt.ColorScheme {
- const dbus_connection = c.g_application_get_dbus_connection(@ptrCast(self.app));
- var err: ?*c.GError = null;
- defer if (err) |e| c.g_error_free(e);
-
- const value = c.g_dbus_connection_call_sync(
- dbus_connection,
- "org.freedesktop.portal.Desktop",
- "/org/freedesktop/portal/desktop",
- "org.freedesktop.portal.Settings",
- "Read",
- c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
- c.G_VARIANT_TYPE("(v)"),
- c.G_DBUS_CALL_FLAGS_NONE,
- -1,
- null,
- &err,
- ) orelse {
- if (err) |e| log.err("Read method failed: {s}", .{e.message});
- return .light;
- };
- defer c.g_variant_unref(value);
-
- if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(v)")) == 1) {
- var inner: ?*c.GVariant = null;
- c.g_variant_get(value, "(v)", &inner);
- defer if (inner) |i| c.g_variant_unref(i);
-
- if (inner) |i| {
- const child = c.g_variant_get_child_value(i, 0) orelse {
- return .light;
- };
- defer c.g_variant_unref(child);
-
- const val = c.g_variant_get_uint32(child);
- return if (val == 1) .dark else .light;
- }
+ // Otherwise, log the error and return .light
+ log.warn("unable to get current color scheme: {s}", .{e.message});
}
- return .light;
+
+ // Fall back
+ self.colorSchemeEvent(.light);
}
/// This will be called by D-Bus when the style changes between light & dark.
@@ -1731,18 +1753,17 @@ fn initActions(self: *App) void {
}
}
-/// This sets the self.menu property to the application menu that can be
-/// shared by all application windows.
-fn initMenu(self: *App) void {
- const menu = c.g_menu_new();
- errdefer c.g_object_unref(menu);
-
+/// Initializes and populates the provided GMenu with sections and actions.
+/// This function is used to set up the application's menu structure, either for
+/// the main menu button or as a context menu when window decorations are disabled.
+fn initMenuContent(menu: *c.GMenu) void {
{
const section = c.g_menu_new();
defer c.g_object_unref(section);
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
c.g_menu_append(section, "New Window", "win.new_window");
c.g_menu_append(section, "New Tab", "win.new_tab");
+ c.g_menu_append(section, "Close Tab", "win.close_tab");
c.g_menu_append(section, "Split Right", "win.split_right");
c.g_menu_append(section, "Split Down", "win.split_down");
c.g_menu_append(section, "Close Window", "win.close");
@@ -1757,13 +1778,14 @@ fn initMenu(self: *App) void {
c.g_menu_append(section, "Reload Configuration", "app.reload-config");
c.g_menu_append(section, "About Ghostty", "win.about");
}
+}
- // {
- // const section = c.g_menu_new();
- // defer c.g_object_unref(section);
- // c.g_menu_append_submenu(menu, "File", @ptrCast(@alignCast(section)));
- // }
-
+/// This sets the self.menu property to the application menu that can be
+/// shared by all application windows.
+fn initMenu(self: *App) void {
+ const menu = c.g_menu_new();
+ errdefer c.g_object_unref(menu);
+ initMenuContent(@ptrCast(menu));
self.menu = menu;
}
@@ -1771,7 +1793,13 @@ fn initContextMenu(self: *App) void {
const menu = c.g_menu_new();
errdefer c.g_object_unref(menu);
- createContextMenuCopyPasteSection(menu, false);
+ {
+ const section = c.g_menu_new();
+ defer c.g_object_unref(section);
+ c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
+ c.g_menu_append(section, "Copy", "win.copy");
+ c.g_menu_append(section, "Paste", "win.paste");
+ }
{
const section = c.g_menu_new();
@@ -1789,21 +1817,21 @@ fn initContextMenu(self: *App) void {
c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
}
- self.context_menu = menu;
-}
-
-fn createContextMenuCopyPasteSection(menu: ?*c.GMenu, has_selection: bool) void {
const section = c.g_menu_new();
defer c.g_object_unref(section);
- c.g_menu_prepend_section(menu, null, @ptrCast(@alignCast(section)));
- // FIXME: Feels really hackish, but disabling sensitivity on this doesn't seems to work(?)
- c.g_menu_append(section, "Copy", if (has_selection) "win.copy" else "noop");
- c.g_menu_append(section, "Paste", "win.paste");
+ const submenu = c.g_menu_new();
+ defer c.g_object_unref(submenu);
+
+ initMenuContent(@ptrCast(submenu));
+ c.g_menu_append_submenu(section, "Menu", @ptrCast(@alignCast(submenu)));
+ c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
+
+ self.context_menu = menu;
}
-pub fn refreshContextMenu(self: *App, has_selection: bool) void {
- c.g_menu_remove(self.context_menu, 0);
- createContextMenuCopyPasteSection(self.context_menu, has_selection);
+pub fn refreshContextMenu(_: *App, window: ?*c.GtkWindow, has_selection: bool) void {
+ const action: ?*c.GSimpleAction = @ptrCast(c.g_action_map_lookup_action(@ptrCast(window), "copy"));
+ c.g_simple_action_set_enabled(action, if (has_selection) 1 else 0);
}
fn isValidAppId(app_id: [:0]const u8) bool {
diff --git a/src/apprt/gtk/Builder.zig b/src/apprt/gtk/Builder.zig
new file mode 100644
index 0000000000..ffacd3adff
--- /dev/null
+++ b/src/apprt/gtk/Builder.zig
@@ -0,0 +1,72 @@
+/// Wrapper around GTK's builder APIs that perform some comptime checks.
+const Builder = @This();
+
+const std = @import("std");
+
+const gtk = @import("gtk");
+const gobject = @import("gobject");
+
+resource_name: [:0]const u8,
+builder: ?*gtk.Builder,
+
+pub fn init(comptime name: []const u8, comptime kind: enum { blp, ui }) Builder {
+ comptime {
+ switch (kind) {
+ .blp => {
+ // Use @embedFile to make sure that the file exists at compile
+ // time. Zig _should_ discard the data so that it doesn't end
+ // up in the final executable. At runtime we will load the data
+ // from a GResource.
+ _ = @embedFile("ui/" ++ name ++ ".blp");
+
+ // Check to make sure that our file is listed as a
+ // `blueprint_file` in `gresource.zig`. If it isn't Ghostty
+ // could crash at runtime when we try and load a nonexistent
+ // GResource.
+ const gresource = @import("gresource.zig");
+ for (gresource.blueprint_files) |blueprint_file| {
+ if (std.mem.eql(u8, blueprint_file, name)) break;
+ } else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
+ },
+ .ui => {
+ // Use @embedFile to make sure that the file exists at compile
+ // time. Zig _should_ discard the data so that it doesn't end
+ // up in the final executable. At runtime we will load the data
+ // from a GResource.
+ _ = @embedFile("ui/" ++ name ++ ".ui");
+
+ // Check to make sure that our file is listed as a `ui_file` in
+ // `gresource.zig`. If it isn't Ghostty could crash at runtime
+ // when we try and load a nonexistent GResource.
+ const gresource = @import("gresource.zig");
+ for (gresource.ui_files) |ui_file| {
+ if (std.mem.eql(u8, ui_file, name)) break;
+ } else @compileError("missing ui file '" ++ name ++ "' in gresource.zig");
+ },
+ }
+ }
+
+ return .{
+ .resource_name = "/com/mitchellh/ghostty/ui/" ++ name ++ ".ui",
+ .builder = null,
+ };
+}
+
+pub fn setWidgetClassTemplate(self: *const Builder, class: *gtk.WidgetClass) void {
+ class.setTemplateFromResource(self.resource_name);
+}
+
+pub fn getObject(self: *Builder, name: [:0]const u8) ?*gobject.Object {
+ const builder = builder: {
+ if (self.builder) |builder| break :builder builder;
+ const builder = gtk.Builder.newFromResource(self.resource_name);
+ self.builder = builder;
+ break :builder builder;
+ };
+
+ return builder.getObject(name);
+}
+
+pub fn deinit(self: *const Builder) void {
+ if (self.builder) |builder| builder.unref();
+}
diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig
index 2d428acb2a..8ddadfd131 100644
--- a/src/apprt/gtk/Split.zig
+++ b/src/apprt/gtk/Split.zig
@@ -313,11 +313,7 @@ pub fn directionMap(self: *const Split, from: Side) DirectionMap {
if (self.directionPrevious(from)) |prev| {
result.put(.previous, prev.surface);
if (!prev.wrapped) {
- // This behavior matches the behavior of macOS at the time of writing
- // this. There is an open issue (#524) to make this depend on the
- // actual physical location of the current split.
result.put(.up, prev.surface);
- result.put(.left, prev.surface);
}
}
@@ -325,13 +321,57 @@ pub fn directionMap(self: *const Split, from: Side) DirectionMap {
result.put(.next, next.surface);
if (!next.wrapped) {
result.put(.down, next.surface);
- result.put(.right, next.surface);
}
}
+ if (self.directionLeft(from)) |left| {
+ result.put(.left, left);
+ }
+
+ if (self.directionRight(from)) |right| {
+ result.put(.right, right);
+ }
+
return result;
}
+fn directionLeft(self: *const Split, from: Side) ?*Surface {
+ switch (from) {
+ .bottom_right => {
+ switch (self.orientation) {
+ .horizontal => return self.top_left.deepestSurface(.bottom_right),
+ .vertical => return directionLeft(
+ self.container.split() orelse return null,
+ .bottom_right,
+ ),
+ }
+ },
+ .top_left => return directionLeft(
+ self.container.split() orelse return null,
+ .bottom_right,
+ ),
+ }
+}
+
+fn directionRight(self: *const Split, from: Side) ?*Surface {
+ switch (from) {
+ .top_left => {
+ switch (self.orientation) {
+ .horizontal => return self.bottom_right.deepestSurface(.top_left),
+ .vertical => return directionRight(
+ self.container.split() orelse return null,
+ .top_left,
+ ),
+ }
+ },
+ .bottom_right => return directionRight(
+ self.container.split() orelse return null,
+ .top_left,
+ ),
+ }
+}
+
+
fn directionPrevious(self: *const Split, from: Side) ?struct {
surface: *Surface,
wrapped: bool,
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index 056a3f40b8..42c8278a23 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -25,7 +25,6 @@ const ResizeOverlay = @import("ResizeOverlay.zig");
const inspector = @import("inspector.zig");
const gtk_key = @import("key.zig");
const c = @import("c.zig").c;
-const x11 = @import("x11.zig");
const log = std.log.scoped(.gtk_surface);
@@ -347,6 +346,11 @@ cursor: ?*c.GdkCursor = null,
/// pass it to GTK.
title_text: ?[:0]const u8 = null,
+/// Our current working directory. We use this value for setting tooltips in
+/// the headerbar subtitle if we have focus. When set, the text in this buf
+/// will be null-terminated because we need to pass it to GTK.
+pwd: ?[:0]const u8 = null,
+
/// The timer used to delay title updates in order to prevent flickering.
update_title_timer: ?c.guint = null,
@@ -364,10 +368,9 @@ cursor_pos: apprt.CursorPos,
inspector: ?*inspector.Inspector = null,
/// Key input states. See gtkKeyPressed for detailed descriptions.
-in_keypress: bool = false,
+in_keyevent: IMKeyEvent = .false,
im_context: *c.GtkIMContext,
im_composing: bool = false,
-im_commit_buffered: bool = false,
im_buf: [128]u8 = undefined,
im_len: u7 = 0,
@@ -375,6 +378,20 @@ im_len: u7 = 0,
/// details on what this is.
cgroup_path: ?[]const u8 = null,
+/// The state of the key event while we're doing IM composition.
+/// See gtkKeyPressed for detailed descriptions.
+pub const IMKeyEvent = enum {
+ /// Not in a key event.
+ false,
+
+ /// In a key event but im_composing was either true or false
+ /// prior to the calling IME processing. This is important to
+ /// work around different input methods calling commit and
+ /// preedit end in a different order.
+ composing,
+ not_composing,
+};
+
/// Configuration used for initializing the surface. We have to copy some
/// data since initialization is delayed with GTK (on realize).
pub const InitConfig = struct {
@@ -492,6 +509,17 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
c.gtk_widget_set_focusable(gl_area, 1);
c.gtk_widget_set_focus_on_click(gl_area, 1);
+ // Set up to handle items being dropped on our surface. Files can be dropped
+ // from Nautilus and strings can be dropped from many programs.
+ const drop_target = c.gtk_drop_target_new(c.G_TYPE_INVALID, c.GDK_ACTION_COPY);
+ errdefer c.g_object_unref(drop_target);
+ var drop_target_types = [_]c.GType{
+ c.gdk_file_list_get_type(),
+ c.G_TYPE_STRING,
+ };
+ c.gtk_drop_target_set_gtypes(drop_target, @ptrCast(&drop_target_types), drop_target_types.len);
+ c.gtk_widget_add_controller(@ptrCast(overlay), @ptrCast(drop_target));
+
// Inherit the parent's font size if we have a parent.
const font_size: ?font.face.DesiredSize = font_size: {
if (!app.config.@"window-inherit-font-size") break :font_size null;
@@ -545,7 +573,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
.font_size = font_size,
.init_config = init_config,
.size = .{ .width = 800, .height = 600 },
- .cursor_pos = .{ .x = 0, .y = 0 },
+ .cursor_pos = .{ .x = -1, .y = -1 },
.im_context = im_context,
.cgroup_path = cgroup_path,
};
@@ -574,6 +602,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
_ = c.g_signal_connect_data(im_context, "preedit-changed", c.G_CALLBACK(>kInputPreeditChanged), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(im_context, "preedit-end", c.G_CALLBACK(>kInputPreeditEnd), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, c.G_CONNECT_DEFAULT);
+ _ = c.g_signal_connect_data(drop_target, "drop", c.G_CALLBACK(>kDrop), self, null, c.G_CONNECT_DEFAULT);
}
fn realize(self: *Surface) !void {
@@ -618,9 +647,6 @@ fn realize(self: *Surface) !void {
try self.core_surface.setFontSize(size);
}
- // Set the initial color scheme
- try self.core_surface.colorSchemeCallback(self.app.getColorScheme());
-
// Note we're realized
self.realized = true;
}
@@ -628,6 +654,7 @@ fn realize(self: *Surface) !void {
pub fn deinit(self: *Surface) void {
self.init_config.deinit(self.app.core_app.alloc);
if (self.title_text) |title| self.app.core_app.alloc.free(title);
+ if (self.pwd) |pwd| self.app.core_app.alloc.free(pwd);
// We don't allocate anything if we aren't realized.
if (!self.realized) return;
@@ -840,6 +867,28 @@ pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void
);
}
+pub fn setSizeLimits(self: *const Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void {
+
+ // There's no support for setting max size at the moment.
+ _ = max_;
+
+ // If we are within a split, do not set the size.
+ if (self.container.split() != null) return;
+
+ // This operation only makes sense if we're within a window view
+ // hierarchy and we're the first tab in the window.
+ const window = self.container.window() orelse return;
+ if (window.notebook.nPages() > 1) return;
+
+ // Note: this doesn't properly take into account the window decorations.
+ // I'm not currently sure how to do that.
+ c.gtk_widget_set_size_request(
+ @ptrCast(window.window),
+ @intCast(min.width),
+ @intCast(min.height),
+ );
+}
+
pub fn grabFocus(self: *Surface) void {
if (self.container.tab()) |tab| {
// If any other surface was focused and zoomed in, set it to non zoomed in
@@ -876,7 +925,7 @@ fn updateTitleLabels(self: *Surface) void {
// I don't know a way around this yet. I've tried re-hiding the
// cursor after setting the title but it doesn't work, I think
// due to some gtk event loop things...
- c.gtk_window_set_title(window.window, title.ptr);
+ window.setTitle(title);
}
}
}
@@ -929,11 +978,27 @@ pub fn getTitle(self: *Surface) ?[:0]const u8 {
return null;
}
+/// Set the current working directory of the surface.
+///
+/// In addition, update the tab's tooltip text, and if we are the focused child,
+/// update the subtitle of the containing window.
pub fn setPwd(self: *Surface, pwd: [:0]const u8) !void {
- // If we have a tab and are the focused child, then we have to update the tab
if (self.container.tab()) |tab| {
tab.setTooltipText(pwd);
+
+ if (tab.focus_child == self) {
+ if (self.container.window()) |window| {
+ if (self.app.config.@"window-subtitle" == .@"working-directory") window.setSubtitle(pwd);
+ }
+ }
}
+
+ const alloc = self.app.core_app.alloc;
+
+ // Failing to set the surface's current working directory is not a big
+ // deal since we just used our slice parameter which is the same value.
+ if (self.pwd) |old| alloc.free(old);
+ self.pwd = alloc.dupeZ(u8, pwd) catch null;
}
pub fn setMouseShape(
@@ -1082,7 +1147,7 @@ pub fn setClipboardString(
c.gdk_clipboard_set_text(clipboard, val.ptr);
// We only toast if we are copying to the standard clipboard.
if (clipboard_type == .standard and
- self.app.config.@"adw-toast".@"clipboard-copy")
+ self.app.config.@"app-notifications".@"clipboard-copy")
{
if (self.container.window()) |window|
window.sendToast("Copied to clipboard");
@@ -1205,10 +1270,12 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void {
return;
};
+ // Convert surface coordinate into coordinate space of the
+ // context menu's parent
var point: c.graphene_point_t = .{ .x = x, .y = y };
if (c.gtk_widget_compute_point(
self.primaryWidget(),
- @ptrCast(window.window),
+ c.gtk_widget_get_parent(@ptrCast(window.context_menu)),
&c.GRAPHENE_POINT_INIT(point.x, point.y),
@ptrCast(&point),
) == 0) {
@@ -1224,7 +1291,7 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void {
};
c.gtk_popover_set_pointing_to(@ptrCast(@alignCast(window.context_menu)), &rect);
- self.app.refreshContextMenu(self.core_surface.hasSelection());
+ self.app.refreshContextMenu(window.window, self.core_surface.hasSelection());
c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu)));
}
@@ -1328,6 +1395,12 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque)
return;
};
+ if (self.container.window()) |window| {
+ window.winproto.resizeEvent() catch |err| {
+ log.warn("failed to notify window protocol of resize={}", .{err});
+ };
+ }
+
self.resize_overlay.maybeShow();
}
}
@@ -1433,31 +1506,37 @@ fn gtkMouseMotion(
.y = @floatCast(scaled.y),
};
- // When the GLArea is resized under the mouse, GTK issues a mouse motion
- // event. This has the unfortunate side effect of causing focus to potentially
- // change when `focus-follows-mouse` is enabled. To prevent this, we check
- // if the cursor is still in the same place as the last event and only grab
- // focus if it has moved.
+ // There seem to be at least two cases where GTK issues a mouse motion
+ // event without the cursor actually moving:
+ // 1. GLArea is resized under the mouse. This has the unfortunate
+ // side effect of causing focus to potentially change when
+ // `focus-follows-mouse` is enabled.
+ // 2. The window title is updated. This can cause the mouse to unhide
+ // incorrectly when hide-mouse-when-typing is enabled.
+ // To prevent incorrect behavior, we'll only grab focus and
+ // continue with callback logic if the cursor has actually moved.
const is_cursor_still = @abs(self.cursor_pos.x - pos.x) < 1 and
@abs(self.cursor_pos.y - pos.y) < 1;
- // If we don't have focus, and we want it, grab it.
- const gl_widget = @as(*c.GtkWidget, @ptrCast(self.gl_area));
- if (!is_cursor_still and c.gtk_widget_has_focus(gl_widget) == 0 and self.app.config.@"focus-follows-mouse") {
- self.grabFocus();
- }
+ if (!is_cursor_still) {
+ // If we don't have focus, and we want it, grab it.
+ const gl_widget = @as(*c.GtkWidget, @ptrCast(self.gl_area));
+ if (c.gtk_widget_has_focus(gl_widget) == 0 and self.app.config.@"focus-follows-mouse") {
+ self.grabFocus();
+ }
- // Our pos changed, update
- self.cursor_pos = pos;
+ // Our pos changed, update
+ self.cursor_pos = pos;
- // Get our modifiers
- const gtk_mods = c.gdk_event_get_modifier_state(event);
- const mods = gtk_key.translateMods(gtk_mods);
+ // Get our modifiers
+ const gtk_mods = c.gdk_event_get_modifier_state(event);
+ const mods = gtk_key.translateMods(gtk_mods);
- self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| {
- log.err("error in cursor pos callback err={}", .{err});
- return;
- };
+ self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| {
+ log.err("error in cursor pos callback err={}", .{err});
+ return;
+ };
+ }
}
fn gtkMouseLeave(
@@ -1537,30 +1616,36 @@ fn gtkKeyReleased(
)) 1 else 0;
}
-/// Key press event. This is where we do ALL of our key handling,
-/// translation to keyboard layouts, dead key handling, etc. Key handling
-/// is complicated so this comment will explain what's going on.
+/// Key press event (press or release).
///
/// At a high level, we want to construct an `input.KeyEvent` and
/// pass that to `keyCallback`. At a low level, this is more complicated
/// than it appears because we need to construct all of this information
/// and its not given to us.
///
-/// For press events, we run the keypress through the input method context
-/// in order to determine if we're in a dead key state, completed unicode
-/// char, etc. This all happens through various callbacks: preedit, commit,
-/// etc. These inspect "in_keypress" if they have to and set some instance
-/// state.
+/// For all events, we run the GdkEvent through the input method context.
+/// This allows the input method to capture the event and trigger
+/// callbacks such as preedit, commit, etc.
+///
+/// There are a couple important aspects to the prior paragraph: we must
+/// send ALL events through the input method context. This is because
+/// input methods use both key press and key release events to determine
+/// the state of the input method. For example, fcitx uses key release
+/// events on modifiers (i.e. ctrl+shift) to switch the input method.
+///
+/// We set some state to note we're in a key event (self.in_keyevent)
+/// because some of the input method callbacks change behavior based on
+/// this state. For example, we don't want to send character events
+/// like "a" via the input "commit" event if we're actively processing
+/// a keypress because we'd lose access to the keycode information.
+/// However, a "commit" event may still happen outside of a keypress
+/// event from e.g. a tablet or on-screen keyboard.
///
-/// We then take all of the information in order to determine if we have
+/// Finally, we take all of the information in order to determine if we have
/// a unicode character or if we have to map the keyval to a code to
/// get the underlying logical key, etc.
///
-/// Finally, we can emit the keyCallback.
-///
-/// Note we ALSO have an IMContext attached directly to the widget
-/// which can emit preedit and commit callbacks. But, if we're not
-/// in a keypress, we let those automatically work.
+/// Then we can emit the keyCallback.
pub fn keyEvent(
self: *Surface,
action: input.Action,
@@ -1569,26 +1654,15 @@ pub fn keyEvent(
keycode: c.guint,
gtk_mods: c.GdkModifierType,
) bool {
+ // log.warn("GTKIM: keyEvent action={}", .{action});
const event = c.gtk_event_controller_get_current_event(
@ptrCast(ec_key),
) orelse return false;
- const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
-
- // Get the unshifted unicode value of the keyval. This is used
- // by the Kitty keyboard protocol.
- const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
- @ptrCast(self.gl_area),
- event,
- keycode,
- );
-
- // We always reset our committed text when ending a keypress so that
- // future keypresses don't think we have a commit event.
- defer self.im_len = 0;
-
- // We only want to send the event through the IM context if we're a press
- if (action == .press or action == .repeat) {
+ // The block below is all related to input method handling. See the function
+ // comment for some high level details and then the comments within
+ // the block for more specifics.
+ {
// This can trigger an input method so we need to notify the im context
// where the cursor is so it can render the dropdowns in the correct
// place.
@@ -1600,41 +1674,98 @@ pub fn keyEvent(
.height = 1,
});
- // We mark that we're in a keypress event. We use this in our
- // IM commit callback to determine if we need to send a char callback
- // to the core surface or not.
- self.in_keypress = true;
- defer self.in_keypress = false;
-
- // Pass the event through the IM controller to handle dead key states.
- // Filter is true if the event was handled by the IM controller.
- const im_handled = c.gtk_im_context_filter_keypress(self.im_context, event) != 0;
- // log.warn("im_handled={} im_len={} im_composing={}", .{ im_handled, self.im_len, self.im_composing });
-
- // If this is a dead key, then we're composing a character and
- // we need to set our proper preedit state.
- if (self.im_composing) preedit: {
- const text = self.im_buf[0..self.im_len];
- self.core_surface.preeditCallback(text) catch |err| {
- log.err("error in preedit callback err={}", .{err});
- break :preedit;
- };
-
- // If we're composing then we don't want to send the key
- // event to the core surface so we always return immediately.
- if (im_handled) return true;
- } else {
- // If we aren't composing, then we set our preedit to
- // empty no matter what.
- self.core_surface.preeditCallback(null) catch {};
-
- // If the IM handled this and we have no text, then we just
- // return because this probably just changed the input method
- // or something.
- if (im_handled and self.im_len == 0) return true;
+ // We note that we're in a keypress because we want some logic to
+ // depend on this. For example, we don't want to send character events
+ // like "a" via the input "commit" event if we're actively processing
+ // a keypress because we'd lose access to the keycode information.
+ //
+ // We have to maintain some additional state here of whether we
+ // were composing because different input methods call the callbacks
+ // in different orders. For example, ibus calls commit THEN preedit
+ // end but simple calls preedit end THEN commit.
+ self.in_keyevent = if (self.im_composing) .composing else .not_composing;
+ defer self.in_keyevent = .false;
+
+ // Pass the event through the input method which returns true if handled.
+ // Confusingly, not all events handled by the input method result
+ // in this returning true so we have to maintain some additional
+ // state about whether we were composing or not to determine if
+ // we should proceed with key encoding.
+ //
+ // Cases where the input method does not mark the event as handled:
+ //
+ // - If we change the input method via keypress while we have preedit
+ // text, the input method will commit the pending text but will not
+ // mark it as handled. We use the `.composing` state to detect
+ // this case.
+ //
+ // - If we switch input methods (i.e. via ctrl+shift with fcitx),
+ // the input method will handle the key release event but will not
+ // mark it as handled. I don't know any way to detect this case so
+ // it will result in a key event being sent to the key callback.
+ // For Kitty text encoding, this will result in modifiers being
+ // triggered despite being technically consumed. At the time of
+ // writing, both Kitty and Alacritty have the same behavior. I
+ // know of no way to fix this.
+ const im_handled = c.gtk_im_context_filter_keypress(
+ self.im_context,
+ event,
+ ) != 0;
+ // log.warn("GTKIM: im_handled={} im_len={} im_composing={}", .{
+ // im_handled,
+ // self.im_len,
+ // self.im_composing,
+ // });
+
+ // If the input method handled the event, you would think we would
+ // never proceed with key encoding for Ghostty but that is not the
+ // case. Input methods will handle basic character encoding like
+ // typing "a" and we want to associate that with the key event.
+ // So we have to check additional state to determine if we exit.
+ if (im_handled) {
+ // If we are composing then we're in a preedit state and do
+ // not want to encode any keys. For example: type a deadkey
+ // such as single quote on a US international keyboard layout.
+ if (self.im_composing) return true;
+
+ // If we were composing and now we're not it means that we committed
+ // the text. We also don't want to encode a key event for this.
+ // Example: enable Japanese input method, press "konn" and then
+ // press enter. The final enter should not be encoded and "konn"
+ // (in hiragana) should be written as "こん".
+ if (self.in_keyevent == .composing) return true;
+
+ // Not composing and our input method buffer is empty. This could
+ // mean that the input method reacted to this event by activating
+ // an onscreen keyboard or something equivalent. We don't know.
+ // But the input method handled it and didn't give us text so
+ // we will just assume we should not encode this. This handles a
+ // real scenario when ibus starts the emoji input method
+ // (super+.).
+ if (self.im_len == 0) return true;
}
+
+ // At this point, for the sake of explanation of internal state:
+ // it is possible that im_len > 0 and im_composing == false. This
+ // means that we received a commit event from the input method that
+ // we want associated with the key event. This is common: its how
+ // basic character translation for simple inputs like "a" work.
}
+ // We always reset the length of the im buffer. There's only one scenario
+ // we reach this point with im_len > 0 and that's if we received a commit
+ // event from the input method. We don't want to keep that state around
+ // since we've handled it here.
+ defer self.im_len = 0;
+
+ // Get the keyvals for this event.
+ const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
+ const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
+ @ptrCast(self.gl_area),
+ event,
+ keycode,
+ );
+
// We want to get the physical unmapped key to process physical keybinds.
// (These are keybinds explicitly marked as requesting physical mapping).
const physical_key = keycode: for (input.keycodes.entries) |entry| {
@@ -1643,11 +1774,11 @@ pub fn keyEvent(
// Get our modifier for the event
const mods: input.Mods = gtk_key.eventMods(
- @ptrCast(self.gl_area),
event,
physical_key,
gtk_mods,
- if (self.app.x11_xkb) |*xkb| xkb else null,
+ action,
+ &self.app.winproto,
);
// Get our consumed modifiers
@@ -1768,12 +1899,11 @@ fn gtkInputPreeditStart(
_: *c.GtkIMContext,
ud: ?*anyopaque,
) callconv(.C) void {
- //log.debug("preedit start", .{});
+ // log.warn("GTKIM: preedit start", .{});
const self = userdataSelf(ud.?);
- if (!self.in_keypress) return;
- // Mark that we are now composing a string with a dead key state.
- // We'll record the string in the preedit-changed callback.
+ // Start our composing state for the input method and reset our
+ // input buffer to empty.
self.im_composing = true;
self.im_len = 0;
}
@@ -1784,52 +1914,33 @@ fn gtkInputPreeditChanged(
) callconv(.C) void {
const self = userdataSelf(ud.?);
- // If there's buffered character, send the characters directly to the surface.
- if (self.im_composing and self.im_commit_buffered) {
- defer self.im_commit_buffered = false;
- defer self.im_len = 0;
- _ = self.core_surface.keyCallback(.{
- .action = .press,
- .key = .invalid,
- .physical_key = .invalid,
- .mods = .{},
- .consumed_mods = .{},
- .composing = false,
- .utf8 = self.im_buf[0..self.im_len],
- }) catch |err| {
- log.err("error in key callback err={}", .{err});
- return;
- };
- }
-
- if (!self.in_keypress) return;
-
// Get our pre-edit string that we'll use to show the user.
var buf: [*c]u8 = undefined;
_ = c.gtk_im_context_get_preedit_string(ctx, &buf, null, null);
defer c.g_free(buf);
const str = std.mem.sliceTo(buf, 0);
- // If our string becomes empty we ignore this. This can happen after
- // a commit event when the preedit is being cleared and we don't want
- // to set im_len to zero. This is safe because preeditstart always sets
- // im_len to zero.
- if (str.len == 0) return;
-
- // Copy the preedit string into the im_buf. This is safe because
- // commit will always overwrite this.
- self.im_len = @intCast(@min(self.im_buf.len, str.len));
- @memcpy(self.im_buf[0..self.im_len], str);
+ // Update our preedit state in Ghostty core
+ // log.warn("GTKIM: preedit change str={s}", .{str});
+ self.core_surface.preeditCallback(str) catch |err| {
+ log.err("error in preedit callback err={}", .{err});
+ };
}
fn gtkInputPreeditEnd(
_: *c.GtkIMContext,
ud: ?*anyopaque,
) callconv(.C) void {
- //log.debug("preedit end", .{});
+ // log.warn("GTKIM: preedit end", .{});
const self = userdataSelf(ud.?);
- if (!self.in_keypress) return;
+
+ // End our composing state for GTK, allowing us to commit the text.
self.im_composing = false;
+
+ // End our preedit state in Ghostty core
+ self.core_surface.preeditCallback(null) catch |err| {
+ log.err("error in preedit callback err={}", .{err});
+ };
}
fn gtkInputCommit(
@@ -1840,35 +1951,64 @@ fn gtkInputCommit(
const self = userdataSelf(ud.?);
const str = std.mem.sliceTo(bytes, 0);
- // If we're in a key event, then we want to buffer the commit so
- // that we can send the proper keycallback followed by the char
- // callback.
- if (self.in_keypress) {
- if (str.len <= self.im_buf.len) {
- @memcpy(self.im_buf[0..str.len], str);
- self.im_len = @intCast(str.len);
+ // log.debug("GTKIM: input commit composing={} keyevent={} str={s}", .{
+ // self.im_composing,
+ // self.in_keyevent,
+ // str,
+ // });
- // If composing is done and character should be committed,
- // It should be committed in preedit callback.
- if (self.im_composing) {
- self.im_commit_buffered = true;
+ // We need to handle commit specially if we're in a key event.
+ // Specifically, GTK will send us a commit event for basic key
+ // encodings like "a" (on a US layout keyboard). We don't want
+ // to treat this as IME committed text because we want to associate
+ // it with a key event (i.e. "a" key press).
+ switch (self.in_keyevent) {
+ // If we're not in a key event then this commit is from
+ // some other source (i.e. on-screen keyboard, tablet, etc.)
+ // and we want to commit the text to the core surface.
+ .false => {},
+
+ // If we're in a composing state and in a key event then this
+ // key event is resulting in a commit of multiple keypresses
+ // and we don't want to encode it alongside the keypress.
+ .composing => {},
+
+ // If we're not composing then this commit is just a normal
+ // key encoding and we want our key event to handle it so
+ // that Ghostty can be aware of the key event alongside
+ // the text.
+ .not_composing => {
+ if (str.len > self.im_buf.len) {
+ log.warn("not enough buffer space for input method commit", .{});
+ return;
}
- // log.debug("input commit len={}", .{self.im_len});
- } else {
- log.warn("not enough buffer space for input method commit", .{});
- }
+ // Copy our committed text to the buffer
+ @memcpy(self.im_buf[0..str.len], str);
+ self.im_len = @intCast(str.len);
- return;
+ // log.debug("input commit len={}", .{self.im_len});
+ return;
+ },
}
- // This prevents staying in composing state after commit even though
- // input method has changed.
+ // If we reach this point from above it means we're composing OR
+ // not in a keypress. In either case, we want to commit the text
+ // given to us because that's what GTK is asking us to do. If we're
+ // not in a keypress it means that this commit came via a non-keyboard
+ // event (i.e. on-screen keyboard, tablet of some kind, etc.).
+
+ // Committing ends composing state
self.im_composing = false;
- // We're not in a keypress, so this was sent from an on-screen emoji
- // keyboard or something like that. Send the characters directly to
- // the surface.
+ // End our preedit state. Well-behaved input methods do this for us
+ // by triggering a preedit-end event but some do not (ibus 1.5.29).
+ self.core_surface.preeditCallback(null) catch |err| {
+ log.err("error in preedit callback err={}", .{err});
+ };
+
+ // Send the text to the core surface, associated with no key (an
+ // invalid key, which should produce no PTY encoding).
_ = self.core_surface.keyCallback(.{
.action = .press,
.key = .invalid,
@@ -1878,7 +2018,7 @@ fn gtkInputCommit(
.composing = false,
.utf8 = str,
}) catch |err| {
- log.err("error in key callback err={}", .{err});
+ log.warn("error in key callback err={}", .{err});
return;
};
}
@@ -1896,6 +2036,12 @@ fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) vo
self.unfocused_widget = null;
}
+ if (self.pwd) |pwd| {
+ if (self.container.window()) |window| {
+ if (self.app.config.@"window-subtitle" == .@"working-directory") window.setSubtitle(pwd);
+ }
+ }
+
// Notify our surface
self.core_surface.focusCallback(true) catch |err| {
log.err("error in focus callback err={}", .{err});
@@ -1980,7 +2126,7 @@ pub fn present(self: *Surface) void {
if (self.container.window()) |window| {
if (self.container.tab()) |tab| {
if (window.notebook.getTabPosition(tab)) |position|
- window.notebook.gotoNthTab(position);
+ _ = window.notebook.gotoNthTab(position);
}
c.gtk_window_present(window.window);
}
@@ -2025,3 +2171,131 @@ pub fn setSplitZoom(self: *Surface, new_split_zoom: bool) void {
pub fn toggleSplitZoom(self: *Surface) void {
self.setSplitZoom(!self.zoomed_in);
}
+
+/// Handle items being dropped on our surface.
+fn gtkDrop(
+ _: *c.GtkDropTarget,
+ value: *c.GValue,
+ x: f64,
+ y: f64,
+ ud: ?*anyopaque,
+) callconv(.C) c.gboolean {
+ _ = x;
+ _ = y;
+ const self = userdataSelf(ud.?);
+ const alloc = self.app.core_app.alloc;
+
+ if (g_value_holds(value, c.G_TYPE_BOXED)) {
+ var data = std.ArrayList(u8).init(alloc);
+ defer data.deinit();
+
+ var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{
+ .child_writer = data.writer(),
+ };
+ const writer = shell_escape_writer.writer();
+
+ const fl: *c.GdkFileList = @ptrCast(c.g_value_get_boxed(value));
+ var l = c.gdk_file_list_get_files(fl);
+
+ while (l != null) : (l = l.*.next) {
+ const file: *c.GFile = @ptrCast(l.*.data);
+ const path = c.g_file_get_path(file) orelse continue;
+
+ writer.writeAll(std.mem.span(path)) catch |err| {
+ log.err("unable to write path to buffer: {}", .{err});
+ continue;
+ };
+ writer.writeAll("\n") catch |err| {
+ log.err("unable to write to buffer: {}", .{err});
+ continue;
+ };
+ }
+
+ const string = data.toOwnedSliceSentinel(0) catch |err| {
+ log.err("unable to convert to a slice: {}", .{err});
+ return 1;
+ };
+ defer alloc.free(string);
+
+ self.doPaste(string);
+
+ return 1;
+ }
+
+ if (g_value_holds(value, c.G_TYPE_STRING)) {
+ if (c.g_value_get_string(value)) |string| {
+ self.doPaste(std.mem.span(string));
+ }
+ return 1;
+ }
+
+ return 1;
+}
+
+fn doPaste(self: *Surface, data: [:0]const u8) void {
+ if (data.len == 0) return;
+
+ self.core_surface.completeClipboardRequest(.paste, data, false) catch |err| switch (err) {
+ error.UnsafePaste,
+ error.UnauthorizedPaste,
+ => {
+ ClipboardConfirmationWindow.create(
+ self.app,
+ data,
+ &self.core_surface,
+ .paste,
+ ) catch |window_err| {
+ log.err("failed to create clipboard confirmation window err={}", .{window_err});
+ };
+ },
+ error.OutOfMemory,
+ error.NoSpaceLeft,
+ => log.err("failed to complete clipboard request err={}", .{err}),
+ };
+}
+
+pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap {
+ const alloc = self.app.core_app.alloc;
+ var env = try internal_os.getEnvMap(alloc);
+ errdefer env.deinit();
+
+ // Don't leak these GTK environment variables to child processes.
+ env.remove("GDK_DEBUG");
+ env.remove("GDK_DISABLE");
+ env.remove("GSK_RENDERER");
+
+ // Unset environment varies set by snaps if we're running in a snap.
+ // This allows Ghostty to further launch additional snaps.
+ if (env.get("SNAP")) |_| {
+ env.remove("SNAP");
+ env.remove("DRIRC_CONFIGDIR");
+ env.remove("__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS");
+ env.remove("__EGL_VENDOR_LIBRARY_DIRS");
+ env.remove("LD_LIBRARY_PATH");
+ env.remove("LIBGL_DRIVERS_PATH");
+ env.remove("LIBVA_DRIVERS_PATH");
+ env.remove("VK_LAYER_PATH");
+ env.remove("XLOCALEDIR");
+ env.remove("GDK_PIXBUF_MODULEDIR");
+ env.remove("GDK_PIXBUF_MODULE_FILE");
+ env.remove("GTK_PATH");
+ }
+
+ if (self.container.window()) |window| {
+ // On some window protocols we might want to add specific
+ // environment variables to subprocesses, such as WINDOWID on X11.
+ try window.winproto.addSubprocessEnv(&env);
+ }
+
+ return env;
+}
+
+/// Check a GValue to see what's type its wrapping. This is equivalent to GTK's
+/// `G_VALUE_HOLDS` macro but Zig's C translator does not like it.
+fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool {
+ if (value_) |value| {
+ if (value.*.g_type == g_type) return true;
+ return c.g_type_check_value_holds(value, g_type) != 0;
+ }
+ return false;
+}
diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig
index ed0804fd3d..d320daa7c8 100644
--- a/src/apprt/gtk/Tab.zig
+++ b/src/apprt/gtk/Tab.zig
@@ -121,10 +121,63 @@ pub fn remove(self: *Tab) void {
self.window.closeTab(self);
}
-pub fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
+/// Helper function to check if any surface in the split hierarchy needs close confirmation
+fn needsConfirm(elem: Surface.Container.Elem) bool {
+ return switch (elem) {
+ .surface => |s| s.core_surface.needsConfirmQuit(),
+ .split => |s| needsConfirm(s.top_left) or needsConfirm(s.bottom_right),
+ };
+}
+
+/// Close the tab, asking for confirmation if any surface requests it.
+pub fn closeWithConfirmation(tab: *Tab) void {
+ switch (tab.elem) {
+ .surface => |s| s.close(s.core_surface.needsConfirmQuit()),
+ .split => |s| {
+ if (needsConfirm(s.top_left) or needsConfirm(s.bottom_right)) {
+ const alert = c.gtk_message_dialog_new(
+ tab.window.window,
+ c.GTK_DIALOG_MODAL,
+ c.GTK_MESSAGE_QUESTION,
+ c.GTK_BUTTONS_YES_NO,
+ "Close this tab?",
+ );
+ c.gtk_message_dialog_format_secondary_text(
+ @ptrCast(alert),
+ "All terminal sessions in this tab will be terminated.",
+ );
+
+ // We want the "yes" to appear destructive.
+ const yes_widget = c.gtk_dialog_get_widget_for_response(
+ @ptrCast(alert),
+ c.GTK_RESPONSE_YES,
+ );
+ c.gtk_widget_add_css_class(yes_widget, "destructive-action");
+
+ // We want the "no" to be the default action
+ c.gtk_dialog_set_default_response(
+ @ptrCast(alert),
+ c.GTK_RESPONSE_NO,
+ );
+
+ _ = c.g_signal_connect_data(alert, "response", c.G_CALLBACK(>kTabCloseConfirmation), tab, null, c.G_CONNECT_DEFAULT);
+ c.gtk_widget_show(alert);
+ return;
+ }
+ tab.remove();
+ },
+ }
+}
+
+fn gtkTabCloseConfirmation(
+ alert: *c.GtkMessageDialog,
+ response: c.gint,
+ ud: ?*anyopaque,
+) callconv(.C) void {
const tab: *Tab = @ptrCast(@alignCast(ud));
- const window = tab.window;
- window.closeTab(tab);
+ c.gtk_window_destroy(@ptrCast(alert));
+ if (response != c.GTK_RESPONSE_YES) return;
+ tab.remove();
}
fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
@@ -135,17 +188,3 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
const tab: *Tab = @ptrCast(@alignCast(ud));
tab.destroy(tab.window.app.core_app.alloc);
}
-
-pub fn gtkTabClick(
- gesture: *c.GtkGestureClick,
- _: c.gint,
- _: c.gdouble,
- _: c.gdouble,
- ud: ?*anyopaque,
-) callconv(.C) void {
- const self: *Tab = @ptrCast(@alignCast(ud));
- const gtk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture));
- if (gtk_button == c.GDK_BUTTON_MIDDLE) {
- self.remove();
- }
-}
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index bf80c4a1a7..ab5e54d9fa 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -22,10 +22,10 @@ const Tab = @import("Tab.zig");
const c = @import("c.zig").c;
const adwaita = @import("adwaita.zig");
const gtk_key = @import("key.zig");
-const Notebook = @import("notebook.zig").Notebook;
-const HeaderBar = @import("headerbar.zig").HeaderBar;
+const Notebook = @import("notebook.zig");
+const HeaderBar = @import("headerbar.zig");
const version = @import("version.zig");
-const wayland = @import("wayland.zig");
+const winproto = @import("winproto.zig");
const log = std.log.scoped(.gtk);
@@ -34,29 +34,26 @@ app: *App,
/// Our window
window: *c.GtkWindow,
-/// The header bar for the window. This is possibly null since it can be
-/// disabled using gtk-titlebar. This is either an AdwHeaderBar or
-/// GtkHeaderBar depending on if adw is enabled and linked.
-header: ?HeaderBar,
+/// The header bar for the window.
+headerbar: HeaderBar,
/// The tab overview for the window. This is possibly null since there is no
/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
tab_overview: ?*c.GtkWidget,
/// The notebook (tab grouping) for this window.
-/// can be either c.GtkNotebook or c.AdwTabView.
notebook: Notebook,
context_menu: *c.GtkWidget,
-/// The libadwaita widget for receiving toast send requests. If libadwaita is
-/// not used, this is null and unused.
-toast_overlay: ?*c.GtkWidget,
+/// The libadwaita widget for receiving toast send requests.
+toast_overlay: *c.GtkWidget,
/// See adwTabOverviewOpen for why we have this.
adw_tab_overview_focus_timer: ?c.guint = null,
-wayland: ?wayland.SurfaceState,
+/// State and logic for windowing protocol for a window.
+winproto: winproto.Window,
pub fn create(alloc: Allocator, app: *App) !*Window {
// Allocate a fixed pointer for our window. We try to minimize
@@ -77,46 +74,36 @@ pub fn init(self: *Window, app: *App) !void {
self.* = .{
.app = app,
.window = undefined,
- .header = null,
+ .headerbar = undefined,
.tab_overview = null,
.notebook = undefined,
.context_menu = undefined,
.toast_overlay = undefined,
- .wayland = null,
+ .winproto = .none,
};
// Create the window
- const window: *c.GtkWidget = window: {
- if ((comptime adwaita.versionAtLeast(0, 0, 0)) and adwaita.enabled(&self.app.config)) {
- const window = c.adw_application_window_new(app.app);
- c.gtk_widget_add_css_class(@ptrCast(window), "adw");
- break :window window;
- } else {
- const window = c.gtk_application_window_new(app.app);
- c.gtk_widget_add_css_class(@ptrCast(window), "gtk");
- break :window window;
- }
- };
- errdefer c.gtk_window_destroy(@ptrCast(window));
+ const gtk_widget = c.adw_application_window_new(app.app);
+ errdefer c.gtk_window_destroy(@ptrCast(gtk_widget));
- const gtk_window: *c.GtkWindow = @ptrCast(window);
- self.window = gtk_window;
- c.gtk_window_set_title(gtk_window, "Ghostty");
- c.gtk_window_set_default_size(gtk_window, 1000, 600);
- c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window");
- c.gtk_widget_add_css_class(@ptrCast(gtk_window), "terminal-window");
+ self.window = @ptrCast(@alignCast(gtk_widget));
+
+ c.gtk_window_set_title(self.window, "Ghostty");
+ c.gtk_window_set_default_size(self.window, 1000, 600);
+ c.gtk_widget_add_css_class(gtk_widget, "window");
+ c.gtk_widget_add_css_class(gtk_widget, "terminal-window");
// GTK4 grabs F10 input by default to focus the menubar icon. We want
// to disable this so that terminal programs can capture F10 (such as htop)
- c.gtk_window_set_handle_menubar_accel(gtk_window, 0);
+ c.gtk_window_set_handle_menubar_accel(self.window, 0);
- c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
+ c.gtk_window_set_icon_name(self.window, build_config.bundle_id);
// Apply class to color headerbar if window-theme is set to `ghostty` and
// GTK version is before 4.16. The conditional is because above 4.16
// we use GTK CSS color variables.
if (!version.atLeast(4, 16, 0) and app.config.@"window-theme" == .ghostty) {
- c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window-theme-ghostty");
+ c.gtk_widget_add_css_class(gtk_widget, "window-theme-ghostty");
}
// Create our box which will hold our widgets in the main content area.
@@ -126,9 +113,9 @@ pub fn init(self: *Window, app: *App) !void {
self.notebook.init();
// If we are using Adwaita, then we can support the tab overview.
- self.tab_overview = if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&self.app.config) and adwaita.versionAtLeast(1, 4, 0)) overview: {
+ self.tab_overview = if (adwaita.versionAtLeast(1, 4, 0)) overview: {
const tab_overview = c.adw_tab_overview_new();
- c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view);
+ c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.tab_view);
c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1);
_ = c.g_signal_connect_data(
tab_overview,
@@ -150,79 +137,65 @@ pub fn init(self: *Window, app: *App) !void {
break :overview tab_overview;
} else null;
- // gtk-titlebar can be used to disable the header bar (but keep
- // the window manager's decorations). We create this no matter if we
- // are decorated or not because we can have a keybind to toggle the
- // decorations.
- if (app.config.@"gtk-titlebar") {
- const header = HeaderBar.init(self);
-
- // If we are not decorated then we hide the titlebar.
- header.setVisible(app.config.@"window-decoration");
-
- {
- const btn = c.gtk_menu_button_new();
- c.gtk_widget_set_tooltip_text(btn, "Main Menu");
- c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
- c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
- header.packEnd(btn);
- }
+ // gtk-titlebar can be used to disable the header bar (but keep the window
+ // manager's decorations). We create this no matter if we are decorated or
+ // not because we can have a keybind to toggle the decorations.
+ self.headerbar.init();
- // If we're using an AdwWindow then we can support the tab overview.
- if (self.tab_overview) |tab_overview| {
- if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
- assert(self.app.config.@"gtk-adwaita" and adwaita.versionAtLeast(1, 4, 0));
- const btn = switch (app.config.@"gtk-tabs-location") {
- .top, .bottom, .left, .right => btn: {
- const btn = c.gtk_toggle_button_new();
- c.gtk_widget_set_tooltip_text(btn, "View Open Tabs");
- c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
- _ = c.g_object_bind_property(
- btn,
- "active",
- tab_overview,
- "open",
- c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE,
- );
-
- break :btn btn;
- },
-
- .hidden => btn: {
- const btn = c.adw_tab_button_new();
- c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view);
- c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
- break :btn btn;
- },
- };
-
- c.gtk_widget_set_focus_on_click(btn, c.FALSE);
- header.packEnd(btn);
- }
+ {
+ const btn = c.gtk_menu_button_new();
+ c.gtk_widget_set_tooltip_text(btn, "Main Menu");
+ c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
+ c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
+ self.headerbar.packEnd(btn);
+ }
- {
- const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
- c.gtk_widget_set_tooltip_text(btn, "New Tab");
- _ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(>kTabNewClick), self, null, c.G_CONNECT_DEFAULT);
- header.packStart(btn);
- }
+ // If we're using an AdwWindow then we can support the tab overview.
+ if (self.tab_overview) |tab_overview| {
+ if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
+ const btn = switch (app.config.@"gtk-tabs-location") {
+ .top, .bottom => btn: {
+ const btn = c.gtk_toggle_button_new();
+ c.gtk_widget_set_tooltip_text(btn, "View Open Tabs");
+ c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
+ _ = c.g_object_bind_property(
+ btn,
+ "active",
+ tab_overview,
+ "open",
+ c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE,
+ );
+
+ break :btn btn;
+ },
- self.header = header;
- }
+ .hidden => btn: {
+ const btn = c.adw_tab_button_new();
+ c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.tab_view);
+ c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
+ break :btn btn;
+ },
+ };
- _ = c.g_signal_connect_data(gtk_window, "notify::decorated", c.G_CALLBACK(>kWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT);
+ c.gtk_widget_set_focus_on_click(btn, c.FALSE);
+ self.headerbar.packEnd(btn);
+ }
- // If we are disabling decorations then disable them right away.
- if (!app.config.@"window-decoration") {
- c.gtk_window_set_decorated(gtk_window, 0);
+ {
+ const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
+ c.gtk_widget_set_tooltip_text(btn, "New Tab");
+ _ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(>kTabNewClick), self, null, c.G_CONNECT_DEFAULT);
+ self.headerbar.packStart(btn);
}
+ _ = c.g_signal_connect_data(self.window, "notify::decorated", c.G_CALLBACK(>kWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT);
+ _ = c.g_signal_connect_data(self.window, "notify::maximized", c.G_CALLBACK(>kWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT);
+ _ = c.g_signal_connect_data(self.window, "notify::fullscreened", c.G_CALLBACK(>kWindowNotifyFullscreened), self, null, c.G_CONNECT_DEFAULT);
+
// If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
// need to stick the headerbar into the content box.
- if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
- if (self.header) |h| {
- c.gtk_box_append(@ptrCast(box), h.asWidget());
- }
+ if (!adwaita.versionAtLeast(1, 4, 0)) {
+ c.gtk_box_append(@ptrCast(box), self.headerbar.asWidget());
}
// In debug we show a warning and apply the 'devel' class to the window.
@@ -230,10 +203,7 @@ pub fn init(self: *Window, app: *App) !void {
if (comptime std.debug.runtime_safety) {
const warning_box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
const warning_text = "⚠️ You're running a debug build of Ghostty! Performance will be degraded.";
- if ((comptime adwaita.versionAtLeast(1, 3, 0)) and
- adwaita.enabled(&app.config) and
- adwaita.versionAtLeast(1, 3, 0))
- {
+ if (adwaita.versionAtLeast(1, 3, 0)) {
const banner = c.adw_banner_new(warning_text);
c.adw_banner_set_revealed(@ptrCast(banner), 1);
c.gtk_box_append(@ptrCast(warning_box), @ptrCast(banner));
@@ -243,37 +213,33 @@ pub fn init(self: *Window, app: *App) !void {
c.gtk_widget_set_margin_bottom(warning, 10);
c.gtk_box_append(@ptrCast(warning_box), warning);
}
- c.gtk_widget_add_css_class(@ptrCast(gtk_window), "devel");
+ c.gtk_widget_add_css_class(gtk_widget, "devel");
c.gtk_widget_add_css_class(@ptrCast(warning_box), "background");
c.gtk_box_append(@ptrCast(box), warning_box);
}
// Setup our toast overlay if we have one
- self.toast_overlay = if (adwaita.enabled(&self.app.config)) toast: {
- const toast_overlay = c.adw_toast_overlay_new();
- c.adw_toast_overlay_set_child(
- @ptrCast(toast_overlay),
- @ptrCast(@alignCast(self.notebook.asWidget())),
- );
- c.gtk_box_append(@ptrCast(box), toast_overlay);
- break :toast toast_overlay;
- } else toast: {
- c.gtk_box_append(@ptrCast(box), self.notebook.asWidget());
- break :toast null;
- };
+ self.toast_overlay = c.adw_toast_overlay_new();
+ c.adw_toast_overlay_set_child(
+ @ptrCast(self.toast_overlay),
+ @ptrCast(@alignCast(self.notebook.asWidget())),
+ );
+ c.gtk_box_append(@ptrCast(box), self.toast_overlay);
// If we have a tab overview then we can set it on our notebook.
if (self.tab_overview) |tab_overview| {
- if (comptime !adwaita.versionAtLeast(1, 3, 0)) unreachable;
- assert(self.notebook == .adw);
- c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view);
+ if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
+ c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.tab_view);
}
self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu)));
- c.gtk_widget_set_parent(self.context_menu, window);
+ c.gtk_widget_set_parent(self.context_menu, box);
c.gtk_popover_set_has_arrow(@ptrCast(@alignCast(self.context_menu)), 0);
c.gtk_widget_set_halign(self.context_menu, c.GTK_ALIGN_START);
+ // If we want the window to be maximized, we do that here.
+ if (app.config.maximize) c.gtk_window_maximize(self.window);
+
// If we are in fullscreen mode, new windows start fullscreen.
if (app.config.fullscreen) c.gtk_window_fullscreen(self.window);
@@ -282,43 +248,39 @@ pub fn init(self: *Window, app: *App) !void {
// focused (i.e. when the libadw tab overview is shown).
const ec_key_press = c.gtk_event_controller_key_new();
errdefer c.g_object_unref(ec_key_press);
- c.gtk_widget_add_controller(window, ec_key_press);
+ c.gtk_widget_add_controller(gtk_widget, ec_key_press);
// All of our events
_ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
- _ = c.g_signal_connect_data(window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT);
- _ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT);
- _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
+ _ = c.g_signal_connect_data(self.window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT);
+ _ = c.g_signal_connect_data(self.window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT);
+ _ = c.g_signal_connect_data(self.window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT);
// Our actions for the menu
initActions(self);
- if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
+ if (adwaita.versionAtLeast(1, 4, 0)) {
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());
- if (self.header) |header| {
- const header_widget = header.asWidget();
- c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget);
- }
+ c.adw_toolbar_view_add_top_bar(toolbar_view, self.headerbar.asWidget());
if (self.app.config.@"gtk-tabs-location" != .hidden) {
const tab_bar = c.adw_tab_bar_new();
- c.adw_tab_bar_set_view(tab_bar, self.notebook.adw.tab_view);
+ c.adw_tab_bar_set_view(tab_bar, self.notebook.tab_view);
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar));
switch (self.app.config.@"gtk-tabs-location") {
- // left and right are not supported in libadwaita.
- .top, .left, .right => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget),
+ .top => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget),
.bottom => c.adw_toolbar_view_add_bottom_bar(toolbar_view, tab_bar_widget),
.hidden => unreachable,
}
}
c.adw_toolbar_view_set_content(toolbar_view, box);
- const toolbar_style: c.AdwToolbarStyle = switch (self.app.config.@"adw-toolbar-style") {
+ const toolbar_style: c.AdwToolbarStyle = switch (self.app.config.@"gtk-toolbar-style") {
.flat => c.ADW_TOOLBAR_FLAT,
.raised => c.ADW_TOOLBAR_RAISED,
.@"raised-border" => c.ADW_TOOLBAR_RAISED_BORDER,
@@ -332,56 +294,48 @@ pub fn init(self: *Window, app: *App) !void {
@ptrCast(@alignCast(toolbar_view)),
);
c.adw_application_window_set_content(
- @ptrCast(gtk_window),
+ @ptrCast(gtk_widget),
@ptrCast(@alignCast(self.tab_overview)),
);
} else tab_bar: {
- switch (self.notebook) {
- .adw => |*adw| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
- if (app.config.@"gtk-tabs-location" == .hidden) break :tab_bar;
- // In earlier adwaita versions, we need to add the tabbar manually since we do not use
- // an AdwToolbarView.
- const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?;
- c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline");
- switch (app.config.@"gtk-tabs-location") {
- .top,
- .left,
- .right,
- => c.gtk_box_prepend(
- @ptrCast(box),
- @ptrCast(@alignCast(tab_bar)),
- ),
-
- .bottom => c.gtk_box_append(
- @ptrCast(box),
- @ptrCast(@alignCast(tab_bar)),
- ),
- .hidden => unreachable,
- }
- c.adw_tab_bar_set_view(tab_bar, adw.tab_view);
-
- if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
- },
-
- .gtk => {},
+ if (app.config.@"gtk-tabs-location" == .hidden) break :tab_bar;
+ // In earlier adwaita versions, we need to add the tabbar manually since we do not use
+ // an AdwToolbarView.
+ const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?;
+ c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline");
+ switch (app.config.@"gtk-tabs-location") {
+ .top => c.gtk_box_insert_child_after(
+ @ptrCast(box),
+ @ptrCast(@alignCast(tab_bar)),
+ @ptrCast(@alignCast(self.headerbar.asWidget())),
+ ),
+ .bottom => c.gtk_box_append(
+ @ptrCast(box),
+ @ptrCast(@alignCast(tab_bar)),
+ ),
+ .hidden => unreachable,
}
+ c.adw_tab_bar_set_view(tab_bar, self.notebook.tab_view);
- // The box is our main child
- if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
- c.adw_application_window_set_content(
- @ptrCast(gtk_window),
- box,
- );
- } else {
- c.gtk_window_set_child(gtk_window, box);
- if (self.header) |h| {
- c.gtk_window_set_titlebar(gtk_window, h.asWidget());
- }
- }
+ if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
}
// Show the window
- c.gtk_widget_show(window);
+ c.gtk_widget_show(gtk_widget);
+}
+
+pub fn updateConfig(
+ self: *Window,
+ config: *const configpkg.Config,
+) !void {
+ self.winproto.updateConfigEvent(config) catch |err| {
+ // We want to continue attempting to make the other config
+ // changes necessary so we just log the error and continue.
+ log.warn("failed to update window protocol config error={}", .{err});
+ };
+
+ // We always resync our appearance whenever the config changes.
+ try self.syncAppearance(config);
}
/// Updates appearance based on config settings. Will be called once upon window
@@ -390,19 +344,48 @@ pub fn init(self: *Window, app: *App) !void {
/// TODO: Many of the initial style settings in `create` could possibly be made
/// reactive by moving them here.
pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void {
- if (config.@"background-opacity" < 1) {
- c.gtk_widget_remove_css_class(@ptrCast(self.window), "background");
- } else {
- c.gtk_widget_add_css_class(@ptrCast(self.window), "background");
+ self.winproto.syncAppearance() catch |err| {
+ log.warn("failed to sync winproto appearance error={}", .{err});
+ };
+
+ toggleCssClass(
+ @ptrCast(self.window),
+ "background",
+ config.@"background-opacity" >= 1,
+ );
+
+ // If we are disabling CSDs then disable them right away.
+ const csd_enabled = self.winproto.clientSideDecorationEnabled();
+ c.gtk_window_set_decorated(self.window, @intFromBool(csd_enabled));
+
+ // If we are not decorated then we hide the titlebar.
+ self.headerbar.setVisible(config.@"gtk-titlebar" and csd_enabled);
+
+ // Disable the title buttons (close, maximize, minimize, ...)
+ // *inside* the tab overview if CSDs are disabled.
+ // We do spare the search button, though.
+ if (self.tab_overview) |tab_overview| {
+ if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
+ c.adw_tab_overview_set_show_start_title_buttons(
+ @ptrCast(tab_overview),
+ @intFromBool(csd_enabled),
+ );
+ c.adw_tab_overview_set_show_end_title_buttons(
+ @ptrCast(tab_overview),
+ @intFromBool(csd_enabled),
+ );
}
+}
- if (self.wayland) |*wl| {
- const blurred = switch (config.@"background-blur-radius") {
- .false => false,
- .true => true,
- .radius => |v| v > 0,
- };
- try wl.setBlur(blurred);
+fn toggleCssClass(
+ widget: *c.GtkWidget,
+ class: [:0]const u8,
+ v: bool,
+) void {
+ if (v) {
+ c.gtk_widget_add_css_class(widget, class);
+ } else {
+ c.gtk_widget_remove_css_class(widget, class);
}
}
@@ -443,13 +426,23 @@ fn initActions(self: *Window) void {
pub fn deinit(self: *Window) void {
c.gtk_widget_unparent(@ptrCast(self.context_menu));
- if (self.wayland) |*wl| wl.deinit();
+ self.winproto.deinit(self.app.core_app.alloc);
if (self.adw_tab_overview_focus_timer) |timer| {
_ = c.g_source_remove(timer);
}
}
+/// Set the title of the window.
+pub fn setTitle(self: *Window, title: [:0]const u8) void {
+ self.headerbar.setTitle(title);
+}
+
+/// Set the subtitle of the window if it has one.
+pub fn setSubtitle(self: *Window, subtitle: [:0]const u8) void {
+ self.headerbar.setSubtitle(subtitle);
+}
+
/// Add a new tab to this window.
pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
const alloc = self.app.core_app.alloc;
@@ -467,23 +460,25 @@ pub fn closeTab(self: *Window, tab: *Tab) void {
}
/// Go to the previous tab for a surface.
-pub fn gotoPreviousTab(self: *Window, surface: *Surface) void {
+pub fn gotoPreviousTab(self: *Window, surface: *Surface) bool {
const tab = surface.container.tab() orelse {
log.info("surface is not attached to a tab bar, cannot navigate", .{});
- return;
+ return false;
};
- self.notebook.gotoPreviousTab(tab);
+ if (!self.notebook.gotoPreviousTab(tab)) return false;
self.focusCurrentTab();
+ return true;
}
/// Go to the next tab for a surface.
-pub fn gotoNextTab(self: *Window, surface: *Surface) void {
+pub fn gotoNextTab(self: *Window, surface: *Surface) bool {
const tab = surface.container.tab() orelse {
log.info("surface is not attached to a tab bar, cannot navigate", .{});
- return;
+ return false;
};
- self.notebook.gotoNextTab(tab);
+ if (!self.notebook.gotoNextTab(tab)) return false;
self.focusCurrentTab();
+ return true;
}
/// Move the current tab for a surface.
@@ -495,31 +490,41 @@ pub fn moveTab(self: *Window, surface: *Surface, position: c_int) void {
self.notebook.moveTab(tab, position);
}
-/// Go to the next tab for a surface.
-pub fn gotoLastTab(self: *Window) void {
- const max = self.notebook.nPages() -| 1;
- self.gotoTab(@intCast(max));
+/// Go to the last tab for a surface.
+pub fn gotoLastTab(self: *Window) bool {
+ const max = self.notebook.nPages();
+ return self.gotoTab(@intCast(max));
}
/// Go to the specific tab index.
-pub fn gotoTab(self: *Window, n: usize) void {
- if (n == 0) return;
+pub fn gotoTab(self: *Window, n: usize) bool {
+ if (n == 0) return false;
const max = self.notebook.nPages();
- if (max == 0) return;
- const page_idx = std.math.cast(c_int, n - 1) orelse return;
- self.notebook.gotoNthTab(@min(page_idx, max - 1));
+ if (max == 0) return false;
+ const page_idx = std.math.cast(c_int, n - 1) orelse return false;
+ if (!self.notebook.gotoNthTab(@min(page_idx, max - 1))) return false;
self.focusCurrentTab();
+ return true;
}
/// Toggle tab overview (if present)
pub fn toggleTabOverview(self: *Window) void {
if (self.tab_overview) |tab_overview_widget| {
- if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
+ if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(tab_overview_widget));
c.adw_tab_overview_set_open(tab_overview, 1 - c.adw_tab_overview_get_open(tab_overview));
}
}
+/// Toggle the maximized state for this window.
+pub fn toggleMaximize(self: *Window) void {
+ if (c.gtk_window_is_maximized(self.window) == 0) {
+ c.gtk_window_maximize(self.window);
+ } else {
+ c.gtk_window_unmaximize(self.window);
+ }
+}
+
/// Toggle fullscreen for this window.
pub fn toggleFullscreen(self: *Window) void {
const is_fullscreen = c.gtk_window_is_fullscreen(self.window);
@@ -532,17 +537,11 @@ pub fn toggleFullscreen(self: *Window) void {
/// Toggle the window decorations for this window.
pub fn toggleWindowDecorations(self: *Window) void {
- const old_decorated = c.gtk_window_get_decorated(self.window) == 1;
- const new_decorated = !old_decorated;
- c.gtk_window_set_decorated(self.window, @intFromBool(new_decorated));
-
- // If we have a titlebar, then we also show/hide it depending on the
- // decorated state. GTK tends to consider the titlebar part of the frame
- // and hides it with decorations, but libadwaita doesn't. This makes it
- // explicit.
- if (self.header) |headerbar| {
- headerbar.setVisible(new_decorated);
- }
+ self.app.config.@"window-decoration" = switch (self.app.config.@"window-decoration") {
+ .auto, .client, .server => .none,
+ .none => .client,
+ };
+ self.updateConfig(&self.app.config) catch {};
}
/// Grabs focus on the currently selected tab.
@@ -558,20 +557,27 @@ pub fn onConfigReloaded(self: *Window) void {
}
pub fn sendToast(self: *Window, title: [:0]const u8) void {
- if (comptime !adwaita.versionAtLeast(0, 0, 0)) return;
- const toast_overlay = self.toast_overlay orelse return;
const toast = c.adw_toast_new(title);
c.adw_toast_set_timeout(toast, 3);
- c.adw_toast_overlay_add_toast(@ptrCast(toast_overlay), toast);
+ c.adw_toast_overlay_add_toast(@ptrCast(self.toast_overlay), toast);
}
fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
const self = userdataSelf(ud.?);
- if (self.app.wayland) |*wl| {
- self.wayland = wayland.SurfaceState.init(v, wl);
+ // Initialize our window protocol logic
+ if (winproto.Window.init(
+ self.app.core_app.alloc,
+ &self.app.winproto,
+ v,
+ &self.app.config,
+ )) |winproto_win| {
+ self.winproto = winproto_win;
+ } else |err| {
+ log.warn("failed to initialize window protocol error={}", .{err});
}
+ // When we are realized we always setup our appearance
self.syncAppearance(&self.app.config) catch |err| {
log.err("failed to initialize appearance={}", .{err});
};
@@ -579,22 +585,67 @@ fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
return true;
}
+fn gtkWindowNotifyMaximized(
+ _: *c.GObject,
+ _: *c.GParamSpec,
+ ud: ?*anyopaque,
+) callconv(.C) void {
+ const self = userdataSelf(ud orelse return);
+
+ // Only toggle visibility of the header bar when we're using CSDs,
+ // and actually intend on displaying the header bar
+ if (!self.winproto.clientSideDecorationEnabled()) return;
+
+ // If we aren't maximized, we should show the headerbar again
+ // if it was originally visible.
+ const maximized = c.gtk_window_is_maximized(self.window) != 0;
+ if (!maximized) {
+ self.headerbar.setVisible(self.app.config.@"gtk-titlebar");
+ return;
+ }
+
+ // If we are maximized, we should hide the headerbar if requested.
+ if (self.app.config.@"gtk-titlebar-hide-when-maximized") {
+ self.headerbar.setVisible(false);
+ }
+}
+
fn gtkWindowNotifyDecorated(
object: *c.GObject,
_: *c.GParamSpec,
- _: ?*anyopaque,
+ ud: ?*anyopaque,
) callconv(.C) void {
- if (c.gtk_window_get_decorated(@ptrCast(object)) == 1) {
- c.gtk_widget_remove_css_class(@ptrCast(object), "ssd");
- c.gtk_widget_remove_css_class(@ptrCast(object), "no-border-radius");
- } else {
- // Fix any artifacting that may occur in window corners. The .ssd CSS
- // class is defined in the GtkWindow documentation:
- // https://docs.gtk.org/gtk4/class.Window.html#css-nodes. A definition
- // for .ssd is provided by GTK and Adwaita.
- c.gtk_widget_add_css_class(@ptrCast(object), "ssd");
- c.gtk_widget_add_css_class(@ptrCast(object), "no-border-radius");
+ const self = userdataSelf(ud orelse return);
+ const is_decorated = c.gtk_window_get_decorated(@ptrCast(object)) == 1;
+
+ // Fix any artifacting that may occur in window corners. The .ssd CSS
+ // class is defined in the GtkWindow documentation:
+ // https://docs.gtk.org/gtk4/class.Window.html#css-nodes. A definition
+ // for .ssd is provided by GTK and Adwaita.
+ toggleCssClass(@ptrCast(object), "csd", is_decorated);
+ toggleCssClass(@ptrCast(object), "ssd", !is_decorated);
+ toggleCssClass(@ptrCast(object), "no-border-radius", !is_decorated);
+
+ // FIXME: This is to update the blur region offset on X11.
+ // Remove this when we move everything related to window appearance
+ // to `syncAppearance` for Ghostty 1.2.
+ self.winproto.syncAppearance() catch {};
+}
+
+fn gtkWindowNotifyFullscreened(
+ object: *c.GObject,
+ _: *c.GParamSpec,
+ ud: ?*anyopaque,
+) callconv(.C) void {
+ const self = userdataSelf(ud orelse return);
+ const fullscreened = c.gtk_window_is_fullscreen(@ptrCast(object)) != 0;
+ if (!fullscreened) {
+ const csd_enabled = self.winproto.clientSideDecorationEnabled();
+ self.headerbar.setVisible(self.app.config.@"gtk-titlebar" and csd_enabled);
+ return;
}
+
+ self.headerbar.setVisible(false);
}
// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
@@ -612,12 +663,12 @@ fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
/// because we need to return an AdwTabPage from this function.
fn gtkNewTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.AdwTabPage {
const self: *Window = userdataSelf(ud.?);
- assert((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config));
+ assert(adwaita.versionAtLeast(1, 4, 0));
const alloc = self.app.core_app.alloc;
const surface = self.actionSurface();
const tab = Tab.create(alloc, self, surface) catch return null;
- return c.adw_tab_view_get_page(self.notebook.adw.tab_view, @ptrCast(@alignCast(tab.box)));
+ return c.adw_tab_view_get_page(self.notebook.tab_view, @ptrCast(@alignCast(tab.box)));
}
fn adwTabOverviewOpen(
@@ -764,7 +815,7 @@ fn gtkKeyPressed(
//
// If someone can confidently show or explain that this is not
// necessary, please remove this check.
- if (comptime adwaita.versionAtLeast(1, 4, 0)) {
+ if (adwaita.versionAtLeast(1, 4, 0)) {
if (self.tab_overview) |tab_overview_widget| {
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(tab_overview_widget));
if (c.adw_tab_overview_get_open(tab_overview) == 0) return 0;
@@ -792,10 +843,7 @@ fn gtkActionAbout(
const icon = "com.mitchellh.ghostty";
const website = "https://ghostty.org";
- if ((comptime adwaita.versionAtLeast(1, 5, 0)) and
- adwaita.versionAtLeast(1, 5, 0) and
- adwaita.enabled(&self.app.config))
- {
+ if (adwaita.versionAtLeast(1, 5, 0)) {
c.adw_show_about_dialog(
@ptrCast(self.window),
"application-name",
diff --git a/src/apprt/gtk/adwaita.zig b/src/apprt/gtk/adwaita.zig
index 0750555867..885627fa47 100644
--- a/src/apprt/gtk/adwaita.zig
+++ b/src/apprt/gtk/adwaita.zig
@@ -1,20 +1,5 @@
const std = @import("std");
const c = @import("c.zig").c;
-const build_options = @import("build_options");
-const Config = @import("../../config.zig").Config;
-
-/// Returns true if Ghostty is configured to build with libadwaita and
-/// the configuration has enabled adwaita.
-///
-/// For a comptime version of this function, use `versionAtLeast` in
-/// a comptime context with all the version numbers set to 0.
-///
-/// This must be `inline` so that the comptime check noops conditional
-/// paths that are not enabled.
-pub inline fn enabled(config: *const Config) bool {
- return build_options.adwaita and
- config.@"gtk-adwaita";
-}
/// Verifies that the running libadwaita version is at least the given
/// version. This will return false if Ghostty is configured to
@@ -33,8 +18,6 @@ pub inline fn versionAtLeast(
comptime minor: u16,
comptime micro: u16,
) bool {
- if (comptime !build_options.adwaita) return false;
-
// If our header has lower versions than the given version,
// we can return false immediately. This prevents us from
// compiling against unknown symbols and makes runtime checks
diff --git a/src/apprt/gtk/builder_check.zig b/src/apprt/gtk/builder_check.zig
new file mode 100644
index 0000000000..015c6310d7
--- /dev/null
+++ b/src/apprt/gtk/builder_check.zig
@@ -0,0 +1,32 @@
+const std = @import("std");
+const build_options = @import("build_options");
+
+const gtk = @import("gtk");
+const adw = @import("adw");
+
+pub fn main() !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ const alloc = gpa.allocator();
+
+ const filename = filename: {
+ var it = try std.process.argsWithAllocator(alloc);
+ defer it.deinit();
+
+ _ = it.next() orelse return error.NoFilename;
+ break :filename try alloc.dupeZ(u8, it.next() orelse return error.NoFilename);
+ };
+ defer alloc.free(filename);
+
+ const data = try std.fs.cwd().readFileAllocOptions(alloc, filename, std.math.maxInt(u16), null, 1, 0);
+ defer alloc.free(data);
+
+ if (gtk.initCheck() == 0) {
+ std.debug.print("{s}: skipping builder check because we can't connect to display!\n", .{filename});
+ return;
+ }
+
+ adw.init();
+
+ const builder = gtk.Builder.newFromString(data.ptr, @intCast(data.len));
+ defer builder.unref();
+}
diff --git a/src/apprt/gtk/c.zig b/src/apprt/gtk/c.zig
index dde99c78ee..c42c35d465 100644
--- a/src/apprt/gtk/c.zig
+++ b/src/apprt/gtk/c.zig
@@ -3,14 +3,14 @@ const build_options = @import("build_options");
/// Imported C API directly from header files
pub const c = @cImport({
@cInclude("gtk/gtk.h");
- if (build_options.adwaita) {
- @cInclude("libadwaita-1/adwaita.h");
- }
+ @cInclude("adwaita.h");
if (build_options.x11) {
// Add in X11-specific GDK backend which we use for specific things
// (e.g. X11 window class).
@cInclude("gdk/x11/gdkx.h");
+ @cInclude("X11/Xlib.h");
+ @cInclude("X11/Xatom.h");
// Xkb for X11 state handling
@cInclude("X11/XKBlib.h");
}
diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig
index 327680993d..050605b00f 100644
--- a/src/apprt/gtk/gresource.zig
+++ b/src/apprt/gtk/gresource.zig
@@ -53,29 +53,33 @@ const icons = [_]struct {
},
};
-pub const gresource_xml = comptimeGenerateGResourceXML();
+pub const ui_files = [_][]const u8{};
+pub const blueprint_files = [_][]const u8{};
-fn comptimeGenerateGResourceXML() []const u8 {
- comptime {
- @setEvalBranchQuota(13000);
- var counter = std.io.countingWriter(std.io.null_writer);
- try writeGResourceXML(&counter.writer());
+pub fn main() !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ const alloc = gpa.allocator();
- var buf: [counter.bytes_written]u8 = undefined;
- var stream = std.io.fixedBufferStream(&buf);
- try writeGResourceXML(stream.writer());
- const final = buf;
- return final[0..stream.getWritten().len];
+ var extra_ui_files = std.ArrayList([]const u8).init(alloc);
+ defer {
+ for (extra_ui_files.items) |item| alloc.free(item);
+ extra_ui_files.deinit();
+ }
+
+ var it = try std.process.argsWithAllocator(alloc);
+ defer it.deinit();
+
+ while (it.next()) |filename| {
+ if (std.mem.eql(u8, std.fs.path.extension(filename), ".ui")) {
+ try extra_ui_files.append(try alloc.dupe(u8, filename));
+ }
}
-}
-fn writeGResourceXML(writer: anytype) !void {
+ const writer = std.io.getStdOut().writer();
+
try writer.writeAll(
\\
\\
- \\
- );
- try writer.writeAll(
\\
\\
);
@@ -87,9 +91,6 @@ fn writeGResourceXML(writer: anytype) !void {
}
try writer.writeAll(
\\
- \\
- );
- try writer.writeAll(
\\
\\
);
@@ -99,6 +100,23 @@ fn writeGResourceXML(writer: anytype) !void {
.{ icon.alias, icon.source },
);
}
+ try writer.writeAll(
+ \\
+ \\
+ \\
+ );
+ for (ui_files) |ui_file| {
+ try writer.print(
+ " src/apprt/gtk/ui/{0s}.ui\n",
+ .{ui_file},
+ );
+ }
+ for (extra_ui_files.items) |ui_file| {
+ try writer.print(
+ " {s}\n",
+ .{ std.fs.path.basename(ui_file), ui_file },
+ );
+ }
try writer.writeAll(
\\
\\
@@ -107,12 +125,24 @@ fn writeGResourceXML(writer: anytype) !void {
}
pub const dependencies = deps: {
- var deps: [css_files.len + icons.len][]const u8 = undefined;
- for (css_files, 0..) |css_file, i| {
- deps[i] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
+ const total = css_files.len + icons.len + ui_files.len + blueprint_files.len;
+ var deps: [total][]const u8 = undefined;
+ var index: usize = 0;
+ for (css_files) |css_file| {
+ deps[index] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
+ index += 1;
+ }
+ for (icons) |icon| {
+ deps[index] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
+ index += 1;
+ }
+ for (ui_files) |ui_file| {
+ deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.ui", .{ui_file});
+ index += 1;
}
- for (icons, css_files.len..) |icon, i| {
- deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
+ for (blueprint_files) |blueprint_file| {
+ deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.blp", .{blueprint_file});
+ index += 1;
}
break :deps deps;
};
diff --git a/src/apprt/gtk/headerbar.zig b/src/apprt/gtk/headerbar.zig
index 5bb92aca2e..8f4c58fc4b 100644
--- a/src/apprt/gtk/headerbar.zig
+++ b/src/apprt/gtk/headerbar.zig
@@ -1,73 +1,59 @@
+const HeaderBar = @This();
+
const std = @import("std");
const c = @import("c.zig").c;
const Window = @import("Window.zig");
-const adwaita = @import("adwaita.zig");
-
-const AdwHeaderBar = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwHeaderBar else void;
-
-pub const HeaderBar = union(enum) {
- adw: *AdwHeaderBar,
- gtk: *c.GtkHeaderBar,
-
- pub fn init(window: *Window) HeaderBar {
- if ((comptime adwaita.versionAtLeast(1, 4, 0)) and
- adwaita.enabled(&window.app.config))
- {
- return initAdw();
- }
-
- return initGtk();
- }
-
- fn initAdw() HeaderBar {
- const headerbar = c.adw_header_bar_new();
- return .{ .adw = @ptrCast(headerbar) };
- }
-
- fn initGtk() HeaderBar {
- const headerbar = c.gtk_header_bar_new();
- return .{ .gtk = @ptrCast(headerbar) };
- }
-
- pub fn setVisible(self: HeaderBar, visible: bool) void {
- c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
- }
-
- pub fn asWidget(self: HeaderBar) *c.GtkWidget {
- return switch (self) {
- .adw => |headerbar| @ptrCast(@alignCast(headerbar)),
- .gtk => |headerbar| @ptrCast(@alignCast(headerbar)),
- };
- }
-
- pub fn packEnd(self: HeaderBar, widget: *c.GtkWidget) void {
- switch (self) {
- .adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
- c.adw_header_bar_pack_end(
- @ptrCast(@alignCast(headerbar)),
- widget,
- );
- },
- .gtk => |headerbar| c.gtk_header_bar_pack_end(
- @ptrCast(@alignCast(headerbar)),
- widget,
- ),
- }
- }
- pub fn packStart(self: HeaderBar, widget: *c.GtkWidget) void {
- switch (self) {
- .adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
- c.adw_header_bar_pack_start(
- @ptrCast(@alignCast(headerbar)),
- widget,
- );
- },
- .gtk => |headerbar| c.gtk_header_bar_pack_start(
- @ptrCast(@alignCast(headerbar)),
- widget,
- ),
- }
- }
-};
+/// the Adwaita headerbar widget
+headerbar: *c.AdwHeaderBar,
+
+/// the Adwaita window title widget
+title: *c.AdwWindowTitle,
+
+pub fn init(self: *HeaderBar) void {
+ const window: *Window = @fieldParentPtr("headerbar", self);
+ self.* = .{
+ .headerbar = @ptrCast(@alignCast(c.adw_header_bar_new())),
+ .title = @ptrCast(@alignCast(c.adw_window_title_new(
+ c.gtk_window_get_title(window.window) orelse "Ghostty",
+ null,
+ ))),
+ };
+ c.adw_header_bar_set_title_widget(
+ self.headerbar,
+ @ptrCast(@alignCast(self.title)),
+ );
+}
+
+pub fn setVisible(self: *const HeaderBar, visible: bool) void {
+ c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
+}
+
+pub fn asWidget(self: *const HeaderBar) *c.GtkWidget {
+ return @ptrCast(@alignCast(self.headerbar));
+}
+
+pub fn packEnd(self: *const HeaderBar, widget: *c.GtkWidget) void {
+ c.adw_header_bar_pack_end(
+ @ptrCast(@alignCast(self.headerbar)),
+ widget,
+ );
+}
+
+pub fn packStart(self: *const HeaderBar, widget: *c.GtkWidget) void {
+ c.adw_header_bar_pack_start(
+ @ptrCast(@alignCast(self.headerbar)),
+ widget,
+ );
+}
+
+pub fn setTitle(self: *const HeaderBar, title: [:0]const u8) void {
+ const window: *const Window = @fieldParentPtr("headerbar", self);
+ c.gtk_window_set_title(window.window, title);
+ c.adw_window_title_set_title(self.title, title);
+}
+
+pub fn setSubtitle(self: *const HeaderBar, subtitle: [:0]const u8) void {
+ c.adw_window_title_set_subtitle(self.title, subtitle);
+}
diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig
index 311bff0da4..60f12edca0 100644
--- a/src/apprt/gtk/key.zig
+++ b/src/apprt/gtk/key.zig
@@ -2,7 +2,7 @@ const std = @import("std");
const build_options = @import("build_options");
const input = @import("../../input.zig");
const c = @import("c.zig").c;
-const x11 = @import("x11.zig");
+const winproto = @import("winproto.zig");
/// Returns a GTK accelerator string from a trigger.
pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
@@ -105,45 +105,66 @@ pub fn keyvalUnicodeUnshifted(
/// This requires a lot of context because the GdkEvent
/// doesn't contain enough on its own.
pub fn eventMods(
- widget: *c.GtkWidget,
event: *c.GdkEvent,
physical_key: input.Key,
gtk_mods: c.GdkModifierType,
- x11_xkb: ?*x11.Xkb,
+ action: input.Action,
+ app_winproto: *winproto.App,
) input.Mods {
const device = c.gdk_event_get_device(event);
- var mods = mods: {
- // Add any modifier state events from Xkb if we have them (X11
- // only). Null back from the Xkb call means there was no modifier
- // event to read. This likely means that the key event did not
- // result in a modifier change and we can safely rely on the GDK
- // state.
- if (comptime build_options.x11) {
- const display = c.gtk_widget_get_display(widget);
- if (x11_xkb) |xkb| {
- if (xkb.modifier_state_from_notify(display)) |x11_mods| break :mods x11_mods;
- break :mods translateMods(gtk_mods);
- }
- }
-
- // On Wayland, we have to use the GDK device because the mods sent
- // to this event do not have the modifier key applied if it was
- // pressed (i.e. left control).
- break :mods translateMods(c.gdk_device_get_modifier_state(device));
- };
-
+ var mods = app_winproto.eventMods(device, gtk_mods);
mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1;
+ // We use the physical key to determine sided modifiers. As
+ // far as I can tell there's no other way to reliably determine
+ // this.
+ //
+ // We also set the main modifier to true if either side is true,
+ // since on both X11/Wayland, GTK doesn't set the main modifier
+ // if only the modifier key is pressed, but our core logic
+ // relies on it.
switch (physical_key) {
- .left_shift => mods.sides.shift = .left,
- .right_shift => mods.sides.shift = .right,
- .left_control => mods.sides.ctrl = .left,
- .right_control => mods.sides.ctrl = .right,
- .left_alt => mods.sides.alt = .left,
- .right_alt => mods.sides.alt = .right,
- .left_super => mods.sides.super = .left,
- .right_super => mods.sides.super = .right,
+ .left_shift => {
+ mods.shift = action != .release;
+ mods.sides.shift = .left;
+ },
+
+ .right_shift => {
+ mods.shift = action != .release;
+ mods.sides.shift = .right;
+ },
+
+ .left_control => {
+ mods.ctrl = action != .release;
+ mods.sides.ctrl = .left;
+ },
+
+ .right_control => {
+ mods.ctrl = action != .release;
+ mods.sides.ctrl = .right;
+ },
+
+ .left_alt => {
+ mods.alt = action != .release;
+ mods.sides.alt = .left;
+ },
+
+ .right_alt => {
+ mods.alt = action != .release;
+ mods.sides.alt = .right;
+ },
+
+ .left_super => {
+ mods.super = action != .release;
+ mods.sides.super = .left;
+ },
+
+ .right_super => {
+ mods.super = action != .release;
+ mods.sides.super = .right;
+ },
+
else => {},
}
diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig
index 4676c2529f..e411ba9ad9 100644
--- a/src/apprt/gtk/notebook.zig
+++ b/src/apprt/gtk/notebook.zig
@@ -1,164 +1,193 @@
+/// An abstraction over the GTK notebook and Adwaita tab view to manage
+/// all the terminal tabs in a window.
+const Notebook = @This();
+
const std = @import("std");
const assert = std.debug.assert;
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const Tab = @import("Tab.zig");
-const NotebookAdw = @import("notebook_adw.zig").NotebookAdw;
-const NotebookGtk = @import("notebook_gtk.zig").NotebookGtk;
const adwaita = @import("adwaita.zig");
const log = std.log.scoped(.gtk);
-const AdwTabView = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabView else anyopaque;
+/// the tab view
+tab_view: *c.AdwTabView,
-/// An abstraction over the GTK notebook and Adwaita tab view to manage
-/// all the terminal tabs in a window.
-/// An abstraction over the GTK notebook and Adwaita tab view to manage
-/// all the terminal tabs in a window.
-pub const Notebook = union(enum) {
- adw: NotebookAdw,
- gtk: NotebookGtk,
+/// Set to true so that the adw close-page handler knows we're forcing
+/// and to allow a close to happen with no confirm. This is a bit of a hack
+/// because we currently use GTK alerts to confirm tab close and they
+/// don't carry with them the ADW state that we are confirming or not.
+/// Long term we should move to ADW alerts so we can know if we are
+/// confirming or not.
+forcing_close: bool = false,
- pub fn init(self: *Notebook) void {
- const window: *Window = @fieldParentPtr("notebook", self);
- const app = window.app;
- if (adwaita.enabled(&app.config)) return NotebookAdw.init(self);
+pub fn init(self: *Notebook) void {
+ const window: *Window = @fieldParentPtr("notebook", self);
- return NotebookGtk.init(self);
- }
+ const tab_view: *c.AdwTabView = c.adw_tab_view_new() orelse unreachable;
+ c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_view)), "notebook");
- pub fn asWidget(self: *Notebook) *c.GtkWidget {
- return switch (self.*) {
- .adw => |*adw| adw.asWidget(),
- .gtk => |*gtk| gtk.asWidget(),
- };
+ if (adwaita.versionAtLeast(1, 2, 0)) {
+ // Adwaita enables all of the shortcuts by default.
+ // We want to manage keybindings ourselves.
+ c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS);
}
- pub fn nPages(self: *Notebook) c_int {
- return switch (self.*) {
- .adw => |*adw| adw.nPages(),
- .gtk => |*gtk| gtk.nPages(),
- };
- }
+ self.* = .{
+ .tab_view = tab_view,
+ };
- /// Returns the index of the currently selected page.
- /// Returns null if the notebook has no pages.
- fn currentPage(self: *Notebook) ?c_int {
- return switch (self.*) {
- .adw => |*adw| adw.currentPage(),
- .gtk => |*gtk| gtk.currentPage(),
- };
- }
+ _ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
+ _ = c.g_signal_connect_data(tab_view, "close-page", c.G_CALLBACK(&adwClosePage), window, null, c.G_CONNECT_DEFAULT);
+ _ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
+ _ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT);
+}
- /// Returns the currently selected tab or null if there are none.
- pub fn currentTab(self: *Notebook) ?*Tab {
- return switch (self.*) {
- .adw => |*adw| adw.currentTab(),
- .gtk => |*gtk| gtk.currentTab(),
- };
- }
+pub fn asWidget(self: *Notebook) *c.GtkWidget {
+ return @ptrCast(@alignCast(self.tab_view));
+}
- pub fn gotoNthTab(self: *Notebook, position: c_int) void {
- switch (self.*) {
- .adw => |*adw| adw.gotoNthTab(position),
- .gtk => |*gtk| gtk.gotoNthTab(position),
- }
- }
+pub fn nPages(self: *Notebook) c_int {
+ return c.adw_tab_view_get_n_pages(self.tab_view);
+}
- pub fn getTabPosition(self: *Notebook, tab: *Tab) ?c_int {
- return switch (self.*) {
- .adw => |*adw| adw.getTabPosition(tab),
- .gtk => |*gtk| gtk.getTabPosition(tab),
- };
- }
+/// Returns the index of the currently selected page.
+/// Returns null if the notebook has no pages.
+fn currentPage(self: *Notebook) ?c_int {
+ const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
+ return c.adw_tab_view_get_page_position(self.tab_view, page);
+}
- pub fn gotoPreviousTab(self: *Notebook, tab: *Tab) void {
- const page_idx = self.getTabPosition(tab) orelse return;
+/// Returns the currently selected tab or null if there are none.
+pub fn currentTab(self: *Notebook) ?*Tab {
+ const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
+ const child = c.adw_tab_page_get_child(page);
+ return @ptrCast(@alignCast(
+ c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null,
+ ));
+}
- // The next index is the previous or we wrap around.
- const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
- const max = self.nPages();
- break :next_idx max -| 1;
- };
+pub fn gotoNthTab(self: *Notebook, position: c_int) bool {
+ const page_to_select = c.adw_tab_view_get_nth_page(self.tab_view, position) orelse return false;
+ c.adw_tab_view_set_selected_page(self.tab_view, page_to_select);
+ return true;
+}
- // Do nothing if we have one tab
- if (next_idx == page_idx) return;
+pub fn getTabPosition(self: *Notebook, tab: *Tab) ?c_int {
+ const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return null;
+ return c.adw_tab_view_get_page_position(self.tab_view, page);
+}
- self.gotoNthTab(next_idx);
- }
+pub fn gotoPreviousTab(self: *Notebook, tab: *Tab) bool {
+ const page_idx = self.getTabPosition(tab) orelse return false;
- pub fn gotoNextTab(self: *Notebook, tab: *Tab) void {
- const page_idx = self.getTabPosition(tab) orelse return;
+ // The next index is the previous or we wrap around.
+ const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
+ const max = self.nPages();
+ break :next_idx max -| 1;
+ };
- const max = self.nPages() -| 1;
- const next_idx = if (page_idx < max) page_idx + 1 else 0;
- if (next_idx == page_idx) return;
+ // Do nothing if we have one tab
+ if (next_idx == page_idx) return false;
- self.gotoNthTab(next_idx);
- }
+ return self.gotoNthTab(next_idx);
+}
- pub fn moveTab(self: *Notebook, tab: *Tab, position: c_int) void {
- const page_idx = self.getTabPosition(tab) orelse return;
+pub fn gotoNextTab(self: *Notebook, tab: *Tab) bool {
+ const page_idx = self.getTabPosition(tab) orelse return false;
- const max = self.nPages() -| 1;
- var new_position: c_int = page_idx + position;
+ const max = self.nPages() -| 1;
+ const next_idx = if (page_idx < max) page_idx + 1 else 0;
+ if (next_idx == page_idx) return false;
- if (new_position < 0) {
- new_position = max + new_position + 1;
- } else if (new_position > max) {
- new_position = new_position - max - 1;
- }
+ return self.gotoNthTab(next_idx);
+}
- if (new_position == page_idx) return;
- self.reorderPage(tab, new_position);
- }
+pub fn moveTab(self: *Notebook, tab: *Tab, position: c_int) void {
+ const page_idx = self.getTabPosition(tab) orelse return;
- pub fn reorderPage(self: *Notebook, tab: *Tab, position: c_int) void {
- switch (self.*) {
- .adw => |*adw| adw.reorderPage(tab, position),
- .gtk => |*gtk| gtk.reorderPage(tab, position),
- }
- }
+ const max = self.nPages() -| 1;
+ var new_position: c_int = page_idx + position;
- pub fn setTabLabel(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
- switch (self.*) {
- .adw => |*adw| adw.setTabLabel(tab, title),
- .gtk => |*gtk| gtk.setTabLabel(tab, title),
- }
+ if (new_position < 0) {
+ new_position = max + new_position + 1;
+ } else if (new_position > max) {
+ new_position = new_position - max - 1;
}
- pub fn setTabTooltip(self: *Notebook, tab: *Tab, tooltip: [:0]const u8) void {
- switch (self.*) {
- .adw => |*adw| adw.setTabTooltip(tab, tooltip),
- .gtk => |*gtk| gtk.setTabTooltip(tab, tooltip),
- }
- }
+ if (new_position == page_idx) return;
+ self.reorderPage(tab, new_position);
+}
- fn newTabInsertPosition(self: *Notebook, tab: *Tab) c_int {
- const numPages = self.nPages();
- return switch (tab.window.app.config.@"window-new-tab-position") {
- .current => if (self.currentPage()) |page| page + 1 else numPages,
- .end => numPages,
- };
- }
+pub fn reorderPage(self: *Notebook, tab: *Tab, position: c_int) void {
+ const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
+ _ = c.adw_tab_view_reorder_page(self.tab_view, page, position);
+}
- /// Adds a new tab with the given title to the notebook.
- pub fn addTab(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
- const position = self.newTabInsertPosition(tab);
- switch (self.*) {
- .adw => |*adw| adw.addTab(tab, position, title),
- .gtk => |*gtk| gtk.addTab(tab, position, title),
- }
- }
+pub fn setTabLabel(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
+ const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
+ c.adw_tab_page_set_title(page, title.ptr);
+}
+
+pub fn setTabTooltip(self: *Notebook, tab: *Tab, tooltip: [:0]const u8) void {
+ const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
+ c.adw_tab_page_set_tooltip(page, tooltip.ptr);
+}
+
+fn newTabInsertPosition(self: *Notebook, tab: *Tab) c_int {
+ const numPages = self.nPages();
+ return switch (tab.window.app.config.@"window-new-tab-position") {
+ .current => if (self.currentPage()) |page| page + 1 else numPages,
+ .end => numPages,
+ };
+}
- pub fn closeTab(self: *Notebook, tab: *Tab) void {
- switch (self.*) {
- .adw => |*adw| adw.closeTab(tab),
- .gtk => |*gtk| gtk.closeTab(tab),
+/// Adds a new tab with the given title to the notebook.
+pub fn addTab(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
+ const position = self.newTabInsertPosition(tab);
+ const box_widget: *c.GtkWidget = @ptrCast(tab.box);
+ const page = c.adw_tab_view_insert(self.tab_view, box_widget, position);
+ c.adw_tab_page_set_title(page, title.ptr);
+ c.adw_tab_view_set_selected_page(self.tab_view, page);
+}
+
+pub fn closeTab(self: *Notebook, tab: *Tab) void {
+ // closeTab always expects to close unconditionally so we mark this
+ // as true so that the close_page call below doesn't request
+ // confirmation.
+ self.forcing_close = true;
+ const n = self.nPages();
+ defer {
+ // self becomes invalid if we close the last page because we close
+ // the whole window
+ if (n > 1) self.forcing_close = false;
+ }
+
+ const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return;
+ c.adw_tab_view_close_page(self.tab_view, page);
+
+ // If we have no more tabs we close the window
+ if (self.nPages() == 0) {
+ const window = tab.window.window;
+
+ // libadw versions <= 1.3.x leak the final page view
+ // which causes our surface to not properly cleanup. We
+ // unref to force the cleanup. This will trigger a critical
+ // warning from GTK, but I don't know any other workaround.
+ // Note: I'm not actually sure if 1.4.0 contains the fix,
+ // I just know that 1.3.x is broken and 1.5.1 is fixed.
+ // If we know that 1.4.0 is fixed, we can change this.
+ if (!adwaita.versionAtLeast(1, 4, 0)) {
+ c.g_object_unref(tab.box);
}
+
+ // `self` will become invalid after this call because it will have
+ // been freed up as part of the process of closing the window.
+ c.gtk_window_destroy(window);
}
-};
+}
pub fn createWindow(currentWindow: *Window) !*Window {
const alloc = currentWindow.app.core_app.alloc;
@@ -167,3 +196,54 @@ pub fn createWindow(currentWindow: *Window) !*Window {
// Create a new window
return Window.create(alloc, app);
}
+
+fn adwPageAttached(_: *c.AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaque) callconv(.C) void {
+ const window: *Window = @ptrCast(@alignCast(ud.?));
+
+ const child = c.adw_tab_page_get_child(page);
+ const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return));
+ tab.window = window;
+
+ window.focusCurrentTab();
+}
+
+fn adwClosePage(
+ _: *c.AdwTabView,
+ page: *c.AdwTabPage,
+ ud: ?*anyopaque,
+) callconv(.C) c.gboolean {
+ const child = c.adw_tab_page_get_child(page);
+ const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(
+ @ptrCast(child),
+ Tab.GHOSTTY_TAB,
+ ) orelse return 0));
+
+ const window: *Window = @ptrCast(@alignCast(ud.?));
+ const notebook = window.notebook;
+ c.adw_tab_view_close_page_finish(
+ notebook.tab_view,
+ page,
+ @intFromBool(notebook.forcing_close),
+ );
+ if (!notebook.forcing_close) tab.closeWithConfirmation();
+ return 1;
+}
+
+fn adwTabViewCreateWindow(
+ _: *c.AdwTabView,
+ ud: ?*anyopaque,
+) callconv(.C) ?*c.AdwTabView {
+ const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
+ const window = createWindow(currentWindow) catch |err| {
+ log.warn("error creating new window error={}", .{err});
+ return null;
+ };
+ return window.notebook.tab_view;
+}
+
+fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void {
+ const window: *Window = @ptrCast(@alignCast(ud.?));
+ const page = c.adw_tab_view_get_selected_page(window.notebook.tab_view) orelse return;
+ const title = c.adw_tab_page_get_title(page);
+ window.setTitle(std.mem.span(title));
+}
diff --git a/src/apprt/gtk/notebook_adw.zig b/src/apprt/gtk/notebook_adw.zig
deleted file mode 100644
index 85083a97eb..0000000000
--- a/src/apprt/gtk/notebook_adw.zig
+++ /dev/null
@@ -1,163 +0,0 @@
-const std = @import("std");
-const assert = std.debug.assert;
-const c = @import("c.zig").c;
-
-const Window = @import("Window.zig");
-const Tab = @import("Tab.zig");
-const Notebook = @import("notebook.zig").Notebook;
-const createWindow = @import("notebook.zig").createWindow;
-const adwaita = @import("adwaita.zig");
-
-const log = std.log.scoped(.gtk);
-
-const AdwTabView = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabView else anyopaque;
-const AdwTabPage = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabPage else anyopaque;
-
-pub const NotebookAdw = struct {
- /// the tab view
- tab_view: *AdwTabView,
-
- pub fn init(notebook: *Notebook) void {
- const window: *Window = @fieldParentPtr("notebook", notebook);
- const app = window.app;
- assert(adwaita.enabled(&app.config));
-
- const tab_view: *c.AdwTabView = c.adw_tab_view_new().?;
- c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_view)), "notebook");
-
- if (comptime adwaita.versionAtLeast(1, 2, 0) and adwaita.versionAtLeast(1, 2, 0)) {
- // Adwaita enables all of the shortcuts by default.
- // We want to manage keybindings ourselves.
- c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS);
- }
-
- notebook.* = .{
- .adw = .{
- .tab_view = tab_view,
- },
- };
-
- _ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
- _ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
- _ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT);
- }
-
- pub fn asWidget(self: *NotebookAdw) *c.GtkWidget {
- return @ptrCast(@alignCast(self.tab_view));
- }
-
- pub fn nPages(self: *NotebookAdw) c_int {
- if (comptime adwaita.versionAtLeast(0, 0, 0))
- return c.adw_tab_view_get_n_pages(self.tab_view)
- else
- unreachable;
- }
-
- /// Returns the index of the currently selected page.
- /// Returns null if the notebook has no pages.
- pub fn currentPage(self: *NotebookAdw) ?c_int {
- if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
- const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
- return c.adw_tab_view_get_page_position(self.tab_view, page);
- }
-
- /// Returns the currently selected tab or null if there are none.
- pub fn currentTab(self: *NotebookAdw) ?*Tab {
- if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
- const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
- const child = c.adw_tab_page_get_child(page);
- return @ptrCast(@alignCast(
- c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null,
- ));
- }
-
- pub fn gotoNthTab(self: *NotebookAdw, position: c_int) void {
- if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
- const page_to_select = c.adw_tab_view_get_nth_page(self.tab_view, position);
- c.adw_tab_view_set_selected_page(self.tab_view, page_to_select);
- }
-
- pub fn getTabPosition(self: *NotebookAdw, tab: *Tab) ?c_int {
- if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
- const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return null;
- return c.adw_tab_view_get_page_position(self.tab_view, page);
- }
-
- pub fn reorderPage(self: *NotebookAdw, tab: *Tab, position: c_int) void {
- if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
- const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
- _ = c.adw_tab_view_reorder_page(self.tab_view, page, position);
- }
-
- pub fn setTabLabel(self: *NotebookAdw, tab: *Tab, title: [:0]const u8) void {
- if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
- const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
- c.adw_tab_page_set_title(page, title.ptr);
- }
-
- pub fn setTabTooltip(self: *NotebookAdw, tab: *Tab, tooltip: [:0]const u8) void {
- if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
- const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
- c.adw_tab_page_set_tooltip(page, tooltip.ptr);
- }
-
- pub fn addTab(self: *NotebookAdw, tab: *Tab, position: c_int, title: [:0]const u8) void {
- if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
- const box_widget: *c.GtkWidget = @ptrCast(tab.box);
- const page = c.adw_tab_view_insert(self.tab_view, box_widget, position);
- c.adw_tab_page_set_title(page, title.ptr);
- c.adw_tab_view_set_selected_page(self.tab_view, page);
- }
-
- pub fn closeTab(self: *NotebookAdw, tab: *Tab) void {
- if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
-
- const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return;
- c.adw_tab_view_close_page(self.tab_view, page);
-
- // If we have no more tabs we close the window
- if (self.nPages() == 0) {
- // libadw versions <= 1.3.x leak the final page view
- // which causes our surface to not properly cleanup. We
- // unref to force the cleanup. This will trigger a critical
- // warning from GTK, but I don't know any other workaround.
- // Note: I'm not actually sure if 1.4.0 contains the fix,
- // I just know that 1.3.x is broken and 1.5.1 is fixed.
- // If we know that 1.4.0 is fixed, we can change this.
- if (!adwaita.versionAtLeast(1, 4, 0)) {
- c.g_object_unref(tab.box);
- }
-
- c.gtk_window_destroy(tab.window.window);
- }
- }
-};
-
-fn adwPageAttached(_: *AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaque) callconv(.C) void {
- const window: *Window = @ptrCast(@alignCast(ud.?));
-
- const child = c.adw_tab_page_get_child(page);
- const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return));
- tab.window = window;
-
- window.focusCurrentTab();
-}
-
-fn adwTabViewCreateWindow(
- _: *AdwTabView,
- ud: ?*anyopaque,
-) callconv(.C) ?*AdwTabView {
- const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
- const window = createWindow(currentWindow) catch |err| {
- log.warn("error creating new window error={}", .{err});
- return null;
- };
- return window.notebook.adw.tab_view;
-}
-
-fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void {
- const window: *Window = @ptrCast(@alignCast(ud.?));
- const page = c.adw_tab_view_get_selected_page(window.notebook.adw.tab_view) orelse return;
- const title = c.adw_tab_page_get_title(page);
- c.gtk_window_set_title(window.window, title);
-}
diff --git a/src/apprt/gtk/notebook_gtk.zig b/src/apprt/gtk/notebook_gtk.zig
deleted file mode 100644
index 6e8b016ba4..0000000000
--- a/src/apprt/gtk/notebook_gtk.zig
+++ /dev/null
@@ -1,285 +0,0 @@
-const std = @import("std");
-const assert = std.debug.assert;
-const c = @import("c.zig").c;
-
-const Window = @import("Window.zig");
-const Tab = @import("Tab.zig");
-const Notebook = @import("notebook.zig").Notebook;
-const createWindow = @import("notebook.zig").createWindow;
-
-const log = std.log.scoped(.gtk);
-
-/// An abstraction over the GTK notebook and Adwaita tab view to manage
-/// all the terminal tabs in a window.
-pub const NotebookGtk = struct {
- notebook: *c.GtkNotebook,
-
- pub fn init(notebook: *Notebook) void {
- const window: *Window = @fieldParentPtr("notebook", notebook);
- const app = window.app;
-
- // Create a notebook to hold our tabs.
- const notebook_widget: *c.GtkWidget = c.gtk_notebook_new();
- c.gtk_widget_add_css_class(notebook_widget, "notebook");
-
- const gtk_notebook: *c.GtkNotebook = @ptrCast(notebook_widget);
- const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") {
- .top, .hidden => c.GTK_POS_TOP,
- .bottom => c.GTK_POS_BOTTOM,
- .left => c.GTK_POS_LEFT,
- .right => c.GTK_POS_RIGHT,
- };
- c.gtk_notebook_set_tab_pos(gtk_notebook, notebook_tab_pos);
- c.gtk_notebook_set_scrollable(gtk_notebook, 1);
- c.gtk_notebook_set_show_tabs(gtk_notebook, 0);
- c.gtk_notebook_set_show_border(gtk_notebook, 0);
-
- // This enables all Ghostty terminal tabs to be exchanged across windows.
- c.gtk_notebook_set_group_name(gtk_notebook, "ghostty-terminal-tabs");
-
- // This is important so the notebook expands to fit available space.
- // Otherwise, it will be zero/zero in the box below.
- c.gtk_widget_set_vexpand(notebook_widget, 1);
- c.gtk_widget_set_hexpand(notebook_widget, 1);
-
- // Remove the background from the stack widget
- const stack = c.gtk_widget_get_last_child(notebook_widget);
- c.gtk_widget_add_css_class(stack, "transparent");
-
- notebook.* = .{
- .gtk = .{
- .notebook = gtk_notebook,
- },
- };
-
- // All of our events
- _ = c.g_signal_connect_data(gtk_notebook, "page-added", c.G_CALLBACK(>kPageAdded), window, null, c.G_CONNECT_DEFAULT);
- _ = c.g_signal_connect_data(gtk_notebook, "page-removed", c.G_CALLBACK(>kPageRemoved), window, null, c.G_CONNECT_DEFAULT);
- _ = c.g_signal_connect_data(gtk_notebook, "switch-page", c.G_CALLBACK(>kSwitchPage), window, null, c.G_CONNECT_DEFAULT);
- _ = c.g_signal_connect_data(gtk_notebook, "create-window", c.G_CALLBACK(>kNotebookCreateWindow), window, null, c.G_CONNECT_DEFAULT);
- }
-
- /// return the underlying widget as a generic GtkWidget
- pub fn asWidget(self: *NotebookGtk) *c.GtkWidget {
- return @ptrCast(@alignCast(self.notebook));
- }
-
- /// returns the number of pages in the notebook
- pub fn nPages(self: *NotebookGtk) c_int {
- return c.gtk_notebook_get_n_pages(self.notebook);
- }
-
- /// Returns the index of the currently selected page.
- /// Returns null if the notebook has no pages.
- pub fn currentPage(self: *NotebookGtk) ?c_int {
- const current = c.gtk_notebook_get_current_page(self.notebook);
- return if (current == -1) null else current;
- }
-
- /// Returns the currently selected tab or null if there are none.
- pub fn currentTab(self: *NotebookGtk) ?*Tab {
- log.warn("currentTab", .{});
- const page = self.currentPage() orelse return null;
- const child = c.gtk_notebook_get_nth_page(self.notebook, page);
- return @ptrCast(@alignCast(
- c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null,
- ));
- }
-
- /// focus the nth tab
- pub fn gotoNthTab(self: *NotebookGtk, position: c_int) void {
- c.gtk_notebook_set_current_page(self.notebook, position);
- }
-
- /// get the position of the current tab
- pub fn getTabPosition(self: *NotebookGtk, tab: *Tab) ?c_int {
- const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return null;
- return getNotebookPageIndex(page);
- }
-
- pub fn reorderPage(self: *NotebookGtk, tab: *Tab, position: c_int) void {
- c.gtk_notebook_reorder_child(self.notebook, @ptrCast(tab.box), position);
- }
-
- pub fn setTabLabel(_: *NotebookGtk, tab: *Tab, title: [:0]const u8) void {
- c.gtk_label_set_text(tab.label_text, title.ptr);
- }
-
- pub fn setTabTooltip(_: *NotebookGtk, tab: *Tab, tooltip: [:0]const u8) void {
- c.gtk_widget_set_tooltip_text(@ptrCast(@alignCast(tab.label_text)), tooltip.ptr);
- }
-
- /// Adds a new tab with the given title to the notebook.
- pub fn addTab(self: *NotebookGtk, tab: *Tab, position: c_int, title: [:0]const u8) void {
- const box_widget: *c.GtkWidget = @ptrCast(tab.box);
-
- // Build the tab label
- const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0);
- const label_box = @as(*c.GtkBox, @ptrCast(label_box_widget));
- const label_text_widget = c.gtk_label_new(title.ptr);
- const label_text: *c.GtkLabel = @ptrCast(label_text_widget);
- c.gtk_box_append(label_box, label_text_widget);
- tab.label_text = label_text;
-
- const window = tab.window;
- if (window.app.config.@"gtk-wide-tabs") {
- c.gtk_widget_set_hexpand(label_box_widget, 1);
- c.gtk_widget_set_halign(label_box_widget, c.GTK_ALIGN_FILL);
- c.gtk_widget_set_hexpand(label_text_widget, 1);
- c.gtk_widget_set_halign(label_text_widget, c.GTK_ALIGN_FILL);
-
- // This ensures that tabs are always equal width. If they're too
- // long, they'll be truncated with an ellipsis.
- c.gtk_label_set_max_width_chars(label_text, 1);
- c.gtk_label_set_ellipsize(label_text, c.PANGO_ELLIPSIZE_END);
-
- // We need to set a minimum width so that at a certain point
- // the notebook will have an arrow button rather than shrinking tabs
- // to an unreadably small size.
- c.gtk_widget_set_size_request(label_text_widget, 100, 1);
- }
-
- // Build the close button for the tab
- const label_close_widget = c.gtk_button_new_from_icon_name("window-close-symbolic");
- const label_close: *c.GtkButton = @ptrCast(label_close_widget);
- c.gtk_button_set_has_frame(label_close, 0);
- c.gtk_box_append(label_box, label_close_widget);
-
- const page_idx = c.gtk_notebook_insert_page(
- self.notebook,
- box_widget,
- label_box_widget,
- position,
- );
-
- // Clicks
- const gesture_tab_click = c.gtk_gesture_click_new();
- c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0);
- c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click));
-
- _ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(&Tab.gtkTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
- _ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(&Tab.gtkTabClick), tab, null, c.G_CONNECT_DEFAULT);
-
- // Tab settings
- c.gtk_notebook_set_tab_reorderable(self.notebook, box_widget, 1);
- c.gtk_notebook_set_tab_detachable(self.notebook, box_widget, 1);
-
- if (self.nPages() > 1) {
- c.gtk_notebook_set_show_tabs(self.notebook, 1);
- }
-
- // Switch to the new tab
- c.gtk_notebook_set_current_page(self.notebook, page_idx);
- }
-
- pub fn closeTab(self: *NotebookGtk, tab: *Tab) void {
- const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return;
-
- // Find page and tab which we're closing
- const page_idx = getNotebookPageIndex(page);
-
- // Remove the page. This will destroy the GTK widgets in the page which
- // will trigger Tab cleanup. The `tab` variable is therefore unusable past that point.
- c.gtk_notebook_remove_page(self.notebook, page_idx);
-
- const remaining = self.nPages();
- switch (remaining) {
- // If we have no more tabs we close the window
- 0 => c.gtk_window_destroy(tab.window.window),
-
- // If we have one more tab we hide the tab bar
- 1 => c.gtk_notebook_set_show_tabs(self.notebook, 0),
-
- else => {},
- }
-
- // If we have remaining tabs, we need to make sure we grab focus.
- if (remaining > 0)
- (self.currentTab() orelse return).window.focusCurrentTab();
- }
-};
-
-fn getNotebookPageIndex(page: *c.GtkNotebookPage) c_int {
- var value: c.GValue = std.mem.zeroes(c.GValue);
- defer c.g_value_unset(&value);
- _ = c.g_value_init(&value, c.G_TYPE_INT);
- c.g_object_get_property(
- @ptrCast(@alignCast(page)),
- "position",
- &value,
- );
-
- return c.g_value_get_int(&value);
-}
-
-fn gtkPageAdded(
- notebook: *c.GtkNotebook,
- _: *c.GtkWidget,
- page_idx: c.guint,
- ud: ?*anyopaque,
-) callconv(.C) void {
- const self: *Window = @ptrCast(@alignCast(ud.?));
-
- // The added page can come from another window with drag and drop, thus we migrate the tab
- // window to be self.
- const page = c.gtk_notebook_get_nth_page(notebook, @intCast(page_idx));
- const tab: *Tab = @ptrCast(@alignCast(
- c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return,
- ));
- tab.window = self;
-
- // Whenever a new page is added, we always grab focus of the
- // currently selected page. This was added specifically so that when
- // we drag a tab out to create a new window ("create-window" event)
- // we grab focus in the new window. Without this, the terminal didn't
- // have focus.
- self.focusCurrentTab();
-}
-
-fn gtkPageRemoved(
- _: *c.GtkNotebook,
- _: *c.GtkWidget,
- _: c.guint,
- ud: ?*anyopaque,
-) callconv(.C) void {
- log.warn("gtkPageRemoved", .{});
- const window: *Window = @ptrCast(@alignCast(ud.?));
-
- // Hide the tab bar if we only have one tab after removal
- const remaining = c.gtk_notebook_get_n_pages(window.notebook.gtk.notebook);
-
- if (remaining == 1) {
- c.gtk_notebook_set_show_tabs(window.notebook.gtk.notebook, 0);
- }
-}
-
-fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void {
- const window: *Window = @ptrCast(@alignCast(ud.?));
- const self = &window.notebook.gtk;
- const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook, page)));
- const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box)));
- const label_text = c.gtk_label_get_text(gtk_label);
- c.gtk_window_set_title(window.window, label_text);
-}
-
-fn gtkNotebookCreateWindow(
- _: *c.GtkNotebook,
- page: *c.GtkWidget,
- ud: ?*anyopaque,
-) callconv(.C) ?*c.GtkNotebook {
- // The tab for the page is stored in the widget data.
- const tab: *Tab = @ptrCast(@alignCast(
- c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null,
- ));
-
- const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
- const newWindow = createWindow(currentWindow) catch |err| {
- log.warn("error creating new window error={}", .{err});
- return null;
- };
-
- // And add it to the new window.
- tab.window = newWindow;
-
- return newWindow.notebook.gtk.notebook;
-}
diff --git a/src/apprt/gtk/wayland.zig b/src/apprt/gtk/wayland.zig
deleted file mode 100644
index 92446cc46a..0000000000
--- a/src/apprt/gtk/wayland.zig
+++ /dev/null
@@ -1,125 +0,0 @@
-const std = @import("std");
-const c = @import("c.zig").c;
-const wayland = @import("wayland");
-const wl = wayland.client.wl;
-const org = wayland.client.org;
-const build_options = @import("build_options");
-
-const log = std.log.scoped(.gtk_wayland);
-
-/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
-pub const AppState = struct {
- display: *wl.Display,
- blur_manager: ?*org.KdeKwinBlurManager = null,
-
- pub fn init(display: ?*c.GdkDisplay) ?AppState {
- if (comptime !build_options.wayland) return null;
-
- // It should really never be null
- const display_ = display orelse return null;
-
- // Check if we're actually on Wayland
- if (c.g_type_check_instance_is_a(
- @ptrCast(@alignCast(display_)),
- c.gdk_wayland_display_get_type(),
- ) == 0)
- return null;
-
- const wl_display: *wl.Display = @ptrCast(c.gdk_wayland_display_get_wl_display(display_) orelse return null);
-
- return .{
- .display = wl_display,
- };
- }
-
- pub fn register(self: *AppState) !void {
- const registry = try self.display.getRegistry();
-
- registry.setListener(*AppState, registryListener, self);
- if (self.display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
-
- log.debug("app wayland init={}", .{self});
- }
-};
-
-/// Wayland state that contains Wayland objects associated with a window (e.g. wl_surface).
-pub const SurfaceState = struct {
- app_state: *AppState,
- surface: *wl.Surface,
-
- /// A token that, when present, indicates that the window is blurred.
- blur_token: ?*org.KdeKwinBlur = null,
-
- pub fn init(window: *c.GtkWindow, app_state: *AppState) ?SurfaceState {
- if (comptime !build_options.wayland) return null;
-
- const surface = c.gtk_native_get_surface(@ptrCast(window)) orelse return null;
-
- // Check if we're actually on Wayland
- if (c.g_type_check_instance_is_a(
- @ptrCast(@alignCast(surface)),
- c.gdk_wayland_surface_get_type(),
- ) == 0)
- return null;
-
- const wl_surface: *wl.Surface = @ptrCast(c.gdk_wayland_surface_get_wl_surface(surface) orelse return null);
-
- return .{
- .app_state = app_state,
- .surface = wl_surface,
- };
- }
-
- pub fn deinit(self: *SurfaceState) void {
- if (self.blur_token) |blur| blur.release();
- }
-
- pub fn setBlur(self: *SurfaceState, blurred: bool) !void {
- log.debug("setting blur={}", .{blurred});
-
- const mgr = self.app_state.blur_manager orelse {
- log.warn("can't set blur: org_kde_kwin_blur_manager protocol unavailable", .{});
- return;
- };
-
- if (self.blur_token) |blur| {
- // Only release token when transitioning from blurred -> not blurred
- if (!blurred) {
- mgr.unset(self.surface);
- blur.release();
- self.blur_token = null;
- }
- } else {
- // Only acquire token when transitioning from not blurred -> blurred
- if (blurred) {
- const blur_token = try mgr.create(self.surface);
- blur_token.commit();
- self.blur_token = blur_token;
- }
- }
- }
-};
-
-fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *AppState) void {
- switch (event) {
- .global => |global| {
- log.debug("got global interface={s}", .{global.interface});
- if (bindInterface(org.KdeKwinBlurManager, registry, global, 1)) |iface| {
- state.blur_manager = iface;
- return;
- }
- },
- .global_remove => {},
- }
-}
-
-fn bindInterface(comptime T: type, registry: *wl.Registry, global: anytype, version: u32) ?*T {
- if (std.mem.orderZ(u8, global.interface, T.interface.name) == .eq) {
- return registry.bind(global.name, T, version) catch |err| {
- log.warn("encountered error={} while binding interface {s}", .{ err, global.interface });
- return null;
- };
- } else {
- return null;
- }
-}
diff --git a/src/apprt/gtk/winproto.zig b/src/apprt/gtk/winproto.zig
new file mode 100644
index 0000000000..c752ee6927
--- /dev/null
+++ b/src/apprt/gtk/winproto.zig
@@ -0,0 +1,140 @@
+const std = @import("std");
+const build_options = @import("build_options");
+const Allocator = std.mem.Allocator;
+const c = @import("c.zig").c;
+const Config = @import("../../config.zig").Config;
+const input = @import("../../input.zig");
+const key = @import("key.zig");
+
+pub const noop = @import("winproto/noop.zig");
+pub const x11 = @import("winproto/x11.zig");
+pub const wayland = @import("winproto/wayland.zig");
+
+pub const Protocol = enum {
+ none,
+ wayland,
+ x11,
+};
+
+/// App-state for the underlying windowing protocol. There should be one
+/// instance of this struct per application.
+pub const App = union(Protocol) {
+ none: noop.App,
+ wayland: if (build_options.wayland) wayland.App else noop.App,
+ x11: if (build_options.x11) x11.App else noop.App,
+
+ pub fn init(
+ alloc: Allocator,
+ gdk_display: *c.GdkDisplay,
+ app_id: [:0]const u8,
+ config: *const Config,
+ ) !App {
+ inline for (@typeInfo(App).Union.fields) |field| {
+ if (try field.type.init(
+ alloc,
+ gdk_display,
+ app_id,
+ config,
+ )) |v| {
+ return @unionInit(App, field.name, v);
+ }
+ }
+
+ return .{ .none = .{} };
+ }
+
+ pub fn deinit(self: *App, alloc: Allocator) void {
+ switch (self.*) {
+ inline else => |*v| v.deinit(alloc),
+ }
+ }
+
+ pub fn eventMods(
+ self: *App,
+ device: ?*c.GdkDevice,
+ gtk_mods: c.GdkModifierType,
+ ) input.Mods {
+ return switch (self.*) {
+ inline else => |*v| v.eventMods(device, gtk_mods),
+ } orelse key.translateMods(gtk_mods);
+ }
+};
+
+/// Per-Window state for the underlying windowing protocol.
+///
+/// In Wayland, the terminology used is "Surface" and for it, this is
+/// really "Surface"-specific state. But Ghostty uses the term "Surface"
+/// heavily to mean something completely different, so we use "Window" here
+/// to better match what it generally maps to in the Ghostty codebase.
+pub const Window = union(Protocol) {
+ none: noop.Window,
+ wayland: if (build_options.wayland) wayland.Window else noop.Window,
+ x11: if (build_options.x11) x11.Window else noop.Window,
+
+ pub fn init(
+ alloc: Allocator,
+ app: *App,
+ window: *c.GtkWindow,
+ config: *const Config,
+ ) !Window {
+ return switch (app.*) {
+ inline else => |*v, tag| {
+ inline for (@typeInfo(Window).Union.fields) |field| {
+ if (comptime std.mem.eql(
+ u8,
+ field.name,
+ @tagName(tag),
+ )) return @unionInit(
+ Window,
+ field.name,
+ try field.type.init(
+ alloc,
+ v,
+ window,
+ config,
+ ),
+ );
+ }
+ },
+ };
+ }
+
+ pub fn deinit(self: *Window, alloc: Allocator) void {
+ switch (self.*) {
+ inline else => |*v| v.deinit(alloc),
+ }
+ }
+
+ pub fn resizeEvent(self: *Window) !void {
+ switch (self.*) {
+ inline else => |*v| try v.resizeEvent(),
+ }
+ }
+
+ pub fn updateConfigEvent(
+ self: *Window,
+ config: *const Config,
+ ) !void {
+ switch (self.*) {
+ inline else => |*v| try v.updateConfigEvent(config),
+ }
+ }
+
+ pub fn syncAppearance(self: *Window) !void {
+ switch (self.*) {
+ inline else => |*v| try v.syncAppearance(),
+ }
+ }
+
+ pub fn clientSideDecorationEnabled(self: Window) bool {
+ return switch (self) {
+ inline else => |v| v.clientSideDecorationEnabled(),
+ };
+ }
+
+ pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
+ switch (self.*) {
+ inline else => |*v| try v.addSubprocessEnv(env),
+ }
+ }
+};
diff --git a/src/apprt/gtk/winproto/noop.zig b/src/apprt/gtk/winproto/noop.zig
new file mode 100644
index 0000000000..cb1c0e9ebb
--- /dev/null
+++ b/src/apprt/gtk/winproto/noop.zig
@@ -0,0 +1,66 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const c = @import("../c.zig").c;
+const Config = @import("../../../config.zig").Config;
+const input = @import("../../../input.zig");
+
+const log = std.log.scoped(.winproto_noop);
+
+pub const App = struct {
+ pub fn init(
+ _: Allocator,
+ _: *c.GdkDisplay,
+ _: [:0]const u8,
+ _: *const Config,
+ ) !?App {
+ return null;
+ }
+
+ pub fn deinit(self: *App, alloc: Allocator) void {
+ _ = self;
+ _ = alloc;
+ }
+
+ pub fn eventMods(
+ _: *App,
+ _: ?*c.GdkDevice,
+ _: c.GdkModifierType,
+ ) ?input.Mods {
+ return null;
+ }
+};
+
+pub const Window = struct {
+ pub fn init(
+ _: Allocator,
+ _: *App,
+ _: *c.GtkWindow,
+ _: *const Config,
+ ) !Window {
+ return .{};
+ }
+
+ pub fn deinit(self: Window, alloc: Allocator) void {
+ _ = self;
+ _ = alloc;
+ }
+
+ pub fn updateConfigEvent(
+ _: *Window,
+ _: *const Config,
+ ) !void {}
+
+ pub fn resizeEvent(_: *Window) !void {}
+
+ pub fn syncAppearance(_: *Window) !void {}
+
+ /// This returns true if CSD is enabled for this window. This
+ /// should be the actual present state of the window, not the
+ /// desired state.
+ pub fn clientSideDecorationEnabled(self: Window) bool {
+ _ = self;
+ return true;
+ }
+
+ pub fn addSubprocessEnv(_: *Window, _: *std.process.EnvMap) !void {}
+};
diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig
new file mode 100644
index 0000000000..f2ef17d73a
--- /dev/null
+++ b/src/apprt/gtk/winproto/wayland.zig
@@ -0,0 +1,308 @@
+//! Wayland protocol implementation for the Ghostty GTK apprt.
+const std = @import("std");
+const wayland = @import("wayland");
+const Allocator = std.mem.Allocator;
+const c = @import("../c.zig").c;
+const Config = @import("../../../config.zig").Config;
+const input = @import("../../../input.zig");
+
+const wl = wayland.client.wl;
+const org = wayland.client.org;
+
+const log = std.log.scoped(.winproto_wayland);
+
+/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
+pub const App = struct {
+ display: *wl.Display,
+ context: *Context,
+
+ const Context = struct {
+ kde_blur_manager: ?*org.KdeKwinBlurManager = null,
+
+ // FIXME: replace with `zxdg_decoration_v1` once GTK merges
+ // https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
+ kde_decoration_manager: ?*org.KdeKwinServerDecorationManager = null,
+
+ default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
+ };
+
+ pub fn init(
+ alloc: Allocator,
+ gdk_display: *c.GdkDisplay,
+ app_id: [:0]const u8,
+ config: *const Config,
+ ) !?App {
+ _ = config;
+ _ = app_id;
+
+ // Check if we're actually on Wayland
+ if (c.g_type_check_instance_is_a(
+ @ptrCast(@alignCast(gdk_display)),
+ c.gdk_wayland_display_get_type(),
+ ) == 0) return null;
+
+ const display: *wl.Display = @ptrCast(c.gdk_wayland_display_get_wl_display(
+ gdk_display,
+ ) orelse return error.NoWaylandDisplay);
+
+ // Create our context for our callbacks so we have a stable pointer.
+ // Note: at the time of writing this comment, we don't really need
+ // a stable pointer, but it's too scary that we'd need one in the future
+ // and not have it and corrupt memory or something so let's just do it.
+ const context = try alloc.create(Context);
+ errdefer alloc.destroy(context);
+ context.* = .{};
+
+ // Get our display registry so we can get all the available interfaces
+ // and bind to what we need.
+ const registry = try display.getRegistry();
+ registry.setListener(*Context, registryListener, context);
+ if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
+
+ if (context.kde_decoration_manager != null) {
+ // FIXME: Roundtrip again because we have to wait for the decoration
+ // manager to respond with the preferred default mode. Ew.
+ if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
+ }
+
+ return .{
+ .display = display,
+ .context = context,
+ };
+ }
+
+ pub fn deinit(self: *App, alloc: Allocator) void {
+ alloc.destroy(self.context);
+ }
+
+ pub fn eventMods(
+ _: *App,
+ _: ?*c.GdkDevice,
+ _: c.GdkModifierType,
+ ) ?input.Mods {
+ return null;
+ }
+
+ fn registryListener(
+ registry: *wl.Registry,
+ event: wl.Registry.Event,
+ context: *Context,
+ ) void {
+ switch (event) {
+ // https://wayland.app/protocols/wayland#wl_registry:event:global
+ .global => |global| {
+ log.debug("wl_registry.global: interface={s}", .{global.interface});
+
+ if (registryBind(
+ org.KdeKwinBlurManager,
+ registry,
+ global,
+ )) |blur_manager| {
+ context.kde_blur_manager = blur_manager;
+ } else if (registryBind(
+ org.KdeKwinServerDecorationManager,
+ registry,
+ global,
+ )) |deco_manager| {
+ context.kde_decoration_manager = deco_manager;
+ deco_manager.setListener(*Context, decoManagerListener, context);
+ }
+ },
+
+ // We don't handle removal events
+ .global_remove => {},
+ }
+ }
+
+ /// Bind a Wayland interface to a global object. Returns non-null
+ /// if the binding was successful, otherwise null.
+ ///
+ /// The type T is the Wayland interface type that we're requesting.
+ /// This function will verify that the global object is the correct
+ /// interface and version before binding.
+ fn registryBind(
+ comptime T: type,
+ registry: *wl.Registry,
+ global: anytype,
+ ) ?*T {
+ if (std.mem.orderZ(
+ u8,
+ global.interface,
+ T.interface.name,
+ ) != .eq) return null;
+
+ return registry.bind(global.name, T, T.generated_version) catch |err| {
+ log.warn("error binding interface {s} error={}", .{
+ global.interface,
+ err,
+ });
+ return null;
+ };
+ }
+
+ fn decoManagerListener(
+ _: *org.KdeKwinServerDecorationManager,
+ event: org.KdeKwinServerDecorationManager.Event,
+ context: *Context,
+ ) void {
+ switch (event) {
+ .default_mode => |mode| {
+ context.default_deco_mode = @enumFromInt(mode.mode);
+ },
+ }
+ }
+};
+
+/// Per-window (wl_surface) state for the Wayland protocol.
+pub const Window = struct {
+ config: DerivedConfig,
+
+ /// The Wayland surface for this window.
+ surface: *wl.Surface,
+
+ /// The context from the app where we can load our Wayland interfaces.
+ app_context: *App.Context,
+
+ /// A token that, when present, indicates that the window is blurred.
+ blur_token: ?*org.KdeKwinBlur,
+
+ /// Object that controls the decoration mode (client/server/auto)
+ /// of the window.
+ decoration: ?*org.KdeKwinServerDecoration,
+
+ const DerivedConfig = struct {
+ blur: bool,
+ window_decoration: Config.WindowDecoration,
+
+ pub fn init(config: *const Config) DerivedConfig {
+ return .{
+ .blur = config.@"background-blur".enabled(),
+ .window_decoration = config.@"window-decoration",
+ };
+ }
+ };
+
+ pub fn init(
+ alloc: Allocator,
+ app: *App,
+ gtk_window: *c.GtkWindow,
+ config: *const Config,
+ ) !Window {
+ _ = alloc;
+
+ const gdk_surface = c.gtk_native_get_surface(
+ @ptrCast(gtk_window),
+ ) orelse return error.NotWaylandSurface;
+
+ // This should never fail, because if we're being called at this point
+ // then we've already asserted that our app state is Wayland.
+ if (c.g_type_check_instance_is_a(
+ @ptrCast(@alignCast(gdk_surface)),
+ c.gdk_wayland_surface_get_type(),
+ ) == 0) return error.NotWaylandSurface;
+
+ const wl_surface: *wl.Surface = @ptrCast(c.gdk_wayland_surface_get_wl_surface(
+ gdk_surface,
+ ) orelse return error.NoWaylandSurface);
+
+ // Get our decoration object so we can control the
+ // CSD vs SSD status of this surface.
+ const deco: ?*org.KdeKwinServerDecoration = deco: {
+ const mgr = app.context.kde_decoration_manager orelse
+ break :deco null;
+
+ const deco: *org.KdeKwinServerDecoration = mgr.create(
+ wl_surface,
+ ) catch |err| {
+ log.warn("could not create decoration object={}", .{err});
+ break :deco null;
+ };
+
+ break :deco deco;
+ };
+
+ return .{
+ .config = DerivedConfig.init(config),
+ .surface = wl_surface,
+ .app_context = app.context,
+ .blur_token = null,
+ .decoration = deco,
+ };
+ }
+
+ pub fn deinit(self: Window, alloc: Allocator) void {
+ _ = alloc;
+ if (self.blur_token) |blur| blur.release();
+ if (self.decoration) |deco| deco.release();
+ }
+
+ pub fn updateConfigEvent(
+ self: *Window,
+ config: *const Config,
+ ) !void {
+ self.config = DerivedConfig.init(config);
+ }
+
+ pub fn resizeEvent(_: *Window) !void {}
+
+ pub fn syncAppearance(self: *Window) !void {
+ try self.syncBlur();
+ try self.syncDecoration();
+ }
+
+ pub fn clientSideDecorationEnabled(self: Window) bool {
+ return switch (self.getDecorationMode()) {
+ .Client => true,
+ // If we support SSDs, then we should *not* enable CSDs if we prefer SSDs.
+ // However, if we do not support SSDs (e.g. GNOME) then we should enable
+ // CSDs even if the user prefers SSDs.
+ .Server => if (self.app_context.kde_decoration_manager) |_| false else true,
+ .None => false,
+ else => unreachable,
+ };
+ }
+
+ pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
+ _ = self;
+ _ = env;
+ }
+
+ /// Update the blur state of the window.
+ fn syncBlur(self: *Window) !void {
+ const manager = self.app_context.kde_blur_manager orelse return;
+ const blur = self.config.blur;
+
+ if (self.blur_token) |tok| {
+ // Only release token when transitioning from blurred -> not blurred
+ if (!blur) {
+ manager.unset(self.surface);
+ tok.release();
+ self.blur_token = null;
+ }
+ } else {
+ // Only acquire token when transitioning from not blurred -> blurred
+ if (blur) {
+ const tok = try manager.create(self.surface);
+ tok.commit();
+ self.blur_token = tok;
+ }
+ }
+ }
+
+ fn syncDecoration(self: *Window) !void {
+ const deco = self.decoration orelse return;
+
+ // The protocol requests uint instead of enum so we have
+ // to convert it.
+ deco.requestMode(@intCast(@intFromEnum(self.getDecorationMode())));
+ }
+
+ fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode {
+ return switch (self.config.window_decoration) {
+ .auto => self.app_context.default_deco_mode orelse .Client,
+ .client => .Client,
+ .server => .Server,
+ .none => .None,
+ };
+ }
+};
diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig
new file mode 100644
index 0000000000..6b60b0edf8
--- /dev/null
+++ b/src/apprt/gtk/winproto/x11.zig
@@ -0,0 +1,488 @@
+//! X11 window protocol implementation for the Ghostty GTK apprt.
+const std = @import("std");
+const builtin = @import("builtin");
+const build_options = @import("build_options");
+const Allocator = std.mem.Allocator;
+const c = @import("../c.zig").c;
+const input = @import("../../../input.zig");
+const Config = @import("../../../config.zig").Config;
+const adwaita = @import("../adwaita.zig");
+
+const log = std.log.scoped(.gtk_x11);
+
+pub const App = struct {
+ display: *c.Display,
+ base_event_code: c_int,
+ atoms: Atoms,
+
+ pub fn init(
+ alloc: Allocator,
+ gdk_display: *c.GdkDisplay,
+ app_id: [:0]const u8,
+ config: *const Config,
+ ) !?App {
+ _ = alloc;
+
+ // If the display isn't X11, then we don't need to do anything.
+ if (c.g_type_check_instance_is_a(
+ @ptrCast(@alignCast(gdk_display)),
+ c.gdk_x11_display_get_type(),
+ ) == 0) return null;
+
+ // Get our X11 display
+ const display: *c.Display = c.gdk_x11_display_get_xdisplay(
+ gdk_display,
+ ) orelse return error.NoX11Display;
+
+ const x11_program_name: [:0]const u8 = if (config.@"x11-instance-name") |pn|
+ pn
+ else if (builtin.mode == .Debug)
+ "ghostty-debug"
+ else
+ "ghostty";
+
+ // Set the X11 window class property (WM_CLASS) if are are on an X11
+ // display.
+ //
+ // Note that we also set the program name here using g_set_prgname.
+ // This is how the instance name field for WM_CLASS is derived when
+ // calling gdk_x11_display_set_program_class; there does not seem to be
+ // a way to set it directly. It does not look like this is being set by
+ // our other app initialization routines currently, but since we're
+ // currently deriving its value from x11-instance-name effectively, I
+ // feel like gating it behind an X11 check is better intent.
+ //
+ // This makes the property show up like so when using xprop:
+ //
+ // WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty"
+ //
+ // Append "-debug" on both when using the debug build.
+ c.g_set_prgname(x11_program_name);
+ c.gdk_x11_display_set_program_class(gdk_display, app_id);
+
+ // XKB
+ log.debug("Xkb.init: initializing Xkb", .{});
+ log.debug("Xkb.init: running XkbQueryExtension", .{});
+ var opcode: c_int = 0;
+ var base_event_code: c_int = 0;
+ var base_error_code: c_int = 0;
+ var major = c.XkbMajorVersion;
+ var minor = c.XkbMinorVersion;
+ if (c.XkbQueryExtension(
+ display,
+ &opcode,
+ &base_event_code,
+ &base_error_code,
+ &major,
+ &minor,
+ ) == 0) {
+ log.err("Fatal: error initializing Xkb extension: error executing XkbQueryExtension", .{});
+ return error.XkbInitializationError;
+ }
+
+ log.debug("Xkb.init: running XkbSelectEventDetails", .{});
+ if (c.XkbSelectEventDetails(
+ display,
+ c.XkbUseCoreKbd,
+ c.XkbStateNotify,
+ c.XkbModifierStateMask,
+ c.XkbModifierStateMask,
+ ) == 0) {
+ log.err("Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails", .{});
+ return error.XkbInitializationError;
+ }
+
+ return .{
+ .display = display,
+ .base_event_code = base_event_code,
+ .atoms = Atoms.init(gdk_display),
+ };
+ }
+
+ pub fn deinit(self: *App, alloc: Allocator) void {
+ _ = self;
+ _ = alloc;
+ }
+
+ /// Checks for an immediate pending XKB state update event, and returns the
+ /// keyboard state based on if it finds any. This is necessary as the
+ /// standard GTK X11 API (and X11 in general) does not include the current
+ /// key pressed in any modifier state snapshot for that event (e.g. if the
+ /// pressed key is a modifier, that is not necessarily reflected in the
+ /// modifiers).
+ ///
+ /// Returns null if there is no event. In this case, the caller should fall
+ /// back to the standard GDK modifier state (this likely means the key
+ /// event did not result in a modifier change).
+ pub fn eventMods(
+ self: App,
+ device: ?*c.GdkDevice,
+ gtk_mods: c.GdkModifierType,
+ ) ?input.Mods {
+ _ = device;
+ _ = gtk_mods;
+
+ // Shoutout to Mozilla for figuring out a clean way to do this, this is
+ // paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
+ if (c.XEventsQueued(self.display, c.QueuedAfterReading) == 0) return null;
+
+ var nextEvent: c.XEvent = undefined;
+ _ = c.XPeekEvent(self.display, &nextEvent);
+ if (nextEvent.type != self.base_event_code) return null;
+
+ const xkb_event: *c.XkbEvent = @ptrCast(&nextEvent);
+ if (xkb_event.any.xkb_type != c.XkbStateNotify) return null;
+
+ const xkb_state_notify_event: *c.XkbStateNotifyEvent = @ptrCast(xkb_event);
+ // Check the state according to XKB masks.
+ const lookup_mods = xkb_state_notify_event.lookup_mods;
+ var mods: input.Mods = .{};
+
+ log.debug("X11: found extra XkbStateNotify event w/lookup_mods: {b}", .{lookup_mods});
+ if (lookup_mods & c.ShiftMask != 0) mods.shift = true;
+ if (lookup_mods & c.ControlMask != 0) mods.ctrl = true;
+ if (lookup_mods & c.Mod1Mask != 0) mods.alt = true;
+ if (lookup_mods & c.Mod4Mask != 0) mods.super = true;
+ if (lookup_mods & c.LockMask != 0) mods.caps_lock = true;
+
+ return mods;
+ }
+};
+
+pub const Window = struct {
+ app: *App,
+ alloc: Allocator,
+ config: DerivedConfig,
+ window: c.Window,
+ gtk_window: *c.GtkWindow,
+
+ blur_region: Region = .{},
+
+ const DerivedConfig = struct {
+ blur: bool,
+ window_decoration: Config.WindowDecoration,
+
+ pub fn init(config: *const Config) DerivedConfig {
+ return .{
+ .blur = config.@"background-blur".enabled(),
+ .window_decoration = config.@"window-decoration",
+ };
+ }
+ };
+
+ pub fn init(
+ alloc: Allocator,
+ app: *App,
+ gtk_window: *c.GtkWindow,
+ config: *const Config,
+ ) !Window {
+ const surface = c.gtk_native_get_surface(
+ @ptrCast(gtk_window),
+ ) orelse return error.NotX11Surface;
+
+ // Check if we're actually on X11
+ if (c.g_type_check_instance_is_a(
+ @ptrCast(@alignCast(surface)),
+ c.gdk_x11_surface_get_type(),
+ ) == 0) return error.NotX11Surface;
+
+ return .{
+ .app = app,
+ .alloc = alloc,
+ .config = DerivedConfig.init(config),
+ .window = c.gdk_x11_surface_get_xid(surface),
+ .gtk_window = gtk_window,
+ };
+ }
+
+ pub fn deinit(self: Window, alloc: Allocator) void {
+ _ = self;
+ _ = alloc;
+ }
+
+ pub fn updateConfigEvent(
+ self: *Window,
+ config: *const Config,
+ ) !void {
+ self.config = DerivedConfig.init(config);
+ }
+
+ pub fn resizeEvent(self: *Window) !void {
+ // The blur region must update with window resizes
+ self.blur_region.width = c.gtk_widget_get_width(@ptrCast(self.gtk_window));
+ self.blur_region.height = c.gtk_widget_get_height(@ptrCast(self.gtk_window));
+ try self.syncBlur();
+ }
+
+ pub fn syncAppearance(self: *Window) !void {
+ self.blur_region = blur: {
+ // NOTE(pluiedev): CSDs are a f--king mistake.
+ // Please, GNOME, stop this nonsense of making a window ~30% bigger
+ // internally than how they really are just for your shadows and
+ // rounded corners and all that fluff. Please. I beg of you.
+ var x: f64 = 0;
+ var y: f64 = 0;
+ c.gtk_native_get_surface_transform(
+ @ptrCast(self.gtk_window),
+ &x,
+ &y,
+ );
+
+ break :blur .{
+ .x = @intFromFloat(x),
+ .y = @intFromFloat(y),
+ };
+ };
+ self.syncBlur() catch |err| {
+ log.err("failed to synchronize blur={}", .{err});
+ };
+ self.syncDecorations() catch |err| {
+ log.err("failed to synchronize decorations={}", .{err});
+ };
+ }
+
+ pub fn clientSideDecorationEnabled(self: Window) bool {
+ return switch (self.config.window_decoration) {
+ .auto, .client => true,
+ .server, .none => false,
+ };
+ }
+
+ fn syncBlur(self: *Window) !void {
+ // FIXME: This doesn't currently factor in rounded corners on Adwaita,
+ // which means that the blur region will grow slightly outside of the
+ // window borders. Unfortunately, actually calculating the rounded
+ // region can be quite complex without having access to existing APIs
+ // (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134)
+ // and I think it's not really noticeable enough to justify the effort.
+ // (Wayland also has this visual artifact anyway...)
+
+ const blur = self.config.blur;
+ log.debug("set blur={}, window xid={}, region={}", .{
+ blur,
+ self.window,
+ self.blur_region,
+ });
+
+ if (blur) {
+ try self.changeProperty(
+ Region,
+ self.app.atoms.kde_blur,
+ c.XA_CARDINAL,
+ ._32,
+ .{ .mode = .replace },
+ &self.blur_region,
+ );
+ } else {
+ try self.deleteProperty(self.app.atoms.kde_blur);
+ }
+ }
+
+ fn syncDecorations(self: *Window) !void {
+ var hints: MotifWMHints = .{};
+
+ self.getWindowProperty(
+ MotifWMHints,
+ self.app.atoms.motif_wm_hints,
+ self.app.atoms.motif_wm_hints,
+ ._32,
+ .{},
+ &hints,
+ ) catch |err| switch (err) {
+ // motif_wm_hints is already initialized, so this is fine
+ error.PropertyNotFound => {},
+
+ error.RequestFailed,
+ error.PropertyTypeMismatch,
+ error.PropertyFormatMismatch,
+ => return err,
+ };
+
+ hints.flags.decorations = true;
+ hints.decorations.all = switch (self.config.window_decoration) {
+ .server => true,
+ .auto, .client, .none => false,
+ };
+
+ try self.changeProperty(
+ MotifWMHints,
+ self.app.atoms.motif_wm_hints,
+ self.app.atoms.motif_wm_hints,
+ ._32,
+ .{ .mode = .replace },
+ &hints,
+ );
+ }
+
+ pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
+ var buf: [64]u8 = undefined;
+ const window_id = try std.fmt.bufPrint(&buf, "{}", .{self.window});
+
+ try env.put("WINDOWID", window_id);
+ }
+
+ fn getWindowProperty(
+ self: *Window,
+ comptime T: type,
+ name: c.Atom,
+ typ: c.Atom,
+ comptime format: PropertyFormat,
+ options: struct {
+ offset: c_long = 0,
+ length: c_long = std.math.maxInt(c_long),
+ delete: bool = false,
+ },
+ result: *T,
+ ) GetWindowPropertyError!void {
+ // FIXME: Maybe we should switch to libxcb one day.
+ // Sounds like a much better idea than whatever this is
+ var actual_type_return: c.Atom = undefined;
+ var actual_format_return: c_int = undefined;
+ var nitems_return: c_ulong = undefined;
+ var bytes_after_return: c_ulong = undefined;
+ var prop_return: ?format.bufferType() = null;
+
+ const code = c.XGetWindowProperty(
+ self.app.display,
+ self.window,
+ name,
+ options.offset,
+ options.length,
+ @intFromBool(options.delete),
+ typ,
+ &actual_type_return,
+ &actual_format_return,
+ &nitems_return,
+ &bytes_after_return,
+ &prop_return,
+ );
+ if (code != c.Success) return error.RequestFailed;
+
+ if (actual_type_return == c.None) return error.PropertyNotFound;
+ if (typ != actual_type_return) return error.PropertyTypeMismatch;
+ if (@intFromEnum(format) != actual_format_return) return error.PropertyFormatMismatch;
+
+ const data_ptr: *T = @ptrCast(prop_return);
+ result.* = data_ptr.*;
+ _ = c.XFree(prop_return);
+ }
+
+ fn changeProperty(
+ self: *Window,
+ comptime T: type,
+ name: c.Atom,
+ typ: c.Atom,
+ comptime format: PropertyFormat,
+ options: struct {
+ mode: PropertyChangeMode,
+ },
+ value: *T,
+ ) X11Error!void {
+ const data: format.bufferType() = @ptrCast(value);
+
+ const status = c.XChangeProperty(
+ self.app.display,
+ self.window,
+ name,
+ typ,
+ @intFromEnum(format),
+ @intFromEnum(options.mode),
+ data,
+ @divExact(@sizeOf(T), @sizeOf(format.elemType())),
+ );
+
+ // For some godforsaken reason Xlib alternates between
+ // error values (0 = success) and booleans (1 = success), and they look exactly
+ // the same in the signature (just `int`, since Xlib is written in C89)...
+ if (status == 0) return error.RequestFailed;
+ }
+
+ fn deleteProperty(self: *Window, name: c.Atom) X11Error!void {
+ const status = c.XDeleteProperty(self.app.display, self.window, name);
+ if (status == 0) return error.RequestFailed;
+ }
+};
+
+const X11Error = error{
+ RequestFailed,
+};
+
+const GetWindowPropertyError = X11Error || error{
+ PropertyNotFound,
+ PropertyTypeMismatch,
+ PropertyFormatMismatch,
+};
+
+const Atoms = struct {
+ kde_blur: c.Atom,
+ motif_wm_hints: c.Atom,
+
+ fn init(display: *c.GdkDisplay) Atoms {
+ return .{
+ .kde_blur = c.gdk_x11_get_xatom_by_name_for_display(
+ display,
+ "_KDE_NET_WM_BLUR_BEHIND_REGION",
+ ),
+ .motif_wm_hints = c.gdk_x11_get_xatom_by_name_for_display(
+ display,
+ "_MOTIF_WM_HINTS",
+ ),
+ };
+ }
+};
+
+const PropertyChangeMode = enum(c_int) {
+ replace = c.PropModeReplace,
+ prepend = c.PropModePrepend,
+ append = c.PropModeAppend,
+};
+
+const PropertyFormat = enum(c_int) {
+ _8 = 8,
+ _16 = 16,
+ _32 = 32,
+
+ fn elemType(comptime self: PropertyFormat) type {
+ return switch (self) {
+ ._8 => c_char,
+ ._16 => c_int,
+ ._32 => c_long,
+ };
+ }
+
+ fn bufferType(comptime self: PropertyFormat) type {
+ // The buffer type has to be a multi-pointer to bytes
+ // *aligned to the element type* (very important,
+ // otherwise you'll read garbage!)
+ //
+ // I know this is really ugly. X11 is ugly. I consider it apropos.
+ return [*]align(@alignOf(self.elemType())) u8;
+ }
+};
+
+const Region = extern struct {
+ x: c_long = 0,
+ y: c_long = 0,
+ width: c_long = 0,
+ height: c_long = 0,
+};
+
+// See Xm/MwmUtil.h, packaged with the Motif Window Manager
+const MotifWMHints = extern struct {
+ flags: packed struct(c_ulong) {
+ _pad: u1 = 0,
+ decorations: bool = false,
+
+ // We don't really care about the other flags
+ _rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 2) = 0,
+ } = .{},
+ functions: c_ulong = 0,
+ decorations: packed struct(c_ulong) {
+ all: bool = false,
+
+ // We don't really care about the other flags
+ _rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 1) = 0,
+ } = .{},
+ input_mode: c_long = 0,
+ status: c_ulong = 0,
+};
diff --git a/src/apprt/gtk/x11.zig b/src/apprt/gtk/x11.zig
deleted file mode 100644
index 21ff87b34f..0000000000
--- a/src/apprt/gtk/x11.zig
+++ /dev/null
@@ -1,119 +0,0 @@
-/// Utility functions for X11 handling.
-const std = @import("std");
-const build_options = @import("build_options");
-const c = @import("c.zig").c;
-const input = @import("../../input.zig");
-
-const log = std.log.scoped(.gtk_x11);
-
-/// Returns true if the passed in display is an X11 display.
-pub fn is_display(display: ?*c.GdkDisplay) bool {
- if (comptime !build_options.x11) return false;
- return c.g_type_check_instance_is_a(
- @ptrCast(@alignCast(display orelse return false)),
- c.gdk_x11_display_get_type(),
- ) != 0;
-}
-
-/// Returns true if the app is running on X11
-pub fn is_current_display_server() bool {
- if (comptime !build_options.x11) return false;
- const display = c.gdk_display_get_default();
- return is_display(display);
-}
-
-pub const Xkb = struct {
- base_event_code: c_int,
-
- /// Initialize an Xkb struct for the given GDK display. If the display isn't
- /// backed by X then this will return null.
- pub fn init(display_: ?*c.GdkDisplay) !?Xkb {
- if (comptime !build_options.x11) return null;
-
- // Display should never be null but we just treat that as a non-X11
- // display so that the caller can just ignore it and not unwrap it.
- const display = display_ orelse return null;
-
- // If the display isn't X11, then we don't need to do anything.
- if (!is_display(display)) return null;
-
- log.debug("Xkb.init: initializing Xkb", .{});
- const xdisplay = c.gdk_x11_display_get_xdisplay(display);
- var result: Xkb = .{
- .base_event_code = 0,
- };
-
- log.debug("Xkb.init: running XkbQueryExtension", .{});
- var opcode: c_int = 0;
- var base_error_code: c_int = 0;
- var major = c.XkbMajorVersion;
- var minor = c.XkbMinorVersion;
- if (c.XkbQueryExtension(
- xdisplay,
- &opcode,
- &result.base_event_code,
- &base_error_code,
- &major,
- &minor,
- ) == 0) {
- log.err("Fatal: error initializing Xkb extension: error executing XkbQueryExtension", .{});
- return error.XkbInitializationError;
- }
-
- log.debug("Xkb.init: running XkbSelectEventDetails", .{});
- if (c.XkbSelectEventDetails(
- xdisplay,
- c.XkbUseCoreKbd,
- c.XkbStateNotify,
- c.XkbModifierStateMask,
- c.XkbModifierStateMask,
- ) == 0) {
- log.err("Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails", .{});
- return error.XkbInitializationError;
- }
-
- return result;
- }
-
- /// Checks for an immediate pending XKB state update event, and returns the
- /// keyboard state based on if it finds any. This is necessary as the
- /// standard GTK X11 API (and X11 in general) does not include the current
- /// key pressed in any modifier state snapshot for that event (e.g. if the
- /// pressed key is a modifier, that is not necessarily reflected in the
- /// modifiers).
- ///
- /// Returns null if there is no event. In this case, the caller should fall
- /// back to the standard GDK modifier state (this likely means the key
- /// event did not result in a modifier change).
- pub fn modifier_state_from_notify(self: Xkb, display_: ?*c.GdkDisplay) ?input.Mods {
- if (comptime !build_options.x11) return null;
-
- const display = display_ orelse return null;
-
- // Shoutout to Mozilla for figuring out a clean way to do this, this is
- // paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
- const xdisplay = c.gdk_x11_display_get_xdisplay(display);
- if (c.XEventsQueued(xdisplay, c.QueuedAfterReading) == 0) return null;
-
- var nextEvent: c.XEvent = undefined;
- _ = c.XPeekEvent(xdisplay, &nextEvent);
- if (nextEvent.type != self.base_event_code) return null;
-
- const xkb_event: *c.XkbEvent = @ptrCast(&nextEvent);
- if (xkb_event.any.xkb_type != c.XkbStateNotify) return null;
-
- const xkb_state_notify_event: *c.XkbStateNotifyEvent = @ptrCast(xkb_event);
- // Check the state according to XKB masks.
- const lookup_mods = xkb_state_notify_event.lookup_mods;
- var mods: input.Mods = .{};
-
- log.debug("X11: found extra XkbStateNotify event w/lookup_mods: {b}", .{lookup_mods});
- if (lookup_mods & c.ShiftMask != 0) mods.shift = true;
- if (lookup_mods & c.ControlMask != 0) mods.ctrl = true;
- if (lookup_mods & c.Mod1Mask != 0) mods.alt = true;
- if (lookup_mods & c.Mod4Mask != 0) mods.super = true;
- if (lookup_mods & c.LockMask != 0) mods.caps_lock = true;
-
- return mods;
- }
-};
diff --git a/src/build/Config.zig b/src/build/Config.zig
index 71dffce4ab..f7bf96d368 100644
--- a/src/build/Config.zig
+++ b/src/build/Config.zig
@@ -19,7 +19,7 @@ const GitVersion = @import("GitVersion.zig");
/// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly.
/// Until then this MUST match build.zig.zon and should always be the
/// _next_ version to release.
-const app_version: std.SemanticVersion = .{ .major = 1, .minor = 0, .patch = 2 };
+const app_version: std.SemanticVersion = .{ .major = 1, .minor = 1, .patch = 3 };
/// Standard build configuration options.
optimize: std.builtin.OptimizeMode,
@@ -32,7 +32,6 @@ renderer: renderer.Impl = .opengl,
font_backend: font.Backend = .freetype,
/// Feature flags
-adwaita: bool = false,
x11: bool = false,
wayland: bool = false,
sentry: bool = true,
@@ -55,6 +54,8 @@ emit_helpgen: bool = false,
emit_docs: bool = false,
emit_webdata: bool = false,
emit_xcframework: bool = false,
+emit_terminfo: bool = false,
+emit_termcap: bool = false,
/// Environmental properties
env: std.process.EnvMap,
@@ -130,12 +131,6 @@ pub fn init(b: *std.Build) !Config {
//---------------------------------------------------------------
// Feature Flags
- config.adwaita = b.option(
- bool,
- "gtk-adwaita",
- "Enables the use of Adwaita when using the GTK rendering backend.",
- ) orelse true;
-
config.flatpak = b.option(
bool,
"flatpak",
@@ -306,6 +301,27 @@ pub fn init(b: *std.Build) !Config {
break :emit_docs path != null;
};
+ config.emit_terminfo = b.option(
+ bool,
+ "emit-terminfo",
+ "Install Ghostty terminfo source file",
+ ) orelse switch (target.result.os.tag) {
+ .windows => true,
+ else => switch (optimize) {
+ .Debug => true,
+ .ReleaseSafe, .ReleaseFast, .ReleaseSmall => false,
+ },
+ };
+
+ config.emit_termcap = b.option(
+ bool,
+ "emit-termcap",
+ "Install Ghostty termcap file",
+ ) orelse switch (optimize) {
+ .Debug => true,
+ .ReleaseSafe, .ReleaseFast, .ReleaseSmall => false,
+ };
+
config.emit_webdata = b.option(
bool,
"emit-webdata",
@@ -374,7 +390,6 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
// We need to break these down individual because addOption doesn't
// support all types.
step.addOption(bool, "flatpak", self.flatpak);
- step.addOption(bool, "adwaita", self.adwaita);
step.addOption(bool, "x11", self.x11);
step.addOption(bool, "wayland", self.wayland);
step.addOption(bool, "sentry", self.sentry);
@@ -419,7 +434,6 @@ pub fn fromOptions() Config {
.version = options.app_version,
.flatpak = options.flatpak,
- .adwaita = options.adwaita,
.app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?,
.font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?,
.renderer = std.meta.stringToEnum(renderer.Impl, @tagName(options.renderer)).?,
@@ -486,6 +500,7 @@ pub const ExeEntrypoint = enum {
mdgen_ghostty_5,
webgen_config,
webgen_actions,
+ webgen_commands,
bench_parser,
bench_stream,
bench_codepoint_width,
diff --git a/src/build/GhosttyFrameData.zig b/src/build/GhosttyFrameData.zig
new file mode 100644
index 0000000000..7c22115b38
--- /dev/null
+++ b/src/build/GhosttyFrameData.zig
@@ -0,0 +1,37 @@
+//! GhosttyFrameData generates a compressed file and zig module which contains (and exposes) the
+//! Ghostty animation frames for use in `ghostty +boo`
+const GhosttyFrameData = @This();
+
+const std = @import("std");
+const Config = @import("Config.zig");
+const SharedDeps = @import("SharedDeps.zig");
+
+/// The exe.
+exe: *std.Build.Step.Compile,
+
+/// The output path for the compressed framedata zig file
+output: std.Build.LazyPath,
+
+pub fn init(b: *std.Build) !GhosttyFrameData {
+ const exe = b.addExecutable(.{
+ .name = "framegen",
+ .root_source_file = b.path("src/build/framegen/main.zig"),
+ .target = b.host,
+ });
+
+ const run = b.addRunArtifact(exe);
+
+ _ = run.addOutputFileArg("framedata.compressed");
+ return .{
+ .exe = exe,
+ .output = run.captureStdOut(),
+ };
+}
+
+/// Add the "framedata" import.
+pub fn addImport(self: *const GhosttyFrameData, step: *std.Build.Step.Compile) void {
+ self.output.addStepDependencies(&step.step);
+ step.root_module.addAnonymousImport("framedata", .{
+ .root_source_file = self.output,
+ });
+}
diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig
index 9c5f7f8094..912308e46c 100644
--- a/src/build/GhosttyResources.zig
+++ b/src/build/GhosttyResources.zig
@@ -23,9 +23,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
// Write it
var wf = b.addWriteFiles();
- const src_source = wf.add("share/terminfo/ghostty.terminfo", str.items);
- const src_install = b.addInstallFile(src_source, "share/terminfo/ghostty.terminfo");
- try steps.append(&src_install.step);
+ const source = wf.add("ghostty.terminfo", str.items);
+
+ if (cfg.emit_terminfo) {
+ const source_install = b.addInstallFile(source, "share/terminfo/ghostty.terminfo");
+ try steps.append(&source_install.step);
+ }
// Windows doesn't have the binaries below.
if (cfg.target.result.os.tag == .windows) break :terminfo;
@@ -33,10 +36,10 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
// Convert to termcap source format if thats helpful to people and
// install it. The resulting value here is the termcap source in case
// that is used for other commands.
- {
+ if (cfg.emit_termcap) {
const run_step = RunStep.create(b, "infotocap");
run_step.addArg("infotocap");
- run_step.addFileArg(src_source);
+ run_step.addFileArg(source);
const out_source = run_step.captureStdOut();
_ = run_step.captureStdErr(); // so we don't see stderr
@@ -49,23 +52,29 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
const run_step = RunStep.create(b, "tic");
run_step.addArgs(&.{ "tic", "-x", "-o" });
const path = run_step.addOutputFileArg("terminfo");
- run_step.addFileArg(src_source);
+ run_step.addFileArg(source);
_ = run_step.captureStdErr(); // so we don't see stderr
- // Depend on the terminfo source install step so that Zig build
- // creates the "share" directory for us.
- run_step.step.dependOn(&src_install.step);
-
- {
- // Use cp -R instead of Step.InstallDir because we need to preserve
- // symlinks in the terminfo database. Zig's InstallDir step doesn't
- // handle symlinks correctly yet.
- const copy_step = RunStep.create(b, "copy terminfo db");
- copy_step.addArgs(&.{ "cp", "-R" });
- copy_step.addFileArg(path);
- copy_step.addArg(b.fmt("{s}/share", .{b.install_path}));
- try steps.append(©_step.step);
+ // Ensure that `share/terminfo` is a directory, otherwise the `cp
+ // -R` will create a file named `share/terminfo`
+ const mkdir_step = RunStep.create(b, "make share/terminfo directory");
+ switch (cfg.target.result.os.tag) {
+ // windows mkdir shouldn't need "-p"
+ .windows => mkdir_step.addArgs(&.{"mkdir"}),
+ else => mkdir_step.addArgs(&.{ "mkdir", "-p" }),
}
+ mkdir_step.addArg(b.fmt("{s}/share/terminfo", .{b.install_path}));
+ try steps.append(&mkdir_step.step);
+
+ // Use cp -R instead of Step.InstallDir because we need to preserve
+ // symlinks in the terminfo database. Zig's InstallDir step doesn't
+ // handle symlinks correctly yet.
+ const copy_step = RunStep.create(b, "copy terminfo db");
+ copy_step.addArgs(&.{ "cp", "-R" });
+ copy_step.addFileArg(path);
+ copy_step.addArg(b.fmt("{s}/share", .{b.install_path}));
+ copy_step.step.dependOn(&mkdir_step.step);
+ try steps.append(©_step.step);
}
}
@@ -200,6 +209,13 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
).step);
+ // Right click menu action for Nautilus. Note that this _must_ be named
+ // `ghostty.py`. Using the full app id causes problems (see #5468).
+ try steps.append(&b.addInstallFile(
+ b.path("dist/linux/ghostty_nautilus.py"),
+ "share/nautilus-python/extensions/ghostty.py",
+ ).step);
+
// Various icons that our application can use, including the icon
// that will be used for the desktop.
try steps.append(&b.addInstallFile(
diff --git a/src/build/GhosttyWebdata.zig b/src/build/GhosttyWebdata.zig
index 6e0acaf173..860feb705a 100644
--- a/src/build/GhosttyWebdata.zig
+++ b/src/build/GhosttyWebdata.zig
@@ -73,6 +73,35 @@ pub fn init(
).step);
}
+ {
+ const webgen_commands = b.addExecutable(.{
+ .name = "webgen_commands",
+ .root_source_file = b.path("src/main.zig"),
+ .target = b.host,
+ });
+ deps.help_strings.addImport(webgen_commands);
+
+ {
+ const buildconfig = config: {
+ var copy = deps.config.*;
+ copy.exe_entrypoint = .webgen_commands;
+ break :config copy;
+ };
+
+ const options = b.addOptions();
+ try buildconfig.addOptions(options);
+ webgen_commands.root_module.addOptions("build_options", options);
+ }
+
+ const webgen_commands_step = b.addRunArtifact(webgen_commands);
+ const webgen_commands_out = webgen_commands_step.captureStdOut();
+
+ try steps.append(&b.addInstallFile(
+ webgen_commands_out,
+ "share/ghostty/webdata/commands.mdx",
+ ).step);
+ }
+
return .{ .steps = steps.items };
}
diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig
index 077da96a62..a90fc330ab 100644
--- a/src/build/SharedDeps.zig
+++ b/src/build/SharedDeps.zig
@@ -6,6 +6,7 @@ const Config = @import("Config.zig");
const HelpStrings = @import("HelpStrings.zig");
const MetallibStep = @import("MetallibStep.zig");
const UnicodeTables = @import("UnicodeTables.zig");
+const GhosttyFrameData = @import("GhosttyFrameData.zig");
config: *const Config,
@@ -13,6 +14,7 @@ options: *std.Build.Step.Options,
help_strings: HelpStrings,
metallib: ?*MetallibStep,
unicode_tables: UnicodeTables,
+framedata: GhosttyFrameData,
/// Used to keep track of a list of file sources.
pub const LazyPathList = std.ArrayList(std.Build.LazyPath);
@@ -22,6 +24,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps {
.config = cfg,
.help_strings = try HelpStrings.init(b, cfg),
.unicode_tables = try UnicodeTables.init(b),
+ .framedata = try GhosttyFrameData.init(b),
// Setup by retarget
.options = undefined,
@@ -430,20 +433,39 @@ pub fn add(
},
.gtk => {
+ const gobject = b.dependency("gobject", .{
+ .target = target,
+ .optimize = optimize,
+ });
+ const gobject_imports = .{
+ .{ "gobject", "gobject2" },
+ .{ "gio", "gio2" },
+ .{ "glib", "glib2" },
+ .{ "gtk", "gtk4" },
+ .{ "gdk", "gdk4" },
+ };
+ inline for (gobject_imports) |import| {
+ const name, const module = import;
+ step.root_module.addImport(name, gobject.module(module));
+ }
+
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
- if (self.config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts);
- if (self.config.x11) step.linkSystemLibrary2("X11", dynamic_link_opts);
+ step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
+ step.root_module.addImport("adw", gobject.module("adw1"));
+
+ if (self.config.x11) {
+ step.linkSystemLibrary2("X11", dynamic_link_opts);
+ step.root_module.addImport("gdk_x11", gobject.module("gdkx114"));
+ }
if (self.config.wayland) {
- const scanner = Scanner.create(b, .{
+ const scanner = Scanner.create(b.dependency("zig_wayland", .{}), .{
// We shouldn't be using getPath but we need to for now
// https://codeberg.org/ifreund/zig-wayland/issues/66
- .wayland_xml_path = b.dependency("wayland", .{})
- .path("protocol/wayland.xml")
- .getPath(b),
- .wayland_protocols_path = b.dependency("wayland_protocols", .{})
- .path("")
- .getPath(b),
+ .wayland_xml = b.dependency("wayland", .{})
+ .path("protocol/wayland.xml"),
+ .wayland_protocols = b.dependency("wayland_protocols", .{})
+ .path(""),
});
const wayland = b.createModule(.{ .root_source_file = scanner.result });
@@ -452,20 +474,64 @@ pub fn add(
.target = target,
.optimize = optimize,
});
+
+ // FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/blur.xml"));
+ scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/server-decoration.xml"));
scanner.generate("wl_compositor", 1);
scanner.generate("org_kde_kwin_blur_manager", 1);
+ scanner.generate("org_kde_kwin_server_decoration_manager", 1);
step.root_module.addImport("wayland", wayland);
+ step.root_module.addImport("gdk_wayland", gobject.module("gdkwayland4"));
step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
}
{
const gresource = @import("../apprt/gtk/gresource.zig");
- const wf = b.addWriteFiles();
- const gresource_xml = wf.add("gresource.xml", gresource.gresource_xml);
+ const gresource_xml = gresource_xml: {
+ const generate_gresource_xml = b.addExecutable(.{
+ .name = "generate_gresource_xml",
+ .root_source_file = b.path("src/apprt/gtk/gresource.zig"),
+ .target = b.host,
+ });
+
+ const generate = b.addRunArtifact(generate_gresource_xml);
+
+ for (gresource.blueprint_files) |blueprint_file| {
+ const blueprint_compiler = b.addSystemCommand(&.{
+ "blueprint-compiler",
+ "compile",
+ "--output",
+ });
+ const ui_file = blueprint_compiler.addOutputFileArg(b.fmt("{s}.ui", .{blueprint_file}));
+ blueprint_compiler.addFileArg(b.path(b.fmt("src/apprt/gtk/ui/{s}.blp", .{blueprint_file})));
+ generate.addFileArg(ui_file);
+ }
+
+ break :gresource_xml generate.captureStdOut();
+ };
+
+ {
+ const gtk_builder_check = b.addExecutable(.{
+ .name = "gtk_builder_check",
+ .root_source_file = b.path("src/apprt/gtk/builder_check.zig"),
+ .target = b.host,
+ });
+ gtk_builder_check.root_module.addOptions("build_options", self.options);
+ gtk_builder_check.root_module.addImport("gtk", gobject.module("gtk4"));
+ gtk_builder_check.root_module.addImport("adw", gobject.module("adw1"));
+
+ for (gresource.dependencies) |pathname| {
+ const extension = std.fs.path.extension(pathname);
+ if (!std.mem.eql(u8, extension, ".ui")) continue;
+ const check = b.addRunArtifact(gtk_builder_check);
+ check.addFileArg(b.path(pathname));
+ step.step.dependOn(&check.step);
+ }
+ }
const generate_resources_c = b.addSystemCommand(&.{
"glib-compile-resources",
@@ -497,6 +563,7 @@ pub fn add(
self.help_strings.addImport(step);
self.unicode_tables.addImport(step);
+ self.framedata.addImport(step);
return static_libs;
}
diff --git a/src/build/UnicodeTables.zig b/src/build/UnicodeTables.zig
index 0159de442c..7a4b0a5a2e 100644
--- a/src/build/UnicodeTables.zig
+++ b/src/build/UnicodeTables.zig
@@ -15,7 +15,6 @@ pub fn init(b: *std.Build) !UnicodeTables {
.root_source_file = b.path("src/unicode/props.zig"),
.target = b.host,
});
- exe.linkLibC();
const ziglyph_dep = b.dependency("ziglyph", .{
.target = b.host,
diff --git a/src/build/docker/debian/Dockerfile b/src/build/docker/debian/Dockerfile
new file mode 100644
index 0000000000..61e9e75c14
--- /dev/null
+++ b/src/build/docker/debian/Dockerfile
@@ -0,0 +1,49 @@
+ARG DISTRO_VERSION="12"
+FROM docker.io/library/debian:${DISTRO_VERSION}
+
+# Install Dependencies
+RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \
+ apt-get -qq -y --no-install-recommends install \
+ # Build Tools
+ build-essential \
+ libbz2-dev \
+ libonig-dev \
+ lintian \
+ lsb-release \
+ pandoc \
+ wget \
+ # Ghostty Dependencies
+ libadwaita-1-dev \
+ libgtk-4-dev && \
+ # Clean up for better caching
+ rm -rf /var/lib/apt/lists/*
+
+# work around the fact that Debian 12 doesn't ship a pkg-config file for bzip2
+RUN . /etc/os-release; if [ $VERSION_ID -le 12 ]; then ln -s libbz2.so /usr/lib/$(gcc -dumpmachine)/libbzip2.so; fi
+
+# Install zig
+# https://ziglang.org/download/
+ARG ZIG_VERSION="0.13.0"
+RUN wget -q "https://ziglang.org/download/$ZIG_VERSION/zig-linux-$(uname -m)-$ZIG_VERSION.tar.xz" && \
+ tar -xf "zig-linux-$(uname -m)-$ZIG_VERSION.tar.xz" -C /opt && \
+ rm zig-linux-* && \
+ ln -s "/opt/zig-linux-$(uname -m)-$ZIG_VERSION/zig" /usr/local/bin/zig
+
+WORKDIR /src
+
+COPY ./dist/linux /src/dist/linux
+COPY ./images /src/images
+COPY ./include /src/include
+COPY ./pkg /src/pkg
+COPY ./nix /src/nix
+COPY ./vendor /src/vendor
+COPY ./build.zig /src/build.zig
+COPY ./build.zig.zon /src/build.zig.zon
+COPY ./build.zig.zon.txt /src/build.zig.zon.txt
+
+RUN ZIG_GLOBAL_CACHE_DIR=/zig/global-cache ./nix/build-support/fetch-zig-cache.sh
+
+COPY ./src /src/src
+
+RUN zig build -Doptimize=Debug -Dcpu=baseline -Dapp-runtime=gtk --system /zig/global-cache/p
+
diff --git a/src/build/framegen/frames/frame_001.txt b/src/build/framegen/frames/frame_001.txt
new file mode 100644
index 0000000000..416e87931d
--- /dev/null
+++ b/src/build/framegen/frames/frame_001.txt
@@ -0,0 +1,41 @@
+
+
+ +++==*%%%%%%%%%%%%*==+++
+ ++****++ ++****++
+ ++**++ ++**++
+ xx**+= o+*%$@@@@@@$%*+o =+**xx
+ xx**oo ·=$@@@@@@@$$$$$$$$@@@@@@@$=· oo**xx
+ xx** x$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$x **xx
+ ox** ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· **xo
+ ==+~ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ~+==
+ x+++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ +++x
+ == ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ==
+ ox++ ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ++xo
+ +++~ @$$$$$@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$@@%%%%%%$$$$$$$@@@@@$$$@@@@@@@@@@@@@@@@@@@@$$$$$$ ==
+ == @$$$$* $$$$% =$$$$$@ ==
+ == ·$$$$@ x@$@ @$$$$$· ==
+ == ·@$$$$% ·$$$$% *$$$$$@· ==
+ == ·@$$$$@@$%%$$$$$$@@@@@@@@$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==x· $@@$$$$$$$$$@@@@@@@@@@$$$$$$$$@@@@@@@@@@$$$$$$$$$@@$ ·+==
+ ++++ =@@@@@@@@@* x$@@@@@@@@$x *@@@@@@@@@= ++++
+ xx==++ ++==oo
+ ++===+ ++%%+o o+%%++ +===++
+ ++=====%+=++++*=*========***++++***========*=*++++=+%=====++
+ xx++==******====++ ++==********==++ ++====******==++xx
+ ++++ ++++ ++++
+
diff --git a/src/build/framegen/frames/frame_002.txt b/src/build/framegen/frames/frame_002.txt
new file mode 100644
index 0000000000..e74134e3ed
--- /dev/null
+++ b/src/build/framegen/frames/frame_002.txt
@@ -0,0 +1,41 @@
+
+ ++++++++++++
+ ++==*%%%**=++++++=**%%%*==++
+ ++**=* *=**++
+ x+**+= =+**+x
+ ++== o=%@@@@@@@@@@@@@@@@%=o ==++
+ ===+ +$@@@@@$$$$$$$$$$$$$$$$@@@@@$+ +===
+ ++=+ =@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@= +=++
+ ++== $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ ==++
+ ** $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ **
+ +++o +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ o+++
+ == %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ xx++ %@$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ ==+· x@$$$@@%=*%$$@@@@@@@@@@@$$$$$@@@@@@@@@@@@@@@@@@$$$$$@x ·+==
+ == @$$$$% ~$@$$$@= x@$$$$$@ ==
+ == $$$$@* %$$@· @$$$$$ ==
+ == ·@$$$@* $$$$+ $$$$$@· ==
+ == ·@$$$$$~ ~ox+=%@@$$$@@*==============*$@$$$$$@· ==
+ == ·@$$$$$@@@@@@@@@@@@@@@@@$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == $@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@$ ==
+ ==+o +@@@$$$$$$$@@@$***%@@@@$$$$$$@@@@%***$@@@$$$$$$$@@@+ o+==
+ ++== ·=$@@@@@$*~ +%@@@@@@%+ ~*$@@@@@$=· ==++
+ ===+ +===
+ ++====x+ *=**== ==**=* +x====++
+ ++=====**%******=========**%****%**=========******%**=====++
+ ++++========++xx ++++========++++ xx++========++++
+
+
diff --git a/src/build/framegen/frames/frame_003.txt b/src/build/framegen/frames/frame_003.txt
new file mode 100644
index 0000000000..6da88ffbf3
--- /dev/null
+++ b/src/build/framegen/frames/frame_003.txt
@@ -0,0 +1,41 @@
+
+ ++====****====++
+ ==***%==xo ox==%***==
+ ===*++ ++*===
+ ++**x+ ·oxx++xxo· +x**++
+ ===+ o=$@@@@@@@@@@@@@@@@@@$=o +===
+ **+x o%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%o x+**
+ ==+o ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ o+==
+ ++++ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ++++
+ ox== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xo
+ ==+~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@+ ·+*$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==+ *@$$$@ o=%@@@@$$$$@@@@@@@@@@@@@@@@@@@@$$$$@* +==
+ == @$$$$$o %$$$$$ *$$$$$@ ==
+ == $$$$$$@@=· @$$@ @$$$$$ ==
+ == ·@$$$$$x %$$$$% =$$$$$@· ==
+ == ·@$$$$@ ·x*$@@@$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$@x o=%@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@% ==
+ ==+x ~$@@@@$$$$@@@@%+xx=$@@@@$$$$@@@@$=xx+*@@@@$$$$@@@@$~ x+==
+ ++== x*$@@@$*x ·=%$@@$%=· x*$@@@$*x ==++
+ ==== =+==
+ x+====+= x+*===+= ======+x =+====xx
+ ++====*%%%%***====++====*%*%%*%*====++=====**%%%%*====++
+ xx++++====++++ ++++====++++ ++++====++++xx
+
+
diff --git a/src/build/framegen/frames/frame_004.txt b/src/build/framegen/frames/frame_004.txt
new file mode 100644
index 0000000000..5ad3a81122
--- /dev/null
+++ b/src/build/framegen/frames/frame_004.txt
@@ -0,0 +1,41 @@
+
+ ++==************==++
+ ++***%=*x~ ~x*=%***++
+ ++**+= ==**++
+ ==== ~x=*%%%%%%*=x~ ====
+ xx**++ o*$@@@@@@@@@@@@@@@@@@@@$*o ++**xo
+ oo**o~ *@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@* ~o**oo
+ **o· =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ·o**
+ ==+x %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% x+==
+ xx== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==xx
+ ==x· @@$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==
+ xx== @$$$@% x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xx
+ +++o @$$$@* ~=@@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$@ o+++
+ == %$$$$$% +$@@$$$$@@%**************%@@$$$$$% ==
+ == @$$$$$@@%+ $$$$$+ ~@$$$$@ ==
+ == $$$$$$$@@@@= *@$$@· @$$$$$ ==
+ == ~@$$$$$@= %@$$$$@+ o$$$$$$@~ ==
+ == ·@$$$$@= ~*@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@@$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@+ ==
+ ==++ *@@@@@@@@@@@%x ~=@@@@@@@@@@@@=~ x%@@@@@@@@@@@* ++==
+ x+==x· o=***=x ~+****+~ x=***=o ·x==xx
+ ====xo xx x+ ox====
+ ======+x =+=*====++ ++====*=+= x+======
+ ++==***%********++++==************==++++********%***==++
+ ++++++++++ +x++++++++xx ++++++++++
+
+
diff --git a/src/build/framegen/frames/frame_005.txt b/src/build/framegen/frames/frame_005.txt
new file mode 100644
index 0000000000..e93b295d5d
--- /dev/null
+++ b/src/build/framegen/frames/frame_005.txt
@@ -0,0 +1,41 @@
+
+ +==*****%%%%*****==+
+ ++***%=+ +=%***++
+ ++**+= =+**++
+ **== ·x=*%$$$$$$%*=x· ==**
+ xx**xx x%@@@@@@@@@$$$$@@@@@@@@@%x xx**xx
+ xx** ~%@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@%~ **xx
+ **o· %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% ·o**
+ ==+o $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ o+==
+ ++++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% +++x
+ == @@$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ xx== @$$$$o o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xx
+ +++o @$$$@% o*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$@ o+++
+ == $$$$$$$o o*@$$$$$@@=+++++++++++++++$@$$$$$$ ==
+ == @$$$$$$@@$=· $$$$@x @$$$$@ ==
+ == $$$$$$$@@@$= $$$$@~ @$$$$$ ==
+ == ·@$$$$$@* x@@$$$$@*~ ~=@$$$$$@· ==
+ == ·@$$$$@% x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@~ ==
+ ++++ +@@@@@@@@@@@*~ x$@@@@@@@@@@$x ·*@@@@@@@@@@@+ ++++
+ xx==+~ x+=+x· o+==+o ·x+=+x ~+==xo
+ ++==+x == == x+==++
+ ++====++ ox=+==**==+= =+=***==+*+x ++====++
+ x+==**********==++++++************++++++==**********==++
+ ++++++xx xx++++xx ++++++++
+
+
diff --git a/src/build/framegen/frames/frame_006.txt b/src/build/framegen/frames/frame_006.txt
new file mode 100644
index 0000000000..f9ad605202
--- /dev/null
+++ b/src/build/framegen/frames/frame_006.txt
@@ -0,0 +1,41 @@
+
+ +++==*%%*%%%%%%*%%*===++
+ ++**=*++ ++*=**++
+ ++**=+ +=**++
+ xx**+= ~+*%$$@@@@$$%*+~ =+**+x
+ xx**ox +$@@@@@@@@$$$$$$@@@@@@@@$+ xo**xx
+ x+** x$@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@$x **+x
+ oo** ·$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$· **oo
+ ==+~ ·@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@· ~+==
+ ++++ %@$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ == @@$$@$%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ox++ ~@$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ++xo
+ +++~ @$$$@% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$@ ~+++
+ == $$$$$$@% =@$$$$$@%xooooooooooooooo*@$$$$$$ ==
+ == @$$$$$$@@@$+ $$$$@o @$$$$@ ==
+ == ·$$$$$$$@@@%+ $$$$@o @$$$$$· ==
+ == ·@$$$$$$* =@@$$$$@$xoooooooooooooox*@$$$$$@· ==
+ == ·@$$$$@% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·x==
+ ++++ ~%@@@@@@@@@$x ·*@@@@@@@@@@*· x$@@@@@@@@@%~ ++++
+ xx==+x ooo· ~oo~ ·oo~ x+==xo
+ x+==++ +%% %%+o ++==+x
+ ++=*==+*xx ++*=**==**+*++ ++*+**==**=*++o~ xx*+==**++
+ ++==********=+xx ++==********==++ xx++********==++
+
+
+
diff --git a/src/build/framegen/frames/frame_007.txt b/src/build/framegen/frames/frame_007.txt
new file mode 100644
index 0000000000..7825600824
--- /dev/null
+++ b/src/build/framegen/frames/frame_007.txt
@@ -0,0 +1,41 @@
+
+ ++++=**%**%%%%**%**=++++
+ ==****++ ++*=**==
+ ++**=+ +=**++
+ x+**+= ~+*%$$$@@$$$%*+~ =+**+x
+ ++**ox +$@@@@@@@@$$$$$$@@@@@@@@$+ xo**++
+ x+** x$@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@$x **+x
+ xx** $@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$ **xx
+ ==+~ ·@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@· ~+==
+ x+++ %@$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% +++x
+ == @@$$@$%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ xx++ ~@$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ +++x
+ +++~ @$$$@$ +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$@ ~+++
+ == $$$$$$@%· =@$$$$$@%xooooooooooooooo*@$$$$$$ ==
+ == @$$$$$$@@@$=· $$$$@o @$$$$@ ==
+ == ·$$$$$$$@@@$+ $$$$@o @$$$$$· ==
+ == ·@$$$$$$% =@$$$$$@$+xxxxxxxxxxxxxxx%@$$$$$@· ==
+ == ·@$$$$@% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ x%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·+==
+ ++++ ·%@@@@@@@@@%o =$@@@@@@@@$= ~%@@@@@@@@@%· ++++
+ ==+x ··· ·· ·· x+==
+ ++=*++ ++%% %*++ ++*=++
+ ++==**=*++ooxx+=*=**==**=*=+xxxx+=%=**==**=*=+xxxx++*=**==+x
+ +++=******==++xx ++++********++++ xx++==******++++
+
+
+
diff --git a/src/build/framegen/frames/frame_008.txt b/src/build/framegen/frames/frame_008.txt
new file mode 100644
index 0000000000..3e4706fdd5
--- /dev/null
+++ b/src/build/framegen/frames/frame_008.txt
@@ -0,0 +1,41 @@
+
+ ++++==**%%%%%%%%**==++++
+ +=***%== ==%***=+
+ ++**+= =+**++
+ x+**=+ ·o+*%$$$$$$%*+o· +=**+x
+ x+**xx x%@@@@@@@@@$$$$@@@@@@@@@%x xx**+x
+ x+**~ ~%@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@%~ ~**+x
+ ox**o· %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% ·o**xo
+ ==+o $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ o+==
+ x+++ %@$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% +++x
+ ==x @@$$@@%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ x==
+ x++= @$$$$ x%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ =+xx
+ +++o @$$$@% +$@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$@ o+++
+ == $$$$$$@% ·*@$$$$$@$+xxxxxxxxxxxxxxx%@$$$$$% ==
+ == @$$$$$$@@@%+ $$$$@o @$$$$@ ==
+ == $$$$$$$@@@$*· $$$$@~ @$$$$$ ==
+ == ·@$$$$$@%· +@$$$$$@%x~~~~~~~~~~~~~~o*@$$$$$@· ==
+ == ·@$$$$$$ x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@$*%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==x· $@@$$$$$$$$$@@@@@@@@@@$$$$$$$$@@@@@@@@@@$$$$$$$$$@@$ ·+==
+ +++= =@@@@@@@@@*· x%@@@@@@@@%x *@@@@@@@@@* =+++
+ ==++ +===
+ xx=*=+ ++%%+x x+%%++ +=**xx
+ ++**=*=+++++==*=**++**=%==++++==%=**++**=***+++++=*=**++
+ xx++==**==++++ ++========++ ++++======++xx
+
+
+
diff --git a/src/build/framegen/frames/frame_009.txt b/src/build/framegen/frames/frame_009.txt
new file mode 100644
index 0000000000..00e5a6170f
--- /dev/null
+++ b/src/build/framegen/frames/frame_009.txt
@@ -0,0 +1,41 @@
+
+ ++++==***%%%%%%***==++++
+ +=****=*o~ ~o*=%***=+
+ ++**=* *=**++
+ x+**== ~x=*%%%%%%*=x~ ==**+x
+ ++**++ o*$@@@@@@@@@@@@@@@@@@@@$*o ++**++
+ x+**o~ *@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@* ~o**+x
+ xx**o· =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ·o**xx
+ ==+o %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% o+==
+ x+++ *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* +++x
+ ==x· @@$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==
+ x+== @$$$$~ ·*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==+x
+ +++o @$$$@% ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$@ o+++
+ == %$$$$$$+ o%@$$$$$@@=++++++++++++++=$@$$$$$% ==
+ == @$$$$$$@@$*~ $$$$@x @$$$$@ ==
+ == $$$$$$$$@@@%~ $$$$@~ @$$$$$ ==
+ == ~@$$$$$@$~ x@$$$$$@*~ ·+@$$$$$@~ ==
+ == ·@$$$$$$ o*@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@$**@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@$$$$$$$$$@@@@$$@@@@$$$$$$$$@@@@$$@@@@$$$$$$$$$@@$ ·+==
+ x+== =$@@@@@@@@* o%@@@@@@@@%o *$@@@@@@@$= ==+x
+ ==++ ++==
+ ===+ +=%%++ ++%%== +===
+ ++**=%==++++****==++**=***++++***=**++==****++++==%***++
+ ++======++++ +++======++x ++++=====+++
+
+
+
diff --git a/src/build/framegen/frames/frame_010.txt b/src/build/framegen/frames/frame_010.txt
new file mode 100644
index 0000000000..bdfaa69d90
--- /dev/null
+++ b/src/build/framegen/frames/frame_010.txt
@@ -0,0 +1,41 @@
+
+ xx++==************==++xx
+ ++***%*%++ ++%*%***++
+ ++**=*+x x+*=**++
+ xx====x ·ox+====+xo· x====xx
+ x+**++ x%@@@@@@@@@@@@@@@@@@@@%x ++**++
+ xx**+o +$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$+ o+**xx
+ oo**+~ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x ~+**oo
+ ==++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++==
+ ++== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==++
+ ==+· $@$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @@$$@* ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xx
+ +++x @$$$$$ ·=@@@@$$$$$$$$$$@@@@@@@@@@@@@@@$$$$$$@ x+++
+ == *$$$$$$ ~*@@$$$$$@@$%%%%%%%%%%%%%%$@@$$$$$* ==
+ == @$$$$$$@@*· $$$$$= o$$$$$@ ==
+ == $$$$$$$$@@@@x $$$$@ @$$$$$ ==
+ == ~@$$$$$@@+ %$$$$$@x ~$$$$$$@~ ==
+ == ·@$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@@$$$$$$$$@@@@$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ~+==
+ xx== +$@@@@@@@$= ~%@@@@@@@@%~ =$@@@@@@@$+ ==xx
+ ===+ +===
+ ==== ==%%++ ++%%== ====
+ ++**=***++==%***==++***%**++++**%***++==***%==++**%=**++
+ ++++==++++ x+++====+++x ++++==+++x
+
+
+
diff --git a/src/build/framegen/frames/frame_011.txt b/src/build/framegen/frames/frame_011.txt
new file mode 100644
index 0000000000..e3170d5333
--- /dev/null
+++ b/src/build/framegen/frames/frame_011.txt
@@ -0,0 +1,41 @@
+
+ ++++==********==++++
+ ++*****%**+x x+**%*****++
+ ++**=*++ ++*=**++
+ ====++ ~ooxxoo~ ++====
+ xx**++ ~+%@@@@@@@@@@@@@@@@@@%+~ ++**xx
+ ++**++ ~*@@@@@$$$$$$$$$$$$$$$$$$@@@@@*~ ++**+x
+ ==+x ·%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%· x+==
+ ==++ x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x ++==
+ xx== ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==xx
+ ==+~ %@$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~+==
+ xx== @@$$@$o +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xx
+ +++x $$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ x+++
+ ==+ =@$$$$$ +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$@= +==
+ == @$$$$$$@%o x$$$$$% =$$$$$@ ==
+ == $$$$$$$$@@@@= $@$$@ @$$$$$ ==
+ == ~@$$$$$@@*~ +$$$$$% *$$$$$@~ ==
+ == ·@$$$$$$ ·*@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@x·~=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ~+==
+ xx== +$@@@@@@@$= ~*@@@@@@@@*~ =$@@@@@@@$+ ==xx
+ ===+ +===
+ ====o ==%%++ ++%%=+ ====
+ ++**=***=+==%***++++==***%====%***==++++***%====***=**++
+ xx++++++++ ++====++ ++++++++xx
+
+
+
diff --git a/src/build/framegen/frames/frame_012.txt b/src/build/framegen/frames/frame_012.txt
new file mode 100644
index 0000000000..90a65ed526
--- /dev/null
+++ b/src/build/framegen/frames/frame_012.txt
@@ -0,0 +1,41 @@
+
+ ++++++========++++++
+ ++==***%*%**==++++==**%*%***==++
+ ++==**=*xo ox*=**==++
+ ++**+= =+**++
+ xx==*= ·+*$@@@@@@@@@@@@@@$*+· =*==xx
+ xx===+ o%@@@@@$$$$$$$$$$$$$$$$@@@@@%o +===xx
+ ==++ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x +===
+ ++=+ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% +=++
+ xx== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ==xx
+ ==+x x@@$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x x+==
+ xx== %@$$$@%**$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==xx
+ ++++ %@$$$$ =@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ ==+· o@$$$$$ ·=@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$@o ·+==
+ == @$$$$$@$~ ~$@$$$$@+ x@$$$$$@ ==
+ == $$$$$$$$@@@%o $$$$@· @$$$$$ ==
+ == ·@$$$$$$@@%x $$$$$+ ~$$$$$@· ==
+ == ·@$$$$$$~ +$@$$$$$@@%***************@@$$$$$@· ==
+ == ·@$$$$@% +$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$= x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ == @@$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@@$$$$$$$@@@% ~+==
+ x+== +%@@@@@@@$+ ~*@@@@@@@@*~ +$@@@@@@@%+ ==+x
+ ++=+ +=++
+ ++==o ==%%++ ++%%== o==++
+ ++***%**====%***++++==***%====%***==++++***%====**%***++
+ ++++++++ ++++++++ x+++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_013.txt b/src/build/framegen/frames/frame_013.txt
new file mode 100644
index 0000000000..b2b6a980c5
--- /dev/null
+++ b/src/build/framegen/frames/frame_013.txt
@@ -0,0 +1,41 @@
+
+ ++++++++====++++++++
+ ++++*******%********%*******++++
+ +x==**=*=+ +=*=**==xx
+ ++**== ==**++
+ ====++ ·x*%@@@@@@@@@@@@%*x· ++====
+ ===+ +$@@@@@@$$$$$$$$$$$$@@@@@@$+ +===
+ ===+ ·%@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@%· +===
+ ++== +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ==++
+ xx==o· =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ·o==xo
+ ==++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++==
+ ox== +@$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ ++++ =@$$$$~ ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ++++
+ ==+· ·@$$$$$ ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$@· ·+==
+ == @$$$$$$+ ~*@$$$$$@$+xxxxxxxxxxxxxx+%@$$$$$@ ==
+ == @$$$$$$@@@*o $$$$@x @$$$$@ ==
+ == ·@$$$$$$@@@$= $$$$@~ @$$$$@· ==
+ == ·@$$$$$@* +@$$$$$@%o~~~~~~~~~~~~~~o*@$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ~+==
+ xx== +$@@@@@@@$= ~%@@@@@@@@%~ =$@@@@@@@$+ ==xx
+ ++=+ +=++
+ ++==o ==%%++ ++%%== o==++
+ ++***%**====%***++++==***%====%***==++++***%====**%***++
+ ++++++++ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_014.txt b/src/build/framegen/frames/frame_014.txt
new file mode 100644
index 0000000000..96194979cd
--- /dev/null
+++ b/src/build/framegen/frames/frame_014.txt
@@ -0,0 +1,41 @@
+
+ xx++++++++++++xx
+ ++==*****%*%%%%%%*%*****==++
+ ++*****%=+ +=%*****++
+ ++====+* *+====++
+ ++==+= ·x+*%%$$$$%%*+x· =+==++
+ ====xx x%@@@@@@@@@$$$$@@@@@@@@@%x xx====
+ ++==o~ ~%@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@%~ ~~==+=
+ ++==o· *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ·o==++
+ xx==+o $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ o+==xx
+ ++++ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ++++
+ ox==x @@$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ x==xo
+ ++++ @$$$@$x ·+$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++++
+ ==+o @$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+==
+ == $$$$$$$ =$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$$ ==
+ == @$$$$$$@%o x$$$$$% *$$$$$@ ==
+ == $$$$$$$$@@@@+ $$$$@ @$$$$$ ==
+ == ·@$$$$$$@%o x$$$$$% *$$$$$@· ==
+ == ·@$$$$$$ =$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@$x ·+$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@$$$$$$$$$@@@@$$@@@@$$$$$$$$@@@@$$@@@@$$$$$$$$$@@% ·+==
+ xx== +$@@@@@@@$= o%@@@@@@@@%o =$@@@@@@@$+ ==xx
+ ++=+ +=++
+ ++== ==%%++ ++%%== ==++
+ x+=*=***++==%***++xx==*%**====**%*==xx++***%==++***=*=+x
+ ++++++++ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_015.txt b/src/build/framegen/frames/frame_015.txt
new file mode 100644
index 0000000000..33cf5ec652
--- /dev/null
+++ b/src/build/framegen/frames/frame_015.txt
@@ -0,0 +1,41 @@
+
+ ++++++++++++
+ xx++==****************==++xx
+ ++==**=***++o o++***=**==++
+ x+=====*+x x+*=====+x
+ ++====ox ·ox++==++xo· xo====++
+ ++==++ x*$@@@@@@@@@@@@@@@@@@$*x ++==++
+ ++==+x x$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$x x+==++
+ ++==+o x@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@x o+==++
+ ===+ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= +===
+ ++== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==++
+ ==+· $@$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ++== @@$$$@$**@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==++
+ +++x @$$$$$ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == *$$$$@$ ~*@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$* ==
+ == @$$$$$@$~ o$$$$$$@= x@$$$$$@ ==
+ == $$$$$$$$@@@%~ $$$$@· @$$$$$ ==
+ == ~@$$$$$$@@$x $$$$$+ ·@$$$$@~ ==
+ == ·@$$$$$$o x$@$$$$$@@*==============*$@$$$$$@· ==
+ == ·@$$$$@% x%@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$+ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· $@@$$$$$$$$$@@@@$@@@@@$$$$$$$$@@@@@$@@@@$$$$$$$$$@@$ ·+==
+ xx== =$@@@@@@@@* o%@@@@@@@@%o *@@@@@@@@$= ==xo
+ ++=+ +=++
+ ++== ==%%++ ++%%== ==++
+ x+**=%==++++%***=+x+==*%**++++**%*==+++=***%++++=*%=**+x
+ ++++++++ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_016.txt b/src/build/framegen/frames/frame_016.txt
new file mode 100644
index 0000000000..50eea78320
--- /dev/null
+++ b/src/build/framegen/frames/frame_016.txt
@@ -0,0 +1,41 @@
+
+ xxxx
+ ++++==************==++++
+ xx++==****%%=*++++++++*=%*****==++xx
+ ++=====*+~ ~+*=====++
+ ++====+= =+====++
+ ++===+ ·+*$@@@@@@@@@@@@@@$*+· +===++
+ ++===+ o%@@@@@$$$$$$$$$$$$$$$$@@@@@%o +===++
+ xx===+ x@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@x +===xx
+ ==++ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ++==
+ ++== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ==++
+ ==+x x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x x+==
+ xx== %@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==++
+ ++++ %@$$$$* ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ ==x· o@$$$$$ ~*@@@@$$$$$$$$$$@@@@@@@@@@@@@@$$$$$$$@o ·x==
+ == @$$$$$$ ~*@@$$$$$@@$%%%%%%%%%%%%%%$@@$$$$$@ ==
+ == $$$$$$$@@= $$$$$* x$$$$$$ ==
+ == ·@$$$$$$$@@@@x $$$$@ @$$$$@· ==
+ == ·@$$$$$@@+ *$$$$$$o ·$$$$$$@· ==
+ == ·@$$$$$$ x$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·+==
+ xx== *@@@@@@@@@%~ +$@@@@@@@@$+ ~%@@@@@@@@@* ==xx
+ ===+ +===
+ ===+ ++%% %%++ +===
+ ++***%++++++**%*=+++===*==++x+==*===++++*%**++x+++%***++
+ x+++====++ ++==++++ ++====+++x
+
+
+
diff --git a/src/build/framegen/frames/frame_017.txt b/src/build/framegen/frames/frame_017.txt
new file mode 100644
index 0000000000..e05674a723
--- /dev/null
+++ b/src/build/framegen/frames/frame_017.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++============++++++
+ ++==**==*%*%********%*%*==**==++
+ ++=====*== ==*=====++
+ ++======+x x+======+x
+ ++====++ o=%$@@@@@@@@@@$%=o ++====++
+ ++==== x%@@@@@@$$$$$$$$$$$$@@@@@@%x ====+x
+ xx===+ *@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* +===xx
+ ==== +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ====
+ x+==x· +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·x==++
+ ==++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++==
+ xx== x@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==xo
+ ++++ +@$$$@@=ox%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ++++
+ ==+~ @$$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+==
+ == $$$$$$$ x%@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$$ ==
+ == @$$$$$@@+ *$$$$$$~ %$$$$$@ ==
+ == ·@$$$$$$$@@@@+ $@$$@ @$$$$@· ==
+ == ·@$$$$$$@$= ~$$$$$* x$$$$$@· ==
+ == ·@$$$$$$ o%@@$$$$$@@@$$$$$$$$$$$$$$$@@$$$$$@· ==
+ == ·@$$$$$$ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@% ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·+==
+ ++++ ~%@@@@@@@@@%o =@@@@@@@@@@= o%@@@@@@@@@%~ ++xx
+ ==+x ~~~ ·~~· ~~· x+==
+ ==++ x+%% %%++ +===
+ ++**=%++ooox==*===++**=%++xoox++%=**++===*==xooo++%=**++
+ ++++==++++ ++++====++++ ++++==++++
+
+
+
diff --git a/src/build/framegen/frames/frame_018.txt b/src/build/framegen/frames/frame_018.txt
new file mode 100644
index 0000000000..5e92ec0e03
--- /dev/null
+++ b/src/build/framegen/frames/frame_018.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++++++++++++++++
+ xx++==****=**%%%%%%**=****==+++x
+ x+++====*%==o~ ~o==%*====+++x
+ ++====+= =+====++
+ ====++ o+=*%%%%%%*=+o ++====
+ ====xx o*$@@@@@@@@@@@@@@@@@@@@$*o xx====
+ ====o· ·*@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@*· ·o====
+ ++==o· *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ·o==++
+ xx==+o $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ o+==xx
+ ==++ *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ++==
+ xx==x· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==xo
+ ++++ @$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++++
+ ==+o @$$$$$~ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+==
+ == %$$$$@% ·*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$% ==
+ == @$$$$$$= ~*@$$$$$@$+xxxxxxxxxxxxxxx%@$$$$$@ ==
+ == $$$$$$$@@@*o $$$$@o @$$$$$ ==
+ == ·@$$$$$$@@@$+ $$$$@~ @$$$$@· ==
+ == ·@$$$$$@* +@$$$$$@%o~~~~~~~~~~~~~~o*@$$$$$@· ==
+ == ·@$$$$@% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ +%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ·@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@· x==
+ ++++ x$@@@@@@@@@@= o%@@@@@@@@@@%o =@@@@@@@@@@$x ++++
+ ==+o ox++o ~x++x~ o++xo o+==
+ **+x ** ** x+**
+ ++**+=o~ ++*+**==**+*xo ox*+**++**+*++ ~o=+**++
+ ++==****++++ ++==****==++ ++++=***==++
+
+
+
diff --git a/src/build/framegen/frames/frame_019.txt b/src/build/framegen/frames/frame_019.txt
new file mode 100644
index 0000000000..f528dc8766
--- /dev/null
+++ b/src/build/framegen/frames/frame_019.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++++++++++++
+ ++++==****************==++++
+ ++===****%++~~ ~~++%****===++
+ ++=====*+x x+*=====++
+ ++====xx ·ox+====+xo· xx====++
+ ====++ x%$@@@@@@@@@@@@@@@@@@$%x ++====
+ ++==+o x$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$x o+==++
+ ++==+o x$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$x o+==++
+ ox==++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++==xo
+ +++= x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x =+++
+ ox==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==xo
+ ++== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==++
+ ==+x @$$$$@= +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+==
+ == *$$$$$$ =$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* ==
+ == @$$$$$$· =@@$$$$$@@%**************%@@$$$$$@ ==
+ == $$$$$$$@@*o $$$$$= ~@$$$$$ ==
+ == ~@$$$$$$$@@@@o $$$$@ @$$$$@~ ==
+ == ·@$$$$$@$o ·%$$$$$@+ o$$$$$$@· ==
+ == ·@$$$$$$ =$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@%==$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@o ==
+ ++++ =@@@@@@@@@@@%o +$@@@@@@@@@@$+ o%@@@@@@@@@@@= ++++
+ **x~ ~+===+o x====x o+===+~ ~+**
+ **xx ++ ++ xx**
+ ==**++ ~o*+**==**+= =+**==**+*o~ ++====
+ +==*****=+++ ++=******=++ +++==****==+
+
+
+
diff --git a/src/build/framegen/frames/frame_020.txt b/src/build/framegen/frames/frame_020.txt
new file mode 100644
index 0000000000..02720e639c
--- /dev/null
+++ b/src/build/framegen/frames/frame_020.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++++
+ ++++==************==++++
+ ++++****=***=+xo~~~~ox+=%**=****++++
+ xx++===*+= =+*===++xx
+ ++====++ ·~~~~· ++====++
+ ++===+ x*$@@@@@@@@@@@@@@@@$*x +===++
+ ++==++ =@@@@@$$$$$$$$$$$$$$$$$$@@@@@= ++==++
+ x+===+ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ++==+x
+ ==++ ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ ++==
+ ++== @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==++
+ ==+o *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* o+==
+ ++== $@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==++
+ +++x $@$$$@$o x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ x+++
+ ==x· +@$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·x==
+ == @$$$$$$ +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@%x x$$$$$% =$$$$$$ ==
+ == ~@$$$$$$$@@@@= $@$$@ @$$$$@· ==
+ == ·@$$$$$@@*~ +$$$$$% *$$$$$@· ==
+ == ·@$$$$$$ ·=@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@x·~=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@* ==
+ +++x $@@@@$$$@@@@@*x~o+%@@@@@$$@@@@@%+o~o=$@@@@$$@@@@@$ x+++
+ **o ~=%$$$%=o +*$$$$*+ o=%$$$%=~ o**
+ xx**o~ ~~**xx
+ xx**+* ++=***=*+x x+*=****++ *+**xx
+ ++==*%%%%%**== x+++*%%%%%%*+++x ++**%%%%%*==++
+
+
+
diff --git a/src/build/framegen/frames/frame_021.txt b/src/build/framegen/frames/frame_021.txt
new file mode 100644
index 0000000000..e7305fbc69
--- /dev/null
+++ b/src/build/framegen/frames/frame_021.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++====********====++++
+ ++==**=**%**++++++++**%**=**==++
+ ++=*=*=* ~ *=*===++
+ ++====+= =+====+x
+ ++===+ ~+%$@@@@@@@@@@@@@@$%+· +===+x
+ ++==++ x$@@@@@$$$$$$$$$$$$$$$$@@@@@%x +===+x
+ xx==++ +@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ +===xo
+ ==++ $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ++==
+ ++== $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ==++
+ ==+o +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x x+==
+ xx== %@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xx
+ ++++ %@$$$@@+~o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++++
+ ==x· +@$$$$% ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ·+==
+ == @$$$$$$ ~*@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@*· =$$$$$$ %$$$$$$ ==
+ == ~@$$$$$$$@@@@+ $$$$@ @$$$$@ ==
+ == ~@$$$$$@@%x o$$$$$* =$$$$$@ ==
+ == ~@$$$$$$ +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == ~@$$$$@% x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@$$$$$@$~ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++o o@@@@$$$$$@@@@%*+=*$@@@$$$$$$@@@@*=+=%@@@@$$$$$@@@@o o+++
+ o ** +%@@@@@%= o*$@@@@$*o +%@@@@@%+ **
+ ++** **++
+ x+**+= x****== ==**=*xo =+**++
+ ++**%%%%%%%*==++ ++==%%%%%%%*==++ x+==*%%%%%%%**=+
+
+
+
diff --git a/src/build/framegen/frames/frame_022.txt b/src/build/framegen/frames/frame_022.txt
new file mode 100644
index 0000000000..943c443257
--- /dev/null
+++ b/src/build/framegen/frames/frame_022.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++========++++++
+ ++==*****%*%********%%%*****+++x
+ ++==***%++ ++*=**==+x
+ ==**+= *+**++
+ ox====oo o=%$@@@@@@@@@@@@$%+~ xx====
+ xx===+ ~*@@@@@@$$$$$$$$$$$$$@@@@@@$+ ====
+ ===+ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ++==
+ ==++ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ==++
+ ++== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·x==xx
+ ==+o +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ xx== %@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==xo
+ +++x $@$$$@@*+=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ++xx
+ ==x =@$$$$% +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ == @$$$$@* +$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$% ==
+ == o$$$$$$@$o ·$$$$$$$o o$$$$$$@ ==
+ == +@$$$$$$$@@@@o @$$$@ @$$$$$ ==
+ == =@$$$$$$@@=· ~@$$$@+ +@$$$$@ ==
+ == +@$$$$$$ ~*@@$$$$$@@$%%%%%%%%%%%%%%$@@$$$$$@ ==
+ == +@$$$$@* ~*@@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@ ==
+ == +@$$$$$@= ·*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ == @@$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ~+==
+ xx== +$@@@@@@@$= ~*@@@@@@@@*~ +$@@@@@@@$+ ==xx
+ ++=+ +=++
+ ++**oo ==%%=+ ++%%== oo=*++
+ x+==*%=*===**=**++++++*%**====**%*++++++******==*=%*==+x
+ ++++++ x+++++++ ++++++
+
+
diff --git a/src/build/framegen/frames/frame_023.txt b/src/build/framegen/frames/frame_023.txt
new file mode 100644
index 0000000000..673dd9482b
--- /dev/null
+++ b/src/build/framegen/frames/frame_023.txt
@@ -0,0 +1,41 @@
+
+
+
+ x+++++++==++++++++
+ ++++*******%%%**%%%*%*%****=++
+ xx==**=*++ ==****++
+ ++**+= ++*=**++
+ xx====oo ~+*$$@@@@@@@@@@$%=o +x*=++
+ x+===+ ·=@@@@@@@$$$$$$$$$$$$@@@@@@%o ~====
+ ===+ o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ==++
+ ==++ %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ ==++
+ ++== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ~+==
+ ==+o +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ xx== $@$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==
+ +++x @@$$$@@%=*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xx
+ == %@$$$$* ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == @$$$$@= o*@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$+ ·+==
+ ox== =$$$$$$@%· o@$$$$$@o +@$$$$$$ ==
+ ox++ %@$$$$$$$@@@%· @$$$@ @$$$$@ ==
+ ox++ %@$$$$$@@@*~ ~@$$$@~ +@$$$$@ ==
+ ox++ %@$$$$$$ ·=@@$$$$$@@***************%@@$$$$$@ ==
+ ox++ %@$$$$@+ =$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@ ==
+ ox++ %@$$$$$$x =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ %@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@$ ·+==
+ ++++ ·*@@@@@@@@@%o +@@@@@@@@@@= ~%@@@@@@@@@* ==+x
+ ===x ·~· ~~ ·~· ++==
+ ==++ x+%% %%+x ++==
+ ++**=*++xoox==*===++===%==xoox==%=**+++=*%=*+xox++*=**++
+ +++++==+++ +++==+++ ++====++++
+
+
diff --git a/src/build/framegen/frames/frame_024.txt b/src/build/framegen/frames/frame_024.txt
new file mode 100644
index 0000000000..2339b7811a
--- /dev/null
+++ b/src/build/framegen/frames/frame_024.txt
@@ -0,0 +1,41 @@
+
+
+
+ x+++++++++++++
+ ++==***%%%%%%%%%%*%*****+++x
+ ++***%== o+*=****++
+ ++**=* ++====+x
+ ====xx x=%$@@@@@@@@@$%=x ++**++
+ ===+ =$@@@@@@@$$$$$$$$$@@@@@@@$+ xx*=++
+ ==++ o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$@@@%~ ~o==++
+ ===+ %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ~x==xo
+ ++== $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ++==
+ ==+~ *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==+x
+ x+++ @@$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% o+==
+ ==+~ @$$$$@@%%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==xo
+ == @$$$$$= +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++++
+ ox== x@$$$$@~ +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$@ o+++
+ xx++ $$$$$$$@= =@$$$$$@x ~*@$$$$$= ·+==
+ ++++ @$$$$$$$@@@@+ @$$$@ x@$$$@% x==
+ ++++ @$$$$$$@@@*x o@$$$$ =@$$$@% ==
+ ++++ @$$$$$$% =@@$$$$$@$===============*@@$$$$@% ==
+ xx++ @$$$$$@~ +$@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@$ ==
+ xx++ @$$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ @$$$$$$@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== *$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == =@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@ ·+==
+ +++x *@@@@@@@@@@@%o +$@@@@@@@@@@@=· ~*@@@@@@@@@@@= +++x
+ **x· ~+=**+o x=**=x· ~+=*=+~ ~+==
+ oo**xx ++ ++ o+**
+ ==**++ =+**==**+= ++**==**==x· ++====
+ +==*%%**===+ ++=***%%*=++ ++===*%%*==+
+
+
diff --git a/src/build/framegen/frames/frame_025.txt b/src/build/framegen/frames/frame_025.txt
new file mode 100644
index 0000000000..9667d9db3e
--- /dev/null
+++ b/src/build/framegen/frames/frame_025.txt
@@ -0,0 +1,41 @@
+
+
+
+ xx++++++++++
+ ++==*****%%$%%%%%%%*****++xx
+ ++*****= x+%***==++
+ ++**=* ==**==
+ ====xx o=%$$@@@@@@@$%*+o ==**xx
+ ===+ +$@@@@@@@$$$$$$$$$@@@@@@@%x x+**+x
+ ==++ o$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* o+**xx
+ ===+ %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ o+==
+ ++== $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= +=++
+ ==x· %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ==xx
+ ++++ @@$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= x+++
+ ==+· @$$$$@@$%$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* **
+ == @$$$$$= =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x =+xx
+ xx++ *$$$$$@· =@@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$@ x+++
+ ++++ @$$$$$$@x ·*@$$$$$@+· o%@$$$$$~ ~+++
+ xx+x $$$$$$$$@@@$x ·@$$$$ +@$$$@= ·+++
+ xx+x @$$$$$$@@@%x o@$$$$ *@$$$@= ·+==
+ xxxx @$$$$$$$ +$@$$$$$@%+++++++++++++++=@@$$$$@= ·+==
+ +++x @$$$$$@· +$@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@= ·+==
+ +++x @$$$$$$% x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ ++++ @$$$$$$@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% x==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% x==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ %$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == $@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@x x==
+ +++o ~$@@@@$$$@@@@@*x~o+*@@@@@$$$@@@@%+o~o=$@@@@$$$@@@@% ++++
+ ** ~+%$$$$*o x*$$$$%+· ~=%$$$%+~ ·o**
+ x+**~~ ~o**xx
+ x+**==o~ ++*=**=*++ oo==****++ =+**xx
+ ++==*%%%%%%*==xx xx==*%%%%%%*==++ +=*%%%%%%*==++
+
+
diff --git a/src/build/framegen/frames/frame_026.txt b/src/build/framegen/frames/frame_026.txt
new file mode 100644
index 0000000000..5558114882
--- /dev/null
+++ b/src/build/framegen/frames/frame_026.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++++
+ ++++****%%%%%%%%%%%***==++
+ ++**=*=* ++*=**++
+ ++**=*x *=**++
+ ++**xx o+*%$$@@@@@$$%=x· +===
+ ===+ +$@@@@@@@$$$$$$$$@@@@@@@@*o ++**xx
+ ===+ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= o+**
+ ===+ %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$o x===
+ ++== ·@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ++++
+ =*o· %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ **
+ ++++ @@$$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ++++
+ ==x· ~@$$$$@@$%$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ ox== @$$$$$* ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xx
+ xx++ $$$$$$@· ·=@@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$ ++++
+ +++x @$$$$$$$x ~*@$$$$$@+~~~~~~~~~~~~~~~o$@$$$$@ o+++
+ +++o $$$$$$$$@@@%o ·@$$$$ +@$$$@~ ~+++
+ +++o @$$$$$$$@@$+ ~@$$$$ =@$$$@o ~+++
+ +++o @$$$$$$$· +$@$$$$$@*+xxxxxxxxxxxxxx=@@$$$$@o ~+++
+ +++o @$$$$$@~ x%@@@@$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@o ~+++
+ +++o @$$$$$$% o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+++
+ +++x @$$$$$$@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ xx+x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% x==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox++ %$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == @@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$@@@$$$$$$$$$$$$$$@* ==
+ ==x· *@@@$$$$$$$@@@@%**%@@@@$$$$$$$@@@$%**$@@@$$$$$$$@@@x x+++
+ xx== =$@@@@@@$+ o%@@@@@@@*· =$@@@@@$=· *=
+ ++== ==++
+ ++**++ ==%%== ++%%** +x**++
+ ++***%%%%%%***++ ++***%*%%%%***=+ x+==*%*%%%%***=+
+ xxxx xxxx xx
+
diff --git a/src/build/framegen/frames/frame_027.txt b/src/build/framegen/frames/frame_027.txt
new file mode 100644
index 0000000000..d0b2080888
--- /dev/null
+++ b/src/build/framegen/frames/frame_027.txt
@@ -0,0 +1,41 @@
+
+
+
+ +++x
+ xx++=***%%%%%%%%%%****++++
+ ++***%=* ==%***++
+ ++**=*x =+**++
+ ++**xx o+*%$$@@@@$$$%=x· =*==
+ ===+ +$@@@@@@@@$$$$$$$@@@@@@@@*~ ++**
+ ==++ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ x+*=
+ ===+ ·%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ ++==
+ ++== ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ==++
+ ** $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ o*=
+ ++++ ~@@$$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ++++
+ == o@$$$$@@$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ·x==
+ ox== @$$$$$% =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ == o
+ xx++ $$$$$$@o ·=@@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$ ++xx
+ +++o @$$$$$$$x ~*@$$$$$@=~~~~~~~~~~~~~~~o$@$$$$@ x+++
+ +++~ ~@$$$$$$$@@@%o @$$$@ x@$$$$ o+++
+ +++~ ~@$$$$$$$@@$=· @$$$@ +@$$$@ o+++
+ +++~ ·@$$$$$$$~ x%@$$$$$@%+xxxxxxxxxxxxxx+$@$$$$@· ~+++
+ +++~ ·@$$$$$@x o%@@@@$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@~ ~+++
+ +++o @$$$$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o @$$$$$$@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ xx+x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ ox++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox++ %$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ ==x $@@@$$$$$$$$@@@@$$@@@@$$$$$$$$$@@@@$$@@@@$$$$$$$@@@= o+++
+ x+== o%@@@@@@@@%o =@@@@@@@@$+ o%@@@@@@@$x ==xx
+ ++=+ +=++
+ ++**++ ++%%*= %%%= x~=*++
+ x+++*%*%====%*%*==xx++**=%*===***===++++***%**==*=%*==xx
+ ++++++++ xx++=+++ ++=+++
+
diff --git a/src/build/framegen/frames/frame_028.txt b/src/build/framegen/frames/frame_028.txt
new file mode 100644
index 0000000000..aec5d18e86
--- /dev/null
+++ b/src/build/framegen/frames/frame_028.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++=***%%%%%%%%%%****++++
+ ++***%=* · ==****++
+ ++**=*x *=**+x
+ ++**xo o+*%$$@@@@$$%%=x ===+
+ ===+ ·=$@@@@@@@@$$$$$$$@@@@@@@@=~ ++==
+ ==+x x$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ x+==
+ ==++ ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· +=++
+ ++== o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@· ==+x
+ ** $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ·x==
+ ++++ o@$$$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++x+
+ == +@$$$$@@@$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·+==
+ ox== @$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ++++ @$$$$$@= +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$% ++xx
+ +++o $$$$$$$@= =@$$$$$@*o~~~~~~~~~~~~~~o*@$$$$@ x+++
+ +++~ x@$$$$$$$@@@$x @$$$@ @$$$$ o+++
+ +++~ x@$$$$$$$@@$*~ @$$$@ ~@$$$@ o+++
+ +++~ o@$$$$$$$x o*@$$$$$@%+xxxxxxxxxxxxxx+%@$$$$@ o+++
+ +++~ o@$$$$$@* ~*@@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$@ o+++
+ +++~ ~@$$$$$$$· ·*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ ·@$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ~+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ xx+x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ == o@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$@@$ ~+++
+ ++=+ +@@@@@@@@@@*~ ~%@@@@@@@@@$x +@@@@@@@@@* ==xo
+ ==+o ~~~ ·~~ ~~ +===
+ ==== **++ =**= +===
+ ++===*=+o~··++*=**++++*%=*x~··x+*=**++++**=*+x··++*=**++
+ ++======++ +=++====++ ++======++
+
diff --git a/src/build/framegen/frames/frame_029.txt b/src/build/framegen/frames/frame_029.txt
new file mode 100644
index 0000000000..a576302733
--- /dev/null
+++ b/src/build/framegen/frames/frame_029.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++=**%%%%%%%%%%%%*==+++x
+ ++==*%=* · ==****=+
+ ++**=* *=**++
+ ++**xo o=*%$$@@@@@$$%=x ==++
+ ===+ ·=$@@@@@@@$$$$$$$$@@@@@@@@=~ ++==
+ ==+x +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ ++==
+ ===+ ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· +=++
+ ++=+ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@· ==++
+ ** @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ·x==
+ ++++ x@$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++xx
+ == +@$$$$$@@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·x==
+ ox== ~@$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ++++ @$$$$$$$ +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$* ++xx
+ +++~ $$$$$$$@* +@$$$$$@%o~~~~~~~~~~~~~~~=@$$$$@ ++++
+ +++· x@$$$$$$$@@@$= $$$$@o @$$$$ x+++
+ +++· x@$$$$$$$@@@*o $$$$@x @$$$@ x+++
+ +++· x@$$$$$$$= ~*@$$$$$@$+xxxxxxxxxxxxxx+%@$$$$@ o+++
+ +++~ x@$$$$$@$ ·*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$@ o+++
+ +++~ o@$$$$$$$x =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ ~@$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ~x== x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ =* +@@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@ ·+++
+ ++++ =@@@@@@@@@@@+ o$@@@@@@@@@@*~ =@@@@@@@@@$o =++x
+ **x~ ox+xo ~x++x· oxx~ x+==
+ **+= ==++ ++== ++**
+ ==**=*+x ·o*+**++===*++ =+*===++**+*~· ·o==**++
+ ++++****==++ ++==**=+++ ++==**==++
+
diff --git a/src/build/framegen/frames/frame_030.txt b/src/build/framegen/frames/frame_030.txt
new file mode 100644
index 0000000000..7a9f0d7b98
--- /dev/null
+++ b/src/build/framegen/frames/frame_030.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++=**%%%%%%%%%%*%*===+xx
+ ++===*== +=%***++
+ x+**=* =+**+x
+ ++*= ·x=%$$@@@@@@$$%=x· ==++
+ ==++ ~*@@@@@@@@$$$$$$$$@@@@@@@@*~ ++==
+ ==+x +@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ x+==
+ ===x o$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· +=++
+ ++++ x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@· ==xx
+ ** @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ·x==
+ ++++ +@$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++xx
+ == =@$$$$$@@$%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·x==
+ ox== o@$$$$$$x ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx+x @$$$$$$@ ~*@@@@$$$$$$$@@@@@@@@@@@@@@@@@$$$$$* ++xx
+ +++~ ~$$$$$$$$$~ o$@$$$$@$x···············x@@$$$@ ++xx
+ +++· +@$$$$$$$$@@@%o +@$$@% %$$$$ x+++
+ +++· +@$$$$$$$@@@%x =@$$$% %$$$@ x+++
+ +++· +@$$$$$$$% =@@$$$$@@*+++++++++++++++*@@$$$@ x+++
+ +++· x@$$$$$$@ =$@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$@ o+++
+ +++~ x@$$$$$$$* +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ o@$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ~+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ ++xx @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% +==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$@@ ·x==
+ +++x ~=@@@@@@@@@@@$x ·x$@@@@@@@@@@@=~ ~*@@@@@@@@@@+ ++++
+ **o· ~+***=x x=***+~ ~+**+~ ~+==
+ xx**x+ +o ++ xx**
+ ++**=*x· o+=+**===*++ ·x=+**====++ ++====
+ +++==*%%*===++ ++===*%%**=+=+ +==***==++
+
diff --git a/src/build/framegen/frames/frame_031.txt b/src/build/framegen/frames/frame_031.txt
new file mode 100644
index 0000000000..f7615a100b
--- /dev/null
+++ b/src/build/framegen/frames/frame_031.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++=**%*%%%$$%%%*%*==++++
+ ++***%++ ++%***++
+ x+**=* =+**++
+ ++== ~+*%$@@@@@@@@$%*+~ ==++
+ =*++ o*@@@@@@@$$$$$$$$$$@@@@@@@*o ++==
+ **+x =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= x+==
+ ==+x x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ +=++
+ ++++ +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==+x
+ ** @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ·x==
+ +++x =@$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++xx
+ == *@$$$$$@@$%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·+==
+ ox== x@$$$$$$= +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ +++x @$$$$$$@~ +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$* ++xx
+ +++~ ~$$$$$$$$@+ =@$$$$$@+ ~%@$$$@ ++xx
+ +++· +@$$$$$$$$@@@$+ @$$$@ x@$$$ x+++
+ +++· +@$$$$$$$@@@%x o@$$$@ =@$$@ x+++
+ +++· +@$$$$$$$$ +$@$$$$$@%=++++++++++++++*@@$$$@ x+++
+ +++· +@$$$$$$@o +$@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$@ x+++
+ +++· x@$$$$$$$$ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ o@$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ ++xx @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% x==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$@@ ·+==
+ +++o o=@@@@@@@@@@@@*o· ~+$@@@@@@@@@@@$+~ o*@@@@@@@@@@* ++xx
+ **o ~=%%%%=o x*%%%*+ o=%%*x ~+**
+ x+**x+ x~ xx ox**xo
+ ==**== ==****=*+o =+****==++ x+====
+ ++==*%%%%**=++ ++==**%%%*===+ ++=*%%**==++
+
diff --git a/src/build/framegen/frames/frame_032.txt b/src/build/framegen/frames/frame_032.txt
new file mode 100644
index 0000000000..0f9edba556
--- /dev/null
+++ b/src/build/framegen/frames/frame_032.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++
+ xx++=*%%%%%%%%%%%%%***==++
+ ++***%++ o+****++
+ ++**=* =+**++
+ ==== o=%$@@@@@@@@@@$%=x +===
+ **++ x%@@@@@@@$$$$$$$$$$$@@@@@@%x ++**
+ **+o *@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* o+**
+ ==+o +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o x=++
+ ++=+ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==+x
+ ** ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ +++x *@$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ++++
+ == %@$$$$$$@$**$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ·x==
+ ox+= +@$$$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ +++x @$$$$$$$$ ·=@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$* ++xx
+ +++~ o$$$$$$$$$$o ~$@$$$$@= o@$$$@ ++++
+ +++· +@$$$$$$$$$@@@$o %@$$@o @$$$ x+++
+ +++· +@$$$$$$$$@@%+ $$$$$= @$$@ x+++
+ +++· +@$$$$$$$$x x%@@$$$$@@%==============*$@$$$@ x+++
+ +++· +@$$$$$$$$ x%@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$@ x+++
+ +++· x@$$$$$$$$* o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ o@$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ~+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ ++xx @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% +==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == $@@@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$@@~ ·x==
+ +++o ·o=@@@@@@$@@@@@@=o~o+%@@@@@@@@@@@@%x~~o=@@@@@@@@@@% ++xx
+ ** ~=%$$$%=~ x*$$$%*x o=%$%+· ·x**
+ ++**~o ~o**xx
+ ++****=* ++==**=*+x xx*=****++ =+**xx
+ ++==*%%%%%**++ ++++*%%%%%%*+++x ++**%%%*==++
+
diff --git a/src/build/framegen/frames/frame_033.txt b/src/build/framegen/frames/frame_033.txt
new file mode 100644
index 0000000000..6155425b4b
--- /dev/null
+++ b/src/build/framegen/frames/frame_033.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++
+ ++==*%%%%%%*****%%%*%**=++
+ +=***%+o · *=**=+
+ ++**+= ++**++
+ ==++ ·x*$$@@@@@@@@@@@$%+~ +===
+ **+x +$@@@@@@$$$$$$$$$$$$@@@@@@@= x+**
+ **+o ·%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%· ~x**
+ ==+o =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ x+==
+ ++=+ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ++++
+ =* ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ **
+ +++x *@$$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ++++
+ == *@$$$$$$@@*=%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ ox== x@$$$$$$$x o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ++++ @$$$$$$$@· o%@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$ ++xx
+ +++~ ·$$$$$$$$$@* x@$$$$$%· =@$$@ xx++
+ +++~ x@$$$$$$$$$@@@@* @$$$$ +@$$ o+++
+ +++~ x@$$$$$$$$@@$+ =@$$$$ %$$@ o+++
+ +++~ o@$$$$$$$$* o%@@$$$$$@@%**************$@@$$@ o+++
+ +++~ o@$$$$$$$@· o%@@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$@ o+++
+ +++~ ~@$$$$$$$$$o ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ ·@$$$$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ~+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ xx+x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == $@@@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@@@@$$$$$$$$$$@@o +==
+ +++o ~o=$@@@@$$$@@@@@%+oo+%@@@@@$$$@@@@@=xoo=$@@@@@@@@@$ ++++
+ o ** ·=%$$$$%+ x*$$$$%=~ ~=%$%=~ ·o**
+ x+**·~ ·~**+x
+ ++****==x ox*=***=+x ==****+= =+**++
+ ++==*%%%%%%*==++ ==*%%%%%%*==++ ++**%%%*==++
+
diff --git a/src/build/framegen/frames/frame_034.txt b/src/build/framegen/frames/frame_034.txt
new file mode 100644
index 0000000000..690b7d83da
--- /dev/null
+++ b/src/build/framegen/frames/frame_034.txt
@@ -0,0 +1,41 @@
+
+
+
+ +++++++++++
+ ++=**%%%**==++==**%*%***++
+ +=**=* =+*===+x
+ ++**++ xx====
+ ==== o=%$@@@@@@@@@@@@@$%+~ ++**xo
+ **+x ·=@@@@@@$$$$$$$$$$$$$$$@@@@@$x ~**+x
+ **+o ~%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@= **xx
+ ==+o =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ ·x**
+ ++=+ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ x+++
+ ** ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ ++++ +@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ o+++
+ == +@$$$$$$$@%xo=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ == @$$$$$$$@ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ++xx
+ ++++ $$$$$$$$$@ ·=@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$@ x+++
+ ++xx @$$$$$$$$$@%~ o$$$$$$+ +$$$~ ~+++
+ +++x @$$$$$$$$$$@@@@% o@$$@% *@@+ ·+++
+ +++x @$$$$$$$$$@@*~ %$$$$$ $$@+ ·+++
+ +++x @$$$$$$$$@x ·=@@@$$$$$@@$$$$$$$$$$$$$$$@@$$@+ ·+++
+ +++x @$$$$$$$$@ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ +++x @$$$$$$$$$@x =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+++
+ xxxx @$$$$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·x==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox+= *$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == $@@@@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$@+ ==
+ +++o ox+*@@@@@$$$@@@@@*+xx=$@@@@$$$$@@@@$=xx+%@@@@@@@@@$ ++++
+ ox** x%$$@$$*x ~=%$@@$%=· x%$$*o **
+ ++** **xx
+ xx*****=xx ==****+= ++****+= =+**+x
+ xx++***%%%%**=++ ++**%%%%%%**++ ++=**%%*==++
+
diff --git a/src/build/framegen/frames/frame_035.txt b/src/build/framegen/frames/frame_035.txt
new file mode 100644
index 0000000000..32f4209a78
--- /dev/null
+++ b/src/build/framegen/frames/frame_035.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++++++++++
+ ++=**%*%==xxxxxx++**%*%*==++
+ +=**=* ~+*=**++
+ ++**++ ==**xx
+ ===+ +%$@@@@@@@@@@@@@@@$%+ ox**++
+ **+x ~*@@@@@$$$$$$$$$$$$$$$$$@@@@@*~ ==++
+ **x~ o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%~ ==++
+ ==+x =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ **xx
+ ++=+ +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ~+==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ +++x
+ ++++ o@$$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·x==
+ ==+· ~@$$$$$$$$@+ ·+$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xo
+ == @$$$$$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++++
+ ox== =$$$$$$$$$$ =$@@$$$$$@@@@@@@@@@@@@@@@@@@@$@~ ~+++
+ xx++ @$$$$$$$$$$@%o x$$$$$$ =$@* ·+==
+ xx++ @$$$$$$$$$$$@@@@= %@$$@~ @@$ ==
+ xx++ @$$$$$$$$$$@%x o$$$$$% +$@$ ==
+ xx++ @$$$$$$$$$$ +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$@$ ==
+ xx++ @$$$$$$$$$$ +%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ @$$$$$$$$$$@x x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ @$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== =$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == $@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@$$$$$$$$$$@= ==
+ +++o oxx=$@@@@$$$$@@@@$=xx+%@@@@$$$$@@@@@*+xx*@@@@@@@@@@· x+++
+ o ** ~=$$@@$%=· +%$@@@$*x ~*$$*x **
+ ++** **+x
+ xx******++ =+****+= +x==**== =+**++
+ +=**%%%%%%**++ ++***%%%%***+++x ++==*%%**=++
+
diff --git a/src/build/framegen/frames/frame_036.txt b/src/build/framegen/frames/frame_036.txt
new file mode 100644
index 0000000000..9545578f3e
--- /dev/null
+++ b/src/build/framegen/frames/frame_036.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++==****==++++
+ x==***%==oo x+==%***=+
+ xx===*++ ++**==
+ ++**x+ ·oxx++xxo· ++**++
+ **++ o*$@@@@@@@@@@@@@@@@@@$=~ +===
+ **+x o%@@@@@$$$$$$$$$$$$$$$$$$@@@@@%~ x+==
+ =*+o o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%· x+==
+ ++=+ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o +=++
+ xx== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· *=o~
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* o+++
+ ox== @@$$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ +++o @$$$$$$$$$$~ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == %$$$$$$$$$@ =$@@@$$$$$$$$$$@@@@@@@@@@@@@@$$$@x ·+==
+ == @$$$$$$$$$$o =@@@$$$$@@@$$$$$$$$$$$$$$$@@$@ ==
+ == o$$$$$$$$$$$@@*~ %$$$$$ $$ ==
+ == x@$$$$$$$$$$$@@@@* +@$$@* %@ ==
+ == x@$$$$$$$$$$@*· +@$$$$@= =$@ ==
+ == x@$$$$$$$$$@ ~*@@@$$$$$$@@@@@@@@@@@@@@@@@@@$@ ==
+ == x@$$$$$$$$$@ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@$$$$$$$$$$@%xx*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@@@@@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@@@@$$$$$$$$$@% ==
+ +++o ~+x+*@@@@@$$$$@@@@%=xx=$@@@@$$$$@@@@@*xx+*@@@@@@@@@o x+++
+ ** o*$@@@$%+ =%$@@$$*o x*$%x **
+ x+** **+x
+ xx******+= x=***==x ==**==++ =+**++
+ ++==*%%%%*%*==xx ++==*%*%%%%*==++ ++**%**=++
+
diff --git a/src/build/framegen/frames/frame_037.txt b/src/build/framegen/frames/frame_037.txt
new file mode 100644
index 0000000000..1e29736ef4
--- /dev/null
+++ b/src/build/framegen/frames/frame_037.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++=***%%%%***=++++
+ ++==*%*%+x x+**%*==++
+ x+**=*+o *=**++
+ ++**x ~x==*****=+o· *===
+ **++ +%@@@@@@@@@@@@@@@@@@@@$=~ ++**xx
+ **+x x$@@@@$$$$$$$$$$$$$$$$$$$$@@@@@* ~o**xx
+ ==+o o$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ·x**
+ ++=+ +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ o+==
+ xx== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ++++
+ +++o %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ == $@$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ++xx
+ ++++ $@$$$$$$$$$* o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·+++
+ +++· o@$$$$$$$$$@ o%@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$@ ==
+ == $$$$$$$$$$$$+ x%@@$$$$@@%*=============*$@$· ==
+ == @$$$$$$$$$$$@@$+ %$$$$% $= ==xx
+ == @$$$$$$$$$$$$@@@$x =@$$@= $* ==xo
+ == @$$$$$$$$$$$@x %@$$$$@*· ~$@= ==xo
+ == @$$$$$$$$$$@ =$@@@$$$$$$@@@@@@@@@@@@@@@@@@@@= ==xo
+ == @$$$$$$$$$$@ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$@$**$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == *@@@@@@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@@$$$$$$$$@% ==
+ +++x ~=xx+%@@@@$$$$@@@@@*+xx*@@@@@$$$$@@@@%=xx+%@@@@@@@@o o+++
+ ** +%$@@$$*o o*$$@@$%+ +%%x **
+ x+** **++
+ ++**==****xo ==****++ =+****+= =+**++
+ ++ x+==*%*%%%%*==++ ==**%%%%%%**=+ +=**%***++
+
diff --git a/src/build/framegen/frames/frame_038.txt b/src/build/framegen/frames/frame_038.txt
new file mode 100644
index 0000000000..01d7bdf81f
--- /dev/null
+++ b/src/build/framegen/frames/frame_038.txt
@@ -0,0 +1,41 @@
+
+
+
+ x+++==*%%%%%%%%%%%%*==++
+ ++***%++ ox**%*==+x
+ x+**+= x+*===
+ ==== ·x=*%$@@@@@$$%*+~ x+**++
+ **++ o*@@@@@@@@$$$$$$$@@@@@@@@%+ ==++
+ **+o =@@@@$$$$$$$$$$$$$$$$$$$$$$$@@@@%o +=++
+ ==+x o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* +=++
+ ++++ x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ ==+x
+ ** @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ·x==
+ ++++ x@$$$$$$$$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ +++x
+ == +@$$$$$$$$$@@@$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·+==
+ ox== @$$$$$$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ++++ @$$$$$$$$$$@= =$@@@$$$$$$$@@@@@@@@@@@@@@@@@@% ++xx
+ +++o $$$$$$$$$$$$@= =@$$$$$@*o~~~~~~~~~~~~~~o%@ x+xx
+ +++~ o@$$$$$$$$$$$$@@@%x @$$$@ ~ o+++
+ +++~ x@$$$$$$$$$$$$@@$*~ @$$$@ x o+++
+ +++· x@$$$$$$$$$$$$o o%@$$$$$@%xxxxxxxxxxxxxxx+$@ o+++
+ +++· x@$$$$$$$$$$@+ o*@@@@$$$$$$$@@@@@@@@@@@@@@@@@@$ o+++
+ +++· +@$$$$$$$$$$$$ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ +++· +@$$$$$$$$$$$@@@$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ +++· =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ +++· =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ xxxx
+ ==+· *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==x %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++++
+ == %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$* ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == x@@@@@@@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@@$$$$$$$@$ ==
+ ++++ *+oox*@@@@@$$$@@@@@*xox=$@@@@$$$@@@@@%+oo+%@@@@@@@x o+++
+ **o· x%$$@$%*o ~=%$$$$%+ +*x **xo
+ xx**~· **++
+ xx**+=****+= ++****== x+******xo =+**+x
+ ++ +=***%%%%%**== ++==*%*%%%%*==+x x+==***=++
+
diff --git a/src/build/framegen/frames/frame_039.txt b/src/build/framegen/frames/frame_039.txt
new file mode 100644
index 0000000000..cea0cf6e28
--- /dev/null
+++ b/src/build/framegen/frames/frame_039.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++
+ +++=**%$%%******%%$%%*=+++
+ ++***%+o x+%=**++
+ ++**+= ==**++
+ ===+ ·x*%$@@@@@@@@@@$%*x ====
+ **+x +$@@@@@@$$$$$$$$$$$$@@@@@@$+ ++**
+ ==+o *@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@%~ ~+**
+ ++=+ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ~+==
+ xx== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ++++
+ ==x· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xo
+ xx++ @@$$$$$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% o+++
+ ==x· @$$$$$$$$$$@@%=*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ == @$$$$$$$$$$$$ ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$* ++xx
+ ox== +$$$$$$$$$$$@% ·*@@@@$$$$$$@@@@@@@@@@@@@@@@@@ o+++
+ xx++ $$$$$$$$$$$$$@$o ~$$$$$$@+ x= ·+==
+ xx++ @$$$$$$$$$$$$$$@@@$o $$$$@ x==
+ xx++ @$$$$$$$$$$$$$@@%o $$$$$+ · x==
+ xx++ @$$$$$$$$$$$$$ =@@$$$$$@@%**************%@$ x==
+ ++++ @$$$$$$$$$$$@% +$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$* ·x==
+ +++x @$$$$$$$$$$$$$+ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ ++xx @$$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++~ ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++~ o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++· +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==+· *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ xx++
+ ==x %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ ==+· ~@@@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@@@@@$$$$$$@$ ==
+ ++++ %*xoox%@@@@@$$@@@@@$+oox=@@@@@$$$@@@@@*xoo+%@@@@@@x ~+==
+ **+· +%$$$$%+ o=%$$$%=o +o **xo
+ xx**o~ **++
+ xx**+=****==+o ~x==**==++ ==****++ =+**+x
+ ++xx x+==*%%%%%%*==++ x+++**%%%%%*==++ ++****++
+
diff --git a/src/build/framegen/frames/frame_040.txt b/src/build/framegen/frames/frame_040.txt
new file mode 100644
index 0000000000..e8bfe577f0
--- /dev/null
+++ b/src/build/framegen/frames/frame_040.txt
@@ -0,0 +1,41 @@
+
+
+ ++++======++++
+ +=***%=*++++xx++++**%*%*==+x
+ xx==**++ o+****++
+ ++**x+ · *=**
+ ===+ ·+*$@@@@@@@@@@@@@@@$*+· xx**++
+ **+x ·*@@@@@$$$$$$$$$$$$$$$$$@@@@@%o =*++
+ ==+x %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@x +=++
+ ++=+ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ==++
+ ox=* @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% **
+ +++o =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ x+++
+ == $@$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ ++++ %@$$$$$$$$$$$@$o ~=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ ==x· o@$$$$$$$$$$$@* ·*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$+ ·+==
+ == @$$$$$$$$$$$$$% ~*@@@$$$$$@@@@@@@@@@@@@@@@@@ ==
+ == $$$$$$$$$$$$$$@@*~ =$$$$$* ==
+ == @$$$$$$$$$$$$$$$@@@@x @$$$@ ==
+ == @$$$$$$$$$$$$$@@*~ =$$$$$* ==
+ == @$$$$$$$$$$$$$* ~=@@@$$$$$@@@@@@@@@@@@@@@@@@o ==
+ == ·@$$$$$$$$$$$$@= ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ == ~@$$$$$$$$$$$$$@%~ ~=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·x==
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+++
+ +++~ ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ~+++
+ +++· +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==+· *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ x+++
+ ==x %$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ +++~ @@$@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@$$$$$@@ == x
+ ++++ =@*o··~+@@@@@@@@@@@@$+~·~x%@@@@@@@@@@@@*o··o=@@@@@x ~+==
+ ==x~ ~=%%$%*+ x*%$%%=~ · **xx
+ x+**x~ oo **++
+ x+**+===****+= *=****==xo ++==**==++ =+**++
+ ++++ ++****%%%***++ ++==**%%%***+++x ++==**++
+
diff --git a/src/build/framegen/frames/frame_041.txt b/src/build/framegen/frames/frame_041.txt
new file mode 100644
index 0000000000..aa4c748987
--- /dev/null
+++ b/src/build/framegen/frames/frame_041.txt
@@ -0,0 +1,41 @@
+
+
+ x+++==******===+++
+ ++==****++xo ·~++**%***++
+ ++****+x ==%*++
+ ++*=ox ~ox+++xx~ ++**++
+ **++ o*$@@@@@@@@@@@@@@@@@@%=~ ====
+ =*+x o%@@@@$$$$$$$$$$$$$$$$$$$@@@@@%o x+**
+ ==+x ·%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o ~+**
+ ++=+ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* x+==
+ ** $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= +=++
+ ++++ x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ **
+ == +@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ++++
+ xx== o@$$$$$$$$$$$$$% ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@o ·+==
+ +++x @$$$$$$$$$$$$$$ ·=@@@@$$$$$$$$$$@@@@@@@@@@@@$@ ==
+ +++· +$$$$$$$$$$$$$$$· ~*@@$$$$$@@@%%%%%%%%%%%%%%= ==xo
+ == %@$$$$$$$$$$$$$$@@=~ $$$$$% ++xx
+ == $$$$$$$$$$$$$$$$$@@@@= *@$$@x ++xx
+ == $$$$$$$$$$$$$$$$@= =@$$$$$x ++xx
+ == $$$$$$$$$$$$$$$$ o%@@@$$$$$$@@@@@@@@@@@@@@@@@ ++xx
+ == @$$$$$$$$$$$$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx+x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++· x$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o+++
+ ==+· =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++++
+ +++o @@$$@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@$$$$@@ ==xo
+ xx== x@@+~ ·x$@@@@@@@@@@@*o o*@@@@@@@@@@@$+~ ·+$@@$o ~+==
+ ==+o +*%%*=o o=*%%*x *=+x
+ ox**+o ++ x+ o~**++
+ xx**===+****==+= ++*=****+= o+*=****==++*=**++
+ ++++xx x+==********==+x ++********==++ ++==++
+
diff --git a/src/build/framegen/frames/frame_042.txt b/src/build/framegen/frames/frame_042.txt
new file mode 100644
index 0000000000..96715cf2b1
--- /dev/null
+++ b/src/build/framegen/frames/frame_042.txt
@@ -0,0 +1,41 @@
+
+
+ xx===***%%%%%%%%***=++
+ ++**=%=+ ·o==%*==++
+ ++**+= *=**++
+ ==== o+=*%$$$$%*=+o =+**
+ **++ ~=$@@@@@@@@@@@@@@@@@@@@@%+ o**++
+ ==+x +$@@@$$$$$$$$$$$$$$$$$$$$$$@@@@$+ ==++
+ ++++ ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ==++
+ xx== $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* *=xo
+ ==+· %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ·x==
+ xx== @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ +++~ @$$$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == @$$$$$$$$$$$$$$$= =$@@@$$$$$$$$$$$$$$$$$$$$$$$$@ ==xo
+ ox== x@$$$$$$$$$$$$$$@ =$@@@$$$$$$$$@@@@@@@@@@@@@@ ++x+
+ xx++ $$$$$$$$$$$$$$$$$* =@@$$$$@@*+++++++++++++ ~+++
+ ++++ @$$$$$$$$$$$$$$$$@@@*x *@$$$% ·+==
+ +++x @$$$$$$$$$$$$$$$$$@@@*o =@$$$* ·+==
+ ++xx @$$$$$$$$$$$$$$$$$~ o$@$$$$@$x············· ·+==
+ xx+x @$$$$$$$$$$$$$$$@ o*@@@@$$$$$$$@@@@@@@@@@@@@@* ·+++
+ +++x @$$$$$$$$$$$$$$$$~ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$+ ·+++
+ +++o @$$$$$$$$$$$$$$$$@@%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+++
+ +++o ·@$$$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++~ o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++· =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+xx
+ == %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++++
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ +++o $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+ ·+++
+ +++~ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ++++ *@@$$@@@@@@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@@@@@@$@@@ ==xx
+ ox== %@@=~ x$@@@@@@@@@@%x ~*@@@@@@@@@@@=· x$$~ ~+==
+ ==++ o+==x~ ~x+=+o *=xx
+ ox**++ == == == xo**++
+ xx==**+=*=****==+= ·x*=******+=x~ *=******+===**++
+ x++=++ ++==********++xx xx++********++++ ++++
+
diff --git a/src/build/framegen/frames/frame_043.txt b/src/build/framegen/frames/frame_043.txt
new file mode 100644
index 0000000000..849ab37940
--- /dev/null
+++ b/src/build/framegen/frames/frame_043.txt
@@ -0,0 +1,41 @@
+
+
+ ++==***%*%%%%%%%%%**=+++
+ ++**=*+x o+**%*==
+ ++**+= x+**=+
+ ===+ o+*%$@@@@@@@$%=+~ ++**+x
+ =*=x o*@@@@@@@$$$$$$$$$@@@@@@@%+ +=++
+ ==+x +@@@@$$$$$$$$$$$$$$$$$$$$$$$@@@@$x x+==
+ ++=+ $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o x+==
+ xx== $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ +=++
+ ==+~ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o *=xo
+ ox== $@$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~+==
+ +++x $@$$$$$$$$$$$$$$@@@%$@@@$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==x =@$$$$$$$$$$$$$$$% +$@@@@$$$$$$$$$$$$$$$$$$$$$$$ ++++
+ == @$$$$$$$$$$$$$$$@= +$@@@$$$$$$$@@@@@@@@@@@@@= ·x==
+ == o$$$$$$$$$$$$$$$$$@= =@$$$$$@*~···········~ ==
+ ox== =@$$$$$$$$$$$$$$$$$@@@$+ @$$$@ ==
+ ox== *@$$$$$$$$$$$$$$$$$@@$=~ @$$$@· ==
+ ox== *@$$$$$$$$$$$$$$$$$o o%@$$$$$@$+xxxxxxxxxxx+ ==
+ ox++ %@$$$$$$$$$$$$$$$@= o%@@@@$$$$$$$@@@@@@@@@@@@@@ ==
+ xx++ %@$$$$$$$$$$$$$$$$$ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $@$$$$$$$$$$$$$$$$@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% x==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ +++~ ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ~+++
+ +++· =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx+x $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$% ·x==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ ++++ o@@$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@@@@@@@@$ ==xx
+ xx== =@@@+ =@@@@@@@@@@%o x$@@@@@@@@@@+ ·= ~x==
+ ===+ ~xxo ·oxo· ·o**xx
+ ===+ =*=+ +=*= x+== xx**++
+ ==**=**=**==**=*+= ·o=+*=****===*x~ +=*=**==***=**++
+ ++++++++ ++==******==++ ++==******==++ x+++
+
diff --git a/src/build/framegen/frames/frame_044.txt b/src/build/framegen/frames/frame_044.txt
new file mode 100644
index 0000000000..5ebc901a33
--- /dev/null
+++ b/src/build/framegen/frames/frame_044.txt
@@ -0,0 +1,41 @@
+
+ xx++++++
+ ++***%*%*%****%%%%$%**==
+ x+==**+= x+%%%*++
+ ++**x+ =***++
+ ===+ ~+%$@@@@@@@@@@@@$*+· *===
+ ===+ +$@@@@@$$$$$$$$$$$$$@@@@@@$=· xx**xx
+ ===+ =@@@@$$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ **+x
+ ++== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$o **xo
+ ** %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·x**
+ ++++ ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ++++
+ == x@$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$@* ==
+ xx== ~@$$$$$$$$$$$$$$$$$@$=+%@@@@$$$$$$$$$$$$$$$$$$$$$$@$ x+++
+ +++x @$$$$$$$$$$$$$$$$$@ x%@@@@$$$$$$$$$$$$$$$$$$$$* ==
+ ==+· +$$$$$$$$$$$$$$$$$$@ x%@@@$$$$$$@@@@@@@@@@@@ ==
+ == $$$$$$$$$$$$$$$$$$$$@= =@$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$@@@@= +@$$@* ++xx
+ == @$$$$$$$$$$$$$$$$$$$@@%x %$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$x +$@@$$$$@@$%%%%%%%%%%* ++xx
+ == @$$$$$$$$$$$$$$$$$$@ x$@@@@$$$$$$$$$@@@@@@@@@@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$% +%@@@@$$$$$$$$$$$$$$$$$$$$$$$= ==xo
+ == @$$$$$$$$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ x+== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ +++o ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ ==+· *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++++
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ ox== +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ~+++
+ xx+= $@@$$$@@@@@@@@@@$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@@@@@@% ++xx
+ ox==x· ·%@@@%o ~%@@@@@@@@$+ =@@@@@@@@@*~ o+==
+ +++= ~x**xo
+ ====~o **== =*** +=%%x+++**++
+ ++**===*%=**====**=*++++==*=**====**=*==++++%**=**==**==++
+ ++++==++++ ++++****==++++ x+++==***=+++x xx
+
diff --git a/src/build/framegen/frames/frame_045.txt b/src/build/framegen/frames/frame_045.txt
new file mode 100644
index 0000000000..5ad22dd68d
--- /dev/null
+++ b/src/build/framegen/frames/frame_045.txt
@@ -0,0 +1,41 @@
+
+ xx++++++++xx
+ x+==***%****======%%%%%%==++
+ xx=**=++ **%*++
+ ++**xx x+%*++
+ ===+ x*$@@@@@@@@@@@@@@$*+· ++**+x
+ ===+ =$@@@@@$$$$$$$$$$$$$$$@@@@@$+ **++
+ ++=+ =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@%~ ==++
+ x+== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ==++
+ ==x· *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ **xo
+ x+++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ·+==
+ ==+· @$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$@@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$@=ox*@@@@$$$$$$$$$$$$$$$$$$$$$@ ·+==
+ xx++ %$$$$$$$$$$$$$$$$$$$$ o*@@@@$$$$$$$$$$$$$$$$$$@ ==
+ +++o @$$$$$$$$$$$$$$$$$$$$ o%@@@$$$$$$@@@@@@@@@@@ ++++
+ +++· x$$$$$$$$$$$$$$$$$$$$$@= =@$$$$$o ++++
+ +++· =@$$$$$$$$$$$$$$$$$$$$$@@@@= %@$$@o x+++
+ +++· =@$$$$$$$$$$$$$$$$$$$$@$= $$$$$% x+++
+ ==+· =@$$$$$$$$$$$$$$$$$$$$ o%@@$$$$$@@@$$$$$$$$$ xx++
+ ==+· *@$$$$$$$$$$$$$$$$$$@$ o*@@@@$$$$$$$$$$$$$$$$$$@ x+++
+ ==x %@$$$$$$$$$$$$$$$$$$$$% o*@@@@$$$$$$$$$$$$$$$$$$$$$$$ ++++
+ == $@$$$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ +++~ ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ ==x %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++++
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* =+xo
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ xx++ %$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ *@$$$$$$$$$$$@@$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$$@@@@@ ~+==
+ ++== *@@@$$$@@@@$%%@@@@$$$$$$$$@@@@%%$@@@$$$$$$$$@@@@$%$* =+xx
+ ==+~ +$@@@%o ~*@@@@@@@$= +$@@@@@@@*o o+==
+ ++==o x+*=xx
+ ====x+ ++%%== ox***= ****+=**++
+ ++**=*=****=**++==**=%****%*%***++==**=*=***%%%***======+x
+ ++++++++ ++++======++ x+++======++++
+
diff --git a/src/build/framegen/frames/frame_046.txt b/src/build/framegen/frames/frame_046.txt
new file mode 100644
index 0000000000..be7984158e
--- /dev/null
+++ b/src/build/framegen/frames/frame_046.txt
@@ -0,0 +1,41 @@
+
+ ++++====++++xx
+ x+==***%=*=+xooox+==%%%*%*==
+ xx**==+x x+%*==+x
+ ++**ox ···· %===
+ ===+ ·+%@@@@@@@@@@@@@@@@$%+· x**++
+ ==++ *@@@@@$$$$$$$$$$$$$$$$$@@@@@%+ +===
+ ++=+ =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@* ++==
+ ox== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ x+==
+ ==+~ x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* +=++
+ ox== %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x *=
+ +++o $@$$$$$$$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$@% o+++
+ == %@$$$$$$$$$$$$$$$$$$$$@$o ·=$@@@$$$$$$$$$$$$$$$$$$@$ ==
+ == @$$$$$$$$$$$$$$$$$$$$@% =$@@@$$$$$$$$$$$$$$$$% ++x+
+ x+++ %$$$$$$$$$$$$$$$$$$$$$$$ =$@@$$$$$@@@@@@@@@ o+++
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$@@%o x$$$$$% ·+++
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ $$$$@ ·x==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$@@%o x$$$$$% ·x==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$ =$@@$$$$$@@@@@@@@@% ·+==
+ xx+x @$$$$$$$$$$$$$$$$$$$$$@% =@@@@$$$$$$$$$$$$$$$$$= ·+==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$@$o ~=$@@@$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++~ x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ ==+· *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·x==
+ +++~ o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ox++
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$= ==xo
+ ox== x$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx== @$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@@ ·+==
+ ox== @@@@$$$@@@@@*+x+*@@@@$$$$$@@@@%=+x=$@@@@$$$$@@@@@*x ==++
+ ==++ x%$@@$=o x%$@@@$%x ~=$@@@@$=~ ++==
+ ++==+o x+==+x
+ ++==++ ++****++ ++*=*=++ x*=**==xx
+ ++==*****%%%%***++++*****%%%%**=**++++==***%*%%*****==++
+ ++++++++++ xx++++++++++xx ++++++++++++
+
diff --git a/src/build/framegen/frames/frame_047.txt b/src/build/framegen/frames/frame_047.txt
new file mode 100644
index 0000000000..8dfb79787b
--- /dev/null
+++ b/src/build/framegen/frames/frame_047.txt
@@ -0,0 +1,41 @@
+
+ ++++========++++
+ ++==**=*==+x~····~x+**%%%%*=+x
+ ++**==xx ·x%*%*++
+ ++==~o ~~ooo~· *=**+x
+ ===+ ~=$@@@@@@@@@@@@@@@@@$*x *==+
+ ===+ *@@@@@$$$$$$$$$$$$$$$$$$@@@@@*· ++**
+ ++=+ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o ox**
+ ox== =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% ~+*=
+ ==+o ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ x+++
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ==+x
+ ++++ *@$$$$$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$@@ ·+==
+ ==x· x@$$$$$$$$$$$$$$$$$$$$$$$%~ x%@@@@$$$$$$$$$$$$$$$$@ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$ x%@@@@$$$$$$$$$$$$$@ x+++
+ o == x$$$$$$$$$$$$$$$$$$$$$$$$$ x%@@$$$$$@@@$$$$= ·+==
+ ox++ %@$$$$$$$$$$$$$$$$$$$$$$$$@$+ ·$$$$$% ==
+ ox++ $@$$$$$$$$$$$$$$$$$$$$$$$$$@@@@= %@$$@o ==
+ ox++ $@$$$$$$$$$$$$$$$$$$$$$$$$@*~ +@$$$$$~ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$ ~*@@@$$$$$$@@@@@@@ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$ ~*@@@@$$$$$$$$$$$$$$$$ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$@+~o=@@@@$$$$$$$$$$$$$$$$$$@$ ==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ +++~ ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++· =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+xx
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ o == +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ x+++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o ~+++
+ ==x· *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ +++x
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$% ++xo
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== @@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$@@$ ~+==
+ ox== +$@@@@@@@@@@@%+~ o*@@@@@@@@@@@@=o ~+$@@@@@@@@@@%· ==++
+ ==++ ~+*%%%=x x=%%%*+~ ~+*%%=x +===
+ ++==++ xo xx +===xx
+ ++====++ ======++ox =+**====++ =+====+x
+ ++++=***********==++++==**=**%******++++==**=*********++
+ ++++++++++ ++++++++++ ++++++++++
+
diff --git a/src/build/framegen/frames/frame_048.txt b/src/build/framegen/frames/frame_048.txt
new file mode 100644
index 0000000000..74a3df818c
--- /dev/null
+++ b/src/build/framegen/frames/frame_048.txt
@@ -0,0 +1,41 @@
+
+ ++======**====++
+ ++****=*++o~ ·~++%*$%**++
+ x+**+= **%*++
+ ==== ~oxxxxxo~ ++**++
+ ===+ o*$@@@@@@@@@@@@@@@@@@%=~ ====
+ ===+ *@@@@$$$$$$$$$$$$$$$$$$$@@@@@%x xx**xo
+ ++== +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@= **xx
+ ** =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x **
+ +++x @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·+==
+ =* x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ ++++ x@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$$$@+ **
+ +++~ @$$$$$$$$$$$$$$$$$$$$$$$$$$$x +$@@@@$$$$$$$$$$$$@= ++xx
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$@· x$@@@@$$$$$$$$$@~ ·+++
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$* x$@@$$$$$@@$$ ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$x +$$$$$o ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o @$$$@ ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$@$o ·$$$$$$% ==
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$@x =$@@@$$$$$@@@@ ==
+ xx== +@$$$$$$$$$$$$$$$$$$$$$$$$$$@x =$@@@$$$$$$$$$$$$$ ==
+ xx== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$@$+x=$@@@$$$$$$$$$$$$$$$$@ ==
+ xx++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ ++xx @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ ==+· *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o ==
+ xx++ %$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ +x+x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ +++· +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ox== *@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$@@% ~+==
+ ==+~ +@@@@@@@@@@%x ~%@@@@@@@@@@= =@@@@@$+ ==++
+ ==== ~ooo ·ooo~ +===
+ xx==++·~+==+ +=** o+** xo====
+ ++==========+= ~ ~x==*=======+=xo ++%=======+=++==*=====
+ x+++++++==**********==++++====********==++++++==****==++
+ ++++++++ xx+++++xxx ++
+
diff --git a/src/build/framegen/frames/frame_049.txt b/src/build/framegen/frames/frame_049.txt
new file mode 100644
index 0000000000..a8e97f28dc
--- /dev/null
+++ b/src/build/framegen/frames/frame_049.txt
@@ -0,0 +1,41 @@
+
+ ++====*****=**=+++
+ ++***%==++ ··x+**$%**++
+ ++**+= =*%*++
+ ==== ·ox+++++xo· x+**++
+ ===+ x*@@@@@@@@@@@@@@@@@@@$=o *+==
+ ===+ ·*@@@@$$$$$$$$$$$$$$$$$$$$@@@@$= xx**xx
+ ++== +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% **+x
+ ** +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ **xo
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ·x**
+ ** o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ++++
+ xx++ ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$% +$@@@$$$$$$$$$$@% +++x
+ == %$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ +$@@@@$$$$$$@+ ·+==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$x +$@@$$$$$@ ==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@%o %$$$$$ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@* o@$$@% ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@=· +@$$$$@* ==
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o%@@@$$$$$$@~ ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o*@@@@$$$$$$$$$$ ==
+ o == +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@%++*@@@@$$$$$$$$$$$$$@ ==
+ x+== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$@ ==
+ xx++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+++
+ +++· +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ +++x
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+ == o
+ ox== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$% ·x==
+ +++~ x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ == ~@@@@@@@@@@$$$$$$$$$@@@@@@@@@@$$$$$$$$@@@@@@@@@@$@@@+ o+==
+ ==+x o =@@@@@@@@@*· o$@@@@@@@@$x *@*~ ==xx
+ +++=x· ==+=
+ oo====xo x+%%+x %%=+ %*%* ++====
+ ++============+==*++++*=%*=======*+=++++==**=========*%=*=++
+ ++==++xx++====******==++ ++====******==++xx++====++++
+ ++++ ++++
+
diff --git a/src/build/framegen/frames/frame_050.txt b/src/build/framegen/frames/frame_050.txt
new file mode 100644
index 0000000000..3151bf4408
--- /dev/null
+++ b/src/build/framegen/frames/frame_050.txt
@@ -0,0 +1,41 @@
+
+ ++===*******===+++
+ ++****=*+x o+**%**=++
+ ++**++ +=$*==
+ ==== ~o++===++o~ o+%*++
+ ==++ x%@@@@@@@@@@@@@@@@@@@$*x =+**
+ ===+ ~%@@@@$$$$$$$$$$$$$$$$$$$$@@@@$= o**+x
+ ++== =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%· **xx
+ ** =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= **xo
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ·o**
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o x+++
+ xx++ o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$@% *=
+ +++~ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$* +$@@@$$$$$@$ +++x
+ == %$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ +$@@@$$@= ·x==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ +$@$@ ==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@%o $$· ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ *@o ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ *@@~ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ +$@@@$@· ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ +$@@@@$$$$@ ==
+ == +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@%+=$@@@@$$$$$$$$@ ==
+ ox== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$@ ==
+ xx++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+++
+ +++· +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ +++x
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+ ==xo
+ ox== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$% x==
+ +++~ o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @@$$$$$$$$$@@@@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@@@@@% ==
+ ==x· $@@@$@@@@$***%@@@$$$$$$$@@@@%***@@@@$$$$$$@@@@$***%· x+==
+ ==++ +$@%+ =$@@@@@$*o x%@@@@@@%+ ·x==xx
+ +++++x xx==++
+ ==+=++ ==**=+ +=%**= x+***==+==++
+ =======*%**==========*****%**==========*******%**=======++
+ ++++====++ ++++========++xx xx++========++++
+
+
diff --git a/src/build/framegen/frames/frame_051.txt b/src/build/framegen/frames/frame_051.txt
new file mode 100644
index 0000000000..bdf577d22a
--- /dev/null
+++ b/src/build/framegen/frames/frame_051.txt
@@ -0,0 +1,41 @@
+
+ x++===*******===+++
+ ++**=*==+x o+**%**=++
+ ++**+= ==%*++
+ ===+ ~x+=====+x~ x=**++
+ **+x +%@@@@@@@@@@@@@@@@@@@$*x =+==
+ ===x o$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$= ox**xx
+ ++=+ %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* **xx
+ oo== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ **
+ ==+o o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·+==
+ == *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ ++++ *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@@@$$$$@+ ==
+ ==x· ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·=@@@@$@= ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·=@@@~ ~+++
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ·* ==
+ == +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@*~ ==
+ ox== +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x ==
+ ox== +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$o ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% =$@ ==
+ ox== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$% =@@@@$$ ==
+ ox++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@*+=$@@@$$$$$@ ==
+ ox++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$$$$$$$$$$ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ +++~ o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ ==x %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++++
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$% +++x
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ xx++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$= ·+++
+ +++· =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == $@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@= **
+ ==+· =@@@@$$$$@@@@@*+xx=$@@@@$$$$@@@@%=xx+%@@@@$$$$@@@@* ++++
+ ==++ +%$@@@%=o o*$@@@$*+ +%$@@$*+ o+==xo
+ ++++++ ++==++
+ ====*=x+ ++**==++ x+*===++ x+====++
+ ++=====***%%***=**+++======*%%%*****==++====*%*%%**=**==++
+ ++++====++++xx +++++===++++++ ++++++==++++++
+
+
diff --git a/src/build/framegen/frames/frame_052.txt b/src/build/framegen/frames/frame_052.txt
new file mode 100644
index 0000000000..22bc6aea29
--- /dev/null
+++ b/src/build/framegen/frames/frame_052.txt
@@ -0,0 +1,41 @@
+
+ ++++===*******===+++
+ +=***%==x~ o+**$%*=++
+ +=**++ **%*++
+ x+**++ ·ox+====+xo~ ==**+x
+ xx**xo o*@@@@@@@@@@@@@@@@@@@@%=~ ==++
+ xx**x· *@@@@$$$$$$$$$$$$$$$$$$$$@@@@@%o ++*=
+ ==+o x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x o+**
+ ++++ x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ~+==
+ ** $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ++++
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==
+ ==+· @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@@ o+++
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· x ==
+ ox++ *$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$* x+++
+ +++x $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$=· ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@*· ·+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@*· ·+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ~+++
+ +++~ ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@=+*@@~ o+++
+ +++· x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$$ o+++
+ ==+· *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ o == +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ +++· +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ x+++
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == $@$$$@@@@@@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$@@@ ·x==
+ ==+· x@@@*o o%@@@@@@@@@@@%x· ~=@@@@@@@@@@@@=~ x%@$~ ==++
+ ==++ x=***=o o+***=+· ++==
+ x+===+ ++ ox ~x +===xx
+ ++====x+==*====+++ ++*=====++ x+*=*===++*+====xx
+ ++==****++++==************++++++=*******%***==++++==**++
+ ++ ++++++++++xx ++++++++++++
+
+
diff --git a/src/build/framegen/frames/frame_053.txt b/src/build/framegen/frames/frame_053.txt
new file mode 100644
index 0000000000..a49bc03a13
--- /dev/null
+++ b/src/build/framegen/frames/frame_053.txt
@@ -0,0 +1,41 @@
+
+ ++++==*********==+xx
+ x+==**=*+=o~ ··++%*%%*=++
+ ==**++ %***++
+ x+**++ ~o++====+x~ *+** x
+ ++** +%@@@@@@@@@@@@@@@@@@@@*x **++
+ ++=* o%@@@@$$$$$$$$$$$$$$$$$$$$@@@@$= +=++
+ ox**o %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* +=++
+ ==+o %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x +=++
+ xx== =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xx
+ ==+~ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·x==
+ == $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ xx++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·+==
+ +++~ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==
+ ==x· *$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$o ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$% ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ++++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% x==
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++· =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+xx
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+ == o
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ox== $@$$$$$$$$$@@@@@@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@@@% ~+==
+ ==x· o$@@@@@@@$+ +@@@@@@@@@@@%o o%@@@@@@@@@@$+ ==xx
+ ==++ ·x+o ·x+=+x· o+==+o +===
+ xx===+ == ++ o+=++===
+ ++====+=xo ~x*===**==+=xo ++****==+=+x ++*=**====
+ xx++=*********++++++=***********++++++==**********==++xx
+ ++xx++ xx++++xx xx++++++
+
+
diff --git a/src/build/framegen/frames/frame_054.txt b/src/build/framegen/frames/frame_054.txt
new file mode 100644
index 0000000000..ffc240ee74
--- /dev/null
+++ b/src/build/framegen/frames/frame_054.txt
@@ -0,0 +1,41 @@
+
+ ++====******====++
+ x+==**=*++o· ~x==%*%*==
+ ++**=*+x ++%===
+ ++**xx ~x++==++o~ o+**++
+ ===+ o*$@@@@@@@@@@@@@@@@@@@*x =+**
+ ===+ ·*@@@@$$$$$$$$$$$$$$$$$$$$@@@@$= oo**xx
+ ++=+ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* **xx
+ xx== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o ·~**
+ ==+~ +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ o+==
+ xx== $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==xx
+ +++o @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ +++x
+ ++++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·x==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·x==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ ++xx @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++~ o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++· =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==x %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ xx== +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·x==
+ xx== @@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$@@@~ ++++
+ ox==x =@@@@@@@@@@%~ ~%@@@@@@@@@$+ =@@@@%~ ·x==
+ ==++ ~oo~ ·ooo· ==++
+ xx===+ %% %% **=+ +x==++
+ ox====**====+*+x ++*=**===*=*++ xx**==**=*==+=+=*=**++
+ ++xx ++==********==++ ++==********==++ ++++****++xx
+
+
+
diff --git a/src/build/framegen/frames/frame_055.txt b/src/build/framegen/frames/frame_055.txt
new file mode 100644
index 0000000000..f233e3ef09
--- /dev/null
+++ b/src/build/framegen/frames/frame_055.txt
@@ -0,0 +1,41 @@
+
+ ++=====*****====++
+ ++***%=*=+o· x+**%%**=+
+ ++**+* ==**=+
+ ==== ·~ox+++xo~ ++**++
+ xx**++ +%@@@@@@@@@@@@@@@@@@@%+· =*==
+ ox**xo +$@@@@$$$$$$$$$$$$$$$$$$$@@@@@*~ ++*=
+ =*+o o$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ x+==
+ ++++ +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ++++
+ ox== ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xx
+ +++o =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==
+ == %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ++xx
+ +x++ =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·+==
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==+ *$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$x ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ +x++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% x==
+ +++x $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+ ·+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ x+++
+ ++++ ·@@$$@@@@@@@@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@@% ==++
+ xx== x$$= ~%@@@@@@@@@%~ =@@@@@@@@@$+ o= o+==
+ ==++ ·· ·· ·x==xx
+ ox===+ %*+x %%=+ **** ++**++
+ x==**=**=**==**+*++xxxx++%***==**=*=*xxoo++%=***=======**++
+ xx++++xx xx++********++xx ++==******==++ ++++
+
+
+
diff --git a/src/build/framegen/frames/frame_056.txt b/src/build/framegen/frames/frame_056.txt
new file mode 100644
index 0000000000..60a5299155
--- /dev/null
+++ b/src/build/framegen/frames/frame_056.txt
@@ -0,0 +1,41 @@
+
+ ++++====**====++++
+ ++***%=*==o~ · o+*=%%%*==++
+ ++**== x+%***++
+ ++**++ ~ooooo~· **==
+ ++**o o=$@@@@@@@@@@@@@@@@@@*x ++**++
+ ++== o%@@@@$$$$$$$$$$$$$$$$$$$@@@@@= **++
+ ++== ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* **++
+ ox=* +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o ·o**
+ ==+o ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ x+==
+ xx== %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xx
+ ==+o @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ~+==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx== +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++xx
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ +++o $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++~ ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++~ ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++· +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++· =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==+· *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==+ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==xo
+ +++x =@@$$$$$$$@@@@@@@@@$$$$$$$$$@@@@@@@@@@$$$$$$$$@@@@@$~ ==
+ xx== +$@@@@@$+ =@@@@@@@@@*· o%@@@@@@@@$+ ++++
+ ===+ x+==
+ ===+ **%% ++%%+x x+%%=+**xx
+ ++**+*++++*=**====**=*=+++++***=**++**=*==++++==%=**++=+
+ xx++=====+xx xx++======++++ ++========++
+
+
+
diff --git a/src/build/framegen/frames/frame_057.txt b/src/build/framegen/frames/frame_057.txt
new file mode 100644
index 0000000000..ce6e787a89
--- /dev/null
+++ b/src/build/framegen/frames/frame_057.txt
@@ -0,0 +1,41 @@
+
+ xx+++===========++xx
+ ++++***%=*==xx~~~~ox==******+++x
+ xx==**== ++**==xx
+ ++**++ ·~~~~· ++**++
+ ==== x*$@@@@@@@@@@@@@@@@@*+ ==*=
+ ===+ =$@@@@$$$$$$$$$$$$$$$$$$@@@@@*· ++**
+ ===+ =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%· x+==
+ ++=+ $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x +=++
+ xx== $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==xx
+ +++x +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ·+==
+ == %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xx
+ xx++ *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ ==+· ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$% ==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==
+ =* @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$x ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==+~ %@@@$$$$$$$$@@@@$$@@@@$$$$$$$$$@@@@$$@@@@$$$$$$$$@@$ ·x==
+ ++== o%@@@@@@@@%o *@@@@@@@@$= x%@@@@@@@$= ++++
+ ===+ +===
+ ====xx ++%%=+ %%== +===
+ ++**=*==++++=**=**++=*****++++==%=**++==***%++++==%***++
+ ++=======+++ ++++=====+++ xx++++==++++
+
+
+
diff --git a/src/build/framegen/frames/frame_058.txt b/src/build/framegen/frames/frame_058.txt
new file mode 100644
index 0000000000..76dd6000d7
--- /dev/null
+++ b/src/build/framegen/frames/frame_058.txt
@@ -0,0 +1,41 @@
+
+ xx++++++====++++++
+ ++==**%%*%**==++++==**%*****++
+ ++***%== x+*=**++
+ ==**++ ==**++
+ xx**++ x*$@@@@@@@@@@@@@@@%=o x+**++
+ ++**xx ~*@@@@@$$$$$$$$$$$$$$$$@@@@@$+ ~==++
+ xx**o~ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ==++
+ =*+~ *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ ·x*=xx
+ ++++ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ x+==
+ x+== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ==xx
+ +++x %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% x+++
+ == %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ xx++ x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==xx
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++++
+ +++o $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ x+++
+ +++~ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ %$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ~+++
+ +++o ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+++
+ xx+x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% x==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==
+ == $$$$@@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$@@@+ x+++
+ ++++ +$@@@@@@@$= ~%@@@@@@@@%~ =$@@@%o ==+x
+ ==++ +=++
+ ===+=*%% ==%%++ ++%%*= x~====
+ ++++++**=%**++==%***==++==*%**++++**%***++==***%***=**++
+ x+++==++++ ++++++==+++x +++++x
+
+
+
diff --git a/src/build/framegen/frames/frame_059.txt b/src/build/framegen/frames/frame_059.txt
new file mode 100644
index 0000000000..c2adc0c02b
--- /dev/null
+++ b/src/build/framegen/frames/frame_059.txt
@@ -0,0 +1,41 @@
+
+ xx++++++++++++++++
+ ++==***%%%************%***==++
+ ++***%== =+****++
+ xx====++ ++====xx
+ ++**++ x=%@@@@@@@@@@@@@$%+o +=**xx
+ ++**oo o%@@@@@@$$$$$$$$$$$$$$@@@@@$= x+*=+x
+ ++== +@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* x+==xx
+ x+== ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o ++==
+ ==+~ ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==+x
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ~+==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==+x
+ ++++ ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$* ==
+ == @@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $%%%$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ ==x %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++++
+ ==+· =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ +++~ x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++o ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+ ·+++
+ xx++ *@$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$$$$$$$$$@ ~+++
+ == x@@@$$$@@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@@$@@@· ++++
+ ++++ +$@@@@@@@$= ~*@@@@@@@@%~ +$@%~ ==
+ ==+x ==++
+ ===+o ==%%+o ==%%++ ++%%== xo=*++
+ ++**++++***%**====%***==++==***%====**%***++++**%%%***++
+ ++++==++ ++++++++++ ++xx
+
+
+
diff --git a/src/build/framegen/frames/frame_060.txt b/src/build/framegen/frames/frame_060.txt
new file mode 100644
index 0000000000..f28078a15d
--- /dev/null
+++ b/src/build/framegen/frames/frame_060.txt
@@ -0,0 +1,41 @@
+
+ ++++++++++++++
+ ++++****%%%%%%%%%%%%%*****++xx
+ ++***%**+x +=*=**==+x
+ x+**=*++ ==**++
+ ++**+= ·x=%$@@@@@@@@@$%=o xx====
+ ==== o*@@@@@@@$$$$$$$$$$$@@@@@@%x ====
+ ==*= =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ==++
+ ++== +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ ==++
+ ox== =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ o+==
+ ==+o x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* =+++
+ x+== %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ~+==
+ ==+o @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xx
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$% ++++
+ xx== x@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ xx++ ~%@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$= ·+==
+ ++++ x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ xx++ =$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ ============*@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ @@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ++++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ +++x
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ ==+· *$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ x+xx
+ +++x @@$$$$$$@@$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$$$@@$$$$$$@ x+xx
+ ox== =@@@@@@$$@@@@$$$$$$$$@@@@$$$@@@@$$$$$$$$@@@$$$@@@@@$ ==xo
+ +++x ~*· =$@@@@@@@%x o%@@@@@@@@* = ·x==
+ **+o ==++
+ ==+= ++%$=* %%** **%% xo**++
+ ++****==++++***%====***=**++++***%**==**%=**++++****==xx
+ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_061.txt b/src/build/framegen/frames/frame_061.txt
new file mode 100644
index 0000000000..c33681cc05
--- /dev/null
+++ b/src/build/framegen/frames/frame_061.txt
@@ -0,0 +1,41 @@
+
+ ++++++++++++++
+ ++++*****%*%%%%%%*%%****==++
+ ++=****%++ x+*===**++
+ x+==**=* =+====++
+ ++**+= ~x=*%$$@@@$$%=+~ ++==++
+ ====oo =%@@@@@@@@$$$$$$@@@@@@@@%+ ox==++
+ ==*= +$@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@%· ·x==++
+ ++== x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ o+==xx
+ x+== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= +===
+ ==+~ +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xx
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ++++
+ ==+· @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==
+ ox== @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xx
+ xx++ %@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++++
+ ++xx *ooooooooooooooo=@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++o $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++o %$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++o =ooooooooooooooo=@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ~+++
+ +++o @@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+++
+ +++x $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ xx+x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ +++o @@$$$$$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$$$$@@ ++++
+ ox== *@@@$@@@$$$@@@@$$$$$$$$@@@@$$@@@@@$$$$$$$@@@@$$$@@@$ ==xo
+ ==+x o%$= ~%@@@@@@@@%~ =$@@@@@@@$+ ·+==
+ **+o ==xx
+ ==+= ++%%=+ ++%**= %%*% oo**++
+ ++***%**++xx+=***%====*=**==++++***%====***=**++++**==+x
+ ++++++++ ++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_062.txt b/src/build/framegen/frames/frame_062.txt
new file mode 100644
index 0000000000..b8e23ebe0e
--- /dev/null
+++ b/src/build/framegen/frames/frame_062.txt
@@ -0,0 +1,41 @@
+
+ xx++++++++
+ xx++==*******%%*******==++++
+ ++==**=***++ · ++==*=**==++
+ ++==**=%+o ox=+====++
+ ++**== ~ox+====++o· xo+===++
+ ++==++ ·+%@@@@@@@@@@@@@@@@@@@@*x +===++
+ ====~ *@@@@@$$$$$$$$$$$$$$$$$$$$@@@@%~ ++==++
+ ++== *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ++==xo
+ ++==o· o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ ==++
+ ==+x ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ·x==xx
+ ++== %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ ==+~ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·x==
+ ox== @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xo
+ ++++ *$$$$@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$* ++++
+ +++x @$$@@%**************%@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ +++~ ·$$$* ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o+++
+ +++~ ~@$@~ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ ~@$$@+ ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ~+++
+ +++o @$$$@@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+++
+ xx+x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+++
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ ox++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ +++~ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ ox== %@@@$$$@@@@$$@@@@$$$$$$$$@@@@$$@@@@@$$$$$$$$@@@@$$@$ ==xo
+ ==+o x%@@@%~ ·*@@@@@@@@$+ x%@@@@@@@@*· ·+==
+ **xo ==xx
+ ==+= ++%%*= %%** **%%+o**++
+ ++***%*=%*==++++***%==++***=**++++***%==++==%***++x++++x
+ ++ ++++++++ ++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_063.txt b/src/build/framegen/frames/frame_063.txt
new file mode 100644
index 0000000000..e3eb943d49
--- /dev/null
+++ b/src/build/framegen/frames/frame_063.txt
@@ -0,0 +1,41 @@
+
+ xxxxxxxx
+ ++++==**************==++++
+ ++++*****%==++xx oo++==%*==**==++
+ ++**=*== ++======xx
+ ++===*++ ~oooo~ ++====++
+ ++==+= ~+%@@@@@@@@@@@@@@@@@$*x +===++
+ ++==xx o%@@@@@$$$$$$$$$$$$$$$$$$@@@@$+ +===++
+ ++==o~ x@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ +===
+ xx==+~ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==++
+ ==++ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ~+==xo
+ ++== +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ ==+o %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ~+==
+ == %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xo
+ xx++ x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$* ++xx
+ ++++ @$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ +++o +$$$$$= *$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o+++
+ +++o @$$$@ @$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o %$$$$$% ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o @$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ~+++
+ +++o $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+++
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% x==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ +++~ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++xx
+ xx== %@@@$$$$$@@@@$@@@@@$$$$$$$$@@@@$@@@@@$$$$$$$$$@@@@$$ ==xo
+ ==+o +$@@@@$+ +$@@@@@@@@%~ ~%@@@@@@@@$+ ·x==
+ **+o ==xx
+ **++ +=%%+o ++%%== *%=+=*++
+ ++***%==****++++==*%**++++*=**==++++***%++++==*=**++++xx
+ ++++ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_064.txt b/src/build/framegen/frames/frame_064.txt
new file mode 100644
index 0000000000..9fd11f4cb2
--- /dev/null
+++ b/src/build/framegen/frames/frame_064.txt
@@ -0,0 +1,41 @@
+
+
+ xx++++==********====++++
+ ++==****%%****++++==**%**=====++++
+ ++==***%++ x+*+====++
+ xx====== =+====++
+ ++====x+ o=%$@@@@@@@@@@@@$%=o xo====++
+ ++===+ ~*$@@@@@$$$$$$$$$$$$$$@@@@@$= +===++
+ x+==++ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% ====
+ ===+ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x ==++
+ ++== $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ~+==xx
+ ox==x· %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·+==
+ ==+· @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xx
+ ox== @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$% ++++
+ xx++ +@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ xx++ +@$$$$@= *@$$$$$$$$$$$$$$$$$$$$$$$$= ·+==
+ ++++ x@$$@* %@$$$$$$$$$$$$$$$$$$$$$$@% ·x==
+ xx++ %$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$@% x==
+ xx++ +$@@$$$$$@@$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$@% x==
+ xx++ @@$$$$$$$$$$@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ ==+· %$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ x+++
+ +++o @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ xxxx
+ ox== %@@$$$$$$$$@@@@@@@@@$$$$$$$$$@@@@@@@@@@$$$$$$$$$@@@@ ++xx
+ ==+o =$@@@@@@@* *@@@@@@@@@%o x%@@@@@@@@@= **
+ **x~ ==++
+ **+= +=%% %%=x *+**++
+ ++***%++++*=**==++===%==++x+==*=**++++*%**++++++*=**++++
+ ++++==++ ++====++ ++++==++++
+
+
+
diff --git a/src/build/framegen/frames/frame_065.txt b/src/build/framegen/frames/frame_065.txt
new file mode 100644
index 0000000000..b1ee24c5bf
--- /dev/null
+++ b/src/build/framegen/frames/frame_065.txt
@@ -0,0 +1,41 @@
+
+
+ xx++++==========++++xx
+ ++++==**=*%%%%******%*%*======++++
+ ++==**=**%+o ++*=====++
+ ++===*+= ======++
+ ====++ ·x=%$@@@@@@@@@$%=x +x====++
+ x+====oo ·=$@@@@@@@$$$$$$$$$@@@@@@@%+ +===++
+ ==== +@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* +===xx
+ ++== ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x ====
+ ++==x· o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ·+==+x
+ ==++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++==
+ ox== +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==xo
+ ++++ =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ++++
+ ==+· x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+==
+ == +%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$* x==
+ == =@$$$$$@+ ~*@$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == @$$$@ o@$$$$$$$$$$$$$$$$$$$$$$ ==
+ == ~@$$$@ +$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +$@$$$$$@$=++++++++++++++*@@$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == $@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ ==x %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ +++· =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ +++~ o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o+++
+ +x+x @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ ox== %@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@o ++++
+ +++o =@@@@@@@@@$x =@@@@@@@@@@* o%@@@@@@@@@%o *=
+ **x· ~~ ~~· ·~~ +=++
+ **++ **** +=%% ~==++
+ ++**=*+xoo++*+**++++**=*+x~~ox=+**==++**=*++ooox++*=**++
+ ++======++ ++====++++ ++++====++
+
+
+
diff --git a/src/build/framegen/frames/frame_066.txt b/src/build/framegen/frames/frame_066.txt
new file mode 100644
index 0000000000..8b97f549e6
--- /dev/null
+++ b/src/build/framegen/frames/frame_066.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++++++++++++++
+ ++++==*******%%%%***==**====++
+ ++=====**%++ x+****====++
+ ++====+=++ +x======+x
+ ++====++ ~x=**%%%%**=+~ +x====++
+ ++===+ o*$@@@@@@@@@@@@@@@@@@@@$*o +===++
+ ++==++ ·*@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@* +===+x
+ xx===+ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ++==
+ ==++ $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==++
+ ++==o %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ~+==xo
+ ==++ ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==++
+ ox== x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ o+==
+ ++== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==xo
+ +++x o*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$@o ==xx
+ +++~ o%@$$$$$@@=++++++++++++++=$@$$$$$$$$$$$$$$$$$$$$$ ++++
+ +++· $$$$@+ @$$$$$$$$$$$$$$$$$$$@ ++++
+ ==+· $$$$@~ @$$$$$$$$$$$$$$$$$$$@ x+++
+ ==+· x@@$$$$@*~ ·+@$$$$$$$$$$$$$$$$$$$$@ xx++
+ ==+· o%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$@ x+xx
+ +++· x@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ +++· =$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ +++· x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ~+++
+ +++o @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+++
+ xx+x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+++
+ ++++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$% ==
+ xx++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ == *@@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$@@$ o+++
+ +++x =@@@@@@@@@@@+ o%@@@@@@@@@@%~ ·=@@@@@@@@@%· ==xx
+ **x· ox++o ~x++x~ o+x~ x+==
+ **++ ==+x x+=+ ++==
+ ++**=%++ ·o=+**++===*++ ++*===+=**==o· ~x*+**++
+ x++==**===++ +++=***=+++x ++=***==++
+
+
+
diff --git a/src/build/framegen/frames/frame_067.txt b/src/build/framegen/frames/frame_067.txt
new file mode 100644
index 0000000000..f8d68716d2
--- /dev/null
+++ b/src/build/framegen/frames/frame_067.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++++++++++xx
+ ++====****************==++++
+ ++++=====*=*+x ~o==***=====++
+ ++====== ++=+====+x
+ xx====+= ~x+=====+o~ ++====++
+ xx====ox o*$@@@@@@@@@@@@@@@@@@@%+· +===++
+ ox==== o%@@@@@$$$$$$$$$$$$$$$$$$$@@@@@= +===++
+ ==== ·%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= +===xo
+ ++==o x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ==++
+ ==+x ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ·x==xo
+ ++== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ ==+o $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·+==
+ ox== @@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==++
+ xx+= ·*@@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$ ++++
+ xx++ ~*@@$$$$$@@%%%%%%%%%%%%%%%%@@$$$$$$$$$$$$$$$$$$@ o+++
+ +++x ~@$$$@x +$$$$$$$$$$$$$$$$$$$x ·+++
+ +++x @$$$@ @$$$$$$$$$$$$$$$$$@+ ·+++
+ +++x ~$$$$$$@o x@$$$$$$$$$$$$$$$$$$@+ ·+++
+ +++x =@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$@+ ·+++
+ +++x =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+++
+ xx+x @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+++
+ +++x $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+==
+ +++x @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* x==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ xx++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@@@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$@@ ·+==
+ +++x ~*@@@@@@@@@@@%o +$@@@@@@@@@@$+ o*@@@@@@@@@+ ++xx
+ **o· ~+===+o x====x ~+=+· o+==
+ x+**x++x ++ ++ x+**
+ ++==*=++ ~o*+**==**+= ++**==**+*x~ ++**=+
+ ++=****===++ ++=******=++ +++==**==+
+
+
+
diff --git a/src/build/framegen/frames/frame_068.txt b/src/build/framegen/frames/frame_068.txt
new file mode 100644
index 0000000000..829d8f6a31
--- /dev/null
+++ b/src/build/framegen/frames/frame_068.txt
@@ -0,0 +1,41 @@
+
+
+ ++++xx++
+ ++++====************==++++
+ ++==**===%**++xo~~~~xx+=%*%****=+++x
+ ++======+= *=*===++
+ ++====++ ·~~~· =+====xx
+ ++==++ ·+%$@@@@@@@@@@@@@@@@$=o +===++
+ ++==+x o*@@@@@$$$$$$$$$$$$$$$$$@@@@@$x +===+x
+ ++==+o ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ +===xo
+ ox==+x =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ++==
+ ++++ +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ·o==+x
+ xx==x· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++==
+ ++++ @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==xo
+ ==+· @@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ++x+
+ == x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ xx== +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$= ·+==
+ xx++ ~$$$$$$ +$$$$$$$$$$$$$$$$$$ ==
+ ++++ @* *@$$@o @$$$$$$$$$$$$$$$$@ ==
+ ++++ x$$$$$$· =$$$$$$$$$$$$$$$$$@ ==
+ ++++ =$@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$@ ==
+ ++++ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ++++ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ %$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ xx++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox++ *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ox== =$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@@@@@$$$$$$$$@@+ ==
+ +++o oo+%@@@@@$$@@@@@%+o~o=$@@@@$$$@@@@@*x~o+%@@@@@@@@% ++++
+ ** x*%$$$*+ ~=%$$$%=o +*%=~ ·o**
+ xx**· ·o**++
+ xx******+= =+****=* ++=***=*+x =+**xx
+ ++==%%%%%%==++ ++==*%%%%%**=+ x+==*%%*==++
+
+
+
diff --git a/src/build/framegen/frames/frame_069.txt b/src/build/framegen/frames/frame_069.txt
new file mode 100644
index 0000000000..2868c56cd4
--- /dev/null
+++ b/src/build/framegen/frames/frame_069.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++==**********====++++
+ xx++==****%%**++++++++**%**=**==++
+ ++**=*=* ~x*=====++
+ xx====+= ++====++
+ ++===+ ~=%$@@@@@@@@@@@@@@$%+· o=*==++
+ x+==++ +$@@@@@$$$$$$$$$$$$$$$$@@@@@%o +===xx
+ ox===+ =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x +===xx
+ ==++ ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ++==
+ ++== @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==xx
+ ==+~ *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ++==
+ ++== $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ ==+x $@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ++++
+ == +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+==
+ == +$@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$ ==
+ == ~$$$$$$+ x$$$$$$$$$$$$$$$$@ ==
+ == %@%· ~@$$@% =@$$$$$$$$$$$$$$$ ==
+ == %$$$$$~ $$$$$$$$$$$$$$$$@ ==
+ == ~*@@@$$$$$@@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$@ ==
+ == ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == $@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@$$$$$$$$$@% ==
+ +++o x*+=*@@@@$$$$$$@@@$*=+=%@@@@$$$$$@@@@%=+=*@@@@$@@@@o x+++
+ ~o** o*$@@@@$*o +%@@@@@%+ x*$%+ **
+ ++** **++
+ ++**==**== ==****xo =+****++ ++**++
+ +x ++==%%%%%%%*==++ x+==*%%%%%%%**=+ +=*%%%**=+
+
+
+
diff --git a/src/build/framegen/frames/frame_070.txt b/src/build/framegen/frames/frame_070.txt
new file mode 100644
index 0000000000..67de5408d6
--- /dev/null
+++ b/src/build/framegen/frames/frame_070.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++========++++++
+ ++==*****%%%********%*%*****==++
+ ++==**=*++ ++*=**==++
+ ++**+= =+**++
+ xx====ox ~+%$@@@@@@@@@@@@$%=~ xo====xx
+ xx===+ ·=@@@@@@$$$$$$$$$$$$$$@@@@@@=· +===++
+ ===+ ~%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%~ +===
+ ++== =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ==++
+ ++==o =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==+x
+ ==++ ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ x+==
+ xx== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ ++++ ~$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++++
+ ==+· x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ·+==
+ == x%@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$@ ==
+ == *@$$$$@+ ·%$$$$$$$$$$$$$$$ ==
+ == ·@@@+ %@$$@x $$$$$$$$$$$$$$@~ ==
+ == ·*o $$$$$* ·$$$$$$$$$$$$$$@~ ==
+ == =@@$$$$$@@$%%%%%%%%%%%%%%%@@$$$$$$$$$$$$$$@~ ==
+ == =$@@@$$$$$$$$$$@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$@~ ==
+ == +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ~@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$@@$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$$$@@$$$$$$$$$@@ ==
+ ==+~ %@@$$@@@@$$$$$$$$@@@@$$$@@@@$$$$$$$$@@@@$$@@@@$$@@@% ·+==
+ xx== =@@@@@@@@%x x%@@@@@@@@= *@@$+ ==xx
+ ++=+ +=++
+ ++==x+**%* **%%+x +=%%=+ oo==++
+ ++==++x+**=%=*==**%***++x+==*%*%====****+++++=*****===++
+ ++++++ ++++++++ ++
+
+
diff --git a/src/build/framegen/frames/frame_071.txt b/src/build/framegen/frames/frame_071.txt
new file mode 100644
index 0000000000..42ed54523f
--- /dev/null
+++ b/src/build/framegen/frames/frame_071.txt
@@ -0,0 +1,41 @@
+
+
+
+ +++++++===+++++++x
+ xx++*****%*%*%%%*%%*%*******++xx
+ xx++***%== +=*=**==++
+ ++**+=+x *=**++
+ ====x+ x=%$@@@@@@@@@@$%*x· xx====
+ ==== x%@@@@@@$$$$$$$$$$$$@@@@@@$+ +===
+ ==== *@@@@$$$$$$$$$$$$$$$$$$$$$$$$$@@@%~ +===
+ ++== x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ==++
+ xx==x· x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==+x
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o x+==
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xx
+ xx++ %$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++++
+ +++o =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·x==
+ == =$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$@ ==
+ == x ·%@$$$$@= o$$$$$$$$$$$$$$$~ ==
+ == @@@$x %@$$@x $$$$$$$$$$$$$@+ ==
+ == @%x $$$$$* $$$$$$$$$$$$$@+ ==
+ =* +$@@$$$$@@%***************@@$$$$$$$$$$$$$@+ ==
+ =* x%@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$@+ ==
+ == x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· $@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@@@@@@@$$$@@@ ·x==
+ xx== =@@@@@@@@@@= o%@@@@@@@@@%~ =@@@%· ++xx
+ ==++ ·· ·~· x+==
+ ==++ x+%% %%+x **=+ +===
+ ++**==++===%==xoox==%===++==*%==+xoo++*=**+++=**=**=**++
+ +++==+++ +++===++++ ++++
+
+
diff --git a/src/build/framegen/frames/frame_072.txt b/src/build/framegen/frames/frame_072.txt
new file mode 100644
index 0000000000..95a4a2af40
--- /dev/null
+++ b/src/build/framegen/frames/frame_072.txt
@@ -0,0 +1,41 @@
+
+
+
+ +++++++++++++x
+ ++==*****%*%%%%%%%%*****==++
+ ++***%=*x~ *=*=**++
+ x+**==++ xx=+**++
+ ++**++ ~+*%$@@@@@@@@$$%=o +x**=+
+ ++==oo ~*@@@@@@@$$$$$$$$$$@@@@@@@%x =*==
+ ++== +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* +===
+ x+==o $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ==++
+ ==+o @@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ==xx
+ ++++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o x+==
+ ==x· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xx
+ xx== $%$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% x+++
+ +++x =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==
+ ==+· =$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$@ ==
+ == @x ·*@$$$$@$o o$@$$$$$$$$$$$$x ==xo
+ == $@@@$x o@$$$$ *@$$$$$$$$$$@* ==xo
+ == @@@*o =@$$$$ %$$$$$$$$$$$@* ==xo
+ == % ·=@@$$$$@@%===============*@@$$$$$$$$$$$@* ==xo
+ == ·=$@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$@* ==xo
+ == % =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x ~@@@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@@@@@@$$$$@@+ ==
+ x+++ =%o +$@@@@@@@@@@@+· o*@@@@@@@@@@@*o ·+$@@@* ++++
+ =*+~ x=**=x· o+=*=+o ·x**
+ **+x x+ +x ++ ox**xo
+ ====*===**+*+x x+*=**===*++ *+**==**++++*===
+ +x xx===*%%*===++ x +==*%%*=*=++ ++=+ x
+
+
diff --git a/src/build/framegen/frames/frame_073.txt b/src/build/framegen/frames/frame_073.txt
new file mode 100644
index 0000000000..d2d6c6d725
--- /dev/null
+++ b/src/build/framegen/frames/frame_073.txt
@@ -0,0 +1,41 @@
+
+
+
+ x+++++++++++
+ ++++****%%%%%%%%$%%%****++++
+ ++****=*+x *=*=**++
+ xx==**++ ++*=**++
+ ++**++ x=%$$@@@@@@@$%*+~ ++**++
+ ++**oo =$@@@@@@@$$$$$$$$$@@@@@@@%x =*==
+ ++==o· o$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* +===
+ x+=*o· %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ==++
+ ==+x $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ==xx
+ ++== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o x+==
+ ==+· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==xx
+ xx== @%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ x+++
+ ++++ o ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==
+ ==x· x o*@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$@ ==
+ == $$~ o$@$$$$@$o ·x@$$$$$$$$$$$$= ==xx
+ == @$@@@*~ *@$$@= $$$$$$$$$$$@% ++xx
+ == @@@@*o %$$$$* $$$$$$$$$$$@% ++xo
+ == @= ~*@@$$$$@@*+++++++++++++++%@$$$$$$$$$$$@% ++xx
+ == @ ·*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$$$$$$$@% ++xx
+ == @+ ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+ == o
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == +@@$$@@@@@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$@% ==
+ ++++ %@$=o~o=$@@@@$$$@@@@@*x~~x*@@@@@$$$@@@@$+o~o=$@@@$~ x+++
+ **o· ~+%$$$%*o x*$$$$%+· · **
+ x+**o~ ~**+x
+ xx**+==+****== ++*=**=*x+ oo==****++ =+**+x
+ ++=+ ++==*%%%%%%*== xx+=*%%%%%%**=++ ++==++
+
+
diff --git a/src/build/framegen/frames/frame_074.txt b/src/build/framegen/frames/frame_074.txt
new file mode 100644
index 0000000000..bb794eea44
--- /dev/null
+++ b/src/build/framegen/frames/frame_074.txt
@@ -0,0 +1,41 @@
+
+
+
+ xx+++++x
+ ++==***%*%%%%%%%%%****+++x
+ x+==***%++ ~x%*****++
+ ++**++ ++*=**xx
+ xx**++ o+*%$$@@@@@$$%*x~ +x**++
+ x+**xx +$@@@@@@@@$$$$$$$@@@@@@@@%x =*++
+ x+**o~ ~%@@@@$$$$$$$$$$$$$$$$$$$$$$$@@@@* +=++
+ o **+~ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ==++
+ ==+x %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ==xx
+ x+== +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o o+==
+ ==+~ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==xo
+ ox== @@%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ x+++
+ ++++ $o o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==
+ +++~ ·@ o%@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$$$$$$@ ==
+ == %$$· x$@$$$$@$x~~~~~~~~~~~~~~~+@$$$$$$$$$$$* =+xo
+ == @$$@@@*~ *@$$@* $$$$$$$$$$$$ ++xx
+ == @$@@@*x *$$$$* $$$$$$$$$$$$ ++xo
+ == @$* =@@$$$$@@=xxxxxxxxxxxxxx+*@$$$$$$$$$$@$ ++xx
+ == @@ =$@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$$$$$$@$ ++xx
+ == @$+ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* =+xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+ ==
+ == %@$$$$$@@@$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$@@@$$$$$@@ ==
+ +++o +@@@@$**%$@@@$$$$$$$@@@@%**%@@@@$$$$$$$@@@$**%@@@@@* ~+==
+ ox** ~ ~*@@@@@@@%o +$@@@@@@$= oo ==xx
+ ++== ==++
+ ++**o+ ++%%** ++%%**xx **%%++ +x**++
+ +=**++ ++***%*%%%%*%*++ xx==*%*%%%%*%*==++ +=**=+
+ xx ++
+
diff --git a/src/build/framegen/frames/frame_075.txt b/src/build/framegen/frames/frame_075.txt
new file mode 100644
index 0000000000..497f7840cf
--- /dev/null
+++ b/src/build/framegen/frames/frame_075.txt
@@ -0,0 +1,41 @@
+
+
+
+ x+++
+ ++==***%%%%%%%%%%***==++
+ ++**=*++ ~x%*%*==+x
+ ++**== ++*===+x
+ xx**== ~+*%$$@@@@@$$%=x· ++**++
+ xx**+x x%@@@@@@@@$$$$$$$@@@@@@@@%o ==++
+ xx**x~ ·*@@@@$$$$$$$$$$$$$$$$$$$$$$$@@@@* +*++
+ **x~ =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ==++
+ ==++ *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* *=xx
+ xx== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x o+==
+ ==+~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==xo
+ == @@$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ x+++
+ ++++ %$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ +++~ @* +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$$$@ ==
+ ==+ *$@= =@$$$$$@*o~~~~~~~~~~~~~~o*@$$$$$$$$$$* +++x
+ == $$$@@@$+ @$$$@ @$$$$$$$$$$ ++xx
+ == @$$@@$*~ @$$$@ ~@$$$$$$$$$$ ++xx
+ == @$$x o%@$$$$$@%+xxxxxxxxxxxxxx+$@$$$$$$$$$$$ ++xx
+ == @@= ~*@@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$ ++xx
+ == @$$ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+ ==xo
+ == $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ +++~ *@@@@@@$$@@@@$$$$$$$$@@@@@$@@@@$$$$$$$$$@@@@$$@@@@@$ ·x==
+ oo== x* ·*@@@@@@@@$x +$@@@@@@@@= ~+ ==+x
+ ++=+ +=++
+ ++==o **** =*%%x ++%$++ ==++
+ x+==****+x++***%**==**%***++++==**=*====%***=+xx++**==xx
+ ++++++ +++++++x
+
diff --git a/src/build/framegen/frames/frame_076.txt b/src/build/framegen/frames/frame_076.txt
new file mode 100644
index 0000000000..517a5a5b34
--- /dev/null
+++ b/src/build/framegen/frames/frame_076.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++==***%%%%%%%%*%***==++
+ +=****++ o+*=%*==xx
+ ++**+= ++*===
+ **+= ~+*%$$@@@@@$$%=x~ ++**++
+ xx**+x x%@@@@@@@@$$$$$$$@@@@@@@@%x ==++
+ ox**x~ *@@@@$$$$$$$$$$$$$$$$$$$$$$$@@@@* =*++
+ =*+~ =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ==++
+ ++++ *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* *=oo
+ xx== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x o+==
+ =++o %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==xo
+ == $@@$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ o+++
+ xx++ *@% =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ +++o @$o ·=@@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$$@· ==
+ ==+· *$$$x ~*@$$$$$@=~~~~~~~~~~~~~~~o%@$$$$$$$$$% ++xx
+ == $$$$@@@%o @$$$@ x@$$$$$$$$@ ++xx
+ == $$$$@@$=· @$$$@ +@$$$$$$$$@ ++xx
+ == $$$$~ x%@$$$$$@*+xxxxxxxxxxxxxx+$@$$$$$$$$$@ ++xx
+ == $$@o o%@@@@$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$$$$$$@ ++xx
+ == $$$% o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==+· $@@$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@@@@@@@@@· ==
+ x+== ·%@$x =@@@@@@@@@@= o$@@@@@@@@@$o =o ++xx
+ ===+ ·~~· ~o~ x+==
+ ===+ ++%% %%+x **=+ ++==
+ ++**=**===++**=*=+~·~o+=%=**++===*==o~··x+%=**++++****++
+ ++ x++=====++xx ++======++
+
diff --git a/src/build/framegen/frames/frame_077.txt b/src/build/framegen/frames/frame_077.txt
new file mode 100644
index 0000000000..4ead3508d6
--- /dev/null
+++ b/src/build/framegen/frames/frame_077.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ +++==**%%%%%%%%*%%**=+++
+ ++****++ ·x**%*==xx
+ ++**+= +x*===
+ ==+= ~+*%$$@@@@@$$%*+~ +x**++
+ xx**+x x%@@@@@@@@$$$$$$$@@@@@@@@%x ==++
+ **x~ *@@@@$$$$$$$$$$$$$$$$$$$$$$$@@@@* +*++
+ ==+~ =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ==++
+ ++++ *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* **oo
+ xx== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ o+==
+ +++o %@$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==xo
+ == $@@@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ o+++
+ ++++ *@$~ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ +++o @$@ x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$$@~ ==
+ ==+· =$$$% x@@$$$$@$x~~~~~~~~~~~~~~~=@$$$$$$$$$% ++xo
+ == $@$$@@@$=· %$$$@+ @$$$$$$$$@ ++xx
+ == $@$$@@@*o %$$$@= $$$$$$$$$@ ++xx
+ == $@$$= ·=@@$$$$@@=xxxxxxxxxxxxxx+*@$$$$$$$$$@ ++xx
+ == $$$$ =$@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$$$$$@ ++xx
+ == $$$$x =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==+· @@$$$@@@@@@@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@@@x ==
+ x+++ x$@@%o +@@@@@@@@@@@+ o%@@@@@@@@@@%o =+ ++++
+ ==+x ox+xo ~x++x· ~+**
+ **++ == ==++ ++== ++**
+ ++**==*=**==**=*++ ·o*+**++===*++ ++*===++****=+
+ ++++ ++++****==++ +==***==++
+
diff --git a/src/build/framegen/frames/frame_078.txt b/src/build/framegen/frames/frame_078.txt
new file mode 100644
index 0000000000..4a7f7ab729
--- /dev/null
+++ b/src/build/framegen/frames/frame_078.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ +++===*%*%%%%%%%%%%*=+=+
+ ++**=*++ ·o**%*==xx
+ ++**+= +x*===
+ ==== o+*%$@@@@@@@$%*+o +x**++
+ xx**+x +%@@@@@@@$$$$$$$$$@@@@@@@%x ==++
+ **x~ ·%@@@@$$$$$$$$$$$$$$$$$$$$$$$@@@@%~ +=++
+ =*+~ =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ==++
+ ++++ *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% **oo
+ ox== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ~+==
+ +++o %@$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==xo
+ == $@$@$%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ o+++
+ xx++ *@$% +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ +++o @$@= +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$@o ==xo
+ ==+· =$$$@= =@$$$$$@=~··············~*@$$$$$$$$$ ++xo
+ == %@$$$@@@$+ @$$$@ ·@$$$$$$$@ ++xx
+ == $@$$$@@$= @$$$@ o@$$$$$$$@ ++xx
+ == $@$$$~ x%@$$$$$@$+++++++++++++++=$@$$$$$$$$@ ++xx
+ == $@$@+ o%@@@@$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$$$$$@ ++xx
+ == $$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ ==x· ~@@$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@@@@@@@@= ==
+ xx++ =@@@@$x ·x$@@@@@@@@@@@=~ o*@@@@@@@@@@@%x ·+* x+++
+ **x~ x=***+~ ~+***=o ·o**
+ **+x o++ ++ ++ oo**xo
+ ====++x+*===**==+o ·x*=**====++ =+**==*=====
+ ++=+++ ++===*%%*==+++ ++==*%%**=++ xx
+
diff --git a/src/build/framegen/frames/frame_079.txt b/src/build/framegen/frames/frame_079.txt
new file mode 100644
index 0000000000..98c847b442
--- /dev/null
+++ b/src/build/framegen/frames/frame_079.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++==**%%%%%%%%%%$%%**=++
+ ++****+o ==%*==xx
+ ++**+= ox*===
+ =*== o=%$$@@@@@@@$$%=o xx**++
+ xx**+x +$@@@@@@@$$$$$$$$$@@@@@@@$+ =*++
+ **o~ ~%@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@%o +=++
+ **x~ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ==++
+ ++=+ *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% **xx
+ xx== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ~+==
+ +++o %@$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ == $@$@@%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ xx++ *@$@· ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ==
+ +++o @$$@ ~*@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$@o ==xo
+ ==+· =$$$$$~ o$@$$$$@%~ x@$$$$$$$$$ ++xx
+ == %@$$$$@@@%o %@$$@+ $$$$$$$$@ ++xx
+ == $@$$$@@$=~ %$$$@* $$$$$$$$@ ++++
+ == $@$$$= o*@@$$$$@@*++++++++++++++=%@$$$$$$$$@ ++xx
+ == $@$$$ ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$$$$@ ++xx
+ == $@$$$+ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* =+xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == o@@$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@@@* ==
+ ++++ *@@@@@*x· ~x%@@@@@@@@@@@$+~ o=@@@@@@@@@@@@*o· ~+* x+++
+ **x· o x*%%%*+· ~=%%%%=o ·o**
+ ox**xo oo xx xx oo**xx
+ ===*+x *+****=*+x =+****==++ ++==*****===
+ ++====++ ++==**%%%**=++ ++=**%%%*==+x ++
+
diff --git a/src/build/framegen/frames/frame_080.txt b/src/build/framegen/frames/frame_080.txt
new file mode 100644
index 0000000000..b8e4d01399
--- /dev/null
+++ b/src/build/framegen/frames/frame_080.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++
+ +==**%*%%$%%%%%%%%%*==++
+ +=**=*x· +=%***++
+ ++**++ *=**+x
+ **++ ~+*$$@@@@@@@@@$$*+~ ox**++
+ xx**oo ~=@@@@@@@$$$$$$$$$$$@@@@@@@=~ +=++
+ xx**o· x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x +==+
+ **x· %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ++++
+ +++x %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ ==xx
+ xx== =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ·+==
+ =++~ $@$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xx
+ == @@$@@%*%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ xx++ %$$$= o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ +++o @$$@x x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$@x ==xo
+ ==+· =$$$$@* +@$$$$$@o =@$$$$$$$$ ++xx
+ == %@$$$$@@@@* @$$$@ o@$$$$$$@ ++xx
+ == $@$$$@@@*~ o@$$$@ =$$$$$$$@ ++++
+ == $@$$$% ·=@@$$$$$@$*==============%@@$$$$$$$@ ++xx
+ == $@$$@~ ·=$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$$$@ ++xx
+ == $@$$$$~ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* =+xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == +@@$$$$$$@@@@@@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@@@% ==
+ ++++ %@@@@@@*x~~x*@@@@@@$@@@@@$+o~o+$@@@@@$@@@@@@*x~~x*~ o+++
+ **o· ·+x o*%$$$%+· ·+%$$$%*o **
+ xx**o~ ~**++
+ xx**+= x+=****=++ ~x==****+= =+****==**++
+ ++==**==xx xx==**%%%%%*==++ ++**%%%%%*==++ ++
+
diff --git a/src/build/framegen/frames/frame_081.txt b/src/build/framegen/frames/frame_081.txt
new file mode 100644
index 0000000000..aed9c8aaac
--- /dev/null
+++ b/src/build/framegen/frames/frame_081.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++++
+ ++==*%*%%%****%%$%%%**++xx
+ xx==**=* ++%***++
+ ++**++ ==**++
+ xx**++ o=%$@@@@@@@@@@@$%=o ==++
+ xx**oo o%@@@@@@$$$$$$$$$$$$$@@@@@@%o +===
+ x+** +@@@@$$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ ++==
+ **o· $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· +=++
+ +++x $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@· ==xx
+ xx== *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ·x==
+ ==+~ @@$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ == @$$@@%==$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ xx++ %$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ +++~ @$$@% =@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$x ==xo
+ ==x· *$$$$@$o ~$$$$$$@x o@$$$$$$$$ ++xx
+ == %@$$$$$@@@$o $$$$@ @$$$$$$@ ++xx
+ == $@$$$$@@*~ $$$$@+ o@$$$$$$@ ++++
+ == $@$$$$ ·=@@$$$$$@@%**************%@@$$$$$$$@ ++xx
+ == $@$$@% =$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$$$@ ++xx
+ == $@$$$$+ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == +@@$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@@@@@$ ==
+ ++++ $@@@@@@$=xox=@@@@@$$$@@@@@*xoo+%@@@@@$$$@@@@$=oox*o o+++
+ **x· ~==· ~=%$$$$*x +%$$$$%=· **
+ x+**~· **+x
+ xx**+= ==****++ ==****+= =+****==**++
+ ++==**==++ ++**%%%%%*==++ +=***%%%%%**++ ++
+
diff --git a/src/build/framegen/frames/frame_082.txt b/src/build/framegen/frames/frame_082.txt
new file mode 100644
index 0000000000..ba491c1f56
--- /dev/null
+++ b/src/build/framegen/frames/frame_082.txt
@@ -0,0 +1,41 @@
+
+
+
+ xx++++++++++
+ ++==***%%%*=++++==**%*%*==++
+ ++**=*++ · *=**++
+ ===* ==**++
+ ++**x+ o=%$@@@@@@@@@@@@@$%=~ ====
+ ++** +$@@@@@$$$$$$$$$$$$$$$@@@@@$+ ++*=
+ xx** *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* x+==
+ o ** o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x ++++
+ ==+o ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==+x
+ x+== %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·x==
+ ==x· @@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$@%xo=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·+==
+ ++++ $$$$@ ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ +++~ ·@$$$@ ·=@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$$$+ ==xo
+ ==+· *$$$$$@*~ x$$$$$$+ =$$$$$$$$ ++xx
+ == $@$$$$$@@@@* +@$$@* %@$$$$$@ ++xx
+ == $@$$$$@@=· %$$$$$ ·$$$$$$$@ ++xx
+ == $@$$$$~ ~*@@@$$$$$@@$$$$$$$$$$$$$$$@@$$$$$$$@ ++xx
+ == $@$$$@ ·*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$$$$o ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == =@$$$$$$$$@@@@@@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@@$ ==
+ +++x $@@@@@@@%+xx=%@@@@$$$$@@@@@*xox*@@@@@$$$$@@@@%+xx=x o+++
+ ** o*%+ +%$@@$%=~ o*$$@@$%+ ** o
+ x+**~ **+x
+ xx**+= =+****++ ++****== x+****==**++
+ ++==*%**++ ++**%%%%$%**=+ ++==*%%%%*%*==++ ++
+
diff --git a/src/build/framegen/frames/frame_083.txt b/src/build/framegen/frames/frame_083.txt
new file mode 100644
index 0000000000..d730b375fd
--- /dev/null
+++ b/src/build/framegen/frames/frame_083.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++++++=++x
+ ++==*%*%**++xxxxxx==%*%**=++
+ +=**=*+~ *=**=+
+ x+**== ++**++
+ ++**xo ·+%$@@@@@@@@@@@@@@@$%x ====
+ ++*= ~*@@@@@$$$$$$$$$$$$$$$$$@@@@@*~ x+**
+ ++== ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%~ ~+**
+ xx** +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= x+==
+ ==x· x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ +=++
+ x+++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ **
+ ==x· @$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ++++
+ ox== @$$$@*~ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ·+==
+ xx++ @$$$@~ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ +++~ o$$$$$x x%@@@$$$$$@@@@@@@@@@@@@@@@@@@$$$$$$$= ==xo
+ ==x %@$$$$@$+ %$$$$$+ ~$$$$$$$$ ++xx
+ == $@$$$$$$@@@$~ @$$@$ +@$$$$$@ ++xx
+ == $@$$$$@@= %$$$$$x ·$$$$$$$@ ++xx
+ == $@$$$@x o*@@@$$$$$@@@@@@@@@@@@@@@@@@@$$$$$$$@ ++xx
+ == $$$$$@ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$$$@=· ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == $$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == =@$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$ ==
+ +++x ·$@@@@@@@$=xx+%@@@@@$$$@@@@@%+xx=$@@@@$$$$@@@@$=xx=x o+++
+ ** o*%=~ x%$@@@$%x ~=%$@@$%=~ **xo
+ ++** **++
+ x+**+= =+****== xx****== ==**==**+x
+ ++=**%**=+ ++***%%%%*%*++xx ++==*%%%%*%*==++ xx
+
diff --git a/src/build/framegen/frames/frame_084.txt b/src/build/framegen/frames/frame_084.txt
new file mode 100644
index 0000000000..1fd3352b97
--- /dev/null
+++ b/src/build/framegen/frames/frame_084.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++===***==++++
+ +=***%**+x~· oo==%%%*++++
+ +=**== x+*=**++
+ ++**++ ~ox+++xo~ ===+
+ ++== +%@@@@@@@@@@@@@@@@@@$%x ++**xx
+ ===+ =@@@@@$$$$$$$$$$$$$$$$$$$@@@@@+ oo**xx
+ ++=+ =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ·o**
+ x+== $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ~+==
+ ** %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% +=++
+ ++++ ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ **
+ == x@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ++++
+ ox== ~@$$$@= o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·x==
+ +++x @$$$@+ o%@@@@$$$$$$$$$$@@@@@@@@@@@@@@$$$$$$$$@ ==
+ +++· +$$$$$% x%@@$$$$$@@$$$$$$$$$$$$$$$$@@$$$$$$= ==xo
+ == $@$$$$@@$+ x@$$$$x =$$$$$$$ ++xx
+ == $$$$$$$$@@@$o @$$$@ @$$$$$@ ++xx
+ == $$$$$$@$x ·$$$$$$$· o$$$$$$$@ ++xx
+ == $$$$$@= +$@@@$$$$$@@@@@@@@@@@@@@@@@@@$$$$$$$@ ++xx
+ == $$$$$$= +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$@@=x+$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++xx
+ == @$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* =+xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == =@$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@$ ==
+ +++x ·@@@@@@@@@%+xx=$@@@@$$$$@@@@$=xx+%@@@@$$$$$@@@@%+x+o o+++
+ ** o*$%x ~=%$@@$%=~ +%$@@@$%x **xo
+ ++** **++
+ ++**+= xx****** ==****xo ==******+x
+ ++***%%*++xx ++==*%%%%*%**=++ ++==*%*%%%%***++
+
diff --git a/src/build/framegen/frames/frame_085.txt b/src/build/framegen/frames/frame_085.txt
new file mode 100644
index 0000000000..4e074e65be
--- /dev/null
+++ b/src/build/framegen/frames/frame_085.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++****%%%%***=++++
+ ++==***%+x ox**%*==++
+ xx===*+x *=**++
+ ++**x+ ~x+=*****=+o· ==*=
+ ===+ x%$@@@@@@@@@@@@@@@@@@@$*o +x**+x
+ ==+x o%@@@@$$$$$$$$$$$$$$$$$$$$$@@@@%~ **xx
+ ===+ ·%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% **xx
+ ++++ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ·x**
+ ** @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ +++x +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x *=
+ =* *@$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++++
+ xx++ +@$$$$x +$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==
+ +++o @$$$@* +$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@ ==
+ ==+· =$$$$$$· +$@$$$$$@@*==============*@@$$$$$$= ==xo
+ == $@$$$$$@@*x $$$$@x o@$$$$@$ ++xx
+ == $$$$$$$$@@@%~ $$$$@ @$$$$$$ ++xx
+ == @$$$$$@%· o$$$$$$@+ +@$$$$$$$ ++xx
+ == @$$$$@* ~*@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$$ ++xx
+ == @$$$$$% ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$@@%*%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == =@$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@$ ==
+ +++x ·$@@@@@@@@$=xx+%@@@@$$$$@@@@@*+xx*@@@@@$$$$@@@@%=xxo o+++
+ ** o*$%+ +%$@@$$*o o*$$@@$%+ ** o
+ ++** **++
+ x+**+= ~x=*****xo ==***=++ =+******+x
+ ++==*%%*==xx x+==*%*%%%%*==++ ==**%$%%%%**=+
+
diff --git a/src/build/framegen/frames/frame_086.txt b/src/build/framegen/frames/frame_086.txt
new file mode 100644
index 0000000000..d1ca5713f2
--- /dev/null
+++ b/src/build/framegen/frames/frame_086.txt
@@ -0,0 +1,41 @@
+
+
+
+ x+++==*%%$%%%%%%%**===++
+ +=****++ ox****==
+ ++**+= ++**++
+ ==== ~+*%$$@@@@@$%*=x· ++**+x
+ xx**xx +%@@@@@@@@$$$$$$$@@@@@@@@*o **++
+ ox**o~ ~%@@@@$$$$$$$$$$$$$$$$$$$$$$$@@@@= ==++
+ **+~ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ==xx
+ ++=x %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= **
+ xx== +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o x+++
+ ==+~ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==
+ == @@$$$@@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ x+++
+ +x++ $$$$$$ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==
+ +++~ ·@$$$@$ +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == %$$$$$@* +@@$$$$@%x~~~~~~~~~~~~~~o=@$$$$$$= ==+x
+ == @$$$$$$@@@$+ $$$$@o @$$$$@$ ++xx
+ == @$$$$$$@@@*o $$$$@x @$$$$@$ ++xx
+ == @$$$$$@= ~*@$$$$$@$+xxxxxxxxxxxxxxx%@$$$$$@$ ++xx
+ == @$$$$@$ ·=@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@$ ++xx
+ == @$$$$$$~ ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xx
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == =@$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@@@% ==
+ +++x $@@@@@@@@@=xox=@@@@@$$$@@@@@%+oo+%@@@@@$$$@@@@$=xo~ o+++
+ ** o*$%=~ o*%$$$$%x +%$$$$%=~ ** o
+ ++**~ **+x
+ x+**+= *=***=++ =+****+= =+******++
+ ++==*%%*==++ ==**%%%%%***++ ++***%%%%%**++
+
diff --git a/src/build/framegen/frames/frame_087.txt b/src/build/framegen/frames/frame_087.txt
new file mode 100644
index 0000000000..cfec2a6984
--- /dev/null
+++ b/src/build/framegen/frames/frame_087.txt
@@ -0,0 +1,41 @@
+
+
+ ++++
+ ++=**%*%*%******%%%*==++
+ xx==*%== +=*=**++
+ ++*=x+ ==**xx
+ xx**++ o=%$@@@@@@@@@@@%*+~ xx**++
+ ++**~ o%@@@@@@$$$$$$$$$$$$@@@@@@$=· =*++
+ x+** =@@@@$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o +=++
+ ** ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ==++
+ ==+~ ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% **
+ x+== %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ o+==
+ ==x· @@$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==xo
+ ox== @$$$$@$=*$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ x+++
+ +++x @$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==
+ ==+· +$$$$$$ =$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == $$$$$$$$o ·%@$$$$@= o$$$$$$$= ==xo
+ == @$$$$$$$@@@$o $@$$@o @$$$$@% ++xx
+ == @$$$$$$@@%x $$$$$= @$$$$@% ++xx
+ == @$$$$$$~ +$@$$$$$@@%**************%@@$$$$$@% ++xx
+ == @$$$$$$ +$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@% ++xo
+ == @$$$$$$= x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == =@@$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@@@% ==
+ +++x $@@@@@@@@@*xoo+$@@@@@$$@@@@@%+oox*@@@@@$$$@@@@@=xo· o+++
+ **o· ~=%%=o +%$$$$%+ o*%$$$%=~ ** o
+ x+**~· ~**+x
+ ++**+= ==****++ =+****+= ++******++
+ ++==*%%**=++ ++***%%%%***++ ++***%%%%%**++
+
diff --git a/src/build/framegen/frames/frame_088.txt b/src/build/framegen/frames/frame_088.txt
new file mode 100644
index 0000000000..237b9c931f
--- /dev/null
+++ b/src/build/framegen/frames/frame_088.txt
@@ -0,0 +1,41 @@
+
+
+ ++++====++++++
+ ++==*%**==++++++++==**%*==++
+ ++**=* ==**++
+ ++**+= · ++**++
+ ++== ~=%@@@@@@@@@@@@@@@@$*x ====
+ ++=+ x$@@@@@$$$$$$$$$$$$$$$$@@@@@$= ++==
+ ++=+ +@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* x+==
+ x+== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o +=++
+ ** %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==xo
+ ++++ o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ·+==
+ =* =@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xx
+ xx++ +@$$$$@x ·+$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ +++~ @$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$% ==
+ == $$$$$$$ =$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == @$$$$$$@%o x$$$$$$ =$$$$$$+ == o
+ == $$$$$$$$@@@@= %@$$@~ @$$$$@= ==xx
+ == @$$$$$$@%o o$$$$$$ =$$$$$@= ==xo
+ == @$$$$$$ +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@= ==xo
+ == @$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$@x +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@@$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@* ==
+ ++++ %@@@@@@@@@*o··o=@@@@@@@@@@@@%x~·~x%@@@@@@@@@@@@=o~ x+++
+ **x· +*%=o ~=%%$%*x x*%$%%=~ ·o**
+ x+**o~ o oo**++
+ ++**+=xo =+****== *+****==+x ++==****++
+ ++==******++ ++****%%%***++ ++==**%%%***==+x
+
diff --git a/src/build/framegen/frames/frame_089.txt b/src/build/framegen/frames/frame_089.txt
new file mode 100644
index 0000000000..b3622446f7
--- /dev/null
+++ b/src/build/framegen/frames/frame_089.txt
@@ -0,0 +1,41 @@
+
+
+ ++++==****==++++
+ ==***%==++ ox==%***== x
+ ==**++ ++*===xx
+ ++**++ ·oxx++xxo~ +x**=+
+ ===+ ~=$@@@@@@@@@@@@@@@@@@$*o ++**
+ ==+x ~%@@@@@$$$$$$$$$$$$$$$$$$$@@@@$x x+**
+ ==+x ·%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o o+==
+ ++=+ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= +=++
+ ox=* ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xo
+ +++o *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ == $@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xx
+ x+++ $@$$$@% ~=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ ==+· x@$$$$$ ~*@@@@$$$$$$$$$$@@@@@@@@@@@@@@@$$$$$$$$ ==
+ == @$$$$$$ ~*@@$$$$$@@$%%%%%%%%%%%%%%$@@$$$$$@ ==
+ == $$$$$$$@@=· $$$$$* o$$$$$$o ==
+ == @$$$$$$$@@@@+ $$$$@· @$$$$@x ==
+ =* @$$$$$$@+ *@$$$$$o %$$$$$@x ==
+ =* @$$$$$$ x%@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@x ==
+ == @$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ == $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@@$$$$$$$$$@@@@@@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@+ ==
+ ++++ *@@@@@@@@@=o ·x%@@@@@@@@@@@%x· ~=@@@@@@@@@@@@=~ ++++
+ **x· o=*+~ x=%%%=x ~+*%%*+~ ·x**
+ x+**xo xo ++ ++oo**+x
+ ++**=*xx *=****==+x *+****==++ ++==****++
+ ++==******++ ++==********++xx ++==********==++
+
diff --git a/src/build/framegen/frames/frame_090.txt b/src/build/framegen/frames/frame_090.txt
new file mode 100644
index 0000000000..e781e1e310
--- /dev/null
+++ b/src/build/framegen/frames/frame_090.txt
@@ -0,0 +1,41 @@
+
+
+ +++=**%%%%%%%%**=+++
+ ++***%==o· ·x==%***++
+ ++**+* *+**++
+ ==== o+=*%$$$$%*=+o ====
+ xx**xx o*@@@@@@@@@@@@@@@@@@@@@@*x xx**+x
+ xx**o~ ·%@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@%~ ~o**xx
+ **x· *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ·o**
+ ==+o %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ o+==
+ xx== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ++++
+ ==x· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ xx== @$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ =++x
+ ++xo @$$$$$o ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ == %$$$$@$ ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$$+ ~*@$$$$$@$=+++++++++++++++%@$$$$$@ ==
+ == $$$$$$$@@@=~ $$$$$x @$$$$$~ ==
+ == @$$$$$$@@@@= $$$$@~ @$$$$@~ ==
+ == @$$$$$@% +@$$$$$@%~··············~=@$$$$$@~ ==
+ == @$$$$@$ x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@~ ==
+ == @$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$@@%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@@$$$$$$$$$@@@@@@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@@@o ==
+ ++++ +@@@@@@@@@+· +$@@@@@@@@@@%o ~*@@@@@@@@@@@+ ++++
+ **+~ x+o o+=+x~ ~x+=+o ~x**
+ xx**+x == == ++++**+x
+ xx**==+= x+==*****=+* ~x*=******=*x~ =+==**==xx
+ ++==******++ ++==********+++x xx++********==+x
+
diff --git a/src/build/framegen/frames/frame_091.txt b/src/build/framegen/frames/frame_091.txt
new file mode 100644
index 0000000000..425fbf0204
--- /dev/null
+++ b/src/build/framegen/frames/frame_091.txt
@@ -0,0 +1,41 @@
+
+
+ ++==**%%*%%%%%%*%%**==++
+ ==***%xx xx%***==
+ ++**++ ++**++
+ ++**++ x=*$$@@@@@@$$*=x ++**+x
+ ++**~o ~*$@@@@@@@$$$$$$$$@@@@@@@$*~ oo**++
+ ++** +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ **++
+ xx** ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ **xx
+ ==+~ ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@$$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ x+++ o@$$$$@@%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o +++x
+ +++~ @$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$$$ x$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@* +@$$$$$@%o··············~=@$$$$$@ ==
+ == $$$$$$$@@@$= $$$$@~ @$$$$$· ==
+ == ·@$$$$$$@@$*o $$$$@x @$$$$@~ ==
+ == ·@$$$$$$+ ~*@$$$$$@$=xxxxxxxxxxxxxx+%@$$$$$@~ ==
+ == ·@$$$$$$ ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@~ ==
+ == ·@$$$$$$o ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@@@ x==
+ ++++ o$@@@@@@@@+ ~*@@@@@@@@@@*~ +@@@@@@@@@@$x ++++
+ ==+o ·o~ ~oo~ ~oxo· o+==
+ xx**+x == == ==++**+x
+ x+==**+*x~ ++*=**==**+*+x x+*=**==**=*++ ~x*=****==xx
+ x+++****==++ ++++********++++ ++==******=++x
+
diff --git a/src/build/framegen/frames/frame_092.txt b/src/build/framegen/frames/frame_092.txt
new file mode 100644
index 0000000000..040799032f
--- /dev/null
+++ b/src/build/framegen/frames/frame_092.txt
@@ -0,0 +1,41 @@
+
+ xx++++xx
+ ===*%%%%*%****%*%%%%*===
+ ++**=%++ ++%=**++
+ ===* **==
+ x+**+x ~+*$@@@@@@@@@@@@$*+~ x+**+x
+ ++== =$@@@@@$$$$$$$$$$$$$$@@@@@$= ==++
+ ++*= ~%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%~ =*++
+ x+== =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ==+x
+ =*o =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= o*=
+ ++++ ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ++++
+ == =@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==
+ x+++ =@$$$$@$**@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ++xx
+ +++· ·@$$$$$ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ·+++
+ == @$$$$@$ ~*@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == $$$$$$@%~ ~$$$$$$@= x@$$$$$$ ==
+ == ·@$$$$$$$@@@%~ $$$$@· @$$$$@· ==
+ == ·@$$$$$$@@%x $$$$$+ ·@$$$$@· ==
+ == ·@$$$$$$~ +$@$$$$$@@%***************$@$$$$$@· ==
+ == ·@$$$$@% x$@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$@= +$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· $@@$$$$$$$@@@@@@@@@@$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@@$ ·+==
+ ++++ =@@@@@@@%o ~%@@@@@@@@@+ +@@@@@@@@@%~ ++++
+ ===+ +===
+ x+=*=+ ++%%++ %%=+ =*====xx
+ ==**=*==++*=*=**==**=*=*++++=**=**===*=**%++++==%***==++xx
+ ++++==**==++ ++==****==++++ ++++****==++++
+
diff --git a/src/build/framegen/frames/frame_093.txt b/src/build/framegen/frames/frame_093.txt
new file mode 100644
index 0000000000..06e4f4dbe5
--- /dev/null
+++ b/src/build/framegen/frames/frame_093.txt
@@ -0,0 +1,41 @@
+
+ xx++++++++xx
+ x+==*%*%*%========%*%*%*==+x
+ ++**=*+x x+*=**++
+ xx**=* ==**xx
+ ++**o x*$@@@@@@@@@@@@@@$*x o**++
+ ++=+ o*@@@@@$$$$$$$$$$$$$$$$@@@@@*o +=++
+ ++=+ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x +=++
+ x+== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==+x
+ ** %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% **
+ +++x x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x x+++
+ =* *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* *=
+ ++++ *@$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++++
+ ==+· o@$$$$@* o*@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ·+==
+ == @$$$$@* =$@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == $$$$$$% o*@@$$$$@@*++==========++=$@$$$$$$ ==
+ == ·@$$$$$@@$+· $$$$$x $$$$$@· ==
+ == ·@$$$$$$@@@*o %$$$@~ @$$$$@· ==
+ == ·@$$$$$$~ o$@$$$$@*~ ·+@$$$$$@· ==
+ == ·@$$$$@= +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$o o*@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ == @@$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$$@@@$$$$$$$$$$$$$$$$@@ *=
+ ==+~ *@@@$$$$$$@@@@%%$@@@@$$$$$$$@@@@$%%@@@@$$$$$$$$@@@@* ~+==
+ ++== o%@@@@@@= x$@@@@@@@%x ~*@@@@@@@@= ==++
+ ===+ +===
+ ox====o+ ++%%== x+%%** *=====xo
+ ++**=**%***=*=**++==***%****%**=**++==**=*=***%**=**==++
+ x+++====++++ ++++======++xx ++++======++++
+
diff --git a/src/build/framegen/frames/frame_094.txt b/src/build/framegen/frames/frame_094.txt
new file mode 100644
index 0000000000..bd9522ac36
--- /dev/null
+++ b/src/build/framegen/frames/frame_094.txt
@@ -0,0 +1,41 @@
+
+ xx++++====++++xx
+ ++==*%**==++oooo++==**%*==++
+ ++**=* *=**++
+ ++**+= ···· =+**++
+ ++== x=$@@@@@@@@@@@@@@@@$=x ==++
+ ==++ =$@@@@$$$$$$$$$$$$$$$$$$@@@@$= ++==
+ ===+ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* +===
+ ++=+ ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· +=++
+ ox** $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ** o
+ +++o =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= o+++
+ == $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ ==+· +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == $$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$ ==
+ == ·@$$$$@= ·*@$$$$@x =@$$$$@· ==
+ == ·@$$$@ ~@$$@ @$$$@· ==
+ == ·@$$$$x +$$$@ x$$$$@· ==
+ == ·@$$$$@@*==============*@@$$$$@$=++++==========*@@$$$$@· ==
+ == ·@$$$$$$@@@@@@@@@@@@@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@% ==
+ ==+o ~@@@@$$$$@@@@*+x+*@@@@$$$$$@@@@%=xx=$@@@@$$$$@@@@@*~ o+==
+ xx== x*$@@$*o o*$@@@@%+ ·=%@@@@$*o ==xx
+ ==== ~·++==
+ ====+= =+*==*++ ++****++ ++=*====
+ ++*****%**%***==++++**=***%%%*****++++***%%%%%***=**++++
+ ++++++++++ ++++++++++++ ++++++++++++
+
diff --git a/src/build/framegen/frames/frame_095.txt b/src/build/framegen/frames/frame_095.txt
new file mode 100644
index 0000000000..b566afe9cb
--- /dev/null
+++ b/src/build/framegen/frames/frame_095.txt
@@ -0,0 +1,41 @@
+
+ ++============++
+ ++**%%=*++o~····~o++*=%%**++
+ +=**== ==**=+
+ ++**++ ·~~oo~~· ++**++
+ ===+ +%@@@@@@@@@@@@@@@@@@%+ +===
+ ===+ ·*@@@@@$$$$$$$$$$$$$$$$$$@@@@@*· ++==
+ ===x %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% x===
+ ++=+ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o +=++
+ xx== @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ ==+~ *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ~+==
+ == $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ +++x $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ x+++
+ ==x· =@$$$$@@@@@@@@@$$$$$$$$$$$$$$$@@@@@@@@@$$$$$$$$$$$$$@= ·x==
+ == @$$$$$% +%@@@@@$$$$$$$$$$% ·+%@@@@@$$$$$$$$$@ ==
+ == $$$$$@o +%@@@$$$$$$@ +%@@@$$$$$$$ ==
+ == ·@$$$$$$~ o$$$$$$$$ o$$$$$$@· ==
+ == ·@$$$$$$@@@%~ x@$$$$$@@@@*· +@$$$$@· ==
+ == ·@$$$$$@+ $$$$$$$$x $$$$$$@· ==
+ == ·@$$$$@o ·+$@@@$$$$$@ ~+$@@@$$$$$@· ==
+ == ·@$$$$$* ~=$@@@@@$$$$$$$$$= ~=$@@@@@$$$$$$$$@· ==
+ == ·@$$$$$@@$%$@@@@@$$$$$$$$$$$$$$@@$%$@@@@$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@@$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@+ ==
+ ==+x %@@@@@@@@@@%x· ·x*@@@@@@@@@@@@=~ ~+$@@@@@@@@@@@%x x+==
+ ++==o· x*%%=x o=%%%*+~ ~+*%%%=x ·x==++
+ ====oo ox xo====
+ ====+=+x ++======+= ======+= ~ ++*=====
+ ++==*******=**==++++************==++++==*****%******++++
+ ++++++++++ +++++++++x ++++++++++
+
diff --git a/src/build/framegen/frames/frame_096.txt b/src/build/framegen/frames/frame_096.txt
new file mode 100644
index 0000000000..aff846aa19
--- /dev/null
+++ b/src/build/framegen/frames/frame_096.txt
@@ -0,0 +1,41 @@
+
+ ++====****====++
+ +=***%==+o o+==%***=+
+ ===*++ ++*===
+ ++**x+ ·ooxxxxoo· +x**++
+ ===+ o=$@@@@@@@@@@@@@@@@@@$=o +===
+ **++ o%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%o ++**
+ ==+x ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ x+==
+ ++++ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ++++
+ ox== ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==xo
+ ==+~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~+==
+ ox== @@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$$@@%%@@@@$$$$$$$$$$$$$$$@@$%%@@@@$$$$$$$$$$$$$@ x+++
+ ==+ =@$$$$$ x%@@@@$$$$$$$$$$$$ x$@@@$$$$$$$$$$@= +==
+ == @$$$$@$ +$@@@$$$$$$$@% +$@@@$$$$$$$@ ==
+ == $$$$$$@% =@$$$$$$$@* *@$$$$$$$ ==
+ == ·@$$$$$$@@@$= $$$$$$$$@@@$+ $$$$$$@· ==
+ == ·@$$$$$$@@@$+ $$$$$$$$@@@$+ $$$$$$@· ==
+ == ·@$$$$$@% =@$$$$$$$@* *@$$$$$$@· ==
+ == ·@$$$$$% +$@@@$$$$$$$@% +$@@@$$$$$$$@· ==
+ == ·@$$$$$$ x%@@@$$$$$$$$$$$$$ x%@@@$$$$$$$$$$$@· ==
+ == ·@$$$$$$@$%%@@@@$$$$$$$$$$$$$$$@@$%%@@@@$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@@ ·x==
+ ++++ o$@@@@@@@@$+ ·*@@@@@@@@@@*· x$@@@@@@@@@$x ++++
+ xx==+x ·oo~ ~oo~ ~ooo~ o+==+x
+ ++==+x ===+ +=** x++==++
+ ++====+=+x x+*+*=====+===o~ ~o=+*+=====*==+x ++*=====++
+ ++++==********==++++==**********==++++++=*********==++++
+ ++++++ xxxx++xxxx ++++++++
+
diff --git a/src/build/framegen/frames/frame_097.txt b/src/build/framegen/frames/frame_097.txt
new file mode 100644
index 0000000000..316e492933
--- /dev/null
+++ b/src/build/framegen/frames/frame_097.txt
@@ -0,0 +1,41 @@
+
+ ++====****====++
+ xx+=***%==x~ ~x==%***=+xx
+ x+===*++ ++*===+x
+ ++**+x ~ox++++xo~ x+**++
+ ==++ x*$@@@@@@@@@@@@@@@@@@$*x ++==
+ **+x x%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%x x+**
+ ==+o o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o o+==
+ ++++ +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ++++
+ xx== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xx
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ x+== @@$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@ ==+x
+ +++x @$$$$@@=xx%@@@@$$$$$$$$$$$$$$@@=x+%@@@$$$$$$$$$$$$$@ x+++
+ ==x *$$$$$$ x$@@@$$$$$$$$$$$% +$@@@$$$$$$$$$$* x==
+ == @$$$$$$ =$@@@$$$$$$$% =$@@@$$$$$$@ ==
+ == $$$$$$@@+ ~$$$$$$$$@@+ ~$$$$$$$$ ==
+ == ~@$$$$$$$@@@@+ $$$$$$$$$@@@@+ $$$$$$@~ ==
+ == ·@$$$$$$@@$+ $$$$$$$$@@$+ $$$$$$@· ==
+ == ·@$$$$$$x +$@$$$$$$$$o =$@$$$$$$@· ==
+ == ·@$$$$@$ x$@@@$$$$$$$$@% x$@@@$$$$$$$$@· ==
+ == ·@$$$$$$o ~*@@@@$$$$$$$$$$$$$~ o%@@@@$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==x· $@@$$$$$$$$@@@@@@@@@@$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@$ ·x==
+ ++++ =@@@@@@@@*~ o%@@@@@@@@$+ =@@@@@@@@@*· ++++
+ ox==++ ++==xx
+ ++===+ **%* =*%% +++===++
+ ++=====%+=++==*==========*==++==***========%+=++++*=*+====++
+ xx++==******==++xxx+++==******==+++x+x++==******====++
+ xx++xx xx++xx xx++++
+
diff --git a/src/build/framegen/frames/frame_098.txt b/src/build/framegen/frames/frame_098.txt
new file mode 100644
index 0000000000..7eb1d4acfc
--- /dev/null
+++ b/src/build/framegen/frames/frame_098.txt
@@ -0,0 +1,41 @@
+
+ xx++==********==++xx
+ x+==*%**=+~· ·~+=**%*==+x
+ x+=*=*+x x+*=*=+x
+ ++**xo ·ox+====+xo· ox**++
+ **++ x*@@@@@@@@@@@@@@@@@@@@*x ++**
+ **+o x$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$x x+**
+ ==+o x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x o+==
+ ++++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++++
+ xx== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xx
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @@$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@@+·~=@@@@$$$$$$$$$$$$$$@@x·~=@@@@$$$$$$$$$$$$@ x+++
+ == *$$$$$$ ·=@@@@$$$$$$$$$@% ~*@@@@$$$$$$$$$* ==
+ == @$$$$$$ ~*@@@$$$$$$$% ~*@@@$$$$$$@ ==
+ == $$$$$$$@*~ =$$$$$$$@@*· =$$$$$$$ ==
+ == ~@$$$$$$$@@@@+ $$$$$$$$$@@@@+ $$$$$$@~ ==
+ == ·@$$$$$$@$x o$$$$$$$@@$x x$$$$$$@· ==
+ == ·@$$$$$$ +$@@$$$$$$$$ +$@@$$$$$$@· ==
+ == ·@$$$$@$ x$@@@@$$$$$$$$@% x$@@@@$$$$$$$$@· ==
+ == ·@$$$$$@$~ x%@@@@$$$$$$$$$$$$$@%· x%@@@@$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == $@$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@$ ==
+ ==+o +@@@$$$$$$@@@@%**%@@@@$$$$$$@@@@$***%@@@@$$$$$$@@@@+ o+==
+ ++== ·=$@@@@@%x o*@@@@@@$= =$@@@@@@*x ==++
+ ===+ +===
+ ++====x+ x+****+x ****++ =+====++
+ ++=====**%****%==========********==========**%****%*======++
+ ++++======++++ ++++========++ ++++========++xx
+
+
diff --git a/src/build/framegen/frames/frame_099.txt b/src/build/framegen/frames/frame_099.txt
new file mode 100644
index 0000000000..624e9eea2d
--- /dev/null
+++ b/src/build/framegen/frames/frame_099.txt
@@ -0,0 +1,41 @@
+
+ +++===********===+++
+ ++==*%**++~· ·~++**%*==+x
+ xx**=%+x x+%=**xx
+ ++*=x ~ox+====+xo~ x=*++
+ **++ x%@@@@@@@@@@@@@@@@@@@@%x ++**
+ **+o +$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$+ o+**
+ =*+o x$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$x ~+*=
+ ++++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++++
+ xx== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xo
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @$$$$@@@@@@$$$$$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$$@ ==xo
+ +++x @$$$$@$o +$@@@$$$$$$$$$$$$$$@$o +$@@@$$$$$$$$$$$$@ x+++
+ == *$$$$$$ +$@@@$$$$$$$$$@% +$@@@$$$$$$$$$* ==
+ == @$$$$$$ +$@@$$$$$$$$ =$@@$$$$$$@ ==
+ == $$$$$$$@%o x$$$$$$$@@%o +$$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$$$$$$@@@@x $$$$$$@· ==
+ == ·@$$$$$$@*~ +$$$$$$$@@*~ =$$$$$$@· ==
+ == ·@$$$$$$ ·=$@@$$$$$$$% ~*@@@$$$$$$@· ==
+ == ·@$$$$$$ =@@@@$$$$$$$$$@% ·=@@@@$$$$$$$$$@· ==
+ == ·@$$$$$@@x·~=$@@@$$$$$$$$$$$$$$@$x·~=@@@@$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@@% ==
+ ==+x ~$@@@@$$$@@@@$=xx+%@@@@$$$$@@@@@*+xx=$@@@@$$$$@@@@%~ x+==
+ ++== o*$@@$%=· +*$@@@$*x ~=%@@@$%+ ==++
+ ==== ~·++==
+ xx====+= ++**=*+= x+====+= x=+====xx
+ ++====*%%%***=====++====%%%%%**=====++====*%*%%*%*====++
+ xx++++===+++++ xx++++====++++ xx++++====++++
+
+
diff --git a/src/build/framegen/frames/frame_100.txt b/src/build/framegen/frames/frame_100.txt
new file mode 100644
index 0000000000..c66e0879c8
--- /dev/null
+++ b/src/build/framegen/frames/frame_100.txt
@@ -0,0 +1,41 @@
+
+ +++===********===+++
+ ++==*%**++~· ·~++**%*==+x
+ xx**=%+x x+%=**xx
+ ++*=x ~ox+====+xo~ x==++
+ **++ +%@@@@@@@@@@@@@@@@@@@@%+ ++**
+ **+o +$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$+ o+**
+ ==+~ x$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$x ~+*=
+ ++++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++++
+ xx== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xo
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @$$$$@@@@@@$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$$@ ==xo
+ +++x @$$$$$%· o*@@@@$$$$$$$$$$$$$@% o%@@@@$$$$$$$$$$$@ x+++
+ == *$$$$@$ o%@@@@$$$$$$$$@% x%@@@@$$$$$$$$* ==
+ == @$$$$$$ x%@@$$$$$$$$ x%@@$$$$$$@ ==
+ == $$$$$$$@$+ ~$$$$$$$@@$x o$$$$$$$ ==
+ == ~@$$$$$$$@@@@+ $$$$$$$$$@@@@x $$$$$$@~ ==
+ == ·@$$$$$@@= *$$$$$$$@@+ *$$$$$$@· ==
+ == ·@$$$$$$ o%@@@$$$$$$$% o%@@@$$$$$$@· ==
+ == ·@$$$$$$ o*@@@@$$$$$$$$$$% o%@@@@$$$$$$$$$@· ==
+ == ·@$$$$$@@=ox*@@@@$$$$$$$$$$$$$$@@=ox*@@@@$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@+ ==
+ ==++ *@@@@@@@@@@@=~ x%@@@@@@@@@@@%x ~=@@@@@@@@@@@@= ++==
+ xx==x· o=***+~ x=***=x ~+****+~ ·x==++
+ ====xo +o xx xx====
+ ======+x o=+=====*+x =+=*====++ ++======
+ ++==***%******==++++==***%********++++==************==++
+ ++++++++++ ++++++++++ ++++++++++++
+
+
diff --git a/src/build/framegen/frames/frame_101.txt b/src/build/framegen/frames/frame_101.txt
new file mode 100644
index 0000000000..63dc3de2c7
--- /dev/null
+++ b/src/build/framegen/frames/frame_101.txt
@@ -0,0 +1,41 @@
+
+ xx+===********===+xx
+ ++==*%**++~· ·~++**%*==+x
+ xx**=*+x x+*=**xx
+ ++*=xo ·ox+====+xo· ox=*++
+ **++ x%@@@@@@@@@@@@@@@@@@@@%x ++**
+ **+o +$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$+ o+**
+ =*+o x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x o+*=
+ ++++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++++
+ xx== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xo
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$$% ~*@@@@$$$$$$$$$$$$$@* o*@@@@$$$$$$$$$$$@ x+++
+ == *$$$$@% o*@@@@$$$$$$$$@% o%@@@@$$$$$$$$* ==
+ == @$$$$$$ o%@@$$$$$$$$ o%@@$$$$$$@ ==
+ == $$$$$$$@$+ ·$$$$$$$$@$+ ~$$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$$$$$$@@@@x $$$$$$@· ==
+ == ·@$$$$$@@+ *$$$$$$$@@+ %$$$$$$@· ==
+ == ·@$$$$$$ x%@@@$$$$$$$% x%@@@$$$$$$@· ==
+ == ·@$$$$$$ o%@@@@$$$$$$$$$$% x%@@@@$$$$$$$$$@· ==
+ == ·@$$$$$@@=xx%@@@@$$$$$$$$$$$$$$@@=x+%@@@@$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@o ==
+ ++++ +@@@@@@@@@@$+ ·=@@@@@@@@@@@*~ o%@@@@@@@@@@$x ++++
+ ox==+~ x+=+o x+=+x· o+==+o ~+==+x
+ ++==+x ++ == ++==++
+ ++====++ x+*+==**==++ ox=+==***=+* =+==**++
+ ++==**********==++xx++**********==++++++************++++
+ ++++++xx ++++++xx xx++++xx
+
+
diff --git a/src/build/framegen/frames/frame_102.txt b/src/build/framegen/frames/frame_102.txt
new file mode 100644
index 0000000000..a53995bece
--- /dev/null
+++ b/src/build/framegen/frames/frame_102.txt
@@ -0,0 +1,41 @@
+
+ xx+===********===+xx
+ +++=****+=o· ·o==****=++x
+ x+=*=*+x x+*=*=+x
+ ++**+x ~x++==++x~ x+**++
+ **++ x*@@@@@@@@@@@@@@@@@@@@*x ++**
+ **xx x%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%x xx**
+ ==+o o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o o+==
+ ++++ +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ++++
+ xx== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xx
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@% ~*@@@@$$$$$$$$$$$$$@* ~*@@@@$$$$$$$$$$$@ x+++
+ == *$$$$@$ ~*@@@@$$$$$$$$@% o*@@@@$$$$$$$$* ==
+ == @$$$$$$ o%@@$$$$$$$$ o%@@$$$$$$@ ==
+ == $$$$$$$@$=· ·$$$$$$$$@$+ ~$$$$$$$ ==
+ == ~@$$$$$$$@@@@+ $$$$$$$$$@@@@x $$$$$$@~ ==
+ == ·@$$$$$@@+ *$$$$$$$@@+ %$$$$$$@· ==
+ == ·@$$$$$$ x%@@@$$$$$$$% x$@@@$$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$% x%@@@@$$$$$$$$$@· ==
+ == ·@$$$$$@@=x+%@@@@$$$$$$$$$$$$$$@@=x+%@@@@$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@ ·x==
+ ++++ ~%@@@@@@@@@%~ o%@@@@@@@@@$+ =@@@@@@@@@@%~ ++++
+ ox==+x oo~ ~oo· ~oo~ x+==xo
+ x+==++ ++** %% ++==++
+ ++=*==+*xx ~x++*=**=*===*+x ++*=**==**+*++ x+*===*=++
+ ++==********++xx ++==********==++ ++==********==++
+
+
+
diff --git a/src/build/framegen/frames/frame_103.txt b/src/build/framegen/frames/frame_103.txt
new file mode 100644
index 0000000000..04ce9a0054
--- /dev/null
+++ b/src/build/framegen/frames/frame_103.txt
@@ -0,0 +1,41 @@
+
+ x++====******====++x
+ ++==***%==xo ox==%***==+x
+ xx=*=*++ ++*=*=xx
+ ++**++ ~ox++++xo~ ++**++
+ **++ o*$@@@@@@@@@@@@@@@@@@$*o ++**
+ **+x o%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%o x+**
+ ==+o ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ o+==
+ ++++ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ++++
+ xx== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xo
+ ==+~ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ~+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$$% ~*@@@@$$$$$$$$$$$$$@* o*@@@@$$$$$$$$$$$@ x+++
+ ==+ *@$$$@% o*@@@@$$$$$$$$@% o%@@@@$$$$$$$@* x==
+ == @$$$$$$ o%@@$$$$$$$$ o%@@$$$$$$@ ==
+ == $$$$$$$@$= ·$$$$$$$$@$+ ~$$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$$$$$$@@@@x $$$$$$@· ==
+ == ·@$$$$$@@+ *$$$$$$$@@+ %$$$$$$@· ==
+ == ·@$$$$$$ x%@@@$$$$$$$% x$@@@$$$$$$@· ==
+ == ·@$$$$$$ o%@@@@$$$$$$$$$$% x%@@@@$$$$$$$$$@· ==
+ == ·@$$$$$@@=xx%@@@@$$$$$$$$$$$$$$@@=x+%@@@@$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==+· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@@@@@@@@$$$$$$$$$@@@ ·+==
+ ++++ %@@@@@@@@@*· *@@@@@@@@@$x x$@@@@@@@@@* ++++
+ ==+x ·· ·· ·· x+==
+ x+==++ +=%% +%*+ ++==++
+ xx==**=*++xxxx*=*=**==**=*++xxxx+=*=**==**=*=+xxxx++*+**==++
+ ++++******==++ xx++*******=++++ ++++=*******++++
+
+
+
diff --git a/src/build/framegen/frames/frame_104.txt b/src/build/framegen/frames/frame_104.txt
new file mode 100644
index 0000000000..6447b659ba
--- /dev/null
+++ b/src/build/framegen/frames/frame_104.txt
@@ -0,0 +1,41 @@
+
+ xx++=====**=====++xx
+ ++++***%**++~· ·~++**%***+++x
+ xx==*%=+ +=%*==xx
+ ++**++ ~~oooo~~ ++**++
+ ==== ·+%@@@@@@@@@@@@@@@@@@%+· ====
+ **+x ~*@@@@@$$$$$$$$$$$$$$$$$$@@@@@*~ x+**
+ ==+x ·%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%· x+==
+ ++=+ x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x +=++
+ xx== ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ==xo
+ ==+~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@@ ==xo
+ +++x $$$$$@%· o%@@@@$$$$$$$$$$$$$@%· x%@@@@$$$$$$$$$$$$ x+++
+ ==+· =@$$$@$ x%@@@@$$$$$$$$@% x%@@@@$$$$$$$@= ·+==
+ == @$$$$$$ x%@@$$$$$$$$ x$@@$$$$$$@ ==
+ == $$$$$$$@$+ ~$$$$$$$@@$x o$$$$$$$ ==
+ == ~@$$$$$$$@@@@+ $$$$$$$$$@@@@x $$$$$$@~ ==
+ == ·@$$$$$$@= =$$$$$$$@@= *$$$$$$@· ==
+ == ·@$$$$$$ o%@@@$$$$$$$% o%@@@$$$$$$@· ==
+ == ·@$$$$$$ o*@@@@$$$$$$$$$$% o*@@@@$$$$$$$$$@· ==
+ == ·@$$$$$@@=oo*@@@@$$$$$$$$$$$$$$@@+ox*@@@@$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· $@@$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@@@@@@@@$$$$$$$$$@@$ ·x==
+ +++= =@@@@@@@@@= +$@@@@@@@@%o ~%@@@@@@@@$= ++++
+ ==++ +===
+ xx=*=+ =*%% ++%%++ +=**+x
+ ++**=*=+++++******++**=%*=++++==*=**++**=*==++++++*=**++
+ xx++======++++ ++======++++ ++++==**==++xx
+
+
+
diff --git a/src/build/framegen/frames/frame_105.txt b/src/build/framegen/frames/frame_105.txt
new file mode 100644
index 0000000000..3a119586e9
--- /dev/null
+++ b/src/build/framegen/frames/frame_105.txt
@@ -0,0 +1,41 @@
+
+ xx++============++xx
+ ++++**%%**==xo~~~~ox==**%%**+++x
+ xx==**== ==**==xx
+ ++**+= ·~~~~· =+**++
+ ===+ x*$@@@@@@@@@@@@@@@@$*x +===
+ **++ =@@@@@$$$$$$$$$$$$$$$$$$@@@@@= ++**
+ ==++ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ++==
+ ++=+ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ +=++
+ xx== @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xx
+ ==+o *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* o+==
+ ox== $@$$$@@@@@@$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@$ ==xo
+ ++++ $@$$$@$o +$@@@$$$$$$$$$$$$$$@$~ +$@@@$$$$$$$$$$$@$ x+++
+ ==+· +@$$$$$ +$@@@@$$$$$$$$@% +$@@@$$$$$$$$@+ ·+==
+ == @$$$$$$ +$@@$$$$$$$$ +$@@$$$$$$@ ==
+ == $$$$$$$@%o x$$$$$$$@@%o x$$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$$$$$$@@@@x $$$$$$@· ==
+ == ·@$$$$$$@*~ +$$$$$$$@@*~ =$$$$$$@· ==
+ == ·@$$$$$$ ·*@@@$$$$$$$% ~*@@@$$$$$$@· ==
+ == ·@$$$$$$ ·=@@@@$$$$$$$$$@% ~*@@@@$$$$$$$$$@· ==
+ == ·@$$$$$@@+·~=@@@@$$$$$$$$$$$$$$@$x·~=@@@@$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@$$$$$$$$$@@@@$@@@@@$$$$$$$$@@@@$$@@@@$$$$$$$$$@@$ ·+==
+ ++== =$@@@@@@@$= x%@@@@@@@@%~ *@@@@@@@@$+ ==+x
+ ==++ ++==
+ ===+ ==*%+x ++%%=+ +===
+ ++**=%==++++*=**==++**=*==++++***=**++******++++==%=**++
+ ++======++++ ++======++++ ++++======++
+
+
+
diff --git a/src/build/framegen/frames/frame_106.txt b/src/build/framegen/frames/frame_106.txt
new file mode 100644
index 0000000000..d472c438fa
--- /dev/null
+++ b/src/build/framegen/frames/frame_106.txt
@@ -0,0 +1,41 @@
+
+ ++++++====++++++
+ xx+=***%%%**==++++==**%%%***=+xx
+ ox==**=*x ox*=**==xo
+ ++**+= =+**++
+ ==== ~+%$@@@@@@@@@@@@@@$%+~ ====
+ ===+ x%@@@@@$$$$$$$$$$$$$$$$@@@@@%x +===
+ ===+ +@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ +===
+ ++=+ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% +=++
+ ox== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ==xx
+ ==+x x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x x+==
+ xx== %@$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@% ==xo
+ ++++ %@$$$@@+~o=@@@@$$$$$$$$$$$$$$@@+~o*@@@@$$$$$$$$$$$@% ++++
+ ==+· x@$$$$$ ~*@@@@$$$$$$$$$$% o*@@@@$$$$$$$$@x ·+==
+ == @$$$$$$ ~*@@@$$$$$$$% o%@@@$$$$$$@ ==
+ == $$$$$$$@=· =$$$$$$$@@= *$$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$$$$$$@@@@x $$$$$$@· ==
+ == ·@$$$$$$@$x o$$$$$$$@@$x x$$$$$$@· ==
+ == ·@$$$$$$ x$@@$$$$$$$$ +$@@$$$$$$@· ==
+ == ·@$$$$@$ x%@@@@$$$$$$$$@% x$@@@@$$$$$$$$@· ==
+ == ·@$$$$$@$~ x%@@@@$$$$$$$$$$$$$@%· x%@@@@$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ·+==
+ x+== +$@@@@@@@$+ o%@@@@@@@@*~ =$@@@@@@@$+ ==xx
+ ===+ +===
+ ==== ==%%++ ++%%== ====
+ ++**=***++==%***==++***%**++++*=%**=++==***%==++**%=**++
+ x+++==++++ x+++++==++xx ++++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_107.txt b/src/build/framegen/frames/frame_107.txt
new file mode 100644
index 0000000000..0d5034d4fc
--- /dev/null
+++ b/src/build/framegen/frames/frame_107.txt
@@ -0,0 +1,41 @@
+
+ ++++++++++++++++
+ ++*****%*%=******=%*%*****++
+ ++**=*+x x+*=**++ x
+ ++**+= =+**++
+ ====ox o=%@@@@@@@@@@@@@@%=o xo====
+ ===+ ~*@@@@@@$$$$$$$$$$$$$$@@@@@@*~ +===
+ ===+ o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o +===
+ ++== *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ==++
+ ox== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==xo
+ ==+x o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o x+==
+ ox== =@$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@= ==xo
+ ++++ *@$$$@@*x+%@@@@$$$$$$$$$$$$$$@@*x+%@@@@$$$$$$$$$$$@* ++++
+ ==x· ~@$$$$$ x%@@@@$$$$$$$$$$% x$@@@@$$$$$$$$@~ ·x==
+ == @$$$$$$ x$@@@$$$$$$$% +$@@@$$$$$$@ ==
+ == $$$$$$@@+ %@$$$$$$@@x %$$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$$$$$$@@@@x @$$$$$@· ==
+ == ·@$$$$$$@$=· $$$$$$$$@$=· ~$$$$$$@· ==
+ == ·@$$$$$$ ~*@@$$$$$$$$ o%@@$$$$$$@· ==
+ == ·@$$$$@% ~*@@@@$$$$$$$$@% ~*@@@@$$$$$$$$@· ==
+ == ·@$$$$$@* ·=@@@@$$$$$$$$$$$$$@* ~*@@@@$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@@$$$$$$$@@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ·+==
+ xx== x%@@@@@@@$+ ~%@@@@@@@@*~ =$@@@@@@@$+ ==xx
+ ===+ +===
+ ====x ==%%++ ++%%== ====
+ ++***%**====%***++++==*%**====%***==++++***%==+=***=**++
+ xx++++++++ ++==++++ ++++++++xx
+
+
+
diff --git a/src/build/framegen/frames/frame_108.txt b/src/build/framegen/frames/frame_108.txt
new file mode 100644
index 0000000000..0b07a3b320
--- /dev/null
+++ b/src/build/framegen/frames/frame_108.txt
@@ -0,0 +1,41 @@
+
+ xx++++++++++++xx
+ ++==***%*%*%%%%%%*%*%***==++
+ ++**=*== ==*=**++
+ ++**=*++ ++*=**++
+ ++**++ ~+*%@@@@@@@@@@%*+~ ++**++
+ ==== o*@@@@@@@$$$$$$$$$$@@@@@@@*o ====
+ ==== =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ====
+ ++== x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x ==++
+ xx==x· x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ·x==xx
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ ox== o@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$@o ==xo
+ ++++ x@$$$$@$%%@@@@$$$$$$$$$$$$$$$$@$%%@@@@$$$$$$$$$$$$@x ++++
+ ==+~ @$$$$$ o%@@@@$$$$$$$$$$$$ o%@@@@$$$$$$$$$@ ~+==
+ == $$$$$@$ o%@@@$$$$$$$@% x%@@@$$$$$$$$ ==
+ == @$$$$$@% x@$$$$$$$@* x@$$$$$$@ ==
+ == ·@$$$$$$$@@@*· $$$$$$$$@@@@* $$$$$$@· ==
+ == ·@$$$$$$@@$+· $$$$$$$$@@$+ $$$$$$@· ==
+ == ·@$$$$$$x o%@$$$$$$$$o x$@$$$$$$@· ==
+ == ·@$$$$@% o%@@@@$$$$$$$@% x%@@@@$$$$$$$@· ==
+ == ·@$$$$$$x o*@@@@$$$$$$$$$$$$$o o%@@@@$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@@$$$$$$$@@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ·+==
+ ox== x%@@@@@@@$+ ~*@@@@@@@@*~ =$@@@@@@@$+ ==+x
+ ++=+ +=++
+ ++==ox ==%%++ ++%%== o==++
+ x+***%**====%***++++==***%====%***==++++***%====**%***++
+ +++++++x ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_109.txt b/src/build/framegen/frames/frame_109.txt
new file mode 100644
index 0000000000..c187dd596a
--- /dev/null
+++ b/src/build/framegen/frames/frame_109.txt
@@ -0,0 +1,41 @@
+
+ xx++++++++++++xx
+ ++==****%%*%%%%%%*%%****==++
+ ++****=*++ ++*=****++
+ ++=*==+= =+====++
+ ++**++ ~+=%$$@@@@$$%=+~ ++**++
+ ++==oo +%@@@@@@@@$$$$$$@@@@@@@@%+ oo==++
+ ++== o$@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@$o ==++
+ ++==~ %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% ~==++
+ ox==+~ $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ ~+==xo
+ ++++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ++++ ~@$$$$@@@@@@@$$$$$$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$$@~ ++++
+ ==+~ @$$$$$~ =@@@@$$$$$$$$$$$$$· ·=@@@@$$$$$$$$$$@ ~+==
+ == $$$$$@% ·=$@@@$$$$$$$@% ·*@@@@$$$$$$$$ ==
+ == @$$$$$$= ·*@$$$$$$$$+ ~*@$$$$$$@ ==
+ == $$$$$$$@@@*x $$$$$$$$@@@*o $$$$$$$ ==
+ == ·@$$$$$$@@@$+ $$$$$$$$@@@%x $$$$$$@· ==
+ == ·@$$$$$@* =@$$$$$$$@= =@$$$$$$@· ==
+ == ·@$$$$@$ +$@@@$$$$$$$@% +$@@@$$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$ +$@@@$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@$$@@@@$$$$$$$$$$$$$$$$@@$$@@@$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@$$$$$$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ·+==
+ xx== +%@@@@@@@$= ~%@@@@@@@@%~ =$@@@@@@@$+ ==xx
+ ++=+ +=++
+ ++==ox ==%%++ ++%%== o==++
+ x+***%**====%***++++==***%====%***==++++***%====**%***++
+ ++++++++ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_110.txt b/src/build/framegen/frames/frame_110.txt
new file mode 100644
index 0000000000..0ecf74c83f
--- /dev/null
+++ b/src/build/framegen/frames/frame_110.txt
@@ -0,0 +1,41 @@
+
+ ++++++++++++
+ ++++==******%%%*******==++++
+ ++==**=***++ ++******==++
+ ++==**=*+o o+*=**==++
+ ++====x ·o++====++o· x====++
+ ++==++ +%@@@@@@@@@@@@@@@@@@@@%+ ++==++
+ ++==xo +$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$+ ox==++
+ ++==+~ x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x ~+==++
+ ==+x =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= x+==
+ ++== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==++
+ ==x· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·x==
+ ++== @$$$$@@@@@@$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$$@ ==++
+ +++x @$$$$$* ·=@@@@$$$$$$$$$$$$$$= ~=@@@@$$$$$$$$$$$@ x+++
+ == *$$$$@$ ·*@@@@$$$$$$$$@% ~*@@@@$$$$$$$$* ==
+ == @$$$$$$ ~*@@$$$$$$$$ ~*@@$$$$$$@ ==
+ == $$$$$$$@@=~ $$$$$$$$@@=· ·$$$$$$$ ==
+ == ~@$$$$$$$@@@@x $$$$$$$$$@@@@o $$$$$$@~ ==
+ == ·@$$$$$@@x %$$$$$$$@@x %$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$% +$@@@$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$% +$@@@$$$$$$$$$$@· ==
+ == ·@$$$$$@@*++%@@@@$$$$$$$$$$$$$$@@*+=%@@@@$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@@$$$$$$$$@@@@$$@@@@$$$$$$$$@@@@$$@@@@$$$$$$$$$@@$ ·+==
+ xx== +$@@@@@@@$= o%@@@@@@@@%o =$@@@@@@@$= ==xx
+ ++=+ +=++
+ ++== ==%%++ ++%%== ==++
+ x+===***++==%***++x+==*%**====**%*==xx++***%==++***=**++
+ ++++++++ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_111.txt b/src/build/framegen/frames/frame_111.txt
new file mode 100644
index 0000000000..f9eafa6a49
--- /dev/null
+++ b/src/build/framegen/frames/frame_111.txt
@@ -0,0 +1,41 @@
+
+ ++xxxx++
+ xx++==****************==++xx
+ x+==**==*%=*++oo oo++==%*==**==++
+ x+++**=*+= =+*=**+++x
+ ++====++ ~oooo~ ++====++
+ ++===+ +%@@@@@@@@@@@@@@@@@@%+ +===++
+ ++==+x ·*@@@@@$$$$$$$$$$$$$$$$$$@@@@@*· x+==++
+ x+==+x %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% x+==+x
+ ==++ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o +===
+ ++== ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ==++
+ ==+~ *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ~+==
+ ++== $@$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@$ ==++
+ +++x $$$$$@@x ·+$@@@$$$$$$$$$$$$$$@$o ~=@@@@$$$$$$$$$$$$$ x+++
+ ==x· =@$$$$$ =$@@@$$$$$$$$$@% =$@@@$$$$$$$$@= ·+==
+ == @$$$$$$ =$@@$$$$$$$% ·=@@@$$$$$$@ ==
+ == $$$$$$$@%o +$$$$$$$@@*~ +$$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$$$$$$@@@@+ $$$$$$@· ==
+ == ·@$$$$$$@%o x$$$$$$$@@%~ +$$$$$$@· ==
+ == ·@$$$$$$ =@@@$$$$$$$$ ·=@@@$$$$$$@· ==
+ == ·@$$$$$$ =$@@@$$$$$$$$$$% =$@@@$$$$$$$$$@· ==
+ == ·@$$$$$@$x ·+$@@@$$$$$$$$$$$$$$@$o ·=$@@@$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· $@@$$$$$$$$$@@@@$@@@@@$$$$$$$$@@@@@$@@@@$$$$$$$$$@@$ ·+==
+ xx== =$@@@@@@@@* o%@@@@@@@@%o *@@@@@@@@$= ==xx
+ ++=+ +=++
+ ++== ==%%++ ++%%== ==++
+ x+**=%*=++++%***=+++==*%**++++**%*==+++=***%++++==%=**++
+ ++++++++ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_112.txt b/src/build/framegen/frames/frame_112.txt
new file mode 100644
index 0000000000..75ce50d77e
--- /dev/null
+++ b/src/build/framegen/frames/frame_112.txt
@@ -0,0 +1,41 @@
+
+
+ xx++====********====++xx
+ ++==**=**%****++++****%**=**==++
+ ++=====*+x ++*=====++
+ ++====== ======++
+ ++====xx o=%$@@@@@@@@@@@@$%=o xx====++
+ ++===+ ·*$@@@@@$$$$$$$$$$$$$$@@@@@$*· +===++
+ xx===+ o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o +===xx
+ ==++ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ++==
+ ++== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==++
+ ==+x ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ x+==
+ xx== =@$$$$$@@$$$$$$$$$$$$$$$$$$$$$@@@$$$$$$$$$$$$$$$@= ==xx
+ ++++ *@$$$$@$*%@@@@$$$$$$$$$$$$$$$@@$*%@@@@$$$$$$$$$$$$@* ++++
+ ==+· ~@$$$$$ ~*@@@@$$$$$$$$$$$$ ~*@@@@$$$$$$$$$@~ ·+==
+ == @$$$$$$ ~*@@@$$$$$$$@% o*@@@$$$$$$$@ ==
+ == $$$$$$@%~ o$$$$$$$$@%· x$$$$$$$$ ==
+ == ·@$$$$$$$@@@%~ $$$$$$$$$@@@%· $$$$$$@· ==
+ == ·@$$$$$$@@%+ $$$$$$$$@@%x $$$$$$@· ==
+ == ·@$$$$$$o x$@$$$$$$$$~ +$@$$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$@% +$@@@$$$$$$$$@· ==
+ == ·@$$$$$$+ x%@@@@$$$$$$$$$$$$$x x%@@@@$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· $@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·x==
+ xx== *@@@@@@@@@%~ +$@@@@@@@@$+ ~%@@@@@@@@@* ==xx
+ ==++ ++==
+ ===+ ++%% %%++ +===
+ ++**=%+++x++**%*=+++===*==++++==*===++++*%**++++++****++
+ ++++====++ ++====++ ++====++++
+
+
+
diff --git a/src/build/framegen/frames/frame_113.txt b/src/build/framegen/frames/frame_113.txt
new file mode 100644
index 0000000000..ee9846d6fb
--- /dev/null
+++ b/src/build/framegen/frames/frame_113.txt
@@ -0,0 +1,41 @@
+
+
+ xx++++============++++xx
+ ++====**=**%%%****%%%**=**====++
+ ++====**=*o~ ~o*=**====++
+ ======++ ++======
+ x+====++ ~+*%$@@@@@@@@$%*+~ ++====++
+ xx==== o%@@@@@@@$$$$$$$$$$@@@@@@@%o ====xx
+ ==== =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ====
+ =+== o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o ==++
+ x+==x· x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ·x==+x
+ ==++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++==
+ xx== o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==xx
+ ++++ x@$$$$@@@@@@@$$$$$$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$$@x ++++
+ ==+~ @$$$$$o ·*@@@@$$$$$$$$$$$$$~ ~*@@@@$$$$$$$$$$@ ~+==
+ == $$$$$@% ~*@@@@$$$$$$$@* ~*@@@@$$$$$$$$ ==
+ == @$$$$$$+ ~*@$$$$$$$$x o*@$$$$$$@ ==
+ == ·@$$$$$$@@@*o $$$$$$$$@@$*~ @$$$$$@· ==
+ == ·@$$$$$$@@@$+ $$$$$$$$@@@$+ @$$$$$@· ==
+ == ·@$$$$$@* +@$$$$$$$@* =@$$$$$$@· ==
+ == ·@$$$$@% +$@@@$$$$$$$@% +$@@@$$$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$$$ +$@@@$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@$$@@@@$$$$$$$$$$$$$$$$@@$$@@@@$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@$$$$$$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·x==
+ xx++ ·%@@@@@@@@@%o =@@@@@@@@@@= o%@@@@@@@@@%~ ++++
+ ==+x ·~~ ·~~· ~~~ x+==
+ ==++ x+%% %%++ +===
+ ++**=%++ooox==*===++**=%++xoox++%=**++===*==xooo++%=**++
+ ++++==++++ x+++====++xx ++++====++
+
+
+
diff --git a/src/build/framegen/frames/frame_114.txt b/src/build/framegen/frames/frame_114.txt
new file mode 100644
index 0000000000..396d5a292c
--- /dev/null
+++ b/src/build/framegen/frames/frame_114.txt
@@ -0,0 +1,41 @@
+
+
+ xx++++++++++++++++++
+ ++++==****=***%%%%***=****==++++
+ ++++=====%==o~ ~o==%=====++++
+ ++====+= =+====++
+ ====++ ~x=**%%%%**=x~ +=====
+ ====++ o*$@@@@@@@@@@@@@@@@@@@@$*o ++====
+ ====o~ ·*@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@*· ~o====
+ ++==x· =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ·x==++
+ xx==+o %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% o+==xx
+ =+++ *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* +++=
+ xx==x· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==xo
+ ++++ @$$$$@@@@@@$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$$@ ++++
+ ==+o @$$$$@$~ x%@@@@$$$$$$$$$$$$$@%· x%@@@@$$$$$$$$$$$@ o+==
+ == %$$$$$$ x%@@@@$$$$$$$$@% x%@@@@$$$$$$$$% ==
+ == @$$$$$$ x%@@$$$$$$$$ +$@@$$$$$$@ ==
+ == $$$$$$$@%+ o$$$$$$$@@%x x$$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $@$$$$$$$@@@@+ $$$$$$@~ ==
+ == ·@$$$$$@@=· =$$$$$$$@@= *$$$$$$@· ==
+ == ·@$$$$$$ ~*@@@$$$$$$$% o*@@@$$$$$$@· ==
+ == ·@$$$$$$ ~*@@@@$$$$$$$$$$% ~*@@@@$$$$$$$$$@· ==
+ == ·@$$$$$@@+~o*@@@@$$$$$$$$$$$$$$@@+~o*@@@@$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x ·@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@~ ==
+ x+++ x$@@@@@@@@@@= o%@@@@@@@@@@%o =@@@@@@@@@@$x +=++
+ ==+o ox++o ~x++x~ o++xo o+==
+ **+x ** ** x+**
+ ++**==o~ ++*+**++**+*xo ox*+**++**=*++ ~o=+**=+
+ ++==***=++++ ++==****==++ x+++****==++
+
+
+
diff --git a/src/build/framegen/frames/frame_115.txt b/src/build/framegen/frames/frame_115.txt
new file mode 100644
index 0000000000..f5d198a488
--- /dev/null
+++ b/src/build/framegen/frames/frame_115.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++++++++++++
+ ++++==****************==++++
+ ++===****%++~~ ~~++%****===++
+ ++=====*+x x+*=====++
+ ++====xx ·ox+====+xo· xx====++
+ ====++ x%$@@@@@@@@@@@@@@@@@@$%x ++====
+ ++==+x x$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$x x+==++
+ ++==+o x$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$x o+==++
+ ox==++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++==xo
+ +++= x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x =+++
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==xo
+ ++== @@$$$$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@ ==++
+ ==+x @$$$$$@*x+%@@@@$$$$$$$$$$$$$$@@*x+%@@@@$$$$$$$$$$$$@ x+==
+ == *$$$$$$ x%@@@@$$$$$$$$$$% x%@@@@$$$$$$$$$* ==
+ == @$$$$$$ x%@@@$$$$$$@% +$@@@$$$$$$@ ==
+ == $$$$$$@@+ *$$$$$$$@@x %$$$$$$$ ==
+ == ~@$$$$$$$@@@@x $$$$$$$$$@@@@x $$$$$$@~ ==
+ == ·@$$$$$$@@=· $$$$$$$$@$= ~$$$$$$@· ==
+ == ·@$$$$$$ ~*@@$$$$$$$$ o*@@$$$$$$@· ==
+ == ·@$$$$@$ ~*@@@@$$$$$$$$@% ~*@@@@$$$$$$$$@· ==
+ == ·@$$$$$@* ~*@@@@$$$$$$$$$$$$$@* ~*@@@@$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@x ==
+ ++++ =@@@@@@@@@@@%o +$@@@@@@@@@@$+ o%@@@@@@@@@@@* ++++
+ **x~ ~+===+o x====x o+=*=+~ ~+**
+ **+x ++ ++ x+**
+ ==**++ ~o*+**=***+= =+**==**+*o~ ++====
+ +==****==+++ ++=******=++ ++===*%**==+
+
+
+
diff --git a/src/build/framegen/frames/frame_116.txt b/src/build/framegen/frames/frame_116.txt
new file mode 100644
index 0000000000..0d845be93e
--- /dev/null
+++ b/src/build/framegen/frames/frame_116.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++++
+ ++++==************==++++
+ ++++****=**%=+xo~~~~ox+=***=****++++
+ xx++===*== =+*===++xx
+ ++====++ ·~~~~· ++====++
+ ++===+ x*$@@@@@@@@@@@@@@@@$*x +===++
+ ++==++ =@@@@@$$$$$$$$$$$$$$$$$$@@@@@= ++==++
+ x+===+ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* +===+x
+ ==++ ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ ++==
+ ++== @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==++
+ ==+o *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* o+==
+ ++== $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==++
+ +++x $@$$$$@@@@@@@$$$$$$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$$@$ x+++
+ ==x· +@$$$$$x o%@@@@$$$$$$$$$$$$$o x%@@@@$$$$$$$$$$@+ ·x==
+ == @$$$$@% o%@@@@$$$$$$$@* x%@@@@$$$$$$$@ ==
+ == $$$$$$$o o%@$$$$$$$$~ x%@$$$$$$$ ==
+ == ·@$$$$$$@@$+ $$$$$$$$@@%+ $$$$$$@· ==
+ == ·@$$$$$$@@@@* $$$$$$$$@@@$* $$$$$$@· ==
+ == ·@$$$$$@* x@$$$$$$$@* x@$$$$$$@· ==
+ == ·@$$$$@% o%@@@$$$$$$$@* x%@@@$$$$$$$@· ==
+ == ·@$$$$$$ o%@@@@$$$$$$$$$$$$ x%@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$@@%$@@@@$$$$$$$$$$$$$$$$@@%$@@@@$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == =@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@* ==
+ +++x $@@@@@$$@@@@@*x~o+%@@@@@$$@@@@@%+o~o=$@@@@$$$@@@@$· x+++
+ **~· ~=%$$$%=o +*$$$$*+ o=%$$$%=~ **
+ xx**o~ ~·**xx
+ ox**+* ++****=*+x x+*=****++ *+**xx
+ ++==*%%%%%**== x++=*%%%%%%*=++x ++**%%%%%*==++
+
+
+
diff --git a/src/build/framegen/frames/frame_117.txt b/src/build/framegen/frames/frame_117.txt
new file mode 100644
index 0000000000..591cfaea9c
--- /dev/null
+++ b/src/build/framegen/frames/frame_117.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++====********====++++
+ ++==**=**%**++++++++**%**=**==++
+ ++=*=*=* *=*=*=++
+ x+====+= =+====+x
+ ++===+ ·+%$@@@@@@@@@@@@@@$%+· +===++
+ x+==++ x%@@@@@$$$$$$$$$$$$$$$$@@@@@%x ++==+x
+ ox==++ +@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ ++==xx
+ ==++ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ++==
+ ++== $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ ==++
+ ==+x +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ x+==
+ ++== %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==++
+ ++++ %@$$$$$@@@$$$$$$$$$$$$$$$$$$$$$@@@$$$$$$$$$$$$$$$$@% ++++
+ ==+· x@$$$$@@%=*@@@@@$$$$$$$$$$$$$$@@%**@@@@$$$$$$$$$$$$$@x ·+==
+ == @$$$$$% +$@@@@$$$$$$$$$$% +$@@@@$$$$$$$$$@ ==
+ == $$$$$@* o*@@@$$$$$$@+ o*@@@$$$$$$$ ==
+ == ·@$$$$$@%· x$$$$$$$@* x$$$$$$@· ==
+ == ·@$$$$$$@@@@* *@$$$$$$@@@@= %@$$$$@· ==
+ == ·@$$$$$@$o ~$$$$$$$@%~ o$$$$$$@· ==
+ == ·@$$$$@* ·=$@@$$$$$$@= ·=$@@$$$$$$@· ==
+ == ·@$$$$$% o%@@@@@$$$$$$$$$* x%@@@@$$$$$$$$$@· ==
+ == ·@$$$$$@@*+=$@@@@$$$$$$$$$$$$$$@@*+=$@@@@$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$@@@$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++o o@@@@$$$$$@@@@%*+=*$@@@$$$$$$@@@$*=+=%@@@@$$$$$@@@@x o+++
+ ** +%@@@@@%+ o*$@@@@$*o +%@@@@@%+ **xo
+ x+** **++
+ x+**++ ox****== ==****xo =+**++
+ +=**%%%%%%%*==++ ++==*%%%%%%*==++ x+==*%%%%%%%**++
+
+
+
diff --git a/src/build/framegen/frames/frame_118.txt b/src/build/framegen/frames/frame_118.txt
new file mode 100644
index 0000000000..436dfbe127
--- /dev/null
+++ b/src/build/framegen/frames/frame_118.txt
@@ -0,0 +1,41 @@
+
+
+
+ x+++++========++++++
+ ++==*****%%%********%%%*****==++
+ ++==**=*=+ ++*=**==++
+ ++**+= =+**++
+ xx====ox ~=%$@@@@@@@@@@@@$%=~ xo====xx
+ x+===+ ·=@@@@@@$$$$$$$$$$$$$$@@@@@@=· +===+x
+ ===+ ~%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%~ +===
+ ++== =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ==++
+ x+== =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ==+x
+ ==+x ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ x+==
+ xx== =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xx
+ ++++ =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ++++
+ ==+· ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ·+==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == $$$$$$@@@@@@@@@@@@@@@@@$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == ·@$$$$@$+xxxxxxxxxxxxxx=$@$$$@$+xxxxxxxxxxxxxx+%@$$$$$@· ==
+ == ·@$$$@o x$$$x @$$$$@· ==
+ == ·@$$$@~ o@$@~ @$$$$@· ==
+ == ·@$$$$@%o··············o%@$$$@%o··············~*@$$$$$@· ==
+ == ·@$$$$$@@@@@@@@@@@@@@@@@@$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ·+==
+ xx== +$@@@@@@@$+ ~*@@@@@@@@*~ +$@@@@@@@$+ ==xx
+ ++=+ +=++
+ ++**oo ==$%++ ++%%== oo==++
+ x+==*%**====****++++++*%**====**%*++++++**=*====*=%*==++
+ ++++++ ++++++++ ++++++
+
+
diff --git a/src/build/framegen/frames/frame_119.txt b/src/build/framegen/frames/frame_119.txt
new file mode 100644
index 0000000000..a334097c43
--- /dev/null
+++ b/src/build/framegen/frames/frame_119.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++====++++++
+ xx++*******%*%%**%%*%*******++xx
+ xx+=****== ==****=+xx
+ ++**+*x x*+**++
+ ====x+ x*%$@@@@@@@@@@$%*x +x====
+ ==== +$@@@@@@$$$$$$$$$$$$@@@@@@$+ ====
+ ==++ *@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* ++==
+ ++== +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ==++
+ xx==x· +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·o==xx
+ ==++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++==
+ ox== +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ ++++ +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ++++
+ ==+· @$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$@ ·+==
+ == @$$$$$$$@@@@$%==**$@@@@$$$$$$$$@@@@@%*==*%@@@@$$$$$$$@ ==
+ == @$$$$$@@= =@@$$$$$@%o o$@$$$$$@ ==
+ == ·@$$$$@* + *$$$$@ xo o@$$$$@· ==
+ == ·@$$$$* ·x * $$$@ * = =@$$$@· ==
+ == ·@$$$@x @ * x $$$$ x+ % * +@$$$@· ==
+ == ·@$$$$$ + *$$$@ x o@$$$$@· ==
+ == ·@$$$$@$ ·· o+=$@@$$$$@x ~ ·++%@@$$$$$@· ==
+ == ·@$$$$$$@@= =@@@$$$$$$$@@*o @@@$$$$$$$@· ==
+ == ·@$$$$$$$$@@@@@@@@@@@$$$$$$$$$$$$@@@@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· $@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·x==
+ xx== *@@@@@@@@@%o =@@@@@@@@@@= o%@@@@@@@@@%· +++x
+ ==++ ·~· ~~ ·~· x+==
+ ==++ x+%% %%+x ++==
+ ++**=*++xox+==%*==++===%==xoox==%===++==*%==+xoo++*=**++
+ +++++===++ +++==+++ +++===++++
+
+
diff --git a/src/build/framegen/frames/frame_120.txt b/src/build/framegen/frames/frame_120.txt
new file mode 100644
index 0000000000..8bd15cf1df
--- /dev/null
+++ b/src/build/framegen/frames/frame_120.txt
@@ -0,0 +1,41 @@
+
+
+
+ x++++++++++++++x
+ ++==*****%%%%%%%%%%*****==++
+ ++**=*=* *=*=**++
+ xx**=*++ ++*=**++
+ ++**x+ ~+*$$@@@@@@@@$$*+~ +x**++
+ ==== o%@@@@@@@$$$$$$$$$$@@@@@@@%o ====
+ ++== =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ==++
+ ++== o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o ==++
+ ox==x· x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ·x==xo
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ ++++ x@$$$$$$$$@@@@@@@@@$$$$$$$$$$$$$$@@@@@@@@@@$$$$$$$@x ++++
+ ==+~ @$$$$$$@@@*x· ·x*$@@$$$$$$$$@@@%=~ o=%@@@$$$$$@ ~+==
+ == $$$$$$@@+ x@@$$$$$@* $@$$$$$$ ==
+ == @$$$$@% ~~o=$x $$$$$@ ·~~x%= +@$$$$@ ==
+ == ·@$$$$$ % = @$$@ =· %$$$$@· ==
+ == ·@$$$@x @ o *x $$$$ ~+ x @ o@$$$@· ==
+ == ·@$$$@+ $ x · @$$$ ·+ + ~ *$$$$@· ==
+ == ·@$$$$$ * $$$$@~ x· +@$$$$@· ==
+ == ·@$$$$$$ ·o%*xxx=$@$$$$$@~ ~=%+ox+%@@$$$$$@· ==
+ == ·@$$$$$$@= x@@@$$$$$$$@$· @@@$$$$$$$@· ==
+ == ·@$$$$$$$@@@$=x~··~x%$$$$$$$$$$$@@@$*+o··~o*$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$@@@@@@@@@$$$$$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@x ==
+ ++++ =@@@@@@@@@@@*o ·+$@@@@@@@@@@$+· o*@@@@@@@@@@@* ++++
+ **+~ ~+=*=+o ·x=**=x· o+=*=+~ ·+**
+ **xx ++ ++ o+**xx
+ ====++ *=**==**+= =+**==**+= ++====
+ +==**%*==+++ ++=******=++ +++=**%%*===
+
+
diff --git a/src/build/framegen/frames/frame_121.txt b/src/build/framegen/frames/frame_121.txt
new file mode 100644
index 0000000000..ad287b6288
--- /dev/null
+++ b/src/build/framegen/frames/frame_121.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++++++++
+ ++++****%%%%%%%%%%%%****++++
+ ++***%*%x~ ~x%*%***++
+ x+**==++ ++==**++
+ ++**++ ·x*%$$@@@@@@$$%*x· ++**++
+ ++==~ o*@@@@@@@@$$$$$$$$@@@@@@@@*o ~==++
+ ++== =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ==++
+ ++== ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ ==++
+ ==x· o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ·x==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ x+++ o@$$$$$$$@@@@@@@@@@@$$$$$$$$$$$$@@@@@@@@@@@$$$$$$$@o ++xx
+ ==x~ @$$$$$$@@%x o*@@$$$$$$$$@@$=~ ~=$@@$$$$$@ ~x==
+ == $$$$$$@$· $@$$$$$@+ *@$$$$$$ ==
+ == @$$$$@* ·~·~x$o %$$$$@ o··o%= ~@$$$$@ ==
+ == ·@$$$$$ * = @$$@ % o *@$$$@· ==
+ == ·@$$$@x @ * *+ $$$$ oo * @ o@$$$@· ==
+ == ·@$$$@+ % @$$$ * %$$$$@· ==
+ == ·@$$$$@ + @$$$@x ~o *@$$$$@· ==
+ == ·@$$$$$@ ·x==x=*%@@$$$$$@x o+=x+=%$@@$$$$$@· ==
+ == ·@$$$$$$@%· x@@$$$$$$$$@@x @@$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@%=++++*$$$$$$$$$$$$$@@@$*=+++=%$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == =@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x $@@@@$$$@@@@@=x~o+%@@@@@$$@@@@@%+o~x=@@@@@$$$@@@@$ x+++
+ **~· ~=%$$$%=o x*$$$$*x o=%$$$%=~ **
+ xx**o~ ~~**xx
+ xx**+= ++******+x x+******++ *+**xx
+ ++==*%%%%%%*=+ xx==*%%%%%%*=+xx +=*%%%%%%*==++
+
+
diff --git a/src/build/framegen/frames/frame_122.txt b/src/build/framegen/frames/frame_122.txt
new file mode 100644
index 0000000000..538806586d
--- /dev/null
+++ b/src/build/framegen/frames/frame_122.txt
@@ -0,0 +1,41 @@
+
+
+
+ x++++++x
+ ++==***%%%%%%%%%%***==++
+ x+==**=*+o o+*=**==+x
+ ==**++ ++**==
+ ++**++ x=%$$@@@@@@$$%=x ++**++
+ ++**oo ~=@@@@@@@@$$$$$$$$@@@@@@@@=~ oo**++
+ ++== +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ ==++
+ ++=* ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ *=++
+ ==x~ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==
+ ++++ o@$$$$$$$@@@@@@@@@@@$$$$$$$$$$$$@@@@@@@@@@@$$$$$$$@o +++x
+ ++x~ @$$$$$$@@%x o*@@$$$$$$$$@@$=· ·=$@@$$$$$@ ~x++
+ == $$$$$$@$ $@$$$$$@x =@$$$$$$ ==
+ == @$$$$@= ~· o%~ *$$$$$ ~ ·*+ @$$$$@ ==
+ == ·@$$$$% = * @$$@ $ + =@$$$@· ==
+ == ·@$$$@o @ ~$ *+ $$$$ xo @ @ x@$$$@· ==
+ == ·@$$$@= = @$$@ % $$$$$@· ==
+ == ·@$$$$@ ~· x@$$$@= x · $@$$$$@· ==
+ == ·@$$$$$@x *@@@@$$$$$@% o$@@@$$$$$$@· ==
+ == ·@$$$$$$@@= x@$$$$$$$$$@@%o @$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@@@@$$$$@@$$$$$$$$$$$$@@@@@@$$$@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == $@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@ ==
+ +++o =@@@$$$$$$$@@@@%**$@@@@$$$$$$@@@@$**%@@@@$$$$$$$@@@= ~+++
+ ox== ~*@@@@@@@*o +$@@@@@@$+ o*@@@@@@@*~ ==xo
+ ++== ==++
+ ++**++ =*%%== ==%%*= +x**++
+ +=***%%%%*%=**+x ++***%%%%%%***++ x+**=%*%%%%***=+
+ xxxx xxxx xxxx
+
diff --git a/src/build/framegen/frames/frame_123.txt b/src/build/framegen/frames/frame_123.txt
new file mode 100644
index 0000000000..db5c78f458
--- /dev/null
+++ b/src/build/framegen/frames/frame_123.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++
+ ++==***%*%%%%%%*%***==++
+ xx==***%+x x+%***==xx
+ ==**++ ++**==
+ ++**++ o=*%$$@@@@$$%*=o ++**++
+ ++**oo ·=$@@@@@@@$$$$$$$$@@@@@@@$=· oo**++
+ ++== +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ ==++
+ x+=* ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· *=xx
+ ==+~ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==
+ xx++ ~@$$$$$$$@@@@@@@@@@@$$$$$$$$$$$$@@@@@@@@@@@@$$$$$$@o ++xx
+ +++~ @$$$$$@@@*o ~*@@@$$$$$$$@@%x +%@@$$$$$@ ~+++
+ == $$$$$$@% %@$$$$@@o +@$$$$$$ ==
+ == @$$$$@+ o ~*~ =@$$$$ ·~ =+ @$$$$@ ==
+ == ·$$$$$% ·= * @$$@ $ + =@$$$$· ==
+ == ·@$$$@o @ ~@ *x $$$$ x~ @ @ x@$$$@· ==
+ == ·@$$$@* +~ ·@$$@ % $$$$$@· ==
+ == ·@$$$$@ ~ · =@$$$$* x o ~@@$$$$@· ==
+ == ·@$$$$$@+ *@@@@$$$$$@$ ~@@@@$$$$$$@· ==
+ == ·@$$$$$$@@%o +$$$$$$$$$$@@$+ $$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@@@@@@@@@@$$$$$$$$$$$$$@@@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@$$$$$$$$$@@@@$$@@@@$$$$$$$$@@@@$$@@@@$$$$$$$$$@@% ·+==
+ xx== +$@@@@@@@@= o%@@@@@@@@%o =@@@@@@@@$+ ==+x
+ ++=+ +=++
+ ++*=o +=%$++ ++$%=+ o==++
+ x+==**=*===*%***++xx++*%*%====%*%*++xx++***%*===***===xx
+ ++++++xx ++++++++ xx++++++
+
diff --git a/src/build/framegen/frames/frame_124.txt b/src/build/framegen/frames/frame_124.txt
new file mode 100644
index 0000000000..d7a2e4e1a3
--- /dev/null
+++ b/src/build/framegen/frames/frame_124.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++++***%%$%%%%$%%***++++
+ ++***%+x x+%***++
+ ++**++ ++**++
+ x+**++ o=*%$$@@@@$$%*=o ++**+x
+ ++**oo ·=$@@@@@@@$$$$$$$$@@@@@@@$=· oo**++
+ ++** +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ **++
+ x+** ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· **xx
+ ==+~ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==
+ xx++ ~@$$$$$$$@@@@@@@@@@@@$$$$$$$$$$$@@@@@@@@@@@@$$$$$$@~ ++xx
+ +++~ @$$$$$@@$= +$@@$$$$$$$@@*o o*@@$$$$$@ ~+++
+ == $$$$$$@* *@$$$$@@· x@$$$$$$ ==
+ == @$$$$@o o = +@$$$% o· xx @$$$$@ ==
+ == ·$$$$$% ~+ * @$$@ $ * +@$$$$· ==
+ == ·@$$$@o @ o@ *x $$$$ x~ @ @ x@$$$@· ==
+ == ·@$$$@* xo o@$$@ $ $$$$$@· ==
+ == ·@$$$$@~ ~ ~o *@$$$$% o + x@$$$$$@· ==
+ == ·@$$$$$@= *@@@@$$$$$@$ ~@@@@$$$$$$@· ==
+ == ·@$$$$$$@@$x +$$$$$$$$$$$@@=· $$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@@@@@@@@@@$$$$$$$$$$$$$@@@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·+==
+ x+++ ~%@@@@@@@@@$x =@@@@@@@@@@= x$@@@@@@@@@%~ ++++
+ ===x ~~~ ·~~· ~~~ x+==
+ ===+ x+%% %%+x +===
+ ++**=%+x··~o==*===++***%=+~··~+=%***++===*==o~··x+%=**++
+ ++=====+++ xx++=====+xx +=======++
+
diff --git a/src/build/framegen/frames/frame_125.txt b/src/build/framegen/frames/frame_125.txt
new file mode 100644
index 0000000000..4a14a3f48f
--- /dev/null
+++ b/src/build/framegen/frames/frame_125.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++==***%%%%%%%%%%***==++
+ +=***%+o o+%***=+
+ +=**++ ++**=+
+ xx**++ o=%%$$@@@@$$%%=o ++**xx
+ x+**oo ~=$@@@@@@@$$$$$$$$@@@@@@@$=~ oo**+x
+ ++** +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ **++
+ xx** ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· **xx
+ ==+~ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==
+ xx++ o@$$$$$$@@@@@@@@@@@@@$$$$$$$$$$$@@@@@@@@@@@@$$$$$$@o ++xx
+ +++~ @$$$$$@@$+ x%@@$$$$$$$@@*· ~*@@$$$$$@ ~+++
+ == $$$$$$@* *@$$$$@$ ~@$$$$$$ ==
+ == @$$$$@o o + x@$$$% x o~ @$$$$@ ==
+ == ·$$$$@* ox * $$$@ $ * +@$$$$· ==
+ == ·@$$$@o @ o@ *o $$$$ x~ @ @ x@$$$@· ==
+ == ·@$$$@* ox o@$$@ $ $$$$$@· ==
+ == ·@$$$$@o ~ ox %@$$$$% ~ = +@$$$$$@· ==
+ == ·@$$$$$@* =@@@@$$$$$@@· ·@@@@$$$$$$@· ==
+ == ·@$$$$$$@@$+ +$$$$$$$$$$$@@*o $$$$$$$$$$@· ==
+ == ·@$$$$$$$$$@@@@@@@@@@$$$$$$$$$$$$$@@@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x ·@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@~ ==
+ ++++ x@@@@@@@@@@@= o%@@@@@@@@@@%o =@@@@@@@@@@@x ++xx
+ ==+o ~x+xo ·x++x· ox+xo o+==
+ **++ == =* x+**
+ +=**==o· ++*=**==**=*+o o+*=**==**=*+x ·o=+**=+
+ ++==****++++ ++==****==++ ++++****==++
+
diff --git a/src/build/framegen/frames/frame_126.txt b/src/build/framegen/frames/frame_126.txt
new file mode 100644
index 0000000000..4ed857cd9d
--- /dev/null
+++ b/src/build/framegen/frames/frame_126.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++==**%%%%%%%%%%%%**==++
+ +=***%+o o+%***=+
+ +=**++ ++**=+
+ xx**++ ·x=%$$@@@@@@$$%=x· ++**xx
+ x+**~ ~*@@@@@@@@$$$$$$$$@@@@@@@@*~ ~**+x
+ ++** +@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ **++
+ xx** ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ **xx
+ ==x· o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ·x==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ xx++ o@$$$$$$@@@@@@@@@@@@@$$$$$$$$$$$@@@@@@@@@@@@$$$$$$@o ++xx
+ ++x~ @$$$$$@@%o ~*@@$$$$$$@@$+ =@@$$$$$@ ~x++
+ == $$$$$$@= =@$$$$@$ @$$$$$$ ==
+ == @$$$$@· o o o@$$$* x @$$$$@ ==
+ == ·@$$$@* xo * $$$@ % % +@$$$@· ==
+ == ·@$$$@o @ o@ =o $$$$ x~ @ @ +@$$$@· ==
+ == ·@$$$$% ·+ x@$$@ $ @$$$$@· ==
+ == ·@$$$$@x ~ x+ ~$@$$$$$ ·* =@$$$$$@· ==
+ == ·@$$$$$@% =@@@@$$$$$@@o @@@@$$$$$$@· ==
+ == ·@$$$$$$@@@*~ =$$$$$$$$$$$@@%x ·$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$@@@@@@@@@@$$$$$$$$$$$$$@@@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@x ==
+ ++++ =@@@@@@@@@@@%o ~+@@@@@@@@@@@@+~ o%@@@@@@@@@@@* ++++
+ **x~ ~+***=o ·x=**=x· o=***+o ·x**
+ **+x ++ ++ ox**oo
+ ====++ *=**==**+= =+**==**=* ++====
+ ++=**%%**=++ ++=**%%**=++ ++=**%%*==++xx
+
diff --git a/src/build/framegen/frames/frame_127.txt b/src/build/framegen/frames/frame_127.txt
new file mode 100644
index 0000000000..a6d93b3ea4
--- /dev/null
+++ b/src/build/framegen/frames/frame_127.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ +++=*%%%%%%%%%%%%%%*=+++
+ +=*%=*x· ·x*=%*=+
+ ++**++ ++**++
+ x+**++ ~+*%$@@@@@@@@$%*+~ ++**+x
+ ++** o*@@@@@@@$$$$$$$$$$@@@@@@@*o **++
+ xx** =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= **xx
+ xx** o$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$o **xx
+ =*x· o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ·x*=
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ =* o@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ xx++ x@$$$$$$@@@@@@@@@@@@@$$$$$$$$$$@@@@@@@@@@@@@$$$$$$@x ++xx
+ +++~ @$$$$$@@*~ *@@$$$$$$@@$x x$@$$$$$@ ~+++
+ == $$$$$$@+ +@$$$$@% @$$$$$$ ==
+ == @$$$$@ ·~ ~ ~@$$@* + $$$$$@ ==
+ == ·@$$$@= +~ * $$$@ % $ x@$$$@· ==
+ == ·@$$$@o @ o@ =~ $$$$ xo @ $ +@$$$@· ==
+ == ·@$$$$% + +@$$@ % @$$$$@· ==
+ == ·@$$$$@+ += o$@$$$$$ ~%~ ·*@$$$$$@· ==
+ == ·@$$$$$@$ =@@@$$$$$$$@x @@@@$$$$$$@· ==
+ == ·@$$$$$$$@@%x =$$$$$$$$$$$@@$+· ~$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$@@@@@@@@@@$$$$$$$$$$$$$@@@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@+ ==
+ ++++ *@@@@@@@@@@@$+~ ·o*@@@@@@@@@@@@*o ~+$@@@@@@@@@@@% ++++
+ **x· x*%%%*+ o=%%%%=o +*%%%*x ·o**
+ ox**xo xx xx ~o**xx
+ ===*+x *+****==++ ++==****+* o+*=**
+ ++==**%%%**=++ ++=*%%%%*=++ ++==*%%%%*==++
+
diff --git a/src/build/framegen/frames/frame_128.txt b/src/build/framegen/frames/frame_128.txt
new file mode 100644
index 0000000000..7efbabf4eb
--- /dev/null
+++ b/src/build/framegen/frames/frame_128.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++
+ ++=**%*%%%%%%%%%%*%**=++
+ x+==*%=* *=%*==+x
+ ===*++ ++*===
+ x+**++ o=%$@@@@@@@@@@$%=o ++**+x
+ ++** x%@@@@@@$$$$$$$$$$$$@@@@@@%x **++
+ ++== *@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* ==++
+ x+** x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x **+x
+ **x· +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·x**
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ =* x@$$$$$$$$$$@@@$$$$$$$$$$$$$$$$$$$$@@@@$$$$$$$$$@x *=
+ xx++ +@$$$$$$@@@@$$$$$@@@@$$$$$$$$$$@@@@@$$$$@@@@@$$$$$@+ ++xx
+ +++~ @$$$$$@@= +@@$$$$$$@@%~ ~%@$$$$$@ ~+++
+ == @$$$$$@o ~~ o@$$$$@* ~~ $@$$$$@ ==
+ == @$$$$@ o~ ·@$$@= x $$$$$@ ==
+ == ·@$$$@= * *~ $$$@ % @ x@$$$@· ==
+ == ·@$$$@o @ ~@ + $$$$ xo @ % =@$$$@· ==
+ == ·@$$$$% * =@$$@ % @$$$$@· ==
+ == ·@$$$$@= **· +@@$$$$@ o%o o%@$$$$$@· ==
+ == ·@$$$$$@@~ =@@@$$$$$$$@+ @@@@$$$$$$@· ==
+ == ·@$$$$$$$@@$+~ *$$$$$$$$$$$@@@*o x$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$@@@@@@@@@@$$$$$$$$$$$$$$@@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == =@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x $@@@@@$@@@@@@=o~o+%@@@@@@@@@@@@%xo~o=@@@@@@$@@@@@$ x+++
+ **o· ~=%$$$%=~ x*$$$$*x ~=%$$$%=~ **
+ x+**o~ ~~**xx
+ xx**+= ++****=*+x xx*=****++ ==**+x
+ ++==*%%%%%**=+ ++++*%%%%%%*++++ ==**%%%%%*==++
+
diff --git a/src/build/framegen/frames/frame_129.txt b/src/build/framegen/frames/frame_129.txt
new file mode 100644
index 0000000000..837a8bd579
--- /dev/null
+++ b/src/build/framegen/frames/frame_129.txt
@@ -0,0 +1,41 @@
+
+
+
+ x++++++x
+ +==**%%%%%****%%%%%**==+
+ ++===*+= =+*===++
+ ===* *===
+ x+**x+ ·+*$@@@@@@@@@@@@$*+· +x**++
+ ++== +$@@@@@@$$$$$$$$$$$$@@@@@@$+ =*++
+ ++== ·%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%· =*++
+ ++== =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ==++
+ **o· =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ·o**
+ ++++ ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ++++
+ =* +@$$$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$@+ *=
+ x+++ =@$$$$$$@@@@$%%%$$@@@$$$$$$$$$$@@@@$%%%%$@@@@$$$$$@= +++x
+ +++· ·@$$$$$@$x o$@$$$$$$@@* *@@$$$$@· ·+++
+ == @$$$$$@· ~xx ·@$$$$@= ·ox~ $@$$$$@ ==
+ == @$$$$@ +· ~ @$$@+ x $$$$$@ ==
+ == ·@$$$@+ * *~ $$$@ * @ x@$$$@· ==
+ == ·@$$$@x @ ·$ x @$$$ ox $ = =@$$$@· ==
+ == ·@$$$$$ * *$$$@ * ~@$$$$@· ==
+ == ·@$$$$@* ·*%~ ~*@@$$$$@ +%x· +$@$$$$$@· ==
+ == ·@$$$$$@@x +@@@$$$$$$$@* @@@$$$$$$$@· ==
+ == ·@$$$$$$$@@@*x· ~*$$$$$$$$$$$@@@%+~ +$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$@@@@@@@@@$$$$$$$$$$$$$$@@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x ·$@@@@$$$@@@@@*xoo+$@@@@@$$@@@@@$+oox*@@@@@$$$@@@@@~ x+++
+ ** ~=%$$$$*o +%$$$$%+ o*$$$$%*o **
+ xx**~~ **++
+ ++**+= ++==**==x~ ~x==**==++ =+**+x
+ ++==*%%%%%%*== x+==*%%%%%%*==+x ==**%%%%%*==++
+
diff --git a/src/build/framegen/frames/frame_130.txt b/src/build/framegen/frames/frame_130.txt
new file mode 100644
index 0000000000..cf834c5fd1
--- /dev/null
+++ b/src/build/framegen/frames/frame_130.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++++++++
+ ++++***%%%==++++==%%%***++++
+ ++**=*+o o+*=**++
+ x+**== ==**+x
+ ++** x*$@@@@@@@@@@@@@@$*x **++
+ ++*+ o%@@@@@@$$$$$$$$$$$$$$@@@@@@%o +*++
+ ++=+ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x +=++
+ ++== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ==++
+ ** %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% **
+ +++x x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x x+++
+ == *@$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$@@@@@@@@$$$$$$$@* ==
+ xx++ *@$$$$$@@@@$*+++=%@@@@$$$$$$$$$@@@$*=++=%$@@@$$$$$@* ++xx
+ ==+· o@$$$$$@%· %@$$$$$$@@x +@@$$$$@o ·+==
+ == @$$$$$@ ·o+=o @$$$$@x ~x=x *@$$$$@ ==
+ == $$$$$@ * x @$$@o oo %$$$$$ ==
+ == ·@$$$@+ $ *x $$$$ = @ o@$$$@· ==
+ == ·@$$$@x @ * ~ @$$$ ~+ * x *@$$$@· ==
+ == ·@$$$$$ * $$$$@ + x@$$$$@· ==
+ == ·@$$$$@$ o**xoo+%@@$$$$@· ~=%+oox*@@$$$$$@· ==
+ == ·@$$$$$@@= x@@@$$$$$$$@$~ @@@$$$$$$$@· ==
+ == ·@$$$$$$$@@@$=xo~~o+%$$$$$$$$$$$@@@@*+o~~ox*$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$@@@@@@@@@$$$$$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++x ~@@@@@$$$@@@@@%+xx=$@@@@$$$$@@@@$=xo+*@@@@@$$$@@@@@o x+++
+ ** o*$$@$$%x ·=%$@@$%=· x%$@@$$*x **
+ x+** **+x
+ xx**+= xx****== ==****xx =+**++
+ ++==*%%%%***++xx ++==*%*%%*%*==++ xx++*%*%%%%*==++
+
diff --git a/src/build/framegen/frames/frame_131.txt b/src/build/framegen/frames/frame_131.txt
new file mode 100644
index 0000000000..9b79db3877
--- /dev/null
+++ b/src/build/framegen/frames/frame_131.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++++++++
+ ++==*%*%==++xxxx++==%*%*==++
+ ++**=* *=**++
+ ++**+= =+**++
+ ++== o=%@@@@@@@@@@@@@@@@%=o ==++
+ ===+ +$@@@@@$$$$$$$$$$$$$$$$@@@@@$+ +===
+ ++=+ =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= +=++
+ ++=+ $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ +=++
+ ** $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ **
+ +++o =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= o+++
+ == $@$$$$$$$@@@@@@@@@$$$$$$$$$$$$$$$@@@@@@@@$$$$$$$@$ ==
+ x+++ %@$$$$$@@@$*xo~ox=$@@@$$$$$$$$@@@@%+o~ox+%@@@$$$$$@% ++++
+ ==+· x@$$$$@@* =@$$$$$$@$~ o@@$$$$@x ·+==
+ == @$$$$$$ ·o+%o $$$$$@~ ·~x*+ =@$$$$@ ==
+ == $$$$$$ * + @$$@· +~ %$$$$$ ==
+ == ·@$$$@x @ ~ *x $$$$ ~+ o @ o@$$$@· ==
+ == ·@$$$@+ $ x @$$$ ·= x · *@$$$@· ==
+ == ·@$$$$$ = $$$$@~ o~ =@$$$$@· ==
+ == ·@$$$$$$ o==o++*$@$$$$$@o ~+*xx+=%@@$$$$$@· ==
+ == ·@$$$$$$@%· x@@$$$$$$$$@@o @@@$$$$$$$@· ==
+ == ·@$$$$$$$@@@@%=+xx+*$$$$$$$$$$$$$@@@$*+xx+=%$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++x ~@@@@@$$$$@@@@%+xx=$@@@@$$$$@@@@$=xx+%@@@@$$$$@@@@@o x+++
+ ** x*$$@@$%x ~=%$@@$%=~ x%$@@@$%x **
+ x+** **+x
+ ++**+= xx****== ==****xx =+**++
+ ++==*%%%%*%*=+++ ++==*%*%%*%*==++ x++=***%%%%**=++
+
diff --git a/src/build/framegen/frames/frame_132.txt b/src/build/framegen/frames/frame_132.txt
new file mode 100644
index 0000000000..fbcdcfb5c6
--- /dev/null
+++ b/src/build/framegen/frames/frame_132.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++===***==++++
+ ==***%==xo ox==%***==
+ ===*++ ++*===
+ ++**++ ·oxx++xxo· ++**++
+ ===+ o=$@@@@@@@@@@@@@@@@@@$=o +===
+ **++ o%@@@@@$$$$$$$$$$$$$$$$$$@@@@@%o ++**
+ ==+x ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ x+==
+ ++=+ x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x ++++
+ xx== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xx
+ ==x~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~x==
+ ox== @@$$$$$$$@@@@@@@@@@$$$$$$$$$$$$$@@@@@@@@@@$$$$$$@@ ==xo
+ +++x @$$$$$$@@$*o ~=$@@$$$$$$$$@@@%x ·x%@@@$$$$$@ x+++
+ ==x =$$$$$@@o ~@@$$$$$@* %@$$$$$= +==
+ == @$$$$@* ·~~+%o %$$$$@ ··~x%= o@$$$$@ ==
+ == $$$$$$ * = @$$@ % ~ *@$$$$ ==
+ == ~@$$$@x @ * *+ $$$$ oo * @ o@$$$@~ ==
+ == ·@$$$@= * @$$$ * %$$$$@· ==
+ == ·@$$$$@ x ~@$$$@+ x %@$$$$@· ==
+ == ·@$$$$$@~ oo·=%$@@$$$$$@= ~o·x%$@@@$$$$$@· ==
+ == ·@$$$$$$@@+ x@$$$$$$$$$@@*· @@$$$$$$$$@· ==
+ == ·@$$$$$$$$@@@@$%%%%@@$$$$$$$$$$$$@@@@@$%%%$@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++x ~@@@@@$$$$@@@@%+xx=$@@@@$$$$@@@@$=xx+%@@@@$$$$@@@@@o o+++
+ ** x*$@@@$%x ~=%$@@$%=~ x%$@@@$%x **
+ x+** **+x
+ ++**+= x+****== ==****+x =+**++
+ ++==*%%%%*%*==++ ++==*%*%%%%*==++ xx==*%*%%%%**=++
+
diff --git a/src/build/framegen/frames/frame_133.txt b/src/build/framegen/frames/frame_133.txt
new file mode 100644
index 0000000000..8aee11d549
--- /dev/null
+++ b/src/build/framegen/frames/frame_133.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++==**%%%%***=++++
+ ++==*%**+x x+**%*==++
+ ++**=* *=**++
+ ==== ox=******=xo ====
+ **++ ·=$@@@@@@@@@@@@@@@@@@@@$=· ++**
+ **+o =@@@@@$$$$$$$$$$$$$$$$$$$$@@@@@= o+**
+ **+~ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ~x**
+ +++x *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* x+++
+ xx== +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ==xx
+ ==x· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==
+ ox== @$$$$$$$@@@@@@@@@@@$$$$$$$$$$$$@@@@@@@@@@@@$$$$$$@ ==xo
+ +++o @$$$$$@@@%x o*@@@$$$$$$$@@$+· ·=$@@$$$$$@ o+++
+ == %$$$$$@$ $@$$$$$@x =@$$$$$% ==
+ == @$$$$@+ ~~ ~%~ *@$$$$ ~ ·== @$$$$@ ==
+ == $$$$$% = * @$$@ % + =@$$$$ ==
+ == ~@$$$@o @ ~@ *x $$$$ xo @ @ x@$$$@~ ==
+ == ·@$$$@= =· @$$@ % $$$$$@· ==
+ == ·@$$$$@ ·~ +@$$$$= o ~ $@$$$$@· ==
+ == ·@$$$$$@x *@@@@$$$$$@% o@@@@$$$$$$@· ==
+ == ·@$$$$$$@@*~ +@$$$$$$$$$@@$x @$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@@@@@@@@@@$$$$$$$$$$$$@@@@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++x ~@@@@@$$$@@@@@%+xx=$@@@@$$$$@@@@$=xx+%@@@@$$$$@@@@@o x+++
+ ** o*$$@@$%x ·=%$@@$%=· x%$@@$$*x **
+ x+** **+x
+ ++**+= xx****== ==****xx =+**++
+ ++==*%%%%*%*=+xx ++==*%%%%%%*==++ xx+=*%*%%%%**=++
+
diff --git a/src/build/framegen/frames/frame_134.txt b/src/build/framegen/frames/frame_134.txt
new file mode 100644
index 0000000000..778a14e192
--- /dev/null
+++ b/src/build/framegen/frames/frame_134.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++=**%%%%%%%%%%**=++++
+ +=***%+x x+%***=+
+ ++**+= =+**++
+ ox**+= o=*%$@@@@@@$%*=o ++**xo
+ ++**oo ·=$@@@@@@@$$$$$$$$@@@@@@@$=· oo**+x
+ x+** x$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$x **+x
+ ox** ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· **xo
+ ==+~ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ==
+ xx++ ~@$$$$$$@@@@@@@@@@@@@$$$$$$$$$$$@@@@@@@@@@@@$$$$$$@~ ++xx
+ +++~ @$$$$$@@$x o%@@$$$$$$$@$= =@@$$$$$@ ~+++
+ == $$$$$$@= =@$$$$@$ ~@$$$$$$ ==
+ == @$$$$@~ o x x@$$@% x ~· @$$$$@ ==
+ == ·$$$$@* xo * $$$@ $ % +@$$$$· ==
+ == ·@$$$@o @ o@ =o $$$$ x~ @· @ +@$$$@· ==
+ == ·@$$$@% ~x x@$$@ $ @$$$$@· ==
+ == ·@$$$$@x · xx ·%@$$$$% ~ = =@$$$$$@· ==
+ == ·@$$$$$@% *@@@@$$$$$@@~ @@@@$$$$$$@· ==
+ == ·@$$$$$$@@$=· +$$$$$$$$$$$@@%o $$$$$$$$$$@· ==
+ == ·@$$$$$$$$$@@@@@@@@@@$$$$$$$$$$$$$@@@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x ·$@@@@$$$@@@@@*xox=$@@@@$$$$@@@@$=oox*@@@@@$$$@@@@@~ x+++
+ ** o*%$$$$*x ·+%$$$$%+· x*$$$$$*o **
+ x+**~ **+x
+ xx**+= +x*=**== ==**==x+ =+**+x
+ ++==*%*%%%**++xx ++==*%%%%%%*==++ xx++**%%%*%*==++
+
diff --git a/src/build/framegen/frames/frame_135.txt b/src/build/framegen/frames/frame_135.txt
new file mode 100644
index 0000000000..2b1bed79ba
--- /dev/null
+++ b/src/build/framegen/frames/frame_135.txt
@@ -0,0 +1,41 @@
+
+
+ ++++
+ ++==*%%%*%****%*%%%*==++
+ x+==*%== ==%*==+x
+ ===*xo ox*===
+ ++**x+ x*%$@@@@@@@@@@$%*x +x**++
+ ++*= +$@@@@@@$$$$$$$$$$$$@@@@@@$+ =*++
+ x+== *@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* ==+x
+ xx== +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ==xx
+ **o· +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·o**
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ =* +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ *=
+ xx++ +@$$$$$$@@@@@@$@@@@@@$$$$$$$$$$@@@@@@$$@@@@@@$$$$$@+ ++xx
+ +++~ @$$$$$@@= +@@$$$$$$@@%o o$@$$$$$@ ~+++
+ == @$$$$$@o ~ o@$$$$@% ·· $$$$$$@ ==
+ == @$$$$@ oo ~@$$@= x $$$$$@ ==
+ == ·@$$$@= = *· $$$@ % $ x@$$$@· ==
+ == ·@$$$@o @ ~@ +· $$$$ xo @ % +@$$$@· ==
+ == ·@$$$$% = =@$$@ % @$$$$@· ==
+ == ·@$$$$@= **· x@@$$$$$ o%o ~%@$$$$$@· ==
+ == ·@$$$$$@$· =@@@$$$$$$$@+ @@@@$$$$$$@· ==
+ == ·@$$$$$$$@@$+ *$$$$$$$$$$$@@$=~ o$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$@@@@@@@@@@$$$$$$$$$$$$$$@@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x $@@@@@$$@@@@@*xoo+%@@@@@$$@@@@@%+o~x=@@@@@$$@@@@@$· x+++
+ **o ~=%$$$%*o +%$$$$%+ o*%$$$%=~ **
+ x+**~~ ~**+x
+ ++**== ++==**==xo o+==**==++ =+**++
+ ++==*%%%%%**++xx ++==*%%%%%%*==++ xx++**%%%%%**=++
+
diff --git a/src/build/framegen/frames/frame_136.txt b/src/build/framegen/frames/frame_136.txt
new file mode 100644
index 0000000000..66c8c4040c
--- /dev/null
+++ b/src/build/framegen/frames/frame_136.txt
@@ -0,0 +1,41 @@
+
+
+ x ++++====++++
+ ++==*%*%==++++++++==%*%*==++
+ ++**=* *=**++
+ x+**+= ·· =+**+x
+ ++== o=%@@@@@@@@@@@@@@@@%=o ==++
+ ===+ +$@@@@@$$$$$$$$$$$$$$$$@@@@@$+ +===
+ ++++ =@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ++++
+ ++=+ $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ +=++
+ ** $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ **
+ +++o =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= o+++
+ == $@$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$@$ ==
+ ++++ $@$$$$$@@@@$*=+=*$@@@@$$$$$$$$$@@@@%=+==%@@@@$$$$$@$ ++++
+ ==+· +@$$$$$@$~ ·%@$$$$$$@@x +@@$$$$@x ·+==
+ == @$$$$$@ ~x=~ @$$$$@x ·o=x *@$$$$@ ==
+ == $$$$$@ = x @$$@o ~x $$$$$$ ==
+ == ·@$$$@+ % *o $$$$ = @ o@$$$@· ==
+ == ·@$$$@x @ * o @$$$ ~x * x *@$$$@· ==
+ == ·@$$$$$ * %$$$@ = x@$$$$@· ==
+ == ·@$$$$@% o**oo~+%@@$$$$@ ·+%xo~x=@@$$$$$@· ==
+ == ·@$$$$$@@= +@@@$$$$$$$@% @@@$$$$$$$@· ==
+ == ·@$$$$$$$@@@$=o~··~x%$$$$$$$$$$$@@@$*x~··~o*$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$@@@@@@@@@$$$$$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@= ==
+ ++++ %@@@@@@@@@@@$+o·~x*@@@@@@@@@@@@*o··~+$@@@@@@@@@@@% x+++
+ **x· +*%%%*+· o=%%%%=o ·+%%$%*+ ·o**
+ x+**o~ oo oo oo**+x
+ ++**+=+~ =+******+x x+******+= ~x==**++
+ ++==**%%%***++xx ++==***%%***==++ xx++***%%%**==++
+
diff --git a/src/build/framegen/frames/frame_137.txt b/src/build/framegen/frames/frame_137.txt
new file mode 100644
index 0000000000..de5f454c6b
--- /dev/null
+++ b/src/build/framegen/frames/frame_137.txt
@@ -0,0 +1,41 @@
+
+
+ ++++==****==++++
+ ==***%==xo ox==%***==
+ ===*++ ++*===
+ ++**x+ ·oxx++xxo· +x**++
+ ===+ o*$@@@@@@@@@@@@@@@@@@$*o +===
+ **++ o%@@@@@$$$$$$$$$$$$$$$$$$@@@@@%o ++**
+ ==+o ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ o+==
+ ++++ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ++++
+ ox== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xo
+ ==+~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~+==
+ ox== @@$$$$$$$@@@@@@@@@$$$$$$$$$$$$$$$@@@@@@@@$$$$$$$@@ ==xo
+ +++x @$$$$$$@@@$*xooox=$@@@$$$$$$$$@@@@%+ooox+%@@@$$$$$$@ x+++
+ ==x =$$$$$$@= =@$$$$$$@$~ o@@$$$$$* x==
+ == @$$$$@$ ·o+%o $$$$$@~ ox*+ =@$$$$@ ==
+ == $$$$$$ * + @$$@· +· %$$$$$ ==
+ == ~@$$$@x @ ~ *x $$$$ ~+ o @ o@$$$@~ ==
+ == ·@$$$@+ $ o @$$$ ·= x · *$$$$@· ==
+ == ·@$$$$$ = $$$$@~ o~ =@$$$$@· ==
+ == ·@$$$$$$ ~==o++*$@$$$$$@o ·x*ox+=%@@$$$$$@· ==
+ == ·@$$$$$$@% x@@$$$$$$$$@$o @@@$$$$$$$@· ==
+ == ·@$$$$$$$@@@@%=+xx+*$$$$$$$$$$$$$@@@$*+xx+=%$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@+ ==
+ ++++ *@@@@@@@@@@@%x· ~=@@@@@@@@@@@@=~ x%@@@@@@@@@@@* ++++
+ **x· o=*%%=x ~+*%%*+~ x=%%*=x ·x**
+ x+**xo ++ ++ ox**+x
+ x+**=*xx *+****==++ ++==****+* x+==**+x
+ ++==********++xx ++==********==++ xx++********==++
+
diff --git a/src/build/framegen/frames/frame_138.txt b/src/build/framegen/frames/frame_138.txt
new file mode 100644
index 0000000000..284028b3b0
--- /dev/null
+++ b/src/build/framegen/frames/frame_138.txt
@@ -0,0 +1,41 @@
+
+
+ +++=**%%%%%%%%**=+++
+ ++***%==o· ·o==%***++
+ ++**+* *+**++
+ ==== o+=*%$$$$%*=+o ====
+ x+**xx x*@@@@@@@@@@@@@@@@@@@@@@*x xx**xx
+ xx**~~ ~%@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@%~ ~~**xx
+ **x· *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ·x**
+ ==+o %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% o+==
+ xx== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==xx
+ ==x· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==
+ xx== @$$$$$$$$@@@@@@@@@$$$$$$$$$$$$$$@@@@@@@@@@$$$$$$$@ ==xx
+ +++o @$$$$$$@@@*x· ·o=$@@$$$$$$$$@@@%+~ ~+%@@@$$$$$@ o+++
+ == %$$$$$@@x o@@$$$$$@* %@$$$$$% ==
+ == @$$$$@% ·~o+%x %$$$$@ ~~x%* x@$$$$@ ==
+ == $$$$$$ % = @$$@ * ~ *@$$$$ ==
+ == ·@$$$@x @ * *+ $$$$ ox * @ o@$$$@· ==
+ == ·@$$$@+ % @$$@ * %$$$$@· ==
+ == ·@$$$$@ + ·@$$$@x o %@$$$$@· ==
+ == ·@$$$$$@ ~xx~=%$@@$$$$$@= ox~x*%@@@$$$$$@· ==
+ == ·@$$$$$$@$x x@@$$$$$$$$@@= @@$$$$$$$$@· ==
+ == ·@$$$$$$$$@@@@$%**%$@$$$$$$$$$$$$@@@@$%**%$@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@o ==
+ ++++ +@@@@@@@@@@@=~ x$@@@@@@@@@@$x ·=@@@@@@@@@@@+ ++++
+ **+~ o+=+x· o+==+o ·x+=+x ~x**
+ xx**+x == == x+**+x
+ xx**==+= ~+*=******+* *+******=*x~ =+==**xx
+ ++==********++ x+++********+++x ++********==++
+
diff --git a/src/build/framegen/frames/frame_139.txt b/src/build/framegen/frames/frame_139.txt
new file mode 100644
index 0000000000..c4537217da
--- /dev/null
+++ b/src/build/framegen/frames/frame_139.txt
@@ -0,0 +1,41 @@
+
+
+ ++==**%%*%%%%%%*%%**==++
+ ==***%xx xx%***==
+ ++**++ ++**++
+ xx**++ x=*$$@@@@@@$$*=x ++**+x
+ ++**~o ~*$@@@@@@@$$$$$$$$@@@@@@@$*~ oo**++
+ ++** +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ **++
+ xx** ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ **xo
+ ==+~ ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ x+++ o@$$$$$$$$@@@@@@@@@@$$$$$$$$$$$$$@@@@@@@@@@$$$$$$$@o +++x
+ +++~ @$$$$$$@@$=o ~+$@@$$$$$$$$@@@%x· ·x%@@@$$$$$@ ~+++
+ == $$$$$$@@o ~@@$$$$$@= %@$$$$$$ ==
+ == @$$$$@* ~~~x%x %$$$$@ ·~~o%= o@$$$$@ ==
+ == ·$$$$$$ * = @$$@ * o *@$$$$· ==
+ == ·@$$$@x @ % *+ $$$$ oo % @ o@$$$@· ==
+ == ·@$$$@= * @$$@ * %$$$$@· ==
+ == ·@$$$$@ x ~@$$$@+ o %@$$$$@· ==
+ == ·@$$$$$@~ ~o~~=%$@@$$$$$@* oo~x%$@@@$$$$$@· ==
+ == ·@$$$$$$@@+ x@$$$$$$$$$@@*· @@$$$$$$$$@· ==
+ == ·@$$$$$$$$@@@@$$%%$$@$$$$$$$$$$$$@@@@@$%%%$@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==+· @@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@ x==
+ ++=+ o$@@@@@@@@@@+ ~*@@@@@@@@@@*~ +@@@@@@@@@@$x ++++
+ ==+x ·oxo~ ~oo~ ~oxo· o+*=
+ x+**+x =* == ++**xx
+ x+==**+*+o ++*=**==**=*+x x+*=**==**=*++ ~+*=**==xx
+ ++++******==++ ++++********++++ ++==******++++
+
diff --git a/src/build/framegen/frames/frame_140.txt b/src/build/framegen/frames/frame_140.txt
new file mode 100644
index 0000000000..be700a44af
--- /dev/null
+++ b/src/build/framegen/frames/frame_140.txt
@@ -0,0 +1,41 @@
+
+ xx++++xx
+ ===*%%%%*%****%*%%%%*===
+ ++**=%++ ++%=**++
+ ===* *===
+ x+**+x ~+*$@@@@@@@@@@@@$*+~ x+**+x
+ ++== =$@@@@@$$$$$$$$$$$$$$@@@@@$= ==++
+ ++*= ~%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%~ =*++
+ x+== =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ==+x
+ =*o =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= o*=
+ ++++ ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ++++
+ == =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==
+ x+++ =@$$$$$$$$@@@@@@@@@$$$$$$$$$$$$$$@@@@@@@@@@$$$$$$$@= ++xx
+ +++· ·@$$$$$$@@@%+~ ~x*@@@$$$$$$$$@@@$=o· ~o=$@@@$$$$$@· ·+++
+ == @$$$$$@@x x@@$$$$$@% $@$$$$$@ ==
+ == $$$$$@% ~o=%x $$$$$@ ~~x%= x@$$$$$ ==
+ == ·@$$$$$ * = @$$@ * · *$$$$@· ==
+ == ·@$$$@x @ = *+ $$$$ ox * @ o@$$$@· ==
+ == ·@$$$@+ % @$$@ * %$$$$@· ==
+ == ·@$$$$@ + ·@$$$@x o %@$$$$@· ==
+ == ·@$$$$$@ ~xo~=%$@@$$$$$@= ox~x*%@@@$$$$$@· ==
+ == ·@$$$$$$@$x x@@$$$$$$$$@@* @@$$$$$$$$@· ==
+ == ·@$$$$$$$$@@@@$%%%%$@$$$$$$$$$$$$@@@@$$%%%$@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==x· $@@$$$$$$$$$@@@@@@@@@@$$$$$$$$@@@@@@@@@@$$$$$$$$$@@$ ·+==
+ ++++ =@@@@@@@@@*· x$@@@@@@@@$x *@@@@@@@@@= ++++
+ ==++ +===
+ xx=*=+ ++%%xx xx%%++ +===xx
+ ==**=*==++++*=*=**==**=%=*++++*=%=**==**=*=*++++==*=**==xx
+ ++++==****==++ ++==****==++ ++==****==++++
+
diff --git a/src/build/framegen/frames/frame_141.txt b/src/build/framegen/frames/frame_141.txt
new file mode 100644
index 0000000000..f5780d60f0
--- /dev/null
+++ b/src/build/framegen/frames/frame_141.txt
@@ -0,0 +1,41 @@
+
+ xx++++++++xx
+ x++=*%*%*%========%*%*%*=++x
+ ++**=*+x x+*=**++
+ xx**=* *=**xx
+ ++**o x*$@@@@@@@@@@@@@@$*x o**++
+ ++=+ o*@@@@@$$$$$$$$$$$$$$$$@@@@@*o +=++
+ ++=+ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x +=++
+ x+== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==+x
+ ** %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% **
+ +++x x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x x+++
+ =* *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* *=
+ ++++ *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++++
+ ==+· o@$$$$$$$@@@@@@@@@@@@@$$$$$$$$$$@@@@@@@@@@@@@$$$$$$$@o ·+==
+ == @$$$$$$@@%~ ~*@@$$$$$$@@$+ +$@$$$$$$@ ==
+ == $$$$$$@o o@$$$$@* $@$$$$$ ==
+ == ·@$$$$@ x @$$@+ o $$$$$@· ==
+ == ·@$$$@+ % %o $$$$ = @ x@$$$@· ==
+ == ·@$$$@+ $ o @$$$ ·= o *$$$$@· ==
+ == ·@$$$$@ x $$$$@x ~ *@$$$$@· ==
+ == ·@$$$$$@~ · +%$@@$$$$$@= ~%%@@@$$$$$@· ==
+ == ·@$$$$$$@@*· +@@$$$$$$$$@@%o @@$$$$$$$$@· ==
+ == ·@$$$$$$$$@@@@@@@@@@@$$$$$$$$$$$$@@@@@@@@@@@$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ == @@$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$@@ ==
+ ==+~ *@@@$$$$$$$@@@@$%%@@@@$$$$$$$$@@@@%%$@@@@$$$$$$$@@@* ~+==
+ ++== o%@@@@@@@$x ·*@@@@@@@@*· x$@@@@@@@%x ==++
+ ===+ +===
+ ox====o+ ==%%++ ++%%== xo====+x
+ ++**=**%****%***==++**=*=******=*=**++==***%****%**=**++
+ x++++=====++++ ++++====++++ ++++======+++x
+
diff --git a/src/build/framegen/frames/frame_142.txt b/src/build/framegen/frames/frame_142.txt
new file mode 100644
index 0000000000..1051e2b805
--- /dev/null
+++ b/src/build/framegen/frames/frame_142.txt
@@ -0,0 +1,41 @@
+
+ xx++++====++++xx
+ ++==*%**==++oooo++==**%*==++
+ ++**=* *=**=+
+ ++**+= ···· =+**++
+ ++== x=$@@@@@@@@@@@@@@@@$=x ==++
+ ==++ =$@@@@$$$$$$$$$$$$$$$$$$@@@@$= ++==
+ ===+ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* +===
+ ++=+ ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· +=++
+ o ** $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ** o
+ +++o =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= o+++
+ == $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ ==+· +@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == $$$$$$@@@@@@@@@@@@@@@@@@$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == ·@$$$$@= ·*@$$$@= +@$$$$$@· ==
+ == ·@$$$@ ~@$@· @$$$$@· ==
+ == ·@$$$$x +$$$+ @$$$$@· ==
+ == ·@$$$$@@*==============*@@$$$@@*===============$@$$$$$@· ==
+ == ·@$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ ==+x ~@@@@$$$$$@@@@%+x+=$@@@@$$$$@@@@$=+x+%@@@@$$$$$@@@@o o+==
+ xx== x*$@@@$%x ~=$@@@@$=~ x%$@@@$*x ==xx
+ ==== ====
+ ====+= xx*=**== ==**=*xx =+====
+ ++*****%*%%*****==++==***%*%%*%***==++==*****%%%%*****++
+ ++++++++++++ x++++++++++x ++++++++++++
+
diff --git a/src/build/framegen/frames/frame_143.txt b/src/build/framegen/frames/frame_143.txt
new file mode 100644
index 0000000000..a5cb02dd27
--- /dev/null
+++ b/src/build/framegen/frames/frame_143.txt
@@ -0,0 +1,41 @@
+
+ ++============++
+ ++**%%=*++o~····~o++*=%%**++
+ +=**== ==**=+
+ ++**++ ·~~oo~~· ++**++
+ ===+ +%@@@@@@@@@@@@@@@@@@%+ +===
+ ==++ ·*@@@@@$$$$$$$$$$$$$$$$$$@@@@@*· ++==
+ ===x %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% x===
+ ++=+ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o +=++
+ ox== @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xx
+ ==+~ *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ~+==
+ == $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ +++x $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ x+++
+ ==x· =@$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·x==
+ == @$$$$$@$**%$@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == $$$$$@ o+*%@@@@$$$$@@@@@@@@@@@@@@@@@@@@$$$$$$ ==
+ == ·@$$$$@ *$$$$% *$$$$$@· ==
+ == ·@$$$$$@ @$$@ @$$$$@· ==
+ == ·@$$$$@ =$$$$% =$$$$$@· ==
+ == ·@$$$$@ ~x=%@@@@$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$@$==%$@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@= ==
+ ==++ *@@@@@@@@@@@$+~ o*@@@@@@@@@@@@*o ·+$@@@@@@@@@@@% x+==
+ ++==x· x=%%%*x o=*%%*=o x*%%%*x ·x==++
+ ====oo xx xx ~o====
+ ====+=++ =+*==*==++ ++=====*+= xx=+====
+ ++==**=*********++++==***%****%***==++++************==++
+ ++++++++++xx ++++++++++++ x ++++++++++
+
diff --git a/src/build/framegen/frames/frame_144.txt b/src/build/framegen/frames/frame_144.txt
new file mode 100644
index 0000000000..b0f7b896b3
--- /dev/null
+++ b/src/build/framegen/frames/frame_144.txt
@@ -0,0 +1,41 @@
+
+ ++====****====++
+ +=***%==+o ox==%***=+
+ ===*++ ++*===
+ ++**x+ ·ooxxxxoo· +x**++
+ ===+ o=$@@@@@@@@@@@@@@@@@@$=o +===
+ **++ o%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%o ++**
+ ==+x ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ x+==
+ ++++ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ++++
+ ox== ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==xo
+ ==+~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~+==
+ ox== @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==+ =@$$$$@= o*@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= +==
+ == @$$$$@+ x%@@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == $$$$$$% =@@$$$$@$+oooooooooooooox%@$$$$$$ ==
+ == ·@$$$$$@@$=~ *@$$$o @$$$$@· ==
+ == ·@$$$$$@@$*x *$$$@o @$$$$@· ==
+ == ·@$$$$$% +@@$$$$@%x~~~~~~~~~~~~~~o*@$$$$$@· ==
+ == ·@$$$$@+ o*@@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$@= ~=$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@ x==
+ ++++ o$@@@@@@@@@$+ ·*@@@@@@@@@@*· +$@@@@@@@@@$o ++++
+ x+==+x ·ooo~ ~oo~ ~ooo· o+==+x
+ ++==++ +** **+ x+==++
+ ++====+=+x ++*========*+x x+*========*++ x+======++
+ ++++==********==++++++==********==++++++==********====++
+ ++++++++ ++++++++ ++++++++
+
diff --git a/src/build/framegen/frames/frame_145.txt b/src/build/framegen/frames/frame_145.txt
new file mode 100644
index 0000000000..f096e8c69b
--- /dev/null
+++ b/src/build/framegen/frames/frame_145.txt
@@ -0,0 +1,41 @@
+
+ ++====****====++
+ xx+=***%==x~ ~x==%***=+xx
+ x+===*++ ++*===+x
+ ++**+x ~ox++++xo~ x+**++
+ ==++ x*$@@@@@@@@@@@@@@@@@@$*x ++==
+ **+x x%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%x x+**
+ ==+o o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o o+==
+ ++++ +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ++++
+ xx== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xx
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ x+== @@$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==+x
+ +++x @$$$$@@$*%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==x *$$$$$% ~=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$* x==
+ == @$$$$@* =$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == $$$$$$@%· %@$$$$@x ~$$$$$$$ ==
+ == ~@$$$$$$$@@@%~ %$$$@ @$$$$@~ ==
+ == ·@$$$$$@@$+ $$$$$= o$$$$$@· ==
+ == ·@$$$$$$ =$@$$$$$@@$%%%%%%%%%%%%%%%@@$$$$$@· ==
+ == ·@$$$$@* ~*@@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$@* o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· $@@$$$$$$$$$@@@@@@@@@@$$$$$$$$@@@@@@@@@@$$$$$$$$$@@$ ·x==
+ ++++ =@@@@@@@@@* x$@@@@@@@@$x *@@@@@@@@@= ++++
+ oo==++ ++==xx
+ ++===+ ++%%+o o+%%++ +===++
+ ++=====%+=++++*=*========***++++***========*=*++++=+%=====++
+ xx++==******====++ ++==********==++ ++====******==++xx
+ ++++ ++++ ++++
+
diff --git a/src/build/framegen/frames/frame_146.txt b/src/build/framegen/frames/frame_146.txt
new file mode 100644
index 0000000000..423d3e734c
--- /dev/null
+++ b/src/build/framegen/frames/frame_146.txt
@@ -0,0 +1,41 @@
+
+ xx++==********===+xx
+ ++==*%**=+~· ·~+=**%*==++
+ x+=*=*+x x+*=*=+x
+ ++**xo ·ox+====+xo· ox**++
+ **++ x*@@@@@@@@@@@@@@@@@@@@*x ++**
+ **+o x$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$x x+**
+ ==+o x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x o+==
+ ++++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++++
+ xx== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xx
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@* ·*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == *$$$$@% o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$* ==
+ == @$$$$$$ o%@@$$$$$@@@$$$$$$$$$$$$$$$@@$$$$$@ ==
+ == $$$$$$$@@=· ~$$$$$* +$$$$$$ ==
+ == ~@$$$$$$$@@@@* $$$$@ @$$$$@~ ==
+ == ·@$$$$$$@*~ =$$$$$$· %$$$$$@· ==
+ == ·@$$$$$$ ~*@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@$x ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == $@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@$ ==
+ ==+o +@@@$$$$$$$@@@$***%@@@@$$$$$$@@@@%***$@@@$$$$$$$@@@+ o+==
+ ++== =$@@@@@$*~ +%@@@@@@%+ ~*$@@@@@$=· ==++
+ ===+ +===
+ ++====x+ *=**== ==**=* +x====++
+ ++=====**%******=========**%****%**=========******%**=====++
+ ++++========++xx ++++========++++ xx++========++++
+
+
diff --git a/src/build/framegen/frames/frame_147.txt b/src/build/framegen/frames/frame_147.txt
new file mode 100644
index 0000000000..baf0b2890f
--- /dev/null
+++ b/src/build/framegen/frames/frame_147.txt
@@ -0,0 +1,41 @@
+
+ ++++==********===+++
+ ++==*%**++~· ·~++**%*==++
+ xx**=%+x x+%=**xx
+ ++*=x ~ox+====+xo~ x=*++
+ **++ x%@@@@@@@@@@@@@@@@@@@@%x ++**
+ **+o +$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$+ o+**
+ =*+o x$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$x o+*=
+ ++++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++++
+ ox== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xo
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xo
+ +++x @$$$$$x x%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == *$$$$@% +$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* ==
+ == @$$$$$$o =$@$$$$$@@$%%%%%%%%%%%%%%%@@$$$$$@ ==
+ == $$$$$$$@@$x $$$$$= o$$$$$$ ==
+ == ·@$$$$$$$@@@@= $$$$@ @$$$$@· ==
+ == ·@$$$$$$@= %$$$$$@x ~$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@=oo*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@% ==
+ ==+x ~$@@@@$$$$@@@@%+xx=$@@@@$$$$@@@@$=xx+*@@@@$$$$@@@@@~ x+==
+ ++== o*$@@@$*x ·=%$@@$%=· x*$@@@$*x ==++
+ ==== =+==
+ xx====+= x+*===+= =+====+x =+====xx
+ ++====*%%%%**=====++====*%*%%*%*====++=====**%%%%*====++
+ xx++++====++++ ++++====++++ ++++====++++x+
+
+
diff --git a/src/build/framegen/frames/frame_148.txt b/src/build/framegen/frames/frame_148.txt
new file mode 100644
index 0000000000..a36374d761
--- /dev/null
+++ b/src/build/framegen/frames/frame_148.txt
@@ -0,0 +1,41 @@
+
+ ++++==********===+++
+ ++==*%**++~· ·~++**%*==++
+ xx**=%+x x+%=**xx
+ ++*=x ~ox+====+xo~ x==++
+ **++ +%@@@@@@@@@@@@@@@@@@@@%+ ++**
+ **+o +$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$+ o+**
+ ==+~ x$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$x ~+*=
+ ++++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++++
+ ox== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xo
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xo
+ +++x @$$$$$x x%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == *$$$$@% x$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* ==
+ == @$$$$$$o =$@$$$$$@@%%%%%%%%%%%%%%%%@@$$$$$@ ==
+ == $$$$$$$@@$x $$$$$= ~$$$$$$ ==
+ == ~@$$$$$$$@@@@+ $$$$@ @$$$$@~ ==
+ == ·@$$$$$@@+ ·$$$$$$@+ o$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@=xx%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@+ ==
+ ==++ *@@@@@@@@@@@%x ~=@@@@@@@@@@@@=~ x%@@@@@@@@@@@% ++==
+ xx==x· o=***=x ~+****+~ x=***=x ·x==++
+ ====xo xx x+ ox====
+ ======+x =+=*====++ ++====*=+= x+*=====
+ ++==***%********++++==************==++++********%***==++
+ ++++++++++ +x++++++++x+ ++++++++++
+
+
diff --git a/src/build/framegen/frames/frame_149.txt b/src/build/framegen/frames/frame_149.txt
new file mode 100644
index 0000000000..eaf1e09a4a
--- /dev/null
+++ b/src/build/framegen/frames/frame_149.txt
@@ -0,0 +1,41 @@
+
+ xx++==********===+xx
+ ++==*%**++~· ·~++**%*==++
+ xx**=*+x x+*=**xx
+ ++*=xo ·ox+====+xo· ox=*++
+ **++ x%@@@@@@@@@@@@@@@@@@@@%x ++**
+ **+o +$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$+ o+**
+ =*+o x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x o+*=
+ ++++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++++
+ ox== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xo
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@= +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == *$$$$@% +$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* ==
+ == @$$$$$$· =$@$$$$$@@$%%%%%%%%%%%%%%%@@$$$$$@ ==
+ == $$$$$$$@@*o $$$$$= ~$$$$$$ ==
+ == ·@$$$$$$$@@@$x $$$$@ @$$$$@· ==
+ == ·@$$$$$@@x ·%$$$$$@+ o$$$$$$@· ==
+ == ·@$$$$$$ =$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@%+=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@o ==
+ ++++ +@@@@@@@@@@@*~ x$@@@@@@@@@@$x ·*@@@@@@@@@@@= ++++
+ ox==+o x+=+x· o+==+o ·x+=+x· ~+==+x
+ ++==+x == == xx==++
+ ++====++ ox*+==**==+= =+=***==+*xo ++====++
+ ++==**********==++++++************++++++==**********==++
+ ++++++++ xx++++xx ++++++++
+
+
diff --git a/src/build/framegen/frames/frame_150.txt b/src/build/framegen/frames/frame_150.txt
new file mode 100644
index 0000000000..9f440069ea
--- /dev/null
+++ b/src/build/framegen/frames/frame_150.txt
@@ -0,0 +1,41 @@
+
+ xx++==********===+xx
+ +++=****==o· ·o==****=+++
+ x+=*=*+x x+*=*=+x
+ ++**+x ~x++==++x~ xx**++
+ **++ x*@@@@@@@@@@@@@@@@@@@@*x ++**
+ **xx x%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%x x+**
+ ==+o o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o o+==
+ ++++ +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ++++
+ ox== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xx
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@* =@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==x *$$$$@% ·=@@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* ==
+ == @$$$$$$ ·*@@$$$$$@@$%%%%%%%%%%%%%%%@@$$$$$@ ==
+ == $$$$$$$@@*~ $$$$$= o$$$$$$ ==
+ == ~@$$$$$$$@@@@+ $$$$@ @$$$$@~ ==
+ == ·@$$$$$@@x %$$$$$@x ~$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ +$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@*+=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·x==
+ ++++ ~%@@@@@@@@@$x ·*@@@@@@@@@@*· x$@@@@@@@@@%o ++++
+ ox==+x ~oo· ~oo~ ·ooo o+==xo
+ ++==++ +%% %%+o ++==++
+ ++=*===*xx ~o++*=**==**+*++ ++*+**==**=*++ ~ xx*===*=++
+ ++==********++xx ++==********==++ xx++********==++
+
+
+
diff --git a/src/build/framegen/frames/frame_151.txt b/src/build/framegen/frames/frame_151.txt
new file mode 100644
index 0000000000..540e3e13c5
--- /dev/null
+++ b/src/build/framegen/frames/frame_151.txt
@@ -0,0 +1,41 @@
+
+ x+++===******====++x
+ ++==***%=*xo ox==%***==++
+ xx=*=*++ ++*=*=xx
+ ++**++ ~ox++++xo~ ++**++
+ **++ o*$@@@@@@@@@@@@@@@@@@$*o ++**
+ **+x o%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%o x+**
+ ==+o ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ o+==
+ ++=+ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ++++
+ ox== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xo
+ ==+~ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ~+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@* ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==+ *@$$$@% ~*@@@@$$$$$$$$$$@@@@@@@@@@@@@@@$$$$$$@* +==
+ == @$$$$$$ ~*@@$$$$$@@$%%%%%%%%%%%%%%$@@$$$$$@ ==
+ == $$$$$$$@$=· $$$$$* x$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@~ ==
+ == ·@$$$$$@@+ %@$$$$$o ·$$$$$$@· ==
+ == ·@$$$$$$ x$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==+· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·+==
+ ++++ %@@@@@@@@@%o =$@@@@@@@@$= ~%@@@@@@@@@%· ++++
+ ==+x ·· ·· ··· x+==
+ x+==++ ++%% %*++ ++*=xx
+ xx==**=*++xxxx+=*=**==**=*=+xxxx+=%=**==**=*=+xxoo++*=**==xx
+ ++++******==++xx ++++********++++ xx++==******=+++
+
+
+
diff --git a/src/build/framegen/frames/frame_152.txt b/src/build/framegen/frames/frame_152.txt
new file mode 100644
index 0000000000..34e342244b
--- /dev/null
+++ b/src/build/framegen/frames/frame_152.txt
@@ -0,0 +1,41 @@
+
+ xx++=====**=====++xx
+ +++=***%**++~· ·~++**%***++++
+ xx==*%=+ +=%*==xx
+ ++**++ ~~oooo~~ ++**++
+ ==== ·+%@@@@@@@@@@@@@@@@@@%+· ====
+ **+x ~*@@@@@$$$$$$$$$$$$$$$$$$@@@@@*~ x+**
+ ==+x ·%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%· x+==
+ ++=+ x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x +=++
+ ox== ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ==xo
+ ==+~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x $$$$$$%· o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ x+++
+ ==+· =@$$$@$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+==
+ == @$$$$$$ x%@@$$$$$@@@$$$$$$$$$$$$$$$@@$$$$$@ ==
+ == $$$$$$$@$+ ~$$$$$* +$$$$$$ ==
+ == ~@$$$$$$$@@@@+ $$$$@ @$$$$@~ ==
+ == ·@$$$$$$@= *$$$$$$· %$$$$$@· ==
+ == ·@$$$$$$ o%@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@=ox*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· $@@$$$$$$$$$@@@@@@@@@@$$$$$$$$@@@@@@@@@@$$$$$$$$$@@$ ·+==
+ +++= =$@@@@@@@@* x%@@@@@@@@%x *@@@@@@@@@* ++++
+ ==++ +===
+ xx=*=+ ++%%+x x+%%=+ +=**+x
+ ++**=*=+++++=**=**++**=%==++++==%=**++**=**=+++++=*=**++
+ xx++======++++ ++========++ ++++==**==++xx
+
+
+
diff --git a/src/build/framegen/frames/frame_153.txt b/src/build/framegen/frames/frame_153.txt
new file mode 100644
index 0000000000..b81afbddb8
--- /dev/null
+++ b/src/build/framegen/frames/frame_153.txt
@@ -0,0 +1,41 @@
+
+ xx++============++xx
+ ++++**%%=*==xo~~~~ox==**%%**++++
+ xx==**== ==**==xx
+ ++**+= ·~~~~· =+**++
+ ===+ x*$@@@@@@@@@@@@@@@@$*x +===
+ **++ =@@@@@$$$$$$$$$$$$$$$$$$@@@@@= ++**
+ ==++ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* +===
+ ++=+ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ +=++
+ xx== @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ ==+o *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* o+==
+ ox== $@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==xo
+ +++x $@$$$@$o x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ x+++
+ ==+· +@$$$$$ +$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+==
+ == @$$$$$$ +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$$@%x o$$$$$% =$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@*~ +$$$$$$ *$$$$$@· ==
+ == ·@$$$$$$ ·*@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@+·~=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@$$$$$$$$$@@@@$$@@@@$$$$$$$$@@@@$$@@@@$$$$$$$$$@@$ ·+==
+ x+== +$@@@@@@@$* o%@@@@@@@@%o *$@@@@@@@$= ==++
+ ==++ ++==
+ ==== ==%%++ ++%%== +===
+ ++**=%==++++****==++**=***++++***=**++==****++++==%=**++
+ +++=====++++ +++======++x ++++=====+++
+
+
+
diff --git a/src/build/framegen/frames/frame_154.txt b/src/build/framegen/frames/frame_154.txt
new file mode 100644
index 0000000000..7f4c1c11d5
--- /dev/null
+++ b/src/build/framegen/frames/frame_154.txt
@@ -0,0 +1,41 @@
+
+ ++++++====++++++
+ xx+=***%%%**==++++==**%%%***=+xx
+ ox==**=*xo ox*=**==xo
+ ++**+= =+**++
+ ==== ~+%$@@@@@@@@@@@@@@$%+~ ====
+ ===+ x%@@@@@$$$$$$$$$$$$$$$$@@@@@%x +===
+ ===+ +@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ +===
+ ++=+ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% +=++
+ ox== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ==xo
+ ==+x x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x x+==
+ ox== %@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==xo
+ ++++ %@$$$@@+~o=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ ==+· x@$$$$$ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+==
+ == @$$$$$$ ~*@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$$@=· =$$$$$$ %$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@$x o$$$$$% +$$$$$@· ==
+ == ·@$$$$$$ x$@@$$$$$@@@$$$$$$$$$$$$$$@@@$$$$$@· ==
+ == ·@$$$$@$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@$~ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@@$$$$$$$$@@@@$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ·+==
+ ++== +$@@@@@@@$= ~%@@@@@@@@%~ =$@@@@@@@$+ ==xx
+ ===+ +===
+ ==== ==%%++ ++%%== ====
+ ++**=***++==%***==++***%**++++**%***++==***%==++**%=**++
+ x+++==++++ x+++====+++x ++++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_155.txt b/src/build/framegen/frames/frame_155.txt
new file mode 100644
index 0000000000..0a9d3d097f
--- /dev/null
+++ b/src/build/framegen/frames/frame_155.txt
@@ -0,0 +1,41 @@
+
+ ++++++++++++++++
+ ++*****%*%=******=%*%*****++
+ x ++**=*++ x+*=**++ x
+ ++**+= =+**++
+ ====ox o=%@@@@@@@@@@@@@@%=o xo====
+ ===+ ~*@@@@@@$$$$$$$$$$$$$$@@@@@@*~ +===
+ ===+ o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o +===
+ ++== *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ==++
+ ox== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==xo
+ ==+x o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o x+==
+ ox== =@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ ++++ *@$$$@@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++++
+ ==x· ~@$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ·x==
+ == @$$$$$$ x$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@+ %@$$$$$o ·$$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@$=· $$$$$* x$$$$$@· ==
+ == ·@$$$$$$ ~*@@$$$$$@@$%%%%%%%%%%%%%%$@@$$$$$@· ==
+ == ·@$$$$@% ~*@@@@$$$$$$$$$$@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$@* ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ·+==
+ xx== x%@@@@@@@$= ~*@@@@@@@@*~ =$@@@@@@@$+ ==xx
+ ===+ +===
+ ====o ==%%++ ++%%== ====
+ ++***%**====%***++++==***%====%***==++++***%==+=***=**++
+ xx++++++++ ++====++ ++++++++xx
+
+
+
diff --git a/src/build/framegen/frames/frame_156.txt b/src/build/framegen/frames/frame_156.txt
new file mode 100644
index 0000000000..d1c6743b80
--- /dev/null
+++ b/src/build/framegen/frames/frame_156.txt
@@ -0,0 +1,41 @@
+
+ xx++++++++++++xx
+ ++==***%*%*%%%%%%*%*%***==++
+ ++**=*== ==*=**++
+ ++**=*++ ++*=**++
+ ++**++ ~+*%@@@@@@@@@@%*+~ ++**++
+ ==== o*@@@@@@@$$$$$$$$$$@@@@@@@*o ====
+ ==== =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ====
+ ++== x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x ==++
+ xx==x· x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ·x==xx
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ ox== o@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==xo
+ ++++ x@$$$$@$*%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ++++
+ ==+~ @$$$$$ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+==
+ == $$$$$@$ o%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@%· x$$$$$$@*· +@$$$$$@ ==
+ == ·@$$$$$$$@@@*· $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@$+ $$$$$x @$$$$@· ==
+ == ·@$$$$$$o x%@$$$$$@@*===============$@$$$$$@· ==
+ == ·@$$$$@% o%@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$x o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$@@ ==
+ ==+~ *@@@$$$$$$$@@@@$$$@@@@$$$$$$$$@@@@$$$@@@@$$$$$$$@@@% ·+==
+ ox== x%@@@@@@@$+ ~*@@@@@@@@*~ +$@@@@@@@$+ ==+x
+ ++=+ +=++
+ ++==ox ==%%++ ++%%== o==++
+ x+***%**====%***++++==***%====%***==++++***%====**%***++
+ +++++++x ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_157.txt b/src/build/framegen/frames/frame_157.txt
new file mode 100644
index 0000000000..03caabe0e9
--- /dev/null
+++ b/src/build/framegen/frames/frame_157.txt
@@ -0,0 +1,41 @@
+
+ xx++++++++++++xx
+ ++==****%%*%%%%%%*%%****==++
+ ++****=*++ ++*=****++
+ ++=*==+= =+====++
+ ++**++ ~+=%$$@@@@$$%=+~ ++**++
+ ++==oo +%@@@@@@@@$$$$$$@@@@@@@@%+ oo==++
+ ++== o$@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@$o ==++
+ ++==~ %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% ~==++
+ ox==+~ $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ ~+==xo
+ ++++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ++++ ~@$$$$@@$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ++x+
+ ==+~ @$$$$$· +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+==
+ == $$$$$@$ =$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$$= *@$$$$$@$xoooooooooooooox*@$$$$$@ ==
+ == $$$$$$$@@@%x $$$$$o @$$$$$ ==
+ == ·@$$$$$$@@@%x $$$$@o @$$$$@· ==
+ == ·@$$$$$@= =@$$$$$@$xooooooooooooooo*@$$$$$@· ==
+ == ·@$$$$@$ +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ·+==
+ xx== +%@@@@@@@$= ~%@@@@@@@@%~ =$@@@@@@@$+ ==xx
+ ++=+ +=++
+ ++*=oo ==%%++ ++%%== ==++
+ x+***%**====%***++++==***%====%***==++++***%====**%***++
+ ++++++++ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_158.txt b/src/build/framegen/frames/frame_158.txt
new file mode 100644
index 0000000000..b35f4a8c0b
--- /dev/null
+++ b/src/build/framegen/frames/frame_158.txt
@@ -0,0 +1,41 @@
+
+ ++++++++++++
+ ++++==******%%%%******==++++
+ ++==**=***++ ++******==++
+ ++==**=*+o o+*=**==++
+ ++====x ·o++====++o· x====++
+ ++==++ +%@@@@@@@@@@@@@@@@@@@@%+ ++==++
+ ++==xo +$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$+ ox==++
+ ++==+~ x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x ~+==++
+ ==+x =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= x+==
+ ++== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==++
+ ==x· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·x==
+ ++== @$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==++
+ +++x @$$$$$= +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ == *$$$$@% =$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* ==
+ == @$$$$$$· =$@$$$$$@@%**************%@@$$$$$@ ==
+ == $$$$$$$@@*o $$$$$= ~$$$$$$ ==
+ == ~@$$$$$$$@@@$x $$$$@ @$$$$@~ ==
+ == ·@$$$$$@$o ·$$$$$$@+ o$$$$$$@· ==
+ == ·@$$$$$$ =@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@%==$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@@$$$$$$$$@@@@$$@@@@$$$$$$$$@@@@$$@@@@$$$$$$$$$@@$ ·+==
+ xx== +$@@@@@@@$= o%@@@@@@@@%o =$@@@@@@@$= ==xx
+ ++=+ +=++
+ ++== ==%%++ ++%%== ==++
+ x+=*=***++==%***++xx==*%**====**%*==xx++***%==++***=**+x
+ ++++++++ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_159.txt b/src/build/framegen/frames/frame_159.txt
new file mode 100644
index 0000000000..88548ed34d
--- /dev/null
+++ b/src/build/framegen/frames/frame_159.txt
@@ -0,0 +1,41 @@
+
+ ++xxxx++
+ xx++==****************==++xx
+ ++==**==*%=*++oo oo++*=%*==**==++
+ xx++**=*+= =+*=*=++xx
+ ++====++ ~oooo~ ++====++
+ ++===+ +%@@@@@@@@@@@@@@@@@@%+ +===++
+ ++==+x ·*@@@@@$$$$$$$$$$$$$$$$$$@@@@@*· x+==++
+ x+==+x %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% x+==++
+ ===+ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o +===
+ ++== ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ==++
+ ==+~ *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ~+==
+ ++== $@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==++
+ +++x $$$$$@$~ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ x+++
+ ==+· =@$$$@$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·x==
+ == @$$$$$$ x%@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$$@$x o$$$$$* +$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@*· =$$$$$$· %$$$$$@· ==
+ == ·@$$$$$$ ~*@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@+~o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· $@@$$$$$$$$$@@@@$@@@@@$$$$$$$$@@@@@$@@@@$$$$$$$$$@@$ ·+==
+ xx== =$@@@@@@@@* o%@@@@@@@@%o *@@@@@@@@$= ==+x
+ ++=+ +=++
+ ++== ==%%++ ++%%== ==++
+ x+**=%*=++++%***++x+==*%**++++**%*==++++***%++++==%=**++
+ ++++++++ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_160.txt b/src/build/framegen/frames/frame_160.txt
new file mode 100644
index 0000000000..2e212c62ad
--- /dev/null
+++ b/src/build/framegen/frames/frame_160.txt
@@ -0,0 +1,41 @@
+
+
+ xx++====********====++xx
+ ++==**=**%****++++****%**=**==++
+ ++=====*+x ++%=====++
+ ++====== ======++
+ ++====xx o=%$@@@@@@@@@@@@$%=o xx====++
+ ++===+ ·*$@@@@@$$$$$$$$$$$$$$@@@@@$*· +===++
+ xx===+ o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o +===xx
+ ==++ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ++==
+ ++== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==++
+ ==+x ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ x+==
+ xx== =@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xx
+ ++++ *@$$$$@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++++
+ ==+· ~@$$$$$ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ·+==
+ == @$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@+ %$$$$$@x ·$$$$$$$ ==
+ == ·@$$$$$$$@@@@x $$$$@ @$$$$@· ==
+ == ·@$$$$$$@@*· $$$$$= x$$$$$@· ==
+ == ·@$$$$$$ ~*@@$$$$$@@$$$$$$$$$$$$$$%$@@$$$$$@· ==
+ == ·@$$$$$% ~=@@@@$$$$$$$$$$@@@@@@@@@@@@@@$$$$$$$$@· ==
+ == ·@$$$$$@* ·*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==+· $@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·+==
+ xx== *@@@@@@@@@%~ +$@@@@@@@@$+ ~%@@@@@@@@@* ==xx
+ ===+ +===
+ ===+ ++%% %%++ +===
+ ++**=%+++x++**%*=+++===*==+xx+==*===+++=*%**++++++****++
+ ++++====++ ++++==++ ++====+++x
+
+
+
diff --git a/src/build/framegen/frames/frame_161.txt b/src/build/framegen/frames/frame_161.txt
new file mode 100644
index 0000000000..d96fee855f
--- /dev/null
+++ b/src/build/framegen/frames/frame_161.txt
@@ -0,0 +1,41 @@
+
+
+ xx++++============++++xx
+ ++====**=**%%%****%%%**=**====++
+ ++====**=*o~ ~o*=**====++
+ ======++ ++======
+ x+====++ ~+*%$@@@@@@@@$%*+~ ++====++
+ xx==== o%@@@@@@@$$$$$$$$$$@@@@@@@%o ====xx
+ ==== =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ====
+ =+== o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o ==++
+ x+==x· x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ·x==+x
+ ==++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++==
+ xx== o@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==xx
+ ++++ x@$$$$@@%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ++++
+ ==+~ @$$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+==
+ == $$$$$@$ o%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@% x@$$$$$@*· +@$$$$$@ ==
+ == ·@$$$$$$@@@@*· $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@$+ $$$$$+ @$$$$@· ==
+ == ·@$$$$$$x x%@$$$$$@@*++++++++++++++=$@$$$$$@· ==
+ == ·@$$$$$$ o%@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$x o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==+· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·x==
+ xx++ ·%@@@@@@@@@%o =@@@@@@@@@@= o%@@@@@@@@@%~ ++++
+ ==+x ·~~ ·~~· ~~~ x+==
+ ==++ x+%% %%++ ++==
+ ++**=%++ooox==*===++**=%++xoox++%=**++===*==xooo++%=**++
+ ++++==++++ x+++====+++x ++++====++
+
+
+
diff --git a/src/build/framegen/frames/frame_162.txt b/src/build/framegen/frames/frame_162.txt
new file mode 100644
index 0000000000..1bac9f41a4
--- /dev/null
+++ b/src/build/framegen/frames/frame_162.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++++++++++++++++
+ ++++==****=***%%%%***=****==++++
+ ++++=====%==o~ ~o==%=====++++
+ ++====+= =+====++
+ =====+ ~x=**%%%%**=x~ +=====
+ ====++ o*$@@@@@@@@@@@@@@@@@@@@$*o ++====
+ ====o~ ·*@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@*· ~o====
+ ++==x· =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ·x==++
+ xx==+o %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% o+==xx
+ =+++ *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* +++=
+ ox==x· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==xo
+ ++++ @$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++++
+ ==+o @$$$$$x o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+==
+ == %$$$$@% o*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$% ==
+ == @$$$$$$x o%@$$$$$@@=++++++++++++++=$@$$$$$@ ==
+ == $$$$$$$@@$=· $$$$@x @$$$$$ ==
+ == ·@$$$$$$@@@@*· $$$$@~ @$$$$@· ==
+ == ·@$$$$$@% x@$$$$$@*~ ·+@$$$$$@· ==
+ == ·@$$$$@% x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@$%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x ·@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@~ ==
+ x+++ x$@@@@@@@@@@= o%@@@@@@@@@@%o =@@@@@@@@@@$x ++++
+ ==+o ox++o ~x++x~ o++xo o+==
+ **+x ** ** x+**
+ ++**==o~ ++*+**++**+*xo ox*+**++**=*++ ~o=+**++
+ ++==****++++ ++==****==++ x+++=***==++
+
+
+
diff --git a/src/build/framegen/frames/frame_163.txt b/src/build/framegen/frames/frame_163.txt
new file mode 100644
index 0000000000..e2828f817e
--- /dev/null
+++ b/src/build/framegen/frames/frame_163.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++++++++++++
+ ++++==****************==++++
+ ++===****%++~~ ~~++%***====++
+ ++=====*+x x+*=====++
+ ++====xx ·ox+====+xo· xx====++
+ ====++ x%$@@@@@@@@@@@@@@@@@@$%x ++====
+ ++==+x x$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$x x+==++
+ ++==+o x$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$x o+==++
+ ox==++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++==xo
+ +++= x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x =+++
+ o ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==xo
+ ++== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==++
+ ==+x @$$$$@= =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+==
+ == *$$$$$$ =$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* ==
+ == @$$$$$$ ·=@@$$$$$@@$%%%%%%%%%%%%%%%@@$$$$$@ ==
+ == $$$$$$$@@*~ $$$$$= o@$$$$$ ==
+ == ~@$$$$$$$@@@@x $$$$@ @$$$$@~ ==
+ == ·@$$$$$@$x %$$$$$@x o$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@%+=$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@x ==
+ ++++ =@@@@@@@@@@@%o +$@@@@@@@@@@$+ o%@@@@@@@@@@@* ++++
+ **x~ ~+===+o x====x o+=*=+~ ~+**
+ **+x ++ ++ x+**
+ ==**++ ~o*+**==**+= =+**==**+*o~ ++====
+ +==****===++ ++=******=++ +++==****==+
+
+
+
diff --git a/src/build/framegen/frames/frame_164.txt b/src/build/framegen/frames/frame_164.txt
new file mode 100644
index 0000000000..93246f70b9
--- /dev/null
+++ b/src/build/framegen/frames/frame_164.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++++
+ ++++==************==++++
+ ++++****=**%=+xo~~~~ox+=%**=****++++
+ xx++===*== =+*===++xx
+ ++====++ ·~~~~· ++====++
+ ++===+ x*$@@@@@@@@@@@@@@@@$*x +===++
+ ++==++ =@@@@@$$$$$$$$$$$$$$$$$$@@@@@= ++==++
+ x+===+ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* +===+x
+ ==++ ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ ++==
+ ++== @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==++
+ ==+o *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* o+==
+ ++== $@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==++
+ +++x $@$$$@$o x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ x+++
+ ==x· +@$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·x==
+ == @$$$$$$ +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@%x x$$$$$% =$$$$$$ ==
+ == ·@$$$$$$$@@@@= $@$$@ @$$$$@· ==
+ == ·@$$$$$@@*~ +$$$$$% *$$$$$@· ==
+ == ·@$$$$$$ ·=@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@x·~=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == =@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@* ==
+ +++x $@@@@@$$@@@@@*x~o+%@@@@@$$@@@@@%+o~o=$@@@@$$$@@@@$· x+++
+ **~· ~=%$$$%=o +*$$$$*+ o=%$$$%=~ **
+ xx**o~ ~·**xx
+ ox**+* ++****=*+x x+*=****++ *+**xx
+ ++==*%%%%%**== x++=*%%%%%%*=++x ==**%%%%%*==++
+
+
+
diff --git a/src/build/framegen/frames/frame_165.txt b/src/build/framegen/frames/frame_165.txt
new file mode 100644
index 0000000000..35acb52511
--- /dev/null
+++ b/src/build/framegen/frames/frame_165.txt
@@ -0,0 +1,41 @@
+
+
+
+ x+++====********====++++
+ ++==**=**%**++++++++**%**=**==++
+ ++=*=*=* *=*=*=++
+ x+====+= =+====+x
+ ++===+ ·+%$@@@@@@@@@@@@@@$%+· +===++
+ x+==++ x%@@@@@$$$$$$$$$$$$$$$$@@@@@%x ++==+x
+ xx==++ +@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ ++==xx
+ ==++ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ++==
+ ++== $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ ==++
+ ==+x +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ x+==
+ ++== %@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==++
+ ++++ %@$$$@@+~o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ ==+· x@$$$$$ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+==
+ == @$$$$$$ ~*@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@*· =$$$$$$· *$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@%x o$$$$$* =$$$$$@· ==
+ == ·@$$$$$$ +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@$~ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++o o@@@@$$$$$@@@@%*+=*$@@@$$$$$$@@@$*=+=%@@@@$$$$$@@@@x o+++
+ ** +%@@@@@%+ o*$@@@@$*o +%@@@@@%+ **xo
+ x+** **++
+ x+**+= ox****== ==****xo =+**++
+ ++**%%%%%%%*==+x ++==*%%%%%%%==++ ++==*%%%%%%%**=+
+
+
+
diff --git a/src/build/framegen/frames/frame_166.txt b/src/build/framegen/frames/frame_166.txt
new file mode 100644
index 0000000000..1cb8aec8c0
--- /dev/null
+++ b/src/build/framegen/frames/frame_166.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++========++++++
+ ++==*****%%%********%%%*****==++
+ ++==**=*=+ ++*=**==++
+ ++**+= =+**++
+ xx====ox ~=%$@@@@@@@@@@@@$%=~ xo====xx
+ x+===+ ·=@@@@@@$$$$$$$$$$$$$$@@@@@@=· +===+x
+ ===+ ~%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%~ +===
+ ++== =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ==++
+ x+== =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ==+x
+ ==+x ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ x+==
+ xx== =@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xx
+ ++++ =@$$$$@*++$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ++++
+ ==+· ~@$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ·+==
+ == @$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@x %$$$$$@x ~$$$$$$$ ==
+ == ·@$$$$$$$@@@@x $$$$@ @$$$$@· ==
+ == ·@$$$$$$@@=~ $$$$$= o@$$$$@· ==
+ == ·@$$$$$$ ~=@@$$$$$@@$%%%%%%%%%%%%%%$@@$$$$$@· ==
+ == ·@$$$$$$ ·*@@@@$$$$$$$$$$@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$@* ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ·+==
+ xx== +$@@@@@@@$+ ~*@@@@@@@@*~ +$@@@@@@@$+ ==xx
+ ++=+ +=++
+ ++**oo ==$%++ ++$$== oo==++
+ x+==*%=*====****++++++*%**====**%*++++++**=*====*=%*==+x
+ ++++++ ++++++++ ++++++
+
+
diff --git a/src/build/framegen/frames/frame_167.txt b/src/build/framegen/frames/frame_167.txt
new file mode 100644
index 0000000000..f61e8743c1
--- /dev/null
+++ b/src/build/framegen/frames/frame_167.txt
@@ -0,0 +1,41 @@
+
+
+
+ x ++++++===+++++++ x
+ xx++*******%*%%*%%%*%*******++xx
+ xx+=****== ==****=+xx
+ ++**+*x x*+**++
+ ====x+ x*%$@@@@@@@@@@$%*x +x====
+ ==== +$@@@@@@$$$$$$$$$$$$@@@@@@$+ ====
+ ==++ *@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* ++==
+ ++== +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ==++
+ x+==o· +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·x==xx
+ ==++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++==
+ ox== +@$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ ++++ +@$$$$@%=*$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ++++
+ ==+· @$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·+==
+ == @$$$$@$ ·=@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == @$$$$$@$o ~$$$$$$@+ o@$$$$$@ ==
+ == ·@$$$$$$$@@@$o $$$$@ @$$$$@· ==
+ == ·@$$$$$$@@*x $$$$@= ~@$$$$@· ==
+ == ·@$$$$$$~ +$@$$$$$@@%***************@@$$$$$@· ==
+ == ·@$$$$@$ +$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$= +$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· $@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·x==
+ xx== *@@@@@@@@@%o =@@@@@@@@@@= o%@@@@@@@@@%· +++x
+ ==++ ·~· ~~ ·~· x+==
+ ==++ x+%% %%+x ++==
+ ++**=*++xox+==%*==++===%==xoox==%===++==*%==+xoo++*=**++
+ +++++===++ +++==+++ +++===++++
+
+
diff --git a/src/build/framegen/frames/frame_168.txt b/src/build/framegen/frames/frame_168.txt
new file mode 100644
index 0000000000..c37a57b0e5
--- /dev/null
+++ b/src/build/framegen/frames/frame_168.txt
@@ -0,0 +1,41 @@
+
+
+
+ xx+++++++++++++x
+ ++==*****%%%%%%%%%%*****==++
+ ++**=*=* *=*=**++
+ ++**=*++ ++*=**++
+ ++**x+ ~+*$$@@@@@@@@$$*+~ +x**++
+ ==== o%@@@@@@@$$$$$$$$$$@@@@@@@%o ====
+ ++== =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ==++
+ ++== o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o ==++
+ ox==x· x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ·x==xo
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ == o@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ ++++ x@$$$$@$%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ++++
+ ==+~ @$$$$$ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+==
+ == $$$$$$$ o*@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@%· o@$$$$$@*· +@$$$$$@ ==
+ == ·@$$$$$$$@@@*~ $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@$= $$$$@+ @$$$$@· ==
+ == ·@$$$$$$x x%@$$$$$@@*===============$@$$$$$@· ==
+ == ·@$$$$@% o%@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$x o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@x ==
+ ++++ =@@@@@@@@@@@*o ·+$@@@@@@@@@@$+· o*@@@@@@@@@@@* ++++
+ **+~ ~+=*=+o ·x=**=x· o+=*=+~ ·+**
+ **xx ++ ++ o+**xx
+ ====++ *=**==**+= =+**==**=* ++====
+ +==**%*==+++ ++=******=++ ++=***%%*==+
+
+
diff --git a/src/build/framegen/frames/frame_169.txt b/src/build/framegen/frames/frame_169.txt
new file mode 100644
index 0000000000..39e0d9023e
--- /dev/null
+++ b/src/build/framegen/frames/frame_169.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++++++++
+ ++++****%%%%%%%%%%%%****++++
+ ++***%*%x~ ~x%*%***++
+ ++**==++ ++==**++
+ ++**++ ·x*%$$@@@@@@$$%*x· ++**++
+ ++==~ o*@@@@@@@@$$$$$$$$@@@@@@@@*o ~==++
+ ++== =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ==++
+ ++== ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ ==++
+ ==x· o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ·x==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == o@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ x+++ o@$$$$@$%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o +++x
+ ==x~ @$$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~x==
+ == $$$$$$% x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@% x@$$$$$@*~ ·=@$$$$$@ ==
+ == ·@$$$$$$@@@@=· $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@$=~ $$$$@x @$$$$@· ==
+ == ·@$$$$$$x o%@$$$$$@@=+++++++++++++++$@$$$$$@· ==
+ == ·@$$$$@% o*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$o ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == =@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x $@@@@$$$@@@@@=x~o+%@@@@@$$@@@@@%+o~x=@@@@@$$$@@@@$ x+++
+ **~· ~=%$$$%=o x*$$$$*x o=%$$$%=~ **
+ xx**o~ ~~**+x
+ x+**+= ++******+x x+*=****++ *+**+x
+ ++==*%%%%%%*== xx==*%%%%%%*==xx ==*%%%%%%*==++
+
+
diff --git a/src/build/framegen/frames/frame_170.txt b/src/build/framegen/frames/frame_170.txt
new file mode 100644
index 0000000000..cd5c33eb07
--- /dev/null
+++ b/src/build/framegen/frames/frame_170.txt
@@ -0,0 +1,41 @@
+
+
+
+ +x++++x+
+ ++==***%%%%%%%%%%***==++
+ x+==**=*+o o+*=**==+x
+ ==**++ ++**==
+ ++**++ x=%$$@@@@@@$$%=x ++**++
+ ++**oo ~=@@@@@@@@$$$$$$$$@@@@@@@@=~ oo**++
+ ++== +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ ==++
+ ++=* ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ *=++
+ ==x~ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o ~x==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@@$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==
+ x+++ o@$$$$@@%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o +++x
+ ++x~ @$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~x++
+ == $$$$$$% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@* +@$$$$$@%o~~~~~~~~~~~~~~~=@$$$$$@ ==
+ == ·@$$$$$$@@@$= $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@@*~ $$$$@x @$$$$@· ==
+ == ·@$$$$$$+ ~*@$$$$$@$+xxxxxxxxxxxxxx+%@$$$$$@· ==
+ == ·@$$$$@% ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$o ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == $@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@ ==
+ +++o =@@@$$$$$$$@@@@%**$@@@@$$$$$$@@@@$**%@@@@$$$$$$$@@@= ~+++
+ xx== ~*@@@@@@@*o +$@@@@@@$+ o*@@@@@@@*~ ==xo
+ ++== ==++
+ ++**++ =*%%== ==%%*= +x**++
+ +=**%%%%%*%=**+x ++***%%%%%%***++ ++**=%*%%%%***=+
+ xxxx xxxx xxxx
+
diff --git a/src/build/framegen/frames/frame_171.txt b/src/build/framegen/frames/frame_171.txt
new file mode 100644
index 0000000000..fdaa337abe
--- /dev/null
+++ b/src/build/framegen/frames/frame_171.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++
+ ++==***%*%%%%%%*%***==++
+ xx==***%+x x+%***==+x
+ ==**++ ++**==
+ ++**++ o=*%$$@@@@$$%*=o ++**++
+ ++**oo ·=$@@@@@@@$$$$$$$$@@@@@@@$=· oo**++
+ ++== +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ ==++
+ xx=* ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· *=+x
+ ==+~ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@@$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==
+ xx++ o@$$$$@@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ++xx
+ +++~ @$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$$% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@* +@$$$$$@%o~~~~~~~~~~~~~~~=@$$$$$@ ==
+ == ·$$$$$$$@@@$+ $$$$@~ @$$$$$· ==
+ == ·@$$$$$$@@@*o $$$$@o @$$$$@· ==
+ == ·@$$$$$$+ ~*@$$$$$@$+xxxxxxxxxxxxxx+%@$$$$$@· ==
+ == ·@$$$$@% ·*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$~ ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@$$$$$$$$$@@@@$$@@@@$$$$$$$$@@@@$$@@@@$$$$$$$$$@@% ·+==
+ xx== +$@@@@@@@@= o%@@@@@@@@%o =@@@@@@@@$+ ==+x
+ ++=+ +=++
+ ++==o +=%$++ ++$%=+ o==++
+ x+==**=*===*%***++xx++*%*%====%*%*++xx++***%*===***===++
+ ++++++xx ++++++++ xx+==+++
+
diff --git a/src/build/framegen/frames/frame_172.txt b/src/build/framegen/frames/frame_172.txt
new file mode 100644
index 0000000000..99d6bf35c9
--- /dev/null
+++ b/src/build/framegen/frames/frame_172.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++++***%%$%%%%$%%***++++
+ ++***%+x x+%***++
+ ++**++ ++**++
+ x+**++ o=*%$$@@@@$$%*=o ++**+x
+ ++**oo ·=$@@@@@@@$$$$$$$$@@@@@@@$=· oo**++
+ ++** +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ **++
+ xx** ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· **xx
+ ==+~ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@@$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==
+ xx++ ~@$$$$@@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ++xx
+ +++~ @$$$$$ +%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$$% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@* +@$$$$$@%o~~~~~~~~~~~~~~~=@$$$$$@ ==
+ == ·$$$$$$$@@@$+ $$$$@~ @$$$$$· ==
+ == ·@$$$$$$@@@*o $$$$@o @$$$$@· ==
+ == ·@$$$$$@+ ~*@$$$$$@$+xxxxxxxxxxxxxx+%@$$$$$@· ==
+ == ·@$$$$@% ·*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$~ ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·x==
+ ++++ ~%@@@@@@@@@$x =@@@@@@@@@@= x$@@@@@@@@@%~ ++++
+ ===x ~~~ ·~~· ~~~ x+==
+ ===+ x+%% %%+x +===
+ ++**=%+x··~o==*===++***%=+o~·~+=%***++===*==o~··x+%=**++
+ +++====+++ xx++====++xx ++======++
+
diff --git a/src/build/framegen/frames/frame_173.txt b/src/build/framegen/frames/frame_173.txt
new file mode 100644
index 0000000000..3c8dff4a21
--- /dev/null
+++ b/src/build/framegen/frames/frame_173.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++==***%%%%%%%%%%***==++
+ +=***%+o o+%***=+
+ +=**++ ++**=+
+ xx**++ o=%%$$@@@@$$%%=o ++**xx
+ x+**oo ~=$@@@@@@@$$$$$$$$@@@@@@@$=~ oo**+x
+ xx** +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ **xx
+ xx** ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· **xx
+ ==+~ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@@$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==
+ xx++ o@$$$$@@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ++xx
+ +++~ @$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$$% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@* +@$$$$$@%o~~~~~~~~~~~~~~~=@$$$$$@ ==
+ == ·$$$$$$$@@@$+ $$$$@~ @$$$$$· ==
+ == ·@$$$$$$@@@*o $$$$@x @$$$$@· ==
+ == ·@$$$$$$+ ~*@$$$$$@$+xxxxxxxxxxxxxx+%@$$$$$@· ==
+ == ·@$$$$@% ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$o ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x ·@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@~ ==
+ ++++ x@@@@@@@@@@@= o%@@@@@@@@@@%o =@@@@@@@@@@@x ++xx
+ ==+o ~x+xo ·x++x· ox+xo o+==
+ **++ == =* x+**
+ +=**==o· ++*=**==**=*+o o+*=**==**=*+x ·o*+**=+
+ ++==****++++ ++==****==++ ++++****==++
+
diff --git a/src/build/framegen/frames/frame_174.txt b/src/build/framegen/frames/frame_174.txt
new file mode 100644
index 0000000000..bd1a0a093f
--- /dev/null
+++ b/src/build/framegen/frames/frame_174.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++==**%%%%%%%%%%%%**==++
+ +=***%+o o+****=+
+ +=**++ ++**=+
+ xx**++ ·x=%$$@@@@@@$$%=x· ++**xx
+ x+**~ ~*@@@@@@@@$$$$$$$$@@@@@@@@*~ ~**+x
+ ++** +@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ **++
+ xx** ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ **xx
+ ==x· o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ·x==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ xx++ o@$$$$@@%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ++xx
+ ++x~ @$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~x++
+ == $$$$$$% x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@% +@$$$$$@*~··············~=@$$$$$@ ==
+ == ·@$$$$$$@@@$=· $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@$=~ $$$$@x @$$$$@· ==
+ == ·@$$$$$$+ o*@$$$$$@$=+++++++++++++++%@$$$$$@· ==
+ == ·@$$$$@% ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$o ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@x ==
+ ++++ =@@@@@@@@@@@%o ~+@@@@@@@@@@@@+~ o%@@@@@@@@@@@* ++++
+ **x~ ~+***=o ·x=**=x· o=***+o ·x**
+ **+x ++ ++ ox**xx
+ ====++ *=**==**+= =+**==**=* ++====
+ ++=**%%**=++ ++=**%%**=++ ++=**%%*==++xx
+
diff --git a/src/build/framegen/frames/frame_175.txt b/src/build/framegen/frames/frame_175.txt
new file mode 100644
index 0000000000..1765bcfbe7
--- /dev/null
+++ b/src/build/framegen/frames/frame_175.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ +++=*%%%%%%%%%%%%%%*=+++
+ +=*%=*x· ·x*=%*=+
+ ++**++ ++**++
+ x+**++ ~+*%$@@@@@@@@$%*+~ ++**+x
+ ++** o*@@@@@@@$$$$$$$$$$@@@@@@@*o **++
+ xx** =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= **xx
+ xx** o$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$o **xx
+ =*x· o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ·x*=
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ =* o@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o *=
+ xx++ x@$$$$@$%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ++xx
+ +++~ @$$$$$ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$$$ x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@% x@$$$$$@*~ ·+@$$$$$@ ==
+ == ·@$$$$$$@@@@*~ $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@$=· $$$$@x @$$$$@· ==
+ == ·@$$$$$$x o%@$$$$$@@=++++++++++++++=$@$$$$$@· ==
+ == ·@$$$$@% o%@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$x o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@+ ==
+ ++++ *@@@@@@@@@@@$+~ ·o*@@@@@@@@@@@@*o ~+$@@@@@@@@@@@% ++++
+ **x· x*%%%*+ o=%%%%=o +*%%%*x ·o**
+ xx**xo xx xx ~x**xx
+ ===*+x *+****==++ ++==****+* o+*=**
+ ++==**%%%**=++ ++=*%%%%*=++ ++==*%%%%*==++
+
diff --git a/src/build/framegen/frames/frame_176.txt b/src/build/framegen/frames/frame_176.txt
new file mode 100644
index 0000000000..5dbc4d2484
--- /dev/null
+++ b/src/build/framegen/frames/frame_176.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++
+ ++=**%*%%%%%%%%%%*%**=++
+ x+==*%=* *=%*==+x
+ ===*++ ++*===
+ x+**++ o=%$@@@@@@@@@@$%=o ++**+x
+ ++** x%@@@@@@$$$$$$$$$$$$@@@@@@%x **++
+ ++== *@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* ==++
+ x+=* x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x **+x
+ **x· +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·x**
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ =* x@$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x *=
+ xx++ +@$$$$@$**$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ++xx
+ +++~ @$$$$$ ·*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == @$$$$@$ ~*@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == @$$$$$@$~ o$$$$$$@= x@$$$$$@ ==
+ == ·@$$$$$$$@@@%o $$$$@· @$$$$@· ==
+ == ·@$$$$$$@@%x $$$$@+ ·@$$$$@· ==
+ == ·@$$$$$$~ +$@$$$$$@@*==============*@@$$$$$@· ==
+ == ·@$$$$@% x%@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$+ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == =@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x $@@@@@$@@@@@@=o~o+%@@@@@@@@@@@@%xo~o=@@@@@@$@@@@@$ x+++
+ **o· ~=%$$$%=~ x*$$$$*x ~=%$$$%=~ **
+ x+**o~ ~~**xx
+ xx**+= ++=***=*+x x+*=***=++ ==**xx
+ ++==*%%%%%**== ++++*%%%%%%*++++ ==**%%%%%*==++
+
diff --git a/src/build/framegen/frames/frame_177.txt b/src/build/framegen/frames/frame_177.txt
new file mode 100644
index 0000000000..828afeb638
--- /dev/null
+++ b/src/build/framegen/frames/frame_177.txt
@@ -0,0 +1,41 @@
+
+
+
+ x++++++x
+ +===*%%%%%****%%%%%*===+
+ ++===*+= =+*===++
+ ===* *===
+ x+**x+ ·+*$@@@@@@@@@@@@$*+· xx**++
+ ++*= +$@@@@@@$$$$$$$$$$$$@@@@@@$+ =*++
+ ++== ·%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%· =*++
+ ++== =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ==++
+ **o· =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ·o**
+ ++++ ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ++++
+ =* +@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ *=
+ x+++ =@$$$$@%==$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= +++x
+ +++· ·@$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ·+++
+ == @$$$$@$ =$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == @$$$$$@$o ·%$$$$$@+ o$$$$$$@ ==
+ == ·@$$$$$$$@@@$o $$$$@ @$$$$@· ==
+ == ·@$$$$$$@@*o $$$$$= o@$$$$@· ==
+ == ·@$$$$$$· =@@$$$$$@@%**************%@@$$$$$@· ==
+ == ·@$$$$$$ =$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$= +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x ·$@@@@$$$@@@@@*xoo+$@@@@@$$@@@@@$+oox*@@@@@$$$@@@@@~ x+++
+ ** ~=%$$$$*o +%$$$$%+ o*$$$$%*o **
+ xx**~~ **++
+ ++**+= ++==**==x~ ~x==**==++ =+**+x
+ ++==*%%%%%%*== x+==*%%%%%%*==++ ==**%%%%%*==++
+
diff --git a/src/build/framegen/frames/frame_178.txt b/src/build/framegen/frames/frame_178.txt
new file mode 100644
index 0000000000..30b75b9a6e
--- /dev/null
+++ b/src/build/framegen/frames/frame_178.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++++++++
+ ++++***%%%==++++==%%%***++++
+ ++**=*+o o+*=**++
+ x+**== ==**+x
+ ++** x*$@@@@@@@@@@@@@@$*x **++
+ ++*+ o%@@@@@@$$$$$$$$$$$$$$@@@@@@%o +*++
+ ++=+ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x +=++
+ ++== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ==++
+ ** %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% **
+ +++x x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x x+++
+ == *@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==
+ xx++ *@$$$$@=ox*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++xx
+ ==+· o@$$$$$ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ·+==
+ == @$$$$$$ o%@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@= *$$$$$$~ %$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@$+ ~$$$$$* +@$$$$@· ==
+ == ·@$$$$$$ x%@@$$$$$@@@$$$$$$$$$$$$$$@@@$$$$$@· ==
+ == ·@$$$$@$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@%· o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++x ~@@@@@$$$@@@@@%+xx=$@@@@$$$$@@@@$=xo+*@@@@@$$$@@@@@o x+++
+ ** o*$$@$$%x ·=%$@@$%=· x%$@@$$*x **
+ x+** **+x
+ xx**+= xx****== ==***=xx =+**++
+ ++==*%%%%***++xx ++==*%*%%*%*==++ xx++*%*$%%%*==++
+
diff --git a/src/build/framegen/frames/frame_179.txt b/src/build/framegen/frames/frame_179.txt
new file mode 100644
index 0000000000..157ef6e500
--- /dev/null
+++ b/src/build/framegen/frames/frame_179.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++++++++
+ ++==*%*%==++xxxx++==%*%*==++
+ ++**=* *=**++
+ ++**+= =+**++
+ ++== o=%@@@@@@@@@@@@@@@@%=o ==++
+ ===+ +$@@@@@$$$$$$$$$$$$$$$$@@@@@$+ +===
+ ++=+ =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= +=++
+ ++=+ $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ +=++
+ ** $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ **
+ +++o =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= o+++
+ == $@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ x+++ %@$$$@@x ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% +++x
+ ==+· x@$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+==
+ == @$$$$$$ ·=@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@*~ +$$$$$% *$$$$$$ ==
+ == ·@$$$$$$$@@@@= $@$$@ @$$$$@· ==
+ == ·@$$$$$$@%o x$$$$$% =$$$$$@· ==
+ == ·@$$$$$$ +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@$o +$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++x ~@@@@@$$$$@@@@%+xx=$@@@@$$$$@@@@$=xx+%@@@@$$$$@@@@@o x+++
+ ** x*$$@@$%x ~=%$@@$%=~ x%$@@@$%x **
+ x+** **+x
+ ++**+= xx****== ==****xx =+**++
+ ++==*%%%%*%*=+++ ++==*%*%%*%*==++ +++=*%*%%%%**=++
+
diff --git a/src/build/framegen/frames/frame_180.txt b/src/build/framegen/frames/frame_180.txt
new file mode 100644
index 0000000000..01fa033f26
--- /dev/null
+++ b/src/build/framegen/frames/frame_180.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++===***==++++
+ ==***%==xo ox==%***==
+ ===*++ ++*===
+ ++**++ ·oxx++xxo· ++**++
+ ===+ o=$@@@@@@@@@@@@@@@@@@$=o +===
+ **++ o%@@@@@$$$$$$$$$$$$$$$$$$@@@@@%o ++**
+ ==+x ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ x+==
+ ++++ x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x ++++
+ xx== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xx
+ ==x~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~x==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@% ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==x =$$$$@$ ~*@@@@$$$$$$$$$$@@@@@@@@@@@@@@$$$$$$$$= x==
+ == @$$$$$$ o*@@$$$$$@@@$$$$$$$$$$$$$$$@@$$$$$@ ==
+ == $$$$$$$@$= ·$$$$$* x@$$$$$ ==
+ == ~@$$$$$$$@@@@x $$$$@ @$$$$@· ==
+ == ·@$$$$$@@+ *$$$$$$o %$$$$$@· ==
+ == ·@$$$$$$ x%@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++x ~@@@@@$$$$@@@@%+xx=$@@@@$$$$@@@@$=xx+%@@@@$$$$@@@@@o o+++
+ ** x*$@@@$%x ~=%$@@$%=~ x%$@@@$%x **
+ x+** **++
+ x+**+= x+****== ==****xx =+**++
+ ++***%%%%*%*==++ ++==*%*%%*%*==++ x+==*%*%%%%**=++
+
diff --git a/src/build/framegen/frames/frame_181.txt b/src/build/framegen/frames/frame_181.txt
new file mode 100644
index 0000000000..ea015b6282
--- /dev/null
+++ b/src/build/framegen/frames/frame_181.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++=***%%%%**==++++
+ ++==*%**+x x+**%*==++
+ ++**=* *=**++
+ ==== ox=******=xo ====
+ **++ ·=$@@@@@@@@@@@@@@@@@@@@$=· ++**
+ **+o =@@@@@$$$$$$$$$$$$$$$$$$$$@@@@@= o+**
+ **x~ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ~x**
+ +++x *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* x+++
+ xx== +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ==xx
+ ==x· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==
+ ox== @$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xo
+ +++o @$$$$$+ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ == %$$$$@$ +%@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$% ==
+ == @$$$$$$~ +$@$$$$$@@%==============*@@$$$$$@ ==
+ == $$$$$$$@@%x $$$$@+ ·@$$$$$ ==
+ == ~@$$$$$$$@@@%o $$$$@· @$$$$@~ ==
+ == ·@$$$$$@$~ o$$$$$$@= x@$$$$$@· ==
+ == ·@$$$$@$ ~*@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ ·*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@$**$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++x ~@@@@@$$$@@@@@%+xx=$@@@@$$$$@@@@$=xx+%@@@@$$$$@@@@@o x+++
+ ** o*$$@@$%x ·=%$@@$%=· x%$@@$$*x **
+ x+** **+x
+ ++**+= xx****== ==****xx =+**++
+ ++==*%%%%*%*=+xx ++==*%%%%%%*==++ xx++*%*%%%%**=++
+
diff --git a/src/build/framegen/frames/frame_182.txt b/src/build/framegen/frames/frame_182.txt
new file mode 100644
index 0000000000..3545521c6f
--- /dev/null
+++ b/src/build/framegen/frames/frame_182.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++=**%%%%%%%%%%**=++++
+ +=***%+x x+%***=+
+ ++**+= =+**++
+ ox**+= o=*%$@@@@@@$%*=o =+**xo
+ ++**oo ·=$@@@@@@@$$$$$$$$@@@@@@@$=· oo**++
+ x+** x$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$x **+x
+ ox** ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· **xo
+ ==+~ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ==
+ xx++ ~@$$$$@@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ++xx
+ +++~ @$$$$$ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$@% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@* +@$$$$$@%x~~~~~~~~~~~~~~o*@$$$$$@ ==
+ == ·$$$$$$$@@@$+ $$$$@~ @$$$$$· ==
+ == ·@$$$$$$@@@*o $$$$@x @$$$$@· ==
+ == ·@$$$$$@+ ~*@$$$$$@$+xxxxxxxxxxxxxxx%@$$$$$@· ==
+ == ·@$$$$@% ·=@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$~ ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x ·$@@@@$$$@@@@@*xox=$@@@@$$$$@@@@$=oox*@@@@@$$$@@@@@~ x+++
+ ** o*%$$$$*x ·+%$$$$%+· x*$$$$$*o **
+ x+**~ **+x
+ x+**+= +x*=**==x ==**==x+ =+**++
+ ++==*%*%%%**++xx ++==*%%%%%%*==++ xx++**%%%*%*==++
+
diff --git a/src/build/framegen/frames/frame_183.txt b/src/build/framegen/frames/frame_183.txt
new file mode 100644
index 0000000000..5368c629ab
--- /dev/null
+++ b/src/build/framegen/frames/frame_183.txt
@@ -0,0 +1,41 @@
+
+
+ ++++
+ ++==*%%%*%****%*%%%*==++
+ x+==*%== ==%*==+x
+ ===*xo ox*===
+ ++**x+ x*%$@@@@@@@@@@$%*x +x**++
+ ++*= +$@@@@@@$$$$$$$$$$$$@@@@@@$+ =*++
+ ++== *@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* ==+x
+ xx== +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ==xx
+ **o· +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·o**
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ ** +@$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ *=
+ xx++ +@$$$$@%=*$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ++xx
+ +++~ @$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·+++
+ == @$$$$$$ ·=@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == @$$$$$@$o ~$$$$$$@+ x@$$$$$@ ==
+ == ·@$$$$$$$@@@$o $$$$@· @$$$$@· ==
+ == ·@$$$$$$@@%o $$$$$+ ~@$$$$@· ==
+ == ·@$$$$$$~ +$@$$$$$@@%**************%@@$$$$$@· ==
+ == ·@$$$$@$ +$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$@= +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x $@@@@@$$@@@@@*xoo+%@@@@@$$@@@@@%+o~x=@@@@@$$@@@@@$· x+++
+ **o ~=%$$$%*o +%$$$$%+ o*%$$$%=~ **
+ x+**~~ ~**+x
+ x+**== ++==**==+o ox==**==++ =+**++
+ ++==*%%%%%**++xx ++==*%%%%%%*==++ x+++**%%%%%**=++
+
diff --git a/src/build/framegen/frames/frame_184.txt b/src/build/framegen/frames/frame_184.txt
new file mode 100644
index 0000000000..f92a332be4
--- /dev/null
+++ b/src/build/framegen/frames/frame_184.txt
@@ -0,0 +1,41 @@
+
+
+ x ++++====++++
+ ++==*%*%==++++++++==%*%*==++
+ ++**=* *=**++
+ x+**+= ·· =+**+x
+ ++== o=%@@@@@@@@@@@@@@@@%=o ==++
+ ===+ +$@@@@@$$$$$$$$$$$$$$$$@@@@@$+ +===
+ ++++ =@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ++++
+ ++=+ $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ +=++
+ ** $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ **
+ +++o =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= o+++
+ == $@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ ++++ $@$$$@@x ·+$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ ==+· +@$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+==
+ == @$$$$$$ =$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$$@*o +$$$$$% *$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@%o x$$$$$% =$$$$$@· ==
+ == ·@$$$$$$ =@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@$o +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@= ==
+ ++++ %@@@@@@@@@@@$+o·~x*@@@@@@@@@@@@*o··~+$@@@@@@@@@@@% x+++
+ **x· +*%%%*+· o=%%%%=o ·+%%$%*+ ·o**
+ x+**o~ oo oo oo**++
+ x+**+=+~ =+******+x x+******+= ~x=+**++
+ ++==**%%%***++xx ++==***%%***==++ xx++***%%%**==++
+
diff --git a/src/build/framegen/frames/frame_185.txt b/src/build/framegen/frames/frame_185.txt
new file mode 100644
index 0000000000..66aaba24cf
--- /dev/null
+++ b/src/build/framegen/frames/frame_185.txt
@@ -0,0 +1,41 @@
+
+
+ ++++==****==++++
+ ==***%==xo ox==%***==
+ ===*++ ++*===
+ ++**x+ ·oxx++xxo· +x**++
+ ===+ o*$@@@@@@@@@@@@@@@@@@$*o +===
+ **++ o%@@@@@$$$$$$$$$$$$$$$$$$@@@@@%o x+**
+ ==+o ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ o+==
+ ++++ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ++++
+ ox== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xo
+ ==+~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@* ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==x *$$$$$$ ~*@@@@$$$$$$$$$$@@@@@@@@@@@@@@$$$$$$$$* x==
+ == @$$$$$$ ~*@@$$$$$@@$%%%%%%%%%%%%%%$@@$$$$$@ ==
+ == $$$$$$$@@= ·$$$$$* x$$$$$$ ==
+ == ~@$$$$$$$@@@@x $$$$@ @$$$$@~ ==
+ == ·@$$$$$@@+ *$$$$$$o ·$$$$$$@· ==
+ == ·@$$$$$$ x$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@+ ==
+ ++++ *@@@@@@@@@@@%x· ~=@@@@@@@@@@@@=~ x%@@@@@@@@@@@* ++++
+ **x· o=*%%=x ~+*%%*+~ x=%%%=x ·x**
+ x+**xo ++ ++ ox**+x
+ x+**=*xx *+****=*++ ++==****+* x+==**++
+ ++==********++xx ++==********==++ xx++********==++
+
diff --git a/src/build/framegen/frames/frame_186.txt b/src/build/framegen/frames/frame_186.txt
new file mode 100644
index 0000000000..d49b01d10e
--- /dev/null
+++ b/src/build/framegen/frames/frame_186.txt
@@ -0,0 +1,41 @@
+
+
+ +++=**%%%%%%%%**=+++
+ ++***%==o· ·o==%***++
+ ++**+* *+**++
+ ==== o+=*%$$$$%*=+o ====
+ x+**xx x*@@@@@@@@@@@@@@@@@@@@@@*x xx**+x
+ xx**~~ ~%@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@%~ ~~**xx
+ **x· *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ·o**
+ ==+o %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% o+==
+ xx== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==xx
+ ==x· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==
+ xx== @$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xx
+ +++o @$$$$$o ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ == %$$$$@% ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$% ==
+ == @$$$$$$+ o*@$$$$$@$=+++++++++++++++%@$$$$$@ ==
+ == $$$$$$$@@@=~ $$$$$x @$$$$$ ==
+ == ·@$$$$$$@@@@= $$$$@~ @$$$$@· ==
+ == ·@$$$$$@% +@$$$$$@%~··············~=@$$$$$@· ==
+ == ·@$$$$@% x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@o ==
+ ++++ +@@@@@@@@@@@=~ x$@@@@@@@@@@$x ·=@@@@@@@@@@@+ ++++
+ **+~ o+=+x· o+==+o ·x+=+x ~x**
+ xx**+x == == x+**+x
+ xx**==+= ~x*=******+* *+******=*x~ =+==**xx
+ ++==********++ x+++********+++x ++********==++
+
diff --git a/src/build/framegen/frames/frame_187.txt b/src/build/framegen/frames/frame_187.txt
new file mode 100644
index 0000000000..a3fe2f9cb9
--- /dev/null
+++ b/src/build/framegen/frames/frame_187.txt
@@ -0,0 +1,41 @@
+
+
+ ++==**%%*%%%%%%*%%**==++
+ ==***%xx xx%***==
+ ++**++ ++**++
+ xx**++ x=*$$@@@@@@$$*=x ++**+x
+ ++**~o ~*$@@@@@@@$$$$$$$$@@@@@@@$*~ oo**++
+ ++** +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ **++
+ xx** ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ **xx
+ ==+~ ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@$$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ x+++ o@$$$$@@%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o +++x
+ +++~ @$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$$$ x$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@* +@$$$$$@%o··············~=@$$$$$@ ==
+ == ·$$$$$$$@@@$= $$$$@~ @$$$$$· ==
+ == ·@$$$$$$@@$*o $$$$@x @$$$$@· ==
+ == ·@$$$$$$+ ~*@$$$$$@$=xxxxxxxxxxxxxx+%@$$$$$@· ==
+ == ·@$$$$$$ ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$o ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==+· @@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@ x==
+ ++=+ o$@@@@@@@@@@+ ~*@@@@@@@@@@*~ +@@@@@@@@@@$x ++++
+ ==+x ·oxo~ ~oo~ ~oxo· o+*=
+ x+**+x =* *= ++**xx
+ x+==**+*+o ++*=**==**=*+x x+*=**==**=*++ ~+*=**==xx
+ ++++******==++ ++++********++++ ++==******++++
+
diff --git a/src/build/framegen/frames/frame_188.txt b/src/build/framegen/frames/frame_188.txt
new file mode 100644
index 0000000000..383a1143f4
--- /dev/null
+++ b/src/build/framegen/frames/frame_188.txt
@@ -0,0 +1,41 @@
+
+ xx++++xx
+ ===*%%%%*%****%*%%%%*===
+ ++**=%++ ++%=*=++
+ ==** **==
+ x+**+x ~+*$@@@@@@@@@@@@$*+~ x+**+x
+ ++== =$@@@@@$$$$$$$$$$$$$$@@@@@$= ==++
+ ++*= ~%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%~ =*++
+ x+== =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ==+x
+ **o =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= o*=
+ ++++ ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ++++
+ == =@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==
+ x+++ =@$$$$@%+=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ++xx
+ +++· ·@$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ·+++
+ == @$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == $$$$$$@@x %$$$$$@x ~$$$$$$$ ==
+ == ·@$$$$$$$@@@$x $$$$@ @$$$$@· ==
+ == ·@$$$$$$@@*~ $$$$$= o$$$$$@· ==
+ == ·@$$$$$$ =$@$$$$$@@$%%%%%%%%%%%%%%%@@$$$$$@· ==
+ == ·@$$$$@% =@@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$@* =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==x· $@@$$$$$$$$$@@@@@@@@@@$$$$$$$$@@@@@@@@@@$$$$$$$$$@@$ ·+==
+ ++++ =@@@@@@@@@*· x$@@@@@@@@$x *@@@@@@@@@= ++++
+ ==++ +===
+ xx=*=+ ++%%xx xx%%++ +===xx
+ ==**=*==++++*=*=**==**=%=*++++*=%***==**=*=*++++==*=**==xx
+ ++++==****==++ ++==****==++ ++==****==++++
+
diff --git a/src/build/framegen/frames/frame_189.txt b/src/build/framegen/frames/frame_189.txt
new file mode 100644
index 0000000000..6fedc5596a
--- /dev/null
+++ b/src/build/framegen/frames/frame_189.txt
@@ -0,0 +1,41 @@
+
+ xx++++++++xx
+ x+==*%*%*%========%*%*%*=++x
+ ++**=*+x x+*=**++
+ xx**=* ==**xx
+ ++**o x*$@@@@@@@@@@@@@@$*x o**++
+ ++=+ o*@@@@@$$$$$$$$$$$$$$$$@@@@@*o +=++
+ ++=+ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x +=++
+ x+== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==+x
+ ** %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% **
+ +++x x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x x+++
+ =* *@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* *=
+ ++++ *@$$$@@=ox*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++++
+ ==+· o@$$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ·+==
+ == @$$$$$$ o%@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@= *$$$$$$~ %$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@$+ ~$$$$$* +$$$$$@· ==
+ == ·@$$$$$$ o%@@$$$$$@@@$$$$$$$$$$$$$$$@@$$$$$@· ==
+ == ·@$$$$@$ o%@@@@$$$$$$$$$$@$$$$$$$$$$$$@$$$$$$$$@· ==
+ == ·@$$$$$$% o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ == @@$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$@@ ==
+ ==+~ *@@@$$$$$$$@@@@$%%@@@@$$$$$$$$@@@@%%$@@@@$$$$$$$@@@* ~+==
+ ++== o%@@@@@@@$x ·*@@@@@@@@*· x$@@@@@@@%x ==++
+ ===+ +===
+ ox====o+ ==%%++ ++%%== +o====+x
+ ++**=**%****%***==++**=*=******=*=**++==***%****%*****++
+ xx+++=====++++ ++++====++++ ++++======+++x
+
diff --git a/src/build/framegen/frames/frame_190.txt b/src/build/framegen/frames/frame_190.txt
new file mode 100644
index 0000000000..95209a1f39
--- /dev/null
+++ b/src/build/framegen/frames/frame_190.txt
@@ -0,0 +1,41 @@
+
+ xx++++====++++xx
+ ++==*%**==++oooo++==**%*==++
+ ++**=* *=**++
+ ++**+= ···· =+**++
+ ++== x=$@@@@@@@@@@@@@@@@$=x ==++
+ ==++ =$@@@@$$$$$$$$$$$$$$$$$$@@@@$= ++==
+ ===+ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* +===
+ ++=+ ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· +=++
+ ox** $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ **xo
+ +++o =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= o+++
+ == $@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ ++++ $@$$$@$x +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ ==+· +@$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+==
+ == @$$$$$$ =$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$$@%o x$$$$$% =$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@%o +$$$$$% *$$$$$@· ==
+ == ·@$$$$$$ =$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ =@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@$x ·+$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ ==+x ~@@@@$$$$$@@@@%+x+=$@@@@$$$$@@@@$=+x+%@@@@$$$$$@@@@o o+==
+ xx== x*$@@@$%x ~=$@@@@$=~ x%$@@@$*x ==xx
+ ==== ====
+ ====+= xx*=**== ==**=*xx =+====
+ ++*****%*%%*****==++==***%*%%*%***==++==*****%%%%*****++
+ ++++++++++++ x++++++++++x ++++++++++++
+
diff --git a/src/build/framegen/frames/frame_191.txt b/src/build/framegen/frames/frame_191.txt
new file mode 100644
index 0000000000..7da0266e4d
--- /dev/null
+++ b/src/build/framegen/frames/frame_191.txt
@@ -0,0 +1,41 @@
+
+ ++====****====++
+ ==**%%=*++oo····~o++*=%%**++
+ +=**== ==**=+
+ ++**++ ·~~oo~~· ++**++
+ ===+ +%@@@@@@@@@@@@@@@@@@%+ +===
+ ===+ ·*@@@@@$$$$$$$$$$$$$$$$$$@@@@@*· ++==
+ ===x %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% x===
+ ++=+ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o +=++
+ xx== @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xx
+ ==+~ *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ~+==
+ == $@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ +++x $$$$$@$~ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ x+++
+ ==x· =@$$$@$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·x==
+ == @$$$$$$ x$@@$$$$$@@@$$$$$$$$$$$$$$@@@$$$$$@ ==
+ == $$$$$$$@$x o$$$$$% +$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@=· =$$$$$$ %$$$$$@· ==
+ == ·@$$$$$$ ~*@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@+~o=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@= ==
+ ==++ *@@@@@@@@@@@$+~ o*@@@@@@@@@@@@*o ·+$@@@@@@@@@@@% x+==
+ ++==x· x=%%%*x o=*%%*=o x*%%%*x ·x==++
+ ====oo xx xx ~o====
+ ====+=++ =+*==*==++ ++=====*+= ox=+====
+ ++==**=*********++++==***%****%***==++++*********=**==++
+ ++++++++++xx ++++++++++++ xx++++++++++
+
diff --git a/src/build/framegen/frames/frame_192.txt b/src/build/framegen/frames/frame_192.txt
new file mode 100644
index 0000000000..f03d9280bf
--- /dev/null
+++ b/src/build/framegen/frames/frame_192.txt
@@ -0,0 +1,41 @@
+
+ ++====****====++
+ +=***%==+o o+==%***=+
+ ===*++ ++*===
+ ++**x+ ·ooxxxxoo· ++**++
+ ===+ o=$@@@@@@@@@@@@@@@@@@$=o +===
+ **++ o%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%o x+**
+ ==+x ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ x+==
+ ++++ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ++++
+ ox== ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==xo
+ ==+~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~x==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@% ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==+ =@$$$@$ ~*@@@@$$$$$$$$$$@@@@@@@@@@@@@@$$$$$$$@= +==
+ == @$$$$$$ o%@@$$$$$@@$$$$$$$$$$$$$$$$@@$$$$$@ ==
+ == $$$$$$$@$=· ·$$$$$* x$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$@@+ *$$$$$$o $$$$$$@· ==
+ == ·@$$$$$$ x%@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@=x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@ x==
+ ++++ o$@@@@@@@@@$+ ·*@@@@@@@@@@*· +$@@@@@@@@@$o ++++
+ x+==+x ·ooo~ ~oo~ ~ooo· o+==+x
+ ++==++ +** **+ x+==++
+ ++====+=+x ++*========*+x x+*========*++ x+======++
+ ++++==********==++++++==********==++++++==********====++
+ ++++++++ ++++++++ ++++++++
+
diff --git a/src/build/framegen/frames/frame_193.txt b/src/build/framegen/frames/frame_193.txt
new file mode 100644
index 0000000000..eea662acae
--- /dev/null
+++ b/src/build/framegen/frames/frame_193.txt
@@ -0,0 +1,41 @@
+
+ ++====****====++
+ x++=***%==x~ ~x==%***=+xx
+ x+===*++ ++*===+x
+ ++**+x ~ox++++xo~ x+**++
+ ==++ x*$@@@@@@@@@@@@@@@@@@$*x ++==
+ **+x x%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%x x+**
+ ==+o o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o o+==
+ ++++ +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ++++
+ xx== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xx
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ x+== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@* =@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==x *$$$$@% ~=@@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* x==
+ == @$$$$$$ ~*@@$$$$$@@$%%%%%%%%%%%%%%$@@$$$$$@ ==
+ == $$$$$$$@@=~ $$$$$= o$$$$$$ ==
+ == ~@$$$$$$$@@@@+ $$$$@ @$$$$@~ ==
+ == ·@$$$$$@@x %@$$$$$x ~$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@*++%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· $@@$$$$$$$$$@@@@@@@@@@$$$$$$$$@@@@@@@@@@$$$$$$$$$@@$ ·x==
+ ++++ =@@@@@@@@@* x$@@@@@@@@$x *@@@@@@@@@= ++++
+ ox==++ ++==xx
+ ++===+ ++%%+o o+%%++ +===++
+ ++=====%+=++++*=*========***++++***========*=*++++=+%=====++
+ xx++==******====++ ++==********==++ ++====******==++xx
+ ++++ ++++ ++++xx
+
diff --git a/src/build/framegen/frames/frame_194.txt b/src/build/framegen/frames/frame_194.txt
new file mode 100644
index 0000000000..1d1213448f
--- /dev/null
+++ b/src/build/framegen/frames/frame_194.txt
@@ -0,0 +1,41 @@
+
+ xx++==********===+xx
+ x+==*%**=+~· ·~+=**%*==++
+ x+=*=*+x x+*=*=+x
+ ++**xo ·ox+====+xo· ox**++
+ **++ x*@@@@@@@@@@@@@@@@@@@@*x ++**
+ **+x x$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$x o+**
+ ==+o x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x o+==
+ ++++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++++
+ xx== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xx
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@* =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == *$$$$@% =@@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* ==
+ == @$$$$$$ =$@$$$$$@@$%%%%%%%%%%%%%%%@@$$$$$@ ==
+ == $$$$$$$@@*~ $$$$$= o$$$$$$ ==
+ == ~@$$$$$$$@@@$x $$$$@ @$$$$@~ ==
+ == ·@$$$$$@@x %$$$$$@x ~$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@%+=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == $@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@$ ==
+ ==+o +@@@$$$$$$$@@@$***%@@@@$$$$$$@@@@%***$@@@$$$$$$$@@@+ o+==
+ ++== =$@@@@@$*~ +%@@@@@@%+ ~*$@@@@@$=· ==++
+ ===+ +===
+ ++====x+ *=**== =+**=* +x====+x
+ ++=====**%*****==========**%****%**=========******%**=====+x
+ ++++========++xx ++++========++++ xx++========++++
+
+
diff --git a/src/build/framegen/frames/frame_195.txt b/src/build/framegen/frames/frame_195.txt
new file mode 100644
index 0000000000..7f66e31460
--- /dev/null
+++ b/src/build/framegen/frames/frame_195.txt
@@ -0,0 +1,41 @@
+
+ ++++==********===+++
+ x+==*%**++~· ·~+=**%*==++
+ xx**=%+x x+%=**xx
+ ++*=x ~ox+====+xo~ ox=*++
+ **++ x%@@@@@@@@@@@@@@@@@@@@%x ++**
+ **+o +$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$+ o+**
+ =*+o x$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$x o+*=
+ ++++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++++
+ ox== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xo
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xo
+ +++x @$$$$@= +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == *$$$$@% +@@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* ==
+ == @$$$$$$· =$@$$$$$@@%%%%%%%%%%%%%%%%@@$$$$$@ ==
+ == $$$$$$$@@*o $$$$$= ~$$$$$$ ==
+ == ·@$$$$$$$@@@$x $$$$@ @$$$$@· ==
+ == ·@$$$$$@@o ·%$$$$$@+ o$$$$$$@· ==
+ == ·@$$$$$$ =$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@%==$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@% ==
+ ==+x ~$@@@@$$$$@@@@%+xx=$@@@@$$$$@@@@$=xx+*@@@@$$$$@@@@@~ x+==
+ ++== o*$@@@$*x ·=%$@@$%=· x*$@@@$*x ==++
+ ==== =+==
+ xx====+= x+*===== ======+x =+====xx
+ ++====*%%%%**=====++====*%*%%*%*====++=====**%%%%*====++
+ xx++++====++++ ++++====++++ ++++====++++x+
+
+
diff --git a/src/build/framegen/frames/frame_196.txt b/src/build/framegen/frames/frame_196.txt
new file mode 100644
index 0000000000..9f6a2941d6
--- /dev/null
+++ b/src/build/framegen/frames/frame_196.txt
@@ -0,0 +1,41 @@
+
+ ++++==********===+++
+ x+==*%**++~· ·~++**%*==++
+ xx**=%+x x+%=**xx
+ ++==x ~ox+====+xo~ x==++
+ **++ +%@@@@@@@@@@@@@@@@@@@@%+ ++**
+ **+o +$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$+ o+**
+ ==+~ x$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$x ~+*=
+ ++++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++++
+ ox== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xo
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xo
+ +++x @$$$$@= +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == *$$$$@% +$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* ==
+ == @$$$$$$· =$@$$$$$@@%%%%%%%%%%%%%%%%@@$$$$$@ ==
+ == $$$$$$$@@*o $$$$$= ~$$$$$$ ==
+ == ~@$$$$$$$@@@$x $$$$@ @$$$$@~ ==
+ == ·@$$$$$@@o ·$$$$$$@+ o$$$$$$@· ==
+ == ·@$$$$$$ =$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@%==$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@+ ==
+ ==++ *@@@@@@@@@@@%x ~=@@@@@@@@@@@@=~ x%@@@@@@@@@@@% ++==
+ xx==+· o=***=x ~+****+~ x=***=x ·x==++
+ ====xo xx x+ ox====
+ ======+x =+=*====++ ++====*=+= x+*=====
+ ++==***%********++++==************==++++********%***==++
+ ++++++++++ +x++++++++x+ ++++++++++
+
+
diff --git a/src/build/framegen/frames/frame_197.txt b/src/build/framegen/frames/frame_197.txt
new file mode 100644
index 0000000000..d7b48f2c1f
--- /dev/null
+++ b/src/build/framegen/frames/frame_197.txt
@@ -0,0 +1,41 @@
+
+ x+++==********===+xx
+ x+==*%**++~· ·~++**%*==++
+ xx**=*+x x+*=**xx
+ ++**xo ·ox+====+xo· ox=*++
+ **++ x%@@@@@@@@@@@@@@@@@@@@%x ++**
+ **+o +$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$+ o+**
+ =*+o x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x o+*=
+ ++++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++++
+ ox== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xo
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@* +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == *$$$$@% =@@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* ==
+ == @$$$$$$· =$@$$$$$@@$%%%%%%%%%%%%%%%@@$$$$$@ ==
+ == $$$$$$$@@*o $$$$$= ~$$$$$$ ==
+ == ·@$$$$$$$@@@$x $$$$@ @$$$$@· ==
+ == ·@$$$$$@@o ·%$$$$$@+ o$$$$$$@· ==
+ == ·@$$$$$$ =$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@%+=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@o ==
+ ++++ +@@@@@@@@@@@*~ x$@@@@@@@@@@$x ·*@@@@@@@@@@@= ++++
+ ox==+o x+=+x· o+==+o ·x+=+x· ~+==+x
+ ++==+x == == xx==++
+ ++====++ ox*+==**==+= =+=***==+*xo ++====++
+ ++==**********==++++++************++++++==**********==++
+ ++++++++ xx++++xx ++++++++
+
+
diff --git a/src/build/framegen/frames/frame_198.txt b/src/build/framegen/frames/frame_198.txt
new file mode 100644
index 0000000000..4195da5e9d
--- /dev/null
+++ b/src/build/framegen/frames/frame_198.txt
@@ -0,0 +1,41 @@
+
+ xx++==********===+xx
+ x++=****==x· ·x==****=+++
+ x+=*=*+x x+*=*=+x
+ ++**+x ~x++==++x~ x+**++
+ **++ x*@@@@@@@@@@@@@@@@@@@@*x ++**
+ **+x x%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%x xx**
+ ==+o o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o o+==
+ ++++ +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ++++
+ xx== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==xx
+ ==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@* =@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == *$$$$@% ·=@@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* x==
+ == @$$$$$$ ·*@@$$$$$@@$%%%%%%%%%%%%%%%@@$$$$$@ ==
+ == $$$$$$$@@*~ $$$$$= o$$$$$$ ==
+ == ~@$$$$$$$@@@@+ $$$$@ @$$$$@~ ==
+ == ·@$$$$$@@x %$$$$$@x ~$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ +$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@*+=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·x==
+ ++++ ~%@@@@@@@@@$x ·*@@@@@@@@@@*· x$@@@@@@@@@%o ++++
+ ox==+x ~oo· ~oo~ ·ooo x+==xo
+ ++==++ +%% %%+o ++==++
+ ++=*===*xx ~ ++*=**==**+*++ ++*=**==**=*++ ~ xx*===*=++
+ ++==********++xx ++==********==++ xx++********==++
+
+
+
diff --git a/src/build/framegen/frames/frame_199.txt b/src/build/framegen/frames/frame_199.txt
new file mode 100644
index 0000000000..d926eed9a2
--- /dev/null
+++ b/src/build/framegen/frames/frame_199.txt
@@ -0,0 +1,41 @@
+
+ x+++===******====++x
+ x+==***%=*xo ox==%***==++
+ xx=*=*++ ++*=*=xx
+ ++**++ ~ox++++xo~ ++**++
+ **++ o*$@@@@@@@@@@@@@@@@@@$*o ++**
+ **+x o%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%o x+**
+ ==+o ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ o+==
+ ++++ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ++++
+ ox== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xo
+ ==+~ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ~+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@* ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==+ *@$$$@% ~*@@@@$$$$$$$$$$@@@@@@@@@@@@@@@$$$$$$@* x==
+ == @$$$$$$ ~*@@$$$$$@@$%%%%%%%%%%%%%%$@@$$$$$@ ==
+ == $$$$$$$@$=· $$$$$* x$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$@@+ %@$$$$$o ·$$$$$$@· ==
+ == ·@$$$$$$ x$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==+· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·+==
+ ++++ %@@@@@@@@@%o =$@@@@@@@@$= ~%@@@@@@@@@%· ++++
+ ==+x ·· ·· ··· x+==
+ x+==++ ++%% %*++ ++*=xx
+ xx==**=*++xxxx+=*=**==**=%=+xxxx+=*=**==**=*=+xxoo++*=**==xx
+ ++++******==++xx ++++********++++ xx++==******==++
+
+
+
diff --git a/src/build/framegen/frames/frame_200.txt b/src/build/framegen/frames/frame_200.txt
new file mode 100644
index 0000000000..86ebca6d11
--- /dev/null
+++ b/src/build/framegen/frames/frame_200.txt
@@ -0,0 +1,41 @@
+
+ xx++=====**=====++xx
+ +++=***%**++~· ·~++**%***++++
+ xx==*%=+ +=%*==xx
+ ++**++ ~~oooo~~ ++**++
+ ==== ·+%@@@@@@@@@@@@@@@@@@%+· ====
+ **+x ~*@@@@@$$$$$$$$$$$$$$$$$$@@@@@*~ x+**
+ ==+x ·%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%· x+==
+ ++=+ x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x +=++
+ ox== ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ==xo
+ ==+~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x $$$$$@%· o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ x+++
+ ==+· =@$$$@$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+==
+ == @$$$$$$ x%@@$$$$$@@@$$$$$$$$$$$$$$$@@$$$$$@ ==
+ == $$$$$$$@$+ ~$$$$$* +$$$$$$ ==
+ == ~@$$$$$$$@@@@+ $$$$@ @$$$$@~ ==
+ == ·@$$$$$$@= *$$$$$$· %$$$$$@· ==
+ == ·@$$$$$$ o%@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@=ox*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· $@@$$$$$$$$$@@@@@@@@@@$$$$$$$$@@@@@@@@@@$$$$$$$$$@@$ ·+==
+ +++= =$@@@@@@@@* x%@@@@@@@@%x *@@@@@@@@@* ++++
+ ==++ +===
+ xx=*=+ ++%%+x x+%%++ +=**+x
+ ++**=*=+++++=**=**++**=%==++++==%=**++**=**=+++++=*=**++
+ xx++======++++ ++========++ ++++==**==++xx
+
+
+
diff --git a/src/build/framegen/frames/frame_201.txt b/src/build/framegen/frames/frame_201.txt
new file mode 100644
index 0000000000..65a82d3bc1
--- /dev/null
+++ b/src/build/framegen/frames/frame_201.txt
@@ -0,0 +1,41 @@
+
+ xx++============++xx
+ ++++**%%**==xo~~~~ox==**%%**++++
+ xx==**== ==**==xx
+ ++**+= ·~~~~· =+**++
+ ===+ x*$@@@@@@@@@@@@@@@@$*x +===
+ **++ =@@@@@$$$$$$$$$$$$$$$$$$@@@@@= ++**
+ ==++ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ++==
+ ++=+ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ +=++
+ xx== @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ ==+o *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* o+==
+ ox== $@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==xo
+ +++x $@$$$@$o x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ x+++
+ ==+· +@$$$$$ +$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+==
+ == @$$$$$$ +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$$@%x o$$$$$% =$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@*~ +$$$$$$ *$$$$$@· ==
+ == ·@$$$$$$ ·*@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@+·~=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@$$$$$$$$$@@@@$$@@@@$$$$$$$$@@@@$$@@@@$$$$$$$$$@@$ ·+==
+ x+== +$@@@@@@@$* o%@@@@@@@@%o *$@@@@@@@$= ==++
+ ==++ ++==
+ ===+ ==%%++ ++%%== +===
+ ++**=%==++++****==++**=***++++***=**++==****++++==%***++
+ ++======++++ x++======+++ ++++======++
+
+
+
diff --git a/src/build/framegen/frames/frame_202.txt b/src/build/framegen/frames/frame_202.txt
new file mode 100644
index 0000000000..d0f7f0f487
--- /dev/null
+++ b/src/build/framegen/frames/frame_202.txt
@@ -0,0 +1,41 @@
+
+ ++++++====++++++
+ xx+=***%%%**==++++==**%%%***=+xx
+ ox==**=*x ox*=**==xo
+ ++**+= =+**++
+ ==== ~+%$@@@@@@@@@@@@@@$%+~ ====
+ ===+ x%@@@@@$$$$$$$$$$$$$$$$@@@@@%x +===
+ ===+ +@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ +===
+ ++=+ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% +=++
+ ox== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ==xo
+ ==+x x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x x+==
+ ox== %@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==xo
+ ++++ %@$$$@@+~o=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ ==+· x@$$$$$ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+==
+ == @$$$$$$ ~*@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$$@=· =$$$$$$ %$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@$x o$$$$$% +$$$$$@· ==
+ == ·@$$$$$$ x$@@$$$$$@@@$$$$$$$$$$$$$$@@@$$$$$@· ==
+ == ·@$$$$@$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@$~ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@@$$$$$$$$@@@@$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ·+==
+ ++== +$@@@@@@@$= ~%@@@@@@@@%~ =$@@@@@@@$+ ==xx
+ ===+ +===
+ ==== ==%%++ ++%%+= ====
+ ++**=***++==%***==++***%**++++**%***++==***%==++**%=**++
+ x+++==++++ x+++====+++x ++++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_203.txt b/src/build/framegen/frames/frame_203.txt
new file mode 100644
index 0000000000..d3e8373453
--- /dev/null
+++ b/src/build/framegen/frames/frame_203.txt
@@ -0,0 +1,41 @@
+
+ ++++++++++++++++
+ ++*****%*%=******=%*%*****++
+ ++**=*++ x+*=**++ x
+ ++**+= =+**++
+ ====ox o=%@@@@@@@@@@@@@@%=o xo====
+ ===+ ~*@@@@@@$$$$$$$$$$$$$$@@@@@@*~ +===
+ ===+ o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o +===
+ ++== *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ==++
+ ox== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==xo
+ ==+x o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o x+==
+ ox== =@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xo
+ ++++ *@$$$@@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++++
+ ==x· ~@$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ·x==
+ == @$$$$$$ x$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@+ %@$$$$$o ·$$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@$=· $$$$$* x$$$$$@· ==
+ == ·@$$$$$$ ~*@@$$$$$@@$%%%%%%%%%%%%%%$@@$$$$$@· ==
+ == ·@$$$$@% ~*@@@@$$$$$$$$$$@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$@* ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ·+==
+ xx== x%@@@@@@@$= ~*@@@@@@@@*~ =$@@@@@@@$+ ==xx
+ ===+ +===
+ ====o ==%%++ ++%%== ====
+ ++***%**====%***++++==***%====%***==++++***%====***=**++
+ xx++++++++ ++====++ ++++=+++xx
+
+
+
diff --git a/src/build/framegen/frames/frame_204.txt b/src/build/framegen/frames/frame_204.txt
new file mode 100644
index 0000000000..4333a6acd9
--- /dev/null
+++ b/src/build/framegen/frames/frame_204.txt
@@ -0,0 +1,41 @@
+
+ xx++++++++++++xx
+ ++==***%*%*%%%%%%*%*%***==++
+ ++**=*== ==*=**++
+ ++**=*++ ++*=**++
+ ++**++ ~+*%@@@@@@@@@@%*+~ ++**++
+ ==== o*@@@@@@@$$$$$$$$$$@@@@@@@*o ====
+ ==== =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ====
+ ++== x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x ==++
+ xx==x· x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ·x==xx
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ ox== o@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==xo
+ ++++ x@$$$$@$*%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ++++
+ ==+~ @$$$$$ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+==
+ == $$$$$@$ o%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@%· x$$$$$$@*· +@$$$$$@ ==
+ == ·@$$$$$$$@@@*· $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@$+ $$$$$x @$$$$@· ==
+ == ·@$$$$$$o x%@$$$$$@@*===============$@$$$$$@· ==
+ == ·@$$$$@% o%@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$x o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$@@ ==
+ ==+~ *@@@$$$$$$$@@@@$$$@@@@$$$$$$$$@@@@$$$@@@@$$$$$$$@@@% ·+==
+ xx== x%@@@@@@@$+ ~*@@@@@@@@*~ +$@@@@@@@$+ ==+x
+ ++=+ +=++
+ ++==ox ==%%++ ++%%== o==++
+ ++***%**====%***++++==***%====%***==++++***%====**%***++
+ +++++++x ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_205.txt b/src/build/framegen/frames/frame_205.txt
new file mode 100644
index 0000000000..ce0f1cf9a3
--- /dev/null
+++ b/src/build/framegen/frames/frame_205.txt
@@ -0,0 +1,41 @@
+
+ xx++++++++++++xx
+ ++==****%%*%%%%%%*%%****==++
+ ++****=*++ ++*=****++
+ ++====+= =+====++
+ ++**++ ~x=%$$@@@@$$%=+~ ++**++
+ ++==oo +%@@@@@@@@$$$$$$@@@@@@@@%+ oo==++
+ ++== o$@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@$o ==++
+ ++==~ %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% ~==++
+ ox==+~ $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ ~+==xo
+ ++++ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ++++ ~@$$$$@@$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ++xx
+ ==+~ @$$$$$· +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+==
+ == $$$$$@$ =$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$$= *@$$$$$@$xoooooooooooooox*@$$$$$@ ==
+ == $$$$$$$@@@%x $$$$$o @$$$$$ ==
+ == ·@$$$$$$@@@%x $$$$@o @$$$$@· ==
+ == ·@$$$$$@= =@$$$$$@$xooooooooooooooo*@$$$$$@· ==
+ == ·@$$$$@$ +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$· +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ·+==
+ xx== +%@@@@@@@$= ~%@@@@@@@@%~ =$@@@@@@@$+ ==xo
+ ++=+ +=++
+ ++*=ox ==%%++ ++%%== o==++
+ x+***%**====%***++++==***%====%***==++++***%====**%***++
+ ++++++++ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_206.txt b/src/build/framegen/frames/frame_206.txt
new file mode 100644
index 0000000000..6c1c344c2a
--- /dev/null
+++ b/src/build/framegen/frames/frame_206.txt
@@ -0,0 +1,41 @@
+
+ ++++++++++++
+ ++++==*******%%*******==++++
+ ++==**=***++ ++***=**==++
+ ++==**=*+o o+*=**==++
+ ++====x ·o++====++o· ====++
+ ++==++ +%@@@@@@@@@@@@@@@@@@@@%+ ++==++
+ ++==xo +$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$+ ox==++
+ ++==+~ x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x ~+==++
+ ==+x =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= x===
+ ++== x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ==++
+ ==x· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·x==
+ ++== @$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==++
+ +++x @$$$$$= +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ == *$$$$@% =$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* ==
+ == @$$$$$$· =$@$$$$$@@%**************%@@$$$$$@ ==
+ == $$$$$$$@@*o $$$$$= ~$$$$$$ ==
+ == ~@$$$$$$$@@@$x $$$$@ @$$$$@~ ==
+ == ·@$$$$$@$o ·$$$$$$@+ o$$$$$$@· ==
+ == ·@$$$$$$ =@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@%==$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@@$$$$$$$$@@@@$$@@@@$$$$$$$$@@@@$$@@@@$$$$$$$$$@@$ ·+==
+ xx== +$@@@@@@@$= o%@@@@@@@@%o =$@@@@@@@$= ==xx
+ ++=+ +=++
+ ++== ==%%++ ++%%== ==++
+ x+=*=***++==%***++xx==*%**====**%*==xx++***%==++***=**+x
+ ++++++++ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_207.txt b/src/build/framegen/frames/frame_207.txt
new file mode 100644
index 0000000000..167764b96b
--- /dev/null
+++ b/src/build/framegen/frames/frame_207.txt
@@ -0,0 +1,41 @@
+
+ ++xxxx++
+ xx++==****************==++xx
+ x+==**==*%=*++oo oo++*=%*==**==++
+ xx++**=*+= =+*=**++xx
+ ++====++ ~oooo~ ++====++
+ ++===+ +%@@@@@@@@@@@@@@@@@@%+ +===++
+ ++==+x ·*@@@@@$$$$$$$$$$$$$$$$$$@@@@@*· x+==++
+ x+==+x %@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@% x+==+x
+ ==++ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o +===
+ ++== ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ==++
+ ==+~ *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ~+==
+ ++== $@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==++
+ +++x $$$$$@$~ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ x+++
+ ==x· =@$$$@$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ·+==
+ == @$$$$$$ x%@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$$@$x o$$$$$* +$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@*· =$$$$$$· %$$$$$@· ==
+ == ·@$$$$$$ ~*@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@+~o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· $@@$$$$$$$$$@@@@$@@@@@$$$$$$$$@@@@@$@@@@$$$$$$$$$@@$ ·+==
+ xx== =$@@@@@@@@* o%@@@@@@@@%o *@@@@@@@@$= ==+x
+ ++=+ +=++
+ ++== ==%%++ ++%%== ==++
+ x+**=%*=++++%***++++==*%**++++**%*==++++***%++++==%=**++
+ ++++++++ ++++++++ ++++++++
+
+
+
diff --git a/src/build/framegen/frames/frame_208.txt b/src/build/framegen/frames/frame_208.txt
new file mode 100644
index 0000000000..3d3c0e0e66
--- /dev/null
+++ b/src/build/framegen/frames/frame_208.txt
@@ -0,0 +1,41 @@
+
+
+ xx++====********====++xx
+ ++==**=**%****++++****%**=**==++
+ ++=====%+x x+*=====++
+ ++====== ======++
+ ++====xx o=%$@@@@@@@@@@@@$%=o xx====++
+ ++===+ ·*$@@@@@$$$$$$$$$$$$$$@@@@@$*· +===++
+ xx===+ o$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$o +===xo
+ ==++ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ++==
+ ++== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==++
+ ==+x ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ x+==
+ xx== =@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xx
+ ++++ *@$$$$@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++++
+ ==+· ~@$$$$$ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ·+==
+ == @$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@+ %$$$$$@x ·$$$$$$$ ==
+ == ·@$$$$$$$@@@@x $@$$@ @$$$$@· ==
+ == ·@$$$$$$@@*· $$$$$= x$$$$$@· ==
+ == ·@$$$$$$ ~*@@$$$$$@@$$$$$$$$$$$$$$%$@@$$$$$@· ==
+ == ·@$$$$$% ~=@@@@$$$$$$$$$$@@@@@@@@@@@@@@$$$$$$$$@· ==
+ == ·@$$$$$@* ·*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· $@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·+==
+ xx== *@@@@@@@@@%~ +$@@@@@@@@$+ ~%@@@@@@@@@* ==xx
+ ===+ ++==
+ ===+ ++%% $$++ +===
+ ++**=%+++x++****=+++===*==++x+==*===+++=*%**++++++****++
+ ++++====++ ++====++ ++====+++x
+
+
+
diff --git a/src/build/framegen/frames/frame_209.txt b/src/build/framegen/frames/frame_209.txt
new file mode 100644
index 0000000000..ad52681100
--- /dev/null
+++ b/src/build/framegen/frames/frame_209.txt
@@ -0,0 +1,41 @@
+
+
+ xx++++============++++xx
+ ++====**=**%%%****%%%**=**====++
+ ++====**=*o~ ~o*=**====++
+ ======++ ++======
+ x+====++ ~+*%$@@@@@@@@$%*+~ ++====++
+ xx==== o%@@@@@@@$$$$$$$$$$@@@@@@@%o ====xx
+ ==== =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ====
+ =+== o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o ==++
+ x+==x· x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ·x==+x
+ ==++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++==
+ xx== o@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==xx
+ ++++ x@$$$$@@%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ++++
+ ==+~ @$$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+==
+ == $$$$$@$ o%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@% x@$$$$$@*· +@$$$$$@ ==
+ == ·@$$$$$$@@@@*· $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@$+ $$$$$+ @$$$$@· ==
+ == ·@$$$$$$x x%@$$$$$@@*++++++++++++++=$@$$$$$@· ==
+ == ·@$$$$$$ o%@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$x o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·x==
+ xx++ ·%@@@@@@@@@%o =@@@@@@@@@@= o%@@@@@@@@@%~ ++++
+ ==+x ·~~ ·~~· ~~~ x+==
+ ==++ x+%% %%++ +===
+ ++**=%++ooox==*===++**=%++xoox++%=**++===*==xooo++%=**++
+ ++++==++++ ++++====+++x ++++==++++
+
+
+
diff --git a/src/build/framegen/frames/frame_210.txt b/src/build/framegen/frames/frame_210.txt
new file mode 100644
index 0000000000..d2748c7040
--- /dev/null
+++ b/src/build/framegen/frames/frame_210.txt
@@ -0,0 +1,41 @@
+
+
+ xx++++++++++++++++++
+ ++++==****=***%%%%***=****==++++
+ ++++=====%==o~ ~o==%=====++++
+ ++====+= =+====++
+ =====+ ~x=**%%%%**=x~ +=====
+ ====++ o*$@@@@@@@@@@@@@@@@@@@@$*o ++====
+ ====o~ ·*@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@*· ~o====
+ ++==x· =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ·x==++
+ xx==+o %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% o+==xx
+ =+++ *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* +++=
+ ox==x· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==xo
+ ++++ @$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ++++
+ ==+o @$$$$$x o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+==
+ == %$$$$@% o*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$% ==
+ == @$$$$$$x x%@$$$$$@@=++++++++++++++=$@$$$$$@ ==
+ == $$$$$$$@@$=· $$$$@x @$$$$$ ==
+ == ·@$$$$$$@@@@*· $$$$@~ @$$$$@~ ==
+ == ·@$$$$$@% x@$$$$$@*~ ·+@$$$$$@· ==
+ == ·@$$$$@% x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@$%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x ·@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@~ ==
+ x+++ x$@@@@@@@@@@= o%@@@@@@@@@@%o =@@@@@@@@@@$x ++++
+ ==+o ox++o ~x++x~ o++xo o+==
+ **+x ** ** x+**
+ ++**==o~ ++*+**++**+*xo ox*=**++**=*++ ~o=+**=+
+ ++==***=++++ ++==****==++ x+++=***==++
+
+
+
diff --git a/src/build/framegen/frames/frame_211.txt b/src/build/framegen/frames/frame_211.txt
new file mode 100644
index 0000000000..cb7316752c
--- /dev/null
+++ b/src/build/framegen/frames/frame_211.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++++++++++++
+ ++++==****************==++++
+ ++===****%++~~ ~~++%***====++
+ ++=====*+x x+*=====++
+ ++====xx ·ox+====+xo· xx====++
+ ====++ x%$@@@@@@@@@@@@@@@@@@$%x ++====
+ ++==+x x$@@@@$$$$$$$$$$$$$$$$$$$$@@@@$x x+==++
+ ++==+o x$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$x o+==++
+ ox==++ =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ++==xo
+ +++= x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x =+++
+ ox==+· $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ·+==xo
+ ++== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==++
+ ==+x @$$$$@= =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+==
+ == *$$$$$$ =$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$* ==
+ == @$$$$$$ ·=@@$$$$$@@$%%%%%%%%%%%%%%%@@$$$$$@ ==
+ == $$$$$$$@@*~ $$$$$= o@$$$$$ ==
+ == ~@$$$$$$$@@@@x $$$$@ @$$$$@~ ==
+ == ·@$$$$$@$x %$$$$$@x o$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@%+=$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@x ==
+ ++++ =@@@@@@@@@@@%o +$@@@@@@@@@@$+ o%@@@@@@@@@@@* ++++
+ **+~ ~+===+o x====x o+=*=+~ ~+**
+ **+x ++ ++ xx**
+ ==**++ ~o*+**==**+= =+**==**=*o~ ++====
+ +==****==+++ ++=******=++ ++==*****==+
+
+
+
diff --git a/src/build/framegen/frames/frame_212.txt b/src/build/framegen/frames/frame_212.txt
new file mode 100644
index 0000000000..c082df71b1
--- /dev/null
+++ b/src/build/framegen/frames/frame_212.txt
@@ -0,0 +1,41 @@
+
+
+ ++++++++
+ ++++==************==++++
+ ++++****=***=+xo~~~~ox+=%**=****++++
+ xx++===*== =+*===++xx
+ ++====++ ·~~~~· ++====++
+ ++===+ x*$@@@@@@@@@@@@@@@@$*x +===++
+ ++==++ =@@@@@$$$$$$$$$$$$$$$$$$@@@@@= ++==++
+ x+===+ *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* +===xx
+ ==++ ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ ++==
+ ++== @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==++
+ ==+o *@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* o+==
+ ++== $@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==++
+ +++x $@$$$@$o x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ x+++
+ ==x· +@$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·x==
+ == @$$$$$$ +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@%x x$$$$$% =$$$$$$ ==
+ == ·@$$$$$$$@@@@= $@$$@ @$$$$@· ==
+ == ·@$$$$$@@*~ +$$$$$% *$$$$$@· ==
+ == ·@$$$$$$ ·=@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@x·~=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == =@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@* ==
+ +++x $@@@@@$$@@@@@*x~o+%@@@@@$$@@@@@%+o~o=$@@@@$$$@@@@$· x+++
+ **~· ~=%$$$%=o +*$$$$*+ o=%$$$%=~ **
+ xx**o~ ~·**xx
+ xx**+* ++****=*+x x+*=****++ *+**xx
+ ++==*%%%%%**== x++=*%%%%%%*++++ ==**%%%%%*==++
+
+
+
diff --git a/src/build/framegen/frames/frame_213.txt b/src/build/framegen/frames/frame_213.txt
new file mode 100644
index 0000000000..61e1218701
--- /dev/null
+++ b/src/build/framegen/frames/frame_213.txt
@@ -0,0 +1,41 @@
+
+
+
+ x+++====********====++++
+ ++==**=**%**++++++++**%**=**==++
+ ++=*=*=* *=*=*=++
+ x+====+= =+====++
+ ++===+ ·+%$@@@@@@@@@@@@@@$%+· +===++
+ x+==++ x%@@@@@$$$$$$$$$$$$$$$$@@@@@%x ++==+x
+ xx==++ +@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ ++==xx
+ ==++ %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ++==
+ ++== $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ ==++
+ ==+x +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ x+==
+ ++== %@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ==++
+ ++++ %@$$$@@+~o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ ==+· x@$$$$$ ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+==
+ == @$$$$$$ ~*@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@*· =$$$$$$· *$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@%x o$$$$$* =$$$$$@· ==
+ == ·@$$$$$$ +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@$~ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@$ ==
+ +++o o@@@@$$$$$@@@@%*+=*$@@@$$$$$$@@@$*=+=%@@@@$$$$$@@@@x o+++
+ ** +%@@@@@%+ o*$@@@@$*o +%@@@@@%+ **xo
+ x+** **++
+ x+**++ ox****== ==****xo =+**++
+ +=**%%%%%%%*==+x ++==*%%%%%%%==++ x+==*%%%%%%%**=+
+
+
+
diff --git a/src/build/framegen/frames/frame_214.txt b/src/build/framegen/frames/frame_214.txt
new file mode 100644
index 0000000000..7eb9facaa6
--- /dev/null
+++ b/src/build/framegen/frames/frame_214.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++========++++++
+ ++==*****%%%********%%%*****==++
+ ++==**=*=+ ++*=**==++
+ ++**+= =+**++
+ xx====ox ~=%$@@@@@@@@@@@@$%=~ xo====xx
+ x+===+ ·=@@@@@@$$$$$$$$$$$$$$@@@@@@=· +===+x
+ ===+ ~%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%~ +===
+ ++== =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ==++
+ x+== =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ==+x
+ ==+x ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ x+==
+ xx== =@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ==xx
+ ++++ =@$$$$@*++$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= ++++
+ ==+· ~@$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ·+==
+ == @$$$$$$ +$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@x %$$$$$@x ~$$$$$$$ ==
+ == ·@$$$$$$$@@@@x $$$$@ @$$$$@· ==
+ == ·@$$$$$$@@=~ $$$$$= o@$$$$@· ==
+ == ·@$$$$$$ ~=@@$$$$$@@$%%%%%%%%%%%%%%$@@$$$$$@· ==
+ == ·@$$$$$$ ·*@@@@$$$$$$$$$$@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$@* ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$@$$$$$$$$$$$$$$$$$@@$$$$$$$$$$$$$$$@@ ==
+ ==+~ %@@@$$$$$$$$@@@$$$@@@@$$$$$$$$@@@@$$$@@@$$$$$$$$@@@% ·+==
+ xx== +$@@@@@@@$+ ~*@@@@@@@@*~ +$@@@@@@@$+ ==xx
+ ++=+ +=++
+ ++**oo ==%%++ ++$$== oo==++
+ x+==*%*%====****++++++*%**====**%*++++++**=*====*=%*==++
+ ++++++ ++++++++ ++++++
+
+
diff --git a/src/build/framegen/frames/frame_215.txt b/src/build/framegen/frames/frame_215.txt
new file mode 100644
index 0000000000..5ca0d40adc
--- /dev/null
+++ b/src/build/framegen/frames/frame_215.txt
@@ -0,0 +1,41 @@
+
+
+
+ x ++++++====++++++ x
+ xx++*******%*%%**%%*%*******++xx
+ xx+=****== ==****=+xx
+ ++**+*x x*+**++
+ ====x+ x*%$@@@@@@@@@@$%*x +x====
+ ==== +$@@@@@@$$$$$$$$$$$$@@@@@@$+ ====
+ ==++ *@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* ++==
+ ++== +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ==++
+ xx==x· +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·x==xx
+ ==++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++==
+ xx== +@$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ==xo
+ ++++ +@$$$$@%=*$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ++++
+ ==+· @$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ·+==
+ == @$$$$@$ ·=@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == @$$$$$@$o ~$$$$$$@+ o@$$$$$@ ==
+ == ·@$$$$$$$@@@$o $$$$@ @$$$$@· ==
+ == ·@$$$$$$@@*x $$$$@= ~@$$$$@· ==
+ == ·@$$$$$$~ +$@$$$$$@@%***************@@$$$$$@· ==
+ == ·@$$$$@$ +$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$= +$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· $@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·x==
+ xx== *@@@@@@@@@%o =@@@@@@@@@@= o%@@@@@@@@@%· +++x
+ ==++ ·~· ~~ ·~· x+==
+ ==++ x+%% %%+x ++==
+ ++**=*++xox+==%*==++===%==xoox==%===++==*%==+xoo++*=**++
+ +++++===++ +++==+++ +++===++++
+
+
diff --git a/src/build/framegen/frames/frame_216.txt b/src/build/framegen/frames/frame_216.txt
new file mode 100644
index 0000000000..0b06bcd29b
--- /dev/null
+++ b/src/build/framegen/frames/frame_216.txt
@@ -0,0 +1,41 @@
+
+
+
+ x+++++++++++++xx
+ ++==*****%%%%%%%%%%*****==++
+ ++**=*=* *=*=**++
+ ++**=*++ ++*=**++
+ ++**x+ ~+*$$@@@@@@@@$$*+~ +x**++
+ ==== o%@@@@@@@$$$$$$$$$$@@@@@@@%o ====
+ ++== =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ==++
+ ++== o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o ==++
+ ox==x· x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x ·x==xo
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ == o@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ ++++ x@$$$$@$%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ++++
+ ==+~ @$$$$$ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+==
+ == $$$$$$$ o*@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@%· o@$$$$$@*· +@$$$$$@ ==
+ == ·@$$$$$$$@@@*~ $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@$= $$$$@+ @$$$$@· ==
+ == ·@$$$$$$x x%@$$$$$@@*===============$@$$$$$@· ==
+ == ·@$$$$@% o%@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$x o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == o@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@x ==
+ ++++ =@@@@@@@@@@@*o ·+$@@@@@@@@@@$+· o*@@@@@@@@@@@* ++++
+ **+~ ~+=*=+o ·x=**=x· o+=*=+~ ·+**
+ **xx ++ ++ o+**xx
+ ====++ *=**==**+= =+**==**+= ++====
+ +==**%*=*=++ ++=******=++ +++=**%%*===
+
+
diff --git a/src/build/framegen/frames/frame_217.txt b/src/build/framegen/frames/frame_217.txt
new file mode 100644
index 0000000000..3b406fd58c
--- /dev/null
+++ b/src/build/framegen/frames/frame_217.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++++++++
+ ++++****%%%%%%%%%%%%****++++
+ ++***%*%x~ ~x%*%***++
+ ++**==++ ++==**++
+ ++**++ ·x*%$$@@@@@@$$%*x· ++**++
+ ++==~ o*@@@@@@@@$$$$$$$$@@@@@@@@*o ~==++
+ ++== =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ==++
+ ++== ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ ==++
+ ==x· o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ·x==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == o@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ==
+ ++++ o@$$$$@$%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o +++x
+ ==x~ @$$$$$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~x==
+ == $$$$$$% x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@% x@$$$$$@*~ ·=@$$$$$@ ==
+ == ·@$$$$$$@@@@=· $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@$=~ $$$$@x @$$$$@· ==
+ == ·@$$$$$$x o%@$$$$$@@=+++++++++++++++$@$$$$$@· ==
+ == ·@$$$$@% o*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$o ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == =@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x $@@@@$$$@@@@@=x~o+%@@@@@$$@@@@@%+o~x=@@@@@$$$@@@@$ x+++
+ **~· ~=%$$$%=o x*$$$$*x o=%$$$%=~ **
+ xx**o~ ~~**xx
+ x+**+= ++******+x x+*=****++ *+**+x
+ ++==*%%%%%%*== xx==*%%%%%%*==xx ==*%%%%%%*==++
+
+
diff --git a/src/build/framegen/frames/frame_218.txt b/src/build/framegen/frames/frame_218.txt
new file mode 100644
index 0000000000..7c43145a8a
--- /dev/null
+++ b/src/build/framegen/frames/frame_218.txt
@@ -0,0 +1,41 @@
+
+
+
+ +x++++++
+ ++==***%%%%%%%%%%***==++
+ x+==**=*+o o+*=**==+x
+ ==**++ ++**==
+ ++**++ x=%$$@@@@@@$$%=x ++**++
+ ++**oo ~=@@@@@@@@$$$$$$$$@@@@@@@@=~ oo**++
+ ++== +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ ==++
+ ++=* ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ *=++
+ ==+~ o@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@o ~x==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@@$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==
+ ++++ o@$$$$@@%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o +++x
+ ++x~ @$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~x++
+ == $$$$$$% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@* +@$$$$$@%o~~~~~~~~~~~~~~~=@$$$$$@ ==
+ == ·@$$$$$$@@@$= $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@@*~ $$$$@x @$$$$@· ==
+ == ·@$$$$$$+ ~*@$$$$$@$+xxxxxxxxxxxxxx+%@$$$$$@· ==
+ == ·@$$$$@% ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$o ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$· ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == $@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@ ==
+ +++o =@@@$$$$$$$@@@@%**$@@@@$$$$$$@@@@$**%@@@@$$$$$$$@@@= ~+++
+ ox== ~*@@@@@@@*o +$@@@@@@$+ o*@@@@@@@*~ ==xo
+ ++== ==++
+ ++**++ =*%%== ==%%*= +x**++
+ +=**%%%%%*%=**+x ++***%%%%%%***++ x+**=%*%%%%***=+
+ xxxx xxxx xxxx
+
diff --git a/src/build/framegen/frames/frame_219.txt b/src/build/framegen/frames/frame_219.txt
new file mode 100644
index 0000000000..427ca641fd
--- /dev/null
+++ b/src/build/framegen/frames/frame_219.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++
+ ++==***%*%%%%%%*%***==++
+ xx==***%+x x+%***==xx
+ ==**++ ++**==
+ ++**++ o=*%$$@@@@$$%*=o ++**++
+ ++**oo ·=$@@@@@@@$$$$$$$$@@@@@@@$=· oo**++
+ ++== +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ ==++
+ x+=* ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· *=xx
+ ==+~ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@@$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==
+ xx++ ~@$$$$@@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ++xx
+ +++~ @$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$$% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@* +@$$$$$@%o~~~~~~~~~~~~~~~=@$$$$$@ ==
+ == ·$$$$$$$@@@$+ $$$$@~ @$$$$$· ==
+ == ·@$$$$$$@@@*o $$$$@o @$$$$@· ==
+ == ·@$$$$$$+ ~*@$$$$$@$+xxxxxxxxxxxxxx+%@$$$$$@· ==
+ == ·@$$$$@% ·*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$~ ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==
+ ==+· %@@$$$$$$$$$@@@@$$@@@@$$$$$$$$@@@@$$@@@@$$$$$$$$$@@% ·+==
+ xx== +$@@@@@@@@= o%@@@@@@@@%o =@@@@@@@@$+ ==+x
+ ++=+ +=++
+ ++*=o +=%$++ ++$%=+ o==++
+ xx==**=*===*%***++xx++*%*%====%*%*++xx++***%*===***===++
+ ++++++xx ++++++++ xx++==++
+
diff --git a/src/build/framegen/frames/frame_220.txt b/src/build/framegen/frames/frame_220.txt
new file mode 100644
index 0000000000..bf319ce3de
--- /dev/null
+++ b/src/build/framegen/frames/frame_220.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++++***%%$%%%%$%%***++++
+ ++***%+x x+%***++
+ ++**++ ++**++
+ x+**++ o=*%$$@@@@$$%*=o ++**+x
+ ++**oo ·=$@@@@@@@$$$$$$$$@@@@@@@$=· oo**++
+ ++** +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ **++
+ xx** ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· **xx
+ ==+~ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@@$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==
+ xx++ ~@$$$$@@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ++xx
+ +++~ @$$$$$ +%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$$% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@* +@$$$$$@%o~~~~~~~~~~~~~~~=@$$$$$@ ==
+ == ·$$$$$$$@@@$+ $$$$@~ @$$$$$· ==
+ == ·@$$$$$$@@@*o $$$$@o @$$$$@· ==
+ == ·@$$$$$@+ ~*@$$$$$@$+xxxxxxxxxxxxxx+%@$$$$$@· ==
+ == ·@$$$$@% ·*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$~ ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$@@@@@@@@@$$$$$$$$$$@@@@@@@@@$$$$$$$$$@@@ ·x==
+ x+++ ~%@@@@@@@@@$x =@@@@@@@@@@= x$@@@@@@@@@%~ +++x
+ ===x ~~~ ·~~· ~~~ x+==
+ ===+ x+%% %%+x +===
+ ++**=%+x··~o==*===++***%=+o~~o+=%***++===*==o~··x+%=**++
+ ++=====+++ xx++====++xx +=======++
+
diff --git a/src/build/framegen/frames/frame_221.txt b/src/build/framegen/frames/frame_221.txt
new file mode 100644
index 0000000000..8d7db5aa0e
--- /dev/null
+++ b/src/build/framegen/frames/frame_221.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++==***%%%%%%%%%%***==++
+ +=***%+o o+%***=+
+ +=**++ ++**=+
+ xx**++ o=%%$$@@@@$$%%=o ++**xx
+ x+**oo ~=$@@@@@@@$$$$$$$$@@@@@@@$=~ oo**+x
+ ++** +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ **xx
+ xx** ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· **xx
+ ==+~ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@@$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ==
+ xx++ o@$$$$@@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ++xx
+ +++~ @$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$$% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@* +@$$$$$@%o~~~~~~~~~~~~~~~=@$$$$$@ ==
+ == ·$$$$$$$@@@$+ $$$$@~ @$$$$$· ==
+ == ·@$$$$$$@@@*o $$$$@x @$$$$@· ==
+ == ·@$$$$$$+ ~*@$$$$$@$+xxxxxxxxxxxxxx+%@$$$$$@· ==
+ == ·@$$$$@% ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$o ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x ·@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@~ ==
+ ++++ x@@@@@@@@@@@= o%@@@@@@@@@@%o =@@@@@@@@@@@x ++xx
+ ==+o ~x+xo ·x++x· ox+xo o+==
+ **++ == =* x+**
+ +=**==o· ++*=**==**=*+o o+*=**==**=*+x ·o==**=+
+ ++==****++++ ++==****==++ ++++****==++
+
diff --git a/src/build/framegen/frames/frame_222.txt b/src/build/framegen/frames/frame_222.txt
new file mode 100644
index 0000000000..7551522edd
--- /dev/null
+++ b/src/build/framegen/frames/frame_222.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ ++==**%%%%%%%%%%%%**==++
+ +=***%+o o+****=+
+ +=**++ ++**=+
+ xx**++ ·x=%$$@@@@@@$$%=x· ++**xx
+ x+**~ ~*@@@@@@@@$$$$$$$$@@@@@@@@*~ ~**+x
+ ++** +@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@+ **++
+ xx** ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ **xx
+ ==x· o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ·x==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ xx++ o@$$$$@@%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ++xx
+ ++x~ @$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~x++
+ == $$$$$$% x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@% +@$$$$$@*~··············~=@$$$$$@ ==
+ == ·@$$$$$$@@@$=· $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@$=~ $$$$@x @$$$$@· ==
+ == ·@$$$$$$+ o*@$$$$$@$=+++++++++++++++%@$$$$$@· ==
+ == ·@$$$$@% ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$o ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@x ==
+ ++++ =@@@@@@@@@@@%o ~+@@@@@@@@@@@@+~ o%@@@@@@@@@@@* ++++
+ **+~ ~+***=o ·x=**=x· o=***+o ·x**
+ **+x ++ ++ ox**xx
+ ====++ *=**==**+= =+**==**=* ++====
+ ++=**%%**=++ ++=**%%**=++ ++=**%%%==++xx
+
diff --git a/src/build/framegen/frames/frame_223.txt b/src/build/framegen/frames/frame_223.txt
new file mode 100644
index 0000000000..7ba0c68168
--- /dev/null
+++ b/src/build/framegen/frames/frame_223.txt
@@ -0,0 +1,41 @@
+
+
+
+
+ +++=*%%%%%%%%%%%%%%*=+++
+ +=*%=*x· ·x*=%*=+
+ ++**++ ++**++
+ x+**++ ~+*%$@@@@@@@@$%*+~ ++**+x
+ ++** o*@@@@@@@$$$$$$$$$$@@@@@@@*o **++
+ xx** =@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@= **xx
+ xx** o$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$o **xx
+ =*x· o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ·x*=
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ =* o@$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o *=
+ xx++ x@$$$$@$%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ++xx
+ +++~ @$$$$$ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$$$ x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@% x@$$$$$@*~ ·+@$$$$$@ ==
+ == ·@$$$$$$@@@@*· $$$$@~ @$$$$@· ==
+ == ·@$$$$$$@@$=· $$$$@x @$$$$@· ==
+ == ·@$$$$$$x o%@$$$$$@@=++++++++++++++=$@$$$$$@· ==
+ == ·@$$$$@% o%@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$x o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@+ ==
+ ++++ *@@@@@@@@@@@$+~ ·o*@@@@@@@@@@@@*o ~+$@@@@@@@@@@@% ++++
+ **x· x*%%%*+ o=%%%%=o +*%%%*x ·o**
+ ox**xo xx xx ~o**xx
+ ===*+x *+****==++ ++==****+* o+*===
+ ++==**%%%**=++ ++=**%%%*=++ ++=**%%%%*==++
+
diff --git a/src/build/framegen/frames/frame_224.txt b/src/build/framegen/frames/frame_224.txt
new file mode 100644
index 0000000000..814ec8673f
--- /dev/null
+++ b/src/build/framegen/frames/frame_224.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++
+ ++=**%*%%%%%%%%%%*%**=++
+ x+==*%=* *=%*==+x
+ ===*++ ++*===
+ x+**++ o=%$@@@@@@@@@@$%=o ++**+x
+ ++** x%@@@@@@$$$$$$$$$$$$@@@@@@%x **++
+ ++== *@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* ==++
+ x+=* x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x **+x
+ **x· +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·x**
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ =* x@$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x *=
+ xx++ +@$$$$@$**$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ +++x
+ +++~ @$$$$$ ·*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == @$$$$@$ ~*@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == @$$$$$@$~ o$$$$$$@= x@$$$$$@ ==
+ == ·@$$$$$$$@@@%o $$$$@· @$$$$@· ==
+ == ·@$$$$$$@@%x $$$$@+ ·@$$$$@· ==
+ == ·@$$$$$$~ +$@$$$$$@@*==============*@@$$$$$@· ==
+ == ·@$$$$@% x%@@@@$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$+ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == =@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x $@@@@@$@@@@@@=o~o+%@@@@@@@@@@@@%xo~o=@@@@@@$@@@@@$ x+++
+ **o· ~=%$$$%=~ x*$$$$*x ~=%$$$%=~ **
+ x+**o~ ~~**xx
+ xx**+= ++=***=*+x xx*=***=++ ==**+x
+ ++==*%%%%%**== ++++*%%%%%%*++++ ==**%%%%%*==++
+
diff --git a/src/build/framegen/frames/frame_225.txt b/src/build/framegen/frames/frame_225.txt
new file mode 100644
index 0000000000..5c50111505
--- /dev/null
+++ b/src/build/framegen/frames/frame_225.txt
@@ -0,0 +1,41 @@
+
+
+
+ x++++++x
+ +===*%%%%%****%%%%%*===+
+ ++===*+= =+*===+x
+ ===* *===
+ ++**xx ·+*$@@@@@@@@@@@@$*+· xx**++
+ ++== +$@@@@@@$$$$$$$$$$$$@@@@@@$+ ==++
+ ++== ·%@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@%· ==++
+ x+== =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= ==++
+ **o· =@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@= ·o**
+ ++++ ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ++++
+ =* +@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ *=
+ x+++ =@$$$$@%==$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= +++x
+ +++· ·@$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ·+++
+ == @$$$$@$ =$@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == @$$$$$@$o ·%$$$$$@+ o$$$$$$@ ==
+ == ·@$$$$$$$@@@$o $$$$@ @$$$$@· ==
+ == ·@$$$$$$@@*o $$$$$= o@$$$$@· ==
+ == ·@$$$$$$· =@@$$$$$@@%**************%@@$$$$$@· ==
+ == ·@$$$$$$ =$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$$= +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x ·$@@@@$$$@@@@@*xoo+$@@@@@$$@@@@@$+oox*@@@@@$$$@@@@@~ x+++
+ ** ~=%$$$$*o +%$$$$%+ o*$$$$%*o **
+ xx**~~ **++
+ ++**+= ++==**==x~ ~x==**==++ =+**+x
+ ++==*%%%%%%*== x+==*%%%%%%*==+x ==*%%%%%%*==++
+
diff --git a/src/build/framegen/frames/frame_226.txt b/src/build/framegen/frames/frame_226.txt
new file mode 100644
index 0000000000..d27b633135
--- /dev/null
+++ b/src/build/framegen/frames/frame_226.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++++++++
+ ++++***%%%==++++==%%%***++++
+ ++**=*+o o+*=**++
+ x+**== ==**+x
+ ++** x*$@@@@@@@@@@@@@@$*x **++
+ ++*+ o%@@@@@@$$$$$$$$$$$$$$@@@@@@%o +*++
+ ++=+ x$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$x +=++
+ ++== %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% ==++
+ ** %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% **
+ +++x x@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@x x+++
+ == *@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ==
+ xx++ *@$$$$@=ox*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@* ++xx
+ ==+· o@$$$$$ o*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o ·+==
+ == @$$$$$$ o%@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@= *$$$$$$~ %$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@$+ ~$$$$$* +@$$$$@· ==
+ == ·@$$$$$$ x%@@$$$$$@@@$$$$$$$$$$$$$$@@@$$$$$@· ==
+ == ·@$$$$@$ o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@%· o%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++x ~@@@@@$$$@@@@@%+xx=$@@@@$$$$@@@@$=xo+*@@@@@$$$@@@@@o x+++
+ ** o*$$@$$%x ·=%$@@$%=· x%$@@$$*x **
+ x+** **+x
+ xx**+= xx****== ==****xx =+**++
+ ++==*%%%%***++xx ++==*%*%%*%*==++ xx++*%*%%%%*==++
+
diff --git a/src/build/framegen/frames/frame_227.txt b/src/build/framegen/frames/frame_227.txt
new file mode 100644
index 0000000000..c94758b19e
--- /dev/null
+++ b/src/build/framegen/frames/frame_227.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++++++++++
+ ++==*%*%==++xxxx++==%*%*==++
+ ++**=* *=**++
+ ++**+= =+**++
+ ++== o=%@@@@@@@@@@@@@@@@%=o ==++
+ ===+ +$@@@@@$$$$$$$$$$$$$$$$@@@@@$+ +===
+ ++=+ =@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@= +=++
+ ++=+ $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ +=++
+ ** $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ **
+ +++o =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= o+++
+ == $@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ ++++ %@$$$@@x ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ++++
+ ==+· x@$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@x ·+==
+ == @$$$$$$ ·=@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$@@*~ +$$$$$% *$$$$$$ ==
+ == ·@$$$$$$$@@@@= $@$$@ @$$$$@· ==
+ == ·@$$$$$$@%o x$$$$$% =$$$$$@· ==
+ == ·@$$$$$$ +$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@$o +$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++x ~@@@@@$$$$@@@@%+xx=$@@@@$$$$@@@@$=xx+%@@@@$$$$@@@@@o x+++
+ ** x*$$@@$%x ~=%$@@$%=~ x%$@@@$%x **
+ x+** **++
+ ++**+= xx****== ==****+x =+**++
+ ++==*%%%%*%*=+++ ++==*%*%%*%*==++ +++=*%*%%%%**=++
+
diff --git a/src/build/framegen/frames/frame_228.txt b/src/build/framegen/frames/frame_228.txt
new file mode 100644
index 0000000000..dafe701996
--- /dev/null
+++ b/src/build/framegen/frames/frame_228.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++===***==++++
+ ==***%==xo ox==%***==
+ ===*++ ++*===
+ ++**++ ·oxx++xxo· ++**++
+ ===+ o=$@@@@@@@@@@@@@@@@@@$=o +===
+ **++ o%@@@@@$$$$$$$$$$$$$$$$$$@@@@@%o ++**
+ ==+x ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ o+==
+ ++=+ x@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@x +=++
+ xx== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xx
+ ==x~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~x==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@% ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==x =$$$$@$ ~*@@@@$$$$$$$$$$@@@@@@@@@@@@@@$$$$$$$$= x==
+ == @$$$$$$ o*@@$$$$$@@@$$$$$$$$$$$$$$$@@$$$$$@ ==
+ == $$$$$$$@$= ·$$$$$* x@$$$$$ ==
+ == ·@$$$$$$$@@@@x $$$$@ @$$$$@~ ==
+ == ·@$$$$$@@+ *$$$$$$o %$$$$$@· ==
+ == ·@$$$$$$ x%@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == %@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++x ~@@@@@$$$$@@@@%+xx=$@@@@$$$$@@@@$=xx+%@@@@$$$$@@@@@o o+++
+ ** x*$@@@$%x ~=%$@@$%=~ x%$@@@$%x **
+ x+** **++
+ ++**+= x+****== ==****+x =+**xx
+ ++==*%%%%*%*==++ ++==*%*%%%%*==++ ++==*%*%%%%**=++
+
diff --git a/src/build/framegen/frames/frame_229.txt b/src/build/framegen/frames/frame_229.txt
new file mode 100644
index 0000000000..a7e73094a2
--- /dev/null
+++ b/src/build/framegen/frames/frame_229.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++==**%%%%***=++++
+ ++==*%=*+x x+**%*==++
+ ++**=* *=**++
+ ==== ox=******=xo ====
+ **++ ·=$@@@@@@@@@@@@@@@@@@@@$=· ++**
+ **+o =@@@@@$$$$$$$$$$$$$$$$$$$$@@@@@= o+**
+ **x~ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ~x**
+ +++x *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* x+++
+ xx== +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ==xx
+ ==x· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==
+ ox== @$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xo
+ +++o @$$$$$+ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ == %$$$$@$ +%@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$% ==
+ == @$$$$$$~ +$@$$$$$@@%==============*@@$$$$$@ ==
+ == $$$$$$$@@%x $$$$@+ ·@$$$$$ ==
+ == ~@$$$$$$$@@@%o $$$$@· @$$$$@~ ==
+ == ·@$$$$$@$~ o$$$$$$@= x@$$$$$@· ==
+ == ·@$$$$@$ ~*@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ ·*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@$**$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$$@@@@@$$$$$$$$$$$$$@% ==
+ +++x ~@@@@@$$$@@@@@%+xx=$@@@@$$$$@@@@$=xx+%@@@@$$$$@@@@@o x+++
+ ** o*$$@@$%x ·=%$@@$%=· x%$@@$$*x **
+ x+** **+x
+ ++**+= xx****== ==****xx =+**+x
+ ++==*%%%%*%*=+xx ++==*%%%%%%*==++ xx++*%*%%%%***++
+
diff --git a/src/build/framegen/frames/frame_230.txt b/src/build/framegen/frames/frame_230.txt
new file mode 100644
index 0000000000..b27d48bccd
--- /dev/null
+++ b/src/build/framegen/frames/frame_230.txt
@@ -0,0 +1,41 @@
+
+
+
+ ++++=**%%%%%%%%%%**=++++
+ +=***%+x x+%***=+
+ ++**+= =+**++
+ ox**+= o=*%$@@@@@@$%*=o =+**xo
+ x+**oo ·=$@@@@@@@$$$$$$$$@@@@@@@$=· oo**+x
+ x+** x$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$x **+x
+ ox** ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· **xo
+ ==+~ ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· ==
+ xx++ ~@$$$$@@$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ++xx
+ +++~ @$$$$$ x$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$@% +$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@* +@$$$$$@%x~~~~~~~~~~~~~~o*@$$$$$@ ==
+ == ·$$$$$$$@@@$+ $$$$@~ @$$$$$· ==
+ == ·@$$$$$$@@@*o $$$$@x @$$$$@· ==
+ == ·@$$$$$@+ ~*@$$$$$@$+xxxxxxxxxxxxxxx%@$$$$$@· ==
+ == ·@$$$$@% ·=@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$~ ·=$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x ·$@@@@$$$@@@@@*xox=$@@@@$$$$@@@@$=oox*@@@@@$$$@@@@@~ x+++
+ ** o*%$$$$*x ·+%$$$$%+· x*$$$$$*o **
+ x+**~ **+x
+ x+**+= +x*=**==x ==**==x+ =+**+x
+ ++==*%*%%%**++xx ++==*%%%%%%*==++ xx++**%%%*%*==++
+
diff --git a/src/build/framegen/frames/frame_231.txt b/src/build/framegen/frames/frame_231.txt
new file mode 100644
index 0000000000..7280be71bd
--- /dev/null
+++ b/src/build/framegen/frames/frame_231.txt
@@ -0,0 +1,41 @@
+
+
+ ++++
+ ++==*%%%*%****%*%%%*==++
+ x+==*%== ==%*==+x
+ ===*xo ox*===
+ ++**x+ x*%$@@@@@@@@@@$%*x +x**++
+ ++*= +$@@@@@@$$$$$$$$$$$$@@@@@@$+ =*++
+ ++== *@@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@@* ==+x
+ xx== +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ==xx
+ **o· +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ ·o**
+ ++++ @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ++++
+ ** +@$$$$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ *=
+ xx++ +@$$$$@%=*$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ++xx
+ +++~ @$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == @$$$$$$ ·=@@@@$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@ ==
+ == @$$$$$@$o ~$$$$$$@+ x@$$$$$@ ==
+ == ·@$$$$$$$@@@$o $$$$@· @$$$$@· ==
+ == ·@$$$$$$@@%o $$$$$+ ~@$$$$@· ==
+ == ·@$$$$$$~ +$@$$$$$@@%**************%@@$$$$$@· ==
+ == ·@$$$$@$ +$@@@$$$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· ==
+ == ·@$$$$$@= +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == *@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@* ==
+ +++x $@@@@@$$@@@@@*xoo+%@@@@@$$@@@@@%+o~x=@@@@@$$@@@@@$· x+++
+ **o ~=%$$$%*o +%$$$$%+ o*%$$$%=~ **
+ x+**~~ ~**+x
+ ++**== ++==***=xo o+==**==++ =+**++
+ ++==*%%%%%**++xx ++==*%%%%%%*==++ xx++**%%%%%**=++
+
diff --git a/src/build/framegen/frames/frame_232.txt b/src/build/framegen/frames/frame_232.txt
new file mode 100644
index 0000000000..9ae60cdee0
--- /dev/null
+++ b/src/build/framegen/frames/frame_232.txt
@@ -0,0 +1,41 @@
+
+
+ ++++====++++
+ ++==*%*%==++++++++==%*%*==++
+ ++**=* *=**++
+ x+**+= ·· =+**+x
+ ++== o=%@@@@@@@@@@@@@@@@%=o ==++
+ ===+ +$@@@@@$$$$$$$$$$$$$$$$@@@@@$+ +===
+ ++++ =@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@= ++++
+ ++=+ $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ +=++
+ ** $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ **
+ +++o =@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@= o+++
+ == $@$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ==
+ ++++ $@$$$@@x ·+$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ ==+· +@$$$$$ =$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@+ ·+==
+ == @$$$$$$ =$@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@ ==
+ == $$$$$$$@*o +$$$$$% *$$$$$$ ==
+ == ·@$$$$$$$@@@@+ $$$$@ @$$$$@· ==
+ == ·@$$$$$$@%o x$$$$$% =$$$$$@· ==
+ == ·@$$$$$$ =@@@$$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@$o +$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == +@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@= ==
+ ++++ %@@@@@@@@@@@$+o·~x*@@@@@@@@@@@@*o··~+$@@@@@@@@@@@% x+++
+ **x· +*%%%*+· o=%%%%=o ·+%%$%*+ ·o**
+ x+**o~ oo oo oo**+x
+ ++**+=+~ =+******+x x+******+= ~x=+**++
+ ++==**%%%***++xx ++==***%%***==++ xx++***%%%**==++
+
diff --git a/src/build/framegen/frames/frame_233.txt b/src/build/framegen/frames/frame_233.txt
new file mode 100644
index 0000000000..5d6e28d055
--- /dev/null
+++ b/src/build/framegen/frames/frame_233.txt
@@ -0,0 +1,41 @@
+
+
+ ++++==****==++++
+ ==***%==xo ox==%***==
+ ===*++ ++*===
+ ++**x+ ·oxx++xxo· +x**++
+ ===+ o*$@@@@@@@@@@@@@@@@@@$*o +===
+ **+x o%@@@@@$$$$$$$$$$$$$$$$$$@@@@@%o x+**
+ ==+o ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ o+==
+ ++++ +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ ++++
+ ox== o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o ==xo
+ ==+~ %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% ~+==
+ ox== @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ==xo
+ +++x @$$$$@* ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ x+++
+ ==x *$$$$$$ ~*@@@@$$$$$$$$$$@@@@@@@@@@@@@@$$$$$$$$* x==
+ == @$$$$$$ ~*@@$$$$$@@$%%%%%%%%%%%%%%$@@$$$$$@ ==
+ == $$$$$$$@@= ·$$$$$* x$$$$$$ ==
+ == ~@$$$$$$$@@@@x $$$$@ @$$$$@~ ==
+ == ·@$$$$$@@+ *$$$$$$o ·$$$$$$@· ==
+ == ·@$$$$$$ x$@@@$$$$$$@@@@@@@@@@@@@@@@@@@$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$@@*x+%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == x@@$$$$$$$$$$$@@@@@@@$$$$$$$$$$$$@@@@@@@$$$$$$$$$$$@@+ ==
+ ++++ *@@@@@@@@@@@%x· ~=@@@@@@@@@@@@=~ x%@@@@@@@@@@@* ++++
+ **x· o=*%%=x ~+*%%*+~ x=%%*=x ·x**
+ x+**xo ++ ++ ox**+x
+ x+**=*xx *+****==++ ++==****=* x+==**+x
+ ++==********++xx ++==********==++ xx++********==++
+
diff --git a/src/build/framegen/frames/frame_234.txt b/src/build/framegen/frames/frame_234.txt
new file mode 100644
index 0000000000..b4e624e019
--- /dev/null
+++ b/src/build/framegen/frames/frame_234.txt
@@ -0,0 +1,41 @@
+
+
+ +++=**%%%%%%%%**=+++
+ ++***%==o· ·o==%***++
+ ++**+* *+**++
+ ==== o+=*%$$$$%*=+o ====
+ x+**xx x*@@@@@@@@@@@@@@@@@@@@@@*x xx**+x
+ xx**~~ ~%@@@@$$$$$$$$$$$$$$$$$$$$$$@@@@%~ ~~**xx
+ **o· *@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@* ·x**
+ ==+o %@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@% o+==
+ xx== *@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@* ==xx
+ ==x· @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ ·x==
+ xx== @$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==xx
+ +++o @$$$$$o ~*@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ o+++
+ == %$$$$@% ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$% ==
+ == @$$$$$$+ o*@$$$$$@$=+++++++++++++++%@$$$$$@ ==
+ == $$$$$$$@@@=~ $$$$$x @$$$$$ ==
+ == ·@$$$$$$@@@@= $$$$@~ @$$$$@· ==
+ == ·@$$$$$@% +@$$$$$@%~··············~=@$$$$$@· ==
+ == ·@$$$$@% x%@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@%%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ == ~@@$$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@o ==
+ ++++ +@@@@@@@@@@@=~ x$@@@@@@@@@@$x ·=@@@@@@@@@@@+ ++++
+ **+~ o+=+x· o+==+o ·x+=+x ~x**
+ xx**+x == == x+**+x
+ xx**==+= ~x*=******+* *+******=*x~ =+==**xx
+ ++==********++ x+++********+++x ++********==++
+
diff --git a/src/build/framegen/frames/frame_235.txt b/src/build/framegen/frames/frame_235.txt
new file mode 100644
index 0000000000..7a37219244
--- /dev/null
+++ b/src/build/framegen/frames/frame_235.txt
@@ -0,0 +1,41 @@
+
+
+ ++==**%%*%%%%%%*%%**==++
+ ==***%xx xx%***==
+ ++**++ ++**++
+ x+**++ x=*$$@@@@@@$$*=x ++**+x
+ ++**~o ~*$@@@@@@@$$$$$$$$@@@@@@@$*~ oo**++
+ ++** +$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$+ **++
+ xx** ~$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$~ **xx
+ ==+~ ~@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@~ ~+==
+ ++++ $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ ++++
+ == ~@$$$$$@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ ==
+ x+++ o@$$$$@@%$@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@o +++x
+ +++~ @$$$$$ x%@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ~+++
+ == $$$$$$$ x$@@@$$$$$$$@@@@@@@@@@@@@@@@@@$$$$$$$ ==
+ == @$$$$$@* +@$$$$$@%o··············~=@$$$$$@ ==
+ == ·$$$$$$$@@@$= $$$$@~ @$$$$$· ==
+ == ·@$$$$$$@@$*o $$$$@x @$$$$@· ==
+ == ·@$$$$$$+ ~*@$$$$$@$=xxxxxxxxxxxxxx+%@$$$$$@· ==
+ == ·@$$$$$$ ~*@@@@$$$$$$$$@@@@@@@@@@@@@@@@@$$$$$$@· ==
+ == ·@$$$$$$o ·=@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· ==
+ == ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ ==
+ == @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ ==
+ ==x· @@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$@@@@@@@@$$$$$$$$$$$@@ x==
+ ++++ o$@@@@@@@@@@+ ~*@@@@@@@@@@*~ +@@@@@@@@@@$x ++++
+ ==+x ·oxo~ ~oo~ ~oxo· o+*=
+ x+**+x =* *= ++**xx
+ x+==**+*+o ++*=**==**=*+x x+*=**==**=*++ ~+*=**==xx
+ ++++******==++ ++++********++++ ++==******+++x
+
diff --git a/src/build/framegen/main.zig b/src/build/framegen/main.zig
new file mode 100644
index 0000000000..1be33eb117
--- /dev/null
+++ b/src/build/framegen/main.zig
@@ -0,0 +1,271 @@
+const std = @import("std");
+const fs = std.fs;
+
+/// Generates a compressed file of all the ghostty frames
+pub fn main() !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+
+ var arg_iter = try std.process.argsWithAllocator(gpa.allocator());
+ // Skip the exe name
+ _ = arg_iter.skip();
+
+ const output_path = arg_iter.next() orelse return error.MissingOutputPath;
+
+ const out_dir_path = fs.path.dirname(output_path) orelse return error.InvalidOutputPath;
+ const out_dir = try fs.cwd().openDir(out_dir_path, .{});
+
+ const compressed_file = try out_dir.createFile(fs.path.basename(output_path), .{});
+
+ // Join the frames with a null byte. We'll split on this later
+ const all_frames = try std.mem.join(gpa.allocator(), "\x01", &frames);
+ var fbs = std.io.fixedBufferStream(all_frames);
+
+ const reader = fbs.reader();
+ try std.compress.flate.compress(reader, compressed_file.writer(), .{});
+
+ const stdout = std.io.getStdOut().writer();
+
+ try stdout.print(
+ \\//! This file is auto-generated. Do not edit.
+ \\
+ \\pub const compressed = @embedFile("{s}");
+ , .{output_path});
+}
+
+const frames = [_][]const u8{
+ @embedFile("frames/frame_001.txt"),
+ @embedFile("frames/frame_002.txt"),
+ @embedFile("frames/frame_003.txt"),
+ @embedFile("frames/frame_004.txt"),
+ @embedFile("frames/frame_005.txt"),
+ @embedFile("frames/frame_006.txt"),
+ @embedFile("frames/frame_007.txt"),
+ @embedFile("frames/frame_008.txt"),
+ @embedFile("frames/frame_009.txt"),
+ @embedFile("frames/frame_010.txt"),
+ @embedFile("frames/frame_011.txt"),
+ @embedFile("frames/frame_012.txt"),
+ @embedFile("frames/frame_013.txt"),
+ @embedFile("frames/frame_014.txt"),
+ @embedFile("frames/frame_015.txt"),
+ @embedFile("frames/frame_016.txt"),
+ @embedFile("frames/frame_017.txt"),
+ @embedFile("frames/frame_018.txt"),
+ @embedFile("frames/frame_019.txt"),
+ @embedFile("frames/frame_020.txt"),
+ @embedFile("frames/frame_021.txt"),
+ @embedFile("frames/frame_022.txt"),
+ @embedFile("frames/frame_023.txt"),
+ @embedFile("frames/frame_024.txt"),
+ @embedFile("frames/frame_025.txt"),
+ @embedFile("frames/frame_026.txt"),
+ @embedFile("frames/frame_027.txt"),
+ @embedFile("frames/frame_028.txt"),
+ @embedFile("frames/frame_029.txt"),
+ @embedFile("frames/frame_030.txt"),
+ @embedFile("frames/frame_031.txt"),
+ @embedFile("frames/frame_032.txt"),
+ @embedFile("frames/frame_033.txt"),
+ @embedFile("frames/frame_034.txt"),
+ @embedFile("frames/frame_035.txt"),
+ @embedFile("frames/frame_036.txt"),
+ @embedFile("frames/frame_037.txt"),
+ @embedFile("frames/frame_038.txt"),
+ @embedFile("frames/frame_039.txt"),
+ @embedFile("frames/frame_040.txt"),
+ @embedFile("frames/frame_041.txt"),
+ @embedFile("frames/frame_042.txt"),
+ @embedFile("frames/frame_043.txt"),
+ @embedFile("frames/frame_044.txt"),
+ @embedFile("frames/frame_045.txt"),
+ @embedFile("frames/frame_046.txt"),
+ @embedFile("frames/frame_047.txt"),
+ @embedFile("frames/frame_048.txt"),
+ @embedFile("frames/frame_049.txt"),
+ @embedFile("frames/frame_050.txt"),
+ @embedFile("frames/frame_051.txt"),
+ @embedFile("frames/frame_052.txt"),
+ @embedFile("frames/frame_053.txt"),
+ @embedFile("frames/frame_054.txt"),
+ @embedFile("frames/frame_055.txt"),
+ @embedFile("frames/frame_056.txt"),
+ @embedFile("frames/frame_057.txt"),
+ @embedFile("frames/frame_058.txt"),
+ @embedFile("frames/frame_059.txt"),
+ @embedFile("frames/frame_060.txt"),
+ @embedFile("frames/frame_061.txt"),
+ @embedFile("frames/frame_062.txt"),
+ @embedFile("frames/frame_063.txt"),
+ @embedFile("frames/frame_064.txt"),
+ @embedFile("frames/frame_065.txt"),
+ @embedFile("frames/frame_066.txt"),
+ @embedFile("frames/frame_067.txt"),
+ @embedFile("frames/frame_068.txt"),
+ @embedFile("frames/frame_069.txt"),
+ @embedFile("frames/frame_070.txt"),
+ @embedFile("frames/frame_071.txt"),
+ @embedFile("frames/frame_072.txt"),
+ @embedFile("frames/frame_073.txt"),
+ @embedFile("frames/frame_074.txt"),
+ @embedFile("frames/frame_075.txt"),
+ @embedFile("frames/frame_076.txt"),
+ @embedFile("frames/frame_077.txt"),
+ @embedFile("frames/frame_078.txt"),
+ @embedFile("frames/frame_079.txt"),
+ @embedFile("frames/frame_080.txt"),
+ @embedFile("frames/frame_081.txt"),
+ @embedFile("frames/frame_082.txt"),
+ @embedFile("frames/frame_083.txt"),
+ @embedFile("frames/frame_084.txt"),
+ @embedFile("frames/frame_085.txt"),
+ @embedFile("frames/frame_086.txt"),
+ @embedFile("frames/frame_087.txt"),
+ @embedFile("frames/frame_088.txt"),
+ @embedFile("frames/frame_089.txt"),
+ @embedFile("frames/frame_090.txt"),
+ @embedFile("frames/frame_091.txt"),
+ @embedFile("frames/frame_092.txt"),
+ @embedFile("frames/frame_093.txt"),
+ @embedFile("frames/frame_094.txt"),
+ @embedFile("frames/frame_095.txt"),
+ @embedFile("frames/frame_096.txt"),
+ @embedFile("frames/frame_097.txt"),
+ @embedFile("frames/frame_098.txt"),
+ @embedFile("frames/frame_099.txt"),
+ @embedFile("frames/frame_100.txt"),
+ @embedFile("frames/frame_101.txt"),
+ @embedFile("frames/frame_102.txt"),
+ @embedFile("frames/frame_103.txt"),
+ @embedFile("frames/frame_104.txt"),
+ @embedFile("frames/frame_105.txt"),
+ @embedFile("frames/frame_106.txt"),
+ @embedFile("frames/frame_107.txt"),
+ @embedFile("frames/frame_108.txt"),
+ @embedFile("frames/frame_109.txt"),
+ @embedFile("frames/frame_110.txt"),
+ @embedFile("frames/frame_111.txt"),
+ @embedFile("frames/frame_112.txt"),
+ @embedFile("frames/frame_113.txt"),
+ @embedFile("frames/frame_114.txt"),
+ @embedFile("frames/frame_115.txt"),
+ @embedFile("frames/frame_116.txt"),
+ @embedFile("frames/frame_117.txt"),
+ @embedFile("frames/frame_118.txt"),
+ @embedFile("frames/frame_119.txt"),
+ @embedFile("frames/frame_120.txt"),
+ @embedFile("frames/frame_121.txt"),
+ @embedFile("frames/frame_122.txt"),
+ @embedFile("frames/frame_123.txt"),
+ @embedFile("frames/frame_124.txt"),
+ @embedFile("frames/frame_125.txt"),
+ @embedFile("frames/frame_126.txt"),
+ @embedFile("frames/frame_127.txt"),
+ @embedFile("frames/frame_128.txt"),
+ @embedFile("frames/frame_129.txt"),
+ @embedFile("frames/frame_130.txt"),
+ @embedFile("frames/frame_131.txt"),
+ @embedFile("frames/frame_132.txt"),
+ @embedFile("frames/frame_133.txt"),
+ @embedFile("frames/frame_134.txt"),
+ @embedFile("frames/frame_135.txt"),
+ @embedFile("frames/frame_136.txt"),
+ @embedFile("frames/frame_137.txt"),
+ @embedFile("frames/frame_138.txt"),
+ @embedFile("frames/frame_139.txt"),
+ @embedFile("frames/frame_140.txt"),
+ @embedFile("frames/frame_141.txt"),
+ @embedFile("frames/frame_142.txt"),
+ @embedFile("frames/frame_143.txt"),
+ @embedFile("frames/frame_144.txt"),
+ @embedFile("frames/frame_145.txt"),
+ @embedFile("frames/frame_146.txt"),
+ @embedFile("frames/frame_147.txt"),
+ @embedFile("frames/frame_148.txt"),
+ @embedFile("frames/frame_149.txt"),
+ @embedFile("frames/frame_150.txt"),
+ @embedFile("frames/frame_151.txt"),
+ @embedFile("frames/frame_152.txt"),
+ @embedFile("frames/frame_153.txt"),
+ @embedFile("frames/frame_154.txt"),
+ @embedFile("frames/frame_155.txt"),
+ @embedFile("frames/frame_156.txt"),
+ @embedFile("frames/frame_157.txt"),
+ @embedFile("frames/frame_158.txt"),
+ @embedFile("frames/frame_159.txt"),
+ @embedFile("frames/frame_160.txt"),
+ @embedFile("frames/frame_161.txt"),
+ @embedFile("frames/frame_162.txt"),
+ @embedFile("frames/frame_163.txt"),
+ @embedFile("frames/frame_164.txt"),
+ @embedFile("frames/frame_165.txt"),
+ @embedFile("frames/frame_166.txt"),
+ @embedFile("frames/frame_167.txt"),
+ @embedFile("frames/frame_168.txt"),
+ @embedFile("frames/frame_169.txt"),
+ @embedFile("frames/frame_170.txt"),
+ @embedFile("frames/frame_171.txt"),
+ @embedFile("frames/frame_172.txt"),
+ @embedFile("frames/frame_173.txt"),
+ @embedFile("frames/frame_174.txt"),
+ @embedFile("frames/frame_175.txt"),
+ @embedFile("frames/frame_176.txt"),
+ @embedFile("frames/frame_177.txt"),
+ @embedFile("frames/frame_178.txt"),
+ @embedFile("frames/frame_179.txt"),
+ @embedFile("frames/frame_180.txt"),
+ @embedFile("frames/frame_181.txt"),
+ @embedFile("frames/frame_182.txt"),
+ @embedFile("frames/frame_183.txt"),
+ @embedFile("frames/frame_184.txt"),
+ @embedFile("frames/frame_185.txt"),
+ @embedFile("frames/frame_186.txt"),
+ @embedFile("frames/frame_187.txt"),
+ @embedFile("frames/frame_188.txt"),
+ @embedFile("frames/frame_189.txt"),
+ @embedFile("frames/frame_190.txt"),
+ @embedFile("frames/frame_191.txt"),
+ @embedFile("frames/frame_192.txt"),
+ @embedFile("frames/frame_193.txt"),
+ @embedFile("frames/frame_194.txt"),
+ @embedFile("frames/frame_195.txt"),
+ @embedFile("frames/frame_196.txt"),
+ @embedFile("frames/frame_197.txt"),
+ @embedFile("frames/frame_198.txt"),
+ @embedFile("frames/frame_199.txt"),
+ @embedFile("frames/frame_200.txt"),
+ @embedFile("frames/frame_201.txt"),
+ @embedFile("frames/frame_202.txt"),
+ @embedFile("frames/frame_203.txt"),
+ @embedFile("frames/frame_204.txt"),
+ @embedFile("frames/frame_205.txt"),
+ @embedFile("frames/frame_206.txt"),
+ @embedFile("frames/frame_207.txt"),
+ @embedFile("frames/frame_208.txt"),
+ @embedFile("frames/frame_209.txt"),
+ @embedFile("frames/frame_210.txt"),
+ @embedFile("frames/frame_211.txt"),
+ @embedFile("frames/frame_212.txt"),
+ @embedFile("frames/frame_213.txt"),
+ @embedFile("frames/frame_214.txt"),
+ @embedFile("frames/frame_215.txt"),
+ @embedFile("frames/frame_216.txt"),
+ @embedFile("frames/frame_217.txt"),
+ @embedFile("frames/frame_218.txt"),
+ @embedFile("frames/frame_219.txt"),
+ @embedFile("frames/frame_220.txt"),
+ @embedFile("frames/frame_221.txt"),
+ @embedFile("frames/frame_222.txt"),
+ @embedFile("frames/frame_223.txt"),
+ @embedFile("frames/frame_224.txt"),
+ @embedFile("frames/frame_225.txt"),
+ @embedFile("frames/frame_226.txt"),
+ @embedFile("frames/frame_227.txt"),
+ @embedFile("frames/frame_228.txt"),
+ @embedFile("frames/frame_229.txt"),
+ @embedFile("frames/frame_230.txt"),
+ @embedFile("frames/frame_231.txt"),
+ @embedFile("frames/frame_232.txt"),
+ @embedFile("frames/frame_233.txt"),
+ @embedFile("frames/frame_234.txt"),
+ @embedFile("frames/frame_235.txt"),
+};
diff --git a/src/build/main.zig b/src/build/main.zig
index 8228abfbff..a0e67543f4 100644
--- a/src/build/main.zig
+++ b/src/build/main.zig
@@ -10,6 +10,7 @@ pub const GitVersion = @import("GitVersion.zig");
pub const GhosttyBench = @import("GhosttyBench.zig");
pub const GhosttyDocs = @import("GhosttyDocs.zig");
pub const GhosttyExe = @import("GhosttyExe.zig");
+pub const GhosttyFrameData = @import("GhosttyFrameData.zig");
pub const GhosttyLib = @import("GhosttyLib.zig");
pub const GhosttyResources = @import("GhosttyResources.zig");
pub const GhosttyXCFramework = @import("GhosttyXCFramework.zig");
@@ -28,3 +29,6 @@ pub const XCFrameworkStep = @import("XCFrameworkStep.zig");
pub const fish_completions = @import("fish_completions.zig").completions;
pub const zsh_completions = @import("zsh_completions.zig").completions;
pub const bash_completions = @import("bash_completions.zig").completions;
+
+// Helpers
+pub const requireZig = @import("zig.zig").requireZig;
diff --git a/src/build/webgen/main_actions.zig b/src/build/webgen/main_actions.zig
index f4dffbc139..5002a5bac9 100644
--- a/src/build/webgen/main_actions.zig
+++ b/src/build/webgen/main_actions.zig
@@ -1,58 +1,8 @@
const std = @import("std");
const help_strings = @import("help_strings");
-const KeybindAction = @import("../../input/Binding.zig").Action;
+const helpgen_actions = @import("../../input/helpgen_actions.zig");
pub fn main() !void {
const output = std.io.getStdOut().writer();
- try genKeybindActions(output);
-}
-
-pub fn genKeybindActions(writer: anytype) !void {
- // Write the header
- try writer.writeAll(
- \\---
- \\title: Keybinding Action Reference
- \\description: Reference of all Ghostty keybinding actions.
- \\editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig
- \\---
- \\
- \\This is a reference of all Ghostty keybinding actions.
- \\
- \\
- );
-
- @setEvalBranchQuota(5_000);
-
- var buffer = std.ArrayList(u8).init(std.heap.page_allocator);
- defer buffer.deinit();
-
- const fields = @typeInfo(KeybindAction).Union.fields;
- inline for (fields) |field| {
- if (field.name[0] == '_') continue;
-
- // Write previously stored doc comment below all related actions
- if (@hasDecl(help_strings.KeybindAction, field.name)) {
- try writer.writeAll(buffer.items);
- try writer.writeAll("\n");
-
- buffer.clearRetainingCapacity();
- }
-
- // Write the field name.
- try writer.writeAll("## `");
- try writer.writeAll(field.name);
- try writer.writeAll("`\n");
-
- if (@hasDecl(help_strings.KeybindAction, field.name)) {
- var iter = std.mem.splitScalar(
- u8,
- @field(help_strings.KeybindAction, field.name),
- '\n',
- );
- while (iter.next()) |s| {
- try buffer.appendSlice(s);
- try buffer.appendSlice("\n");
- }
- }
- }
+ try helpgen_actions.generate(output, .markdown, true, std.heap.page_allocator);
}
diff --git a/src/build/webgen/main_commands.zig b/src/build/webgen/main_commands.zig
new file mode 100644
index 0000000000..6e6b00c5e3
--- /dev/null
+++ b/src/build/webgen/main_commands.zig
@@ -0,0 +1,51 @@
+const std = @import("std");
+const Action = @import("../../cli/action.zig").Action;
+const help_strings = @import("help_strings");
+
+pub fn main() !void {
+ const output = std.io.getStdOut().writer();
+ try genActions(output);
+}
+
+// Note: as a shortcut for defining inline editOnGithubLinks per cli action the user
+// is directed to the folder view on Github. This includes a README pointing them to
+// the files to edit.
+pub fn genActions(writer: anytype) !void {
+ // Write the header
+ try writer.writeAll(
+ \\---
+ \\title: Reference
+ \\description: Reference of all Ghostty action subcommands.
+ \\editOnGithubLink: https://github.com/ghostty-org/ghostty/tree/main/src/cli
+ \\---
+ \\Ghostty includes a number of utility actions that can be accessed as subcommands.
+ \\Actions provide utilities to work with config, list keybinds, list fonts, demo themes,
+ \\and debug.
+ \\
+ );
+
+ inline for (@typeInfo(Action).Enum.fields) |field| {
+ const action = std.meta.stringToEnum(Action, field.name).?;
+
+ switch (action) {
+ .help, .version => try writer.writeAll("## " ++ field.name ++ "\n"),
+ else => try writer.writeAll("## " ++ field.name ++ "\n"),
+ }
+
+ if (@hasDecl(help_strings.Action, field.name)) {
+ var iter = std.mem.splitScalar(u8, @field(help_strings.Action, field.name), '\n');
+ var first = true;
+ while (iter.next()) |s| {
+ try writer.writeAll(s);
+ try writer.writeAll("\n");
+ first = false;
+ }
+ try writer.writeAll("\n```\n");
+ switch (action) {
+ .help, .version => try writer.writeAll("ghostty --" ++ field.name ++ "\n"),
+ else => try writer.writeAll("ghostty +" ++ field.name ++ "\n"),
+ }
+ try writer.writeAll("```\n\n");
+ }
+ }
+}
diff --git a/src/build/zig.zig b/src/build/zig.zig
new file mode 100644
index 0000000000..7e327127d4
--- /dev/null
+++ b/src/build/zig.zig
@@ -0,0 +1,17 @@
+const std = @import("std");
+const builtin = @import("builtin");
+
+/// Require a specific version of Zig to build this project.
+pub fn requireZig(comptime required_zig: []const u8) void {
+ // Fail compilation if the current Zig version doesn't meet requirements.
+ const current_vsn = builtin.zig_version;
+ const required_vsn = std.SemanticVersion.parse(required_zig) catch unreachable;
+ if (current_vsn.major != required_vsn.major or
+ current_vsn.minor != required_vsn.minor)
+ {
+ @compileError(std.fmt.comptimePrint(
+ "Your Zig version v{} does not meet the required build version of v{}",
+ .{ current_vsn, required_vsn },
+ ));
+ }
+}
diff --git a/src/cli/README.md b/src/cli/README.md
new file mode 100644
index 0000000000..7a1d99409c
--- /dev/null
+++ b/src/cli/README.md
@@ -0,0 +1,13 @@
+# Subcommand Actions
+
+This is the cli specific code. It contains cli actions and tui definitions and
+argument parsing.
+
+This README is meant as developer documentation and not as user documentation.
+For user documentation, see the main README or [ghostty.org](https://ghostty.org/docs).
+
+## Updating documentation
+
+Each cli action is defined in it's own file. Documentation for each action is defined
+in the doc comment associated with the `run` function. For example the `run` function
+in `list_keybinds.zig` contains the help text for `ghostty +list-keybinds`.
diff --git a/src/cli/action.zig b/src/cli/action.zig
index a84a400241..f3da2e2efd 100644
--- a/src/cli/action.zig
+++ b/src/cli/action.zig
@@ -13,6 +13,7 @@ const show_config = @import("show_config.zig");
const validate_config = @import("validate_config.zig");
const crash_report = @import("crash_report.zig");
const show_face = @import("show_face.zig");
+const boo = @import("boo.zig");
/// Special commands that can be invoked via CLI flags. These are all
/// invoked by using `+` as a CLI flag. The only exception is
@@ -45,11 +46,14 @@ pub const Action = enum {
// Validate passed config file
@"validate-config",
+ // Show which font face Ghostty loads a codepoint from.
+ @"show-face",
+
// List, (eventually) view, and (eventually) send crash reports.
@"crash-report",
- // Show which font face Ghostty loads a codepoint from.
- @"show-face",
+ // Boo!
+ boo,
pub const Error = error{
/// Multiple actions were detected. You can specify at most one
@@ -151,6 +155,7 @@ pub const Action = enum {
.@"validate-config" => try validate_config.run(alloc),
.@"crash-report" => try crash_report.run(alloc),
.@"show-face" => try show_face.run(alloc),
+ .boo => try boo.run(alloc),
};
}
@@ -186,6 +191,7 @@ pub const Action = enum {
.@"validate-config" => validate_config.Options,
.@"crash-report" => crash_report.Options,
.@"show-face" => show_face.Options,
+ .boo => boo.Options,
};
}
}
diff --git a/src/cli/args.zig b/src/cli/args.zig
index 23dcf77331..7385e6a3ea 100644
--- a/src/cli/args.zig
+++ b/src/cli/args.zig
@@ -8,6 +8,8 @@ const internal_os = @import("../os/main.zig");
const Diagnostic = diags.Diagnostic;
const DiagnosticList = diags.DiagnosticList;
+const log = std.log.scoped(.cli);
+
// TODO:
// - Only `--long=value` format is accepted. Do we want to allow
// `--long value`? Not currently allowed.
@@ -38,6 +40,12 @@ pub const Error = error{
/// "DiagnosticList" and any diagnostic messages will be added to that list.
/// When diagnostics are present, only allocation errors will be returned.
///
+/// If the destination type has a decl "renamed", it must be of type
+/// std.StaticStringMap([]const u8) and contains a mapping from the old
+/// field name to the new field name. This is used to allow renaming fields
+/// while still supporting the old name. If a renamed field is set, parsing
+/// will automatically set the new field name.
+///
/// Note: If the arena is already non-null, then it will be used. In this
/// case, in the case of an error some memory might be leaked into the arena.
pub fn parse(
@@ -49,6 +57,24 @@ pub fn parse(
const info = @typeInfo(T);
assert(info == .Struct);
+ comptime {
+ // Verify all renamed fields are valid (source does not exist,
+ // destination does exist).
+ if (@hasDecl(T, "renamed")) {
+ for (T.renamed.keys(), T.renamed.values()) |key, value| {
+ if (@hasField(T, key)) {
+ @compileLog(key);
+ @compileError("renamed field source exists");
+ }
+
+ if (!@hasField(T, value)) {
+ @compileLog(value);
+ @compileError("renamed field destination does not exist");
+ }
+ }
+ }
+ }
+
// Make an arena for all our allocations if we support it. Otherwise,
// use an allocator that always fails. If the arena is already set on
// the config, then we reuse that. See memory note in parse docs.
@@ -367,6 +393,16 @@ pub fn parseIntoField(
}
}
+ // Unknown field, is the field renamed?
+ if (@hasDecl(T, "renamed")) {
+ for (T.renamed.keys(), T.renamed.values()) |old, new| {
+ if (mem.eql(u8, old, key)) {
+ try parseIntoField(T, alloc, dst, new, value);
+ return;
+ }
+ }
+ }
+
return error.InvalidField;
}
@@ -1104,6 +1140,24 @@ test "parseIntoField: tagged union missing tag" {
);
}
+test "parseIntoField: renamed field" {
+ const testing = std.testing;
+ var arena = ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ var data: struct {
+ a: []const u8,
+
+ const renamed = std.StaticStringMap([]const u8).initComptime(&.{
+ .{ "old", "a" },
+ });
+ } = undefined;
+
+ try parseIntoField(@TypeOf(data), alloc, &data, "old", "42");
+ try testing.expectEqualStrings("42", data.a);
+}
+
/// An iterator that considers its location to be CLI args. It
/// iterates through an underlying iterator and increments a counter
/// to track the current CLI arg index.
@@ -1206,9 +1260,11 @@ pub fn LineIterator(comptime ReaderType: type) type {
const buf = buf: {
while (true) {
// Read the full line
- var entry = self.r.readUntilDelimiterOrEof(self.entry[2..], '\n') catch {
- // TODO: handle errors
- unreachable;
+ var entry = self.r.readUntilDelimiterOrEof(self.entry[2..], '\n') catch |err| switch (err) {
+ inline else => |e| {
+ log.warn("cannot read from \"{s}\": {}", .{ self.filepath, e });
+ return null;
+ },
} orelse return null;
// Increment our line counter
diff --git a/src/cli/boo.zig b/src/cli/boo.zig
new file mode 100644
index 0000000000..7ecbf79fbb
--- /dev/null
+++ b/src/cli/boo.zig
@@ -0,0 +1,234 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const args = @import("args.zig");
+const Action = @import("action.zig").Action;
+const Allocator = std.mem.Allocator;
+const help_strings = @import("help_strings");
+const vaxis = @import("vaxis");
+
+const framedata = @import("framedata");
+
+const vxfw = vaxis.vxfw;
+
+pub const Options = struct {
+ pub fn deinit(self: Options) void {
+ _ = self;
+ }
+
+ /// Enables `-h` and `--help` to work.
+ pub fn help(self: Options) !void {
+ _ = self;
+ return Action.help_error;
+ }
+};
+
+const Boo = struct {
+ frame: u8,
+ framerate: u32, // 30 fps
+ // We know the size of this at compile time, but we heap allocate the slice to prevent the
+ // binary from increasing too much in size
+ buffer: [frame_width * frame_height]vaxis.Cell = undefined,
+
+ ghostty_style: vaxis.Style,
+ outline_style: vaxis.Style,
+
+ // Width of a single frame
+ const frame_width = 100;
+ // Height of a single frame
+ const frame_height = 41;
+
+ fn widget(self: *Boo) vxfw.Widget {
+ return .{
+ .userdata = self,
+ .eventHandler = Boo.typeErasedEventHandler,
+ .drawFn = Boo.typeErasedDrawFn,
+ };
+ }
+
+ fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
+ const self: *Boo = @ptrCast(@alignCast(ptr));
+ switch (event) {
+ .init,
+ .tick,
+ => {
+ self.updateFrame();
+ ctx.redraw = true;
+ return ctx.tick(self.framerate, self.widget());
+ },
+ .key_press => |key| {
+ if (key.matches('c', .{ .ctrl = true }) or
+ key.matches(vaxis.Key.escape, .{}))
+ {
+ ctx.quit = true;
+ return;
+ }
+ },
+ else => {},
+ }
+ }
+
+ fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
+ const self: *Boo = @ptrCast(@alignCast(ptr));
+ const max = ctx.max.size();
+
+ // Warn for screen size
+ if (max.width < frame_width or max.height < frame_height) {
+ const text: vxfw.Text = .{ .text = "Screen must be at least 100w x 41h" };
+ const center: vxfw.Center = .{ .child = text.widget() };
+ return center.draw(ctx);
+ }
+
+ // Calculate x and y offsets to center the animation frame
+ const offset_y = (max.height - frame_height) / 2;
+ const offset_x = (max.width - frame_width) / 2;
+
+ // Create the animation surface
+ const child: vxfw.Surface = .{
+ .size = .{ .width = @intCast(frame_width), .height = @intCast(frame_height) },
+ .widget = self.widget(),
+ .buffer = &self.buffer,
+ .children = &.{},
+ };
+
+ // Allocate a slice of child surfaces
+ var children = try ctx.arena.alloc(vxfw.SubSurface, 1);
+ children[0] = .{
+ .origin = .{ .row = @intCast(offset_y), .col = @intCast(offset_x) },
+ .surface = child,
+ };
+
+ return .{
+ .size = max,
+ .widget = self.widget(),
+ .buffer = &.{},
+ .children = children,
+ };
+ }
+
+ /// Updates our internal buffer with the current frame, then advances the frame index
+ fn updateFrame(self: *Boo) void {
+ const frame = frames[self.frame];
+ // A frame is characters with html spans. When we encounter a span, we use the outline style
+ // until the span ends. That is, when we find a '<', we parse until '>'. Then we use the
+ // outline styule until the next '<', and skip until the next '>'
+
+ const State = enum {
+ normal,
+ span,
+ in_tag,
+ in_closing_tag,
+ };
+
+ var cell_idx: usize = 0;
+
+ var line_iter = std.mem.splitScalar(u8, frame, '\n');
+ while (line_iter.next()) |line| {
+ var state: State = .normal;
+ var style = self.ghostty_style;
+ var cp_iter: std.unicode.Utf8Iterator = .{ .bytes = line, .i = 0 };
+ while (cp_iter.nextCodepointSlice()) |char| {
+ switch (state) {
+ .normal => if (std.mem.eql(u8, "<", char)) {
+ state = .in_tag;
+ // We will be entering a span
+ style = self.outline_style;
+ continue;
+ },
+ .span => if (std.mem.eql(u8, "<", char)) {
+ state = .in_tag;
+ style = self.ghostty_style;
+ continue;
+ },
+ .in_tag => {
+ // If we encounter a '/', we are a closing tag
+ // If we parse all the way to a '>' we are an opening tag: we are now in a span
+ if (std.mem.eql(u8, "/", char))
+ state = .in_closing_tag
+ else if (std.mem.eql(u8, ">", char))
+ state = .span;
+ continue;
+ },
+ .in_closing_tag => {
+ // If we are closing a tag, we will enter the normal state
+ if (std.mem.eql(u8, ">", char)) state = .normal;
+ continue;
+ },
+ }
+ self.buffer[cell_idx] = .{
+ .char = .{
+ .grapheme = char,
+ .width = 1,
+ },
+ .style = style,
+ };
+ cell_idx += 1;
+ }
+ }
+ std.debug.assert(cell_idx == self.buffer.len);
+
+ // Lastly, update the frame index
+ self.frame += 1;
+ if (self.frame == frames.len) self.frame = 0;
+ }
+};
+
+/// The `boo` command is used to display the animation from the Ghostty website in the terminal
+pub fn run(gpa: Allocator) !u8 {
+ // Disable on non-desktop systems.
+ switch (builtin.os.tag) {
+ .windows, .macos, .linux => {},
+ else => return 1,
+ }
+
+ var opts: Options = .{};
+ defer opts.deinit();
+
+ {
+ var iter = try args.argsIterator(gpa);
+ defer iter.deinit();
+ try args.parse(Options, gpa, &opts, &iter);
+ }
+
+ try decompressFrames(gpa);
+ defer {
+ gpa.free(frames);
+ gpa.free(decompressed_data);
+ }
+
+ var app = try vxfw.App.init(gpa);
+ defer app.deinit();
+
+ var boo: Boo = undefined;
+ boo.frame = 0;
+ boo.framerate = 1000 / 30;
+ boo.ghostty_style = .{};
+ boo.outline_style = .{ .fg = .{ .index = 4 } };
+ @memset(&boo.buffer, .{});
+
+ try app.run(boo.widget(), .{});
+
+ return 0;
+}
+
+/// We store a global ref to the decompressed data. All of our frames reference into this data
+var decompressed_data: []const u8 = undefined;
+
+/// Heap allocated list of frames. The underlying frame data references decompressed_data
+var frames: []const []const u8 = undefined;
+
+/// Decompress the frames into a slice of individual frames
+fn decompressFrames(gpa: Allocator) !void {
+ var fbs = std.io.fixedBufferStream(framedata.compressed);
+ var list = std.ArrayList(u8).init(gpa);
+
+ try std.compress.flate.decompress(fbs.reader(), list.writer());
+ decompressed_data = try list.toOwnedSlice();
+
+ var frame_list = try std.ArrayList([]const u8).initCapacity(gpa, 235);
+
+ var frame_iter = std.mem.splitScalar(u8, decompressed_data, '\x01');
+ while (frame_iter.next()) |frame| {
+ try frame_list.append(frame);
+ }
+ frames = try frame_list.toOwnedSlice();
+}
diff --git a/src/cli/crash_report.zig b/src/cli/crash_report.zig
index dd5fe99cce..ff85097972 100644
--- a/src/cli/crash_report.zig
+++ b/src/cli/crash_report.zig
@@ -53,7 +53,7 @@ pub fn run(alloc_gpa: Allocator) !u8 {
// print a message, otherwise we do nothing.
if (reports.items.len == 0) {
if (std.posix.isatty(stdout.handle)) {
- try stdout.writeAll("No crash reports! 👻");
+ try stdout.writeAll("No crash reports! 👻\n");
}
return 0;
}
diff --git a/src/cli/help.zig b/src/cli/help.zig
index daadc37ccd..22fe27d8d1 100644
--- a/src/cli/help.zig
+++ b/src/cli/help.zig
@@ -15,9 +15,11 @@ pub const Options = struct {
}
};
-/// The `help` command shows general help about Ghostty. You can also specify
-/// `--help` or `-h` along with any action such as `+list-themes` to see help
-/// for a specific action.
+/// The `help` command shows general help about Ghostty. Recognized as either
+/// `-h, `--help`, or like other actions `+help`.
+///
+/// You can also specify `--help` or `-h` along with any action such as
+/// `+list-themes` to see help for a specific action.
pub fn run(alloc: Allocator) !u8 {
var opts: Options = .{};
defer opts.deinit();
diff --git a/src/cli/list_actions.zig b/src/cli/list_actions.zig
index 65b9dcdadc..1d17873cc9 100644
--- a/src/cli/list_actions.zig
+++ b/src/cli/list_actions.zig
@@ -2,7 +2,7 @@ const std = @import("std");
const args = @import("args.zig");
const Action = @import("action.zig").Action;
const Allocator = std.mem.Allocator;
-const help_strings = @import("help_strings");
+const helpgen_actions = @import("../input/helpgen_actions.zig");
pub const Options = struct {
/// If `true`, print out documentation about the action associated with the
@@ -24,7 +24,9 @@ pub const Options = struct {
/// actions for Ghostty. These are distinct from the CLI Actions which can
/// be listed via `+help`
///
-/// The `--docs` argument will print out the documentation for each action.
+/// Flags:
+///
+/// * `--docs`: will print out the documentation for each action.
pub fn run(alloc: Allocator) !u8 {
var opts: Options = .{};
defer opts.deinit();
@@ -36,19 +38,7 @@ pub fn run(alloc: Allocator) !u8 {
}
const stdout = std.io.getStdOut().writer();
- const info = @typeInfo(help_strings.KeybindAction);
- inline for (info.Struct.decls) |field| {
- try stdout.print("{s}", .{field.name});
- if (opts.docs) {
- try stdout.print(":\n", .{});
- var iter = std.mem.splitScalar(u8, std.mem.trimRight(u8, @field(help_strings.KeybindAction, field.name), &std.ascii.whitespace), '\n');
- while (iter.next()) |line| {
- try stdout.print(" {s}\n", .{line});
- }
- } else {
- try stdout.print("\n", .{});
- }
- }
+ try helpgen_actions.generate(stdout, .plaintext, opts.docs, std.heap.page_allocator);
return 0;
}
diff --git a/src/cli/list_fonts.zig b/src/cli/list_fonts.zig
index 9d1f34cd1e..e8a010ecd0 100644
--- a/src/cli/list_fonts.zig
+++ b/src/cli/list_fonts.zig
@@ -44,14 +44,21 @@ pub const Options = struct {
/// the sorting will be disabled and the results instead will be shown in the
/// same priority order Ghostty would use to pick a font.
///
-/// The `--family` argument can be used to filter results to a specific family.
-/// The family handling is identical to the `font-family` set of Ghostty
-/// configuration values, so this can be used to debug why your desired font may
-/// not be loading.
+/// Flags:
///
-/// The `--bold` and `--italic` arguments can be used to filter results to
-/// specific styles. It is not guaranteed that only those styles are returned,
-/// it will just prioritize fonts that match those styles.
+/// * `--bold`: Filter results to specific bold styles. It is not guaranteed
+/// that only those styles are returned. They are only prioritized.
+///
+/// * `--italic`: Filter results to specific italic styles. It is not guaranteed
+/// that only those styles are returned. They are only prioritized.
+///
+/// * `--style`: Filter results based on the style string advertised by a font.
+/// It is not guaranteed that only those styles are returned. They are only
+/// prioritized.
+///
+/// * `--family`: Filter results to a specific font family. The family handling
+/// is identical to the `font-family` set of Ghostty configuration values, so
+/// this can be used to debug why your desired font may not be loading.
pub fn run(alloc: Allocator) !u8 {
var iter = try args.argsIterator(alloc);
defer iter.deinit();
diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig
index ddaf751770..6cd989201c 100644
--- a/src/cli/list_keybinds.zig
+++ b/src/cli/list_keybinds.zig
@@ -42,11 +42,15 @@ pub const Options = struct {
/// changes to the keybinds it will print out the default ones configured for
/// Ghostty
///
-/// The `--default` argument will print out all the default keybinds configured
-/// for Ghostty
+/// Flags:
///
-/// The `--plain` flag will disable formatting and make the output more
-/// friendly for Unix tooling. This is default when not printing to a tty.
+/// * `--default`: will print out all the default keybinds
+///
+/// * `--docs`: currently does nothing, intended to print out documentation
+/// about the action associated with the keybinds
+///
+/// * `--plain`: will disable formatting and make the output more
+/// friendly for Unix tooling. This is default when not printing to a tty.
pub fn run(alloc: Allocator) !u8 {
var opts: Options = .{};
defer opts.deinit();
@@ -64,7 +68,9 @@ pub fn run(alloc: Allocator) !u8 {
// Despite being under the posix namespace, this also works on Windows as of zig 0.13.0
if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) {
- return prettyPrint(alloc, config.keybind);
+ var arena = std.heap.ArenaAllocator.init(alloc);
+ defer arena.deinit();
+ return prettyPrint(arena.allocator(), config.keybind);
} else {
try config.keybind.formatEntryDocs(
configpkg.entryFormatter("keybind", stdout.writer()),
@@ -75,6 +81,111 @@ pub fn run(alloc: Allocator) !u8 {
return 0;
}
+const TriggerList = std.SinglyLinkedList(Binding.Trigger);
+
+const ChordBinding = struct {
+ triggers: TriggerList,
+ action: Binding.Action,
+
+ // Order keybinds based on various properties
+ // 1. Longest chord sequence
+ // 2. Most active modifiers
+ // 3. Alphabetically by active modifiers
+ // 4. Trigger key order
+ // These properties propagate through chorded keypresses
+ //
+ // Adapted from Binding.lessThan
+ pub fn lessThan(_: void, lhs: ChordBinding, rhs: ChordBinding) bool {
+ const lhs_len = lhs.triggers.len();
+ const rhs_len = rhs.triggers.len();
+
+ std.debug.assert(lhs_len != 0);
+ std.debug.assert(rhs_len != 0);
+
+ if (lhs_len != rhs_len) {
+ return lhs_len > rhs_len;
+ }
+
+ const lhs_count: usize = blk: {
+ var count: usize = 0;
+ var maybe_trigger = lhs.triggers.first;
+ while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
+ if (trigger.data.mods.super) count += 1;
+ if (trigger.data.mods.ctrl) count += 1;
+ if (trigger.data.mods.shift) count += 1;
+ if (trigger.data.mods.alt) count += 1;
+ }
+ break :blk count;
+ };
+ const rhs_count: usize = blk: {
+ var count: usize = 0;
+ var maybe_trigger = rhs.triggers.first;
+ while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
+ if (trigger.data.mods.super) count += 1;
+ if (trigger.data.mods.ctrl) count += 1;
+ if (trigger.data.mods.shift) count += 1;
+ if (trigger.data.mods.alt) count += 1;
+ }
+
+ break :blk count;
+ };
+
+ if (lhs_count != rhs_count)
+ return lhs_count > rhs_count;
+
+ {
+ var l_trigger = lhs.triggers.first;
+ var r_trigger = rhs.triggers.first;
+ while (l_trigger != null and r_trigger != null) {
+ const l_int = l_trigger.?.data.mods.int();
+ const r_int = r_trigger.?.data.mods.int();
+
+ if (l_int != r_int) {
+ return l_int > r_int;
+ }
+
+ l_trigger = l_trigger.?.next;
+ r_trigger = r_trigger.?.next;
+ }
+ }
+
+ var l_trigger = lhs.triggers.first;
+ var r_trigger = rhs.triggers.first;
+
+ while (l_trigger != null and r_trigger != null) {
+ const lhs_key: c_int = blk: {
+ switch (l_trigger.?.data.key) {
+ .translated => |key| break :blk @intFromEnum(key),
+ .physical => |key| break :blk @intFromEnum(key),
+ .unicode => |key| break :blk @intCast(key),
+ }
+ };
+ const rhs_key: c_int = blk: {
+ switch (r_trigger.?.data.key) {
+ .translated => |key| break :blk @intFromEnum(key),
+ .physical => |key| break :blk @intFromEnum(key),
+ .unicode => |key| break :blk @intCast(key),
+ }
+ };
+
+ l_trigger = l_trigger.?.next;
+ r_trigger = r_trigger.?.next;
+
+ if (l_trigger == null or r_trigger == null) {
+ return lhs_key < rhs_key;
+ }
+
+ if (lhs_key != rhs_key) {
+ return lhs_key < rhs_key;
+ }
+ }
+
+ // The previous loop will always return something on its final iteration so we cannot
+ // reach this point
+ unreachable;
+ }
+};
+
fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
// Set up vaxis
var tty = try vaxis.Tty.init();
@@ -107,26 +218,11 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
const win = vx.window();
- // Get all of our keybinds into a list. We also search for the longest printed keyname so we can
- // align things nicely
+ // Generate a list of bindings, recursively traversing chorded keybindings
var iter = keybinds.set.bindings.iterator();
- var bindings = std.ArrayList(Binding).init(alloc);
- var widest_key: u16 = 0;
- var buf: [64]u8 = undefined;
- while (iter.next()) |bind| {
- const action = switch (bind.value_ptr.*) {
- .leader => continue, // TODO: support this
- .leaf => |leaf| leaf.action,
- };
- const key = switch (bind.key_ptr.key) {
- .translated => |k| try std.fmt.bufPrint(&buf, "{s}", .{@tagName(k)}),
- .physical => |k| try std.fmt.bufPrint(&buf, "physical:{s}", .{@tagName(k)}),
- .unicode => |c| try std.fmt.bufPrint(&buf, "{u}", .{c}),
- };
- widest_key = @max(widest_key, win.gwidth(key));
- try bindings.append(.{ .trigger = bind.key_ptr.*, .action = action });
- }
- std.mem.sort(Binding, bindings.items, {}, Binding.lessThan);
+ const bindings, const widest_chord = try iterateBindings(alloc, &iter, &win);
+
+ std.mem.sort(ChordBinding, bindings, {}, ChordBinding.lessThan);
// Set up styles for each modifier
const super_style: vaxis.Style = .{ .fg = .{ .index = 1 } };
@@ -134,41 +230,41 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
const alt_style: vaxis.Style = .{ .fg = .{ .index = 3 } };
const shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } };
- var longest_col: u16 = 0;
-
// Print the list
- for (bindings.items) |bind| {
+ for (bindings) |bind| {
win.clear();
var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false };
- const trigger = bind.trigger;
- if (trigger.mods.super) {
- result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
- result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
- }
- if (trigger.mods.ctrl) {
- result = win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col });
- result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
- }
- if (trigger.mods.alt) {
- result = win.printSegment(.{ .text = "alt ", .style = alt_style }, .{ .col_offset = result.col });
- result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
- }
- if (trigger.mods.shift) {
- result = win.printSegment(.{ .text = "shift", .style = shift_style }, .{ .col_offset = result.col });
- result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
- }
-
- const key = switch (trigger.key) {
- .translated => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}),
- .physical => |k| try std.fmt.allocPrint(alloc, "physical:{s}", .{@tagName(k)}),
- .unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}),
- };
- // We don't track the key print because we index the action off the *widest* key so we get
- // nice alignment no matter what was printed for mods
- _ = win.printSegment(.{ .text = key }, .{ .col_offset = result.col });
+ var maybe_trigger = bind.triggers.first;
+ while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
+ if (trigger.data.mods.super) {
+ result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
+ result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
+ }
+ if (trigger.data.mods.ctrl) {
+ result = win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col });
+ result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
+ }
+ if (trigger.data.mods.alt) {
+ result = win.printSegment(.{ .text = "alt ", .style = alt_style }, .{ .col_offset = result.col });
+ result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
+ }
+ if (trigger.data.mods.shift) {
+ result = win.printSegment(.{ .text = "shift", .style = shift_style }, .{ .col_offset = result.col });
+ result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
+ }
+ const key = switch (trigger.data.key) {
+ .translated => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}),
+ .physical => |k| try std.fmt.allocPrint(alloc, "physical:{s}", .{@tagName(k)}),
+ .unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}),
+ };
+ result = win.printSegment(.{ .text = key }, .{ .col_offset = result.col });
- if (longest_col < result.col) longest_col = result.col;
+ // Print a separator between chorded keys
+ if (trigger.next != null) {
+ result = win.printSegment(.{ .text = " > ", .style = .{ .bold = true, .fg = .{ .index = 6 } } }, .{ .col_offset = result.col });
+ }
+ }
const action = try std.fmt.allocPrint(alloc, "{}", .{bind.action});
// If our action has an argument, we print the argument in a different color
@@ -177,12 +273,69 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
.{ .text = action[0..idx] },
.{ .text = action[idx .. idx + 1], .style = .{ .dim = true } },
.{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } },
- }, .{ .col_offset = longest_col + widest_key + 2 });
+ }, .{ .col_offset = widest_chord + 3 });
} else {
- _ = win.printSegment(.{ .text = action }, .{ .col_offset = longest_col + widest_key + 2 });
+ _ = win.printSegment(.{ .text = action }, .{ .col_offset = widest_chord + 3 });
}
try vx.prettyPrint(writer);
}
try buf_writer.flush();
return 0;
}
+
+fn iterateBindings(alloc: Allocator, iter: anytype, win: *const vaxis.Window) !struct { []ChordBinding, u16 } {
+ var widest_chord: u16 = 0;
+ var bindings = std.ArrayList(ChordBinding).init(alloc);
+ while (iter.next()) |bind| {
+ const width = blk: {
+ var buf = std.ArrayList(u8).init(alloc);
+ const t = bind.key_ptr.*;
+
+ if (t.mods.super) try std.fmt.format(buf.writer(), "super + ", .{});
+ if (t.mods.ctrl) try std.fmt.format(buf.writer(), "ctrl + ", .{});
+ if (t.mods.alt) try std.fmt.format(buf.writer(), "alt + ", .{});
+ if (t.mods.shift) try std.fmt.format(buf.writer(), "shift + ", .{});
+
+ switch (t.key) {
+ .translated => |k| try std.fmt.format(buf.writer(), "{s}", .{@tagName(k)}),
+ .physical => |k| try std.fmt.format(buf.writer(), "physical:{s}", .{@tagName(k)}),
+ .unicode => |c| try std.fmt.format(buf.writer(), "{u}", .{c}),
+ }
+
+ break :blk win.gwidth(buf.items);
+ };
+
+ switch (bind.value_ptr.*) {
+ .leader => |leader| {
+
+ // Recursively iterate on the set of bindings for this leader key
+ var n_iter = leader.bindings.iterator();
+ const sub_bindings, const max_width = try iterateBindings(alloc, &n_iter, win);
+
+ // Prepend the current keybind onto the list of sub-binds
+ for (sub_bindings) |*nb| {
+ const prepend_node = try alloc.create(TriggerList.Node);
+ prepend_node.* = TriggerList.Node{ .data = bind.key_ptr.* };
+ nb.triggers.prepend(prepend_node);
+ }
+
+ // Add the longest sub-bind width to the current bind width along with a padding
+ // of 5 for the ' > ' spacer
+ widest_chord = @max(widest_chord, width + max_width + 5);
+ try bindings.appendSlice(sub_bindings);
+ },
+ .leaf => |leaf| {
+ const node = try alloc.create(TriggerList.Node);
+ node.* = TriggerList.Node{ .data = bind.key_ptr.* };
+ const triggers = TriggerList{
+ .first = node,
+ };
+
+ widest_chord = @max(widest_chord, width);
+ try bindings.append(.{ .triggers = triggers, .action = leaf.action });
+ },
+ }
+ }
+
+ return .{ try bindings.toOwnedSlice(), widest_chord };
+}
diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig
index 22e22a972d..8ebac4487a 100644
--- a/src/cli/list_themes.zig
+++ b/src/cli/list_themes.zig
@@ -91,6 +91,7 @@ const ThemeListElement = struct {
/// Flags:
///
/// * `--path`: Show the full path to the theme.
+///
/// * `--plain`: Force a plain listing of themes.
pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
var opts: Options = .{};
@@ -191,6 +192,7 @@ const Preview = struct {
normal,
help,
search,
+ save,
},
color_scheme: vaxis.Color.Scheme,
text_input: vaxis.widgets.TextInput,
@@ -376,6 +378,8 @@ const Preview = struct {
self.mode = .help;
if (key.matches('/', .{}))
self.mode = .search;
+ if (key.matchesAny(&.{ vaxis.Key.enter, vaxis.Key.kp_enter }, .{}))
+ self.mode = .save;
if (key.matchesAny(&.{ 'x', '/' }, .{ .ctrl = true })) {
self.text_input.buf.clearRetainingCapacity();
try self.updateFiltered();
@@ -430,6 +434,12 @@ const Preview = struct {
try self.text_input.update(.{ .key_press = key });
try self.updateFiltered();
},
+ .save => {
+ if (key.matches('q', .{}))
+ self.should_quit = true;
+ if (key.matchesAny(&.{ vaxis.Key.escape, vaxis.Key.enter, vaxis.Key.kp_enter }, .{}))
+ self.mode = .normal;
+ },
}
},
.color_scheme => |color_scheme| self.color_scheme = color_scheme,
@@ -673,7 +683,7 @@ const Preview = struct {
.{ .keys = "End", .help = "Go to the end of the list." },
.{ .keys = "/", .help = "Start search." },
.{ .keys = "^X, ^/", .help = "Clear search." },
- .{ .keys = "⏎", .help = "Close search window." },
+ .{ .keys = "⏎", .help = "Save theme or close search window." },
};
for (key_help, 0..) |help, captured_i| {
@@ -724,6 +734,51 @@ const Preview = struct {
child.fill(.{ .style = self.ui_standard() });
self.text_input.drawWithStyle(child, self.ui_standard());
},
+ .save => {
+ const theme = self.themes[self.filtered.items[self.current]];
+
+ const width = 90;
+ const height = 12;
+ const child = win.child(
+ .{
+ .x_off = win.width / 2 -| width / 2,
+ .y_off = win.height / 2 -| height / 2,
+ .width = width,
+ .height = height,
+ .border = .{
+ .where = .all,
+ .style = self.ui_standard(),
+ },
+ },
+ );
+
+ child.fill(.{ .style = self.ui_standard() });
+
+ const save_instructions = [_][]const u8{
+ "To apply this theme, add the following line to your Ghostty configuration:",
+ "",
+ try std.fmt.allocPrint(alloc, "theme = {s}", .{theme.theme}),
+ "",
+ "Save the configuration file and then reload it to apply the new theme.",
+ "For more details on configuration and themes, visit the Ghostty documentation:",
+ "",
+ "https://ghostty.org/docs/config/reference",
+ };
+
+ for (save_instructions, 0..) |instruction, captured_i| {
+ const i: u16 = @intCast(captured_i);
+ _ = child.printSegment(
+ .{
+ .text = instruction,
+ .style = self.ui_standard(),
+ },
+ .{
+ .row_offset = i + 1,
+ .col_offset = 2,
+ },
+ );
+ }
+ },
}
}
diff --git a/src/cli/validate_config.zig b/src/cli/validate_config.zig
index 1615ef66b3..5bc6ff4062 100644
--- a/src/cli/validate_config.zig
+++ b/src/cli/validate_config.zig
@@ -23,10 +23,13 @@ pub const Options = struct {
/// The `validate-config` command is used to validate a Ghostty config file.
///
-/// When executed without any arguments, this will load the config from the default location.
+/// When executed without any arguments, this will load the config from the default
+/// location.
///
-/// The `--config-file` argument can be passed to validate a specific target config
-/// file in a non-default location.
+/// Flags:
+///
+/// * `--config-file`: can be passed to validate a specific target config file in
+/// a non-default location
pub fn run(alloc: std.mem.Allocator) !u8 {
var opts: Options = .{};
defer opts.deinit();
diff --git a/src/cli/version.zig b/src/cli/version.zig
index b001525896..f6d2ea9df7 100644
--- a/src/cli/version.zig
+++ b/src/cli/version.zig
@@ -10,7 +10,8 @@ const gtk = if (build_config.app_runtime == .gtk) @import("../apprt/gtk/c.zig").
pub const Options = struct {};
-/// The `version` command is used to display information about Ghostty.
+/// The `version` command is used to display information about Ghostty. Recognized as
+/// either `+version` or `--version`.
pub fn run(alloc: Allocator) !u8 {
_ = alloc;
@@ -50,19 +51,15 @@ pub fn run(alloc: Allocator) !u8 {
gtk.gtk_get_minor_version(),
gtk.gtk_get_micro_version(),
});
- if (comptime build_options.adwaita) {
- try stdout.print(" - libadwaita : enabled\n", .{});
- try stdout.print(" build : {s}\n", .{
- gtk.ADW_VERSION_S,
- });
- try stdout.print(" runtime : {}.{}.{}\n", .{
- gtk.adw_get_major_version(),
- gtk.adw_get_minor_version(),
- gtk.adw_get_micro_version(),
- });
- } else {
- try stdout.print(" - libadwaita : disabled\n", .{});
- }
+ try stdout.print(" - libadwaita : enabled\n", .{});
+ try stdout.print(" build : {s}\n", .{
+ gtk.ADW_VERSION_S,
+ });
+ try stdout.print(" runtime : {}.{}.{}\n", .{
+ gtk.adw_get_major_version(),
+ gtk.adw_get_minor_version(),
+ gtk.adw_get_micro_version(),
+ });
if (comptime build_options.x11) {
try stdout.print(" - libX11 : enabled\n", .{});
} else {
diff --git a/src/config.zig b/src/config.zig
index 75dbaae02b..a8ffe2ec7b 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -27,6 +27,7 @@ pub const OptionAsAlt = Config.OptionAsAlt;
pub const RepeatableCodepointMap = Config.RepeatableCodepointMap;
pub const RepeatableFontVariation = Config.RepeatableFontVariation;
pub const RepeatableString = Config.RepeatableString;
+pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig");
pub const RepeatablePath = Config.RepeatablePath;
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
pub const WindowPaddingColor = Config.WindowPaddingColor;
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 2f38676c56..9e6f17ef4e 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -34,6 +34,7 @@ const KeyValue = @import("key.zig").Value;
const ErrorList = @import("ErrorList.zig");
const MetricModifier = fontpkg.Metrics.Modifier;
const help_strings = @import("help_strings");
+const RepeatableStringMap = @import("RepeatableStringMap.zig");
const log = std.log.scoped(.config);
@@ -42,6 +43,16 @@ const c = @cImport({
@cInclude("unistd.h");
});
+/// Renamed fields, used by cli.parse
+pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
+ // Ghostty 1.1 introduced background-blur support for Linux which
+ // doesn't support a specific radius value. The renaming is to let
+ // one field be used for both platforms (macOS retained the ability
+ // to set a radius).
+ .{ "background-blur-radius", "background-blur" },
+ .{ "adw-toolbar-style", "gtk-toolbar-style" },
+});
+
/// The font families to use.
///
/// You can generate the list of valid values using the CLI:
@@ -248,6 +259,28 @@ const c = @cImport({
/// This is currently only supported on macOS.
@"font-thicken-strength": u8 = 255,
+/// What color space to use when performing alpha blending.
+///
+/// This affects the appearance of text and of any images with transparency.
+/// Additionally, custom shaders will receive colors in the configured space.
+///
+/// Valid values:
+///
+/// * `native` - Perform alpha blending in the native color space for the OS.
+/// On macOS this corresponds to Display P3, and on Linux it's sRGB.
+///
+/// * `linear` - Perform alpha blending in linear space. This will eliminate
+/// the darkening artifacts around the edges of text that are very visible
+/// when certain color combinations are used (e.g. red / green), but makes
+/// dark text look much thinner than normal and light text much thicker.
+/// This is also sometimes known as "gamma correction".
+/// (Currently only supported on macOS. Has no effect on Linux.)
+///
+/// * `linear-corrected` - Same as `linear`, but with a correction step applied
+/// for text that makes it look nearly or completely identical to `native`,
+/// but without any of the darkening artifacts.
+@"alpha-blending": AlphaBlending = .native,
+
/// All of the configurations behavior adjust various metrics determined by the
/// font. The values can be integers (1, -1, etc.) or a percentage (20%, -15%,
/// etc.). In each case, the values represent the amount to change the original
@@ -256,7 +289,7 @@ const c = @cImport({
/// For example, a value of `1` increases the value by 1; it does not set it to
/// literally 1. A value of `20%` increases the value by 20%. And so on.
///
-/// There is little to no validation on these values so the wrong values (i.e.
+/// There is little to no validation on these values so the wrong values (e.g.
/// `-100%`) can cause the terminal to be unusable. Use with caution and reason.
///
/// Some values are clamped to minimum or maximum values. This can make it
@@ -286,14 +319,14 @@ const c = @cImport({
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-underline-thickness": ?MetricModifier = null,
/// Distance in pixels or percentage adjustment from the top of the cell to the top of the strikethrough.
-/// Increase to move strikethrough DOWN, decrease to move underline UP.
+/// Increase to move strikethrough DOWN, decrease to move strikethrough UP.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-strikethrough-position": ?MetricModifier = null,
/// Thickness in pixels or percentage adjustment of the strikethrough.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-strikethrough-thickness": ?MetricModifier = null,
/// Distance in pixels or percentage adjustment from the top of the cell to the top of the overline.
-/// Increase to move overline DOWN, decrease to move underline UP.
+/// Increase to move overline DOWN, decrease to move overline UP.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-overline-position": ?MetricModifier = null,
/// Thickness in pixels or percentage adjustment of the overline.
@@ -441,7 +474,7 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
/// The minimum contrast ratio between the foreground and background colors.
/// The contrast ratio is a value between 1 and 21. A value of 1 allows for no
-/// contrast (i.e. black on black). This value is the contrast ratio as defined
+/// contrast (e.g. black on black). This value is the contrast ratio as defined
/// by the [WCAG 2.0 specification](https://www.w3.org/TR/WCAG20/).
///
/// If you want to avoid invisible text (same color as background), a value of
@@ -604,7 +637,7 @@ palette: Palette = .{},
///
/// Supported on macOS and on some Linux desktop environments, including:
///
-/// * KDE Plasma (Wayland only)
+/// * KDE Plasma (Wayland and X11)
///
/// Warning: the exact blur intensity is _ignored_ under KDE Plasma, and setting
/// this setting to either `true` or any positive blur intensity value would
@@ -623,7 +656,7 @@ palette: Palette = .{},
/// need to set environment-specific settings and/or install third-party plugins
/// in order to support background blur, as there isn't a unified interface for
/// doing so.
-@"background-blur-radius": BackgroundBlur = .false,
+@"background-blur": BackgroundBlur = .false,
/// The opacity level (opposite of transparency) of an unfocused split.
/// Unfocused splits by default are slightly faded out to make it easier to see
@@ -696,7 +729,7 @@ command: ?[]const u8 = null,
/// injecting any configured shell integration into the command's
/// environment. With `-e` its highly unlikely that you're executing a
/// shell and forced shell integration is likely to cause problems
-/// (i.e. by wrapping your command in a shell, setting env vars, etc.).
+/// (e.g. by wrapping your command in a shell, setting env vars, etc.).
/// This is a safety measure to prevent unexpected behavior. If you want
/// shell integration with a `-e`-executed command, you must either
/// name your binary appropriately or source the shell integration script
@@ -704,6 +737,42 @@ command: ?[]const u8 = null,
///
@"initial-command": ?[]const u8 = null,
+/// Extra environment variables to pass to commands launched in a terminal
+/// surface. The format is `env=KEY=VALUE`.
+///
+/// `env = foo=bar`
+/// `env = bar=baz`
+///
+/// Setting `env` to an empty string will reset the entire map to default
+/// (empty).
+///
+/// `env =`
+///
+/// Setting a key to an empty string will remove that particular key and
+/// corresponding value from the map.
+///
+/// `env = foo=bar`
+/// `env = foo=`
+///
+/// will result in `foo` not being passed to the launched commands.
+///
+/// Setting a key multiple times will overwrite previous entries.
+///
+/// `env = foo=bar`
+/// `env = foo=baz`
+///
+/// will result in `foo=baz` being passed to the launched commands.
+///
+/// These environment variables will override any existing environment
+/// variables set by Ghostty. For example, if you set `GHOSTTY_RESOURCES_DIR`
+/// then the value you set here will override the value Ghostty typically
+/// automatically injects.
+///
+/// These environment variables _will not_ be passed to commands run by Ghostty
+/// for other purposes, like `open` or `xdg-open` used to open URLs in your
+/// browser.
+env: RepeatableStringMap = .{},
+
/// If true, keep the terminal open after the command exits. Normally, the
/// terminal window closes when the running command (such as a shell) exits.
/// With this true, the terminal window will stay open until any keypress is
@@ -744,7 +813,7 @@ command: ?[]const u8 = null,
/// Match a regular expression against the terminal text and associate clicking
/// it with an action. This can be used to match URLs, file paths, etc. Actions
-/// can be opening using the system opener (i.e. `open` or `xdg-open`) or
+/// can be opening using the system opener (e.g. `open` or `xdg-open`) or
/// executing any arbitrary binding action.
///
/// Links that are configured earlier take precedence over links that are
@@ -764,6 +833,11 @@ link: RepeatableLink = .{},
/// `link`). If you want to customize URL matching, use `link` and disable this.
@"link-url": bool = true,
+/// Whether to start the window in a maximized state. This setting applies
+/// to new windows and does not apply to tabs, splits, etc. However, this setting
+/// will apply to all new windows, not just the first one.
+maximize: bool = false,
+
/// Start new windows in fullscreen. This setting applies to new windows and
/// does not apply to tabs, splits, etc. However, this setting will apply to all
/// new windows, not just the first one.
@@ -845,7 +919,7 @@ class: ?[:0]const u8 = null,
/// Valid keys are currently only listed in the
/// [Ghostty source code](https://github.com/ghostty-org/ghostty/blob/d6e76858164d52cff460fedc61ddf2e560912d71/src/input/key.zig#L255).
/// This is a documentation limitation and we will improve this in the future.
-/// A common gotcha is that numeric keys are written as words: i.e. `one`,
+/// A common gotcha is that numeric keys are written as words: e.g. `one`,
/// `two`, `three`, etc. and not `1`, `2`, `3`. This will also be improved in
/// the future.
///
@@ -888,7 +962,7 @@ class: ?[:0]const u8 = null,
/// * Ghostty will wait an indefinite amount of time for the next key in
/// the sequence. There is no way to specify a timeout. The only way to
/// force the output of a prefix key is to assign another keybind to
-/// specifically output that key (i.e. `ctrl+a>ctrl+a=text:foo`) or
+/// specifically output that key (e.g. `ctrl+a>ctrl+a=text:foo`) or
/// press an unbound key which will send both keys to the program.
///
/// * If a prefix in a sequence is previously bound, the sequence will
@@ -918,13 +992,13 @@ class: ?[:0]const u8 = null,
/// including `physical:`-prefixed triggers without specifying the
/// prefix.
///
-/// * `csi:text` - Send a CSI sequence. i.e. `csi:A` sends "cursor up".
+/// * `csi:text` - Send a CSI sequence. e.g. `csi:A` sends "cursor up".
///
-/// * `esc:text` - Send an escape sequence. i.e. `esc:d` deletes to the
+/// * `esc:text` - Send an escape sequence. e.g. `esc:d` deletes to the
/// end of the word to the right.
///
/// * `text:text` - Send a string. Uses Zig string literal syntax.
-/// i.e. `text:\x15` sends Ctrl-U.
+/// e.g. `text:\x15` sends Ctrl-U.
///
/// * All other actions can be found in the documentation or by using the
/// `ghostty +list-actions` command.
@@ -950,12 +1024,12 @@ class: ?[:0]const u8 = null,
/// keybinds only apply to the focused terminal surface. If this is true,
/// then the keybind will be sent to all terminal surfaces. This only
/// applies to actions that are surface-specific. For actions that
-/// are already global (i.e. `quit`), this prefix has no effect.
+/// are already global (e.g. `quit`), this prefix has no effect.
///
/// * `global:` - Make the keybind global. By default, keybinds only work
/// within Ghostty and under the right conditions (application focused,
/// sometimes terminal focused, etc.). If you want a keybind to work
-/// globally across your system (i.e. even when Ghostty is not focused),
+/// globally across your system (e.g. even when Ghostty is not focused),
/// specify this prefix. This prefix implies `all:`. Note: this does not
/// work in all environments; see the additional notes below for more
/// information.
@@ -979,6 +1053,12 @@ class: ?[:0]const u8 = null,
/// performable (acting identically to not having a keybind set at
/// all).
///
+/// Performable keybinds will not appear as menu shortcuts in the
+/// application menu. This is because the menu shortcuts force the
+/// action to be performed regardless of the state of the terminal.
+/// Performable keybinds will still work, they just won't appear as
+/// a shortcut label in the menu.
+///
/// Keybind triggers are not unique per prefix combination. For example,
/// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind
/// set later will overwrite the keybind set earlier. In this case, the
@@ -1056,7 +1136,7 @@ keybind: Keybinds = .{},
/// any of the heuristics that disable extending noted below.
///
/// The "extend" value will be disabled in certain scenarios. On primary
-/// screen applications (i.e. not something like Neovim), the color will not
+/// screen applications (e.g. not something like Neovim), the color will not
/// be extended vertically if any of the following are true:
///
/// * The nearest row has any cells that have the default background color.
@@ -1096,21 +1176,52 @@ keybind: Keybinds = .{},
/// configuration `font-size` will be used.
@"window-inherit-font-size": bool = true,
+/// Configure a preference for window decorations. This setting specifies
+/// a _preference_; the actual OS, desktop environment, window manager, etc.
+/// may override this preference. Ghostty will do its best to respect this
+/// preference but it may not always be possible.
+///
/// Valid values:
///
-/// * `true`
-/// * `false` - windows won't have native decorations, i.e. titlebar and
-/// borders. On macOS this also disables tabs and tab overview.
+/// * `none` - All window decorations will be disabled. Titlebar,
+/// borders, etc. will not be shown. On macOS, this will also disable
+/// tabs (enforced by the system).
+///
+/// * `auto` - Automatically decide to use either client-side or server-side
+/// decorations based on the detected preferences of the current OS and
+/// desktop environment. This option usually makes Ghostty look the most
+/// "native" for your desktop.
+///
+/// * `client` - Prefer client-side decorations.
+///
+/// * `server` - Prefer server-side decorations. This is only relevant
+/// on Linux with GTK, either on X11, or Wayland on a compositor that
+/// supports the `org_kde_kwin_server_decoration` protocol (e.g. KDE Plasma,
+/// but almost any non-GNOME desktop supports this protocol).
+///
+/// If `server` is set but the environment doesn't support server-side
+/// decorations, client-side decorations will be used instead.
+///
+/// The default value is `auto`.
+///
+/// For the sake of backwards compatibility and convenience, this setting also
+/// accepts boolean true and false values. If set to `true`, this is equivalent
+/// to `auto`. If set to `false`, this is equivalent to `none`.
+/// This is convenient for users who live primarily on systems that don't
+/// differentiate between client and server-side decorations (e.g. macOS and
+/// Windows).
///
/// The "toggle_window_decorations" keybind action can be used to create
-/// a keybinding to toggle this setting at runtime.
+/// a keybinding to toggle this setting at runtime. This will always toggle
+/// back to "auto" if the current value is "none" (this is an issue
+/// that will be fixed in the future).
///
/// Changing this configuration in your configuration and reloading will
/// only affect new windows. Existing windows will not be affected.
///
/// macOS: To hide the titlebar without removing the native window borders
/// or rounded corners, use `macos-titlebar-style = hidden` instead.
-@"window-decoration": bool = true,
+@"window-decoration": WindowDecoration = .auto,
/// The font that will be used for the application's window and tab titles.
///
@@ -1120,6 +1231,15 @@ keybind: Keybinds = .{},
/// required to be a fixed-width font.
@"window-title-font-family": ?[:0]const u8 = null,
+/// The text that will be displayed in the subtitle of the window. Valid values:
+///
+/// * `false` - Disable the subtitle.
+/// * `working-directory` - Set the subtitle to the working directory of the
+/// surface.
+///
+/// This feature is only supported on GTK.
+@"window-subtitle": WindowSubtitle = .false,
+
/// The theme to use for the windows. Valid values:
///
/// * `auto` - Determine the theme based on the configured terminal
@@ -1130,8 +1250,7 @@ keybind: Keybinds = .{},
/// * `light` - Use the light theme regardless of system theme.
/// * `dark` - Use the dark theme regardless of system theme.
/// * `ghostty` - Use the background and foreground colors specified in the
-/// Ghostty configuration. This is only supported on Linux builds with
-/// Adwaita and `gtk-adwaita` enabled.
+/// Ghostty configuration. This is only supported on Linux builds.
///
/// On macOS, if `macos-titlebar-style` is "tabs", the window theme will be
/// automatically set based on the luminosity of the terminal background color.
@@ -1141,12 +1260,16 @@ keybind: Keybinds = .{},
/// This is currently only supported on macOS and Linux.
@"window-theme": WindowTheme = .auto,
-/// The colorspace to use for the terminal window. The default is `srgb` but
-/// this can also be set to `display-p3` to use the Display P3 colorspace.
+/// The color space to use when interpreting terminal colors. "Terminal colors"
+/// refers to colors specified in your configuration and colors produced by
+/// direct-color SGR sequences.
///
-/// Changing this value at runtime will only affect new windows.
+/// Valid values:
+///
+/// * `srgb` - Interpret colors in the sRGB color space. This is the default.
+/// * `display-p3` - Interpret colors in the Display P3 color space.
///
-/// This setting is only supported on macOS.
+/// This setting is currently only supported on macOS.
@"window-colorspace": WindowColorspace = .srgb,
/// The initial window size. This size is in terminal grid cells by default.
@@ -1324,7 +1447,7 @@ keybind: Keybinds = .{},
@"resize-overlay-duration": Duration = .{ .duration = 750 * std.time.ns_per_ms },
/// If true, when there are multiple split panes, the mouse selects the pane
-/// that is focused. This only applies to the currently focused window; i.e.
+/// that is focused. This only applies to the currently focused window; e.g.
/// mousing over a split in an unfocused window will not focus that split
/// and bring the window to front.
///
@@ -1368,7 +1491,7 @@ keybind: Keybinds = .{},
/// and a minor amount of user interaction).
@"title-report": bool = false,
-/// The total amount of bytes that can be used for image data (i.e. the Kitty
+/// The total amount of bytes that can be used for image data (e.g. the Kitty
/// image protocol) per terminal screen. The maximum value is 4,294,967,295
/// (4GiB). The default is 320MB. If this is set to zero, then all image
/// protocols will be disabled.
@@ -1378,24 +1501,19 @@ keybind: Keybinds = .{},
@"image-storage-limit": u32 = 320 * 1000 * 1000,
/// Whether to automatically copy selected text to the clipboard. `true`
-/// will prefer to copy to the selection clipboard if supported by the
-/// OS, otherwise it will copy to the system clipboard.
+/// will prefer to copy to the selection clipboard, otherwise it will copy to
+/// the system clipboard.
///
/// The value `clipboard` will always copy text to the selection clipboard
-/// (for supported systems) as well as the system clipboard. This is sometimes
-/// a preferred behavior on Linux.
+/// as well as the system clipboard.
///
-/// Middle-click paste will always use the selection clipboard on Linux
-/// and the system clipboard on macOS. Middle-click paste is always enabled
-/// even if this is `false`.
+/// Middle-click paste will always use the selection clipboard. Middle-click
+/// paste is always enabled even if this is `false`.
///
-/// The default value is true on Linux and false on macOS. macOS copy on
-/// select behavior is not typical for applications so it is disabled by
-/// default. On Linux, this is a standard behavior so it is enabled by
-/// default.
+/// The default value is true on Linux and macOS.
@"copy-on-select": CopyOnSelect = switch (builtin.os.tag) {
.linux => .true,
- .macos => .false,
+ .macos => .true,
else => .false,
},
@@ -1561,6 +1679,23 @@ keybind: Keybinds = .{},
/// Set it to false for the quick terminal to remain open even when it loses focus.
@"quick-terminal-autohide": bool = true,
+/// This configuration option determines the behavior of the quick terminal
+/// when switching between macOS spaces. macOS spaces are virtual desktops
+/// that can be manually created or are automatically created when an
+/// application is in full-screen mode.
+///
+/// Valid values are:
+///
+/// * `move` - When switching to another space, the quick terminal will
+/// also moved to the current space.
+///
+/// * `remain` - The quick terminal will stay only in the space where it
+/// was originally opened and will not follow when switching to another
+/// space.
+///
+/// The default value is `move`.
+@"quick-terminal-space-behavior": QuickTerminalSpaceBehavior = .move,
+
/// Whether to enable shell integration auto-injection or not. Shell integration
/// greatly enhances the terminal experience by enabling a number of features:
///
@@ -1587,7 +1722,9 @@ keybind: Keybinds = .{},
/// The default value is `detect`.
@"shell-integration": ShellIntegration = .detect,
-/// Shell integration features to enable if shell integration itself is enabled.
+/// Shell integration features to enable. These require our shell integration
+/// to be loaded, either automatically via shell-integration or manually.
+///
/// The format of this is a list of features to enable separated by commas. If
/// you prefix a feature with `no-` then it is disabled. If you omit a feature,
/// its default value is used, so you must explicitly disable features you don't
@@ -1616,7 +1753,7 @@ keybind: Keybinds = .{},
///
/// * `none` - OSC 4/10/11 queries receive no reply
///
-/// * `8-bit` - Color components are return unscaled, i.e. `rr/gg/bb`
+/// * `8-bit` - Color components are return unscaled, e.g. `rr/gg/bb`
///
/// * `16-bit` - Color components are returned scaled, e.g. `rrrr/gggg/bbbb`
///
@@ -1677,6 +1814,31 @@ keybind: Keybinds = .{},
/// open terminals.
@"custom-shader-animation": CustomShaderAnimation = .true,
+/// Control the in-app notifications that Ghostty shows.
+///
+/// On Linux (GTK), in-app notifications show up as toasts. Toasts appear
+/// overlaid on top of the terminal window. They are used to show information
+/// that is not critical but may be important.
+///
+/// Possible notifications are:
+///
+/// - `clipboard-copy` (default: true) - Show a notification when text is copied
+/// to the clipboard.
+///
+/// To specify a notification to enable, specify the name of the notification.
+/// To specify a notification to disable, prefix the name with `no-`. For
+/// example, to disable `clipboard-copy`, set this configuration to
+/// `no-clipboard-copy`. To enable it, set this configuration to `clipboard-copy`.
+///
+/// Multiple notifications can be enabled or disabled by separating them
+/// with a comma.
+///
+/// A value of "false" will disable all notifications. A value of "true" will
+/// enable all notifications.
+///
+/// This configuration only applies to GTK.
+@"app-notifications": AppNotifications = .{},
+
/// If anything other than false, fullscreen mode on macOS will not use the
/// native fullscreen, but make the window fullscreen without animations and
/// using a new space. It's faster than the native fullscreen mode since it
@@ -1694,9 +1856,14 @@ keybind: Keybinds = .{},
///
/// Allowable values are:
///
-/// * `visible-menu` - Use non-native macOS fullscreen, keep the menu bar visible
/// * `true` - Use non-native macOS fullscreen, hide the menu bar
/// * `false` - Use native macOS fullscreen
+/// * `visible-menu` - Use non-native macOS fullscreen, keep the menu bar
+/// visible
+/// * `padded-notch` - Use non-native macOS fullscreen, hide the menu bar,
+/// but ensure the window is not obscured by the notch on applicable
+/// devices. The area around the notch will remain transparent currently,
+/// but in the future we may fill it with the window background color.
///
/// Changing this option at runtime works, but will only apply to the next
/// time the window is made fullscreen. If a window is already fullscreen,
@@ -1715,7 +1882,7 @@ keybind: Keybinds = .{},
/// typical for a macOS application and may not work well with all themes.
///
/// The "transparent" style will also update in real-time to dynamic
-/// changes to the window background color, i.e. via OSC 11. To make this
+/// changes to the window background color, e.g. via OSC 11. To make this
/// more aesthetically pleasing, this only happens if the terminal is
/// a window, tab, or split that borders the top of the window. This
/// avoids a disjointed appearance where the titlebar color changes
@@ -1731,9 +1898,12 @@ keybind: Keybinds = .{},
/// The "hidden" style hides the titlebar. Unlike `window-decoration = false`,
/// however, it does not remove the frame from the window or cause it to have
/// squared corners. Changing to or from this option at run-time may affect
-/// existing windows in buggy ways. The top titlebar area of the window will
-/// continue to drag the window around and you will not be able to use
-/// the mouse for terminal events in this space.
+/// existing windows in buggy ways.
+///
+/// When "hidden", the top titlebar area can no longer be used for dragging
+/// the window. To drag the window, you can use option+click on the resizable
+/// areas of the frame to drag the window. This is a standard macOS behavior
+/// and not something Ghostty enables.
///
/// The default value is "transparent". This is an opinionated choice
/// but its one I think is the most aesthetically pleasing and works in
@@ -1782,7 +1952,7 @@ keybind: Keybinds = .{},
/// - U.S. International
///
/// Note that if an *Option*-sequence doesn't produce a printable character, it
-/// will be treated as *Alt* regardless of this setting. (i.e. `alt+ctrl+a`).
+/// will be treated as *Alt* regardless of this setting. (e.g. `alt+ctrl+a`).
///
/// Explicit values that can be set:
///
@@ -1804,6 +1974,26 @@ keybind: Keybinds = .{},
/// find false more visually appealing.
@"macos-window-shadow": bool = true,
+/// If true, the macOS icon in the dock and app switcher will be hidden. This is
+/// mainly intended for those primarily using the quick-terminal mode.
+///
+/// Note that setting this to true means that keyboard layout changes
+/// will no longer be automatic.
+///
+/// Control whether macOS app is excluded from the dock and app switcher,
+/// a "hidden" state. This is mainly intended for those primarily using
+/// quick-terminal mode, but is a general configuration for any use
+/// case.
+///
+/// Available values:
+///
+/// * `never` - The macOS app is never hidden.
+/// * `always` - The macOS app is always hidden.
+///
+/// Note: When the macOS application is hidden, keyboard layout changes
+/// will no longer be automatic. This is a limitation of macOS.
+@"macos-hidden": MacHidden = .never,
+
/// If true, Ghostty on macOS will automatically enable the "Secure Input"
/// feature when it detects that a password prompt is being displayed.
///
@@ -1844,6 +2034,9 @@ keybind: Keybinds = .{},
/// Valid values:
///
/// * `official` - Use the official Ghostty icon.
+/// * `blueprint`, `chalkboard`, `microchip`, `glass`, `holographic`,
+/// `paper`, `retro`, `xray` - Official variants of the Ghostty icon
+/// hand-created by artists (no AI).
/// * `custom-style` - Use the official Ghostty icon but with custom
/// styles applied to various layers. The custom styles must be
/// specified using the additional `macos-icon`-prefixed configurations.
@@ -1954,6 +2147,18 @@ keybind: Keybinds = .{},
/// must always be able to move themselves into an isolated cgroup.
@"linux-cgroup-hard-fail": bool = false,
+/// Enable or disable GTK's OpenGL debugging logs. The default is `true` for
+/// debug builds, `false` for all others.
+@"gtk-opengl-debug": bool = builtin.mode == .Debug,
+
+/// After GTK 4.14.0, we need to force the GSK renderer to OpenGL as the default
+/// GSK renderer is broken on some systems. If you would like to override
+/// that bekavior, set `gtk-gsk-renderer=default` and either use your system's
+/// default GSK renderer, or set the GSK_RENDERER environment variable to your
+/// renderer of choice before launching Ghostty. This setting has no effect when
+/// using versions of GTK earlier than 4.14.0.
+@"gtk-gsk-renderer": GtkGskRenderer = .opengl,
+
/// If `true`, the Ghostty GTK application will run in single-instance mode:
/// each new `ghostty` process launched will result in a new window if there is
/// already a running process.
@@ -1981,22 +2186,20 @@ keybind: Keybinds = .{},
@"gtk-titlebar": bool = true,
/// Determines the side of the screen that the GTK tab bar will stick to.
-/// Top, bottom, left, right, and hidden are supported. The default is top.
+/// Top, bottom, and hidden are supported. The default is top.
///
-/// If this option has value `left` or `right` when using Adwaita, it falls
-/// back to `top`. `hidden`, meaning that tabs don't exist, is not supported
-/// without using Adwaita, falling back to `top`.
-///
-/// When `hidden` is set and Adwaita is enabled, a tab button displaying the
-/// number of tabs will appear in the title bar. It has the ability to open a
-/// tab overview for displaying tabs. Alternatively, you can use the
-/// `toggle_tab_overview` action in a keybind if your window doesn't have a
-/// title bar, or you can switch tabs with keybinds.
+/// When `hidden` is set, a tab button displaying the number of tabs will appear
+/// in the title bar. It has the ability to open a tab overview for displaying
+/// tabs. Alternatively, you can use the `toggle_tab_overview` action in a
+/// keybind if your window doesn't have a title bar, or you can switch tabs
+/// with keybinds.
@"gtk-tabs-location": GtkTabsLocation = .top,
-/// Determines the appearance of the top and bottom bars when using the
-/// Adwaita tab bar. This requires `gtk-adwaita` to be enabled (it is
-/// by default).
+/// If this is `true`, the titlebar will be hidden when the window is maximized,
+/// and shown when the titlebar is unmaximized. GTK only.
+@"gtk-titlebar-hide-when-maximized": bool = false,
+
+/// Determines the appearance of the top and bottom bars tab bar.
///
/// Valid values are:
///
@@ -2006,30 +2209,7 @@ keybind: Keybinds = .{},
/// more subtle border.
///
/// Changing this value at runtime will only affect new windows.
-@"adw-toolbar-style": AdwToolbarStyle = .raised,
-
-/// Control the toasts that Ghostty shows. Toasts are small notifications
-/// that appear overlaid on top of the terminal window. They are used to
-/// show information that is not critical but may be important.
-///
-/// Possible toasts are:
-///
-/// - `clipboard-copy` (default: true) - Show a toast when text is copied
-/// to the clipboard.
-///
-/// To specify a toast to enable, specify the name of the toast. To specify
-/// a toast to disable, prefix the name with `no-`. For example, to disable
-/// the clipboard-copy toast, set this configuration to `no-clipboard-copy`.
-/// To enable the clipboard-copy toast, set this configuration to
-/// `clipboard-copy`.
-///
-/// Multiple toasts can be enabled or disabled by separating them with a comma.
-///
-/// A value of "false" will disable all toasts. A value of "true" will
-/// enable all toasts.
-///
-/// This configuration only applies to GTK with Adwaita enabled.
-@"adw-toast": AdwToast = .{},
+@"gtk-toolbar-style": GtkToolbarStyle = .raised,
/// If `true` (default), then the Ghostty GTK tabs will be "wide." Wide tabs
/// are the new typical Gnome style where tabs fill their available space.
@@ -2037,20 +2217,6 @@ keybind: Keybinds = .{},
/// which is the old style.
@"gtk-wide-tabs": bool = true,
-/// If `true` (default), Ghostty will enable Adwaita theme support. This
-/// will make `window-theme` work properly and will also allow Ghostty to
-/// properly respond to system theme changes, light/dark mode changing, etc.
-/// This requires a GTK4 desktop with a GTK4 theme.
-///
-/// If you are running GTK3 or have a GTK3 theme, you may have to set this
-/// to false to get your theme picked up properly. Having this set to true
-/// with GTK3 should not cause any problems, but it may not work exactly as
-/// expected.
-///
-/// This configuration only has an effect if Ghostty was built with
-/// Adwaita support.
-@"gtk-adwaita": bool = true,
-
/// Custom CSS files to be loaded.
///
/// This configuration can be repeated multiple times to load multiple files.
@@ -2269,13 +2435,13 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
try result.keybind.set.put(
alloc,
.{ .key = .{ .translated = .j }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
- .{ .write_scrollback_file = .paste },
+ .{ .write_screen_file = .paste },
);
try result.keybind.set.put(
alloc,
.{ .key = .{ .translated = .j }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true, .alt = true }) },
- .{ .write_scrollback_file = .open },
+ .{ .write_screen_file = .open },
);
// Expand Selection
@@ -2367,6 +2533,11 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
.{ .key = .{ .translated = .t }, .mods = .{ .ctrl = true, .shift = true } },
.{ .new_tab = {} },
);
+ try result.keybind.set.put(
+ alloc,
+ .{ .key = .{ .translated = .w }, .mods = .{ .ctrl = true, .shift = true } },
+ .{ .close_tab = {} },
+ );
try result.keybind.set.put(
alloc,
.{ .key = .{ .translated = .left }, .mods = .{ .ctrl = true, .shift = true } },
@@ -2632,6 +2803,11 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
.{ .key = .{ .translated = .w }, .mods = .{ .super = true } },
.{ .close_surface = {} },
);
+ try result.keybind.set.put(
+ alloc,
+ .{ .key = .{ .translated = .w }, .mods = .{ .super = true, .alt = true } },
+ .{ .close_tab = {} },
+ );
try result.keybind.set.put(
alloc,
.{ .key = .{ .translated = .w }, .mods = .{ .super = true, .shift = true } },
@@ -2749,6 +2925,13 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
.{ .toggle_fullscreen = {} },
);
+ // Selection clipboard paste, matches Terminal.app
+ try result.keybind.set.put(
+ alloc,
+ .{ .key = .{ .translated = .v }, .mods = .{ .super = true, .shift = true } },
+ .{ .paste_from_selection = {} },
+ );
+
// "Natural text editing" keybinds. This forces these keys to go back
// to legacy encoding (not fixterms). It seems macOS users more than
// others are used to these keys so we set them as defaults. If
@@ -2767,7 +2950,7 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
try result.keybind.set.put(
alloc,
.{ .key = .{ .translated = .backspace }, .mods = .{ .super = true } },
- .{ .esc = "\x15" },
+ .{ .text = "\\x15" },
);
try result.keybind.set.put(
alloc,
@@ -3954,6 +4137,7 @@ pub const NonNativeFullscreen = enum(c_int) {
false,
true,
@"visible-menu",
+ @"padded-notch",
};
/// Valid values for macos-option-as-alt.
@@ -3970,6 +4154,11 @@ pub const WindowPaddingColor = enum {
@"extend-always",
};
+pub const WindowSubtitle = enum {
+ false,
+ @"working-directory",
+};
+
/// Color represents a color using RGB.
///
/// This is a packed struct so that the C API to read color values just
@@ -5569,6 +5758,12 @@ pub const MacTitlebarProxyIcon = enum {
hidden,
};
+/// See macos-hidden
+pub const MacHidden = enum {
+ never,
+ always,
+};
+
/// See macos-icon
///
/// Note: future versions of Ghostty can support a custom icon with
@@ -5576,6 +5771,14 @@ pub const MacTitlebarProxyIcon = enum {
/// format at all.
pub const MacAppIcon = enum {
official,
+ blueprint,
+ chalkboard,
+ microchip,
+ glass,
+ holographic,
+ paper,
+ retro,
+ xray,
@"custom-style",
};
@@ -5598,20 +5801,18 @@ pub const GtkSingleInstance = enum {
pub const GtkTabsLocation = enum {
top,
bottom,
- left,
- right,
hidden,
};
-/// See adw-toolbar-style
-pub const AdwToolbarStyle = enum {
+/// See gtk-toolbar-style
+pub const GtkToolbarStyle = enum {
flat,
raised,
@"raised-border",
};
-/// See adw-toast
-pub const AdwToast = packed struct {
+/// See app-notifications
+pub const AppNotifications = packed struct {
@"clipboard-copy": bool = true,
};
@@ -5677,12 +5878,32 @@ pub const QuickTerminalScreen = enum {
@"macos-menu-bar",
};
+// See quick-terminal-space-behavior
+pub const QuickTerminalSpaceBehavior = enum {
+ remain,
+ move,
+};
+
/// See grapheme-width-method
pub const GraphemeWidthMethod = enum {
legacy,
unicode,
};
+/// See alpha-blending
+pub const AlphaBlending = enum {
+ native,
+ linear,
+ @"linear-corrected",
+
+ pub fn isLinear(self: AlphaBlending) bool {
+ return switch (self) {
+ .native => false,
+ .linear, .@"linear-corrected" => true,
+ };
+ }
+};
+
/// See freetype-load-flag
pub const FreetypeLoadFlags = packed struct {
// The defaults here at the time of writing this match the defaults
@@ -5708,7 +5929,7 @@ pub const AutoUpdate = enum {
download,
};
-/// See background-blur-radius
+/// See background-blur
pub const BackgroundBlur = union(enum) {
false,
true,
@@ -5731,6 +5952,14 @@ pub const BackgroundBlur = union(enum) {
) catch return error.InvalidValue };
}
+ pub fn enabled(self: BackgroundBlur) bool {
+ return switch (self) {
+ .false => false,
+ .true => true,
+ .radius => |v| v > 0,
+ };
+ }
+
pub fn cval(self: BackgroundBlur) u8 {
return switch (self) {
.false => 0,
@@ -5772,6 +6001,62 @@ pub const BackgroundBlur = union(enum) {
}
};
+/// See window-decoration
+pub const WindowDecoration = enum {
+ auto,
+ client,
+ server,
+ none,
+
+ pub fn parseCLI(input_: ?[]const u8) !WindowDecoration {
+ const input = input_ orelse return .auto;
+
+ return if (cli.args.parseBool(input)) |b|
+ if (b) .auto else .none
+ else |_| if (std.meta.stringToEnum(WindowDecoration, input)) |v|
+ v
+ else
+ error.InvalidValue;
+ }
+
+ test "parse WindowDecoration" {
+ const testing = std.testing;
+
+ {
+ const v = try WindowDecoration.parseCLI(null);
+ try testing.expectEqual(WindowDecoration.auto, v);
+ }
+ {
+ const v = try WindowDecoration.parseCLI("true");
+ try testing.expectEqual(WindowDecoration.auto, v);
+ }
+ {
+ const v = try WindowDecoration.parseCLI("false");
+ try testing.expectEqual(WindowDecoration.none, v);
+ }
+ {
+ const v = try WindowDecoration.parseCLI("server");
+ try testing.expectEqual(WindowDecoration.server, v);
+ }
+ {
+ const v = try WindowDecoration.parseCLI("client");
+ try testing.expectEqual(WindowDecoration.client, v);
+ }
+ {
+ const v = try WindowDecoration.parseCLI("auto");
+ try testing.expectEqual(WindowDecoration.auto, v);
+ }
+ {
+ const v = try WindowDecoration.parseCLI("none");
+ try testing.expectEqual(WindowDecoration.none, v);
+ }
+ {
+ try testing.expectError(error.InvalidValue, WindowDecoration.parseCLI(""));
+ try testing.expectError(error.InvalidValue, WindowDecoration.parseCLI("aaaa"));
+ }
+ }
+};
+
/// See theme
pub const Theme = struct {
light: []const u8,
@@ -6101,6 +6386,12 @@ pub const WindowPadding = struct {
}
};
+/// See the `gtk-gsk-renderer` config.
+pub const GtkGskRenderer = enum {
+ default,
+ opengl,
+};
+
test "parse duration" {
inline for (Duration.units) |unit| {
var buf: [16]u8 = undefined;
diff --git a/src/config/RepeatableStringMap.zig b/src/config/RepeatableStringMap.zig
new file mode 100644
index 0000000000..6f143e95d7
--- /dev/null
+++ b/src/config/RepeatableStringMap.zig
@@ -0,0 +1,198 @@
+/// RepeatableStringMap is a key/value that can be repeated to accumulate a
+/// string map. This isn't called "StringMap" because I find that sometimes
+/// leads to confusion that it _accepts_ a map such as JSON dict.
+const RepeatableStringMap = @This();
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const formatterpkg = @import("formatter.zig");
+
+const Map = std.ArrayHashMapUnmanaged(
+ [:0]const u8,
+ [:0]const u8,
+ std.array_hash_map.StringContext,
+ true,
+);
+
+// Allocator for the list is the arena for the parent config.
+map: Map = .{},
+
+pub fn parseCLI(
+ self: *RepeatableStringMap,
+ alloc: Allocator,
+ input: ?[]const u8,
+) !void {
+ const value = input orelse return error.ValueRequired;
+
+ // Empty value resets the list. We don't need to free our values because
+ // the allocator used is always an arena.
+ if (value.len == 0) {
+ self.map.clearRetainingCapacity();
+ return;
+ }
+
+ const index = std.mem.indexOfScalar(
+ u8,
+ value,
+ '=',
+ ) orelse return error.ValueRequired;
+
+ const key = std.mem.trim(u8, value[0..index], &std.ascii.whitespace);
+ const val = std.mem.trim(u8, value[index + 1 ..], &std.ascii.whitespace);
+
+ const key_copy = try alloc.dupeZ(u8, key);
+ errdefer alloc.free(key_copy);
+
+ // Empty value removes the key from the map.
+ if (val.len == 0) {
+ _ = self.map.orderedRemove(key_copy);
+ alloc.free(key_copy);
+ return;
+ }
+
+ const val_copy = try alloc.dupeZ(u8, val);
+ errdefer alloc.free(val_copy);
+
+ try self.map.put(alloc, key_copy, val_copy);
+}
+
+/// Deep copy of the struct. Required by Config.
+pub fn clone(
+ self: *const RepeatableStringMap,
+ alloc: Allocator,
+) Allocator.Error!RepeatableStringMap {
+ var map: Map = .{};
+ try map.ensureTotalCapacity(alloc, self.map.count());
+
+ errdefer {
+ var it = map.iterator();
+ while (it.next()) |entry| {
+ alloc.free(entry.key_ptr.*);
+ alloc.free(entry.value_ptr.*);
+ }
+ map.deinit(alloc);
+ }
+
+ var it = self.map.iterator();
+ while (it.next()) |entry| {
+ const key = try alloc.dupeZ(u8, entry.key_ptr.*);
+ const value = try alloc.dupeZ(u8, entry.value_ptr.*);
+ map.putAssumeCapacity(key, value);
+ }
+
+ return .{ .map = map };
+}
+
+/// The number of items in the map
+pub fn count(self: RepeatableStringMap) usize {
+ return self.map.count();
+}
+
+/// Iterator over the entries in the map.
+pub fn iterator(self: RepeatableStringMap) Map.Iterator {
+ return self.map.iterator();
+}
+
+/// Compare if two of our value are requal. Required by Config.
+pub fn equal(self: RepeatableStringMap, other: RepeatableStringMap) bool {
+ if (self.map.count() != other.map.count()) return false;
+ var it = self.map.iterator();
+ while (it.next()) |entry| {
+ const value = other.map.get(entry.key_ptr.*) orelse return false;
+ if (!std.mem.eql(u8, entry.value_ptr.*, value)) return false;
+ } else return true;
+}
+
+/// Used by formatter
+pub fn formatEntry(self: RepeatableStringMap, formatter: anytype) !void {
+ // If no items, we want to render an empty field.
+ if (self.map.count() == 0) {
+ try formatter.formatEntry(void, {});
+ return;
+ }
+
+ var it = self.map.iterator();
+ while (it.next()) |entry| {
+ var buf: [256]u8 = undefined;
+ const value = std.fmt.bufPrint(&buf, "{s}={s}", .{ entry.key_ptr.*, entry.value_ptr.* }) catch |err| switch (err) {
+ error.NoSpaceLeft => return error.OutOfMemory,
+ };
+ try formatter.formatEntry([]const u8, value);
+ }
+}
+
+test "RepeatableStringMap: parseCLI" {
+ const testing = std.testing;
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ var map: RepeatableStringMap = .{};
+
+ try testing.expectError(error.ValueRequired, map.parseCLI(alloc, "A"));
+
+ try map.parseCLI(alloc, "A=B");
+ try map.parseCLI(alloc, "B=C");
+ try testing.expectEqual(@as(usize, 2), map.count());
+
+ try map.parseCLI(alloc, "");
+ try testing.expectEqual(@as(usize, 0), map.count());
+
+ try map.parseCLI(alloc, "A=B");
+ try testing.expectEqual(@as(usize, 1), map.count());
+ try map.parseCLI(alloc, "A=C");
+ try testing.expectEqual(@as(usize, 1), map.count());
+}
+
+test "RepeatableStringMap: formatConfig empty" {
+ const testing = std.testing;
+ var buf = std.ArrayList(u8).init(testing.allocator);
+ defer buf.deinit();
+
+ var list: RepeatableStringMap = .{};
+ try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
+ try std.testing.expectEqualSlices(u8, "a = \n", buf.items);
+}
+
+test "RepeatableStringMap: formatConfig single item" {
+ const testing = std.testing;
+
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ {
+ var buf = std.ArrayList(u8).init(testing.allocator);
+ defer buf.deinit();
+ var map: RepeatableStringMap = .{};
+ try map.parseCLI(alloc, "A=B");
+ try map.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
+ try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.items);
+ }
+ {
+ var buf = std.ArrayList(u8).init(testing.allocator);
+ defer buf.deinit();
+ var map: RepeatableStringMap = .{};
+ try map.parseCLI(alloc, " A = B ");
+ try map.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
+ try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.items);
+ }
+}
+
+test "RepeatableStringMap: formatConfig multiple items" {
+ const testing = std.testing;
+
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ {
+ var buf = std.ArrayList(u8).init(testing.allocator);
+ defer buf.deinit();
+ var list: RepeatableStringMap = .{};
+ try list.parseCLI(alloc, "A=B");
+ try list.parseCLI(alloc, "B = C");
+ try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
+ try std.testing.expectEqualSlices(u8, "a = A=B\na = B=C\n", buf.items);
+ }
+}
diff --git a/src/config/c_get.zig b/src/config/c_get.zig
index 6804b0ae0c..251a95e772 100644
--- a/src/config/c_get.zig
+++ b/src/config/c_get.zig
@@ -192,21 +192,21 @@ test "c_get: background-blur" {
defer c.deinit();
{
- c.@"background-blur-radius" = .false;
+ c.@"background-blur" = .false;
var cval: u8 = undefined;
- try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
+ try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
try testing.expectEqual(0, cval);
}
{
- c.@"background-blur-radius" = .true;
+ c.@"background-blur" = .true;
var cval: u8 = undefined;
- try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
+ try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
try testing.expectEqual(20, cval);
}
{
- c.@"background-blur-radius" = .{ .radius = 42 };
+ c.@"background-blur" = .{ .radius = 42 };
var cval: u8 = undefined;
- try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
+ try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
try testing.expectEqual(42, cval);
}
}
diff --git a/src/config/theme.zig b/src/config/theme.zig
index b851ec3d46..2d206e1f64 100644
--- a/src/config/theme.zig
+++ b/src/config/theme.zig
@@ -104,6 +104,10 @@ pub const LocationIterator = struct {
/// Due to the way allocations are handled, an Arena allocator (or another
/// similar allocator implementation) should be used. It may not be safe to
/// free the returned allocations.
+///
+/// This will never return anything other than a handle to a regular file. If
+/// the theme resolves to something other than a regular file a diagnostic entry
+/// will be added to the list and null will be returned.
pub fn open(
arena_alloc: Allocator,
theme: []const u8,
@@ -119,6 +123,29 @@ pub fn open(
theme,
diags,
) orelse return null;
+ const stat = file.stat() catch |err| {
+ try diags.append(arena_alloc, .{
+ .message = try std.fmt.allocPrintZ(
+ arena_alloc,
+ "not reading theme from \"{s}\": {}",
+ .{ theme, err },
+ ),
+ });
+ return null;
+ };
+ switch (stat.kind) {
+ .file => {},
+ else => {
+ try diags.append(arena_alloc, .{
+ .message = try std.fmt.allocPrintZ(
+ arena_alloc,
+ "not reading theme from \"{s}\": it is a {s}",
+ .{ theme, @tagName(stat.kind) },
+ ),
+ });
+ return null;
+ },
+ }
return .{ .path = theme, .file = file };
}
@@ -140,9 +167,34 @@ pub fn open(
const cwd = std.fs.cwd();
while (try it.next()) |loc| {
const path = try std.fs.path.join(arena_alloc, &.{ loc.dir, theme });
- if (cwd.openFile(path, .{})) |file| return .{
- .path = path,
- .file = file,
+ if (cwd.openFile(path, .{})) |file| {
+ const stat = file.stat() catch |err| {
+ try diags.append(arena_alloc, .{
+ .message = try std.fmt.allocPrintZ(
+ arena_alloc,
+ "not reading theme from \"{s}\": {}",
+ .{ theme, err },
+ ),
+ });
+ return null;
+ };
+ switch (stat.kind) {
+ .file => {},
+ else => {
+ try diags.append(arena_alloc, .{
+ .message = try std.fmt.allocPrintZ(
+ arena_alloc,
+ "not reading theme from \"{s}\": it is a {s}",
+ .{ theme, @tagName(stat.kind) },
+ ),
+ });
+ return null;
+ },
+ }
+ return .{
+ .path = path,
+ .file = file,
+ };
} else |err| switch (err) {
// Not an error, just continue to the next location.
error.FileNotFound => {},
diff --git a/src/config/url.zig b/src/config/url.zig
index dcc1902e66..3a7034e0ad 100644
--- a/src/config/url.zig
+++ b/src/config/url.zig
@@ -26,7 +26,7 @@ pub const regex =
"(?:" ++ url_schemes ++
\\)(?:
++ ipv6_url_pattern ++
- \\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?
+ \\"
+ \\" THIS FILE IS AUTO-GENERATED
+ \\
+ \\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/* set ft=ghostty
+ \\
+;
pub const ftplugin =
\\" Vim filetype plugin file
\\" Language: Ghostty config file
@@ -31,13 +40,19 @@ pub const ftplugin =
\\
;
pub const compiler =
+ \\" Vim compiler file
+ \\" Language: Ghostty config file
+ \\" Maintainer: Ghostty
+ \\"
+ \\" THIS FILE IS AUTO-GENERATED
+ \\
\\if exists("current_compiler")
\\ finish
\\endif
\\let current_compiler = "ghostty"
\\
- \\CompilerSet makeprg=ghostty\ +validate-config
- \\CompilerSet errorformat=%f:%l:%m
+ \\CompilerSet makeprg=ghostty\ +validate-config\ --config-file=%:S
+ \\CompilerSet errorformat=%f:%l:%m,%m
\\
;
diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig
index 6661295f39..3749b48241 100644
--- a/src/font/face/coretext.zig
+++ b/src/font/face/coretext.zig
@@ -343,13 +343,12 @@ pub const Face = struct {
} = if (!self.isColorGlyph(glyph_index)) .{
.color = false,
.depth = 1,
- .space = try macos.graphics.ColorSpace.createDeviceGray(),
- .context_opts = @intFromEnum(macos.graphics.BitmapInfo.alpha_mask) &
- @intFromEnum(macos.graphics.ImageAlphaInfo.only),
+ .space = try macos.graphics.ColorSpace.createNamed(.linearGray),
+ .context_opts = @intFromEnum(macos.graphics.ImageAlphaInfo.only),
} else .{
.color = true,
.depth = 4,
- .space = try macos.graphics.ColorSpace.createDeviceRGB(),
+ .space = try macos.graphics.ColorSpace.createNamed(.displayP3),
.context_opts = @intFromEnum(macos.graphics.BitmapInfo.byte_order_32_little) |
@intFromEnum(macos.graphics.ImageAlphaInfo.premultiplied_first),
};
diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig
index ba7caa26a7..2cd3d929b8 100644
--- a/src/font/sprite/Box.zig
+++ b/src/font/sprite/Box.zig
@@ -184,6 +184,10 @@ const SmoothMosaic = packed struct(u10) {
}
};
+// Octant range, inclusive
+const octant_min = 0x1cd00;
+const octant_max = 0x1cde5;
+
// Utility names for common fractions
const one_eighth: f64 = 0.125;
const one_quarter: f64 = 0.25;
@@ -581,6 +585,8 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void
0x1fb00...0x1fb3b => self.draw_sextant(canvas, cp),
+ octant_min...octant_max => self.draw_octant(canvas, cp),
+
// '🬼'
0x1fb3c => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from(
\\...
@@ -2484,6 +2490,65 @@ fn draw_sextant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[1], self.metrics.cell_width, self.metrics.cell_height);
}
+fn draw_octant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
+ assert(cp >= octant_min and cp <= octant_max);
+
+ // Octant representation. We use the funny numeric string keys
+ // so its easier to parse the actual name used in the Symbols for
+ // Legacy Computing spec.
+ const Octant = packed struct(u8) {
+ @"1": bool = false,
+ @"2": bool = false,
+ @"3": bool = false,
+ @"4": bool = false,
+ @"5": bool = false,
+ @"6": bool = false,
+ @"7": bool = false,
+ @"8": bool = false,
+ };
+
+ // Parse the octant data. This is all done at comptime so this is
+ // static data that is embedded in the binary.
+ const octants_len = octant_max - octant_min + 1;
+ const octants: [octants_len]Octant = comptime octants: {
+ @setEvalBranchQuota(10_000);
+
+ var result: [octants_len]Octant = .{.{}} ** octants_len;
+ var i: usize = 0;
+
+ const data = @embedFile("octants.txt");
+ var it = std.mem.splitScalar(u8, data, '\n');
+ while (it.next()) |line| {
+ // Skip comments
+ if (line.len == 0 or line[0] == '#') continue;
+
+ const current = &result[i];
+ i += 1;
+
+ // Octants are in the format "BLOCK OCTANT-1235". The numbers
+ // at the end are keys into our packed struct. Since we're
+ // at comptime we can metaprogram it all.
+ const idx = std.mem.indexOfScalar(u8, line, '-').?;
+ for (line[idx + 1 ..]) |c| @field(current, &.{c}) = true;
+ }
+
+ assert(i == octants_len);
+ break :octants result;
+ };
+
+ const x_halfs = self.xHalfs();
+ const y_quads = self.yQuads();
+ const oct = octants[cp - octant_min];
+ if (oct.@"1") self.rect(canvas, 0, 0, x_halfs[0], y_quads[0]);
+ if (oct.@"2") self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_quads[0]);
+ if (oct.@"3") self.rect(canvas, 0, y_quads[0], x_halfs[0], y_quads[1]);
+ if (oct.@"4") self.rect(canvas, x_halfs[1], y_quads[0], self.metrics.cell_width, y_quads[1]);
+ if (oct.@"5") self.rect(canvas, 0, y_quads[1], x_halfs[0], y_quads[2]);
+ if (oct.@"6") self.rect(canvas, x_halfs[1], y_quads[1], self.metrics.cell_width, y_quads[2]);
+ if (oct.@"7") self.rect(canvas, 0, y_quads[2], x_halfs[0], self.metrics.cell_height);
+ if (oct.@"8") self.rect(canvas, x_halfs[1], y_quads[2], self.metrics.cell_width, self.metrics.cell_height);
+}
+
fn xHalfs(self: Box) [2]u32 {
return .{
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2))),
@@ -2500,6 +2565,21 @@ fn yThirds(self: Box) [2]u32 {
};
}
+// assume octants might be striped across multiple rows of cells. to maximize
+// distance between excess pixellines, we want (1) an arbitrary region (there
+// will be a pattern of 1'-3-1'-3-1'-3 no matter what), (2) discontiguous
+// regions (0 and 2 or 1 and 3), and (3) an arbitrary three regions (there will
+// be a pattern of 3-1-3-1-3-1 no matter what).
+fn yQuads(self: Box) [3]u32 {
+ return switch (@mod(self.metrics.cell_height, 4)) {
+ 0 => .{ self.metrics.cell_height / 4, 2 * self.metrics.cell_height / 4, 3 * self.metrics.cell_height / 4 },
+ 1 => .{ self.metrics.cell_height / 4, 2 * self.metrics.cell_height / 4 + 1, 3 * self.metrics.cell_height / 4 },
+ 2 => .{ self.metrics.cell_height / 4 + 1, 2 * self.metrics.cell_height / 4, 3 * self.metrics.cell_height / 4 + 1 },
+ 3 => .{ self.metrics.cell_height / 4 + 1, 2 * self.metrics.cell_height / 4 + 1, 3 * self.metrics.cell_height / 4 },
+ else => unreachable,
+ };
+}
+
fn draw_smooth_mosaic(
self: Box,
canvas: *font.sprite.Canvas,
@@ -3064,7 +3144,7 @@ fn testRenderAll(self: Box, alloc: Allocator, atlas: *font.Atlas) !void {
);
}
- // Symbols for Legacy Computing Supplement.
+ // Symbols for Legacy Computing Supplement: Quadrants
//
cp = 0x1cc21;
while (cp <= 0x1cc2f) : (cp += 1) {
@@ -3077,6 +3157,19 @@ fn testRenderAll(self: Box, alloc: Allocator, atlas: *font.Atlas) !void {
else => {},
}
}
+
+ // Symbols for Legacy Computing Supplement: Octants
+ cp = 0x1CD00;
+ while (cp <= 0x1CDE5) : (cp += 1) {
+ switch (cp) {
+ 0x1CD00...0x1CDE5 => _ = try self.renderGlyph(
+ alloc,
+ atlas,
+ cp,
+ ),
+ else => {},
+ }
+ }
}
test "render all sprites" {
diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig
index cebf44429b..f15423ada0 100644
--- a/src/font/sprite/Face.zig
+++ b/src/font/sprite/Face.zig
@@ -236,6 +236,8 @@ const Kind = enum {
// (Geometric Shapes)
//
0x1FBCE...0x1FBEF,
+ // (Octants)
+ 0x1CD00...0x1CDE5,
=> .box,
// Branch drawing character set, used for drawing git-like
diff --git a/src/font/sprite/octants.txt b/src/font/sprite/octants.txt
new file mode 100644
index 0000000000..db79aa2c63
--- /dev/null
+++ b/src/font/sprite/octants.txt
@@ -0,0 +1,234 @@
+# This is the list of all the octants for the Symbols for Legacy
+# Computing block. It is used at comptime to generate the lookup
+# table for drawing them since we weren't able to discern a
+# mathematical pattern for them.
+BLOCK OCTANT-3
+BLOCK OCTANT-23
+BLOCK OCTANT-123
+BLOCK OCTANT-4
+BLOCK OCTANT-14
+BLOCK OCTANT-124
+BLOCK OCTANT-34
+BLOCK OCTANT-134
+BLOCK OCTANT-234
+BLOCK OCTANT-5
+BLOCK OCTANT-15
+BLOCK OCTANT-25
+BLOCK OCTANT-125
+BLOCK OCTANT-135
+BLOCK OCTANT-235
+BLOCK OCTANT-1235
+BLOCK OCTANT-45
+BLOCK OCTANT-145
+BLOCK OCTANT-245
+BLOCK OCTANT-1245
+BLOCK OCTANT-345
+BLOCK OCTANT-1345
+BLOCK OCTANT-2345
+BLOCK OCTANT-12345
+BLOCK OCTANT-6
+BLOCK OCTANT-16
+BLOCK OCTANT-26
+BLOCK OCTANT-126
+BLOCK OCTANT-36
+BLOCK OCTANT-136
+BLOCK OCTANT-236
+BLOCK OCTANT-1236
+BLOCK OCTANT-146
+BLOCK OCTANT-246
+BLOCK OCTANT-1246
+BLOCK OCTANT-346
+BLOCK OCTANT-1346
+BLOCK OCTANT-2346
+BLOCK OCTANT-12346
+BLOCK OCTANT-56
+BLOCK OCTANT-156
+BLOCK OCTANT-256
+BLOCK OCTANT-1256
+BLOCK OCTANT-356
+BLOCK OCTANT-1356
+BLOCK OCTANT-2356
+BLOCK OCTANT-12356
+BLOCK OCTANT-456
+BLOCK OCTANT-1456
+BLOCK OCTANT-2456
+BLOCK OCTANT-12456
+BLOCK OCTANT-3456
+BLOCK OCTANT-13456
+BLOCK OCTANT-23456
+BLOCK OCTANT-17
+BLOCK OCTANT-27
+BLOCK OCTANT-127
+BLOCK OCTANT-37
+BLOCK OCTANT-137
+BLOCK OCTANT-237
+BLOCK OCTANT-1237
+BLOCK OCTANT-47
+BLOCK OCTANT-147
+BLOCK OCTANT-247
+BLOCK OCTANT-1247
+BLOCK OCTANT-347
+BLOCK OCTANT-1347
+BLOCK OCTANT-2347
+BLOCK OCTANT-12347
+BLOCK OCTANT-157
+BLOCK OCTANT-257
+BLOCK OCTANT-1257
+BLOCK OCTANT-357
+BLOCK OCTANT-2357
+BLOCK OCTANT-12357
+BLOCK OCTANT-457
+BLOCK OCTANT-1457
+BLOCK OCTANT-12457
+BLOCK OCTANT-3457
+BLOCK OCTANT-13457
+BLOCK OCTANT-23457
+BLOCK OCTANT-67
+BLOCK OCTANT-167
+BLOCK OCTANT-267
+BLOCK OCTANT-1267
+BLOCK OCTANT-367
+BLOCK OCTANT-1367
+BLOCK OCTANT-2367
+BLOCK OCTANT-12367
+BLOCK OCTANT-467
+BLOCK OCTANT-1467
+BLOCK OCTANT-2467
+BLOCK OCTANT-12467
+BLOCK OCTANT-3467
+BLOCK OCTANT-13467
+BLOCK OCTANT-23467
+BLOCK OCTANT-123467
+BLOCK OCTANT-567
+BLOCK OCTANT-1567
+BLOCK OCTANT-2567
+BLOCK OCTANT-12567
+BLOCK OCTANT-3567
+BLOCK OCTANT-13567
+BLOCK OCTANT-23567
+BLOCK OCTANT-123567
+BLOCK OCTANT-4567
+BLOCK OCTANT-14567
+BLOCK OCTANT-24567
+BLOCK OCTANT-124567
+BLOCK OCTANT-34567
+BLOCK OCTANT-134567
+BLOCK OCTANT-234567
+BLOCK OCTANT-1234567
+BLOCK OCTANT-18
+BLOCK OCTANT-28
+BLOCK OCTANT-128
+BLOCK OCTANT-38
+BLOCK OCTANT-138
+BLOCK OCTANT-238
+BLOCK OCTANT-1238
+BLOCK OCTANT-48
+BLOCK OCTANT-148
+BLOCK OCTANT-248
+BLOCK OCTANT-1248
+BLOCK OCTANT-348
+BLOCK OCTANT-1348
+BLOCK OCTANT-2348
+BLOCK OCTANT-12348
+BLOCK OCTANT-58
+BLOCK OCTANT-158
+BLOCK OCTANT-258
+BLOCK OCTANT-1258
+BLOCK OCTANT-358
+BLOCK OCTANT-1358
+BLOCK OCTANT-2358
+BLOCK OCTANT-12358
+BLOCK OCTANT-458
+BLOCK OCTANT-1458
+BLOCK OCTANT-2458
+BLOCK OCTANT-12458
+BLOCK OCTANT-3458
+BLOCK OCTANT-13458
+BLOCK OCTANT-23458
+BLOCK OCTANT-123458
+BLOCK OCTANT-168
+BLOCK OCTANT-268
+BLOCK OCTANT-1268
+BLOCK OCTANT-368
+BLOCK OCTANT-2368
+BLOCK OCTANT-12368
+BLOCK OCTANT-468
+BLOCK OCTANT-1468
+BLOCK OCTANT-12468
+BLOCK OCTANT-3468
+BLOCK OCTANT-13468
+BLOCK OCTANT-23468
+BLOCK OCTANT-568
+BLOCK OCTANT-1568
+BLOCK OCTANT-2568
+BLOCK OCTANT-12568
+BLOCK OCTANT-3568
+BLOCK OCTANT-13568
+BLOCK OCTANT-23568
+BLOCK OCTANT-123568
+BLOCK OCTANT-4568
+BLOCK OCTANT-14568
+BLOCK OCTANT-24568
+BLOCK OCTANT-124568
+BLOCK OCTANT-34568
+BLOCK OCTANT-134568
+BLOCK OCTANT-234568
+BLOCK OCTANT-1234568
+BLOCK OCTANT-178
+BLOCK OCTANT-278
+BLOCK OCTANT-1278
+BLOCK OCTANT-378
+BLOCK OCTANT-1378
+BLOCK OCTANT-2378
+BLOCK OCTANT-12378
+BLOCK OCTANT-478
+BLOCK OCTANT-1478
+BLOCK OCTANT-2478
+BLOCK OCTANT-12478
+BLOCK OCTANT-3478
+BLOCK OCTANT-13478
+BLOCK OCTANT-23478
+BLOCK OCTANT-123478
+BLOCK OCTANT-578
+BLOCK OCTANT-1578
+BLOCK OCTANT-2578
+BLOCK OCTANT-12578
+BLOCK OCTANT-3578
+BLOCK OCTANT-13578
+BLOCK OCTANT-23578
+BLOCK OCTANT-123578
+BLOCK OCTANT-4578
+BLOCK OCTANT-14578
+BLOCK OCTANT-24578
+BLOCK OCTANT-124578
+BLOCK OCTANT-34578
+BLOCK OCTANT-134578
+BLOCK OCTANT-234578
+BLOCK OCTANT-1234578
+BLOCK OCTANT-678
+BLOCK OCTANT-1678
+BLOCK OCTANT-2678
+BLOCK OCTANT-12678
+BLOCK OCTANT-3678
+BLOCK OCTANT-13678
+BLOCK OCTANT-23678
+BLOCK OCTANT-123678
+BLOCK OCTANT-4678
+BLOCK OCTANT-14678
+BLOCK OCTANT-24678
+BLOCK OCTANT-124678
+BLOCK OCTANT-34678
+BLOCK OCTANT-134678
+BLOCK OCTANT-234678
+BLOCK OCTANT-1234678
+BLOCK OCTANT-15678
+BLOCK OCTANT-25678
+BLOCK OCTANT-125678
+BLOCK OCTANT-35678
+BLOCK OCTANT-235678
+BLOCK OCTANT-1235678
+BLOCK OCTANT-45678
+BLOCK OCTANT-145678
+BLOCK OCTANT-1245678
+BLOCK OCTANT-1345678
+BLOCK OCTANT-2345678
diff --git a/src/font/sprite/testdata/Box.ppm b/src/font/sprite/testdata/Box.ppm
index 36519a1e95..0feb3ebe49 100644
Binary files a/src/font/sprite/testdata/Box.ppm and b/src/font/sprite/testdata/Box.ppm differ
diff --git a/src/global.zig b/src/global.zig
index c00ce27a4b..d5a7af630e 100644
--- a/src/global.zig
+++ b/src/global.zig
@@ -111,6 +111,9 @@ pub const GlobalState = struct {
}
}
+ // Setup our signal handlers before logging
+ initSignals();
+
// Output some debug information right away
std.log.info("ghostty version={s}", .{build_config.version_string});
std.log.info("ghostty build optimize={s}", .{build_config.mode_string});
@@ -175,6 +178,28 @@ pub const GlobalState = struct {
_ = value.deinit();
}
}
+
+ fn initSignals() void {
+ // Only posix systems.
+ if (comptime builtin.os.tag == .windows) return;
+
+ const p = std.posix;
+
+ var sa: p.Sigaction = .{
+ .handler = .{ .handler = p.SIG.IGN },
+ .mask = p.empty_sigset,
+ .flags = 0,
+ };
+
+ // We ignore SIGPIPE because it is a common signal we may get
+ // due to how we implement termio. When a terminal is closed we
+ // often write to a broken pipe to exit the read thread. This should
+ // be fixed one day but for now this helps make this a bit more
+ // robust.
+ p.sigaction(p.SIG.PIPE, &sa, null) catch |err| {
+ std.log.warn("failed to ignore SIGPIPE err={}", .{err});
+ };
+ }
};
/// Maintains the Unix resource limits that we set for our process. This
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index 64e07e85e1..f919672935 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -236,9 +236,9 @@ pub const Action = union(enum) {
/// Send an `ESC` sequence.
esc: []const u8,
- // Send the given text. Uses Zig string literal syntax. This is currently
- // not validated. If the text is invalid (i.e. contains an invalid escape
- // sequence), the error will currently only show up in logs.
+ /// Send the given text. Uses Zig string literal syntax. This is currently
+ /// not validated. If the text is invalid (i.e. contains an invalid escape
+ /// sequence), the error will currently only show up in logs.
text: []const u8,
/// Send data to the pty depending on whether cursor key mode is enabled
@@ -259,6 +259,10 @@ pub const Action = union(enum) {
paste_from_clipboard: void,
paste_from_selection: void,
+ /// Copy the URL under the cursor to the clipboard. If there is no
+ /// URL under the cursor, this does nothing.
+ copy_url_to_clipboard: void,
+
/// Increase/decrease the font size by a certain amount.
increase_font_size: f32,
decrease_font_size: f32,
@@ -280,8 +284,15 @@ pub const Action = union(enum) {
scroll_page_fractional: f32,
scroll_page_lines: i16,
- /// Adjust an existing selection in a given direction. This action
- /// does nothing if there is no active selection.
+ /// Adjust the current selection in a given direction. Does nothing if no
+ /// selection exists.
+ ///
+ /// Arguments:
+ /// - left, right, up, down, page_up, page_down, home, end,
+ /// beginning_of_line, end_of_line
+ ///
+ /// Example: Extend selection to the right
+ /// keybind = shift+right=adjust_selection:right
adjust_selection: AdjustSelection,
/// Jump the viewport forward or back by prompt. Positive number is the
@@ -337,8 +348,17 @@ pub const Action = union(enum) {
/// This only works with libadwaita enabled currently.
toggle_tab_overview: void,
- /// Create a new split in the given direction. The new split will appear in
- /// the direction given. For example `new_split:up`. Valid values are left, right, up, down and auto.
+ /// Change the title of the current focused surface via a prompt.
+ /// This only works on macOS currently.
+ prompt_surface_title: void,
+
+ /// Create a new split in the given direction.
+ ///
+ /// Arguments:
+ /// - right, down, left, up, auto (splits along the larger direction)
+ ///
+ /// Example: Create split on the right
+ /// keybind = cmd+shift+d=new_split:right
new_split: SplitDirection,
/// Focus on a split in a given direction. For example `goto_split:up`.
@@ -348,15 +368,26 @@ pub const Action = union(enum) {
/// zoom/unzoom the current split.
toggle_split_zoom: void,
- /// Resize the current split by moving the split divider in the given
- /// direction. For example `resize_split:left,10`. The valid directions are up, down, left and right.
+ /// Resize the current split in a given direction.
+ ///
+ /// Arguments:
+ /// - up, down, left, right
+ /// - the number of pixels to resize the split by
+ ///
+ /// Example: Move divider up 10 pixels
+ /// keybind = cmd+shift+up=resize_split:up,10
resize_split: SplitResizeParameter,
/// Equalize all splits in the current window
equalize_splits: void,
- /// Show, hide, or toggle the terminal inspector for the currently focused
- /// terminal.
+ /// Control the terminal inspector visibility.
+ ///
+ /// Arguments:
+ /// - toggle, show, hide
+ ///
+ /// Example: Toggle inspector visibility
+ /// keybind = cmd+i=inspector:toggle
inspector: InspectorMode,
/// Open the configuration file in the default OS editor. If your default OS
@@ -375,6 +406,10 @@ pub const Action = union(enum) {
/// configured.
close_surface: void,
+ /// Close the current tab, regardless of how many splits there may be.
+ /// This will trigger close confirmation as configured.
+ close_tab: void,
+
/// Close the window, regardless of how many tabs or splits there may be.
/// This will trigger close confirmation as configured.
close_window: void,
@@ -383,6 +418,9 @@ pub const Action = union(enum) {
/// This only works for macOS currently.
close_all_windows: void,
+ /// Toggle maximized window state. This only works on Linux.
+ toggle_maximize: void,
+
/// Toggle fullscreen mode of window.
toggle_fullscreen: void,
@@ -408,7 +446,7 @@ pub const Action = union(enum) {
/// is preserved between appearances, so you can always press the keybinding
/// to bring it back up.
///
- /// To enable the quick terminally globally so that Ghostty doesn't
+ /// To enable the quick terminal globally so that Ghostty doesn't
/// have to be focused, prefix your keybind with `global`. Example:
///
/// ```ini
@@ -433,10 +471,12 @@ pub const Action = union(enum) {
toggle_quick_terminal: void,
/// Show/hide all windows. If all windows become shown, we also ensure
- /// Ghostty is focused.
+ /// Ghostty becomes focused. When hiding all windows, focus is yielded
+ /// to the next application as determined by the OS.
+ ///
+ /// Note: When the focused surface is fullscreen, this method does nothing.
///
- /// This currently only works on macOS. When hiding all windows, we do
- /// not yield focus to the previous application.
+ /// This currently only works on macOS.
toggle_visibility: void,
/// Quit ghostty.
@@ -505,7 +545,6 @@ pub const Action = union(enum) {
pub const SplitFocusDirection = enum {
previous,
next,
-
up,
left,
down,
@@ -707,11 +746,13 @@ pub const Action = union(enum) {
.cursor_key,
.reset,
.copy_to_clipboard,
+ .copy_url_to_clipboard,
.paste_from_clipboard,
.paste_from_selection,
.increase_font_size,
.decrease_font_size,
.reset_font_size,
+ .prompt_surface_title,
.clear_screen,
.select_all,
.scroll_to_top,
@@ -726,7 +767,9 @@ pub const Action = union(enum) {
.write_screen_file,
.write_selection_file,
.close_surface,
+ .close_tab,
.close_window,
+ .toggle_maximize,
.toggle_fullscreen,
.toggle_window_decorations,
.toggle_secure_input,
@@ -1194,6 +1237,13 @@ pub const Set = struct {
/// This is a conscious decision since the primary use case of the reverse
/// map is to support GUI toolkit keyboard accelerators and no mainstream
/// GUI toolkit supports sequences.
+ ///
+ /// Performable triggers are also not present in the reverse map. This
+ /// is so that GUI toolkits don't register performable triggers as
+ /// menu shortcuts (the primary use case of the reverse map). GUI toolkits
+ /// such as GTK handle menu shortcuts too early in the event lifecycle
+ /// for performable to work so this is a conscious decision to ease the
+ /// integration with GUI toolkits.
reverse: ReverseMap = .{},
/// The entry type for the forward mapping of trigger to action.
@@ -1458,6 +1508,11 @@ pub const Set = struct {
// unbind should never go into the set, it should be handled prior
assert(action != .unbind);
+ // This is true if we're going to track this entry as
+ // a reverse mapping. There are certain scenarios we don't.
+ // See the reverse map docs for more information.
+ const track_reverse: bool = !flags.performable;
+
const gop = try self.bindings.getOrPut(alloc, t);
if (gop.found_existing) switch (gop.value_ptr.*) {
@@ -1469,7 +1524,7 @@ pub const Set = struct {
// If we have an existing binding for this trigger, we have to
// update the reverse mapping to remove the old action.
- .leaf => {
+ .leaf => if (track_reverse) {
const t_hash = t.hash();
var it = self.reverse.iterator();
while (it.next()) |reverse_entry| it: {
@@ -1486,8 +1541,9 @@ pub const Set = struct {
.flags = flags,
} };
errdefer _ = self.bindings.remove(t);
- try self.reverse.put(alloc, action, t);
- errdefer _ = self.reverse.remove(action);
+
+ if (track_reverse) try self.reverse.put(alloc, action, t);
+ errdefer if (track_reverse) self.reverse.remove(action);
}
/// Get a binding for a given trigger.
@@ -2337,6 +2393,39 @@ test "set: maintains reverse mapping" {
}
}
+test "set: performable is not part of reverse mappings" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var s: Set = .{};
+ defer s.deinit(alloc);
+
+ try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
+ {
+ const trigger = s.getTrigger(.{ .new_window = {} }).?;
+ try testing.expect(trigger.key.translated == .a);
+ }
+
+ // trigger should be non-performable
+ try s.putFlags(
+ alloc,
+ .{ .key = .{ .translated = .b } },
+ .{ .new_window = {} },
+ .{ .performable = true },
+ );
+ {
+ const trigger = s.getTrigger(.{ .new_window = {} }).?;
+ try testing.expect(trigger.key.translated == .a);
+ }
+
+ // removal of performable should do nothing
+ s.remove(alloc, .{ .key = .{ .translated = .b } });
+ {
+ const trigger = s.getTrigger(.{ .new_window = {} }).?;
+ try testing.expect(trigger.key.translated == .a);
+ }
+}
+
test "set: overriding a mapping updates reverse" {
const testing = std.testing;
const alloc = testing.allocator;
diff --git a/src/input/KeymapDarwin.zig b/src/input/KeymapDarwin.zig
index 3d81b0f4bf..154f648a68 100644
--- a/src/input/KeymapDarwin.zig
+++ b/src/input/KeymapDarwin.zig
@@ -50,10 +50,13 @@ pub const State = struct {
pub const Translation = struct {
/// The translation result. If this is a dead key state, then this will
/// be pre-edit text that can be displayed but will ultimately be replaced.
- text: []const u8,
+ text: []const u8 = "",
/// Whether the text is still composing, i.e. this is a dead key state.
- composing: bool,
+ composing: bool = false,
+
+ /// The mods that were consumed to produce this translation
+ mods: Mods = .{},
};
pub fn init() !Keymap {
@@ -122,8 +125,18 @@ pub fn translate(
out: []u8,
state: *State,
code: u16,
- mods: Mods,
+ input_mods: Mods,
) !Translation {
+ // On macOS we strip ctrl because UCKeyTranslate
+ // converts to the masked values (i.e. ctrl+c becomes 3)
+ // and we don't want that behavior in Ghostty ever. This makes
+ // this file not a general-purpose keymap implementation.
+ const mods: Mods = mods: {
+ var v = input_mods;
+ v.ctrl = false;
+ break :mods v;
+ };
+
// Get the keycode for the space key, using comptime.
const code_space: u16 = comptime space: for (codes) |entry| {
if (std.mem.eql(u8, entry.code, "Space"))
@@ -183,7 +196,11 @@ pub fn translate(
// Convert the utf16 to utf8
const len = try std.unicode.utf16leToUtf8(out, char[0..char_count]);
- return .{ .text = out[0..len], .composing = composing };
+ return .{
+ .text = out[0..len],
+ .composing = composing,
+ .mods = mods,
+ };
}
/// Map to the modifiers format used by the UCKeyTranslate function.
diff --git a/src/input/KeymapNoop.zig b/src/input/KeymapNoop.zig
index 414c52954a..b6a9d57b96 100644
--- a/src/input/KeymapNoop.zig
+++ b/src/input/KeymapNoop.zig
@@ -6,8 +6,9 @@ const Mods = @import("key.zig").Mods;
pub const State = struct {};
pub const Translation = struct {
- text: []const u8,
- composing: bool,
+ text: []const u8 = "",
+ composing: bool = false,
+ mods: Mods = .{},
};
pub fn init() !KeymapNoop {
diff --git a/src/input/helpgen_actions.zig b/src/input/helpgen_actions.zig
new file mode 100644
index 0000000000..58305455b9
--- /dev/null
+++ b/src/input/helpgen_actions.zig
@@ -0,0 +1,113 @@
+//! This module is a help generator for keybind actions documentation.
+//! It can generate documentation in different formats (plaintext for CLI,
+//! markdown for website) while maintaining consistent content.
+
+const std = @import("std");
+const KeybindAction = @import("Binding.zig").Action;
+const help_strings = @import("help_strings");
+
+/// Format options for generating keybind actions documentation
+pub const Format = enum {
+ /// Plain text output with indentation
+ plaintext,
+ /// Markdown formatted output
+ markdown,
+
+ fn formatFieldName(self: Format, writer: anytype, field_name: []const u8) !void {
+ switch (self) {
+ .plaintext => {
+ try writer.writeAll(field_name);
+ try writer.writeAll(":\n");
+ },
+ .markdown => {
+ try writer.writeAll("## `");
+ try writer.writeAll(field_name);
+ try writer.writeAll("`\n");
+ },
+ }
+ }
+
+ fn formatDocLine(self: Format, writer: anytype, line: []const u8) !void {
+ switch (self) {
+ .plaintext => {
+ try writer.appendSlice(" ");
+ try writer.appendSlice(line);
+ try writer.appendSlice("\n");
+ },
+ .markdown => {
+ try writer.appendSlice(line);
+ try writer.appendSlice("\n");
+ },
+ }
+ }
+
+ fn header(self: Format) ?[]const u8 {
+ return switch (self) {
+ .plaintext => null,
+ .markdown =>
+ \\---
+ \\title: Keybinding Action Reference
+ \\description: Reference of all Ghostty keybinding actions.
+ \\editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig
+ \\---
+ \\
+ \\This is a reference of all Ghostty keybinding actions.
+ \\
+ \\
+ ,
+ };
+ }
+};
+
+/// Generate keybind actions documentation with the specified format
+pub fn generate(
+ writer: anytype,
+ format: Format,
+ show_docs: bool,
+ page_allocator: std.mem.Allocator,
+) !void {
+ if (format.header()) |header| {
+ try writer.writeAll(header);
+ }
+
+ var buffer = std.ArrayList(u8).init(page_allocator);
+ defer buffer.deinit();
+
+ const fields = @typeInfo(KeybindAction).Union.fields;
+ inline for (fields) |field| {
+ if (field.name[0] == '_') continue;
+
+ // Write previously stored doc comment below all related actions
+ if (show_docs and @hasDecl(help_strings.KeybindAction, field.name)) {
+ try writer.writeAll(buffer.items);
+ try writer.writeAll("\n");
+
+ buffer.clearRetainingCapacity();
+ }
+
+ if (show_docs) {
+ try format.formatFieldName(writer, field.name);
+ } else {
+ try writer.writeAll(field.name);
+ try writer.writeAll("\n");
+ }
+
+ if (show_docs and @hasDecl(help_strings.KeybindAction, field.name)) {
+ var iter = std.mem.splitScalar(
+ u8,
+ @field(help_strings.KeybindAction, field.name),
+ '\n',
+ );
+ while (iter.next()) |s| {
+ // If it is the last line and empty, then skip it.
+ if (iter.peek() == null and s.len == 0) continue;
+ try format.formatDocLine(&buffer, s);
+ }
+ }
+ }
+
+ // Write any remaining buffered documentation
+ if (buffer.items.len > 0) {
+ try writer.writeAll(buffer.items);
+ }
+}
diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig
index 54d49b0883..1824f5eadc 100644
--- a/src/inspector/Inspector.zig
+++ b/src/inspector/Inspector.zig
@@ -53,6 +53,22 @@ key_events: inspector.key.EventRing,
vt_events: inspector.termio.VTEventRing,
vt_stream: inspector.termio.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,
+
+/// Enum representing keyboard navigation actions
+const KeyAction = enum {
+ down,
+ none,
+ up,
+};
+
const CellInspect = union(enum) {
/// Idle, no cell inspection is requested
idle: void,
@@ -1014,6 +1030,24 @@ fn renderKeyboardWindow(self: *Inspector) void {
} // table
}
+/// Helper function to check keyboard state and determine navigation action.
+fn getKeyAction(self: *Inspector) KeyAction {
+ _ = self;
+ 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.igIsKeyPressed_Bool(k.key, false)) {
+ return k.action;
+ }
+ }
+ return .none;
+}
+
fn renderTermioWindow(self: *Inspector) void {
// Start our window. If we're collapsed we do nothing.
defer cimgui.c.igEnd();
@@ -1090,6 +1124,60 @@ fn renderTermioWindow(self: *Inspector) void {
0,
);
+ // Handle keyboard navigation when window is focused
+ if (cimgui.c.igIsWindowFocused(cimgui.c.ImGuiFocusedFlags_RootAndChildWindows)) {
+ const key_pressed = self.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 (!self.vt_events.empty()) {
+ var it = self.vt_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 = self.vt_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 inspector.termio.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 = self.vt_events.iterator(.reverse);
while (it.next()) |ev| {
// Need to push an ID so that our selectable is unique.
@@ -1098,12 +1186,32 @@ fn renderTermioWindow(self: *Inspector) void {
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
_ = cimgui.c.igTableNextColumn();
- _ = cimgui.c.igSelectable_BoolPtr(
+
+ // 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.igSelectable_BoolPtr(
"##select",
&ev.imgui_selected,
cimgui.c.ImGuiSelectableFlags_SpanAllColumns,
.{ .x = 0, .y = 0 },
- );
+ )) {
+ // 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.igSameLine(0, 0);
cimgui.c.igText("%d", ev.seq);
_ = cimgui.c.igTableNextColumn();
@@ -1159,6 +1267,12 @@ fn renderTermioWindow(self: *Inspector) void {
cimgui.c.igText("%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.igSetScrollHereY(0.5);
+ self.need_scroll_to_selected = false;
+ }
}
}
} // table
diff --git a/src/main.zig b/src/main.zig
index ecf38fbb30..121a3b7d20 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -9,6 +9,7 @@ const entrypoint = switch (build_config.exe_entrypoint) {
.mdgen_ghostty_5 => @import("build/mdgen/main_ghostty_5.zig"),
.webgen_config => @import("build/webgen/main_config.zig"),
.webgen_actions => @import("build/webgen/main_actions.zig"),
+ .webgen_commands => @import("build/webgen/main_commands.zig"),
.bench_parser => @import("bench/parser.zig"),
.bench_stream => @import("bench/stream.zig"),
.bench_codepoint_width => @import("bench/codepoint-width.zig"),
diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig
index 0a66c59878..5645e337a1 100644
--- a/src/os/cgroup.zig
+++ b/src/os/cgroup.zig
@@ -77,7 +77,22 @@ pub fn cloneInto(cgroup: []const u8) !posix.pid_t {
// Get a file descriptor that refers to the cgroup directory in the cgroup
// sysfs to pass to the kernel in clone3.
const fd: linux.fd_t = fd: {
- const rc = linux.open(path, linux.O{ .PATH = true, .DIRECTORY = true }, 0);
+ const rc = linux.open(
+ path,
+ .{
+ // Self-explanatory: we expect to open a directory, and
+ // we only need the path-level permissions.
+ .PATH = true,
+ .DIRECTORY = true,
+
+ // We don't want to leak this fd to the child process
+ // when we clone below since we're using this fd for
+ // a cgroup clone.
+ .CLOEXEC = true,
+ },
+ 0,
+ );
+
switch (posix.errno(rc)) {
.SUCCESS => break :fd @as(linux.fd_t, @intCast(rc)),
else => |errno| {
@@ -87,6 +102,7 @@ pub fn cloneInto(cgroup: []const u8) !posix.pid_t {
}
};
assert(fd >= 0);
+ defer _ = linux.close(fd);
const args: extern struct {
flags: u64,
@@ -115,7 +131,8 @@ pub fn cloneInto(cgroup: []const u8) !posix.pid_t {
};
const rc = linux.syscall2(linux.SYS.clone3, @intFromPtr(&args), @sizeOf(@TypeOf(args)));
- return switch (posix.errno(rc)) {
+ // do not use posix.errno, when linking libc it will use the libc errno which will not be set when making the syscall directly
+ return switch (std.os.linux.E.init(rc)) {
.SUCCESS => @as(posix.pid_t, @intCast(rc)),
else => |errno| err: {
log.err("unable to clone: {}", .{errno});
diff --git a/src/os/env.zig b/src/os/env.zig
index cf6cc0fe75..1916053b32 100644
--- a/src/os/env.zig
+++ b/src/os/env.zig
@@ -2,6 +2,17 @@ const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const posix = std.posix;
+const isFlatpak = @import("flatpak.zig").isFlatpak;
+
+pub const Error = Allocator.Error;
+
+/// Get the environment map.
+pub fn getEnvMap(alloc: Allocator) !std.process.EnvMap {
+ return if (isFlatpak())
+ std.process.EnvMap.init(alloc)
+ else
+ try std.process.getEnvMap(alloc);
+}
/// Append a value to an environment variable such as PATH.
/// The returned value is always allocated so it must be freed.
@@ -9,7 +20,7 @@ pub fn appendEnv(
alloc: Allocator,
current: []const u8,
value: []const u8,
-) ![]u8 {
+) Error![]u8 {
// If there is no prior value, we return it as-is
if (current.len == 0) return try alloc.dupe(u8, value);
@@ -26,7 +37,7 @@ pub fn appendEnvAlways(
alloc: Allocator,
current: []const u8,
value: []const u8,
-) ![]u8 {
+) Error![]u8 {
return try std.fmt.allocPrint(alloc, "{s}{c}{s}", .{
current,
std.fs.path.delimiter,
@@ -40,7 +51,7 @@ pub fn prependEnv(
alloc: Allocator,
current: []const u8,
value: []const u8,
-) ![]u8 {
+) Error![]u8 {
// If there is no prior value, we return it as-is
if (current.len == 0) return try alloc.dupe(u8, value);
@@ -68,7 +79,7 @@ pub const GetEnvResult = struct {
/// This will allocate on Windows but not on other platforms. The returned
/// value should have deinit called to do the proper cleanup no matter what
/// platform you are on.
-pub fn getenv(alloc: Allocator, key: []const u8) !?GetEnvResult {
+pub fn getenv(alloc: Allocator, key: []const u8) Error!?GetEnvResult {
return switch (builtin.os.tag) {
// Non-Windows doesn't need to allocate
else => if (posix.getenv(key)) |v| .{ .value = v } else null,
@@ -78,7 +89,8 @@ pub fn getenv(alloc: Allocator, key: []const u8) !?GetEnvResult {
.value = v,
} else |err| switch (err) {
error.EnvironmentVariableNotFound => null,
- else => err,
+ error.InvalidWtf8 => null,
+ else => |e| e,
},
};
}
diff --git a/src/os/flatpak.zig b/src/os/flatpak.zig
index faac4bd272..09570554ef 100644
--- a/src/os/flatpak.zig
+++ b/src/os/flatpak.zig
@@ -265,16 +265,12 @@ pub const FlatpakHostCommand = struct {
}
// Build our args
- const args_ptr = c.g_ptr_array_new();
- {
- errdefer _ = c.g_ptr_array_free(args_ptr, 1);
- for (self.argv) |arg| {
- const argZ = try arena.dupeZ(u8, arg);
- c.g_ptr_array_add(args_ptr, argZ.ptr);
- }
+ const args = try arena.alloc(?[*:0]u8, self.argv.len + 1);
+ for (0.., self.argv) |i, arg| {
+ const argZ = try arena.dupeZ(u8, arg);
+ args[i] = argZ.ptr;
}
- const args = c.g_ptr_array_free(args_ptr, 0);
- defer c.g_free(@as(?*anyopaque, @ptrCast(args)));
+ args[args.len - 1] = null;
// Get the cwd in case we don't have ours set. A small optimization
// would be to do this only if we need it but this isn't a
@@ -286,7 +282,7 @@ pub const FlatpakHostCommand = struct {
const params = c.g_variant_new(
"(^ay^aay@a{uh}@a{ss}u)",
@as(*const anyopaque, if (self.cwd) |*cwd| cwd.ptr else g_cwd),
- args,
+ args.ptr,
c.g_variant_builder_end(fd_builder),
c.g_variant_builder_end(env_builder),
@as(c_int, 0),
diff --git a/src/os/main.zig b/src/os/main.zig
index e652a7981b..cb93559315 100644
--- a/src/os/main.zig
+++ b/src/os/main.zig
@@ -21,10 +21,12 @@ pub const passwd = @import("passwd.zig");
pub const xdg = @import("xdg.zig");
pub const windows = @import("windows.zig");
pub const macos = @import("macos.zig");
+pub const shell = @import("shell.zig");
// Functions and types
pub const CFReleaseThread = @import("cf_release_thread.zig");
pub const TempDir = @import("TempDir.zig");
+pub const getEnvMap = env.getEnvMap;
pub const appendEnv = env.appendEnv;
pub const appendEnvAlways = env.appendEnvAlways;
pub const prependEnv = env.prependEnv;
@@ -48,3 +50,4 @@ pub const open = openpkg.open;
pub const OpenType = openpkg.Type;
pub const pipe = pipepkg.pipe;
pub const resourcesDir = resourcesdir.resourcesDir;
+pub const ShellEscapeWriter = shell.ShellEscapeWriter;
diff --git a/src/os/pipe.zig b/src/os/pipe.zig
index 392f720834..2cb7bd4a39 100644
--- a/src/os/pipe.zig
+++ b/src/os/pipe.zig
@@ -3,10 +3,11 @@ const builtin = @import("builtin");
const windows = @import("windows.zig");
const posix = std.posix;
-/// pipe() that works on Windows and POSIX.
+/// pipe() that works on Windows and POSIX. For POSIX systems, this sets
+/// CLOEXEC on the file descriptors.
pub fn pipe() ![2]posix.fd_t {
switch (builtin.os.tag) {
- else => return try posix.pipe(),
+ else => return try posix.pipe2(.{ .CLOEXEC = true }),
.windows => {
var read: windows.HANDLE = undefined;
var write: windows.HANDLE = undefined;
diff --git a/src/os/resourcesdir.zig b/src/os/resourcesdir.zig
index c0f82dec5d..4ef256c1ac 100644
--- a/src/os/resourcesdir.zig
+++ b/src/os/resourcesdir.zig
@@ -21,7 +21,11 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
// This is the sentinel value we look for in the path to know
// we've found the resources directory.
- const sentinel = "terminfo/ghostty.termcap";
+ const sentinel = switch (comptime builtin.target.os.tag) {
+ .windows => "terminfo/ghostty.terminfo",
+ .macos => "terminfo/78/xterm-ghostty",
+ else => "terminfo/x/xterm-ghostty",
+ };
// Get the path to our running binary
var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
diff --git a/src/os/shell.zig b/src/os/shell.zig
new file mode 100644
index 0000000000..23648a82ae
--- /dev/null
+++ b/src/os/shell.zig
@@ -0,0 +1,95 @@
+const std = @import("std");
+const testing = std.testing;
+
+/// Writer that escapes characters that shells treat specially to reduce the
+/// risk of injection attacks or other such weirdness. Specifically excludes
+/// linefeeds so that they can be used to delineate lists of file paths.
+///
+/// T should be a Zig type that follows the `std.io.Writer` interface.
+pub fn ShellEscapeWriter(comptime T: type) type {
+ return struct {
+ child_writer: T,
+
+ fn write(self: *ShellEscapeWriter(T), data: []const u8) error{Error}!usize {
+ var count: usize = 0;
+ for (data) |byte| {
+ const buf = switch (byte) {
+ '\\',
+ '"',
+ '\'',
+ '$',
+ '`',
+ '*',
+ '?',
+ ' ',
+ '|',
+ => &[_]u8{ '\\', byte },
+ else => &[_]u8{byte},
+ };
+ self.child_writer.writeAll(buf) catch return error.Error;
+ count += 1;
+ }
+ return count;
+ }
+
+ const Writer = std.io.Writer(*ShellEscapeWriter(T), error{Error}, write);
+
+ pub fn writer(self: *ShellEscapeWriter(T)) Writer {
+ return .{ .context = self };
+ }
+ };
+}
+
+test "shell escape 1" {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
+ const writer = shell.writer();
+ try writer.writeAll("abc");
+ try testing.expectEqualStrings("abc", fmt.getWritten());
+}
+
+test "shell escape 2" {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
+ const writer = shell.writer();
+ try writer.writeAll("a c");
+ try testing.expectEqualStrings("a\\ c", fmt.getWritten());
+}
+
+test "shell escape 3" {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
+ const writer = shell.writer();
+ try writer.writeAll("a?c");
+ try testing.expectEqualStrings("a\\?c", fmt.getWritten());
+}
+
+test "shell escape 4" {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
+ const writer = shell.writer();
+ try writer.writeAll("a\\c");
+ try testing.expectEqualStrings("a\\\\c", fmt.getWritten());
+}
+
+test "shell escape 5" {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
+ const writer = shell.writer();
+ try writer.writeAll("a|c");
+ try testing.expectEqualStrings("a\\|c", fmt.getWritten());
+}
+
+test "shell escape 6" {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
+ const writer = shell.writer();
+ try writer.writeAll("a\"c");
+ try testing.expectEqualStrings("a\\\"c", fmt.getWritten());
+}
diff --git a/src/pty.zig b/src/pty.zig
index c0d082411c..6f97e190d4 100644
--- a/src/pty.zig
+++ b/src/pty.zig
@@ -41,12 +41,16 @@ pub const Mode = packed struct {
// a termio that doesn't use a pty. This isn't used in any user-facing
// artifacts, this is just a stopgap to get compilation to work on iOS.
const NullPty = struct {
+ pub const Error = OpenError || GetModeError || SetSizeError || ChildPreExecError;
+
pub const Fd = posix.fd_t;
master: Fd,
slave: Fd,
- pub fn open(size: winsize) !Pty {
+ pub const OpenError = error{};
+
+ pub fn open(size: winsize) OpenError!Pty {
_ = size;
return .{ .master = 0, .slave = 0 };
}
@@ -55,17 +59,23 @@ const NullPty = struct {
_ = self;
}
- pub fn getMode(self: Pty) error{GetModeFailed}!Mode {
+ pub const GetModeError = error{GetModeFailed};
+
+ pub fn getMode(self: Pty) GetModeError!Mode {
_ = self;
return .{};
}
- pub fn setSize(self: *Pty, size: winsize) !void {
+ pub const SetSizeError = error{};
+
+ pub fn setSize(self: *Pty, size: winsize) SetSizeError!void {
_ = self;
_ = size;
}
- pub fn childPreExec(self: Pty) !void {
+ pub const ChildPreExecError = error{};
+
+ pub fn childPreExec(self: Pty) ChildPreExecError!void {
_ = self;
}
};
@@ -74,6 +84,8 @@ const NullPty = struct {
/// of Linux syscalls. The caller is responsible for detail-oriented handling
/// of the returned file handles.
const PosixPty = struct {
+ pub const Error = OpenError || GetModeError || GetSizeError || SetSizeError || ChildPreExecError;
+
pub const Fd = posix.fd_t;
// https://github.com/ziglang/zig/issues/13277
@@ -94,11 +106,16 @@ const PosixPty = struct {
};
/// The file descriptors for the master and slave side of the pty.
+ /// The slave side is never closed automatically by this struct
+ /// so the caller is responsible for closing it if things
+ /// go wrong.
master: Fd,
slave: Fd,
+ pub const OpenError = error{OpenptyFailed};
+
/// Open a new PTY with the given initial size.
- pub fn open(size: winsize) !Pty {
+ pub fn open(size: winsize) OpenError!Pty {
// Need to copy so that it becomes non-const.
var sizeCopy = size;
@@ -117,6 +134,24 @@ const PosixPty = struct {
_ = posix.system.close(slave_fd);
}
+ // Set CLOEXEC on the master fd, only the slave fd should be inherited
+ // by the child process (shell/command).
+ cloexec: {
+ const flags = std.posix.fcntl(master_fd, std.posix.F.GETFD, 0) catch |err| {
+ log.warn("error getting flags for master fd err={}", .{err});
+ break :cloexec;
+ };
+
+ _ = std.posix.fcntl(
+ master_fd,
+ std.posix.F.SETFD,
+ flags | std.posix.FD_CLOEXEC,
+ ) catch |err| {
+ log.warn("error setting CLOEXEC on master fd err={}", .{err});
+ break :cloexec;
+ };
+ }
+
// Enable UTF-8 mode. I think this is on by default on Linux but it
// is NOT on by default on macOS so we ensure that it is always set.
var attrs: c.termios = undefined;
@@ -126,7 +161,7 @@ const PosixPty = struct {
if (c.tcsetattr(master_fd, c.TCSANOW, &attrs) != 0)
return error.OpenptyFailed;
- return Pty{
+ return .{
.master = master_fd,
.slave = slave_fd,
};
@@ -134,11 +169,12 @@ const PosixPty = struct {
pub fn deinit(self: *Pty) void {
_ = posix.system.close(self.master);
- _ = posix.system.close(self.slave);
self.* = undefined;
}
- pub fn getMode(self: Pty) error{GetModeFailed}!Mode {
+ pub const GetModeError = error{GetModeFailed};
+
+ pub fn getMode(self: Pty) GetModeError!Mode {
var attrs: c.termios = undefined;
if (c.tcgetattr(self.master, &attrs) != 0)
return error.GetModeFailed;
@@ -149,8 +185,10 @@ const PosixPty = struct {
};
}
+ pub const GetSizeError = error{IoctlFailed};
+
/// Return the size of the pty.
- pub fn getSize(self: Pty) !winsize {
+ pub fn getSize(self: Pty) GetSizeError!winsize {
var ws: winsize = undefined;
if (c.ioctl(self.master, TIOCGWINSZ, @intFromPtr(&ws)) < 0)
return error.IoctlFailed;
@@ -158,15 +196,19 @@ const PosixPty = struct {
return ws;
}
+ pub const SetSizeError = error{IoctlFailed};
+
/// Set the size of the pty.
- pub fn setSize(self: *Pty, size: winsize) !void {
+ pub fn setSize(self: *Pty, size: winsize) SetSizeError!void {
if (c.ioctl(self.master, TIOCSWINSZ, @intFromPtr(&size)) < 0)
return error.IoctlFailed;
}
+ pub const ChildPreExecError = error{ OperationNotSupported, ProcessGroupFailed, SetControllingTerminalFailed };
+
/// This should be called prior to exec in the forked child process
/// in order to setup the tty properly.
- pub fn childPreExec(self: Pty) !void {
+ pub fn childPreExec(self: Pty) ChildPreExecError!void {
// Reset our signals
var sa: posix.Sigaction = .{
.handler = .{ .handler = posix.SIG.DFL },
@@ -181,6 +223,7 @@ const PosixPty = struct {
try posix.sigaction(posix.SIG.HUP, &sa, null);
try posix.sigaction(posix.SIG.ILL, &sa, null);
try posix.sigaction(posix.SIG.INT, &sa, null);
+ try posix.sigaction(posix.SIG.PIPE, &sa, null);
try posix.sigaction(posix.SIG.SEGV, &sa, null);
try posix.sigaction(posix.SIG.TRAP, &sa, null);
try posix.sigaction(posix.SIG.TERM, &sa, null);
@@ -201,13 +244,13 @@ const PosixPty = struct {
// Can close master/slave pair now
posix.close(self.slave);
posix.close(self.master);
-
- // TODO: reset signals
}
};
/// Windows PTY creation and management.
const WindowsPty = struct {
+ pub const Error = OpenError || GetSizeError || SetSizeError;
+
pub const Fd = windows.HANDLE;
// Process-wide counter for pipe names
@@ -220,8 +263,10 @@ const WindowsPty = struct {
pseudo_console: windows.exp.HPCON,
size: winsize,
+ pub const OpenError = error{Unexpected};
+
/// Open a new PTY with the given initial size.
- pub fn open(size: winsize) !Pty {
+ pub fn open(size: winsize) OpenError!Pty {
var pty: Pty = undefined;
var pipe_path_buf: [128]u8 = undefined;
@@ -330,13 +375,17 @@ const WindowsPty = struct {
self.* = undefined;
}
+ pub const GetSizeError = error{};
+
/// Return the size of the pty.
- pub fn getSize(self: Pty) !winsize {
+ pub fn getSize(self: Pty) GetSizeError!winsize {
return self.size;
}
+ pub const SetSizeError = error{ResizeFailed};
+
/// Set the size of the pty.
- pub fn setSize(self: *Pty, size: winsize) !void {
+ pub fn setSize(self: *Pty, size: winsize) SetSizeError!void {
const result = windows.exp.kernel32.ResizePseudoConsole(
self.pseudo_console,
.{ .X = @intCast(size.ws_col), .Y = @intCast(size.ws_row) },
diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig
index 09dafd1fc7..ca13f87deb 100644
--- a/src/renderer/Metal.zig
+++ b/src/renderer/Metal.zig
@@ -21,6 +21,7 @@ const renderer = @import("../renderer.zig");
const math = @import("../math.zig");
const Surface = @import("../Surface.zig");
const link = @import("link.zig");
+const graphics = macos.graphics;
const fgMode = @import("cell.zig").fgMode;
const isCovering = @import("cell.zig").isCovering;
const shadertoy = @import("shadertoy.zig");
@@ -105,10 +106,6 @@ default_cursor_color: ?terminal.color.RGB,
/// foreground color as the cursor color.
cursor_invert: bool,
-/// The current frame background color. This is only updated during
-/// the updateFrame method.
-current_background_color: terminal.color.RGB,
-
/// The current set of cells to render. This is rebuilt on every frame
/// but we keep this around so that we don't reallocate. Each set of
/// cells goes into a separate shader.
@@ -151,6 +148,9 @@ layer: objc.Object, // CAMetalLayer
/// a display link.
display_link: ?DisplayLink = null,
+/// The `CGColorSpace` that represents our current terminal color space
+terminal_colorspace: *graphics.ColorSpace,
+
/// Custom shader state. This is only set if we have custom shaders.
custom_shader_state: ?CustomShaderState = null,
@@ -182,15 +182,34 @@ pub const GPUState = struct {
/// This buffer is written exactly once so we can use it globally.
instance: InstanceBuffer, // MTLBuffer
+ /// The default storage mode to use for resources created with our device.
+ ///
+ /// This is based on whether the device is a discrete GPU or not, since
+ /// discrete GPUs do not have unified memory and therefore do not support
+ /// the "shared" storage mode, instead we have to use the "managed" mode.
+ default_storage_mode: mtl.MTLResourceOptions.StorageMode,
+
pub fn init() !GPUState {
const device = try chooseDevice();
const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{});
errdefer queue.release();
+ // We determine whether our device is a discrete GPU based on these:
+ // - We're on macOS (iOS, iPadOS, etc. are guaranteed to be integrated).
+ // - We're not on aarch64 (Apple Silicon, therefore integrated).
+ // - The device reports that it does not have unified memory.
+ const is_discrete =
+ builtin.target.os.tag == .macos and
+ builtin.target.cpu.arch != .aarch64 and
+ !device.getProperty(bool, "hasUnifiedMemory");
+
+ const default_storage_mode: mtl.MTLResourceOptions.StorageMode =
+ if (is_discrete) .managed else .shared;
+
var instance = try InstanceBuffer.initFill(device, &.{
0, 1, 3, // Top-left triangle
1, 2, 3, // Bottom-right triangle
- });
+ }, .{ .storage_mode = default_storage_mode });
errdefer instance.deinit();
var result: GPUState = .{
@@ -198,11 +217,12 @@ pub const GPUState = struct {
.queue = queue,
.instance = instance,
.frames = undefined,
+ .default_storage_mode = default_storage_mode,
};
// Initialize all of our frame state.
for (&result.frames) |*frame| {
- frame.* = try FrameState.init(result.device);
+ frame.* = try FrameState.init(result.device, default_storage_mode);
}
return result;
@@ -288,18 +308,47 @@ pub const FrameState = struct {
const CellBgBuffer = mtl_buffer.Buffer(mtl_shaders.CellBg);
const CellTextBuffer = mtl_buffer.Buffer(mtl_shaders.CellText);
- pub fn init(device: objc.Object) !FrameState {
+ pub fn init(
+ device: objc.Object,
+ /// Storage mode for buffers and textures.
+ storage_mode: mtl.MTLResourceOptions.StorageMode,
+ ) !FrameState {
// Uniform buffer contains exactly 1 uniform struct. The
// uniform data will be undefined so this must be set before
// a frame is drawn.
- var uniforms = try UniformBuffer.init(device, 1);
+ var uniforms = try UniformBuffer.init(
+ device,
+ 1,
+ .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = storage_mode,
+ },
+ );
errdefer uniforms.deinit();
// Create the buffers for our vertex data. The preallocation size
// is likely too small but our first frame update will resize it.
- var cells = try CellTextBuffer.init(device, 10 * 10);
+ var cells = try CellTextBuffer.init(
+ device,
+ 10 * 10,
+ .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = storage_mode,
+ },
+ );
errdefer cells.deinit();
- var cells_bg = try CellBgBuffer.init(device, 10 * 10);
+ var cells_bg = try CellBgBuffer.init(
+ device,
+ 10 * 10,
+ .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = storage_mode,
+ },
+ );
+
errdefer cells_bg.deinit();
// Initialize our textures for our font atlas.
@@ -307,13 +356,13 @@ pub const FrameState = struct {
.data = undefined,
.size = 8,
.format = .grayscale,
- });
+ }, storage_mode);
errdefer grayscale.release();
const color = try initAtlasTexture(device, &.{
.data = undefined,
.size = 8,
.format = .rgba,
- });
+ }, storage_mode);
errdefer color.release();
return .{
@@ -390,6 +439,8 @@ pub const DerivedConfig = struct {
custom_shaders: configpkg.RepeatablePath,
links: link.Set,
vsync: bool,
+ colorspace: configpkg.Config.WindowColorspace,
+ blending: configpkg.Config.AlphaBlending,
pub fn init(
alloc_gpa: Allocator,
@@ -460,7 +511,8 @@ pub const DerivedConfig = struct {
.custom_shaders = custom_shaders,
.links = links,
.vsync = config.@"window-vsync",
-
+ .colorspace = config.@"window-colorspace",
+ .blending = config.@"alpha-blending",
.arena = arena,
};
}
@@ -490,10 +542,6 @@ pub fn surfaceInit(surface: *apprt.Surface) !void {
}
pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
- var arena = ArenaAllocator.init(alloc);
- defer arena.deinit();
- const arena_alloc = arena.allocator();
-
const ViewInfo = struct {
view: objc.Object,
scaleFactor: f64,
@@ -512,7 +560,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
nswindow.getProperty(?*anyopaque, "contentView").?,
);
const scaleFactor = nswindow.getProperty(
- macos.graphics.c.CGFloat,
+ graphics.c.CGFloat,
"backingScaleFactor",
);
@@ -553,6 +601,40 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
layer.setProperty("opaque", options.config.background_opacity >= 1);
layer.setProperty("displaySyncEnabled", options.config.vsync);
+ // Set our layer's pixel format appropriately.
+ layer.setProperty(
+ "pixelFormat",
+ // Using an `*_srgb` pixel format makes Metal gamma encode
+ // the pixels written to it *after* blending, which means
+ // we get linear alpha blending rather than gamma-incorrect
+ // blending.
+ if (options.config.blending.isLinear())
+ @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
+ else
+ @intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
+ );
+
+ // Set our layer's color space to Display P3.
+ // This allows us to have "Apple-style" alpha blending,
+ // since it seems to be the case that Apple apps like
+ // Terminal and TextEdit render text in the display's
+ // color space using converted colors, which reduces,
+ // but does not fully eliminate blending artifacts.
+ const colorspace = try graphics.ColorSpace.createNamed(.displayP3);
+ defer colorspace.release();
+ layer.setProperty("colorspace", colorspace);
+
+ // Create a colorspace the represents our terminal colors
+ // this will allow us to create e.g. `CGColor`s for things
+ // like the current background color.
+ const terminal_colorspace = try graphics.ColorSpace.createNamed(
+ switch (options.config.colorspace) {
+ .@"display-p3" => .displayP3,
+ .srgb => .sRGB,
+ },
+ );
+ errdefer terminal_colorspace.release();
+
// Make our view layer-backed with our Metal layer. On iOS views are
// always layer backed so we don't need to do this. But on iOS the
// caller MUST be sure to set the layerClass to CAMetalLayer.
@@ -578,54 +660,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
});
errdefer font_shaper.deinit();
- // Load our custom shaders
- const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles(
- arena_alloc,
- options.config.custom_shaders,
- .msl,
- ) catch |err| err: {
- log.warn("error loading custom shaders err={}", .{err});
- break :err &.{};
- };
-
- // If we have custom shaders then setup our state
- var custom_shader_state: ?CustomShaderState = state: {
- if (custom_shaders.len == 0) break :state null;
-
- // Build our sampler for our texture
- var sampler = try mtl_sampler.Sampler.init(gpu_state.device);
- errdefer sampler.deinit();
-
- break :state .{
- // Resolution and screen textures will be fixed up by first
- // call to setScreenSize. Draw calls will bail out early if
- // the screen size hasn't been set yet, so it won't error.
- .front_texture = undefined,
- .back_texture = undefined,
- .sampler = sampler,
- .uniforms = .{
- .resolution = .{ 0, 0, 1 },
- .time = 1,
- .time_delta = 1,
- .frame_rate = 1,
- .frame = 1,
- .channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
- .channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
- .mouse = .{ 0, 0, 0, 0 },
- .date = .{ 0, 0, 0, 0 },
- .sample_rate = 1,
- },
-
- .first_frame_time = try std.time.Instant.now(),
- .last_frame_time = try std.time.Instant.now(),
- };
- };
- errdefer if (custom_shader_state) |*state| state.deinit();
-
- // Initialize our shaders
- var shaders = try Shaders.init(alloc, gpu_state.device, custom_shaders);
- errdefer shaders.deinit(alloc);
-
// Initialize all the data that requires a critical font section.
const font_critical: struct {
metrics: font.Metrics,
@@ -661,7 +695,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.cursor_color = null,
.default_cursor_color = options.config.cursor_color,
.cursor_invert = options.config.cursor_invert,
- .current_background_color = options.config.background,
// Render state
.cells = .{},
@@ -674,7 +707,16 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.min_contrast = options.config.min_contrast,
.cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) },
.cursor_color = undefined,
+ .bg_color = .{
+ options.config.background.r,
+ options.config.background.g,
+ options.config.background.b,
+ @intFromFloat(@round(options.config.background_opacity * 255.0)),
+ },
.cursor_wide = false,
+ .use_display_p3 = options.config.colorspace == .@"display-p3",
+ .use_linear_blending = options.config.blending.isLinear(),
+ .use_linear_correction = options.config.blending == .@"linear-corrected",
},
// Fonts
@@ -682,16 +724,19 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.font_shaper = font_shaper,
.font_shaper_cache = font.ShaperCache.init(),
- // Shaders
- .shaders = shaders,
+ // Shaders (initialized below)
+ .shaders = undefined,
// Metal stuff
.layer = layer,
.display_link = display_link,
- .custom_shader_state = custom_shader_state,
+ .terminal_colorspace = terminal_colorspace,
+ .custom_shader_state = null,
.gpu_state = gpu_state,
};
+ try result.initShaders();
+
// Do an initialize screen size setup to ensure our undefined values
// above are initialized.
try result.setScreenSize(result.size);
@@ -709,6 +754,8 @@ pub fn deinit(self: *Metal) void {
}
}
+ self.terminal_colorspace.release();
+
self.cells.deinit(self.alloc);
self.font_shaper.deinit();
@@ -723,11 +770,82 @@ pub fn deinit(self: *Metal) void {
}
self.image_placements.deinit(self.alloc);
+ self.deinitShaders();
+
+ self.* = undefined;
+}
+
+fn deinitShaders(self: *Metal) void {
if (self.custom_shader_state) |*state| state.deinit();
self.shaders.deinit(self.alloc);
+}
- self.* = undefined;
+fn initShaders(self: *Metal) !void {
+ var arena = ArenaAllocator.init(self.alloc);
+ defer arena.deinit();
+ const arena_alloc = arena.allocator();
+
+ // Load our custom shaders
+ const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles(
+ arena_alloc,
+ self.config.custom_shaders,
+ .msl,
+ ) catch |err| err: {
+ log.warn("error loading custom shaders err={}", .{err});
+ break :err &.{};
+ };
+
+ var custom_shader_state: ?CustomShaderState = state: {
+ if (custom_shaders.len == 0) break :state null;
+
+ // Build our sampler for our texture
+ var sampler = try mtl_sampler.Sampler.init(self.gpu_state.device);
+ errdefer sampler.deinit();
+
+ break :state .{
+ // Resolution and screen textures will be fixed up by first
+ // call to setScreenSize. Draw calls will bail out early if
+ // the screen size hasn't been set yet, so it won't error.
+ .front_texture = undefined,
+ .back_texture = undefined,
+ .sampler = sampler,
+ .uniforms = .{
+ .resolution = .{ 0, 0, 1 },
+ .time = 1,
+ .time_delta = 1,
+ .frame_rate = 1,
+ .frame = 1,
+ .channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
+ .channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
+ .mouse = .{ 0, 0, 0, 0 },
+ .date = .{ 0, 0, 0, 0 },
+ .sample_rate = 1,
+ },
+
+ .first_frame_time = try std.time.Instant.now(),
+ .last_frame_time = try std.time.Instant.now(),
+ };
+ };
+ errdefer if (custom_shader_state) |*state| state.deinit();
+
+ var shaders = try Shaders.init(
+ self.alloc,
+ self.gpu_state.device,
+ custom_shaders,
+ // Using an `*_srgb` pixel format makes Metal gamma encode
+ // the pixels written to it *after* blending, which means
+ // we get linear alpha blending rather than gamma-incorrect
+ // blending.
+ if (self.config.blending.isLinear())
+ mtl.MTLPixelFormat.bgra8unorm_srgb
+ else
+ mtl.MTLPixelFormat.bgra8unorm,
+ );
+ errdefer shaders.deinit(self.alloc);
+
+ self.shaders = shaders;
+ self.custom_shader_state = custom_shader_state;
}
/// This is called just prior to spinning up the renderer thread for
@@ -977,19 +1095,6 @@ pub fn updateFrame(
}
}
- // If our terminal screen size doesn't match our expected renderer
- // size then we skip a frame. This can happen if the terminal state
- // is resized between when the renderer mailbox is drained and when
- // the state mutex is acquired inside this function.
- //
- // For some reason this doesn't seem to cause any significant issues
- // with flickering while resizing. '\_('-')_/'
- if (self.cells.size.rows != state.terminal.rows or
- self.cells.size.columns != state.terminal.cols)
- {
- return;
- }
-
// Get the viewport pin so that we can compare it to the current.
const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
@@ -1111,7 +1216,38 @@ pub fn updateFrame(
self.cells_viewport = critical.viewport_pin;
// Update our background color
- self.current_background_color = critical.bg;
+ self.uniforms.bg_color = .{
+ critical.bg.r,
+ critical.bg.g,
+ critical.bg.b,
+ @intFromFloat(@round(self.config.background_opacity * 255.0)),
+ };
+
+ // Update the background color on our layer
+ //
+ // TODO: Is this expensive? Should we be checking if our
+ // bg color has changed first before doing this work?
+ {
+ const color = graphics.c.CGColorCreate(
+ @ptrCast(self.terminal_colorspace),
+ &[4]f64{
+ @as(f64, @floatFromInt(critical.bg.r)) / 255.0,
+ @as(f64, @floatFromInt(critical.bg.g)) / 255.0,
+ @as(f64, @floatFromInt(critical.bg.b)) / 255.0,
+ self.config.background_opacity,
+ },
+ );
+ defer graphics.c.CGColorRelease(color);
+
+ // We use a CATransaction so that Core Animation knows that we
+ // updated the background color property. Otherwise it behaves
+ // weird, not updating the color until we resize.
+ const CATransaction = objc.getClass("CATransaction").?;
+ CATransaction.msgSend(void, "begin", .{});
+ defer CATransaction.msgSend(void, "commit", .{});
+
+ self.layer.setProperty("backgroundColor", color);
+ }
// Go through our images and see if we need to setup any textures.
{
@@ -1128,7 +1264,11 @@ pub fn updateFrame(
.replace_gray_alpha,
.replace_rgb,
.replace_rgba,
- => try kv.value_ptr.image.upload(self.alloc, self.gpu_state.device),
+ => try kv.value_ptr.image.upload(
+ self.alloc,
+ self.gpu_state.device,
+ self.gpu_state.default_storage_mode,
+ ),
.unload_pending,
.unload_replace,
@@ -1196,7 +1336,12 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
self.font_grid.lock.lockShared();
defer self.font_grid.lock.unlockShared();
frame.grayscale_modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
- try syncAtlasTexture(self.gpu_state.device, &self.font_grid.atlas_grayscale, &frame.grayscale);
+ try syncAtlasTexture(
+ self.gpu_state.device,
+ &self.font_grid.atlas_grayscale,
+ &frame.grayscale,
+ self.gpu_state.default_storage_mode,
+ );
}
texture: {
const modified = self.font_grid.atlas_color.modified.load(.monotonic);
@@ -1204,7 +1349,12 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
self.font_grid.lock.lockShared();
defer self.font_grid.lock.unlockShared();
frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic);
- try syncAtlasTexture(self.gpu_state.device, &self.font_grid.atlas_color, &frame.color);
+ try syncAtlasTexture(
+ self.gpu_state.device,
+ &self.font_grid.atlas_color,
+ &frame.color,
+ self.gpu_state.default_storage_mode,
+ );
}
// Command buffer (MTLCommandBuffer)
@@ -1233,10 +1383,10 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store));
attachment.setProperty("texture", screen_texture.value);
attachment.setProperty("clearColor", mtl.MTLClearColor{
- .red = @as(f32, @floatFromInt(self.current_background_color.r)) / 255 * self.config.background_opacity,
- .green = @as(f32, @floatFromInt(self.current_background_color.g)) / 255 * self.config.background_opacity,
- .blue = @as(f32, @floatFromInt(self.current_background_color.b)) / 255 * self.config.background_opacity,
- .alpha = self.config.background_opacity,
+ .red = 0.0,
+ .green = 0.0,
+ .blue = 0.0,
+ .alpha = 0.0,
});
}
@@ -1252,19 +1402,19 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
// Draw background images first
- try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]);
+ try self.drawImagePlacements(encoder, frame, self.image_placements.items[0..self.image_bg_end]);
// Then draw background cells
try self.drawCellBgs(encoder, frame);
// Then draw images under text
- try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]);
+ try self.drawImagePlacements(encoder, frame, self.image_placements.items[self.image_bg_end..self.image_text_end]);
// Then draw fg cells
try self.drawCellFgs(encoder, frame, fg_count);
// Then draw remaining images
- try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]);
+ try self.drawImagePlacements(encoder, frame, self.image_placements.items[self.image_text_end..]);
}
// If we have custom shaders, then we render them.
@@ -1457,6 +1607,7 @@ fn drawPostShader(
fn drawImagePlacements(
self: *Metal,
encoder: objc.Object,
+ frame: *const FrameState,
placements: []const mtl_image.Placement,
) !void {
if (placements.len == 0) return;
@@ -1468,15 +1619,16 @@ fn drawImagePlacements(
.{self.shaders.image_pipeline.value},
);
- // Set our uniform, which is the only shared buffer
+ // Set our uniforms
encoder.msgSend(
void,
- objc.sel("setVertexBytes:length:atIndex:"),
- .{
- @as(*const anyopaque, @ptrCast(&self.uniforms)),
- @as(c_ulong, @sizeOf(@TypeOf(self.uniforms))),
- @as(c_ulong, 1),
- },
+ objc.sel("setVertexBuffer:offset:atIndex:"),
+ .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
+ );
+ encoder.msgSend(
+ void,
+ objc.sel("setFragmentBuffer:offset:atIndex:"),
+ .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
);
for (placements) |placement| {
@@ -1529,7 +1681,11 @@ fn drawImagePlacement(
@as(f32, @floatFromInt(p.width)),
@as(f32, @floatFromInt(p.height)),
},
- }});
+ }}, .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = self.gpu_state.default_storage_mode,
+ });
defer buf.deinit();
// Set our buffer
@@ -1588,6 +1744,11 @@ fn drawCellBgs(
);
// Set our buffers
+ encoder.msgSend(
+ void,
+ objc.sel("setVertexBuffer:offset:atIndex:"),
+ .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
+ );
encoder.msgSend(
void,
objc.sel("setFragmentBuffer:offset:atIndex:"),
@@ -1647,18 +1808,17 @@ fn drawCellFgs(
encoder.msgSend(
void,
objc.sel("setFragmentTexture:atIndex:"),
- .{
- frame.grayscale.value,
- @as(c_ulong, 0),
- },
+ .{ frame.grayscale.value, @as(c_ulong, 0) },
);
encoder.msgSend(
void,
objc.sel("setFragmentTexture:atIndex:"),
- .{
- frame.color.value,
- @as(c_ulong, 1),
- },
+ .{ frame.color.value, @as(c_ulong, 1) },
+ );
+ encoder.msgSend(
+ void,
+ objc.sel("setFragmentBuffer:offset:atIndex:"),
+ .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 2) },
);
encoder.msgSend(
@@ -2003,17 +2163,73 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
// Set our new minimum contrast
self.uniforms.min_contrast = config.min_contrast;
+ // Set our new color space and blending
+ self.uniforms.use_display_p3 = config.colorspace == .@"display-p3";
+ self.uniforms.use_linear_blending = config.blending.isLinear();
+ self.uniforms.use_linear_correction = config.blending == .@"linear-corrected";
+
// Set our new colors
self.default_background_color = config.background;
self.default_foreground_color = config.foreground;
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
self.cursor_invert = config.cursor_invert;
+ // Update our layer's opaqueness and display sync in case they changed.
+ {
+ // We use a CATransaction so that Core Animation knows that we
+ // updated the opaque property. Otherwise it behaves weird, not
+ // properly going from opaque to transparent unless we resize.
+ const CATransaction = objc.getClass("CATransaction").?;
+ CATransaction.msgSend(void, "begin", .{});
+ defer CATransaction.msgSend(void, "commit", .{});
+
+ self.layer.setProperty("opaque", config.background_opacity >= 1);
+ self.layer.setProperty("displaySyncEnabled", config.vsync);
+ }
+
+ // Update our terminal colorspace if it changed
+ if (self.config.colorspace != config.colorspace) {
+ const terminal_colorspace = try graphics.ColorSpace.createNamed(
+ switch (config.colorspace) {
+ .@"display-p3" => .displayP3,
+ .srgb => .sRGB,
+ },
+ );
+ errdefer terminal_colorspace.release();
+ self.terminal_colorspace.release();
+ self.terminal_colorspace = terminal_colorspace;
+ }
+
+ const old_blending = self.config.blending;
+ const old_custom_shaders = self.config.custom_shaders;
+
self.config.deinit();
self.config = config.*;
// Reset our viewport to force a rebuild, in case of a font change.
self.cells_viewport = null;
+
+ // We reinitialize our shaders if our
+ // blending or custom shaders changed.
+ if (old_blending != config.blending or
+ !old_custom_shaders.equal(config.custom_shaders))
+ {
+ self.deinitShaders();
+ try self.initShaders();
+ // We call setScreenSize to reinitialize
+ // the textures used for custom shaders.
+ if (self.custom_shader_state != null) {
+ try self.setScreenSize(self.size);
+ }
+ // And we update our layer's pixel format appropriately.
+ self.layer.setProperty(
+ "pixelFormat",
+ if (config.blending.isLinear())
+ @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
+ else
+ @intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
+ );
+ }
}
/// Resize the screen.
@@ -2057,7 +2273,7 @@ pub fn setScreenSize(
}
// Set the size of the drawable surface to the bounds
- self.layer.setProperty("drawableSize", macos.graphics.Size{
+ self.layer.setProperty("drawableSize", graphics.Size{
.width = @floatFromInt(size.screen.width),
.height = @floatFromInt(size.screen.height),
});
@@ -2089,7 +2305,11 @@ pub fn setScreenSize(
.min_contrast = old.min_contrast,
.cursor_pos = old.cursor_pos,
.cursor_color = old.cursor_color,
+ .bg_color = old.bg_color,
.cursor_wide = old.cursor_wide,
+ .use_display_p3 = old.use_display_p3,
+ .use_linear_blending = old.use_linear_blending,
+ .use_linear_correction = old.use_linear_correction,
};
// Reset our cell contents if our grid size has changed.
@@ -2124,7 +2344,17 @@ pub fn setScreenSize(
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
break :init id_init;
};
- desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm));
+ desc.setProperty(
+ "pixelFormat",
+ // Using an `*_srgb` pixel format makes Metal gamma encode
+ // the pixels written to it *after* blending, which means
+ // we get linear alpha blending rather than gamma-incorrect
+ // blending.
+ if (self.config.blending.isLinear())
+ @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
+ else
+ @intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
+ );
desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width)));
desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height)));
desc.setProperty(
@@ -2154,7 +2384,17 @@ pub fn setScreenSize(
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
break :init id_init;
};
- desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm));
+ desc.setProperty(
+ "pixelFormat",
+ // Using an `*_srgb` pixel format makes Metal gamma encode
+ // the pixels written to it *after* blending, which means
+ // we get linear alpha blending rather than gamma-incorrect
+ // blending.
+ if (self.config.blending.isLinear())
+ @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
+ else
+ @intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
+ );
desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width)));
desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height)));
desc.setProperty(
@@ -2251,12 +2491,22 @@ fn rebuildCells(
}
}
- // Go row-by-row to build the cells. We go row by row because we do
- // font shaping by row. In the future, we will also do dirty tracking
- // by row.
+ // We rebuild the cells row-by-row because we
+ // do font shaping and dirty tracking by row.
var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
- var y: terminal.size.CellCountInt = screen.pages.rows;
+ // If our cell contents buffer is shorter than the screen viewport,
+ // we render the rows that fit, starting from the bottom. If instead
+ // the viewport is shorter than the cell contents buffer, we align
+ // the top of the viewport with the top of the contents buffer.
+ var y: terminal.size.CellCountInt = @min(
+ screen.pages.rows,
+ self.cells.size.rows,
+ );
while (row_it.next()) |row| {
+ // The viewport may have more rows than our cell contents,
+ // so we need to break from the loop early if we hit y = 0.
+ if (y == 0) break;
+
y -= 1;
if (!rebuild) {
@@ -2315,7 +2565,11 @@ fn rebuildCells(
var shaper_cells: ?[]const font.shape.Cell = null;
var shaper_cells_i: usize = 0;
- const row_cells = row.cells(.all);
+ const row_cells_all = row.cells(.all);
+
+ // If our viewport is wider than our cell contents buffer,
+ // we still only process cells up to the width of the buffer.
+ const row_cells = row_cells_all[0..@min(row_cells_all.len, self.cells.size.columns)];
for (row_cells, 0..) |*cell, x| {
// If this cell falls within our preedit range then we
@@ -2466,8 +2720,10 @@ fn rebuildCells(
// Foreground alpha for this cell.
const alpha: u8 = if (style.flags.faint) 175 else 255;
- // If the cell has a background color, set it.
- if (bg) |rgb| {
+ // Set the cell's background color.
+ {
+ const rgb = bg orelse self.background_color orelse self.default_background_color;
+
// Determine our background alpha. If we have transparency configured
// then this is dynamic depending on some situations. This is all
// in an attempt to make transparency look the best for various
@@ -2477,23 +2733,19 @@ fn rebuildCells(
if (self.config.background_opacity >= 1) break :bg_alpha default;
- // If we're selected, we do not apply background opacity
+ // Cells that are selected should be fully opaque.
if (selected) break :bg_alpha default;
- // If we're reversed, do not apply background opacity
+ // Cells that are reversed should be fully opaque.
if (style.flags.inverse) break :bg_alpha default;
- // If we have a background and its not the default background
- // then we apply background opacity
- if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color orelse self.default_background_color)) {
+ // Cells that have an explicit bg color should be fully opaque.
+ if (bg_style != null) {
break :bg_alpha default;
}
- // We apply background opacity.
- var bg_alpha: f64 = @floatFromInt(default);
- bg_alpha *= self.config.background_opacity;
- bg_alpha = @ceil(bg_alpha);
- break :bg_alpha @intFromFloat(bg_alpha);
+ // Otherwise, we use the configured background opacity.
+ break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0));
};
self.cells.bgCell(y, x).* = .{
@@ -3032,14 +3284,20 @@ fn addPreeditCell(
/// Sync the atlas data to the given texture. This copies the bytes
/// associated with the atlas to the given texture. If the atlas no longer
/// fits into the texture, the texture will be resized.
-fn syncAtlasTexture(device: objc.Object, atlas: *const font.Atlas, texture: *objc.Object) !void {
+fn syncAtlasTexture(
+ device: objc.Object,
+ atlas: *const font.Atlas,
+ texture: *objc.Object,
+ /// Storage mode for the MTLTexture object
+ storage_mode: mtl.MTLResourceOptions.StorageMode,
+) !void {
const width = texture.getProperty(c_ulong, "width");
if (atlas.size > width) {
// Free our old texture
texture.*.release();
// Reallocate
- texture.* = try initAtlasTexture(device, atlas);
+ texture.* = try initAtlasTexture(device, atlas, storage_mode);
}
texture.msgSend(
@@ -3062,7 +3320,12 @@ fn syncAtlasTexture(device: objc.Object, atlas: *const font.Atlas, texture: *obj
}
/// Initialize a MTLTexture object for the given atlas.
-fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object {
+fn initAtlasTexture(
+ device: objc.Object,
+ atlas: *const font.Atlas,
+ /// Storage mode for the MTLTexture object
+ storage_mode: mtl.MTLResourceOptions.StorageMode,
+) !objc.Object {
// Determine our pixel format
const pixel_format: mtl.MTLPixelFormat = switch (atlas.format) {
.grayscale => .r8unorm,
@@ -3083,15 +3346,14 @@ fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object
desc.setProperty("width", @as(c_ulong, @intCast(atlas.size)));
desc.setProperty("height", @as(c_ulong, @intCast(atlas.size)));
- // Xcode tells us that this texture should be shared mode on
- // aarch64. This configuration is not supported on x86_64 so
- // we only set it on aarch64.
- if (comptime builtin.target.cpu.arch == .aarch64) {
- desc.setProperty(
- "storageMode",
- @as(c_ulong, mtl.MTLResourceStorageModeShared),
- );
- }
+ desc.setProperty(
+ "resourceOptions",
+ mtl.MTLResourceOptions{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = storage_mode,
+ },
+ );
// Initialize
const id = device.msgSend(
diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig
index e5dec6b2bf..3e674c7155 100644
--- a/src/renderer/OpenGL.zig
+++ b/src/renderer/OpenGL.zig
@@ -706,8 +706,6 @@ pub fn updateFrame(
// Update all our data as tightly as possible within the mutex.
var critical: Critical = critical: {
- const grid_size = self.size.grid();
-
state.mutex.lock();
defer state.mutex.unlock();
@@ -748,19 +746,6 @@ pub fn updateFrame(
}
}
- // If our terminal screen size doesn't match our expected renderer
- // size then we skip a frame. This can happen if the terminal state
- // is resized between when the renderer mailbox is drained and when
- // the state mutex is acquired inside this function.
- //
- // For some reason this doesn't seem to cause any significant issues
- // with flickering while resizing. '\_('-')_/'
- if (grid_size.rows != state.terminal.rows or
- grid_size.columns != state.terminal.cols)
- {
- return;
- }
-
// Get the viewport pin so that we can compare it to the current.
const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
@@ -1276,10 +1261,23 @@ pub fn rebuildCells(
}
}
- // Build each cell
+ const grid_size = self.size.grid();
+
+ // We rebuild the cells row-by-row because we do font shaping by row.
var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
- var y: terminal.size.CellCountInt = screen.pages.rows;
+ // If our cell contents buffer is shorter than the screen viewport,
+ // we render the rows that fit, starting from the bottom. If instead
+ // the viewport is shorter than the cell contents buffer, we align
+ // the top of the viewport with the top of the contents buffer.
+ var y: terminal.size.CellCountInt = @min(
+ screen.pages.rows,
+ grid_size.rows,
+ );
while (row_it.next()) |row| {
+ // The viewport may have more rows than our cell contents,
+ // so we need to break from the loop early if we hit y = 0.
+ if (y == 0) break;
+
y -= 1;
// True if we want to do font shaping around the cursor. We want to
@@ -1356,7 +1354,11 @@ pub fn rebuildCells(
var shaper_cells: ?[]const font.shape.Cell = null;
var shaper_cells_i: usize = 0;
- const row_cells = row.cells(.all);
+ const row_cells_all = row.cells(.all);
+
+ // If our viewport is wider than our cell contents buffer,
+ // we still only process cells up to the width of the buffer.
+ const row_cells = row_cells_all[0..@min(row_cells_all.len, grid_size.columns)];
for (row_cells, 0..) |*cell, x| {
// If this cell falls within our preedit range then we
@@ -2350,11 +2352,9 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void {
}
/// Draw the custom shaders.
-fn drawCustomPrograms(
- self: *OpenGL,
- custom_state: *custom.State,
-) !void {
+fn drawCustomPrograms(self: *OpenGL, custom_state: *custom.State) !void {
_ = self;
+ assert(custom_state.programs.len > 0);
// Bind our state that is global to all custom shaders
const custom_bind = try custom_state.bind();
@@ -2365,10 +2365,10 @@ fn drawCustomPrograms(
// Go through each custom shader and draw it.
for (custom_state.programs) |program| {
- // Bind our cell program state, buffers
const bind = try program.bind();
defer bind.unbind();
try bind.draw();
+ try custom_state.copyFramebuffer();
}
}
diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig
index 48056ae5ee..535a0b42b0 100644
--- a/src/renderer/metal/api.zig
+++ b/src/renderer/metal/api.zig
@@ -24,12 +24,36 @@ pub const MTLStoreAction = enum(c_ulong) {
store = 1,
};
-/// https://developer.apple.com/documentation/metal/mtlstoragemode?language=objc
-pub const MTLStorageMode = enum(c_ulong) {
- shared = 0,
- managed = 1,
- private = 2,
- memoryless = 3,
+/// https://developer.apple.com/documentation/metal/mtlresourceoptions?language=objc
+pub const MTLResourceOptions = packed struct(c_ulong) {
+ /// https://developer.apple.com/documentation/metal/mtlcpucachemode?language=objc
+ cpu_cache_mode: CPUCacheMode = .default,
+ /// https://developer.apple.com/documentation/metal/mtlstoragemode?language=objc
+ storage_mode: StorageMode,
+ /// https://developer.apple.com/documentation/metal/mtlhazardtrackingmode?language=objc
+ hazard_tracking_mode: HazardTrackingMode = .default,
+
+ _pad: @Type(.{
+ .Int = .{ .signedness = .unsigned, .bits = @bitSizeOf(c_ulong) - 10 },
+ }) = 0,
+
+ pub const CPUCacheMode = enum(u4) {
+ default = 0,
+ write_combined = 1,
+ };
+
+ pub const StorageMode = enum(u4) {
+ shared = 0,
+ managed = 1,
+ private = 2,
+ memoryless = 3,
+ };
+
+ pub const HazardTrackingMode = enum(u2) {
+ default = 0,
+ untracked = 1,
+ tracked = 2,
+ };
};
/// https://developer.apple.com/documentation/metal/mtlprimitivetype?language=objc
@@ -74,6 +98,7 @@ pub const MTLPixelFormat = enum(c_ulong) {
rgba8unorm = 70,
rgba8uint = 73,
bgra8unorm = 80,
+ bgra8unorm_srgb = 81,
};
/// https://developer.apple.com/documentation/metal/mtlpurgeablestate?language=objc
@@ -138,10 +163,6 @@ pub const MTLTextureUsage = enum(c_ulong) {
pixel_format_view = 8,
};
-/// https://developer.apple.com/documentation/metal/mtlresourceoptions?language=objc
-/// (incomplete, we only use this mode so we just hardcode it)
-pub const MTLResourceStorageModeShared: c_ulong = @intFromEnum(MTLStorageMode.shared) << 4;
-
pub const MTLClearColor = extern struct {
red: f64,
green: f64,
diff --git a/src/renderer/metal/buffer.zig b/src/renderer/metal/buffer.zig
index 55a207f030..4128e297b7 100644
--- a/src/renderer/metal/buffer.zig
+++ b/src/renderer/metal/buffer.zig
@@ -2,6 +2,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const objc = @import("objc");
+const macos = @import("macos");
const mtl = @import("api.zig");
@@ -14,35 +15,46 @@ pub fn Buffer(comptime T: type) type {
return struct {
const Self = @This();
+ /// The resource options for this buffer.
+ options: mtl.MTLResourceOptions,
+
buffer: objc.Object, // MTLBuffer
/// Initialize a buffer with the given length pre-allocated.
- pub fn init(device: objc.Object, len: usize) !Self {
+ pub fn init(
+ device: objc.Object,
+ len: usize,
+ options: mtl.MTLResourceOptions,
+ ) !Self {
const buffer = device.msgSend(
objc.Object,
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(len * @sizeOf(T))),
- mtl.MTLResourceStorageModeShared,
+ options,
},
);
- return .{ .buffer = buffer };
+ return .{ .buffer = buffer, .options = options };
}
/// Init the buffer filled with the given data.
- pub fn initFill(device: objc.Object, data: []const T) !Self {
+ pub fn initFill(
+ device: objc.Object,
+ data: []const T,
+ options: mtl.MTLResourceOptions,
+ ) !Self {
const buffer = device.msgSend(
objc.Object,
objc.sel("newBufferWithBytes:length:options:"),
.{
@as(*const anyopaque, @ptrCast(data.ptr)),
@as(c_ulong, @intCast(data.len * @sizeOf(T))),
- mtl.MTLResourceStorageModeShared,
+ options,
},
);
- return .{ .buffer = buffer };
+ return .{ .buffer = buffer, .options = options };
}
pub fn deinit(self: *Self) void {
@@ -85,7 +97,7 @@ pub fn Buffer(comptime T: type) type {
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(size * @sizeOf(T))),
- mtl.MTLResourceStorageModeShared,
+ self.options,
},
);
}
@@ -106,6 +118,18 @@ pub fn Buffer(comptime T: type) type {
};
@memcpy(dst, src);
+
+ // If we're using the managed resource storage mode, then
+ // we need to signal Metal to synchronize the buffer data.
+ //
+ // Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc
+ if (self.options.storage_mode == .managed) {
+ self.buffer.msgSend(
+ void,
+ "didModifyRange:",
+ .{macos.foundation.Range.init(0, req_bytes)},
+ );
+ }
}
/// Like Buffer.sync but takes data from an array of ArrayLists,
@@ -130,7 +154,7 @@ pub fn Buffer(comptime T: type) type {
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(size * @sizeOf(T))),
- mtl.MTLResourceStorageModeShared,
+ self.options,
},
);
}
@@ -153,6 +177,18 @@ pub fn Buffer(comptime T: type) type {
i += list.items.len * @sizeOf(T);
}
+ // If we're using the managed resource storage mode, then
+ // we need to signal Metal to synchronize the buffer data.
+ //
+ // Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc
+ if (self.options.storage_mode == .managed) {
+ self.buffer.msgSend(
+ void,
+ "didModifyRange:",
+ .{macos.foundation.Range.init(0, req_bytes)},
+ );
+ }
+
return total_len;
}
};
diff --git a/src/renderer/metal/image.zig b/src/renderer/metal/image.zig
index 9d72cae96a..835fbd672b 100644
--- a/src/renderer/metal/image.zig
+++ b/src/renderer/metal/image.zig
@@ -358,6 +358,8 @@ pub const Image = union(enum) {
self: *Image,
alloc: Allocator,
device: objc.Object,
+ /// Storage mode for the MTLTexture object
+ storage_mode: mtl.MTLResourceOptions.StorageMode,
) !void {
// Convert our data if we have to
try self.convert(alloc);
@@ -366,7 +368,7 @@ pub const Image = union(enum) {
const p = self.pending().?;
// Create our texture
- const texture = try initTexture(p, device);
+ const texture = try initTexture(p, device, storage_mode);
errdefer texture.msgSend(void, objc.sel("release"), .{});
// Upload our data
@@ -424,7 +426,12 @@ pub const Image = union(enum) {
};
}
- fn initTexture(p: Pending, device: objc.Object) !objc.Object {
+ fn initTexture(
+ p: Pending,
+ device: objc.Object,
+ /// Storage mode for the MTLTexture object
+ storage_mode: mtl.MTLResourceOptions.StorageMode,
+ ) !objc.Object {
// Create our descriptor
const desc = init: {
const Class = objc.getClass("MTLTextureDescriptor").?;
@@ -438,6 +445,15 @@ pub const Image = union(enum) {
desc.setProperty("width", @as(c_ulong, @intCast(p.width)));
desc.setProperty("height", @as(c_ulong, @intCast(p.height)));
+ desc.setProperty(
+ "resourceOptions",
+ mtl.MTLResourceOptions{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = storage_mode,
+ },
+ );
+
// Initialize
const id = device.msgSend(
?*anyopaque,
diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig
index b909a2f2a9..b297de809c 100644
--- a/src/renderer/metal/shaders.zig
+++ b/src/renderer/metal/shaders.zig
@@ -13,9 +13,7 @@ const log = std.log.scoped(.metal);
pub const Shaders = struct {
library: objc.Object,
- /// The cell shader is the shader used to render the terminal cells.
- /// It is a single shader that is used for both the background and
- /// foreground.
+ /// Renders cell foreground elements (text, decorations).
cell_text_pipeline: objc.Object,
/// The cell background shader is the shader used to render the
@@ -40,17 +38,18 @@ pub const Shaders = struct {
alloc: Allocator,
device: objc.Object,
post_shaders: []const [:0]const u8,
+ pixel_format: mtl.MTLPixelFormat,
) !Shaders {
const library = try initLibrary(device);
errdefer library.msgSend(void, objc.sel("release"), .{});
- const cell_text_pipeline = try initCellTextPipeline(device, library);
+ const cell_text_pipeline = try initCellTextPipeline(device, library, pixel_format);
errdefer cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
- const cell_bg_pipeline = try initCellBgPipeline(device, library);
+ const cell_bg_pipeline = try initCellBgPipeline(device, library, pixel_format);
errdefer cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
- const image_pipeline = try initImagePipeline(device, library);
+ const image_pipeline = try initImagePipeline(device, library, pixel_format);
errdefer image_pipeline.msgSend(void, objc.sel("release"), .{});
const post_pipelines: []const objc.Object = initPostPipelines(
@@ -58,6 +57,7 @@ pub const Shaders = struct {
device,
library,
post_shaders,
+ pixel_format,
) catch |err| err: {
// If an error happens while building postprocess shaders we
// want to just not use any postprocess shaders since we don't
@@ -137,9 +137,29 @@ pub const Uniforms = extern struct {
cursor_pos: [2]u16 align(4),
cursor_color: [4]u8 align(4),
- // Whether the cursor is 2 cells wide.
+ /// The background color for the whole surface.
+ bg_color: [4]u8 align(4),
+
+ /// Whether the cursor is 2 cells wide.
cursor_wide: bool align(1),
+ /// Indicates that colors provided to the shader are already in
+ /// the P3 color space, so they don't need to be converted from
+ /// sRGB.
+ use_display_p3: bool align(1),
+
+ /// Indicates that the color attachments for the shaders have
+ /// an `*_srgb` pixel format, which means the shaders need to
+ /// output linear RGB colors rather than gamma encoded colors,
+ /// since blending will be performed in linear space and then
+ /// Metal itself will re-encode the colors for storage.
+ use_linear_blending: bool align(1),
+
+ /// Enables a weight correction step that makes text rendered
+ /// with linear alpha blending have a similar apparent weight
+ /// (thickness) to gamma-incorrect blending.
+ use_linear_correction: bool align(1) = false,
+
const PaddingExtend = packed struct(u8) {
left: bool = false,
right: bool = false,
@@ -201,6 +221,7 @@ fn initPostPipelines(
device: objc.Object,
library: objc.Object,
shaders: []const [:0]const u8,
+ pixel_format: mtl.MTLPixelFormat,
) ![]const objc.Object {
// If we have no shaders, do nothing.
if (shaders.len == 0) return &.{};
@@ -220,7 +241,12 @@ fn initPostPipelines(
// Build each shader. Note we don't use "0.." to build our index
// because we need to keep track of our length to clean up above.
for (shaders) |source| {
- pipelines[i] = try initPostPipeline(device, library, source);
+ pipelines[i] = try initPostPipeline(
+ device,
+ library,
+ source,
+ pixel_format,
+ );
i += 1;
}
@@ -232,6 +258,7 @@ fn initPostPipeline(
device: objc.Object,
library: objc.Object,
data: [:0]const u8,
+ pixel_format: mtl.MTLPixelFormat,
) !objc.Object {
// Create our library which has the shader source
const post_library = library: {
@@ -301,8 +328,7 @@ fn initPostPipeline(
.{@as(c_ulong, 0)},
);
- // Value is MTLPixelFormatBGRA8Unorm
- attachment.setProperty("pixelFormat", @as(c_ulong, 80));
+ attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
}
// Make our state
@@ -343,7 +369,11 @@ pub const CellText = extern struct {
};
/// Initialize the cell render pipeline for our shader library.
-fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object {
+fn initCellTextPipeline(
+ device: objc.Object,
+ library: objc.Object,
+ pixel_format: mtl.MTLPixelFormat,
+) !objc.Object {
// Get our vertex and fragment functions
const func_vert = func_vert: {
const str = try macos.foundation.String.createWithBytes(
@@ -427,8 +457,7 @@ fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object
.{@as(c_ulong, 0)},
);
- // Value is MTLPixelFormatBGRA8Unorm
- attachment.setProperty("pixelFormat", @as(c_ulong, 80));
+ attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
// Blending. This is required so that our text we render on top
// of our drawable properly blends into the bg.
@@ -458,11 +487,15 @@ fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object
pub const CellBg = [4]u8;
/// Initialize the cell background render pipeline for our shader library.
-fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
+fn initCellBgPipeline(
+ device: objc.Object,
+ library: objc.Object,
+ pixel_format: mtl.MTLPixelFormat,
+) !objc.Object {
// Get our vertex and fragment functions
const func_vert = func_vert: {
const str = try macos.foundation.String.createWithBytes(
- "full_screen_vertex",
+ "cell_bg_vertex",
.utf8,
false,
);
@@ -507,8 +540,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
.{@as(c_ulong, 0)},
);
- // Value is MTLPixelFormatBGRA8Unorm
- attachment.setProperty("pixelFormat", @as(c_ulong, 80));
+ attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
// Blending. This is required so that our text we render on top
// of our drawable properly blends into the bg.
@@ -535,7 +567,11 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
}
/// Initialize the image render pipeline for our shader library.
-fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object {
+fn initImagePipeline(
+ device: objc.Object,
+ library: objc.Object,
+ pixel_format: mtl.MTLPixelFormat,
+) !objc.Object {
// Get our vertex and fragment functions
const func_vert = func_vert: {
const str = try macos.foundation.String.createWithBytes(
@@ -619,8 +655,7 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object {
.{@as(c_ulong, 0)},
);
- // Value is MTLPixelFormatBGRA8Unorm
- attachment.setProperty("pixelFormat", @as(c_ulong, 80));
+ attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
// Blending. This is required so that our text we render on top
// of our drawable properly blends into the bg.
diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig
index 2cab0940c5..859277ce51 100644
--- a/src/renderer/opengl/custom.zig
+++ b/src/renderer/opengl/custom.zig
@@ -230,6 +230,21 @@ pub const State = struct {
};
}
+ /// Copy the fbo's attached texture to the backbuffer.
+ pub fn copyFramebuffer(self: *State) !void {
+ const texbind = try self.fb_texture.bind(.@"2D");
+ errdefer texbind.unbind();
+ try texbind.copySubImage2D(
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ @intFromFloat(self.uniforms.resolution[0]),
+ @intFromFloat(self.uniforms.resolution[1]),
+ );
+ }
+
pub const Binding = struct {
vao: gl.VertexArray.Binding,
ebo: gl.Buffer.Binding,
@@ -251,7 +266,6 @@ pub const Program = struct {
const program = try gl.Program.createVF(
@embedFile("../shaders/custom.v.glsl"),
src,
- //@embedFile("../shaders/temp.f.glsl"),
);
errdefer program.destroy();
diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal
index 2a107402b2..e24ddcb1ef 100644
--- a/src/renderer/shaders/cell.metal
+++ b/src/renderer/shaders/cell.metal
@@ -18,7 +18,11 @@ struct Uniforms {
float min_contrast;
ushort2 cursor_pos;
uchar4 cursor_color;
+ uchar4 bg_color;
bool cursor_wide;
+ bool use_display_p3;
+ bool use_linear_blending;
+ bool use_linear_correction;
};
//-------------------------------------------------------------------
@@ -26,40 +30,88 @@ struct Uniforms {
//-------------------------------------------------------------------
#pragma mark - Colors
-// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
-float luminance_component(float c) {
- if (c <= 0.03928f) {
- return c / 12.92f;
- } else {
- return pow((c + 0.055f) / 1.055f, 2.4f);
- }
+// D50-adapted sRGB to XYZ conversion matrix.
+// http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html
+constant float3x3 sRGB_XYZ = transpose(float3x3(
+ 0.4360747, 0.3850649, 0.1430804,
+ 0.2225045, 0.7168786, 0.0606169,
+ 0.0139322, 0.0971045, 0.7141733
+));
+// XYZ to Display P3 conversion matrix.
+// http://endavid.com/index.php?entry=79
+constant float3x3 XYZ_DP3 = transpose(float3x3(
+ 2.40414768,-0.99010704,-0.39759019,
+ -0.84239098, 1.79905954, 0.01597023,
+ 0.04838763,-0.09752546, 1.27393636
+));
+// By composing the two above matrices we get
+// our sRGB to Display P3 conversion matrix.
+constant float3x3 sRGB_DP3 = XYZ_DP3 * sRGB_XYZ;
+
+// Converts a color in linear sRGB to linear Display P3
+//
+// TODO: The color matrix should probably be computed
+// dynamically and passed as a uniform, rather
+// than being hard coded above.
+float3 srgb_to_display_p3(float3 srgb) {
+ return sRGB_DP3 * srgb;
+}
+
+// Converts a color from sRGB gamma encoding to linear.
+float4 linearize(float4 srgb) {
+ bool3 cutoff = srgb.rgb <= 0.04045;
+ float3 lower = srgb.rgb / 12.92;
+ float3 higher = pow((srgb.rgb + 0.055) / 1.055, 2.4);
+ srgb.rgb = mix(higher, lower, float3(cutoff));
+
+ return srgb;
}
+float linearize(float v) {
+ return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
+}
+
+// Converts a color from linear to sRGB gamma encoding.
+float4 unlinearize(float4 linear) {
+ bool3 cutoff = linear.rgb <= 0.0031308;
+ float3 lower = linear.rgb * 12.92;
+ float3 higher = pow(linear.rgb, 1.0 / 2.4) * 1.055 - 0.055;
+ linear.rgb = mix(higher, lower, float3(cutoff));
-float relative_luminance(float3 color) {
- color.r = luminance_component(color.r);
- color.g = luminance_component(color.g);
- color.b = luminance_component(color.b);
- float3 weights = float3(0.2126f, 0.7152f, 0.0722f);
- return dot(color, weights);
+ return linear;
+}
+float unlinearize(float v) {
+ return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055;
+}
+
+// Compute the luminance of the provided color.
+//
+// Takes colors in linear RGB space. If your colors are gamma
+// encoded, linearize them before using them with this function.
+float luminance(float3 color) {
+ return dot(color, float3(0.2126f, 0.7152f, 0.0722f));
}
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
+//
+// Takes colors in linear RGB space. If your colors are gamma
+// encoded, linearize them before using them with this function.
float contrast_ratio(float3 color1, float3 color2) {
- float l1 = relative_luminance(color1);
- float l2 = relative_luminance(color2);
+ float l1 = luminance(color1);
+ float l2 = luminance(color2);
return (max(l1, l2) + 0.05f) / (min(l1, l2) + 0.05f);
}
// Return the fg if the contrast ratio is greater than min, otherwise
// return a color that satisfies the contrast ratio. Currently, the color
// is always white or black, whichever has the highest contrast ratio.
+//
+// Takes colors in linear RGB space. If your colors are gamma
+// encoded, linearize them before using them with this function.
float4 contrasted_color(float min, float4 fg, float4 bg) {
- float3 fg_premult = fg.rgb * fg.a;
- float3 bg_premult = bg.rgb * bg.a;
- float ratio = contrast_ratio(fg_premult, bg_premult);
+ float ratio = contrast_ratio(fg.rgb, bg.rgb);
if (ratio < min) {
- float white_ratio = contrast_ratio(float3(1.0f), bg_premult);
- float black_ratio = contrast_ratio(float3(0.0f), bg_premult);
+ float white_ratio = contrast_ratio(float3(1.0f), bg.rgb);
+ float black_ratio = contrast_ratio(float3(0.0f), bg.rgb);
if (white_ratio > black_ratio) {
return float4(1.0f);
} else {
@@ -70,6 +122,62 @@ float4 contrasted_color(float min, float4 fg, float4 bg) {
return fg;
}
+// Load a 4 byte RGBA non-premultiplied color and linearize
+// and convert it as necessary depending on the provided info.
+//
+// Returns a color in the Display P3 color space.
+//
+// If `display_p3` is true, then the provided color is assumed to
+// already be in the Display P3 color space, otherwise it's treated
+// as an sRGB color and is appropriately converted to Display P3.
+//
+// `linear` controls whether the returned color is linear or gamma encoded.
+float4 load_color(
+ uchar4 in_color,
+ bool display_p3,
+ bool linear
+) {
+ // 0 .. 255 -> 0.0 .. 1.0
+ float4 color = float4(in_color) / 255.0f;
+
+ // If our color is already in Display P3 and
+ // we aren't doing linear blending, then we
+ // already have the correct color here and
+ // can premultiply and return it.
+ if (display_p3 && !linear) {
+ color.rgb *= color.a;
+ return color;
+ }
+
+ // The color is in either the sRGB or Display P3 color space,
+ // so in either case, it's a color space which uses the sRGB
+ // transfer function, so we can use one function in order to
+ // linearize it in either case.
+ //
+ // Even if we aren't doing linear blending, the color
+ // needs to be in linear space to convert color spaces.
+ color = linearize(color);
+
+ // If we're *NOT* using display P3 colors, then we're dealing
+ // with an sRGB color, in which case we need to convert it in
+ // to the Display P3 color space, since our output is always
+ // Display P3.
+ if (!display_p3) {
+ color.rgb = srgb_to_display_p3(color.rgb);
+ }
+
+ // If we're not doing linear blending, then we need to
+ // unlinearize after doing the color space conversion.
+ if (!linear) {
+ color = unlinearize(color);
+ }
+
+ // Premultiply our color by its alpha.
+ color.rgb *= color.a;
+
+ return color;
+}
+
//-------------------------------------------------------------------
// Full Screen Vertex Shader
//-------------------------------------------------------------------
@@ -112,25 +220,62 @@ vertex FullScreenVertexOut full_screen_vertex(
//-------------------------------------------------------------------
#pragma mark - Cell BG Shader
+struct CellBgVertexOut {
+ float4 position [[position]];
+ float4 bg_color;
+};
+
+vertex CellBgVertexOut cell_bg_vertex(
+ uint vid [[vertex_id]],
+ constant Uniforms& uniforms [[buffer(1)]]
+) {
+ CellBgVertexOut out;
+
+ float4 position;
+ position.x = (vid == 2) ? 3.0 : -1.0;
+ position.y = (vid == 0) ? -3.0 : 1.0;
+ position.zw = 1.0;
+ out.position = position;
+
+ // Convert the background color to Display P3
+ out.bg_color = load_color(
+ uniforms.bg_color,
+ uniforms.use_display_p3,
+ uniforms.use_linear_blending
+ );
+
+ return out;
+}
+
fragment float4 cell_bg_fragment(
- FullScreenVertexOut in [[stage_in]],
+ CellBgVertexOut in [[stage_in]],
constant uchar4 *cells [[buffer(0)]],
constant Uniforms& uniforms [[buffer(1)]]
) {
int2 grid_pos = int2(floor((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size));
+ float4 bg = float4(0.0);
+ // If we have any background transparency then we render bg-colored cells as
+ // fully transparent, since the background is handled by the layer bg color
+ // and we don't want to double up our bg color, but if our bg color is fully
+ // opaque then our layer is opaque and can't handle transparency, so we need
+ // to return the bg color directly instead.
+ if (uniforms.bg_color.a == 255) {
+ bg = in.bg_color;
+ }
+
// Clamp x position, extends edge bg colors in to padding on sides.
if (grid_pos.x < 0) {
if (uniforms.padding_extend & EXTEND_LEFT) {
grid_pos.x = 0;
} else {
- return float4(0.0);
+ return bg;
}
} else if (grid_pos.x > uniforms.grid_size.x - 1) {
if (uniforms.padding_extend & EXTEND_RIGHT) {
grid_pos.x = uniforms.grid_size.x - 1;
} else {
- return float4(0.0);
+ return bg;
}
}
@@ -139,18 +284,40 @@ fragment float4 cell_bg_fragment(
if (uniforms.padding_extend & EXTEND_UP) {
grid_pos.y = 0;
} else {
- return float4(0.0);
+ return bg;
}
} else if (grid_pos.y > uniforms.grid_size.y - 1) {
if (uniforms.padding_extend & EXTEND_DOWN) {
grid_pos.y = uniforms.grid_size.y - 1;
} else {
- return float4(0.0);
+ return bg;
}
}
- // Retrieve color for cell and return it.
- return float4(cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x]) / 255.0;
+ // Load the color for the cell.
+ uchar4 cell_color = cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x];
+
+ // We have special case handling for when the cell color matches the bg color.
+ if (all(cell_color == uniforms.bg_color)) {
+ return bg;
+ }
+
+ // Convert the color and return it.
+ //
+ // TODO: We may want to blend the color with the background
+ // color, rather than purely replacing it, this needs
+ // some consideration about config options though.
+ //
+ // TODO: It might be a good idea to do a pass before this
+ // to convert all of the bg colors, so we don't waste
+ // a bunch of work converting the cell color in every
+ // fragment of each cell. It's not the most epxensive
+ // operation, but it is still wasted work.
+ return load_color(
+ cell_color,
+ uniforms.use_display_p3,
+ uniforms.use_linear_blending
+ );
}
//-------------------------------------------------------------------
@@ -192,8 +359,9 @@ struct CellTextVertexIn {
struct CellTextVertexOut {
float4 position [[position]];
- uint8_t mode;
- float4 color;
+ uint8_t mode [[flat]];
+ float4 color [[flat]];
+ float4 bg_color [[flat]];
float2 tex_coord;
};
@@ -222,7 +390,6 @@ vertex CellTextVertexOut cell_text_vertex(
CellTextVertexOut out;
out.mode = in.mode;
- out.color = float4(in.color) / 255.0f;
// === Grid Cell ===
// +X
@@ -277,6 +444,21 @@ vertex CellTextVertexOut cell_text_vertex(
// be sampled with pixel coordinate mode.
out.tex_coord = float2(in.glyph_pos) + float2(in.glyph_size) * corner;
+ // Get our color. We always fetch a linearized version to
+ // make it easier to handle minimum contrast calculations.
+ out.color = load_color(
+ in.color,
+ uniforms.use_display_p3,
+ true
+ );
+
+ // Get the BG color
+ out.bg_color = load_color(
+ bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x],
+ uniforms.use_display_p3,
+ true
+ );
+
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
@@ -285,21 +467,24 @@ vertex CellTextVertexOut cell_text_vertex(
// and Powerline glyphs to be unaffected (else parts of the line would
// have different colors as some parts are displayed via background colors).
if (uniforms.min_contrast > 1.0f && in.mode == MODE_TEXT) {
- float4 bg_color = float4(bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x]) / 255.0f;
- out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color);
+ // Ensure our minimum contrast
+ out.color = contrasted_color(uniforms.min_contrast, out.color, out.bg_color);
}
- // If this cell is the cursor cell, then we need to change the color.
- if (
- in.mode != MODE_TEXT_CURSOR &&
- (
+ // Check if current position is under cursor (including wide cursor)
+ bool is_cursor_pos = (
in.grid_pos.x == uniforms.cursor_pos.x ||
uniforms.cursor_wide &&
in.grid_pos.x == uniforms.cursor_pos.x + 1
- ) &&
- in.grid_pos.y == uniforms.cursor_pos.y
- ) {
- out.color = float4(uniforms.cursor_color) / 255.0f;
+ ) && in.grid_pos.y == uniforms.cursor_pos.y;
+
+ // If this cell is the cursor cell, then we need to change the color.
+ if (in.mode != MODE_TEXT_CURSOR && is_cursor_pos) {
+ out.color = load_color(
+ uniforms.cursor_color,
+ uniforms.use_display_p3,
+ false
+ );
}
return out;
@@ -308,7 +493,8 @@ vertex CellTextVertexOut cell_text_vertex(
fragment float4 cell_text_fragment(
CellTextVertexOut in [[stage_in]],
texture2d textureGrayscale [[texture(0)]],
- texture2d textureColor [[texture(1)]]
+ texture2d textureColor [[texture(1)]],
+ constant Uniforms& uniforms [[buffer(2)]]
) {
constexpr sampler textureSampler(
coord::pixel,
@@ -322,20 +508,72 @@ fragment float4 cell_text_fragment(
case MODE_TEXT_CONSTRAINED:
case MODE_TEXT_POWERLINE:
case MODE_TEXT: {
- // We premult the alpha to our whole color since our blend function
- // uses One/OneMinusSourceAlpha to avoid blurry edges.
- // We first premult our given color.
- float4 premult = float4(in.color.rgb * in.color.a, in.color.a);
-
- // Then premult the texture color
+ // Our input color is always linear.
+ float4 color = in.color;
+
+ // If we're not doing linear blending, then we need to
+ // re-apply the gamma encoding to our color manually.
+ //
+ // Since the alpha is premultiplied, we need to divide
+ // it out before unlinearizing and re-multiply it after.
+ if (!uniforms.use_linear_blending) {
+ color.rgb /= color.a;
+ color = unlinearize(color);
+ color.rgb *= color.a;
+ }
+
+ // Fetch our alpha mask for this pixel.
float a = textureGrayscale.sample(textureSampler, in.tex_coord).r;
- premult = premult * a;
- return premult;
+ // Linear blending weight correction corrects the alpha value to
+ // produce blending results which match gamma-incorrect blending.
+ if (uniforms.use_linear_correction) {
+ // Short explanation of how this works:
+ //
+ // We get the luminances of the foreground and background colors,
+ // and then unlinearize them and perform blending on them. This
+ // gives us our desired luminance, which we derive our new alpha
+ // value from by mapping the range [bg_l, fg_l] to [0, 1], since
+ // our final blend will be a linear interpolation from bg to fg.
+ //
+ // This yields virtually identical results for grayscale blending,
+ // and very similar but non-identical results for color blending.
+ float4 bg = in.bg_color;
+ float fg_l = luminance(color.rgb);
+ float bg_l = luminance(bg.rgb);
+ // To avoid numbers going haywire, we don't apply correction
+ // when the bg and fg luminances are within 0.001 of each other.
+ if (abs(fg_l - bg_l) > 0.001) {
+ float blend_l = linearize(unlinearize(fg_l) * a + unlinearize(bg_l) * (1.0 - a));
+ a = clamp((blend_l - bg_l) / (fg_l - bg_l), 0.0, 1.0);
+ }
+ }
+
+ // Multiply our whole color by the alpha mask.
+ // Since we use premultiplied alpha, this is
+ // the correct way to apply the mask.
+ color *= a;
+
+ return color;
}
case MODE_TEXT_COLOR: {
- return textureColor.sample(textureSampler, in.tex_coord);
+ // For now, we assume that color glyphs are
+ // already premultiplied Display P3 colors.
+ float4 color = textureColor.sample(textureSampler, in.tex_coord);
+
+ // If we aren't doing linear blending, we can return this right away.
+ if (!uniforms.use_linear_blending) {
+ return color;
+ }
+
+ // Otherwise we need to linearize the color. Since the alpha is
+ // premultiplied, we need to divide it out before linearizing.
+ color.rgb /= color.a;
+ color = linearize(color);
+ color.rgb *= color.a;
+
+ return color;
}
}
}
@@ -409,7 +647,8 @@ vertex ImageVertexOut image_vertex(
fragment float4 image_fragment(
ImageVertexOut in [[stage_in]],
- texture2d image [[texture(0)]]
+ texture2d image [[texture(0)]],
+ constant Uniforms& uniforms [[buffer(1)]]
) {
constexpr sampler textureSampler(address::clamp_to_edge, filter::linear);
@@ -418,10 +657,12 @@ fragment float4 image_fragment(
// our texture to BGRA8Unorm.
uint4 rgba = image.sample(textureSampler, in.tex_coord);
- // Convert to float4 and premultiply the alpha. We should also probably
- // premultiply the alpha in the texture.
- float4 result = float4(rgba) / 255.0f;
- result.rgb *= result.a;
- return result;
+ return load_color(
+ uchar4(rgba),
+ // We assume all images are sRGB regardless of the configured colorspace
+ // TODO: Maybe support wide gamut images?
+ false,
+ uniforms.use_linear_blending
+ );
}
diff --git a/src/shell-integration/README.md b/src/shell-integration/README.md
index 976cf49240..3d5159c711 100644
--- a/src/shell-integration/README.md
+++ b/src/shell-integration/README.md
@@ -6,7 +6,7 @@ supports.
This README is meant as developer documentation and not as
user documentation. For user documentation, see the main
-README.
+README or [ghostty.org](https://ghostty.org/docs)
## Implementation Details
diff --git a/src/shell-integration/bash/bash-preexec.sh b/src/shell-integration/bash/bash-preexec.sh
index 14a677888d..e07da0d1e5 100644
--- a/src/shell-integration/bash/bash-preexec.sh
+++ b/src/shell-integration/bash/bash-preexec.sh
@@ -250,10 +250,8 @@ __bp_preexec_invoke_exec() {
fi
local this_command
- this_command=$(
- export LC_ALL=C
- HISTTIMEFORMAT='' builtin history 1 | sed '1 s/^ *[0-9][0-9]*[* ] //'
- )
+ this_command=$(LC_ALL=C HISTTIMEFORMAT='' builtin history 1)
+ this_command="${this_command#*[[:digit:]][* ] }"
# Sanity check to make sure we have something to invoke our function with.
if [[ -z "$this_command" ]]; then
@@ -297,10 +295,8 @@ __bp_install() {
trap '__bp_preexec_invoke_exec "$_"' DEBUG
# Preserve any prior DEBUG trap as a preexec function
- local prior_trap
- # we can't easily do this with variable expansion. Leaving as sed command.
- # shellcheck disable=SC2001
- prior_trap=$(sed "s/[^']*'\(.*\)'[^']*/\1/" <<<"${__bp_trap_string:-}")
+ eval "local trap_argv=(${__bp_trap_string:-})"
+ local prior_trap=${trap_argv[2]:-}
unset __bp_trap_string
if [[ -n "$prior_trap" ]]; then
eval '__bp_original_debug_trap() {
diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash
index 71c644b696..7fae435a3a 100644
--- a/src/shell-integration/bash/ghostty.bash
+++ b/src/shell-integration/bash/ghostty.bash
@@ -20,14 +20,13 @@
if [[ "$-" != *i* ]] ; then builtin return; fi
if [ -z "$GHOSTTY_RESOURCES_DIR" ]; then builtin return; fi
-# When automatic shell integration is active, we need to manually
-# load the normal bash startup files based on the injected state.
+# When automatic shell integration is active, we were started in POSIX
+# mode and need to manually recreate the bash startup sequence.
if [ -n "$GHOSTTY_BASH_INJECT" ]; then
- builtin declare ghostty_bash_inject="$GHOSTTY_BASH_INJECT"
- builtin unset GHOSTTY_BASH_INJECT ENV
-
- # At this point, we're in POSIX mode and rely on the injected
- # flags to guide is through the rest of the startup sequence.
+ # Store a temporary copy of our startup flags and unset these global
+ # environment variables so we can safely handle reentrancy.
+ builtin declare __ghostty_bash_flags="$GHOSTTY_BASH_INJECT"
+ builtin unset ENV GHOSTTY_BASH_INJECT
# Restore bash's default 'posix' behavior. Also reset 'inherit_errexit',
# which doesn't happen as part of the 'posix' reset.
@@ -40,35 +39,34 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
builtin unset GHOSTTY_BASH_UNEXPORT_HISTFILE
fi
- # Manually source the startup files, respecting the injected flags like
- # --norc and --noprofile that we parsed with the shell integration code.
- #
- # See also: run_startup_files() in shell.c in the Bash source code
+ # Manually source the startup files. See INVOCATION in bash(1) and
+ # run_startup_files() in shell.c in the Bash source code.
if builtin shopt -q login_shell; then
- if [[ $ghostty_bash_inject != *"--noprofile"* ]]; then
+ if [[ $__ghostty_bash_flags != *"--noprofile"* ]]; then
[ -r /etc/profile ] && builtin source "/etc/profile"
- for rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
- [ -r "$rcfile" ] && { builtin source "$rcfile"; break; }
+ for __ghostty_rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
+ [ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile"; break; }
done
fi
else
- if [[ $ghostty_bash_inject != *"--norc"* ]]; then
+ if [[ $__ghostty_bash_flags != *"--norc"* ]]; then
# The location of the system bashrc is determined at bash build
# time via -DSYS_BASHRC and can therefore vary across distros:
# Arch, Debian, Ubuntu use /etc/bash.bashrc
# Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_BASHRC
# Void Linux uses /etc/bash/bashrc
# Nixos uses /etc/bashrc
- for rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do
- [ -r "$rcfile" ] && { builtin source "$rcfile"; break; }
+ for __ghostty_rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do
+ [ -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"
fi
fi
+ builtin unset __ghostty_rcfile
+ builtin unset __ghostty_bash_flags
builtin unset GHOSTTY_BASH_RCFILE
- builtin unset ghostty_bash_inject rcfile
fi
# Sudo
diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish
index 420a495286..cd4f56105b 100644
--- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish
+++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish
@@ -71,11 +71,11 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
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 --local 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 --local sudo_has_sudoedit_flags "yes"
+ set --function sudo_has_sudoedit_flags "yes"
break
end
# Check if argument is neither an option nor a key-value pair
diff --git a/src/stb/stb_image.h b/src/stb/stb_image.h
index 5e807a0a6e..3ae1815c16 100644
--- a/src/stb/stb_image.h
+++ b/src/stb/stb_image.h
@@ -4962,7 +4962,7 @@ static int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int
p = (stbi_uc *) stbi__malloc_mad2(pixel_count, pal_img_n, 0);
if (p == NULL) return stbi__err("outofmem", "Out of memory");
- // between here and free(out) below, exitting would leak
+ // between here and free(out) below, exiting would leak
temp_out = p;
if (pal_img_n == 3) {
diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig
index 260733b945..b838332b0a 100644
--- a/src/terminal/PageList.zig
+++ b/src/terminal/PageList.zig
@@ -520,6 +520,7 @@ pub fn clone(
assert(node.data.capacity.rows >= chunk.end - chunk.start);
defer node.data.assertIntegrity();
node.data.size.rows = chunk.end - chunk.start;
+ node.data.size.cols = chunk.node.data.size.cols;
try node.data.cloneFrom(
&chunk.node.data,
chunk.start,
diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig
index 9aebdbd3a5..a779c3350e 100644
--- a/src/terminal/Parser.zig
+++ b/src/terminal/Parser.zig
@@ -6,6 +6,7 @@ const Parser = @This();
const std = @import("std");
const builtin = @import("builtin");
+const assert = std.debug.assert;
const testing = std.testing;
const table = @import("parse_table.zig").table;
const osc = @import("osc.zig");
@@ -81,11 +82,15 @@ pub const Action = union(enum) {
pub const CSI = struct {
intermediates: []u8,
params: []u16,
+ params_sep: SepList,
final: u8,
- sep: Sep,
+
+ /// The list of separators used for CSI params. The value of the
+ /// bit can be mapped to Sep.
+ pub const SepList = std.StaticBitSet(MAX_PARAMS);
/// The separator used for CSI params.
- pub const Sep = enum { semicolon, colon };
+ pub const Sep = enum(u1) { semicolon = 0, colon = 1 };
// Implement formatter for logging
pub fn format(
@@ -183,15 +188,6 @@ pub const Action = union(enum) {
}
};
-/// Keeps track of the parameter sep used for CSI params. We allow colons
-/// to be used ONLY by the 'm' CSI action.
-pub const ParamSepState = enum(u8) {
- none = 0,
- semicolon = ';',
- colon = ':',
- mixed = 1,
-};
-
/// Maximum number of intermediate characters during parsing. This is
/// 4 because we also use the intermediates array for UTF8 decoding which
/// can be at most 4 bytes.
@@ -207,8 +203,8 @@ intermediates_idx: u8 = 0,
/// Param tracking, building
params: [MAX_PARAMS]u16 = undefined,
+params_sep: Action.CSI.SepList = Action.CSI.SepList.initEmpty(),
params_idx: u8 = 0,
-params_sep: ParamSepState = .none,
param_acc: u16 = 0,
param_acc_idx: u8 = 0,
@@ -312,13 +308,9 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action {
// Ignore too many parameters
if (self.params_idx >= MAX_PARAMS) break :param null;
- // If this is our first time seeing a parameter, we track
- // the separator used so that we can't mix separators later.
- if (self.params_idx == 0) self.params_sep = @enumFromInt(c);
- if (@as(ParamSepState, @enumFromInt(c)) != self.params_sep) self.params_sep = .mixed;
-
// Set param final value
self.params[self.params_idx] = self.param_acc;
+ if (c == ':') self.params_sep.set(self.params_idx);
self.params_idx += 1;
// Reset current param value to 0
@@ -359,29 +351,18 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action {
.csi_dispatch = .{
.intermediates = self.intermediates[0..self.intermediates_idx],
.params = self.params[0..self.params_idx],
+ .params_sep = self.params_sep,
.final = c,
- .sep = switch (self.params_sep) {
- .none, .semicolon => .semicolon,
- .colon => .colon,
-
- // There is nothing that treats mixed separators specially
- // afaik so we just treat it as a semicolon.
- .mixed => .semicolon,
- },
},
};
// We only allow colon or mixed separators for the 'm' command.
- switch (self.params_sep) {
- .none => {},
- .semicolon => {},
- .colon, .mixed => if (c != 'm') {
- log.warn(
- "CSI colon or mixed separators only allowed for 'm' command, got: {}",
- .{result},
- );
- break :csi_dispatch null;
- },
+ if (c != 'm' and self.params_sep.count() > 0) {
+ log.warn(
+ "CSI colon or mixed separators only allowed for 'm' command, got: {}",
+ .{result},
+ );
+ break :csi_dispatch null;
}
break :csi_dispatch result;
@@ -400,7 +381,7 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action {
pub fn clear(self: *Parser) void {
self.intermediates_idx = 0;
self.params_idx = 0;
- self.params_sep = .none;
+ self.params_sep = Action.CSI.SepList.initEmpty();
self.param_acc = 0;
self.param_acc_idx = 0;
}
@@ -507,10 +488,11 @@ test "csi: SGR ESC [ 38 : 2 m" {
const d = a[1].?.csi_dispatch;
try testing.expect(d.final == 'm');
- try testing.expect(d.sep == .colon);
try testing.expect(d.params.len == 2);
try testing.expectEqual(@as(u16, 38), d.params[0]);
+ try testing.expect(d.params_sep.isSet(0));
try testing.expectEqual(@as(u16, 2), d.params[1]);
+ try testing.expect(!d.params_sep.isSet(1));
}
}
@@ -581,13 +563,17 @@ test "csi: SGR ESC [ 48 : 2 m" {
const d = a[1].?.csi_dispatch;
try testing.expect(d.final == 'm');
- try testing.expect(d.sep == .colon);
try testing.expect(d.params.len == 5);
try testing.expectEqual(@as(u16, 48), d.params[0]);
+ try testing.expect(d.params_sep.isSet(0));
try testing.expectEqual(@as(u16, 2), d.params[1]);
+ try testing.expect(d.params_sep.isSet(1));
try testing.expectEqual(@as(u16, 240), d.params[2]);
+ try testing.expect(d.params_sep.isSet(2));
try testing.expectEqual(@as(u16, 143), d.params[3]);
+ try testing.expect(d.params_sep.isSet(3));
try testing.expectEqual(@as(u16, 104), d.params[4]);
+ try testing.expect(!d.params_sep.isSet(4));
}
}
@@ -608,10 +594,11 @@ test "csi: SGR ESC [4:3m colon" {
const d = a[1].?.csi_dispatch;
try testing.expect(d.final == 'm');
- try testing.expect(d.sep == .colon);
try testing.expect(d.params.len == 2);
try testing.expectEqual(@as(u16, 4), d.params[0]);
+ try testing.expect(d.params_sep.isSet(0));
try testing.expectEqual(@as(u16, 3), d.params[1]);
+ try testing.expect(!d.params_sep.isSet(1));
}
}
@@ -634,14 +621,71 @@ test "csi: SGR with many blank and colon" {
const d = a[1].?.csi_dispatch;
try testing.expect(d.final == 'm');
- try testing.expect(d.sep == .colon);
try testing.expect(d.params.len == 6);
try testing.expectEqual(@as(u16, 58), d.params[0]);
+ try testing.expect(d.params_sep.isSet(0));
try testing.expectEqual(@as(u16, 2), d.params[1]);
+ try testing.expect(d.params_sep.isSet(1));
try testing.expectEqual(@as(u16, 0), d.params[2]);
+ try testing.expect(d.params_sep.isSet(2));
try testing.expectEqual(@as(u16, 240), d.params[3]);
+ try testing.expect(d.params_sep.isSet(3));
try testing.expectEqual(@as(u16, 143), d.params[4]);
+ try testing.expect(d.params_sep.isSet(4));
try testing.expectEqual(@as(u16, 104), d.params[5]);
+ try testing.expect(!d.params_sep.isSet(5));
+ }
+}
+
+// This is from a Kakoune actual SGR sequence.
+test "csi: SGR mixed colon and semicolon with blank" {
+ var p = init();
+ _ = p.next(0x1B);
+ for ("[;4:3;38;2;175;175;215;58:2::190:80:70") |c| {
+ const a = p.next(c);
+ try testing.expect(a[0] == null);
+ try testing.expect(a[1] == null);
+ try testing.expect(a[2] == null);
+ }
+
+ {
+ const a = p.next('m');
+ try testing.expect(p.state == .ground);
+ try testing.expect(a[0] == null);
+ try testing.expect(a[1].? == .csi_dispatch);
+ try testing.expect(a[2] == null);
+
+ const d = a[1].?.csi_dispatch;
+ try testing.expect(d.final == 'm');
+ try testing.expectEqual(14, d.params.len);
+ try testing.expectEqual(@as(u16, 0), d.params[0]);
+ try testing.expect(!d.params_sep.isSet(0));
+ try testing.expectEqual(@as(u16, 4), d.params[1]);
+ try testing.expect(d.params_sep.isSet(1));
+ try testing.expectEqual(@as(u16, 3), d.params[2]);
+ try testing.expect(!d.params_sep.isSet(2));
+ try testing.expectEqual(@as(u16, 38), d.params[3]);
+ try testing.expect(!d.params_sep.isSet(3));
+ try testing.expectEqual(@as(u16, 2), d.params[4]);
+ try testing.expect(!d.params_sep.isSet(4));
+ try testing.expectEqual(@as(u16, 175), d.params[5]);
+ try testing.expect(!d.params_sep.isSet(5));
+ try testing.expectEqual(@as(u16, 175), d.params[6]);
+ try testing.expect(!d.params_sep.isSet(6));
+ try testing.expectEqual(@as(u16, 215), d.params[7]);
+ try testing.expect(!d.params_sep.isSet(7));
+ try testing.expectEqual(@as(u16, 58), d.params[8]);
+ try testing.expect(d.params_sep.isSet(8));
+ try testing.expectEqual(@as(u16, 2), d.params[9]);
+ try testing.expect(d.params_sep.isSet(9));
+ try testing.expectEqual(@as(u16, 0), d.params[10]);
+ try testing.expect(d.params_sep.isSet(10));
+ try testing.expectEqual(@as(u16, 190), d.params[11]);
+ try testing.expect(d.params_sep.isSet(11));
+ try testing.expectEqual(@as(u16, 80), d.params[12]);
+ try testing.expect(d.params_sep.isSet(12));
+ try testing.expectEqual(@as(u16, 70), d.params[13]);
+ try testing.expect(!d.params_sep.isSet(13));
}
}
diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig
index eb70d32d07..273e1aebec 100644
--- a/src/terminal/Screen.zig
+++ b/src/terminal/Screen.zig
@@ -278,12 +278,9 @@ pub fn reset(self: *Screen) void {
.page_cell = cursor_rac.cell,
};
- // Clear kitty graphics
- self.kitty_images.delete(
- self.alloc,
- undefined, // All image deletion doesn't need the terminal
- .{ .all = true },
- );
+ // Reset kitty graphics storage
+ self.kitty_images.deinit(self.alloc, self);
+ self.kitty_images = .{ .dirty = true };
// Reset our basic state
self.saved_cursor = null;
@@ -474,26 +471,42 @@ pub fn adjustCapacity(
const new_node = try self.pages.adjustCapacity(node, adjustment);
const new_page: *Page = &new_node.data;
- // All additions below have unreachable catches because when
- // we adjust cap we should have enough memory to fit the
- // existing data.
-
- // Re-add the style
+ // Re-add the style, if the page somehow doesn't have enough
+ // memory to add it, we emit a warning and gracefully degrade
+ // to the default style for the cursor.
if (self.cursor.style_id != 0) {
self.cursor.style_id = new_page.styles.add(
new_page.memory,
self.cursor.style,
- ) catch unreachable;
+ ) catch |err| id: {
+ // TODO: Should we increase the capacity further in this case?
+ log.warn(
+ "(Screen.adjustCapacity) Failed to add cursor style back to page, err={}",
+ .{err},
+ );
+
+ // Reset the cursor style.
+ self.cursor.style = .{};
+ break :id style.default_id;
+ };
}
- // Re-add the hyperlink
+ // Re-add the hyperlink, if the page somehow doesn't have enough
+ // memory to add it, we emit a warning and gracefully degrade to
+ // no hyperlink.
if (self.cursor.hyperlink) |link| {
// So we don't attempt to free any memory in the replaced page.
self.cursor.hyperlink_id = 0;
self.cursor.hyperlink = null;
// Re-add
- self.startHyperlinkOnce(link.*) catch unreachable;
+ self.startHyperlinkOnce(link.*) catch |err| {
+ // TODO: Should we increase the capacity further in this case?
+ log.warn(
+ "(Screen.adjustCapacity) Failed to add cursor hyperlink back to page, err={}",
+ .{err},
+ );
+ };
// Remove our old link
link.deinit(self.alloc);
@@ -1003,8 +1016,9 @@ pub fn cursorCopy(self: *Screen, other: Cursor, opts: struct {
/// Always use this to write to cursor.page_pin.*.
///
/// This specifically handles the case when the new pin is on a different
-/// page than the old AND we have a style set. In that case, we must release
-/// our old style and upsert our new style since styles are stored per-page.
+/// page than the old AND we have a style or hyperlink set. In that case,
+/// we must release our old one and insert the new one, since styles are
+/// stored per-page.
fn cursorChangePin(self: *Screen, new: Pin) void {
// Moving the cursor affects text run splitting (ligatures) so
// we must mark the old and new page dirty. We do this as long
@@ -1576,6 +1590,18 @@ fn resizeInternal(
self.cursor.hyperlink = null;
}
+ // We need to insert a tracked pin for our saved cursor so we can
+ // modify its X/Y for reflow.
+ const saved_cursor_pin: ?*Pin = saved_cursor: {
+ const sc = self.saved_cursor orelse break :saved_cursor null;
+ const pin = self.pages.pin(.{ .active = .{
+ .x = sc.x,
+ .y = sc.y,
+ } }) orelse break :saved_cursor null;
+ break :saved_cursor try self.pages.trackPin(pin);
+ };
+ defer if (saved_cursor_pin) |p| self.pages.untrackPin(p);
+
// Perform the resize operation.
try self.pages.resize(.{
.rows = rows,
@@ -1595,6 +1621,36 @@ fn resizeInternal(
// state is correct.
self.cursorReload();
+ // If we reflowed a saved cursor, update it.
+ if (saved_cursor_pin) |p| {
+ // This should never fail because a non-null saved_cursor_pin
+ // implies a non-null saved_cursor.
+ const sc = &self.saved_cursor.?;
+ if (self.pages.pointFromPin(.active, p.*)) |pt| {
+ sc.x = @intCast(pt.active.x);
+ sc.y = @intCast(pt.active.y);
+
+ // If we had pending wrap set and we're no longer at the end of
+ // the line, we unset the pending wrap and move the cursor to
+ // reflect the correct next position.
+ if (sc.pending_wrap and sc.x != cols - 1) {
+ sc.pending_wrap = false;
+ sc.x += 1;
+ }
+ } else {
+ // I think this can happen if the screen is resized to be
+ // less rows or less cols and our saved cursor moves outside
+ // the active area. In this case, there isn't anything really
+ // reasonable we can do so we just move the cursor to the
+ // top-left. It may be reasonable to also move the cursor to
+ // match the primary cursor. Any behavior is fine since this is
+ // totally unspecified.
+ sc.x = 0;
+ sc.y = 0;
+ sc.pending_wrap = false;
+ }
+ }
+
// Fix up our hyperlink if we had one.
if (hyperlink_) |link| {
self.startHyperlink(link.uri, switch (link.id) {
@@ -1986,9 +2042,40 @@ pub fn cursorSetHyperlink(self: *Screen) !void {
} else |err| switch (err) {
// hyperlink_map is out of space, realloc the page to be larger
error.HyperlinkMapOutOfMemory => {
+ const uri_size = if (self.cursor.hyperlink) |link| link.uri.len else 0;
+
+ var string_bytes = page.capacity.string_bytes;
+
+ // Attempt to allocate the space that would be required to
+ // insert a new copy of the cursor hyperlink uri in to the
+ // string alloc, since right now adjustCapacity always just
+ // adds an extra copy even if one already exists in the page.
+ // If this alloc fails then we know we also need to grow our
+ // string bytes.
+ //
+ // FIXME: This SUCKS
+ if (page.string_alloc.alloc(
+ u8,
+ page.memory,
+ uri_size,
+ )) |slice| {
+ // We don't bother freeing because we're
+ // about to free the entire page anyway.
+ _ = &slice;
+ } else |_| {
+ // We didn't have enough room, let's just double our
+ // string bytes until there's definitely enough room
+ // for our uri.
+ const before = string_bytes;
+ while (string_bytes - before < uri_size) string_bytes *= 2;
+ }
+
_ = try self.adjustCapacity(
self.cursor.page_pin.node,
- .{ .hyperlink_bytes = page.capacity.hyperlink_bytes * 2 },
+ .{
+ .hyperlink_bytes = page.capacity.hyperlink_bytes * 2,
+ .string_bytes = string_bytes,
+ },
);
// Retry
@@ -2591,13 +2678,36 @@ pub fn selectOutput(self: *Screen, pin: Pin) ?Selection {
const start: Pin = boundary: {
var it = pin.rowIterator(.left_up, null);
var it_prev = pin;
+
+ // First, iterate until we find the first line of command output
while (it.next()) |p| {
+ it_prev = p;
const row = p.rowAndCell().row;
switch (row.semantic_prompt) {
- .command => break :boundary p,
- else => {},
+ .command => break,
+
+ .unknown,
+ .prompt,
+ .prompt_continuation,
+ .input,
+ => {},
}
+ }
+ // Because the first line of command output may span multiple visual rows we must now
+ // iterate until we find the first row of anything other than command output and then
+ // yield the previous row.
+ while (it.next()) |p| {
+ const row = p.rowAndCell().row;
+ switch (row.semantic_prompt) {
+ .command => {},
+
+ .unknown,
+ .prompt,
+ .prompt_continuation,
+ .input,
+ => break :boundary it_prev,
+ }
it_prev = p;
}
@@ -2859,6 +2969,9 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
.protected = self.cursor.protected,
};
+ // If we have a hyperlink, add it to the cell.
+ if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink();
+
// If we have a ref-counted style, increase.
if (self.cursor.style_id != style.default_id) {
const page = self.cursor.page_pin.node.data;
@@ -2877,6 +2990,9 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
.protected = self.cursor.protected,
};
+ // If we have a hyperlink, add it to the cell.
+ if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink();
+
self.cursor.page_row.wrap = true;
try self.cursorDownOrScroll();
self.cursorHorizontalAbsolute(0);
@@ -2892,6 +3008,9 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
.protected = self.cursor.protected,
};
+ // If we have a hyperlink, add it to the cell.
+ if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink();
+
// Write our tail
self.cursorRight(1);
self.cursor.page_cell.* = .{
@@ -2901,6 +3020,9 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
.protected = self.cursor.protected,
};
+ // If we have a hyperlink, add it to the cell.
+ if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink();
+
// If we have a ref-counted style, increase twice.
if (self.cursor.style_id != style.default_id) {
const page = self.cursor.page_pin.node.data;
@@ -7641,17 +7763,17 @@ test "Screen: selectOutput" {
// zig fmt: off
{
- // line number:
- try s.testWriteString("output1\n"); // 0
- try s.testWriteString("output1\n"); // 1
- try s.testWriteString("prompt2\n"); // 2
- try s.testWriteString("input2\n"); // 3
- try s.testWriteString("output2\n"); // 4
- try s.testWriteString("output2\n"); // 5
- try s.testWriteString("prompt3$ input3\n"); // 6
- try s.testWriteString("output3\n"); // 7
- try s.testWriteString("output3\n"); // 8
- try s.testWriteString("output3"); // 9
+ // line number:
+ try s.testWriteString("output1\n"); // 0
+ try s.testWriteString("output1\n"); // 1
+ try s.testWriteString("prompt2\n"); // 2
+ try s.testWriteString("input2\n"); // 3
+ try s.testWriteString("output2output2output2output2\n"); // 4, 5, 6 due to overflow
+ try s.testWriteString("output2\n"); // 7
+ try s.testWriteString("prompt3$ input3\n"); // 8
+ try s.testWriteString("output3\n"); // 9
+ try s.testWriteString("output3\n"); // 10
+ try s.testWriteString("output3"); // 11
}
// zig fmt: on
@@ -7670,13 +7792,23 @@ test "Screen: selectOutput" {
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
+ {
+ const pin = s.pages.pin(.{ .screen = .{ .y = 5 } }).?;
+ const row = pin.rowAndCell().row;
+ row.semantic_prompt = .command;
+ }
{
const pin = s.pages.pin(.{ .screen = .{ .y = 6 } }).?;
const row = pin.rowAndCell().row;
+ row.semantic_prompt = .command;
+ }
+ {
+ const pin = s.pages.pin(.{ .screen = .{ .y = 8 } }).?;
+ const row = pin.rowAndCell().row;
row.semantic_prompt = .input;
}
{
- const pin = s.pages.pin(.{ .screen = .{ .y = 7 } }).?;
+ const pin = s.pages.pin(.{ .screen = .{ .y = 9 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
@@ -7701,7 +7833,7 @@ test "Screen: selectOutput" {
{
var sel = s.selectOutput(s.pages.pin(.{ .active = .{
.x = 3,
- .y = 5,
+ .y = 7,
} }).?).?;
defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .active = .{
@@ -7710,23 +7842,23 @@ test "Screen: selectOutput" {
} }, s.pages.pointFromPin(.active, sel.start()).?);
try testing.expectEqual(point.Point{ .active = .{
.x = 9,
- .y = 5,
+ .y = 7,
} }, s.pages.pointFromPin(.active, sel.end()).?);
}
// No end marker, should select till the end
{
var sel = s.selectOutput(s.pages.pin(.{ .active = .{
.x = 2,
- .y = 7,
+ .y = 10,
} }).?).?;
defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .active = .{
.x = 0,
- .y = 7,
+ .y = 9,
} }, s.pages.pointFromPin(.active, sel.start()).?);
try testing.expectEqual(point.Point{ .active = .{
.x = 9,
- .y = 10,
+ .y = 12,
} }, s.pages.pointFromPin(.active, sel.end()).?);
}
// input / prompt at y = 0, pt.y = 0
@@ -8692,6 +8824,40 @@ test "Screen: hyperlink cursor state on resize" {
}
}
+test "Screen: cursorSetHyperlink OOM + URI too large for string alloc" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var s = try init(alloc, 80, 24, 0);
+ defer s.deinit();
+
+ // Start a hyperlink with a URI that just barely fits in the string alloc.
+ // This will ensure that additional string alloc space is needed for the
+ // redundant copy of the URI when the page is re-alloced.
+ const uri = "a" ** (pagepkg.std_capacity.string_bytes - 8);
+ try s.startHyperlink(uri, null);
+
+ // Figure out how many cells should can have hyperlinks in this page,
+ // and write twice that number, to guarantee the capacity needs to be
+ // increased at some point.
+ const base_capacity = s.cursor.page_pin.node.data.hyperlinkCapacity();
+ const base_string_bytes = s.cursor.page_pin.node.data.capacity.string_bytes;
+ for (0..base_capacity * 2) |_| {
+ try s.cursorSetHyperlink();
+ if (s.cursor.x >= s.pages.cols - 1) {
+ try s.cursorDownOrScroll();
+ s.cursorHorizontalAbsolute(0);
+ } else {
+ s.cursorRight(1);
+ }
+ }
+
+ // Make sure the capacity really did increase.
+ try testing.expect(base_capacity < s.cursor.page_pin.node.data.hyperlinkCapacity());
+ // And that our string_bytes increased as well.
+ try testing.expect(base_string_bytes < s.cursor.page_pin.node.data.capacity.string_bytes);
+}
+
test "Screen: adjustCapacity cursor style ref count" {
const testing = std.testing;
const alloc = testing.allocator;
@@ -8726,6 +8892,102 @@ test "Screen: adjustCapacity cursor style ref count" {
}
}
+test "Screen: adjustCapacity cursor hyperlink exceeds string alloc size" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var s = try init(alloc, 80, 24, 0);
+ defer s.deinit();
+
+ // Start a hyperlink with a URI that just barely fits in the string alloc.
+ // This will ensure that the redundant copy added in `adjustCapacity` won't
+ // fit in the available string alloc space.
+ const uri = "a" ** (pagepkg.std_capacity.string_bytes - 8);
+ try s.startHyperlink(uri, null);
+
+ // Write some characters with this so that the URI
+ // is copied to the new page when adjusting capacity.
+ try s.testWriteString("Hello");
+
+ // Adjust the capacity, right now this will cause a redundant copy of
+ // the URI to be added to the string alloc, but since there isn't room
+ // for this this will clear the cursor hyperlink.
+ _ = try s.adjustCapacity(s.cursor.page_pin.node, .{});
+
+ // The cursor hyperlink should have been cleared by the `adjustCapacity`
+ // call, because there isn't enough room to add the redundant URI string.
+ //
+ // This behavior will change, causing this test to fail, if any of these
+ // changes are made:
+ //
+ // - The string alloc is changed to intern strings.
+ //
+ // - The adjustCapacity function is changed to ensure the new
+ // capacity will fit the redundant copy of the hyperlink uri.
+ //
+ // - The cursor managed memory handling is reworked so that it
+ // doesn't reside in the pages anymore and doesn't need this
+ // accounting.
+ //
+ // In such a case, adjust this test accordingly.
+ try testing.expectEqual(null, s.cursor.hyperlink);
+ try testing.expectEqual(0, s.cursor.hyperlink_id);
+}
+
+test "Screen: adjustCapacity cursor style exceeds style set capacity" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var s = try init(alloc, 80, 24, 1000);
+ defer s.deinit();
+
+ const page = &s.cursor.page_pin.node.data;
+
+ // We add unique styles to the page until no more will fit.
+ fill: for (0..255) |bg| {
+ for (0..255) |fg| {
+ const st: style.Style = .{
+ .bg_color = .{ .palette = @intCast(bg) },
+ .fg_color = .{ .palette = @intCast(fg) },
+ };
+
+ s.cursor.style = st;
+
+ // Try to insert the new style, if it doesn't fit then
+ // we succeeded in filling the style set, so we break.
+ s.cursor.style_id = page.styles.add(
+ page.memory,
+ s.cursor.style,
+ ) catch break :fill;
+
+ try s.testWriteString("a");
+ }
+ }
+
+ // Adjust the capacity, this should cause the style set to reach the
+ // same state it was in to begin with, since it will clone the page
+ // in the same order as the styles were added to begin with, meaning
+ // the cursor style will not be able to be added to the set, which
+ // should, right now, result in the cursor style being cleared.
+ _ = try s.adjustCapacity(s.cursor.page_pin.node, .{});
+
+ // The cursor style should have been cleared by the `adjustCapacity`.
+ //
+ // This behavior will change, causing this test to fail, if either
+ // of these changes are made:
+ //
+ // - The adjustCapacity function is changed to ensure the
+ // new capacity will definitely fit the cursor style.
+ //
+ // - The cursor managed memory handling is reworked so that it
+ // doesn't reside in the pages anymore and doesn't need this
+ // accounting.
+ //
+ // In such a case, adjust this test accordingly.
+ try testing.expect(s.cursor.style.default());
+ try testing.expectEqual(style.default_id, s.cursor.style_id);
+}
+
test "Screen UTF8 cell map with newlines" {
const testing = std.testing;
const alloc = testing.allocator;
diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig
index 65476108d7..bec0a24a23 100644
--- a/src/terminal/Terminal.zig
+++ b/src/terminal/Terminal.zig
@@ -10708,6 +10708,87 @@ test "Terminal: resize with high unique style per cell with wrapping" {
try t.resize(alloc, 60, 30);
}
+test "Terminal: resize with reflow and saved cursor" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, .{ .cols = 2, .rows = 3 });
+ defer t.deinit(alloc);
+ try t.printString("1A2B");
+ t.setCursorPos(2, 2);
+ {
+ const list_cell = t.screen.pages.getCell(.{ .active = .{
+ .x = t.screen.cursor.x,
+ .y = t.screen.cursor.y,
+ } }).?;
+ const cell = list_cell.cell;
+ try testing.expectEqual(@as(u32, 'B'), cell.content.codepoint);
+ }
+
+ {
+ const str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("1A\n2B", str);
+ }
+
+ t.saveCursor();
+ try t.resize(alloc, 5, 3);
+ try t.restoreCursor();
+
+ {
+ const str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("1A2B", str);
+ }
+
+ // Verify our cursor is still in the same place
+ {
+ const list_cell = t.screen.pages.getCell(.{ .active = .{
+ .x = t.screen.cursor.x,
+ .y = t.screen.cursor.y,
+ } }).?;
+ const cell = list_cell.cell;
+ try testing.expectEqual(@as(u32, 'B'), cell.content.codepoint);
+ }
+}
+
+test "Terminal: resize with reflow and saved cursor pending wrap" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, .{ .cols = 2, .rows = 3 });
+ defer t.deinit(alloc);
+ try t.printString("1A2B");
+ {
+ const list_cell = t.screen.pages.getCell(.{ .active = .{
+ .x = t.screen.cursor.x,
+ .y = t.screen.cursor.y,
+ } }).?;
+ const cell = list_cell.cell;
+ try testing.expectEqual(@as(u32, 'B'), cell.content.codepoint);
+ }
+
+ {
+ const str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("1A\n2B", str);
+ }
+
+ t.saveCursor();
+ try t.resize(alloc, 5, 3);
+ try t.restoreCursor();
+
+ {
+ const str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("1A2B", str);
+ }
+
+ // Pending wrap should be reset
+ try t.print('X');
+ {
+ const str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("1A2BX", str);
+ }
+}
+
test "Terminal: DECCOLM without DEC mode 40" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
diff --git a/src/terminal/hyperlink.zig b/src/terminal/hyperlink.zig
index 1ab3c5ea74..bb9e78ca65 100644
--- a/src/terminal/hyperlink.zig
+++ b/src/terminal/hyperlink.zig
@@ -194,14 +194,24 @@ pub const Set = RefCountedSet(
Id,
size.CellCountInt,
struct {
+ /// The page which holds the strings for items in this set.
page: ?*Page = null,
+ /// The page which holds the strings for items
+ /// looked up with, e.g., `add` or `lookup`,
+ /// if different from the destination page.
+ src_page: ?*const Page = null,
+
pub fn hash(self: *const @This(), link: PageEntry) u64 {
- return link.hash(self.page.?.memory);
+ return link.hash((self.src_page orelse self.page.?).memory);
}
pub fn eql(self: *const @This(), a: PageEntry, b: PageEntry) bool {
- return a.eql(self.page.?.memory, &b, self.page.?.memory);
+ return a.eql(
+ (self.src_page orelse self.page.?).memory,
+ &b,
+ self.page.?.memory,
+ );
}
pub fn deleted(self: *const @This(), link: PageEntry) void {
diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig
index 33d753c9f3..faf376d13d 100644
--- a/src/terminal/osc.zig
+++ b/src/terminal/osc.zig
@@ -178,6 +178,9 @@ pub const Command = union(enum) {
progress: ?u8 = null,
},
+ /// Wait input (OSC 9;5)
+ wait_input: void,
+
pub const ColorKind = union(enum) {
palette: u8,
foreground,
@@ -269,6 +272,9 @@ pub const Parser = struct {
// Maximum length of a single OSC command. This is the full OSC command
// sequence length (excluding ESC ]). This is arbitrary, I couldn't find
// any definitive resource on how long this should be.
+ //
+ // NOTE: This does mean certain OSC sequences such as OSC 8 (hyperlinks)
+ // won't work if their parameters are larger than fit in the buffer.
const MAX_BUF = 2048;
pub const State = enum {
@@ -422,9 +428,23 @@ pub const Parser = struct {
/// Consume the next character c and advance the parser state.
pub fn next(self: *Parser, c: u8) void {
- // If our buffer is full then we're invalid.
+ // If our buffer is full then we're invalid, so we set our state
+ // accordingly and indicate the sequence is incomplete so that we
+ // don't accidentally issue a command when ending.
if (self.buf_idx >= self.buf.len) {
+ if (self.state != .invalid) {
+ log.warn(
+ "OSC sequence too long (> {d}), ignoring. state={}",
+ .{ self.buf.len, self.state },
+ );
+ }
+
self.state = .invalid;
+
+ // We have to do this here because it will never reach the
+ // switch statement below, since our buf_idx will always be
+ // too high after this.
+ self.complete = false;
return;
}
@@ -811,6 +831,11 @@ pub const Parser = struct {
'4' => {
self.state = .conemu_progress_prestate;
},
+ '5' => {
+ self.state = .swallow;
+ self.command = .{ .wait_input = {} };
+ self.complete = true;
+ },
// Todo: parse out other ConEmu operating system commands.
// Even if we don't support them we probably don't want
@@ -1635,10 +1660,11 @@ test "OSC: longer than buffer" {
var p: Parser = .{};
- const input = "a" ** (Parser.MAX_BUF + 2);
+ const input = "0;" ++ "a" ** (Parser.MAX_BUF + 2);
for (input) |ch| p.next(ch);
try testing.expect(p.end(null) == null);
+ try testing.expect(p.complete == false);
}
test "OSC: report default foreground color" {
@@ -2096,6 +2122,30 @@ test "OSC: OSC9 progress pause with progress" {
try testing.expect(cmd.progress.progress == 100);
}
+test "OSC: OSC9 conemu wait input" {
+ const testing = std.testing;
+
+ var p: Parser = .{};
+
+ const input = "9;5";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?;
+ try testing.expect(cmd == .wait_input);
+}
+
+test "OSC: OSC9 conemu wait ignores trailing characters" {
+ const testing = std.testing;
+
+ var p: Parser = .{};
+
+ const input = "9;5;foo";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?;
+ try testing.expect(cmd == .wait_input);
+}
+
test "OSC: empty param" {
const testing = std.testing;
diff --git a/src/terminal/page.zig b/src/terminal/page.zig
index ae14b8c016..30f6658aa5 100644
--- a/src/terminal/page.zig
+++ b/src/terminal/page.zig
@@ -821,11 +821,7 @@ pub const Page = struct {
if (self.hyperlink_set.lookupContext(
self.memory,
other_link.*,
-
- // `lookupContext` uses the context for hashing, and
- // that doesn't write to the page, so this constCast
- // is completely safe.
- .{ .page = @constCast(other) },
+ .{ .page = self, .src_page = @constCast(other) },
)) |i| {
self.hyperlink_set.use(self.memory, i);
break :dst_id i;
diff --git a/src/terminal/ref_counted_set.zig b/src/terminal/ref_counted_set.zig
index 1a58a4e5b5..b674295dcb 100644
--- a/src/terminal/ref_counted_set.zig
+++ b/src/terminal/ref_counted_set.zig
@@ -38,8 +38,14 @@ const fastmem = @import("../fastmem.zig");
///
/// `Context`
/// A type containing methods to define behaviors.
+///
/// - `fn hash(*Context, T) u64` - Return a hash for an item.
+///
/// - `fn eql(*Context, T, T) bool` - Check two items for equality.
+/// The first of the two items passed in is guaranteed to be from
+/// a value passed in to an `add` or `lookup` function, the second
+/// is guaranteed to be a value already resident in the set.
+///
/// - `fn deleted(*Context, T) void` - [OPTIONAL] Deletion callback.
/// If present, called whenever an item is finally deleted.
/// Useful if the item has memory that needs to be freed.
diff --git a/src/terminal/sgr.zig b/src/terminal/sgr.zig
index cdf39657bd..52bfb2c31a 100644
--- a/src/terminal/sgr.zig
+++ b/src/terminal/sgr.zig
@@ -1,13 +1,17 @@
//! SGR (Select Graphic Rendition) attrinvbute parsing and types.
const std = @import("std");
+const assert = std.debug.assert;
const testing = std.testing;
const color = @import("color.zig");
+const SepList = @import("Parser.zig").Action.CSI.SepList;
/// Attribute type for SGR
pub const Attribute = union(enum) {
+ pub const Tag = std.meta.FieldEnum(Attribute);
+
/// Unset all attributes
- unset: void,
+ unset,
/// Unknown attribute, the raw CSI command parameters are here.
unknown: struct {
@@ -19,43 +23,43 @@ pub const Attribute = union(enum) {
},
/// Bold the text.
- bold: void,
- reset_bold: void,
+ bold,
+ reset_bold,
/// Italic text.
- italic: void,
- reset_italic: void,
+ italic,
+ reset_italic,
/// Faint/dim text.
/// Note: reset faint is the same SGR code as reset bold
- faint: void,
+ faint,
/// Underline the text
underline: Underline,
- reset_underline: void,
+ reset_underline,
underline_color: color.RGB,
@"256_underline_color": u8,
- reset_underline_color: void,
+ reset_underline_color,
// Overline the text
- overline: void,
- reset_overline: void,
+ overline,
+ reset_overline,
/// Blink the text
- blink: void,
- reset_blink: void,
+ blink,
+ reset_blink,
/// Invert fg/bg colors.
- inverse: void,
- reset_inverse: void,
+ inverse,
+ reset_inverse,
/// Invisible
- invisible: void,
- reset_invisible: void,
+ invisible,
+ reset_invisible,
/// Strikethrough the text.
- strikethrough: void,
- reset_strikethrough: void,
+ strikethrough,
+ reset_strikethrough,
/// Set foreground color as RGB values.
direct_color_fg: color.RGB,
@@ -68,8 +72,8 @@ pub const Attribute = union(enum) {
@"8_fg": color.Name,
/// Reset the fg/bg to their default values.
- reset_fg: void,
- reset_bg: void,
+ reset_fg,
+ reset_bg,
/// Set the background/foreground as a named bright color attribute.
@"8_bright_bg": color.Name,
@@ -94,11 +98,9 @@ pub const Attribute = union(enum) {
/// Parser parses the attributes from a list of SGR parameters.
pub const Parser = struct {
params: []const u16,
+ params_sep: SepList = SepList.initEmpty(),
idx: usize = 0,
- /// True if the separator is a colon
- colon: bool = false,
-
/// Next returns the next attribute or null if there are no more attributes.
pub fn next(self: *Parser) ?Attribute {
if (self.idx > self.params.len) return null;
@@ -106,220 +108,261 @@ pub const Parser = struct {
// Implicitly means unset
if (self.params.len == 0) {
self.idx += 1;
- return Attribute{ .unset = {} };
+ return .unset;
}
const slice = self.params[self.idx..self.params.len];
+ const colon = self.params_sep.isSet(self.idx);
self.idx += 1;
// Our last one will have an idx be the last value.
if (slice.len == 0) return null;
+ // If we have a colon separator then we need to ensure we're
+ // parsing a value that allows it.
+ if (colon) switch (slice[0]) {
+ 4, 38, 48, 58 => {},
+
+ else => {
+ // Consume all the colon separated values.
+ const start = self.idx;
+ while (self.params_sep.isSet(self.idx)) self.idx += 1;
+ self.idx += 1;
+ return .{ .unknown = .{
+ .full = self.params,
+ .partial = slice[0 .. self.idx - start + 1],
+ } };
+ },
+ };
+
switch (slice[0]) {
- 0 => return Attribute{ .unset = {} },
-
- 1 => return Attribute{ .bold = {} },
-
- 2 => return Attribute{ .faint = {} },
-
- 3 => return Attribute{ .italic = {} },
-
- 4 => blk: {
- if (self.colon) {
- switch (slice.len) {
- // 0 is unreachable because we're here and we read
- // an element to get here.
- 0 => unreachable,
-
- // 1 is possible if underline is the last element.
- 1 => return Attribute{ .underline = .single },
-
- // 2 means we have a specific underline style.
- 2 => {
- self.idx += 1;
- switch (slice[1]) {
- 0 => return Attribute{ .reset_underline = {} },
- 1 => return Attribute{ .underline = .single },
- 2 => return Attribute{ .underline = .double },
- 3 => return Attribute{ .underline = .curly },
- 4 => return Attribute{ .underline = .dotted },
- 5 => return Attribute{ .underline = .dashed },
-
- // For unknown underline styles, just render
- // a single underline.
- else => return Attribute{ .underline = .single },
- }
- },
-
- // Colon-separated must only be 2.
- else => break :blk,
+ 0 => return .unset,
+
+ 1 => return .bold,
+
+ 2 => return .faint,
+
+ 3 => return .italic,
+
+ 4 => underline: {
+ if (colon) {
+ assert(slice.len >= 2);
+ if (self.isColon()) {
+ self.consumeUnknownColon();
+ break :underline;
+ }
+
+ self.idx += 1;
+ switch (slice[1]) {
+ 0 => return .reset_underline,
+ 1 => return .{ .underline = .single },
+ 2 => return .{ .underline = .double },
+ 3 => return .{ .underline = .curly },
+ 4 => return .{ .underline = .dotted },
+ 5 => return .{ .underline = .dashed },
+
+ // For unknown underline styles, just render
+ // a single underline.
+ else => return .{ .underline = .single },
}
}
- return Attribute{ .underline = .single };
+ return .{ .underline = .single };
},
- 5 => return Attribute{ .blink = {} },
+ 5 => return .blink,
- 6 => return Attribute{ .blink = {} },
+ 6 => return .blink,
- 7 => return Attribute{ .inverse = {} },
+ 7 => return .inverse,
- 8 => return Attribute{ .invisible = {} },
+ 8 => return .invisible,
- 9 => return Attribute{ .strikethrough = {} },
+ 9 => return .strikethrough,
- 21 => return Attribute{ .underline = .double },
+ 21 => return .{ .underline = .double },
- 22 => return Attribute{ .reset_bold = {} },
+ 22 => return .reset_bold,
- 23 => return Attribute{ .reset_italic = {} },
+ 23 => return .reset_italic,
- 24 => return Attribute{ .reset_underline = {} },
+ 24 => return .reset_underline,
- 25 => return Attribute{ .reset_blink = {} },
+ 25 => return .reset_blink,
- 27 => return Attribute{ .reset_inverse = {} },
+ 27 => return .reset_inverse,
- 28 => return Attribute{ .reset_invisible = {} },
+ 28 => return .reset_invisible,
- 29 => return Attribute{ .reset_strikethrough = {} },
+ 29 => return .reset_strikethrough,
- 30...37 => return Attribute{
+ 30...37 => return .{
.@"8_fg" = @enumFromInt(slice[0] - 30),
},
38 => if (slice.len >= 2) switch (slice[1]) {
// `2` indicates direct-color (r, g, b).
// We need at least 3 more params for this to make sense.
- 2 => if (slice.len >= 5) {
- self.idx += 4;
- // When a colon separator is used, there may or may not be
- // a color space identifier as the third param, which we
- // need to ignore (it has no standardized behavior).
- const rgb = if (slice.len == 5 or !self.colon)
- slice[2..5]
- else rgb: {
- self.idx += 1;
- break :rgb slice[3..6];
- };
+ 2 => if (self.parseDirectColor(
+ .direct_color_fg,
+ slice,
+ colon,
+ )) |v| return v,
- // We use @truncate because the value should be 0 to 255. If
- // it isn't, the behavior is undefined so we just... truncate it.
- return Attribute{
- .direct_color_fg = .{
- .r = @truncate(rgb[0]),
- .g = @truncate(rgb[1]),
- .b = @truncate(rgb[2]),
- },
- };
- },
// `5` indicates indexed color.
5 => if (slice.len >= 3) {
self.idx += 2;
- return Attribute{
+ return .{
.@"256_fg" = @truncate(slice[2]),
};
},
else => {},
},
- 39 => return Attribute{ .reset_fg = {} },
+ 39 => return .reset_fg,
- 40...47 => return Attribute{
+ 40...47 => return .{
.@"8_bg" = @enumFromInt(slice[0] - 40),
},
48 => if (slice.len >= 2) switch (slice[1]) {
// `2` indicates direct-color (r, g, b).
// We need at least 3 more params for this to make sense.
- 2 => if (slice.len >= 5) {
- self.idx += 4;
- // When a colon separator is used, there may or may not be
- // a color space identifier as the third param, which we
- // need to ignore (it has no standardized behavior).
- const rgb = if (slice.len == 5 or !self.colon)
- slice[2..5]
- else rgb: {
- self.idx += 1;
- break :rgb slice[3..6];
- };
+ 2 => if (self.parseDirectColor(
+ .direct_color_bg,
+ slice,
+ colon,
+ )) |v| return v,
- // We use @truncate because the value should be 0 to 255. If
- // it isn't, the behavior is undefined so we just... truncate it.
- return Attribute{
- .direct_color_bg = .{
- .r = @truncate(rgb[0]),
- .g = @truncate(rgb[1]),
- .b = @truncate(rgb[2]),
- },
- };
- },
// `5` indicates indexed color.
5 => if (slice.len >= 3) {
self.idx += 2;
- return Attribute{
+ return .{
.@"256_bg" = @truncate(slice[2]),
};
},
else => {},
},
- 49 => return Attribute{ .reset_bg = {} },
+ 49 => return .reset_bg,
- 53 => return Attribute{ .overline = {} },
- 55 => return Attribute{ .reset_overline = {} },
+ 53 => return .overline,
+ 55 => return .reset_overline,
58 => if (slice.len >= 2) switch (slice[1]) {
// `2` indicates direct-color (r, g, b).
// We need at least 3 more params for this to make sense.
- 2 => if (slice.len >= 5) {
- self.idx += 4;
- // When a colon separator is used, there may or may not be
- // a color space identifier as the third param, which we
- // need to ignore (it has no standardized behavior).
- const rgb = if (slice.len == 5 or !self.colon)
- slice[2..5]
- else rgb: {
- self.idx += 1;
- break :rgb slice[3..6];
- };
+ 2 => if (self.parseDirectColor(
+ .underline_color,
+ slice,
+ colon,
+ )) |v| return v,
- // We use @truncate because the value should be 0 to 255. If
- // it isn't, the behavior is undefined so we just... truncate it.
- return Attribute{
- .underline_color = .{
- .r = @truncate(rgb[0]),
- .g = @truncate(rgb[1]),
- .b = @truncate(rgb[2]),
- },
- };
- },
// `5` indicates indexed color.
5 => if (slice.len >= 3) {
self.idx += 2;
- return Attribute{
+ return .{
.@"256_underline_color" = @truncate(slice[2]),
};
},
else => {},
},
- 59 => return Attribute{ .reset_underline_color = {} },
+ 59 => return .reset_underline_color,
- 90...97 => return Attribute{
+ 90...97 => return .{
// 82 instead of 90 to offset to "bright" colors
.@"8_bright_fg" = @enumFromInt(slice[0] - 82),
},
- 100...107 => return Attribute{
+ 100...107 => return .{
.@"8_bright_bg" = @enumFromInt(slice[0] - 92),
},
else => {},
}
- return Attribute{ .unknown = .{ .full = self.params, .partial = slice } };
+ return .{ .unknown = .{ .full = self.params, .partial = slice } };
+ }
+
+ fn parseDirectColor(
+ self: *Parser,
+ comptime tag: Attribute.Tag,
+ slice: []const u16,
+ colon: bool,
+ ) ?Attribute {
+ // Any direct color style must have at least 5 values.
+ if (slice.len < 5) return null;
+
+ // Only used for direct color sets (38, 48, 58) and subparam 2.
+ assert(slice[1] == 2);
+
+ // Note: We use @truncate because the value should be 0 to 255. If
+ // it isn't, the behavior is undefined so we just... truncate it.
+
+ // If we don't have a colon, then we expect exactly 3 semicolon
+ // separated values.
+ if (!colon) {
+ self.idx += 4;
+ return @unionInit(Attribute, @tagName(tag), .{
+ .r = @truncate(slice[2]),
+ .g = @truncate(slice[3]),
+ .b = @truncate(slice[4]),
+ });
+ }
+
+ // We have a colon, we might have either 5 or 6 values depending
+ // on if the colorspace is present.
+ const count = self.countColon();
+ switch (count) {
+ 3 => {
+ self.idx += 4;
+ return @unionInit(Attribute, @tagName(tag), .{
+ .r = @truncate(slice[2]),
+ .g = @truncate(slice[3]),
+ .b = @truncate(slice[4]),
+ });
+ },
+
+ 4 => {
+ self.idx += 5;
+ return @unionInit(Attribute, @tagName(tag), .{
+ .r = @truncate(slice[3]),
+ .g = @truncate(slice[4]),
+ .b = @truncate(slice[5]),
+ });
+ },
+
+ else => {
+ self.consumeUnknownColon();
+ return null;
+ },
+ }
+ }
+
+ /// Returns true if the present position has a colon separator.
+ /// This always returns false for the last value since it has no
+ /// separator.
+ fn isColon(self: *Parser) bool {
+ // The `- 1` here is because the last value has no separator.
+ if (self.idx >= self.params.len - 1) return false;
+ return self.params_sep.isSet(self.idx);
+ }
+
+ fn countColon(self: *Parser) usize {
+ var count: usize = 0;
+ var idx = self.idx;
+ while (idx < self.params.len - 1 and self.params_sep.isSet(idx)) : (idx += 1) {
+ count += 1;
+ }
+ return count;
+ }
+
+ /// Consumes all the remaining parameters separated by a colon and
+ /// returns an unknown attribute.
+ fn consumeUnknownColon(self: *Parser) void {
+ const count = self.countColon();
+ self.idx += count + 1;
}
};
@@ -329,7 +372,7 @@ fn testParse(params: []const u16) Attribute {
}
fn testParseColon(params: []const u16) Attribute {
- var p: Parser = .{ .params = params, .colon = true };
+ var p: Parser = .{ .params = params, .params_sep = SepList.initFull() };
return p.next().?;
}
@@ -366,6 +409,35 @@ test "sgr: Parser multiple" {
try testing.expect(p.next() == null);
}
+test "sgr: unsupported with colon" {
+ var p: Parser = .{
+ .params = &[_]u16{ 0, 4, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ list.set(0);
+ break :sep list;
+ },
+ };
+ try testing.expect(p.next().? == .unknown);
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
+test "sgr: unsupported with multiple colon" {
+ var p: Parser = .{
+ .params = &[_]u16{ 0, 4, 2, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ list.set(0);
+ list.set(1);
+ break :sep list;
+ },
+ };
+ try testing.expect(p.next().? == .unknown);
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
test "sgr: bold" {
{
const v = testParse(&[_]u16{1});
@@ -439,6 +511,37 @@ test "sgr: underline styles" {
}
}
+test "sgr: underline style with more" {
+ var p: Parser = .{
+ .params = &[_]u16{ 4, 2, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ list.set(0);
+ break :sep list;
+ },
+ };
+
+ try testing.expect(p.next().? == .underline);
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
+test "sgr: underline style with too many colons" {
+ var p: Parser = .{
+ .params = &[_]u16{ 4, 2, 3, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ list.set(0);
+ list.set(1);
+ break :sep list;
+ },
+ };
+
+ try testing.expect(p.next().? == .unknown);
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
test "sgr: blink" {
{
const v = testParse(&[_]u16{5});
@@ -592,13 +695,13 @@ test "sgr: underline, bg, and fg" {
test "sgr: direct color fg missing color" {
// This used to crash
- var p: Parser = .{ .params = &[_]u16{ 38, 5 }, .colon = false };
+ var p: Parser = .{ .params = &[_]u16{ 38, 5 } };
while (p.next()) |_| {}
}
test "sgr: direct color bg missing color" {
// This used to crash
- var p: Parser = .{ .params = &[_]u16{ 48, 5 }, .colon = false };
+ var p: Parser = .{ .params = &[_]u16{ 48, 5 } };
while (p.next()) |_| {}
}
@@ -608,7 +711,7 @@ test "sgr: direct fg/bg/underline ignore optional color space" {
// Colon version should skip the optional color space identifier
{
// 3 8 : 2 : Pi : Pr : Pg : Pb
- const v = testParseColon(&[_]u16{ 38, 2, 0, 1, 2, 3, 4 });
+ const v = testParseColon(&[_]u16{ 38, 2, 0, 1, 2, 3 });
try testing.expect(v == .direct_color_fg);
try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
@@ -616,7 +719,7 @@ test "sgr: direct fg/bg/underline ignore optional color space" {
}
{
// 4 8 : 2 : Pi : Pr : Pg : Pb
- const v = testParseColon(&[_]u16{ 48, 2, 0, 1, 2, 3, 4 });
+ const v = testParseColon(&[_]u16{ 48, 2, 0, 1, 2, 3 });
try testing.expect(v == .direct_color_bg);
try testing.expectEqual(@as(u8, 1), v.direct_color_bg.r);
try testing.expectEqual(@as(u8, 2), v.direct_color_bg.g);
@@ -624,7 +727,7 @@ test "sgr: direct fg/bg/underline ignore optional color space" {
}
{
// 5 8 : 2 : Pi : Pr : Pg : Pb
- const v = testParseColon(&[_]u16{ 58, 2, 0, 1, 2, 3, 4 });
+ const v = testParseColon(&[_]u16{ 58, 2, 0, 1, 2, 3 });
try testing.expect(v == .underline_color);
try testing.expectEqual(@as(u8, 1), v.underline_color.r);
try testing.expectEqual(@as(u8, 2), v.underline_color.g);
@@ -634,7 +737,7 @@ test "sgr: direct fg/bg/underline ignore optional color space" {
// Semicolon version should not parse optional color space identifier
{
// 3 8 ; 2 ; Pr ; Pg ; Pb
- const v = testParse(&[_]u16{ 38, 2, 0, 1, 2, 3, 4 });
+ const v = testParse(&[_]u16{ 38, 2, 0, 1, 2, 3 });
try testing.expect(v == .direct_color_fg);
try testing.expectEqual(@as(u8, 0), v.direct_color_fg.r);
try testing.expectEqual(@as(u8, 1), v.direct_color_fg.g);
@@ -642,7 +745,7 @@ test "sgr: direct fg/bg/underline ignore optional color space" {
}
{
// 4 8 ; 2 ; Pr ; Pg ; Pb
- const v = testParse(&[_]u16{ 48, 2, 0, 1, 2, 3, 4 });
+ const v = testParse(&[_]u16{ 48, 2, 0, 1, 2, 3 });
try testing.expect(v == .direct_color_bg);
try testing.expectEqual(@as(u8, 0), v.direct_color_bg.r);
try testing.expectEqual(@as(u8, 1), v.direct_color_bg.g);
@@ -650,10 +753,114 @@ test "sgr: direct fg/bg/underline ignore optional color space" {
}
{
// 5 8 ; 2 ; Pr ; Pg ; Pb
- const v = testParse(&[_]u16{ 58, 2, 0, 1, 2, 3, 4 });
+ const v = testParse(&[_]u16{ 58, 2, 0, 1, 2, 3 });
try testing.expect(v == .underline_color);
try testing.expectEqual(@as(u8, 0), v.underline_color.r);
try testing.expectEqual(@as(u8, 1), v.underline_color.g);
try testing.expectEqual(@as(u8, 2), v.underline_color.b);
}
}
+
+test "sgr: direct fg colon with too many colons" {
+ var p: Parser = .{
+ .params = &[_]u16{ 38, 2, 0, 1, 2, 3, 4, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ for (0..6) |idx| list.set(idx);
+ break :sep list;
+ },
+ };
+
+ try testing.expect(p.next().? == .unknown);
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
+test "sgr: direct fg colon with colorspace and extra param" {
+ var p: Parser = .{
+ .params = &[_]u16{ 38, 2, 0, 1, 2, 3, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ for (0..5) |idx| list.set(idx);
+ break :sep list;
+ },
+ };
+
+ {
+ const v = p.next().?;
+ std.log.warn("WHAT={}", .{v});
+ try testing.expect(v == .direct_color_fg);
+ try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
+ try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
+ try testing.expectEqual(@as(u8, 3), v.direct_color_fg.b);
+ }
+
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
+test "sgr: direct fg colon no colorspace and extra param" {
+ var p: Parser = .{
+ .params = &[_]u16{ 38, 2, 1, 2, 3, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ for (0..4) |idx| list.set(idx);
+ break :sep list;
+ },
+ };
+
+ {
+ const v = p.next().?;
+ try testing.expect(v == .direct_color_fg);
+ try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
+ try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
+ try testing.expectEqual(@as(u8, 3), v.direct_color_fg.b);
+ }
+
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
+// Kakoune sent this complex SGR sequence that caused invalid behavior.
+test "sgr: kakoune input" {
+ // This used to crash
+ var p: Parser = .{
+ .params = &[_]u16{ 0, 4, 3, 38, 2, 175, 175, 215, 58, 2, 0, 190, 80, 70 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ list.set(1);
+ list.set(8);
+ list.set(9);
+ list.set(10);
+ list.set(11);
+ list.set(12);
+ break :sep list;
+ },
+ };
+
+ {
+ const v = p.next().?;
+ try testing.expect(v == .unset);
+ }
+ {
+ const v = p.next().?;
+ try testing.expect(v == .underline);
+ try testing.expectEqual(Attribute.Underline.curly, v.underline);
+ }
+ {
+ const v = p.next().?;
+ try testing.expect(v == .direct_color_fg);
+ try testing.expectEqual(@as(u8, 175), v.direct_color_fg.r);
+ try testing.expectEqual(@as(u8, 175), v.direct_color_fg.g);
+ try testing.expectEqual(@as(u8, 215), v.direct_color_fg.b);
+ }
+ {
+ const v = p.next().?;
+ try testing.expect(v == .underline_color);
+ try testing.expectEqual(@as(u8, 190), v.underline_color.r);
+ try testing.expectEqual(@as(u8, 80), v.underline_color.g);
+ try testing.expectEqual(@as(u8, 70), v.underline_color.b);
+ }
+
+ //try testing.expect(p.next() == null);
+}
diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig
index f75d86c0a4..eb5ab2c656 100644
--- a/src/terminal/stream.zig
+++ b/src/terminal/stream.zig
@@ -253,15 +253,11 @@ pub fn Stream(comptime Handler: type) type {
// A parameter separator:
':', ';' => if (self.parser.params_idx < 16) {
self.parser.params[self.parser.params_idx] = self.parser.param_acc;
+ if (c == ':') self.parser.params_sep.set(self.parser.params_idx);
self.parser.params_idx += 1;
self.parser.param_acc = 0;
self.parser.param_acc_idx = 0;
-
- // Keep track of separator state.
- const sep: Parser.ParamSepState = @enumFromInt(c);
- if (self.parser.params_idx == 1) self.parser.params_sep = sep;
- if (self.parser.params_sep != sep) self.parser.params_sep = .mixed;
},
// Explicitly ignored:
0x7F => {},
@@ -937,7 +933,10 @@ pub fn Stream(comptime Handler: type) type {
'm' => switch (input.intermediates.len) {
0 => if (@hasDecl(T, "setAttribute")) {
// log.info("parse SGR params={any}", .{action.params});
- var p: sgr.Parser = .{ .params = input.params, .colon = input.sep == .colon };
+ var p: sgr.Parser = .{
+ .params = input.params,
+ .params_sep = input.params_sep,
+ };
while (p.next()) |attr| {
// log.info("SGR attribute: {}", .{attr});
try self.handler.setAttribute(attr);
@@ -1605,7 +1604,7 @@ pub fn Stream(comptime Handler: type) type {
} else log.warn("unimplemented OSC callback: {}", .{cmd});
},
- .progress, .sleep, .show_message_box, .change_conemu_tab_title => {
+ .progress, .sleep, .show_message_box, .change_conemu_tab_title, .wait_input => {
log.warn("unimplemented OSC callback: {}", .{cmd});
},
}
diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig
index 1a3b8cad00..5a2d2a5070 100644
--- a/src/termio/Exec.zig
+++ b/src/termio/Exec.zig
@@ -179,8 +179,17 @@ pub fn threadExit(self: *Exec, td: *termio.Termio.ThreadData) void {
// Quit our read thread after exiting the subprocess so that
// we don't get stuck waiting for data to stop flowing if it is
// a particularly noisy process.
- _ = posix.write(exec.read_thread_pipe, "x") catch |err|
- log.warn("error writing to read thread quit pipe err={}", .{err});
+ _ = posix.write(exec.read_thread_pipe, "x") catch |err| switch (err) {
+ // BrokenPipe means that our read thread is closed already,
+ // which is completely fine since that is what we were trying
+ // to achieve.
+ error.BrokenPipe => {},
+
+ else => log.warn(
+ "error writing to read thread quit pipe err={}",
+ .{err},
+ ),
+ };
if (comptime builtin.os.tag == .windows) {
// Interrupt the blocking read so the thread can see the quit message
@@ -673,6 +682,8 @@ pub const ThreadData = struct {
pub const Config = struct {
command: ?[]const u8 = null,
+ env: EnvMap,
+ env_override: configpkg.RepeatableStringMap = .{},
shell_integration: configpkg.Config.ShellIntegration = .detect,
shell_integration_features: configpkg.Config.ShellIntegrationFeatures = .{},
working_directory: ?[]const u8 = null,
@@ -694,7 +705,7 @@ const Subprocess = struct {
arena: std.heap.ArenaAllocator,
cwd: ?[]const u8,
- env: EnvMap,
+ env: ?EnvMap,
args: [][]const u8,
grid_size: renderer.GridSize,
screen_size: renderer.ScreenSize,
@@ -712,19 +723,9 @@ const Subprocess = struct {
errdefer arena.deinit();
const alloc = arena.allocator();
- // Set our env vars. For Flatpak builds running in Flatpak we don't
- // inherit our environment because the login shell on the host side
- // will get it.
- var env = env: {
- if (comptime build_config.flatpak) {
- if (internal_os.isFlatpak()) {
- break :env std.process.EnvMap.init(alloc);
- }
- }
-
- break :env try std.process.getEnvMap(alloc);
- };
- errdefer env.deinit();
+ // Get our env. If a default env isn't provided by the caller
+ // then we get it ourselves.
+ var env = cfg.env;
// If we have a resources dir then set our env var
if (cfg.resources_dir) |dir| {
@@ -838,35 +839,11 @@ const Subprocess = struct {
try env.put("TERM_PROGRAM", "ghostty");
try env.put("TERM_PROGRAM_VERSION", build_config.version_string);
- // When embedding in macOS and running via XCode, XCode injects
- // a bunch of things that break our shell process. We remove those.
- if (comptime builtin.target.isDarwin() and build_config.artifact == .lib) {
- if (env.get("__XCODE_BUILT_PRODUCTS_DIR_PATHS") != null) {
- env.remove("__XCODE_BUILT_PRODUCTS_DIR_PATHS");
- env.remove("__XPC_DYLD_LIBRARY_PATH");
- env.remove("DYLD_FRAMEWORK_PATH");
- env.remove("DYLD_INSERT_LIBRARIES");
- env.remove("DYLD_LIBRARY_PATH");
- env.remove("LD_LIBRARY_PATH");
- env.remove("SECURITYSESSIONID");
- env.remove("XPC_SERVICE_NAME");
- }
-
- // Remove this so that running `ghostty` within Ghostty works.
- env.remove("GHOSTTY_MAC_APP");
- }
-
// VTE_VERSION is set by gnome-terminal and other VTE-based terminals.
// We don't want our child processes to think we're running under VTE.
+ // This is not apprt-specific, so we do it here.
env.remove("VTE_VERSION");
- // Don't leak these GTK environment variables to child processes.
- if (comptime build_config.app_runtime == .gtk) {
- env.remove("GDK_DEBUG");
- env.remove("GDK_DISABLE");
- env.remove("GSK_RENDERER");
- }
-
// Setup our shell integration, if we can.
const integrated_shell: ?shell_integration.Shell, const shell_command: []const u8 = shell: {
const default_shell_command = cfg.command orelse switch (builtin.os.tag) {
@@ -875,7 +852,11 @@ const Subprocess = struct {
};
const force: ?shell_integration.Shell = switch (cfg.shell_integration) {
- .none => break :shell .{ null, default_shell_command },
+ .none => {
+ // Even if shell integration is none, we still want to set up the feature env vars
+ try shell_integration.setupFeatures(&env, cfg.shell_integration_features);
+ break :shell .{ null, default_shell_command };
+ },
.detect => null,
.bash => .bash,
.elvish => .elvish,
@@ -909,6 +890,15 @@ const Subprocess = struct {
log.warn("shell could not be detected, no automatic shell integration will be injected", .{});
}
+ // Add the environment variables that override any others.
+ {
+ var it = cfg.env_override.iterator();
+ while (it.next()) |entry| try env.put(
+ entry.key_ptr.*,
+ entry.value_ptr.*,
+ );
+ }
+
// Build our args list
const args = args: {
const cap = 9; // the most we'll ever use
@@ -971,12 +961,12 @@ const Subprocess = struct {
// which we may not want. If we specify "-l" then we can avoid
// this behavior but now the shell isn't a login shell.
//
- // There is another issue: `login(1)` only checks for ".hushlogin"
- // in the working directory. This means that if we specify "-l"
- // then we won't get hushlogin honored if its in the home
- // directory (which is standard). To get around this, we
- // check for hushlogin ourselves and if present specify the
- // "-q" flag to login(1).
+ // There is another issue: `login(1)` on macOS 14.3 and earlier
+ // checked for ".hushlogin" in the working directory. This means
+ // that if we specify "-l" then we won't get hushlogin honored
+ // if its in the home directory (which is standard). To get
+ // around this, we check for hushlogin ourselves and if present
+ // specify the "-q" flag to login(1).
//
// So to get all the behaviors we want, we specify "-l" but
// execute "bash" (which is built-in to macOS). We then use
@@ -1073,6 +1063,7 @@ const Subprocess = struct {
pub fn deinit(self: *Subprocess) void {
self.stop();
if (self.pty) |*pty| pty.deinit();
+ if (self.env) |*env| env.deinit();
self.arena.deinit();
self.* = undefined;
}
@@ -1094,6 +1085,10 @@ const Subprocess = struct {
});
self.pty = pty;
errdefer {
+ if (comptime builtin.os.tag != .windows) {
+ _ = posix.close(pty.slave);
+ }
+
pty.deinit();
self.pty = null;
}
@@ -1151,7 +1146,7 @@ const Subprocess = struct {
var cmd: Command = .{
.path = self.args[0],
.args = self.args,
- .env = &self.env,
+ .env = if (self.env) |*env| env else null,
.cwd = cwd,
.stdin = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave },
.stdout = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave },
@@ -1178,6 +1173,19 @@ const Subprocess = struct {
log.info("subcommand cgroup={s}", .{self.linux_cgroup orelse "-"});
}
+ if (comptime builtin.os.tag != .windows) {
+ // Once our subcommand is started we can close the slave
+ // side. This prevents the slave fd from being leaked to
+ // future children.
+ _ = posix.close(pty.slave);
+ }
+
+ // Successful start we can clear out some memory.
+ if (self.env) |*env| {
+ env.deinit();
+ self.env = null;
+ }
+
self.command = cmd;
return switch (builtin.os.tag) {
.windows => .{
@@ -1452,6 +1460,13 @@ pub const ReadThread = struct {
log.info("read thread got quit signal", .{});
return;
}
+
+ // If our pty fd is closed, then we're also done with our
+ // read thread.
+ if (pollfds[0].revents & posix.POLL.HUP != 0) {
+ log.info("pty fd closed, read thread exiting", .{});
+ return;
+ }
}
}
diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig
index ab61ae4ca7..8a2e6cc7a7 100644
--- a/src/termio/Termio.zig
+++ b/src/termio/Termio.zig
@@ -220,7 +220,7 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
.renderer_mailbox = opts.renderer_mailbox,
.surface_mailbox = opts.surface_mailbox,
.size = opts.size,
- .backend = opts.backend,
+ .backend = backend,
.mailbox = opts.mailbox,
.terminal_stream = .{
.handler = handler,
diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig
index 8cd2a92ae2..423e2f5186 100644
--- a/src/termio/shell_integration.zig
+++ b/src/termio/shell_integration.zig
@@ -58,67 +58,73 @@ pub fn setup(
break :exe std.fs.path.basename(command[0..idx]);
};
- const result: ShellIntegration = shell: {
- if (std.mem.eql(u8, "bash", exe)) {
- // Apple distributes their own patched version of Bash 3.2
- // on macOS that disables the ENV-based POSIX startup path.
- // This means we're unable to perform our automatic shell
- // integration sequence in this specific environment.
- //
- // If we're running "/bin/bash" on Darwin, we can assume
- // we're using Apple's Bash because /bin is non-writable
- // on modern macOS due to System Integrity Protection.
- if (comptime builtin.target.isDarwin()) {
- if (std.mem.eql(u8, "/bin/bash", command)) {
- return null;
- }
- }
+ const result = try setupShell(alloc_arena, resource_dir, command, env, exe);
- const new_command = try setupBash(
- alloc_arena,
- command,
- resource_dir,
- env,
- ) orelse return null;
- break :shell .{
- .shell = .bash,
- .command = new_command,
- };
- }
+ // Setup our feature env vars
+ try setupFeatures(env, features);
- if (std.mem.eql(u8, "elvish", exe)) {
- try setupXdgDataDirs(alloc_arena, resource_dir, env);
- break :shell .{
- .shell = .elvish,
- .command = try alloc_arena.dupe(u8, command),
- };
- }
+ return result;
+}
- if (std.mem.eql(u8, "fish", exe)) {
- try setupXdgDataDirs(alloc_arena, resource_dir, env);
- break :shell .{
- .shell = .fish,
- .command = try alloc_arena.dupe(u8, command),
- };
+fn setupShell(
+ alloc_arena: Allocator,
+ resource_dir: []const u8,
+ command: []const u8,
+ env: *EnvMap,
+ exe: []const u8,
+) !?ShellIntegration {
+ if (std.mem.eql(u8, "bash", exe)) {
+ // Apple distributes their own patched version of Bash 3.2
+ // on macOS that disables the ENV-based POSIX startup path.
+ // This means we're unable to perform our automatic shell
+ // integration sequence in this specific environment.
+ //
+ // If we're running "/bin/bash" on Darwin, we can assume
+ // we're using Apple's Bash because /bin is non-writable
+ // on modern macOS due to System Integrity Protection.
+ if (comptime builtin.target.isDarwin()) {
+ if (std.mem.eql(u8, "/bin/bash", command)) {
+ return null;
+ }
}
- if (std.mem.eql(u8, "zsh", exe)) {
- try setupZsh(resource_dir, env);
- break :shell .{
- .shell = .zsh,
- .command = try alloc_arena.dupe(u8, command),
- };
- }
+ const new_command = try setupBash(
+ alloc_arena,
+ command,
+ resource_dir,
+ env,
+ ) orelse return null;
+ return .{
+ .shell = .bash,
+ .command = new_command,
+ };
+ }
- return null;
- };
+ if (std.mem.eql(u8, "elvish", exe)) {
+ try setupXdgDataDirs(alloc_arena, resource_dir, env);
+ return .{
+ .shell = .elvish,
+ .command = try alloc_arena.dupe(u8, command),
+ };
+ }
- // Setup our feature env vars
- if (!features.cursor) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR", "1");
- if (!features.sudo) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_SUDO", "1");
- if (!features.title) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_TITLE", "1");
+ if (std.mem.eql(u8, "fish", exe)) {
+ try setupXdgDataDirs(alloc_arena, resource_dir, env);
+ return .{
+ .shell = .fish,
+ .command = try alloc_arena.dupe(u8, command),
+ };
+ }
- return result;
+ if (std.mem.eql(u8, "zsh", exe)) {
+ try setupZsh(resource_dir, env);
+ return .{
+ .shell = .zsh,
+ .command = try alloc_arena.dupe(u8, command),
+ };
+ }
+
+ return null;
}
test "force shell" {
@@ -138,6 +144,58 @@ test "force shell" {
}
}
+/// Setup shell integration feature environment variables without
+/// performing full shell integration setup.
+pub fn setupFeatures(
+ env: *EnvMap,
+ features: config.ShellIntegrationFeatures,
+) !void {
+ if (!features.cursor) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR", "1");
+ if (!features.sudo) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_SUDO", "1");
+ if (!features.title) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_TITLE", "1");
+}
+
+test "setup features" {
+ const testing = std.testing;
+
+ var arena = ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ // Test: all features enabled (no environment variables should be set)
+ {
+ var env = EnvMap.init(alloc);
+ defer env.deinit();
+
+ try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true });
+ try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR") == null);
+ try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO") == null);
+ try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE") == null);
+ }
+
+ // Test: all features disabled
+ {
+ var env = EnvMap.init(alloc);
+ defer env.deinit();
+
+ try setupFeatures(&env, .{ .cursor = false, .sudo = false, .title = false });
+ try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR").?);
+ try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO").?);
+ try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE").?);
+ }
+
+ // Test: mixed features
+ {
+ var env = EnvMap.init(alloc);
+ defer env.deinit();
+
+ try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false });
+ try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR").?);
+ try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO") == null);
+ try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE").?);
+ }
+}
+
/// Setup the bash automatic shell integration. This works by
/// starting bash in POSIX mode and using the ENV environment
/// variable to load our bash integration script. This prevents
@@ -145,8 +203,6 @@ test "force shell" {
/// our script's responsibility (along with disabling POSIX
/// mode).
///
-/// This approach requires bash version 4 or later.
-///
/// This returns a new (allocated) shell command string that
/// enables the integration or null if integration failed.
fn setupBash(
@@ -188,12 +244,6 @@ fn setupBash(
// Unsupported options:
// -c -c is always non-interactive
// --posix POSIX mode (a la /bin/sh)
- //
- // Some additional cases we don't yet cover:
- //
- // - If additional file arguments are provided (after a `-` or `--` flag),
- // and the `i` shell option isn't being explicitly set, we can assume a
- // non-interactive shell session and skip loading our shell integration.
var rcfile: ?[]const u8 = null;
while (iter.next()) |arg| {
if (std.mem.eql(u8, arg, "--posix")) {
@@ -210,6 +260,14 @@ fn setupBash(
return null;
}
try args.append(arg);
+ } else if (std.mem.eql(u8, arg, "-") or std.mem.eql(u8, arg, "--")) {
+ // All remaining arguments should be passed directly to the shell
+ // command. We shouldn't perform any further option processing.
+ try args.append(arg);
+ while (iter.next()) |remaining_arg| {
+ try args.append(remaining_arg);
+ }
+ break;
} else {
try args.append(arg);
}
@@ -372,6 +430,30 @@ test "bash: HISTFILE" {
}
}
+test "bash: additional arguments" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var env = EnvMap.init(alloc);
+ defer env.deinit();
+
+ // "-" argument separator
+ {
+ const command = try setupBash(alloc, "bash - --arg file1 file2", ".", &env);
+ defer if (command) |c| alloc.free(c);
+
+ try testing.expectEqualStrings("bash --posix - --arg file1 file2", command.?);
+ }
+
+ // "--" argument separator
+ {
+ const command = try setupBash(alloc, "bash -- --arg file1 file2", ".", &env);
+ defer if (command) |c| alloc.free(c);
+
+ try testing.expectEqualStrings("bash --posix -- --arg file1 file2", command.?);
+ }
+}
+
/// Setup automatic shell integration for shells that include
/// their modules from paths in `XDG_DATA_DIRS` env variable.
///
diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig
index 849e5c107f..e9bb353fba 100644
--- a/src/termio/stream_handler.zig
+++ b/src/termio/stream_handler.zig
@@ -1418,11 +1418,13 @@ pub const StreamHandler = struct {
var buf = std.ArrayList(u8).init(self.alloc);
defer buf.deinit();
const writer = buf.writer();
- try writer.writeAll("\x1b]21");
for (request.list.items) |item| {
switch (item) {
.query => |key| {
+ // If the writer buffer is empty, we need to write our prefix
+ if (buf.items.len == 0) try writer.writeAll("\x1b]21");
+
const color: terminal.color.RGB = switch (key) {
.palette => |palette| self.terminal.color_palette.colors[palette],
.special => |special| switch (special) {
@@ -1517,14 +1519,16 @@ pub const StreamHandler = struct {
}
}
- try writer.writeAll(request.terminator.string());
-
- self.messageWriter(.{
- .write_alloc = .{
- .alloc = self.alloc,
- .data = try buf.toOwnedSlice(),
- },
- });
+ // If we had any writes to our buffer, we queue them now
+ if (buf.items.len > 0) {
+ try writer.writeAll(request.terminator.string());
+ self.messageWriter(.{
+ .write_alloc = .{
+ .alloc = self.alloc,
+ .data = try buf.toOwnedSlice(),
+ },
+ });
+ }
// Note: we don't have to do a queueRender here because every
// processed stream will queue a render once it is done processing
diff --git a/src/unicode/props.zig b/src/unicode/props.zig
index d77bf4c8ae..8c7621b795 100644
--- a/src/unicode/props.zig
+++ b/src/unicode/props.zig
@@ -131,7 +131,9 @@ pub fn get(cp: u21) Properties {
/// Runnable binary to generate the lookup tables and output to stdout.
pub fn main() !void {
- const alloc = std.heap.c_allocator;
+ var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ defer arena_state.deinit();
+ const alloc = arena_state.allocator();
const gen: lut.Generator(
Properties,
diff --git a/typos.toml b/typos.toml
index 87b41336bd..d449e3ffa5 100644
--- a/typos.toml
+++ b/typos.toml
@@ -43,6 +43,7 @@ Strat = "Strat"
grey = "gray"
greyscale = "grayscale"
DECID = "DECID"
+flate = "flate"
[type.swift.extend-words]
inout = "inout"