commit d07c2a5cc9eeb566cde80e0bf212ff50606fc7d3 Author: entailz Date: Tue May 12 23:33:02 2026 -0700 tabs and tab overview diff --git a/.builds/alpine-x64.yml.disabled b/.builds/alpine-x64.yml.disabled new file mode 100644 index 0000000..d1336b6 --- /dev/null +++ b/.builds/alpine-x64.yml.disabled @@ -0,0 +1,55 @@ +image: alpine/edge +packages: + - musl-dev + - eudev-libs + - eudev-dev + - linux-headers + - meson + - ninja + - gcc + - scdoc + - wayland-dev + - wayland-protocols + - freetype-dev + - fontconfig-dev + - harfbuzz-dev + - utf8proc-dev + - pixman-dev + - libxkbcommon-dev + - ncurses + - python3 + - py3-pip + - check-dev + - ttf-hack + - font-noto-emoji + +sources: + - https://git.sr.ht/~dnkl/foot + +# triggers: +# - action: email +# condition: failure +# to: + +tasks: + - fcft: | + cd foot/subprojects + git clone https://codeberg.org/dnkl/fcft.git + cd ../.. + - debug: | + mkdir -p bld/debug + meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug + ninja -C bld/debug -k0 + meson test -C bld/debug --print-errorlogs + - release: | + mkdir -p bld/release + meson --buildtype=minsize -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release + ninja -C bld/release -k0 + meson test -C bld/release --print-errorlogs + - codespell: | + python3 -m venv codespell-venv + source codespell-venv/bin/activate + pip install codespell + cd foot + ~/.local/bin/codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + deactivate diff --git a/.builds/alpine-x86.yml.disabled b/.builds/alpine-x86.yml.disabled new file mode 100644 index 0000000..6d79022 --- /dev/null +++ b/.builds/alpine-x86.yml.disabled @@ -0,0 +1,43 @@ +image: alpine/edge +arch: x86 +packages: + - musl-dev + - eudev-libs + - eudev-dev + - linux-headers + - meson + - ninja + - gcc + - scdoc + - wayland-dev + - wayland-protocols + - freetype-dev + - fontconfig-dev + - harfbuzz-dev + - utf8proc-dev + - pixman-dev + - libxkbcommon-dev + - ncurses + - check-dev + - ttf-hack + - font-noto-emoji + +sources: + - https://git.sr.ht/~dnkl/foot + +# triggers: +# - action: email +# condition: failure +# to: + +tasks: + - debug: | + mkdir -p bld/debug + meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug + ninja -C bld/debug -k0 + meson test -C bld/debug --print-errorlogs + - release: | + mkdir -p bld/release + meson --buildtype=minsize -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release + ninja -C bld/release -k0 + meson test -C bld/release --print-errorlogs diff --git a/.builds/freebsd-x64.yml b/.builds/freebsd-x64.yml new file mode 100644 index 0000000..77775ac --- /dev/null +++ b/.builds/freebsd-x64.yml @@ -0,0 +1,49 @@ +image: freebsd/latest +packages: + - evdev-proto + - libepoll-shim + - meson + - ninja + - pkgconf + - scdoc + - wayland + - wayland-protocols + - freetype2 + - fontconfig + - harfbuzz + - utf8proc + - pixman + - libxkbcommon + - check + - hack-font + - noto-emoji + +sources: + - https://codeberg.org/dnkl/foot.git + +# triggers: +# - action: email +# condition: failure +# to: + +tasks: + - fcft: | + cd foot/subprojects + git clone https://codeberg.org/dnkl/tllist.git + git clone https://codeberg.org/dnkl/fcft.git + cd ../.. + - debug: | + mkdir -p bld/debug + meson setup --buildtype=debug -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug + ninja -C bld/debug -k0 + meson test -C bld/debug --print-errorlogs + bld/debug/foot --version + bld/debug/footclient --version + + - release: | + mkdir -p bld/release + meson setup --buildtype=minsize -Db_pgo=generate -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release + ninja -C bld/release -k0 + meson test -C bld/release --print-errorlogs + bld/release/foot --version + bld/release/footclient --version diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ef74858 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +indent_style = space +indent_size = 4 +max_line_length = 70 + +[{meson.build,PKGBUILD}] +indent_size = 2 + +[*.scd] +indent_style = tab +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21655ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/bld/ +/build/ +/build-pgo/ +/pkg/ +/src/ +/.cache/ +/subprojects/*/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/.woodpecker.yaml b/.woodpecker.yaml new file mode 100644 index 0000000..900251a --- /dev/null +++ b/.woodpecker.yaml @@ -0,0 +1,139 @@ +# -*- yaml -*- + +steps: + - name: pychecks + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + image: alpine:edge + commands: + - apk add openssl + - apk add python3 + - apk add py3-pip + - python3 -m venv venv + - source venv/bin/activate + - python -m pip install --upgrade pip + - pip install codespell + - pip install mypy + - pip install ruff + - codespell + - mypy + - ruff check + - deactivate + + - name: subprojects + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + image: alpine:edge + commands: + - apk add git + - mkdir -p subprojects && cd subprojects + - git clone https://codeberg.org/dnkl/tllist.git + - git clone https://codeberg.org/dnkl/fcft.git + - cd .. + + - name: x64 + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + depends_on: [subprojects] + image: alpine:edge + commands: + - apk update + - apk add musl-dev linux-headers meson ninja gcc clang scdoc ncurses + - apk add libxkbcommon-dev pixman-dev freetype-dev fontconfig-dev harfbuzz-dev utf8proc-dev + - apk add wayland-dev wayland-protocols + - apk add git + - apk add check-dev + - apk add ttf-hack font-noto-emoji + + # Debug + - mkdir -p bld/debug-x64 + - cd bld/debug-x64 + - meson setup --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. + + # Release (gcc) + - mkdir -p bld/release-x64 + - cd bld/release-x64 + - meson setup --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. + + # Release (clang) + - mkdir -p bld/release-x64-clang + - cd bld/release-x64-clang + - CC=clang meson setup --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. + + # no grapheme clustering + - apk del harfbuzz harfbuzz-dev utf8proc utf8proc-dev + - mkdir -p bld/debug + - cd bld/debug + - meson setup --buildtype=debug -Dgrapheme-clustering=disabled -Dfcft:grapheme-shaping=disabled -Dfcft:run-shaping=disabled -Dfcft:test-text-shaping=false ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. + + - name: x86 + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + depends_on: [subprojects] + image: i386/alpine:edge + commands: + - apk update + - apk add musl-dev linux-headers meson ninja gcc clang scdoc ncurses + - apk add libxkbcommon-dev pixman-dev freetype-dev fontconfig-dev harfbuzz-dev utf8proc-dev + - apk add wayland-dev wayland-protocols + - apk add git + - apk add check-dev + - apk add ttf-hack font-noto-emoji + + # Debug + - mkdir -p bld/debug-x86 + - cd bld/debug-x86 + - meson setup --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. + + # Release (gcc) + - mkdir -p bld/release-x86 + - cd bld/release-x86 + - meson setup --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. + + # Release (clang) + - mkdir -p bld/release-x86-clang + - cd bld/release-x86-clang + - CC=clang meson setup --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..675e135 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3703 @@ +# Changelog + +* [Unreleased](#unreleased) +* [1.26.1](#1-26-1) +* [1.26.0](#1-26-0) +* [1.25.0](#1-25-0) +* [1.24.0](#1-24-0) +* [1.23.1](#1-23-1) +* [1.23.0](#1-23-0) +* [1.22.3](#1-22-3) +* [1.22.2](#1-22-2) +* [1.22.1](#1-22-1) +* [1.22.0](#1-22-0) +* [1.21.0](#1-21-0) +* [1.20.2](#1-20-2) +* [1.20.1](#1-20-1) +* [1.20.0](#1-20-0) +* [1.19.0](#1-19-0) +* [1.18.1](#1-18-1) +* [1.18.0](#1-18-0) +* [1.17.2](#1-17-2) +* [1.17.1](#1-17-1) +* [1.17.0](#1-17-0) +* [1.16.2](#1-16-2) +* [1.16.1](#1-16-1) +* [1.16.0](#1-16-0) +* [1.15.3](#1-15-3) +* [1.15.2](#1-15-2) +* [1.15.1](#1-15-1) +* [1.15.0](#1-15-0) +* [1.14.0](#1-14-0) +* [1.13.1](#1-13-1) +* [1.13.0](#1-13-0) +* [1.12.1](#1-12-1) +* [1.12.0](#1-12-0) +* [1.11.0](#1-11-0) +* [1.10.3](#1-10-3) +* [1.10.2](#1-10-2) +* [1.10.1](#1-10-1) +* [1.10.0](#1-10-0) +* [1.9.2](#1-9-2) +* [1.9.1](#1-9-1) +* [1.9.0](#1-9-0) +* [1.8.2](#1-8-2) +* [1.8.1](#1-8-1) +* [1.8.0](#1-8-0) +* [1.7.2](#1-7-2) +* [1.7.1](#1-7-1) +* [1.7.0](#1-7-0) +* [1.6.4](#1-6-4) +* [1.6.3](#1-6-3) +* [1.6.2](#1-6-2) +* [1.6.1](#1-6-1) +* [1.6.0](#1-6-0) +* [1.5.4](#1-5-4) +* [1.5.3](#1-5-3) +* [1.5.2](#1-5-2) +* [1.5.1](#1-5-1) +* [1.5.0](#1-5-0) +* [1.4.4](#1-4-4) +* [1.4.3](#1-4-3) +* [1.4.2](#1-4-2) +* [1.4.1](#1-4-1) +* [1.4.0](#1-4-0) +* [1.3.0](#1-3-0) +* [1.2.3](#1-2-3) +* [1.2.2](#1-2-2) +* [1.2.1](#1-2-1) +* [1.2.0](#1-2-0) + + +## Unreleased +### Added + +* `url.style=none|single|double|curly|dotted|dashed` option added, + allowing you to configure how URL underlines are drawn. The default + is `dotted` ([#2302][2302]). + +[2302]: https://codeberg.org/dnkl/foot/issues/2302 + + +### Changed + +* URL underlines are now dotted by default, instead of plain + underlines. This can be changed with the new `url.style` option. + + +### Deprecated +### Removed +### Fixed + +* Other output (key presses, query replies etc) being mixed with paste + data, both interactive pastes and OSC-52 ([#2307][2307]). +* Scrollback search not working correctly when the terminal + application has enabled the kitty keyboard protocol with release + event reporting ([#2316][2316]). +* Keypad escapes in the legacy keyboard protocol ignoring the shift + modifier ([#2324][2324]). + +[2307]: https://codeberg.org/dnkl/foot/issues/2307 +[2316]: https://codeberg.org/dnkl/foot/issues/2316 +[2324]: https://codeberg.org/dnkl/foot/issues/2324 + + +### Security +### Contributors + + +## 1.26.1 + +### Fixed + +* Wrong documented default value for `initial-color-theme` in + `foot.ini(5)` ([#2292][2292]). +* Occasional crashes when closing a window and + `tweak.pre-apply-damage=yes` (the default) ([#2288][2288]). + +[2292]: https://codeberg.org/dnkl/foot/issues/2292 +[2288]: https://codeberg.org/dnkl/foot/issues/2288 + + +### Contributors + +* Roshless +* vlkrs + + +## 1.26.0 + +### Added + +* `toplevel-tag` option (and `--toplevel-tag` command line options to + `foot` and `footclient`), allowing you to set a custom toplevel + tag. The compositor must implement the new `xdg-toplevel-tag-v1` + Wayland protocol ([#2212][2212]). +* `[colors-dark]` section to `foot.ini`. Replaces `[colors]`. +* `[colors-light]` section to `foot.ini`. Replaces `[colors2]`. +* `XTGETTCAP`: added `query-os-name`, returning the OS foot is + compiled for (e.g. _'Linux'_) ([#2209][2209]). +* `pad` option now supports 4-directional padding format: + `LEFTxTOPxRIGHTxBOTTOM` (e.g., `20x10x20x10`). +* `--config=PATH` option is now automatically passed to new + terminals spawned via `spawn-terminal` action ([#2259][2259]). +* Preliminary (untested) support for background blur via the new + `ext-background-effect-v1` protocol. Enable by setting + `colors-{dark,light}.blur=yes`. Foot needs to have been **built** + against `wayland-protocols >= 1.45`, and the compositor **must** + implement the `ext-background-effect-v1` protocol, **and** the + `blur` effect. + +[2212]: https://codeberg.org/dnkl/foot/issues/2212 +[2209]: https://codeberg.org/dnkl/foot/issues/2209 +[2259]: https://codeberg.org/dnkl/foot/pulls/2259 + + +### Changed + +* When enabling _"focus mode"_ (private mode 1004), foot now sends a + focus event immediately, to inform the application what the current + state is ([#2202][2202]). +* Scrollback search is now case sensitive when the search string + contains at least one upper case character. +* Mouse tracking in SGR pixel mode no longer emits negative column/row + pixel values ([#2226][2226]). +* Foot now always uses ARGB SHM surfaces. In earlier versions, XRGB + surfaces were used for opaque surfaces. Unfortunately, several + compositors had issues when foot switched between ARGB and XRGB + surfaces (for example when switching color theme, or toggling + fullscreen). + +[2202]: https://codeberg.org/dnkl/foot/issues/2202 +[2226]: https://codeberg.org/dnkl/foot/issues/2226 + + +### Deprecated + +* `[colors]` section in `foot.ini`. Use `[colors-dark]` instead. +* `[colors2]` section in `foot.ini`. Use `[colors-light]` instead. + + +### Removed + +* `cursor.color` config option (deprecated in 1.23.0). Use + `colors-{dark,light}.cursor` instead. + + +### Fixed + +* Search mode: composing keys not ignored. +* Crash when triple-clicking a soft-wrapped line and there is a quote + character in the last column. +* Crash when reverse-scrolling (terminfo capability `rin`) such that + the current viewport ends up outside the scrollback ([#2232][2232]). +* Regression: visual glitches in rare circumstances. +* Key release events for shortcuts being sent to the client + application (kitty keyboard protocol only) ([#2257][2257]). +* Crash when application emits sixel RA with a height of 0, a width > + 0, and then starts writing sixel data ([#2267][2267]). +* Crash if shutting down terminal instance while a "pre-apply damage" + thread is running ([#2263][2263]). + +[2232]: https://codeberg.org/dnkl/foot/issues/2232 +[2257]: https://codeberg.org/dnkl/foot/issues/2257 +[2267]: https://codeberg.org/dnkl/foot/issues/2267 +[2263]: https://codeberg.org/dnkl/foot/issues/2263 + + +### Contributors + +* Andrei +* Barinderpreet Singh +* c4llv07e +* Johannes Altmanninger +* nariby +* pi66 +* Ronan Pigott +* Stéphane Klein +* valoq +* Whyme Lyu +* Yaakov Selkowitz + + +## 1.25.0 + +### Added + +* Performance increased and input latency decreased on compositors + that do not release SHM buffers immediately ([#2188][2188]). +* `colors{,2}.dim-blend-towards=black|white` option, allowing you to + select towards which color to blend when dimming text. Defaults to + `black` in `[colors]`, and `white` in `[colors2]` ([#2187][2187]). + +[2188]: https://codeberg.org/dnkl/foot/issues/2188 +[2187]: https://codeberg.org/dnkl/foot/issues/2187 + + +### Changed + +* SHM buffer sizes are now rounded up to nearest page size, and their + stride is always an even multiple of 256 bytes (by default, + configurable by setting `tweak.min-stride-alignment`). This allows + compositor to directly import foot's SHM buffers to the GPU, with + e.g. integrated graphics ([#2182][2182]). +* Jump label colors in the modus-operandi theme, for improved + readability. + +[2182]: https://codeberg.org/dnkl/foot/issues/2182 + + +### Fixed + +* URL labels misplaces when URL contains double-width characters + ([#2179][2179]). +* One space too much consumed when copying (or pipe:ing) contents with + tabs ([#2194][2194]) +* Ensure we render a new frame when changing fullscreen state. Before, + this was automatically done if the window was also resized. But, it + is possible for a compositor to change an application's fullscreen + state without resizing the window. + +[2179]: https://codeberg.org/dnkl/foot/issues/2179 +[2194]: https://codeberg.org/dnkl/foot/issues/2194 + + +### Contributors + +* Charalampos Mitrodimas +* Matthias Heyman + + +## 1.24.0 + +### Added + +* The `uppercase-regex-insert` option controls whether an uppercase hint + character will insert the selected text into the prompt in `regex-copy` + or `show-urls-copy` mode. It defaults to `true`. ([#2159][2159]). + +[2159]: https://codeberg.org/dnkl/foot/issues/2159 + +### Changed + +* The label letters are no longer sorted before being assigned to URLs + ([#2140][2140]). +* Sending SIGUSR1/SIGUSR2 to a `foot --server` process now causes + newly spawned client instances to use the selected theme, instead of + the original one. +* SIGUSR1/SIGUSR2 can now be sent to `footclient` processes, to change + the theme of that particular instance ([#2156][2156]). + +[2156]: https://codeberg.org/dnkl/foot/issues/2156 + + +### Fixed + +* Invalid configuration values overriding valid ones in surprising + ways. +* Bug where the libutempter utmp backend did not record logouts + correctly. + +### Contributors + +* Ryan Roden-Corrent +* Tobias Mock + + +## 1.23.1 + +### Changed + +* URL labels are now assigned in reverse order, from bottom to + top. This ensures the **last** URL (which is often the one you are + interested in) is always assigned the same key ([#2140][2140]). +* Sending `SIGUSR1` no longer **toggles** between `[colors]` and + `[colors2]`, but explicitly changes to `[colors]`. `SIGUSR2` changes + to `[colors2]` ([#2144][2144]). + +[2140]: https://codeberg.org/dnkl/foot/issues/2140 +[2144]: https://codeberg.org/dnkl/foot/issues/2144 + + +### Fixed + +* 10-bit surfaces sometimes used instead of 16-bit. +* OSC-104/110/111/112/117/119 (reset colors) not taking the currently + active theme into account. + + +## 1.23.0 + +### Added + +* `colors2` config section. This section duplicates the `colors` + section, and lets you define an alternative color theme. +* `key-bindings.color-theme-switch-1`, + `key-bindings.color-theme-switch-2` and + `key-bindings.color-theme-toggle` key bindings. These can be used to + switch between the primary and alternative color themes. They are + not bound by default. +* Sending `SIGUSR1` to the foot process now triggers a theme switch + (in server mode, **all** instances toggles their themes). +* Support for private mode 2031 - [_Dark and Light Mode + Detection_](https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) + ([#2025][2025]) +* Added `initial-color-theme=1|2` config option. `1` uses colors from + the `[colors]` section, `2` uses `[colors2]`. +* Combined dark/light theme files for (dark variant is the default, + set `initial-color-theme=2` to use the light variant by default): + - gruvbox + - nvim + - paper-color + - selenized + - solarized +* `regex-copy`/`show-urls-copy` will copy and paste the selected text if the hint + is completed with an uppercase character ([#1975][1975]). +* `16-bit` to `tweak.surface-bit-depth`. Makes foot use 16-bit image + buffers. They provide the necessary color precision required by + `gamma-correct-blending=yes`. +* New cursor shapes, from `cursor-shape-v1` version 2. +* `center-when-fullscreen` and `center-when-maximized-and-fullscreen` + to the `pad` option. This allows you to configure when the grid is + centered in more detail ([#2111][2111]). + +[2025]: https://codeberg.org/dnkl/foot/issues/2025 +[1975]: https://codeberg.org/dnkl/foot/issues/1975 +[2111]: https://codeberg.org/dnkl/foot/issues/2111 + + +### Changed + +* `cursor.color` moved to `colors.cursor`. +* OSC-11 without an alpha value will now restore the configured + (i.e. from `foot.ini`) alpha, rather than keeping whatever the + current alpha value is, unchanged. +* `gamma-correct-blending=yes` now defaults to `16-bit` image buffers, + instead of `10-bit`. +* Allow setting either selection background, or selection foreground, + only ([#1846][1846]). +* Drop required version of libxkbcommon from 1.8.0 back to 1.0.0 + ([#2103][2103]). +* OSC-52: an empty payload now clears the clipboard. +* DA (Device Attributes): include `52` in the reply, to indicate + OSC-52 support (when at least _copy_ has been enabled in + `security.osc52`). + +[1846]: https://codeberg.org/dnkl/foot/issues/1846 +[2103]: https://codeberg.org/dnkl/foot/issues/2103 + + +### Deprecated + +* `cursor.color` config option; use `colors.cursor` instead. + + +### Removed + +* Subsurface unmap quirk for Sway. This was a workaround added in + 1.12.1, for Sway issue [#6960][sway-6960]. + + +### Fixed + +* `REP`: wrong width of repeated multi-codepoint graphemes. +* Incorrect surface commit after a configure event, under certain + conditions ([#2105][2105]). + +[2105]: https://codeberg.org/dnkl/foot/issues/2105 + + +### Contributors + +* Chen Mulong +* Kirill Primak +* Ryan Roden-Corrent +* tokyo4j + + +## 1.22.3 + +### Added + +* `auto` to the `tweak.surface-bit-depth` option. + + +### Changed + +* `gamma-correct-blending` now defaults to `no` instead of `yes`. +* `tweak.surface-bit-depth` default value changed to `auto`; uses + 10-bit surfaces when `gamma-correct-blending=yes`, and 8-bit + surfaces otherwise. + + +### Fixed + +* Inaccurate colors when `gamma-correct-blending=yes` ([#2082][2082]). + +[2082]: https://codeberg.org/dnkl/foot/issues/2082 + + +## 1.22.2 + +### Changed + +* `gamma-correct-blending=yes` now uses a pure gamma 2.2 transfer + function, instead of the piece-wise sRGB transfer function, to match + what compositors do. + + +### Fixed + +* Wrong colors when `gamma-correct-blending=yes` (the default when + there is compositor support). Note that some colors will still be + off by a **very** small amount, due to loss of precision when + converting to a linear color space. ([#2035][2035]). + +[2035]: https://codeberg.org/dnkl/foot/issues/2035 + + +## 1.22.1 + +### Fixed + +* `colors.alpha-mode=matching` not working as intended. +* Grapheme shaping was allowed to be "enabled" at runtime, even though + disabled at compile time. This caused mis-rendering of certain + codepoints ([#2039][2039]). +* Keyboard modifiers not being reset on keyboard leave events + ([#2034][2034]). +* Fallback font (and possibly wrong color) being used when a character + was followed by a zero-width grapheme breaking codepoint (for + example, _LEFT-TO-RIGHT MARK_) ([#2049][2049]). +* Regression: alpha applied to inversed text/selections + ([#2073][2073]). + +[2039]: https://codeberg.org/dnkl/foot/issues/2039 +[2034]: https://codeberg.org/dnkl/foot/issues/2034 +[2049]: https://codeberg.org/dnkl/foot/issues/2049 +[2073]: https://codeberg.org/dnkl/foot/issues/2073 + + +### Contributors + +* Jan Palus +* valoq + + +## 1.22.0 + +### Added + +* Support for toplevel edge constraints. When the compositor indicates + the toplevel has edge constraints, foot will not allow the window to + be resized (via CSDs) in the constrained directions. +* Virtual modifiers (e.g. `Alt` instead of `Mod1`, `Super` instead of + `Mod4` etc) in key bindings are now recognized as being virtual, and + are automatically mapped to the corresponding real modifier. This + means you can use e.g. `Alt+b` instead of `Mod1+b`. +* `alpha-mode` option to `foot.ini`. Defaults to `default`. This + config changes how alpha is handled on background colours not set by + the terminal.(e.g. vim) ([#2026](2026)) + +[2026]: https://codeberg.org/dnkl/foot/issues/2026 + + +### Changed + +* UTF-8 error recovery now discards fewer bytes. +* Auto-calculated dimmed and brightened colors (e.g. when custom dim + colors has not configured) is now done by linear RGB interpolation, + rather than converting to HSL and adjusting the luminance + ([#2006][2006]). +* Virtual modifiers in keyboard events from the compositor are now + supported. This works around various issues seen when running foot + under mutter (GNOME) ([#2009][2009]): + - Some key combinations generating the wrong escape sequence in the + kitty keyboard protocol. + - some of foot's default shortcuts not working (mainly those using + `Mod1`) out of the box. +* Default URL regex changed to a much more strict variant + ([#2016][2016]). You can manually set the [old + one](https://codeberg.org/dnkl/foot/src/tag/1.21.0/foot.ini#L72), if + you prefer it over the new regex. +* A tiled window can now be resized in the corners (via CSDs), unless + the compositor has indicated the toplevel has edge constraints. + +[2006]: https://codeberg.org/dnkl/foot/issues/2006 +[2009]: https://codeberg.org/dnkl/foot/issues/2009 +[2016]: https://codeberg.org/dnkl/foot/issues/2016 + + +### Fixed + +* Regression: assertion in `url-mode.c` when activating a second URL + via `show-urls-persistent` ([#2000][2000]). +* Build failure (`srgb.h` not found) when doing a parallel build. +* Regression: reflowing (changing the window size) removing empty + lines ([#2011][2011]). +* `url/regex-copy` not handling double-width characters correctly + ([#2027][2027]). + +[2000]: https://codeberg.org/dnkl/foot/issues/2000 +[2011]: https://codeberg.org/dnkl/foot/issues/2011 +[2027]: https://codeberg.org/dnkl/foot/issues/2027 + + +### Contributors + +* Alex Xu (Hello71) +* datsudo +* Dominique Martinet +* Fazzi +* llyyr +* Łukasz Wojniłowicz +* Sam McCall + + +## 1.21.0 + +### Added + +* Support for the new Wayland protocol `xdg-system-bell-v1` protocol + (added in wayland-protocols 1.38), via the new config option + `bell.system=no|yes` (defaults to `yes`). +* Support for custom regex matching ([#1386][1386], + [#1872][1872]) +* Support for kitty's text-sizing protocol (`w`, width, only), OSC-66. +* `cursor.style` can now be set to `hollow` ([#1965][1965]). +* `search-bindings.delete-to-start` and + `search-bindings.delete-to-end` key bindings, defaulting to + `Control+u` and `Control+k` respectively ([#1972][1972]). +* Gamma-correct font rendering. Requires compositor support + (`wp_color_management_v1`, and specifically, the `ext_linear` + transfer function). Enabled by default when compositor support is + available. Can be explicitly enabled or disabled with + `gamma-correct-blending=no|yes`. + +[1386]: https://codeberg.org/dnkl/foot/issues/1386 +[1872]: https://codeberg.org/dnkl/foot/issues/1872 +[1965]: https://codeberg.org/dnkl/foot/issues/1965 +[1972]: https://codeberg.org/dnkl/foot/issues/1972 + + +### Changed + +* Do not try to set a zero width, or height, if the compositor sends a + _configure_ event with only one dimension being zero + ([#1925][1925]). +* Auto-detection of URLs (i.e. not OSC-8 based URLs) are now regex + based. +* Rename Tokyo Night Day theme to Tokyo Night Light and update colors. +* fcft >= 3.3.1 is now required. + - `tweak.scaling-filter` now supports more scaling-filters + - scaled bitmap fonts (when enabled in FontConfig) no longer have a + scaling-filter applied +* Linefeed:ing control characters (e.g. `\n`) no longer **clears** a + row's internal linebreak flag. This fixes an issue where + e.g. multi-line prompt input in fish is treated as separate lines, + rather than one logical, when selecting and copying it + ([#1487][1487]). +* wayland-protocols >= 1.41 is now required. + +[1925]: https://codeberg.org/dnkl/foot/issues/1925 +[1487]: https://codeberg.org/dnkl/foot/issues/1487 + + +### Removed + +* `url.uri-characters` and `url.protocols`. Both options have been + replaced by `url.regex`. +* `notify` option (has been deprecated since 1.18.0). +* `notify-focus-inhibit` option (has been deprecated since 1.18.0). + + +### Fixed + +* Kitty keyboard protocol: alternate key reporting failing to report + the alternate codepoint in some corner cases ([#1918][1918]). +* `foot` and `footclient` hanging, or terminating with `SIGABRT`, when + starting inside a directory whose total length is more than 1024 + characters. +* Regression: reflowing (resizing the window) a line that ends with a + double-width glyph that was pushed to the next line due to there + being only one cell left on current line, did not remove the virtual + space inserted at the end of the current line. +* Wrong key bindings executed when using alternative keyboard layouts + ([#1929][1929]). +* Foot not closing file descriptors for unrecognized or `no_keymap` + keymaps. +* Combining characters (including emojis consisting of multiple + codepoints) not being handled correctly when _insert mode_ is + enabled ([#1947][1947]). +* Reflow of the cursor (active + saved) when at the end of the line + with a pending wrap (LCF set) ([#1954][1954]). +* ~~Zero-width characters that also are grapheme breaks (e.g. U+200B, + ZERO WIDTH SPACE) being ignored (discarded and never stored in the + grid) ([#1960][1960]).~~ (reverted) +* `--server=` not working on FreeBSD ([#1956][1956]). +* Crash when resetting the terminal and an application had previously + set a custom app ID ([#1963][1963]) +* Grapheme clustering state not reset on cursor movements. +* Kitty keyboard protocol: no release events emitted for composed + keys. +* IME: the initial cursor position was reported as 0,0,0,0 + ([#1994][1994]). + +[1918]: https://codeberg.org/dnkl/foot/issues/1918 +[1929]: https://codeberg.org/dnkl/foot/issues/1929 +[1947]: https://codeberg.org/dnkl/foot/issues/1947 +[1954]: https://codeberg.org/dnkl/foot/issues/1954 +[1960]: https://codeberg.org/dnkl/foot/issues/1960 +[1956]: https://codeberg.org/dnkl/foot/issues/1956 +[1963]: https://codeberg.org/dnkl/foot/issues/1963 +[1994]: https://codeberg.org/dnkl/foot/issues/1994 + + +### Contributors + +* Adrian fxj9a +* Alexander Orzechowski +* Attila Fidan +* camel-cdr +* Craig Barnes +* Guillaume Outters +* Johannes Altmanninger +* Ludovico Gerardi +* sewn +* Thomas Bonnefille + + +## 1.20.2 + +### Changed + +* The `CSI 21 t` (report window title) and `OSC 176 ?` (report app-id) + escape sequences are now ignored ([#1894][1894]). + +[1894]: https://codeberg.org/dnkl/foot/issues/1894 + + +### Fixed + +* 'flash' overlay (triggered by either `tput flash`, or enabling + `bell.visual` and then sending `BEL` to the terminal) stuck when + `colors.flash-alpha=1.0`. +* Crash when compositor sends a keyboard enter event before the foot + window has been mapped ([#1910][1910]). +* Build failures (`utf8proc.h` not found) on at least FreeBSD, but + most likely other BSDs, as well as some Linuxes ([#1903][1903]). + +[1910]: https://codeberg.org/dnkl/foot/issues/1910 +[1903]: https://codeberg.org/dnkl/foot/issues/1903 + + +### Contributors + +* Alexander Orzechowski + + +## 1.20.1 + +### Changed + +* Runtime changes to the app-id (OSC-176) now limits the app-id string + to 2048 characters ([#1897][1897]). +* `colors.flash-alpha` can no longer be set to 1.0 (i.e. fully + opaque). This fixes an issue where the window would be stuck in the + flash state. + +[1897]: https://codeberg.org/dnkl/foot/issues/1897 + + +### Fixed + +* Regression: trying to print a Unicode _"Legacy Computing symbol"_, + in the range U+1FB00 - U+1FB9B would crash foot ([#1901][1901]). + +[1901]: https://codeberg.org/dnkl/foot/issues/1901 + + +## 1.20.0 + +### Added + +* Unicode data files updated to Unicode 16. Foot uses these to + determine which VS-15 and VS-16 sequences are valid, and which are + not. +* Box drawing characters U+1CD00...U+1CDE5 (the _"octants"_ from the + _"Symbols for Legacy Computing Supplement"_ codepoint range, added + in Unicode 16.0). +* `security.osc52` option, allowing you to partially or fully disable + host clipboard access via the OSC-52 escape sequence + ([#1867][1867]). + +[1867]: https://codeberg.org/dnkl/foot/issues/1867 + + +### Changed + +* OSC-9: sequences beginning with `;` are now ignored. These + sequences are ConEmu/Windows Terminal sequences, and not intended to + be notifications. +* Use `utf8proc_charwidth()` instead of `wcwidth()`+`wcswidth()` when + calculating character width, when foot has been built with utf8proc + support ([#1865][1865]). +* Run-time changes to the window title, and the app ID now require the + new value to consist of printable characters only. +* Kitty keyboard protocol: Enter, Tab and Backspace no longer report + _release_ events unless _"Report all keys as escape codes"_ is + enabled ([#1892][1892]). + +[1865]: https://codeberg.org/dnkl/foot/issues/1865 +[1892]: https://codeberg.org/dnkl/foot/issues/1892 + + +### Fixed + +* Crash when receiving an OSC-9 or OSC-777 with an empty notification + body ([#1866][1866]). +* Crash when tripple-clicking on region containing `NUL` characters. + +[1866]: https://codeberg.org/dnkl/foot/issues/1866 + + +### Contributors + +* cy +* Denis Zharikov +* heather7283 +* Jack Wilsdon +* Mark Stosberg + + +## 1.19.0 + +### Added + +* `resize-keep-grid` option, controlling whether the window is resized + (and the grid reflowed) or not when e.g. zooming in/out + ([#1807][1807]). +* `strikeout-thickness` option. +* Implemented the new `xdg-toplevel-icon-v1` protocol. +* Implemented `CSI 21 t`: report window title. +* `colors.sixelNN` option, controlling the default sixel color + palette. + +[1807]: https://codeberg.org/dnkl/foot/issues/1807 + + +### Changed + +* `cursor.unfocused-style` is now effective even when `cursor.style` + is not `block`. +* Activating a notification triggered with OSC-777, or BEL, now + focuses the foot window, if XDG activation tokens are supported by + the compositor, the notification daemon, and the notification helper + used by foot (i.e. `desktop-notifications.command`). This has been + supported for OSC-99 since 1.18.0, and now we also support it for + BEL and OSC-777 ([#1822][1822]). +* Sixel background color (when `P2=0|2`) is now set to the **sixel** + color palette entry #0, instead of using the current ANSI background + color. This is what a real VT340 does. +* The `.desktop` files no longer use the reverse DNS naming scheme, + and their names now match the default app-ids used by foot (`foot` + and `footclient`) ([#1607][1607]). +* `file://` prefix are now stripped from OSC-8 URIs when + activated/opened, **if** the hostname matches the hostname of the + computer foot is running on ([#1840][1840]). + +[1822]: https://codeberg.org/dnkl/foot/issues/1822 +[1607]: https://codeberg.org/dnkl/foot/issues/1607 +[1840]: https://codeberg.org/dnkl/foot/issues/1840 + + +### Fixed + +* Some invalid UTF-8 strings passing the validity check when setting + the window title, triggering a Wayland protocol error which then + caused foot to shutdown. +* "Too large" values for `scrollback.lines` causing an integer + overflow, resulting in either visual glitches, crashes, or both + ([#1828][1828]). +* Crash when trying to set an invalid cursor shape with OSC-22, when + foot uses server-side cursor shapes. +* Occasional visual glitches when selecting text, when foot is running + under a compositor that forces foot to double buffer + (e.g. KDE/KWin) ([#1715][1715]). +* Sixels flickering when foot is running under a compositor that + forces foot to double buffer (e.g. KDE, or Smithay based + compositors) ([#1851][1851]). + +[1828]: https://codeberg.org/dnkl/foot/issues/1828 +[1715]: https://codeberg.org/dnkl/foot/issues/1715 +[1851]: https://codeberg.org/dnkl/foot/issues/1851 + + +### Contributors + +* Andrew J. Hesford +* Craig Barnes +* Oleh Hushchenkov +* tokyo4j + + +## 1.18.1 + +### Added + +* OSC-99: support for the `s` parameter. Supported keywords are + `silent`, `system` and names from the freedesktop sound naming + specification. +* `${muted}` and `${sound-name}` added to the + `desktop-notifications.command` template. + + +### Changed + +* CSD buttons now activate on mouse button **release**, rather than + press ([#1787][1787]). + +[1787]: https://codeberg.org/dnkl/foot/issues/1787 + + +### Fixed + +* Regression: OSC-111 not handling alpha changes correctly, causing + visual glitches ([#1801][1801]). + +[1801]: https://codeberg.org/dnkl/foot/issues/1801 + + +### Contributors + +* Craig Barnes +* Shogo Yamazaki + + +## 1.18.0 + +### Added + +* `cursor.blink-rate` option, allowing you to configure the rate the + cursor blinks with (when `cursor.blink=yes`) ([#1707][1707]); +* Support for `wp_single_pixel_buffer_v1`; certain overlay surfaces + will now utilize the new single-pixel buffer protocol. This mainly + reduces the memory usage, but should also be slightly faster. +* Support for high-res mouse wheel scroll events ([#1738][1738]). +* Styled and colored underlines ([#828][828]). +* Support for SGR 21 (double underline). +* Support for `XTPUSHCOLORS`, `XTPOPCOLORS` and `XTREPORTCOLORS`, + i.e. color palette stack ([#856][856]). +* Log output now respects the [`NO_COLOR`](http://no-color.org/) + environment variable ([#1771][1771]). +* Support for [in-band window resize + notifications](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83), + private mode `2048`. +* Support for OSC-99 [_"Kitty desktop + notifications"_](https://sw.kovidgoyal.net/kitty/desktop-notifications/). +* `desktop-notifications.command` option, replaces `notify`. +* `desktop-notifications.inhibit-when-focused` option, replaces + `notify-focus-inhibit`. +* `${category}`, `${urgency}`, `${expire-time}`, `${replace-id}`, + `${icon}` and `${action-argument}` added to the + `desktop-notifications.command` template. +* `desktop-notifications.command-action-argument` option, defining how + `${action-argument}` (in `desktop-notifications.command`) should be + expanded. +* `desktop-notifications.close` option, defining what to execute when + an application wants to close an existing notification (via an + OSC-99 escape sequence). + +[1707]: https://codeberg.org/dnkl/foot/issues/1707 +[1738]: https://codeberg.org/dnkl/foot/issues/1738 +[828]: https://codeberg.org/dnkl/foot/issues/828 +[856]: https://codeberg.org/dnkl/foot/issues/856 +[1771]: https://codeberg.org/dnkl/foot/issues/1771 + + +### Changed + +* All `XTGETTCAP` capabilities are now in the `tigetstr()` format: + + - parameterized string capabilities were previously "source + encoded", meaning e.g. `\E` where not "decoded" into `\x1b`. + - Control characters were also "source encoded", meaning they were + returned as e.g. "^G" instead of `\x07` ([#1701][1701]). + + In other words, if, after this change, `XTGETTCAP` returns a string + that is different compared to `tigetstr()`, then it is likely a bug + in foot's implementation of `XTGETTCAP`. +* If the cursor foreground and background colors are identical (for + example, when cursor uses inverted colors and the cell's foreground + and background are the same), the cursor will instead be rendered + using the default foreground and background colors, inverted + ([#1761][1761]). +* Mouse wheel events now generate `BTN_WHEEL_BACK` and + `BTN_WHEEL_FORWARD` "button presses", instead of `BTN_BACK` and + `BTN_FORWARD`. The default bindings have been updated, and + `scrollback-up-mouse`, `scrollback-down-mouse`, `font-increase` and + `font-decrease` now use the new button names. + + This change allow users to separate physical mouse buttons that + _also_ generates `BTN_BACK` and `BTN_FORWARD`, from wheel scrolling + ([#1763][1763]). +* Replaced the old catppuccin theme with updated flavored themes + pulled from [catppuccin/foot](https://github.com/catppuccin/foot) +* Mouse selections can now be started inside the margins + ([#1702][1702]). + +[1701]: https://codeberg.org/dnkl/foot/issues/1701 +[1761]: https://codeberg.org/dnkl/foot/issues/1761 +[1763]: https://codeberg.org/dnkl/foot/issues/1763 +[1702]: https://codeberg.org/dnkl/foot/issues/1702 + + +### Deprecated + +* `notify` option; replaced by `desktop-notifications.command`. +* `notify-focus-inhibit` option; replaced by + `desktop-notifications.inhibit-when-focused`. + + +### Fixed + +* Crash when zooming in or out, with `dpi-aware=yes`, and the + monitor's DPI is 0 (this is true for, for example, nested Wayland + sessions, or in virtualized environments). +* No error response for empty `XTGETTCAP` request ([#1694][1694]). +* Unicode-mode in one foot client affecting other clients, in foot + server mode ([#1717][1717]). +* IME interfering in URL-mode ([#1718][1718]). +* OSC-52 reply interleaved with other data sent to the client + ([#1734][1734]). +* XKB compose state being reset when foot receives a new keymap + ([#1744][1744]). +* Regression: alpha changes through OSC-11 sequences not taking effect + until window is resized. +* VS15 being ignored ([#1742][1742]). +* VS16 being ignored for a subset of the valid VS16 sequences + ([#1742][1742]). +* Crash in debug builds, when using OSC-12 to set the cursor color and + foot config has not set any custom cursor colors (i.e. without + OSC-12, inverted fg/bg would be used). +* Wrong color used when drawing the unfocused, hollow cursor. +* Encoding of `BTN_BACK` and `BTN_FORWARD`, when sending a mouse input + escape sequence to the terminal application. + +[1694]: https://codeberg.org/dnkl/foot/issues/1694 +[1717]: https://codeberg.org/dnkl/foot/issues/1717 +[1718]: https://codeberg.org/dnkl/foot/issues/1718 +[1734]: https://codeberg.org/dnkl/foot/issues/1734 +[1744]: https://codeberg.org/dnkl/foot/issues/1744 +[1742]: https://codeberg.org/dnkl/foot/issues/1742 + + +### Contributors + +* abs3nt +* Artturin +* Craig Barnes +* Jan Beich +* Mariusz Bialonczyk +* Nicolas Kolling Ribas + + +## 1.17.2 + +### Changed + +* Notifications with invalid UTF-8 strings are now ignored. + + +### Fixed + +* Crash when changing aspect ratio of a sixel, in the middle of the + sixel data (this is unsupported in foot, but should of course not + result in a crash). +* Crash when printing double-width (or longer) characters to, or near, + the last column, when auto-wrap (private mode 7) has been disabled. +* Dynamically sized sixel being trimmed to nothing. +* Flickering with `dpi-aware=yes` and window is unmapped/remapped + (some compositors do this when window is minimized), in a + multi-monitor setup with different monitor DPIs. + + +## 1.17.1 + +### Added + +* `cursor.unfocused-style=unchanged|hollow|none` to `foot.ini`. The + default is `hollow` ([#1582][1582]). +* New key binding: `quit` ([#1475][1475]). + +[1582]: https://codeberg.org/dnkl/foot/issues/1582 +[1475]: https://codeberg.org/dnkl/foot/issues/1475 + + +### Fixed + +* Log-level not respected by syslog. +* Regression: terminal shutting down when the PTY is closed by the + client application, which may be earlier than when the client + application exits ([#1666][1666]). +* When closing the window, send `SIGHUP` to the client application, + before sending `SIGTERM`. The signal sequence is now `SIGHUP`, wait, + `SIGTERM`, wait `SIGKILL`. +* Crash when receiving a `DECRQSS` request with more than 2 bytes in + the `q` parameter. + +[1666]: https://codeberg.org/dnkl/foot/issues/1666 + + +### Contributors + +* Holger Weiß +* izmyname +* Marcin Puc +* tunjan + + +## 1.17.0 + +### Added + +- Support for opening an existing PTY, e.g. a VM console. + ([#1564][1564]) +* Unicode input mode now accepts input from the numpad as well, + numlock is ignored. +* A new `resize-by-cells` option, enabled by default, allows the size + of floating windows to be constrained to multiples of the cell size. +* Support for custom (i.e. other than ctrl/shift/alt/super) modifiers + in key bindings ([#1348][1348]). +* `pipe-command-output` key binding. +* Support for OSC-176, _"Set App-ID"_ + (https://gist.github.com/delthas/d451e2cc1573bb2364839849c7117239). +* Support for `DECRQM` queries with ANSI/ECMA-48 modes (`CSI Ps $ p`). +* Rectangular edit functions: `DECCARA`, `DECRARA`, `DECCRA`, `DECFRA` + and `DECERA` ([#1633][1633]). +* `Rect` capability to terminfo. +* `fe` and `fd` (focus in/out enable/disable) capabilities to + terminfo. +* `nel` capability to terminfo. + +[1348]: https://codeberg.org/dnkl/foot/issues/1348 +[1633]: https://codeberg.org/dnkl/foot/issues/1633 +[1564]: https://codeberg.org/dnkl/foot/pulls/1564 +[`DECBKM`]: https://vt100.net/docs/vt510-rm/DECBKM.html + + +### Changed + +* config: ARGB color values now default to opaque, rather than + transparent, when the alpha component has been left out + ([#1526][1526]). +* The `foot` process now changes CWD to `/` after spawning the shell + process. This ensures the terminal itself does not "lock" a + directory; for example, preventing a mount point from being + unmounted ([#1528][1528]). +* Kitty keyboard protocol: updated behavior of modifiers bits during + modifier key events, to match the (new [#6913][kitty-6913]) behavior + in kitty >= 0.32.0 ([#1561][1561]). +* When changing font sizes or display scales in floating windows, the + window will be resized as needed to preserve the same grid size. +* `smm` now disables private mode 1036 (_"send ESC when Meta modifies + a key"_), and enables private mode 1034 (_"8-bit Meta mode"_). `rmm` + does the opposite ([#1584][1584]). +* Grid is now always centered in the window, when either fullscreened + or maximized. +* Ctrl+wheel up/down bound to `font-increase` and `font-decrease` + respectively (in addition to the already existing default key + bindings `ctrl-+` and `ctrl+-`). +* Use XRGB pixel format (instead of ARGB) when there is no + transparency. +* Prefer CSS xcursor names, and fallback to legacy X11 names. +* Kitty keyboard protocol: use the `XKB` mode when retrieving locked + modifiers, instead of the `GTK` mode. This fixes an issue where some + key combinations (e.g. Shift+space) produces different results + depending on the state of e.g. the NumLock key. +* Kitty keyboard protocol: filter out **all** locked modifiers (as + reported by XKB), rather than hardcoding it to CapsLock only, when + determining whether a key combination produces text or not. +* CSI-t queries now report pixel values **unscaled**, instead of + **scaled** ([#1643][1643]). +* Sixel: text cursor is now placed on the last text row touched by the + sixel, instead of the text row touched by the _upper_ pixel of the + last sixel ([#chafa-192][chafa-192]). +* Sixel: trailing, fully transparent rows are now trimmed + ([#chafa-192][chafa-192]). +* `1004` (enable focus in/out events) removed from the `XM` terminfo + capability. To enable focus in/out, use the `fe` and `fd` + capabilities instead. +* Tightened the regular expression in the `rv` terminfo capability. +* Tightened the regular expression in the `xr` terminfo capability. +* `DECRQM` queries for private mode 67 ([`DECBKM`]) now reply with mode + value 4 ("permanently reset") instead of 0 ("not recognized"). + +[1526]: https://codeberg.org/dnkl/foot/issues/1526 +[1528]: https://codeberg.org/dnkl/foot/issues/1528 +[1561]: https://codeberg.org/dnkl/foot/issues/1561 +[kitty-6913]: https://github.com/kovidgoyal/kitty/issues/6913 +[1584]: https://codeberg.org/dnkl/foot/issues/1584 +[1643]: https://codeberg.org/dnkl/foot/issues/1643 +[chafa-192]: https://github.com/hpjansson/chafa/issues/192 + + +### Fixed + +* config: improved validation of color values. +* config: double close of file descriptor, resulting in a chain of + errors ultimately leading to a startup failure ([#1531][1531]). +* Crash when using a desktop scaling factor > 1, on compositors that + implements neither the `fractional-scale-v1`, nor the + `cursor-shape-v1` Wayland protocols ([#1573][1573]). +* Crash in `--server` mode when one or more environment variables are + set in `[environment]`. +* Environment variables normally set by foot lost with `footclient + -E,--client-environment` ([#1568][1568]). +* XDG toplevel protocol violation, by trying to set a title that + contains an invalid UTF-8 sequence ([#1552][1552]). +* Crash when erasing the scrollback, when scrollback history is + exactly 0 rows. This happens when `[scrollback].line = 0`, and the + window size (number of rows) is a power of two (i.e. 2, 4, 8, 16 + etc) ([#1610][1610]). +* VS16 (variation selector 16 - emoji representation) should only + affect emojis. +* Pressing a modifier key while the kitty keyboard protocol is enabled + no longer resets the viewport, or clears the selection. +* Crash when failing to load an xcursor image ([#1624][1624]). +* Crash when resizing a dynamically sized sixel (no raster + attributes), with a non-1:1 aspect ratio. +* The default sixel color table is now initialized to the colors used + by the VT340, instead of not being initialized at all (thus + requiring the sixel escape sequence to explicitly set all colors it + used). + +[1531]: https://codeberg.org/dnkl/foot/issues/1531 +[1573]: https://codeberg.org/dnkl/foot/issues/1573 +[1568]: https://codeberg.org/dnkl/foot/issues/1568 +[1552]: https://codeberg.org/dnkl/foot/issues/1552 +[1610]: https://codeberg.org/dnkl/foot/issues/1610 +[1624]: https://codeberg.org/dnkl/foot/issues/1624 + + +### Contributors + +* Alyssa Ross +* Andrew J. Hesford +* Artturin +* Craig Barnes +* delthas +* eugenrh +* Fazzi +* Gregory Anders +* Jan Palus +* Leonardo Hernández Hernández +* LmbMaxim +* Matheus Afonso Martins Moreira +* Sivecano +* Tim Culverhouse +* xnuk + + +## 1.16.2 + +### Fixed + +* Last row and/or column of opaque sixels (not having a size that is a + multiple of the cell size) being the wrong color ([#1520][1520]). + +[1520]: https://codeberg.org/dnkl/foot/issues/1520 + + +## 1.16.1 + +### Fixed + +* Foot not starting on linux kernels before 6.3 ([#1514][1514]). +* Cells underneath erased sixels not being repainted ([#1515][1515]). + +[1514]: https://codeberg.org/dnkl/foot/issues/1514 +[1515]: https://codeberg.org/dnkl/foot/issues/1515 + + +## 1.16.0 + +### Added + +* Support for building with _wayland-protocols_ as a subproject. +* Mouse wheel scrolls can now be used in `mouse-bindings` + ([#1077][1077]). +* New mouse bindings: `scrollback-up-mouse` and + `scrollback-down-mouse`, bound to `BTN_BACK` and `BTN_FORWARD` + respectively. +* New key binding: `select-quote`. This key binding selects text + between quote characters, and falls back to selecting the entire + row ([#1364][1364]). +* Support for DECSET/DECRST/DECRQM 2027 (_Grapheme cluster + processing_). +* New **search mode** key bindings (along with their defaults) + ([#419][419]): + - `extend-char` (shift+right) + - `extend-line-down` (shift+down) + - `extend-backward-char` (shift+left) + - `extend-backward-to-word-boundary` (ctrl+shift+left) + - `extend-backward-to-next-whitespace` (none) + - `extend-line-up` (shift+up) + - `scrollback-up-page` (shift+page-up) + - `scrollback-up-half-page` (none) + - `scrollback-up-line` (none) + - `scrollback-down-page` (shift+page-down) + - `scrollback-down-half-page` (none) + - `scrollback-down-line` (none) +* Support for visual bell which flashes the terminal window. + ([#1337][1337]). + +[1077]: https://codeberg.org/dnkl/foot/issues/1077 +[1364]: https://codeberg.org/dnkl/foot/issues/1364 +[419]: https://codeberg.org/dnkl/foot/issues/419 +[1337]: https://codeberg.org/dnkl/foot/issues/1337 + + +### Changed + +* Minimum required version of _wayland-protocols_ is now 1.32 + ([#1391][1391]). +* `foot-server.service` systemd now checks for + `ConditionEnvironment=WAYLAND_DISPLAY` for consistency with the + socket unit ([#1448][1448]) +* Default key binding for `select-row` is now `BTN_LEFT+4`. However, + in many cases, triple clicking will still be enough to select the + entire row; see the new key binding `select-quote` (mapped to + `BTN_LEFT+3` by default) ([#1364][1364]). +* `file://` prefix from URI's are no longer stripped when + opened/activated ([#1474][1474]). +* `XTGETTCAP` with capabilities that are not properly hex encoded will + be ignored, instead of echo:ed back to the TTY in an error response. +* Command line configuration overrides are now applied even if the + configuration file does not exist or can't be + parsed. ([#1495][1495]). +* Wayland surface damage is now more fine-grained. This should result + in lower latencies in many use cases, especially on high DPI + monitors. + +[1391]: https://codeberg.org/dnkl/foot/issues/1391 +[1448]: https://codeberg.org/dnkl/foot/pulls/1448 +[1474]: https://codeberg.org/dnkl/foot/pulls/1474 +[1495]: https://codeberg.org/dnkl/foot/pulls/1495 + + +### Removed + +* `utempter` config option (was deprecated in 1.15.0). + + +### Fixed + +* Race condition for systemd units start in GNOME and KDE + ([#1436][1436]). +* One frame being rendered at the wrong scale after being hidden by + another opaque, maximized window ([#1464][1464]). +* Double-width characters, and grapheme clusters breaking URL + auto-detection ([#1465][1465]). +* Crash when `XDG_ACTIVATION_TOKEN` is set, but compositor does not + support XDG activation ([#1493][1493]). +* Crash when compositor calls `fractional_scale::preferred_scale()` + when there are no monitors (for example, after a monitor has been + turned off and then back on again) ([#1498][1498]). +* Transparency in margins (padding) not being disabled in fullscreen + mode ([#1503][1503]). +* Crash when a scrollback search match is in the last column. +* Scrollback search: grapheme clusters not matching correctly. +* Wrong baseline offset for some fonts ([#1511][1511]). + +[1436]: https://codeberg.org/dnkl/foot/issues/1436 +[1464]: https://codeberg.org/dnkl/foot/issues/1464 +[1465]: https://codeberg.org/dnkl/foot/issues/1465 +[1493]: https://codeberg.org/dnkl/foot/pulls/1493 +[1498]: https://codeberg.org/dnkl/foot/issues/1498 +[1503]: https://codeberg.org/dnkl/foot/issues/1503 +[1511]: https://codeberg.org/dnkl/foot/issues/1511 + +### Contributors + +* 6t8k +* Alyssa Ross +* CismonX +* Max Gautier +* raggedmyth +* Raimund Sacherer +* Sertonix + + +## 1.15.3 + +### Fixed + +* `-f,--font` command line option not affecting `csd.font` (if unset). +* Vertical alignment in URL jump labels, and the scrollback position + indicator. The fix in 1.15.2 was incorrect, and was reverted in the + last minute. But we forgot to remove the entry from the changelog + ([#1430][1430]). + + +## 1.15.2 + +### Added + +* `[tweak].bold-text-in-bright-amount` option ([#1434][1434]). +* `-Dterminfo-base-name` meson option, allowing you to name the + terminfo files to something other than `-Ddefault-terminfo`. Use + case: have foot default to using the terminfo from ncurses (`foot`, + `foot-direct`), while still packaging foot's terminfo files, but + under a different name (e.g. `foot-extra`, `foot-extra-direct`). + +[1434]: https://codeberg.org/dnkl/foot/issues/1434 + + +### Fixed + +* Crash when copying text that contains invalid UTF-8 ([#1423][1423]). +* Wrong font size after suspending the monitor ([#1431][1431]). +* Vertical alignment in URL jump labels, and the scrollback position + indicator ([#1430][1430]). +* Regression: line- and box drawing characters not covering the full + height of the line, when a custom `line-height` is being used + ([#1430][1430]). +* Crash when compositor does not implement the _viewporter_ interface + ([#1444][1444]). +* CSD rendering with fractional scaling ([#1441][1441]). +* Regression: crash with certain combinations of + `--window-size-chars=NxM` and desktop scaling factors + ([#1446][1446]). + +[1423]: https://codeberg.org/dnkl/foot/issues/1423 +[1431]: https://codeberg.org/dnkl/foot/issues/1431 +[1430]: https://codeberg.org/dnkl/foot/issues/1430 +[1444]: https://codeberg.org/dnkl/foot/issues/1444 +[1441]: https://codeberg.org/dnkl/foot/issues/1441 +[1446]: https://codeberg.org/dnkl/foot/issues/1446 + + +## 1.15.1 + +### Changed + +* When window is mapped, use metadata (DPI, scaling factor, subpixel + configuration) from the monitor we were most recently mapped on, + instead of the one least recently. +* Starlight theme (the default theme) updated to [V4][starlight-v4] +* Background transparency (alpha) is now disabled in fullscreened + windows ([#1416][1416]). +* Foot server systemd units now use the standard + graphical-session.target ([#1281][1281]). +* If `$XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock` does not exist, + `footclient` now tries `$XDG_RUNTIME_DIR/foot.sock`, then + `/tmp/foot.sock`, even if `$WAYLAND_DISPLAY` and/or + `$XDG_RUNTIME_DIR` are defined ([#1281][1281]). +* Font baseline calculation: try to center the text within the line, + instead of anchoring it at the top ([#1302][1302]). + +[starlight-v4]: https://github.com/CosmicToast/starlight/blob/v4/CHANGELOG.md#v4 +[1416]: https://codeberg.org/dnkl/foot/issues/1416 +[1281]: https://codeberg.org/dnkl/foot/pulls/1281 +[1302]: https://codeberg.org/dnkl/foot/issues/1302 + + +### Fixed + +* Use appropriate rounding when applying fractional scales. +* Xcursor not being scaled correctly on `fractional-scale-v1` capable + compositors. +* `dpi-aware=yes` being broken on `fractional-scale-v1` capable + compositors (and when a fractional scaling factor is being used) + ([#1404][1404]). +* Initial font size being wrong on `fractional-scale-v1` capable + compositors, with multiple monitors with different scaling factors + connected ([#1404][1404]). +* Crash when _pointer capability_ is removed from a seat, on + compositors without `cursor-shape-v1 support` ([#1411][1411]). +* Crash on exit, if the mouse is hovering over the foot window (does + not happen on all compositors) +* Visual glitches when CSD titlebar is transparent. + +[1404]: https://codeberg.org/dnkl/foot/issues/1404 +[1411]: https://codeberg.org/dnkl/foot/pulls/1411 + + +### Contributors + +* Ayush Agarwal +* CismonX +* Max Gautier +* Ronan Pigott +* xdavidwu + + +## 1.15.0 + +### Added + +* VT: implemented `XTQMODKEYS` query (`CSI ? Pp m`). +* Meson option `utmp-backend=none|libutempter|ulog|auto`. The default + is `auto`, which will select `libutempter` on Linux, `ulog` on + FreeBSD, and `none` for all others. +* Sixel aspect ratio. +* Support for the new `fractional-scale-v1` Wayland protocol. This + brings true fractional scaling to Wayland in general, and with this + release, to foot. +* Support for the new `cursor-shape-v1` Wayland protocol, i.e. server + side cursor shapes ([#1379][1379]). +* Support for touchscreen input ([#517][517]). +* `csd.double-click-to-maximize` option to `foot.ini`. Defaults to + `yes` ([#1293][1293]). + +[1379]: https://codeberg.org/dnkl/foot/issues/1379 +[517]: https://codeberg.org/dnkl/foot/issues/517 +[1293]: https://codeberg.org/dnkl/foot/issues/1293 + + +### Changed + +* Default color theme is now + [starlight](https://github.com/CosmicToast/starlight) + ([#1321][1321]). +* Minimum required meson version is now 0.59 ([#1371][1371]). +* `Control+Shift+u` is now bound to `unicode-input` instead of + `show-urls-launch`, to follow the convention established in GTK and + Qt ([#1183][1183]). +* `show-urls-launch` now bound to `Control+Shift+o` ([#1183][1183]). +* Kitty keyboard protocol: F3 is now encoded as `CSI 13~` instead of + `CSI R`. The kitty keyboard protocol originally allowed F3 to be + encoded as `CSI R`, but this was removed from the specification + since `CSI R` conflicts with the _"Cursor Position Report"_. +* `[main].utempter` renamed to `[main].utmp-helper`. The old option + name is still recognized, but will log a deprecation warning. +* Meson option `default-utempter-path` renamed to + `utmp-default-helper-path`. +* Opaque sixels now retain the background opacity (when current + background color is the **default** background color) + ([#1360][1360]). +* Text cursor's vertical position after emitting a sixel, when sixel + scrolling is **enabled** (the default) has been updated to match + XTerm's, and the VT382's behavior: the cursor is positioned **on** + the last sixel row, rather than _after_ it. This allows printing + sixels on the last row without scrolling up, but also means + applications may have to explicitly emit a newline to ensure the + sixel is visible. For example, `cat`:ing a sixel in the shell will + typically result in the last row not being visible, unless a newline + is explicitly added. +* Default sixel aspect ratio is now 2:1 instead of 1:1. +* Sixel images are no longer cropped to the last non-transparent row. +* Sixel images are now re-scaled when the font size is changed + ([#1383][1383]). +* `dpi-aware` now defaults to `no`, and the `auto` value has been + removed. +* When using custom cursor colors (`cursor.color` is set in + `foot.ini`), the cursor is no longer inverted when the cell is + selected, or when the cell has the `reverse` (SGR 7) attribute set + ([#1347][1347]). + +[1321]: https://codeberg.org/dnkl/foot/issues/1321 +[1371]: https://codeberg.org/dnkl/foot/pulls/1371 +[1183]: https://codeberg.org/dnkl/foot/issues/1183 +[1360]: https://codeberg.org/dnkl/foot/issues/1360 +[1383]: https://codeberg.org/dnkl/foot/issues/1383 +[1347]: https://codeberg.org/dnkl/foot/issues/1347 + + +### Deprecated + +* `[main].utempter` option. + + +### Removed + +* `auto` value for the `dpi-aware` option. + + +### Fixed + +* Incorrect icon in dock and window switcher on Gnome ([#1317][1317]) +* Crash when scrolling after resizing the window with non-zero + scrolling regions. +* `XTMODKEYS` state not being reset on a terminal reset. +* In Gnome dock foot always groups under "foot client". Change + instances of footclient and foot to appear as "foot client" and + "foot" respectively. ([#1355][1355]). +* Glitchy rendering when alpha (transparency) is changed between + opaque and non-opaque at runtime (using OSC-11). +* Regression: crash when resizing the window when `resize-delay-ms > + 0` ([#1377][1377]). +* Crash when scrolling up while running something that generates a lot + of output (for example, `yes`) ([#1380][1380]). +* Default key binding for URL mode conflicting with Unicode input on + some DEs; `show-urls-launched` is now mapped to `Control+Shift+o` by + default, instead of `Control+Shift+u` ([#1183][1183]). + +[1317]: https://codeberg.org/dnkl/foot/issues/1317 +[1355]: https://codeberg.org/dnkl/foot/issues/1355 +[1377]: https://codeberg.org/dnkl/foot/issues/1377 +[1380]: https://codeberg.org/dnkl/foot/issues/1380 + + +### Contributors + +* Antoine Beaupré +* CismonX +* Craig Barnes +* Dan Bungert +* jdevdevdev +* Kyle Gunger +* locture +* Phillip Susi +* sewn +* ShugarSkull +* Vivian Szczepanski +* Vladimir Bauer +* wout +* CosmicToast + + +## 1.14.0 + +### Added + +* Support for adjusting the thickness of regular underlines ([#1136][1136]). +* Support (optional) for utmp logging with libutempter. +* `kxIN` and `kxOUT` (focus in/out events) to terminfo. +* `name` capability to `XTGETTCAP`. +* String values in `foot.ini` may now be quoted. This can be used to + set a value to the empty string, for example. +* Environment variables can now be **unset**, by setting + `[environment].=""` (quotes are required) ([#1225][1225]). +* `font-size-adjustment=N[px]` option, letting you configure how much + to increment/decrement the font size when zooming in or out + ([#1188][1188]). +* Bracketed paste terminfo entries (`BD`, `BE`, `PE` and `PS`, added + to ncurses in 2022-12-24). Vim makes use of these. +* "Report version" terminfo entries (`XR`/`xr`). +* "Report DA2" terminfo entries (`RV`/`rv`). +* `XF` terminfo capability (focus in/out events available). +* `$TERM_PROGRAM` and `$TERM_PROGRAM_VERSION` environment variables + unset in the slave process. + +[1136]: https://codeberg.org/dnkl/foot/issues/1136 +[1225]: https://codeberg.org/dnkl/foot/issues/1225 +[1188]: https://codeberg.org/dnkl/foot/issues/1188 + + +### Changed + +* Default color theme from a variant of the Zenburn theme, to a + variant of the Solarized dark theme. +* Default `pad` from 2x2 to 0x0 (i.e. no padding at all). +* Current working directory (as set by OSC-7) is now passed to the + program executed by the `pipe-*` key bindings ([#1166][1166]). +* `DECRPM` replies (to `DECRQM` queries) now report a value of `4` + ("permanently reset") instead of `2` ("reset") for DEC private + modes that are known but unsupported. +* Set `PWD` environment variable in the slave process ([#1179][1179]). +* DPI is now forced to 96 when found to be unreasonably high. +* Set default log level to warning ([#1215][1215]). +* Default `grapheme-width-method` from `wcswidth` to `double-width`. +* When determining initial font size, do FontConfig config + substitution if the user-provided font pattern has no {pixel}size + option ([#1287][1287]). +* DECRST of DECCOLM and DECSCLM removed from terminfo. + +[1166]: https://codeberg.org/dnkl/foot/issues/1166 +[1179]: https://codeberg.org/dnkl/foot/issues/1179 +[1215]: https://codeberg.org/dnkl/foot/pulls/1215 +[1287]: https://codeberg.org/dnkl/foot/issues/1287 + + +### Fixed + +* Crash in `foot --server` on key press, after another `footclient` + has terminated very early (for example, by trying to launch a + non-existing shell/client). +* Glitchy rendering when scrolling in the scrollback, on compositors + that does not allow Wayland buffer reuse (e.g. KDE/plasma) + ([#1173][1173]) +* Scrollback search matches not being highlighted correctly, on + compositors that does not allow Wayland buffer reuse + (e.g. KDE/plasma). +* Nanosecs "overflow" when calculating timeout value for + `resize-delay-ms` option. +* Missing backslash in ST terminator in escape sequences in the + built-in terminfo (accessed via XTGETTCAP). +* Crash when interactively resizing the window with a very large + scrollback. +* Crash when a sixel image exceeds the current sixel max height. +* Crash after reverse-scrolling (`CSI Ps T`) in the 'normal' + (non-alternate) screen ([#1190][1190]). +* Background transparency being applied to the text "behind" the + cursor. Only applies to block cursor using inversed fg/bg + colors. ([#1205][1205]). +* Crash when monitor's physical size is "too small" ([#1209][1209]). +* Line-height adjustment when incrementing/decrementing the font size + with a user-set line-height ([#1218][1218]). +* Scaling factor not being correctly applied when converting pt-or-px + config values (e.g. letter offsets, line height etc). +* Selection being stuck visually when `IL` and `DL`. +* URL underlines sometimes still being visible after exiting URL mode. +* Text-bindings, and pipe-* bindings, with multiple key mappings + causing a crash (double-free) on exit ([#1259][1259]). +* Double-width glyphs glitching when surrounded by glyphs overflowing + into the double-width glyph ([#1256][1256]). +* Wayland protocol violation when ack:ing a configure event for an + unmapped surface ([#1249][1249]). +* `xdg_toplevel::set_min_size()` not being called. +* Key bindings with consumed modifiers masking other key bindings + ([#1280][1280]). +* Multi-character compose sequences with the kitty keyboard protocol + ([#1288][1288]). +* Crash when application output scrolls very fast, e.g. `yes` + ([#1305][1305]). +* Crash when application scrolls **many** lines (> ~2³¹). +* DECCOLM erasing the screen ([#1265][1265]). + +[1173]: https://codeberg.org/dnkl/foot/issues/1173 +[1190]: https://codeberg.org/dnkl/foot/issues/1190 +[1205]: https://codeberg.org/dnkl/foot/issues/1205 +[1209]: https://codeberg.org/dnkl/foot/issues/1209 +[1218]: https://codeberg.org/dnkl/foot/issues/1218 +[1259]: https://codeberg.org/dnkl/foot/issues/1259 +[1256]: https://codeberg.org/dnkl/foot/issues/1256 +[1249]: https://codeberg.org/dnkl/foot/issues/1249 +[1280]: https://codeberg.org/dnkl/foot/issues/1280 +[1288]: https://codeberg.org/dnkl/foot/issues/1288 +[1305]: https://codeberg.org/dnkl/foot/issues/1305 +[1265]: https://codeberg.org/dnkl/foot/issues/1265 + + +### Contributors + +* Alexey Sakovets +* Andrea Pappacoda +* Antoine Beaupré +* argosatcore +* Craig Barnes +* EuCaue +* Grigory Kirillov +* Harri Nieminen +* Hugo Osvaldo Barrera +* jaroeichler +* Joakim Nohlgård +* Nick Hastings +* Soren A D +* Torsten Trautwein +* Vladimír Magyar +* woojiq +* Yorick Peterse + + +## 1.13.1 + +### Changed + +* Window is now dimmed while in Unicode input mode. + + +### Fixed + +* Compiling against wayland-protocols < 1.25 +* Crash on buggy compositors (GNOME) that sometimes send pointer-enter + events with a NULL surface. Foot now ignores these events, and the + subsequent motion and leave events. +* Regression: "random" selected empty cells being highlighted as + selected when they should not. +* Crash when either resizing the terminal window, or scrolling in the + scrollback history ([#1074][1074]) +* OSC-8 URLs with matching IDs, but mismatching URIs being incorrectly + connected. + +[1074]: https://codeberg.org/dnkl/foot/pulls/1074 + + +## 1.13.0 + +### Added + +* XDG activation support when opening URLs ([#1058][1058]). +* `-Dsystemd-units-dir=` meson command line option. +* Support for custom environment variables in `foot.ini` + ([#1070][1070]). +* Support for jumping to previous/next prompt (requires shell + integration). By default bound to `ctrl`+`shift`+`z` and + `ctrl`+`shift`+`x` respectively ([#30][30]). +* `colors.search-box-no-match` and `colors.search-box-match` options + to `foot.ini` ([#1112][1112]). +* Very basic Unicode input mode via the new + `key-bindings.unicode-input` and `search-bindings.unicode-input` key + bindings. Note that there is no visual feedback, as the preferred + way of entering Unicode characters is with an IME ([#1116][1116]). +* Support for `xdg_toplevel.wm_capabilities`, to adapt the client-side + decoration buttons to the compositor capabilities ([#1061][1061]). + +[1058]: https://codeberg.org/dnkl/foot/issues/1058 +[1070]: https://codeberg.org/dnkl/foot/issues/1070 +[30]: https://codeberg.org/dnkl/foot/issues/30 +[1112]: https://codeberg.org/dnkl/foot/issues/1112 +[1116]: https://codeberg.org/dnkl/foot/issues/1116 +[1061]: https://codeberg.org/dnkl/foot/pulls/1061 + + +### Changed + +* Use `$HOME` instead of `getpwuid()` to retrieve the user's home + directory when searching for `foot.ini`. +* HT, VT and FF are no longer stripped when pasting in non-bracketed + mode ([#1084][1084]). +* NUL is now stripped when pasting in non-bracketed mode + ([#1084][1084]). +* `alt`+`escape` now emits `\E\E` instead of a `CSI 27` sequence + ([#1105][1105]). + +[1084]: https://codeberg.org/dnkl/foot/issues/1084 +[1105]: https://codeberg.org/dnkl/foot/issues/1105 + + +### Fixed + +* Graphical corruption when viewport is at the top of the scrollback, + and the output is scrolling. +* Improved text reflow of logical lines with trailing empty cells + ([#1055][1055]) +* IME focus is now tracked independently from keyboard focus. +* Workaround for buggy compositors (e.g. some versions of GNOME) + allowing drag-and-drops even though foot has reported it does not + support the offered mime-types ([#1092][1092]). +* Keyboard enter/leave events being ignored if there is no keymap + ([#1097][1097]). +* Crash when application emitted an invalid `CSI 38;5;m`, `CSI + 38:5:m`, `CSI 48;5;m` or `CSI 48:5:m` sequence + ([#1111][1111]). +* Certain dead-key combinations resulting in different escape + sequences compared to kitty, when the kitty keyboard protocol is + used ([#1120][1120]). +* Search matches ending with a double-width character not being + highlighted correctly. +* Selection not being cancelled correctly when scrolled out. +* Extending a multi-page selection behaving inconsistently. +* Poor performance when making very large selections ([#1114][1114]). +* Bogus error message when using systemd socket activation for server + mode ([#1107][1107]) +* Empty line at the bottom after a window resize ([#1108][1108]). + +[1055]: https://codeberg.org/dnkl/foot/issues/1055 +[1092]: https://codeberg.org/dnkl/foot/issues/1092 +[1097]: https://codeberg.org/dnkl/foot/issues/1097 +[1111]: https://codeberg.org/dnkl/foot/issues/1111 +[1120]: https://codeberg.org/dnkl/foot/issues/1120 +[1114]: https://codeberg.org/dnkl/foot/issues/1114 +[1107]: https://codeberg.org/dnkl/foot/issues/1107 +[1108]: https://codeberg.org/dnkl/foot/issues/1108 + + +### Contributors + +* Craig Barnes +* Lorenz +* Max Gautier +* Simon Ser +* Stefan Prosiegel + + +## 1.12.1 + +### Added + +* Workaround for Sway bug [#6960][sway-6960]: scrollback search and + the OSC-555 ("flash") escape sequence leaves dimmed (search) and + yellow (flash) artifacts ([#1046][1046]). +* `Control+Shift+v` and `XF86Paste` have been added to the default set + of key bindings that paste from the clipboard into the scrollback + search buffer. This is in addition to the pre-existing `Control+v` + and `Control+y` bindings. + +[sway-6960]: https://github.com/swaywm/sway/issues/6960 +[1046]: https://codeberg.org/dnkl/foot/issues/1046 + + +### Changed + +* Scrollback search's `extend-to-word-boundary` no longer stops at + space-to-word boundaries, making selection extension feel more + natural. + + +### Fixed + +* build: missing symbols when linking the `pgo` helper binary. +* UI not refreshing when pasting something into the scrollback search + box, that does not result in a grid update (for example, when the + search criteria did not result in any matches) ([#1040][1040]). +* foot freezing in scrollback search mode, using 100% CPU + ([#1036][1036], [#1047][1047]). +* Crash when extending a selection to the next word boundary in + scrollback search mode ([#1036][1036]). +* Scrollback search mode not always highlighting all matches + correctly. +* Sixel options not being reset on hard resets (`\Ec`) + +[1040]: https://codeberg.org/dnkl/foot/issues/1040 +[1036]: https://codeberg.org/dnkl/foot/issues/1036 +[1047]: https://codeberg.org/dnkl/foot/issues/1047 + + +## 1.12.0 + +### Added + +* OSC-22 - set xcursor pointer. +* Add "xterm" as fallback cursor where "text" is not available. +* `[key-bindings].scrollback-home|end` options. +* Socket activation for `foot --server` and accompanying systemd unit + files +* Support for re-mapping input, i.e. mapping input to custom escape + sequences ([#325][325]). +* Support for [DECNKM](https://vt100.net/docs/vt510-rm/DECNKM.html), + which allows setting/saving/restoring/querying the keypad mode. +* Sixel support can be disabled by setting `[tweak].sixel=no` + ([#950][950]). +* footclient: `-E,--client-environment` command line option. When + used, the child process in the new terminal instance inherits the + environment from the footclient process instead of the server's + ([#1004][1004]). +* `[csd].hide-when-maximized=yes|no` option ([#1019][1019]). +* Scrollback search mode now highlights all matches. +* `[key-binding].show-urls-persistent` action. This key binding action + is similar to `show-urls-launch`, but does not automatically exit + URL mode after activating an URL ([#964][964]). +* Support for `CSI > 4 n`, disable _modifyOtherKeys_. Note that since + foot only supports level 1 and 2 (and not level 0), this sequence + does not disable _modifyOtherKeys_ completely, but simply reverts it + back to level 1 (the default). +* `-Dtests=false|true` meson command line option. When disabled, test + binaries will neither be built, nor will `ninja test` attempt to + execute them. Enabled by default ([#919][919]). + +[325]: https://codeberg.org/dnkl/foot/issues/325 +[950]: https://codeberg.org/dnkl/foot/issues/950 +[1004]: https://codeberg.org/dnkl/foot/issues/1004 +[1019]: https://codeberg.org/dnkl/foot/issues/1019 +[964]: https://codeberg.org/dnkl/foot/issues/964 +[919]: https://codeberg.org/dnkl/foot/issues/919 + + +### Changed + +* Minimum required meson version is now 0.58. +* Mouse selections are now finalized when the window is resized + ([#922][922]). +* OSC-4 and OSC-11 replies now uses four digits instead of 2 + ([#971][971]). +* `\r` is no longer translated to `\n` when pasting clipboard data + ([#980][980]). +* Use circles for rendering light arc box-drawing characters + ([#988][988]). +* Example configuration is now installed to + `${sysconfdir}/xdg/foot/foot.ini`, typically resolving to + `/etc/xdg/foot/foot.ini` ([#1001][1001]). + +[922]: https://codeberg.org/dnkl/foot/issues/922 +[971]: https://codeberg.org/dnkl/foot/issues/971 +[980]: https://codeberg.org/dnkl/foot/issues/980 +[988]: https://codeberg.org/dnkl/foot/issues/988 +[1001]: https://codeberg.org/dnkl/foot/issues/1001 + + +### Removed + +* DECSET mode 27127 (which was first added in release 1.6.0). + The kitty keyboard protocol (added in release 1.10.3) can + be used to similar effect. + + +### Fixed + +* Build: missing `wayland_client` dependency in `test-config` + ([#918][918]). +* "(null)" being logged as font-name (for some fonts) when warning + about a non-monospaced primary font. +* Rare crash when the window is resized while a mouse selection is + ongoing ([#922][922]). +* Large selections crossing the scrollback wrap-around ([#924][924]). +* Crash in `pipe-scrollback` ([#926][926]). +* Exit code being 0 when a foot server with no open windows terminate + due to e.g. a Wayland connection failure ([#943][943]). +* Key binding collisions not detected for bindings specified as option + overrides on the command line. +* Crash when seat has no keyboard ([#963][963]). +* Key presses with e.g. `AltGr` triggering key combinations with the + base symbol ([#983][983]). +* Underline cursor sometimes being positioned too low, either making + it look thinner than what it should be, or being completely + invisible ([#1005][1005]). +* Fallback to `/etc/xdg` if `XDG_CONFIG_DIRS` is unset + ([#1008][1008]). +* Improved compatibility with XTerm when `modifyOtherKeys=2` + ([#1009][1009]). +* Window geometry when CSDs are enabled and CSD border width set to a + non-zero value. This fixes window snapping in e.g. GNOME. +* Window size "jumping" when starting an interactive resize when CSDs + are enabled, and CSD border width set to a non-zero value. +* Key binding overrides on the command line having no effect with + `footclient` instances ([#931][931]). +* Search prev/next not updating the selection correctly when the + previous and new match overlaps. +* Various minor fixes to scrollback search, and how it finds the + next/prev match. + +[918]: https://codeberg.org/dnkl/foot/issues/918 +[922]: https://codeberg.org/dnkl/foot/issues/922 +[924]: https://codeberg.org/dnkl/foot/issues/924 +[926]: https://codeberg.org/dnkl/foot/issues/926 +[943]: https://codeberg.org/dnkl/foot/issues/943 +[963]: https://codeberg.org/dnkl/foot/issues/963 +[983]: https://codeberg.org/dnkl/foot/issues/983 +[1005]: https://codeberg.org/dnkl/foot/issues/1005 +[1008]: https://codeberg.org/dnkl/foot/issues/1008 +[1009]: https://codeberg.org/dnkl/foot/issues/1009 +[931]: https://codeberg.org/dnkl/foot/issues/931 + + +### Contributors + +* Ashish SHUKLA +* Craig Barnes +* Enes Hecan +* Johannes Altmanninger +* L3MON4D3 +* Leonardo Neumann +* Mariusz Bialonczyk +* Max Gautier +* Merlin Büge +* jvoisin +* merkix + + +## 1.11.0 + +### Added + +* `[mouse-bindings].selection-override-modifiers` option, specifying + which modifiers to hold to override mouse grabs by client + applications and force selection instead. +* _irc://_ and _ircs://_ to the default set of protocols recognized + when auto-detecting URLs. +* [SGR-Pixels (1016) mouse extended coordinates](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates) is now supported + ([#762](https://codeberg.org/dnkl/foot/issues/762)). +* `XTGETTCAP` - builtin terminfo. See + [README.md::XTGETTCAP](README.md#xtgettcap) for details + ([#846](https://codeberg.org/dnkl/foot/issues/846)). +* `DECRQSS` - _Request Selection or Setting_ + ([#798](https://codeberg.org/dnkl/foot/issues/798)). Implemented settings + are: + - `DECSTBM` - _Set Top and Bottom Margins_ + - `SGR` - _Set Graphic Rendition_ + - `DECSCUSR` - _Set Cursor Style_ +* Support for searching for the last searched-for string in scrollback + search (search for next/prev match with an empty search string). + + +### Changed + +* PaperColorDark and PaperColorLight themes renamed to + paper-color-dark and paper-color-light, for consistency with other + theme names. +* `[scrollback].multiplier` is now applied in "alternate scroll" mode, + where scroll events are translated to fake arrow key presses on the + alt screen ([#859](https://codeberg.org/dnkl/foot/issues/859)). +* The width of the block cursor's outline in an unfocused window is + now scaled by the output scaling factor ("desktop + scaling"). Previously, it was always 1px. +* Foot will now try to change the locale to either "C.UTF-8" or + "en_US.UTF-8" if started with a non-UTF8 locale. If this fails, foot + will start, but only to display a window with an error (user's shell + is not executed). +* `gettimeofday()` has been replaced with `clock_gettime()`, due to it being + marked as obsolete by POSIX. +* `alt+tab` now emits `ESC \t` instead of `CSI 27;3;9~` + ([#900](https://codeberg.org/dnkl/foot/issues/900)). +* File pasted, or dropped, on the alt screen is no longer quoted + ([#379](https://codeberg.org/dnkl/foot/issues/379)). +* Line-based selections now include a trailing newline when copied + ([#869](https://codeberg.org/dnkl/foot/issues/869)). +* Foot now clears the signal mask and resets all signal handlers to + their default handlers at startup + ([#854](https://codeberg.org/dnkl/foot/issues/854)). +* `Copy` and `Paste` keycodes are supported by default for the + clipboard. These are useful for keyboards with custom firmware like + QMK to enable global copy/paste shortcuts that work inside and + outside the terminal (https://codeberg.org/dnkl/foot/pulls/894). + + +### Removed + +* Workaround for slow resize in Sway <= 1.5, when a foot window was + hidden, for example, in a tabbed view + (https://codeberg.org/dnkl/foot/pulls/507). + + +### Fixed + +* Font size adjustment ("zooming") when font is configured with a + **pixelsize**, and `dpi-aware=no` + ([#842](https://codeberg.org/dnkl/foot/issues/842)). +* Key presses triggering keyboard layout switches also emitting CSI + codes in the Kitty keyboard protocol. +* Assertion in `shm.c:buffer_release()` + ([#844](https://codeberg.org/dnkl/foot/issues/844)). +* Crash when setting a key- or mouse binding to the empty string + ([#851](https://codeberg.org/dnkl/foot/issues/851)). +* Crash when maximizing the window and `[csd].size=1` + ([#857](https://codeberg.org/dnkl/foot/issues/857)). +* OSC-8 URIs not getting overwritten (erased) by double-width + characters (e.g. emojis). +* Rendering of CSD borders when `csd.border-width > 0` and desktop + scaling has been enabled. +* Failure to launch when `exec(3)`:ed with an empty argv. +* Pasting from the primary clipboard (mouse middle clicking) did not + reset the scrollback view to the bottom. +* Wrong mouse binding triggered when doing two mouse selections in + very quick (< 300ms) succession + ([#883](https://codeberg.org/dnkl/foot/issues/883)). +* Bash completion giving an error when completing a list of short + options +* Sixel: large image resizes (triggered by e.g. large repeat counts in + `DECGRI`) are now truncated instead of ignored. +* Sixel: a repeat count of 0 in `DECGRI` now emits a single sixel. +* LIGHT ARC box drawing characters incorrectly rendered + platforms ([#914](https://codeberg.org/dnkl/foot/issues/914)). + + +### Contributors + +* [lamonte](https://codeberg.org/lamonte) +* Érico Nogueira +* feeptr +* Felix Lechner +* grtcdr +* Mark Stosberg +* Nicolai Dagestad +* Oğuz Ersen +* Pranjal Kole +* Simon Ser + + +## 1.10.3 + +### Added + +* Kitty keyboard protocol ([#319](https://codeberg.org/dnkl/foot/issues/319)): + - [Report event types](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-events) + (mode `0b10`) + - [Report alternate keys](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-alternates) + (mode `0b100`) + - [Report all keys as escape codes](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-all-keys) + (mode `0b1000`) + - [Report associated text](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-text) + (mode `0b10000`) + + +### Fixed + +* Crash when bitmap fonts are scaled down to very small font sizes + ([#830](https://codeberg.org/dnkl/foot/issues/830)). +* Crash when overwriting/erasing an OSC-8 URL. + + +## 1.10.2 + +### Added + +* New value, `max`, for `[tweak].grapheme-width-method`. +* Initial support for the [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/). + Modes supported: + - [Disambiguate escape codes](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#disambiguate) (mode `0b1`) +* "Window menu" (compositor provided) on right clicks on the CSD title + bar. + + +### Fixed + +* An ongoing mouse selection is now finalized on a pointer leave event + (for example by switching workspace while doing a mouse selection). +* OSC-8 URIs in the last column +* OSC-8 URIs sometimes being applied to too many, and seemingly + unrelated cells ([#816](https://codeberg.org/dnkl/foot/issues/816)). +* OSC-8 URIs incorrectly being dropped when resizing the terminal + window with the alternate screen active. +* CSD border not being dimmed when window is not focused. +* Visual corruption with large CSD borders + ([#823](https://codeberg.org/dnkl/foot/issues/823)). +* Mouse cursor shape sometimes not being updated correctly. +* Color palette changes (via OSC 4/104) no longer affect RGB colors + ([#678](https://codeberg.org/dnkl/foot/issues/678)). + + +### Contributors + +* Jonas Ådahl + + +## 1.10.1 + +### Added + +* `-Dthemes=false|true` meson command line option. When disabled, + example theme files are **not** installed. +* XDG desktop file for footclient. + + +### Fixed + +* Regression: `letter-spacing` resulting in a "not a valid option" + error ([#795](https://codeberg.org/dnkl/foot/issues/795)). +* Regression: bad section name in configuration error messages. +* Regression: `pipe-*` key bindings not being parsed correctly, + resulting in invalid error messages + ([#809](https://codeberg.org/dnkl/foot/issues/809)). +* OSC-8 data not being cleared when cell is overwritten + ([#804](https://codeberg.org/dnkl/foot/issues/804), + [#801](https://codeberg.org/dnkl/foot/issues/801)). + + +### Contributors + +* Arnavion +* Craig Barnes +* Soc Virnyl Silab Estela +* Xiretza + + +## 1.10.0 + +### Added + +* `notify-focus-inhibit` boolean option, which can be used to control + whether desktop notifications should be inhibited when the terminal + has keyboard focus +* `[colors].scrollback-indicator` color-pair option, which specifies + foreground and background colors for the scrollback indicator. +* `[key-bindings].noop` action. Key combinations assigned to this + action will not be sent to the application + ([#765](https://codeberg.org/dnkl/foot/issues/765)). +* Color schemes are now installed to `${datadir}/foot/themes`. +* `[csd].border-width` and `[csd].border-color`, allowing you to + configure the width and color of the CSD border. +* Support for `XTMODKEYS` with `Pp=4` and `Pv=2` (_modifyOtherKeys=2_). +* `[colors].dim0-7` options, allowing you to configure custom "dim" + colors ([#776](https://codeberg.org/dnkl/foot/issues/776)). + + +### Changed + +* `[tweak].grapheme-shaping` is now enabled by default when both foot + itself, and fcft has been compiled with support for it. +* Default value of `[tweak].grapheme-width-method` changed from + `double-width` to `wcswidth`. +* INSTALL.md: `--override tweak.grapheme-shaping=no` added to PGO + command line. +* Foot now terminates if there are no available seats - for example, + due to the compositor not implementing a recent enough version of + the `wl_seat` interface ([#779](https://codeberg.org/dnkl/foot/issues/779)). +* Boolean options in `foot.ini` are now limited to + "yes|true|on|1|no|false|off|0", Previously, anything that did not + match "yes|true|on", or a number greater than 0, was treated as + "false". +* `[scrollback].multiplier` is no longer applied when the alternate + screen is in use ([#787](https://codeberg.org/dnkl/foot/issues/787)). + + +### Removed + +* The bundled PKGBUILD. +* Deprecated `bell` option (replaced with `[bell]` section in 1.8.0). +* Deprecated `url-launch`, `jump-label-letters` and `osc8-underline` + options (moved to a dedicated `[url]` section in 1.8.0) + + +### Fixed + +* 'Sticky' modifiers in input handling; when determining modifier + state, foot was looking at **depressed** modifiers, not + **effective** modifiers, like it should. +* Fix crashes after enabling CSD at runtime when `csd.size` is 0. +* Convert `\r` to `\n` when reading clipboard data + ([#752](https://codeberg.org/dnkl/foot/issues/752)). +* Clipboard occasionally ceasing to work, until window has been + re-focused ([#753](https://codeberg.org/dnkl/foot/issues/753)). +* Don't propagate window title updates to the Wayland compositor + unless the new title is different from the old title. + + +### Contributors + +* armin +* Craig Barnes +* Daniel Martí +* feeptr +* Mitja Horvat +* Ronan Pigott +* Stanislav Ochotnický + + +## 1.9.2 + +### Changed + +* PGO helper scripts no longer set `LC_CTYPE=en_US.UTF-8`. But, note + that "full" PGO builds still **require** a UTF-8 locale; you need + to set one manually in your build script + ([#728](https://codeberg.org/dnkl/foot/issues/728)). + + +## 1.9.1 + +### Added + +* Warn when it appears the primary font is not monospaced. Can be + disabled by setting `[tweak].font-monospace-warn=no` + ([#704](https://codeberg.org/dnkl/foot/issues/704)). +* PGO build scripts, in the `pgo` directory. See INSTALL.md - + _Performance optimized, PGO_, for details + ([#701](https://codeberg.org/dnkl/foot/issues/701)). +* Braille characters (U+2800 - U+28FF) are now rendered by foot + itself ([#702](https://codeberg.org/dnkl/foot/issues/702)). +* `-e` command-line option. This option is simply ignored, to appease + program launchers that blindly pass `-e` to any terminal emulator + ([#184](https://codeberg.org/dnkl/foot/issues/184)). + + +### Changed + +* `-Ddefault-terminfo` is now also applied to the generated terminfo + definitions when `-Dterminfo=enabled`. +* `-Dcustom-terminfo-install-location` no longer accepts `no` as a + special value, to disable exporting `TERMINFO`. To achieve the same + result, simply don't set it at all. If it _is_ set, `TERMINFO` is + still exported, like before. +* The default install location for the terminfo definitions have been + changed back to `${datadir}/terminfo`. +* `dpi-aware=auto`: fonts are now scaled using the monitor's DPI only + when **all** monitors have a scaling factor of one + ([#714](https://codeberg.org/dnkl/foot/issues/714)). +* fcft >= 3.0.0 in now required. + + +### Fixed + +* Added workaround for GNOME bug where multiple button press events + (for the same button) is sent to the CSDs without any release or + leave events in between ([#709](https://codeberg.org/dnkl/foot/issues/709)). +* Line-wise selection not taking soft line-wrapping into account + ([#726](https://codeberg.org/dnkl/foot/issues/726)). + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) +* Arnavion + + +## 1.9.0 + +### Added + +* Window title in the CSDs + ([#638](https://codeberg.org/dnkl/foot/issues/638)). +* `-Ddocs=disabled|enabled|auto` meson command line option. +* Support for `~`-expansion in the `include` directive + ([#659](https://codeberg.org/dnkl/foot/issues/659)). +* Unicode 13 characters U+1FB3C - U+1FB6F, U+1FB9A and U+1FB9B to list + of box drawing characters rendered by foot itself (rather than using + font glyphs) ([#474](https://codeberg.org/dnkl/foot/issues/474)). +* `XM`+`xm` to terminfo. +* Mouse buttons 6/7 (mouse wheel left/right). +* `url.uri-characters` option to `foot.ini` + ([#654](https://codeberg.org/dnkl/foot/issues/654)). + + +### Changed + +* Terminfo files can now co-exist with the foot terminfo files from + ncurses. See `INSTALL.md` for more information + ([#671](https://codeberg.org/dnkl/foot/issues/671)). +* `bold-text-in-bright=palette-based` now only brightens colors from palette +* Raised grace period between closing the PTY and sending `SIGKILL` (when + terminating the client application) from 4 to 60 seconds. +* When terminating the client application, foot now sends `SIGTERM` immediately + after closing the PTY, instead of waiting 2 seconds. +* Foot now sends `SIGTERM`/`SIGKILL` to the client application's process group, + instead of just to the client application's process. +* `kmous` terminfo capability from `\E[M` to `\E[<`. +* pt-or-px values (`letter-spacing`, etc) and the line thickness + (`tweak.box-drawing-base-thickness`) in box drawing characters are + now translated to pixel values using the monitor's scaling factor + when `dpi-aware=no`, or `dpi-aware=auto` and the scaling factor is + larger than 1 ([#680](https://codeberg.org/dnkl/foot/issues/680)). +* Spawning a new terminal with a working directory that does not exist + is no longer a fatal error. + + +### Removed + +* `km`/`smm`/`rmm` from terminfo; foot prefixes Alt-key combinations + with `ESC`, and not by setting the 8:th "meta" bit, regardless of + `smm`/`rmm`. While this _can_ be disabled by, resetting private mode + 1036, the terminfo should reflect the **default** behavior + ([#670](https://codeberg.org/dnkl/foot/issues/670)). +* Keypad application mode keys from terminfo; enabling the keypad + application mode is not enough to make foot emit these sequences - + you also need to disable private mode 1035 + ([#670](https://codeberg.org/dnkl/foot/issues/670)). + + +### Fixed + +* Rendering into the right margin area with `tweak.overflowing-glyphs` + enabled. +* PGO builds with clang ([#642](https://codeberg.org/dnkl/foot/issues/642)). +* Crash in scrollback search mode when selection has been canceled due + to terminal content updates + ([#644](https://codeberg.org/dnkl/foot/issues/644)). +* Foot process not terminating when the Wayland connection is broken + ([#651](https://codeberg.org/dnkl/foot/issues/651)). +* Output scale being zero on compositors that does not advertise a + scaling factor. +* Slow-to-terminate client applications causing other footclient instances to + freeze when closing a footclient window. +* Underlying cell content showing through in the left-most column of + sixels. +* `cursor.blink` not working in GNOME + ([#686](https://codeberg.org/dnkl/foot/issues/686)). +* Blinking cursor stops blinking, or becoming invisible, when + switching focus from, and then back to a terminal window on GNOME + ([#686](https://codeberg.org/dnkl/foot/issues/686)). + + +### Contributors + +* Nihal Jere +* [nowrep](https://codeberg.org/nowrep) +* [clktmr](https://codeberg.org/clktmr) + + +## 1.8.2 + +### Added + +* `locked-title=no|yes` to `foot.ini` + ([#386](https://codeberg.org/dnkl/foot/issues/386)). +* `tweak.overflowing-glyphs` option, which can be enabled to fix rendering + issues with glyphs of any width that appear cut-off + ([#592](https://codeberg.org/dnkl/foot/issues/592)). + + +### Changed + +* Non-empty lines are now considered to have a hard linebreak, + _unless_ an actual word-wrap is inserted. +* Setting `DECSDM` now _disables_ sixel scrolling, while resetting it + _enables_ scrolling ([#631](https://codeberg.org/dnkl/foot/issues/631)). + + +### Removed + +* The `tweak.allow-overflowing-double-width-glyphs` and + `tweak.pua-double-width` options (which have been superseded by + `tweak.overflowing-glyphs`). + + +### Fixed + +* FD exhaustion when repeatedly entering/exiting URL mode with many + URLs. +* Double free of URL while removing duplicated and/or overlapping URLs + in URL mode ([#627](https://codeberg.org/dnkl/foot/issues/627)). +* Crash when an unclosed OSC-8 URL ran into un-allocated scrollback + rows. +* Some box-drawing characters were rendered incorrectly on big-endian + architectures. +* Crash when resizing the window to the smallest possible size while + scrollback search is active. +* Scrollback indicator being incorrectly rendered when window size is + very small. +* Reduced memory usage in URL mode. +* Crash when the `E3` escape (`\E[3J`) was executed, and there was a + selection, or sixel image, in the scrollback + ([#633](https://codeberg.org/dnkl/foot/issues/633)). + + +### Contributors + +* [clktmr](https://codeberg.org/clktmr) + + +## 1.8.1 + +### Added + +* `--log-level=none` command-line option. +* `Tc`, `setrgbf` and `setrgbb` capabilities in `foot` and `foot-direct` + terminfo entries. This should make 24-bit RGB colors work in tmux and + neovim, without the need for config hacks or detection heuristics + ([#615](https://codeberg.org/dnkl/foot/issues/615)). + + +### Changed + +* Grapheme cluster width is now limited to two cells by default. This + may cause cursor synchronization issues with many applications. You + can set `[tweak].grapheme-width-method=wcswidth` to revert to the + behavior in foot-1.8.0. + + +### Fixed + +* Grapheme cluster state being reset between codepoints. +* Regression: custom URL key bindings not working + ([#614](https://codeberg.org/dnkl/foot/issues/614)). + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) + + +## 1.8.0 + +### Grapheme shaping + +This release adds _experimental, opt-in_ support for grapheme cluster +segmentation and grapheme shaping. + +(note: several of the examples below may not render correctly in your +browser, viewer or editor). + +Grapheme cluster segmentation is the art of splitting up text into +grapheme clusters, where a cluster may consist of more than one +Unicode codepoint. For example, 🙂 is a single codepoint, while 👩🏽‍🚀 +consists of 4 codepoints (_Woman_ + _Medium skin tone_ + _Zero width +joiner_ + _Rocket_). The goal is to _cluster_ codepoints belonging to +the same grapheme in the same cell in the terminal. + +Previous versions of foot implemented a simple grapheme cluster +segmentation technique that **only** handled zero-width +codepoints. This allowed us to cluster combining characters, like q́ +(_q_ + _COMBINING ACUTE ACCENT_). + +Once we have a grapheme cluster, we need to _shape_ it. + +Combining characters are simple: they are typically rendered as +multiple glyphs layered on top of each other. This is why previous +versions of foot got away with it without any actual text shaping +support. + +Beyond that, support from the font library is needed. Foot now depends +on fcft-2.4, which added support for grapheme and text shaping. When +rendering a cell, we ask the font library: give us the glyph(s) for +this sequence of codepoints. + +Fancy emoji sequences aside, using libutf8proc for grapheme cluster +segmentation means **improved correctness**. + +For full support, the following is required: + +* fcft compiled with HarfBuzz support +* foot compiled with libutf8proc support +* `tweak.grapheme-shaping=yes` in `foot.ini` + +If `tweak.grapheme-shaping` has **not** been enabled, foot will +neither use libutf8proc to do grapheme cluster segmentation, nor will +it use fcft's grapheme shaping capabilities to shape combining +characters. + +This feature is _experimental_ mostly due to the "wcwidth" problem; +how many cells should foot allocate for a grapheme cluster? While the +answer may seem simple, the problem is that, whatever the answer is, +the client application **must** come up with the **same** +answer. Otherwise we get cursor synchronization issues. + +In this release, foot simply adds together the `wcwidth()` of all +codepoints in the grapheme cluster. This is equivalent to running +`wcswidth()` on the entire cluster. **This is likely to change in the +future**. + +Finally, note that grapheme shaping is not the same thing as text (or +text run) shaping. In this version, foot only shapes individual +graphemes, not entire text runs. That means e.g. ligatures are **not** +supported. + + +### Added + +* Support for DECSET/DECRST 2026, as an alternative to the existing + "synchronized updates" DCS sequences + ([#459](https://codeberg.org/dnkl/foot/issues/459)). +* `cursor.beam-thickness` option to `foot.ini` + ([#464](https://codeberg.org/dnkl/foot/issues/464)). +* `cursor.underline-thickness` option to `foot.ini` + ([#524](https://codeberg.org/dnkl/foot/issues/524)). +* Unicode 13 characters U+1FB70 - U+1FB8B to list of box drawing + characters rendered by foot itself (rather than using font glyphs) + ([#471](https://codeberg.org/dnkl/foot/issues/471)). +* Dedicated `[bell]` section to config, supporting multiple actions + and a new `command` action to run an arbitrary command. + (https://codeberg.org/dnkl/foot/pulls/483) +* Dedicated `[url]` section to config. +* `[url].protocols` option to `foot.ini` + ([#531](https://codeberg.org/dnkl/foot/issues/531)). +* Support for setting the full 256 color palette in foot.ini + ([#489](https://codeberg.org/dnkl/foot/issues/489)) +* XDG activation support, will be used by `[bell].urgent` when + available (falling back to coloring the window margins red when + unavailable) ([#487](https://codeberg.org/dnkl/foot/issues/487)). +* `ctrl`+`c` as a default key binding; to cancel search/url mode. +* `${window-title}` to `notify`. +* Support for including files in `foot.ini` + ([#555](https://codeberg.org/dnkl/foot/issues/555)). +* `ENVIRONMENT` section in **foot**(1) and **footclient**(1) man pages + ([#556](https://codeberg.org/dnkl/foot/issues/556)). +* `tweak.pua-double-width` option to `foot.ini`, letting you force + _Private Usage Area_ codepoints to be treated as double-width + characters. +* OSC 9 desktop notifications (iTerm2 compatible). +* Support for LS2 and LS3 (locking shift) escape sequences + ([#581](https://codeberg.org/dnkl/foot/issues/581)). +* Support for overriding configuration options on the command line + ([#554](https://codeberg.org/dnkl/foot/issues/554), + [#600](https://codeberg.org/dnkl/foot/issues/600)). +* `underline-offset` option to `foot.ini` + ([#490](https://codeberg.org/dnkl/foot/issues/490)). +* `csd.button-color` option to `foot.ini`. +* `-Dterminfo-install-location=disabled|` meson command + line option ([#569](https://codeberg.org/dnkl/foot/issues/569)). + + +### Changed + +* [fcft](https://codeberg.org/dnkl/fcft): required version bumped from + 2.3.x to 2.4.x. +* `generate-alt-random-writes.py --sixel`: width and height of emitted + sixels has been adjusted. +* _Concealed_ text (`\E[8m`) is now revealed when highlighted. +* The background color of highlighted text is now adjusted, when the + foreground and background colors are the same, making the + highlighted text legible + ([#455](https://codeberg.org/dnkl/foot/issues/455)). +* `cursor.style=bar` to `cursor.style=beam`. `bar` remains a + recognized value, but will eventually be deprecated, and removed. +* Point values in `line-height`, `letter-spacing`, + `horizontal-letter-offset` and `vertical-letter-offset` are now + rounded, not truncated, when translated to pixel values. +* Foot's exit code is now -26/230 when foot itself failed to launch + (due to invalid command line options, client application/shell not + found etc). Footclient's exit code is -36/220 when it itself fails + to launch (e.g. bad command line option) and -26/230 when the foot + server failed to instantiate a new window + ([#466](https://codeberg.org/dnkl/foot/issues/466)). +* Background alpha no longer applied to palette or RGB colors that + matches the background color. +* Improved performance on compositors that does not release shm + buffers immediately, e.g. KWin + ([#478](https://codeberg.org/dnkl/foot/issues/478)). +* `ctrl + w` (_extend-to-word-boundary_) can now be used across lines + ([#421](https://codeberg.org/dnkl/foot/issues/421)). +* Ignore auto-detected URLs that overlap with OSC-8 URLs. +* Default value for the `notify` option to use `-a ${app-id} -i + ${app-id} ...` instead of `-a foot -i foot ...`. +* `scrollback-*`+`pipe-scrollback` key bindings are now passed through + to the client application when the alt screen is active + ([#573](https://codeberg.org/dnkl/foot/issues/573)). +* Reverse video (`\E[?5h`) now only swaps the default foreground and + background colors. Cells with explicit foreground and/or background + colors remain unchanged. +* Tabs (`\t`) are now preserved when the window is resized, and when + copying text ([#508](https://codeberg.org/dnkl/foot/issues/508)). +* Writing a sixel on top of another sixel no longer erases the first + sixel, but the two are instead blended + ([#562](https://codeberg.org/dnkl/foot/issues/562)). +* Running foot without a configuration file is no longer an error; it + has been demoted to a warning, and is no longer presented as a + notification in the terminal window, but only logged on stderr. + + +### Deprecated + +* `bell` option in `foot.ini`; set actions in the `[bell]` section + instead. +* `url-launch` option in `foot.ini`; use `launch` in the `[url]` + section instead. +* `jump-label-letters` option in `foot.ini`; use `label-letters` in + the `[url]` section instead. +* `osc8-underline` option in `foot.ini`; use `osc8-underline` in the + `[url]` section instead. + + +### Removed + +* Buffer damage quirk for Plasma/KWin. + + +### Fixed + +* `generate-alt-random-writes.py --sixel` sometimes crashing, + resulting in PGO build failures. +* Wrong colors in the 256-color cube + ([#479](https://codeberg.org/dnkl/foot/issues/479)). +* Memory leak triggered by "opening" an OSC-8 URI and then resetting + the terminal without closing the URI + ([#495](https://codeberg.org/dnkl/foot/issues/495)). +* Assertion when emitting a sixel occupying the entire scrollback + history ([#494](https://codeberg.org/dnkl/foot/issues/494)). +* Font underlines being positioned below the cell (and thus being + invisible) for certain combinations of fonts and font sizes + ([#503](https://codeberg.org/dnkl/foot/issues/503)). +* Sixels with transparent bottom border being resized below the size + specified in _"Set Raster Attributes"_. +* Fonts sometimes not being reloaded with the correct scaling factor + when `dpi-aware=no`, or `dpi-aware=auto` with monitor(s) with a + scaling factor > 1 ([#509](https://codeberg.org/dnkl/foot/issues/509)). +* Crash caused by certain CSI sequences with very large parameter + values ([#522](https://codeberg.org/dnkl/foot/issues/522)). +* Rare occurrences where the window did not close when the shell + exited. Only seen on FreeBSD + ([#534](https://codeberg.org/dnkl/foot/issues/534)) +* Foot process(es) sometimes remaining, using 100% CPU, when closing + multiple foot windows at the same time + ([#542](https://codeberg.org/dnkl/foot/issues/542)). +* Regression where `+shift+tab` always produced `\E[Z` instead of + the correct `\E[27;;9~` sequence + ([#547](https://codeberg.org/dnkl/foot/issues/547)). +* Crash when a line wrapping OSC-8 URI crossed the scrollback wrap + around ([#552](https://codeberg.org/dnkl/foot/issues/552)). +* Selection incorrectly wrapping rows ending with an explicit newline + ([#565](https://codeberg.org/dnkl/foot/issues/565)). +* Off-by-one error in markup of auto-detected URLs when the URL ends + in the right-most column. +* Multi-column characters being cut in half when resizing the + alternate screen. +* Restore `SIGHUP` in spawned processes. +* Text reflow performance ([#504](https://codeberg.org/dnkl/foot/issues/504)). +* IL+DL (`CSI Ps L` + `CSI Ps M`) now moves the cursor to column 0. +* SS2 and SS3 (single shift) escape sequences behaving like locking + shifts ([#580](https://codeberg.org/dnkl/foot/issues/580)). +* `TEXT`+`STRING`+`UTF8_STRING` mime types not being recognized in + clipboard offers ([#583](https://codeberg.org/dnkl/foot/issues/583)). +* Memory leak caused by custom box drawing glyphs not being completely + freed when destroying a foot window instance + ([#586](https://codeberg.org/dnkl/foot/issues/586)). +* Crash in scrollback search when current XKB layout is missing + _compose_ definitions. +* Window title not being updated while window is hidden + ([#591](https://codeberg.org/dnkl/foot/issues/591)). +* Crash on badly formatted URIs in e.g. OSC-8 URLs. +* Window being incorrectly resized on CSD/SSD run-time changes. + + +### Contributors +* [r\_c\_f](https://codeberg.org/r_c_f) +* [craigbarnes](https://codeberg.org/craigbarnes) + + +## 1.7.2 + +### Added + +* URxvt OSC-11 extension to set background alpha + ([#436](https://codeberg.org/dnkl/foot/issues/436)). +* OSC 17/117/19/119 - change/reset selection background/foreground + color. +* `box-drawings-uses-font-glyphs=yes|no` option to `foot.ini` + ([#430](https://codeberg.org/dnkl/foot/issues/430)). + + +### Changed + +* Underline cursor is now rendered below text underline + ([#415](https://codeberg.org/dnkl/foot/issues/415)). +* Foot now tries much harder to keep URL jump labels inside the window + geometry ([#443](https://codeberg.org/dnkl/foot/issues/443)). +* `bold-text-in-bright` may now be set to `palette-based`, in which + case it will use the corresponding bright palette color when the + color to brighten matches one of the base 8 colors, instead of + increasing the luminance + ([#449](https://codeberg.org/dnkl/foot/issues/449)). + + +### Fixed + +* Reverted _"Consumed modifiers are no longer sent to the client + application"_ ([#425](https://codeberg.org/dnkl/foot/issues/425)). +* Crash caused by a double free originating in `XTSMGRAPHICS` - set + number of color registers + ([#427](https://codeberg.org/dnkl/foot/issues/427)). +* Wrong action referenced in error message for key binding collisions + ([#432](https://codeberg.org/dnkl/foot/issues/432)). +* OSC 4/104 out-of-bounds accesses to the color table. This was the + reason pywal turned foot windows transparent + ([#434](https://codeberg.org/dnkl/foot/issues/434)). +* PTY not being drained when the client application terminates. +* `auto_left_margin` not being limited to `cub1` + ([#441](https://codeberg.org/dnkl/foot/issues/441)). +* Crash in scrollback search mode when searching beyond the last output. + + +### Contributors + +* [cglogic](https://codeberg.org/cglogic) + + +## 1.7.1 + +### Changed + +* Update PGO build instructions in `INSTALL.md` + ([#418](https://codeberg.org/dnkl/foot/issues/418)). +* In scrollback search mode, empty cells can now be matched by spaces. + + +### Fixed + +* Logic that repairs invalid key bindings ended up breaking valid key + bindings instead ([#407](https://codeberg.org/dnkl/foot/issues/407)). +* Custom `line-height` settings now scale when increasing or + decreasing the font size at run-time. +* Newlines sometimes incorrectly inserted into copied text + ([#410](https://codeberg.org/dnkl/foot/issues/410)). +* Crash when compositor send `text-input-v3::enter` events without + first having sent a `keyboard::enter` event + ([#411](https://codeberg.org/dnkl/foot/issues/411)). +* Deadlock when rendering sixel images. +* URL labels, scrollback search box or scrollback position indicator + sometimes not showing up, caused by invalidly sized surface buffers + when output scaling was enabled + ([#409](https://codeberg.org/dnkl/foot/issues/409)). +* Empty sixels resulted in non-empty images. + + +## 1.7.0 + +### Added + +* The `pad` option now accepts an optional third argument, `center` + (e.g. `pad=5x5 center`), causing the grid to be centered in the + window, with equal amount of padding of the left/right and + top/bottom side ([#273](https://codeberg.org/dnkl/foot/issues/273)). +* `line-height`, `letter-spacing`, `horizontal-letter-offset` and + `vertical-letter-offset` to `foot.ini`. These options let you tweak + cell size and glyph positioning + ([#244](https://codeberg.org/dnkl/foot/issues/244)). +* Key/mouse binding `select-extend-character-wise`, which forces the + selection mode to 'character-wise' when extending a selection. +* `DECSET` `47`, `1047` and `1048`. +* URL detection and OSC-8 support. URLs are highlighted and activated + using the keyboard (**no** mouse support). See **foot**(1)::URLs, or + [README.md](README.md#urls) for details + ([#14](https://codeberg.org/dnkl/foot/issues/14)). +* `-d,--log-level={info|warning|error}` to both `foot` and + `footclient` ([#337](https://codeberg.org/dnkl/foot/issues/337)). +* `-D,--working-directory=DIR` to both `foot` and `footclient` + ([#347](https://codeberg.org/dnkl/foot/issues/347)) +* `DECSET 80` - sixel scrolling + ([#361](https://codeberg.org/dnkl/foot/issues/361)). +* `DECSET 1070` - sixel private color palette + ([#362](https://codeberg.org/dnkl/foot/issues/362)). +* `DECSET 8452` - position cursor to the right of sixels + ([#363](https://codeberg.org/dnkl/foot/issues/363)). +* Man page **foot-ctlseqs**(7), documenting all supported escape + sequences ([#235](https://codeberg.org/dnkl/foot/issues/235)). +* Support for transparent sixels (DCS parameter `P2=1`) + ([#391](https://codeberg.org/dnkl/foot/issues/391)). +* `-N,--no-wait` to `footclient` + ([#395](https://codeberg.org/dnkl/foot/issues/395)). +* Completions for Bash shell + ([#10](https://codeberg.org/dnkl/foot/issues/10)). +* Implement `XTVERSION` (`CSI > 0q`). Foot will reply with + `DCS>|foot(..)ST` + ([#359](https://codeberg.org/dnkl/foot/issues/359)). + + +### Changed + +* The fcft and tllist library subprojects are now handled via Meson + [wrap files](https://mesonbuild.com/Wrap-dependency-system-manual.html) + instead of needing to be manually cloned. +* Box drawing characters are now rendered by foot, instead of using + font glyphs ([#198](https://codeberg.org/dnkl/foot/issues/198)) +* Double- or triple clicking then dragging now extends the selection + word- or line-wise ([#267](https://codeberg.org/dnkl/foot/issues/267)). +* The line thickness of box drawing characters now depend on the font + size ([#281](https://codeberg.org/dnkl/foot/issues/281)). +* Extending a word/line-wise selection now uses the original selection + mode instead of switching to character-wise. +* While doing an interactive resize of a foot window, foot now + requires 100ms of idle time (where the window size does not change) + before sending the new dimensions to the client application. The + timing can be tweaked, or completely disabled, by setting + `resize-delay-ms` ([#301](https://codeberg.org/dnkl/foot/issues/301)). +* `CSI 13 ; 2 t` now reports (0,0). +* Key binding matching logic; key combinations like `Control+Shift+C` + **must** now be written as either `Control+C` or `Control+Shift+c`, + the latter being the preferred + variant. ([#376](https://codeberg.org/dnkl/foot/issues/376)) +* Consumed modifiers are no longer sent to the client application + ([#376](https://codeberg.org/dnkl/foot/issues/376)). +* The minimum version requirement for the libxkbcommon dependency is + now 1.0.0. +* Empty pixel rows at the bottom of a sixel is now trimmed. +* Sixels with DCS parameter `P2=0|2` now use the _current_ ANSI + background color for empty pixels instead of the default background + color ([#391](https://codeberg.org/dnkl/foot/issues/391)). +* Sixel decoding optimized; up to 100% faster in some cases. +* Reported sixel "max geometry" from current window size, to the + configured maximum size (defaulting to 10000x10000). + + +### Removed + +* The `-g,--geometry` command-line option (which had been deprecated + and superseded by `-w,--window-size-pixels` since 1.5.0). + + +### Fixed + +* Some mouse bindings (_primary paste_, for example) did not require + `shift` to be pressed while used in a mouse grabbing + application. This meant the mouse event was never seen by the + application. +* Terminals spawned with `ctrl`+`shift`+`n` not terminating when + exiting shell ([#366](https://codeberg.org/dnkl/foot/issues/366)). +* Default value of `-t,--term` in `--help` output when foot was built + without terminfo support. +* Drain PTY when the client application terminates. + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) +* toast +* [l3mon4d3](https://codeberg.org/l3mon4d3) +* [Simon Schricker](mailto:s.schricker@sillage.at) + + +## 1.6.4 + +### Added + +* `selection-target=none|primary|clipboard|both` to `foot.ini`. It can + be used to configure which clipboard(s) selected text should be + copied to. The default is `primary`, which corresponds to the + behavior in older foot releases + ([#288](https://codeberg.org/dnkl/foot/issues/288)). + + +### Changed + +* The IME state no longer stays stuck in the terminal if the IME goes + away during preedit. +* `-Dterminfo` changed from a `boolean` to a `feature` option. +* Use standard signals instead of a signalfd to handle + `SIGCHLD`. Fixes an issue on FreeBSD where foot did not detect when + the client application had terminated. + + +### Fixed + +* `BS`, `HT` and `DEL` from being stripped in bracketed paste mode. + + +### Contributors + +* [tdeo](https://codeberg.org/tdeo) +* jbeich + + +## 1.6.3 + +### Added + +* Completions for fish shell + ([#11](https://codeberg.org/dnkl/foot/issues/11)) +* FreeBSD support ([#238](https://codeberg.org/dnkl/foot/issues/238)). +* IME popup location support: foot now sends the location of the cursor + so any popup can be displayed near the text that is being typed. + + +### Changed + +* Trailing comments in `foot.ini` must now be preceded by a space or tab + ([#270](https://codeberg.org/dnkl/foot/issues/270)) +* The scrollback search box no longer accepts non-printable characters. +* Non-formatting C0 control characters, `BS`, `HT` and `DEL` are now + stripped from pasted text. + + +### Fixed + +* Exit when the client application terminates, not when the TTY file + descriptor is closed. +* Crash on compositors not implementing the _text input_ interface + ([#259](https://codeberg.org/dnkl/foot/issues/259)). +* Erased, overflowing glyphs (when + `tweak.allow-overflowing-double-width-glyphs=yes` - the default) not + properly erasing the cell overflowed **into**. +* `word-delimiters` option ignores `#` and subsequent characters + ([#270](https://codeberg.org/dnkl/foot/issues/270)) +* Combining characters not being rendered when composed with colored + bitmap glyphs (i.e. colored emojis). +* Pasting URIs from the clipboard when the source has not + newline-terminated the last URI + ([#291](https://codeberg.org/dnkl/foot/issues/291)). +* Sixel "current geometry" query response not being bounded by the + current window dimensions (fixes `lsix` output) +* Crash on keyboard input when repeat rate was zero (i.e. no repeat). +* Wrong button encoding of mouse buttons 6 and 7 in mouse events. +* Scrollback search not matching composed characters. +* High CPU usage when holding down e.g. arrow keys while in scrollback + search mode. +* Rendering of composed characters in the scrollback search box. +* IME pre-edit cursor when positioned at the end of the pre-edit + string. +* Scrollback search not matching multi-column characters. + + +### Contributors + +* [pc](https://codeberg.org/pc) +* [FollieHiyuki](https://codeberg.org/FollieHiyuki) +* jbeich +* [tdeo](https://codeberg.org/tdeo) + + +## 1.6.2 + +### Fixed + +* Version number in `meson.build`. + + +## 1.6.1 +### Added + +* `--seed` to `generate-alt-random.py`, enabling deterministic PGO + builds. + + +### Changed + + +* Use `-std=c11` instead of `-std=c18`. +* Added `-Wno-profile-instr-unprofiled` to Clang cflags in PGO builds + ([INSTALL.md](https://codeberg.org/dnkl/foot/src/branch/releases/1.6/INSTALL.md#user-content-performance-optimized-pgo)) + + +### Fixed + +* Missing dependencies in meson, causing heavily parallelized builds + to fail. +* Background color when alpha < 1.0 being wrong + ([#249](https://codeberg.org/dnkl/foot/issues/249)). +* `generate-alt-random.py` failing in containers. + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) +* [sterni](https://codeberg.org/sterni) + + +## 1.6.0 + +### For packagers + +Starting with this release, foot can be PGO:d (compiled using profile +guided optimizations) **without** a running Wayland session. This +means foot can be PGO:d in e.g. sandboxed build scripts. See +[INSTALL.md](INSTALL.md#user-content-performance-optimized-pgo). + + +### Added + +* IME support. This is compile-time optional, see + [INSTALL.md](INSTALL.md#user-content-options) + ([#134](https://codeberg.org/dnkl/foot/issues/134)). +* `DECSET` escape to enable/disable IME: `CSI ? 737769 h` enables IME + and `CSI ? 737769 l` disables it. This can be used to + e.g. enable/disable IME when entering/leaving insert mode in vim. +* `dpi-aware` option to `foot.ini`. The default, `auto`, sizes fonts + using the monitor's DPI when output scaling has been + **disabled**. If output scaling has been **enabled**, fonts are + sized using the scaling factor. DPI-only font sizing can be forced + by setting `dpi-aware=yes`. Setting `dpi-aware=no` forces font + sizing to be based on the scaling factor. + ([#206](https://codeberg.org/dnkl/foot/issues/206)). +* Implement reverse auto-wrap (_auto\_left\_margin_, _bw_, in + terminfo). This mode can be enabled/disabled with `CSI ? 45 h` and + `CSI ? 45 l`. It is **enabled** by default + ([#150](https://codeberg.org/dnkl/foot/issues/150)). +* `bell` option to `foot.ini`. Can be set to `set-urgency` to make + foot render the margins in red when receiving `BEL` while **not** + having keyboard focus. Applications can dynamically enable/disable + this with the `CSI ? 1042 h` and `CSI ? 1042 l` escape + sequences. Note that Wayland does **not** implement an _urgency_ + hint like X11, but that there is a + [proposal](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/9) + to add support for this. The value `set-urgency` was chosen for + forward-compatibility, in the hopes that this proposal eventualizes + ([#157](https://codeberg.org/dnkl/foot/issues/157)). +* `bell` option can also be set to `notify`, in which case a desktop + notification is emitted when foot receives `BEL` in an unfocused + window. +* `word-delimiters` option to `foot.ini` + ([#156](https://codeberg.org/dnkl/foot/issues/156)). +* `csd.preferred` can now be set to `none` to disable window + decorations. Note that some compositors will render SSDs despite + this option being used ([#163](https://codeberg.org/dnkl/foot/issues/163)). +* Terminal content is now auto-scrolled when moving the mouse above or + below the window while selecting + ([#149](https://codeberg.org/dnkl/foot/issues/149)). +* `font-bold`, `font-italic` `font-bold-italic` options to + `foot.ini`. These options allow custom bold/italic fonts. They are + unset by default, meaning the bold/italic version of the regular + font is used ([#169](https://codeberg.org/dnkl/foot/issues/169)). +* Drag & drop support; text, files and URLs can now be dropped in a + foot terminal window ([#175](https://codeberg.org/dnkl/foot/issues/175)). +* `clipboard-paste` and `primary-paste` scrollback search bindings. By + default, they are bound to `ctrl+v ctrl+y` and `shift+insert` + respectively, and lets you paste from the clipboard or primary + selection into the search buffer. +* Support for `pipe-*` actions in mouse bindings. It was previously + not possible to add a command to these actions when used in mouse + bindings, making them useless + ([#183](https://codeberg.org/dnkl/foot/issues/183)). +* `bold-text-in-bright` option to `foot.ini`. When enabled, bold text + is rendered in a brighter color + ([#199](https://codeberg.org/dnkl/foot/issues/199)). +* `-w,--window-size-pixels` and `-W,--window-size-chars` command line + options to `footclient` ([#189](https://codeberg.org/dnkl/foot/issues/189)). +* Short command line options for `--title`, `--maximized`, + `--fullscreen`, `--login-shell`, `--hold` and `--check-config`. +* `DECSET` escape to modify the `escape` key to send `\E[27;1;27~` + instead of `\E`: `CSI ? 27127 h` enables the new behavior, `CSI ? + 27127 l` disables it (the default). +* OSC 777;notify: desktop notifications. Use in combination with the + new `notify` option in `foot.ini` + ([#224](https://codeberg.org/dnkl/foot/issues/224)). +* Status line terminfo capabilities `hs`, `tsl`, `fsl` and `dsl`. This + enables e.g. vim to set the window title + ([#242](https://codeberg.org/dnkl/foot/issues/242)). + + +### Changed + +* Blinking text now uses the foreground color, but dimmed down in its + off state, instead of the background color. +* Sixel default maximum size is now 10000x10000 instead of the current + window size. +* Graphical glitches/flashes when resizing the window while running a + fullscreen application, i.e. the 'alt' screen + ([#221](https://codeberg.org/dnkl/foot/issues/221)). +* Cursor will now blink if **either** `CSI ? 12 h` or `CSI Ps SP q` + has been used to enable blinking. **cursor.blink** in `foot.ini` + controls the default state of `CSI Ps SP q` + ([#218](https://codeberg.org/dnkl/foot/issues/218)). +* The sub-parameter versions of the SGR RGB color escapes (e.g + `\E[38:2...m`) can now be used _without_ the color space ID + parameter. +* SGR 21 no longer disables **bold**. According to ECMA-48, SGR 21 is + _"double underline_". Foot does not (yet) implement that, but that's + no reason to implement a non-standard behavior. +* `DECRQM` now returns actual state of the requested mode, instead of + always returning `2`. + + +### Removed + +* Support for loading configuration from `$XDG_CONFIG_HOME/footrc`. +* `scrollback` option from `foot.ini`. +* `geometry` from `foot.ini`. +* Key binding action `scrollback-up` and `scrollback-down`. + + +### Fixed + +* Error when re-assigning a default key binding + ([#233](https://codeberg.org/dnkl/foot/issues/233)). +* `\E[s`+`\E[u` (save/restore cursor) now saves and restores + attributes and charset configuration, just like `\E7`+`\E8`. +* Report mouse motion events to the client application also while + dragging the cursor outside the grid. +* Parsing of the sub-parameter versions of indexed SGR color escapes + (e.g. `\E[38:5...m`) +* Frames occasionally being rendered while application synchronized + updates is in effect. +* Handling of failures to parse the font specification string. +* Extra private/intermediate characters in escape sequences not being + ignored. + + +### Contributors + +* [kennylevinsen](https://codeberg.org/kennylevinsen) +* [craigbarnes](https://codeberg.org/craigbarnes) + + +## 1.5.4 + +### Changed + + +* Num Lock by default overrides the keypad mode. See + **foot.ini**(5)::KEYPAD, or + [README.md](README.md#user-content-keypad) for details + ([#194](https://codeberg.org/dnkl/foot/issues/194)). +* Single-width characters with double-width glyphs are now allowed to + overflow into neighboring cells by default. Set + **tweak.allow-overflowing-double-width-glyphs** to 'no' to disable + this. + +### Fixed + +* Resize very slow when window is hidden + ([#190](https://codeberg.org/dnkl/foot/issues/190)). +* Key mappings for key combinations with `shift`+`tab` + ([#210](https://codeberg.org/dnkl/foot/issues/210)). +* Key mappings for key combinations with `alt`+`return`. +* `footclient` `-m` (`--maximized`) flag being ignored. +* Crash with explicitly sized sixels with a height less than 6 pixels. +* Key mappings for `esc` with modifiers. + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) + + +## 1.5.3 + +### Fixed + +* Crash when libxkbcommon cannot find a suitable libX11 _compose_ + file. Note that foot will run, but without support for dead keys. + ([#170](https://codeberg.org/dnkl/foot/issues/170)). +* Restored window size when window is un-tiled. +* XCursor shape in CSD corners when window is tiled. +* Error handling when processing keyboard input (maybe + [#171](https://codeberg.org/dnkl/foot/issues/171)). +* Compilation error _"overflow in conversion from long 'unsigned int' + to 'int' changes value... "_ seen on platforms where the `request` + argument in `ioctl(3)` is an `int` (for example: linux/ppc64). +* Crash when using the mouse in alternate scroll mode in an unfocused + window ([#179](https://codeberg.org/dnkl/foot/issues/179)). +* Character dropped from selection when "right-click-hold"-extending a + selection ([#180](https://codeberg.org/dnkl/foot/issues/180)). + + +## 1.5.2 + +### Fixed + +* Regression: middle clicking double pastes in e.g. vim + ([#168](https://codeberg.org/dnkl/foot/issues/168)) + + +## 1.5.1 + +### Changed + +* Default value of the **scrollback.multiplier** option in `foot.ini` + from `1.0` to `3.0`. +* `shift`+`insert` now pastes from the primary selection by + default. This is in addition to middle-clicking with the mouse. + + +### Fixed + +* Mouse bindings now match even if the actual click count is larger + than specified in the binding. This allows you to, for example, + quickly press the middle-button to paste multiple times + ([#146](https://codeberg.org/dnkl/foot/issues/146)). +* Color flashes when changing the color palette with OSC 4,10,11 + ([#141](https://codeberg.org/dnkl/foot/issues/141)). +* Scrollback position is now retained when resizing the window + ([#142](https://codeberg.org/dnkl/foot/issues/142)). +* Trackpad scrolling speed to better match the mouse scrolling speed, + and to be consistent with other (Wayland) terminal emulators. Note + that it is (much) slower compared to previous foot versions. Use the + **scrollback.multiplier** option in `foot.ini` if you find the new + speed too slow ([#144](https://codeberg.org/dnkl/foot/issues/144)). +* Crash when `foot.ini` contains an invalid section name + ([#159](https://codeberg.org/dnkl/foot/issues/159)). +* Background opacity when in _reverse video_ mode. +* Crash when writing a sixel image that extends outside the terminal's + right margin ([#151](https://codeberg.org/dnkl/foot/issues/151)). +* Sixel image at non-zero column positions getting sheared at + seemingly random occasions + ([#151](https://codeberg.org/dnkl/foot/issues/151)). +* Crash after either resizing a window or changing the font size if + there were sixels present in the scrollback while doing so. +* _Send Device Attributes_ to only send a response if `Ps == 0`. +* Paste from primary when clipboard is empty. + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) +* [zar](https://codeberg.org/zar) + + +## 1.5.0 + +### Deprecated + +* `$XDG_CONFIG_HOME/footrc`/`~/.config/footrc`. Use + `$XDG_CONFIG_HOME/foot/foot.ini`/`~/.config/foot/foot.ini` instead. +* **scrollback** option in `foot.ini`. Use **scrollback.lines** + instead. +* **scrollback-up** key binding. Use **scrollback-up-page** instead. +* **scrollback-down** key binding. Use **scrollback-down-page** + instead. + + +### Added + +* Scrollback position indicator. This feature is optional and + controlled by the **scrollback.indicator-position** and + **scrollback.indicator-format** options in `foot.ini` + ([#42](https://codeberg.org/dnkl/foot/issues/42)). +* Key bindings in _scrollback search_ mode are now configurable. +* `--check-config` command line option. +* **pipe-selected** key binding. Works like **pipe-visible** and + **pipe-scrollback**, but only pipes the currently selected text, if + any ([#51](https://codeberg.org/dnkl/foot/issues/51)). +* **mouse.hide-when-typing** option to `foot.ini`. +* **scrollback.multiplier** option to `foot.ini` + ([#54](https://codeberg.org/dnkl/foot/issues/54)). +* **colors.selection-foreground** and **colors.selection-background** + options to `foot.ini`. +* **tweak.render-timer** option to `foot.ini`. +* Modifier support in mouse bindings + ([#77](https://codeberg.org/dnkl/foot/issues/77)). +* Click count support in mouse bindings, i.e double- and triple-click + ([#78](https://codeberg.org/dnkl/foot/issues/78)). +* All mouse actions (begin selection, select word, select row etc) are + now configurable, via the new **select-begin**, + **select-begin-block**, **select-extend**, **select-word**, + **select-word-whitespace** and **select-row** options in the + **mouse-bindings** section in `foot.ini` + ([#79](https://codeberg.org/dnkl/foot/issues/79)). +* Implement XTSAVE/XTRESTORE escape sequences, `CSI ? Ps s` and `CSI ? + Ps r` ([#91](https://codeberg.org/dnkl/foot/issues/91)). +* `$COLORTERM` is now set to `truecolor` at startup, to indicate + support for 24-bit RGB colors. +* Experimental support for rendering double-width glyphs with a + character width of 1. Must be explicitly enabled with + `tweak.allow-overflowing-double-width-glyphs` + ([#116](https://codeberg.org/dnkl/foot/issues/116)). +* **initial-window-size-pixels** options to `foot.ini` and + `-w,--window-size-pixels` command line option to `foot`. This option + replaces the now deprecated **geometry** and `-g,--geometry` + options. +* **initial-window-size-chars** option to `foot.ini` and + `-W,--window-size-chars` command line option to `foot`. This option + configures the initial window size in **characters**, and is an + alternative to **initial-window-size-pixels**. +* **scrollback-up-half-page** and **scrollback-down-half-page** key + bindings. They scroll up/down half of a page in the scrollback + ([#128](https://codeberg.org/dnkl/foot/issues/128)). +* **scrollback-up-line** and **scrollback-down-line** key + bindings. They scroll up/down a single line in the scrollback. +* **mouse.alternate-scroll-mode** option to `foot.ini`. This option + controls the initial state of the _Alternate Scroll Mode_, and + defaults to `yes`. When enabled, mouse scroll events are translated + to up/down key events in the alternate screen, letting you scroll in + e.g. `less` and other applications without enabling native mouse + support in them ([#135](https://codeberg.org/dnkl/foot/issues/135)). + + +### Changed + +* Renamed man page for `foot.ini` from **foot**(5) to **foot.ini**(5). +* Configuration errors are no longer fatal; foot will start and print + an error inside the terminal (and of course still log errors on + stderr). +* Default `--server` socket path to use `$WAYLAND_DISPLAY` instead of + `$XDG_SESSION_ID` ([#55](https://codeberg.org/dnkl/foot/issues/55)). +* Trailing empty cells are no longer highlighted in mouse selections. +* Foot now searches for its configuration in + `$XDG_DATA_DIRS/foot/foot.ini`, if no configuration is found in + `$XDG_CONFIG_HOME/foot/foot.ini` or in `$XDG_CONFIG_HOME/footrc`. +* Minimum window size changed from four rows and 20 columns, to 1 row + and 2 columns. +* **scrollback-up/down** renamed to **scrollback-up/down-page**. +* fcft >= 2.3.0 is now required. + + +### Fixed + +* Command lines for **pipe-visible** and **pipe-scrollback** are now + tokenized (i.e. syntax checked) when the configuration is loaded, + instead of every time the key binding is executed. +* Incorrect multi-column character spacer insertion when reflowing + text. +* Compilation errors in 32-bit builds. +* Mouse cursor style in top and left margins. +* Selection is now **updated** when the cursor moves outside the grid + ([#70](https://codeberg.org/dnkl/foot/issues/70)). +* Viewport sometimes not moving when doing a scrollback search. +* Crash when canceling a scrollback search and the window had been + resized while searching. +* Selection start point not moving when the selection changes + direction. +* OSC 10/11/104/110/111 (modify colors) did not update existing screen + content ([#94](https://codeberg.org/dnkl/foot/issues/94)). +* Extra newlines when copying empty cells + ([#97](https://codeberg.org/dnkl/foot/issues/97)). +* Mouse events from being sent to client application when a mouse + binding has consumed it. +* Input events from getting mixed with paste data + ([#101](https://codeberg.org/dnkl/foot/issues/101)). +* Missing DPI values for "some" monitors on Gnome + ([#118](https://codeberg.org/dnkl/foot/issues/118)). +* Handling of multi-column composed characters while reflowing. +* Escape sequences sent for key combinations with `Return`, that did + **not** include `Alt`. +* Clipboard (or primary selection) is now cleared when receiving an + OSC-52 command with an invalid base64 encoded payload. +* Cursor position being set outside the grid when reflowing text. +* CSD buttons to be hidden when window size becomes so small that they + no longer fit. + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) +* [birger](https://codeberg.org/birger) +* [Ordoviz](https://codeberg.org/Ordoviz) +* [cherti](https://codeberg.org/cherti) + + +## 1.4.4 +### Changed + +* Mouse cursor is now always a `left_ptr` when inside the margins, to + indicate it is not possible to start a selection. + + +### Fixed + +* Crash when starting a selection inside the margins. +* Improved font size consistency across multiple monitors with + different DPI ([#47](https://codeberg.org/dnkl/foot/issues/47)). +* Handle trailing comments in `footrc` + + +## 1.4.3 +### Added + +* Section to [README.md](README.md) describing how to programmatically + identify foot. +* [LICENSE](LICENSE), [README.md](README.md) and + [CHANGELOG.md](CHANGELOG.md) are now installed to + `${datadir}/doc/foot`. +* Support for escaping quotes in **pipe-visible** and + **pipe-scrollback** commands. + + +### Changed + +* Primary DA to no longer indicate support for _Selective Erase_, + _Technical Characters_ and _Terminal State Interrogation_. +* Secondary DA to report foot as a VT220 instead of a VT420. +* Secondary DA to report foot's version number in parameter 2, the + _Firmware Version_. The string is made up of foot's major, minor and + patch version numbers, always using two digits for each version + number and without any other separating characters. Thus, _1.4.2_ + would be reported as `010402` (i.e. the full response would be + `\E[>1;010402;0c`). +* Scrollback search to only move the viewport if the match lies + outside it. +* Scrollback search to focus match, that requires a viewport change, + roughly in the center of the screen. +* Extending a selection with the right mouse button now works while + dragging the mouse. + + +### Fixed + +* Crash in scrollback search. +* Crash when a **pipe-visible** or **pipe-scrollback** command + contained an unclosed quote + ([#49](https://codeberg.org/dnkl/foot/issues/49)). + + +### Contributors + +* [birger](https://codeberg.org/birger) +* [cherti](https://codeberg.org/cherti) + + +## 1.4.2 + +### Changed + +* Maximum window title length from 100 to 2048. + + +### Fixed + +* Crash when overwriting a sixel and the row being overwritten did not + cover an entire cell. +* Assertion failure in debug builds when overwriting a sixel image. + + +## 1.4.1 + +### Fixed + +* Compilation errors in release builds with some combinations of + compilers and compiler flags. + + +## 1.4.0 + +### Added + +* `Sync` to terminfo. This is a tmux extension that indicates + _"Synchronized Updates"_ are supported. +* `--hold` command line option to `footclient`. +* Key mapping for `KP_Decimal`. +* Terminfo entries for keypad keys: `ka1`, `ka2`, `ka3`, `kb1`, `kb3`, + `kc1`, `kc2`, `kc3`, `kp5`, `kpADD`, `kpCMA`, `kpDIV`, `kpDOT`, + `kpMUL`, `kpSUB` and `kpZRO`. +* **blink** option to `footrc`; a boolean that lets you control + whether the cursor should blink or not by default. Note that + applications can override this. +* Multi-seat support +* Implemented `C0::FF` (form feed) +* **pipe-visible** and **pipe-scrollback** key bindings. These let you + pipe either the currently visible text, or the entire scrollback to + external tools ([#29](https://codeberg.org/dnkl/foot/issues/29)). Example: + `pipe-visible=[sh -c "xurls | bemenu | xargs -r firefox] Control+Print` + + +### Changed + +* Background transparency to only be used with the default background + color. +* Copy-to-clipboard/primary-selection to insert a line break if either + the last cell on the previous line or the first cell on the next + line is empty. +* Number of lines to scroll is now always clamped to the number of + lines in the scrolling region.. +* New terminal windows spawned with `ctrl`+`shift`+`n` are no longer + double forked. +* Unicode combining character overflow errors are only logged when + debug logging has been enabled. +* OSC 4 (_Set Color_) now updates already rendered cells, excluding + scrollback. +* Mouse cursor from `hand2` to `left_ptr` when client is capturing the + mouse. +* Sixel images are now removed when the font size is **decreased**. +* `DECSCUSR` (_Set Cursor Style_, `CSI Ps SP q`) now uses `Ps=0` + instead of `Ps=2` to reset the style to the user configured default + style. `Ps=2` now always configures a _Steady Block_ cursor. +* `Se` terminfo capability from `\E[2 q` to `\E[ q`. +* Hollow cursor to be drawn when window has lost _keyboard_ focus + rather than _visual_ focus. + + +### Fixed + +* Do not stop an ongoing selection when `shift` is released. When the + client application is capturing the mouse, one must hold down + `shift` to start a selection. This selection is now finalized only + when the mouse button is released - not as soon as `shift` is + released. +* Selected cells did not appear selected if programmatically modified. +* Rare crash when scrolling and the new viewport ended up **exactly** + on the wrap around. +* Selection handling when viewport wrapped around. +* Restore signal mask in the client process. +* Set `IUTF8`. +* Selection of double-width characters. It is no longer possible to + select half of a double-width character. +* Draw hollow block cursor on top of character. +* Set an initial `TIOCSWINSZ`. This ensures clients never read a + `0x0` terminal size ([#20](https://codeberg.org/dnkl/foot/issues/20)). +* Glyphs overflowing into surrounding cells + ([#21](https://codeberg.org/dnkl/foot/issues/21)). +* Crash when last rendered cursor cell had scrolled off screen and + `\E[J3` was executed. +* Assert (debug builds) when an `\e]4` OSC escape was not followed by + a `;`. +* Window title always being set to "foot" on reset. +* Terminfo entry `kb2` (center keypad key); it is now set to `\EOu` + (which is what foot emits) instead of the incorrect value `\EOE`. +* Palette reuse in sixel images. Previously, the palette was reset + after each image. +* Do not auto-resize a sixel image for which the client has specified + a size. This fixes an issue where an image would incorrectly + overflow into the cell row beneath. +* Text printed, or other sixel images drawn, on top of a sixel image + no longer erases the entire image, only the part(s) covered by the + new text or image. +* Sixel images being erased when printing text next to them. +* Sixel handling when resizing window. +* Sixel handling when scrollback wraps around. +* Foot now issues much fewer `wl_surface_damage_buffer()` calls + ([#35](https://codeberg.org/dnkl/foot/issues/35)). +* `C0::VT` to be processed as `C0::LF`. Previously, `C0::VT` would + only move the cursor down, but never scroll. +* `C0::HT` (_Horizontal Tab_, or `\t`) no longer clears `LCF` (_Last + Column Flag_). +* `C0::LF` now always clears `LCF`. Previously, it only cleared it + when the cursor was **not** at the bottom of the scrolling region. +* `IND` and `RI` now clears `LCF`. +* `DECAWM` now clears `LCF`. +* A multi-column character that does not fit on the current line is + now printed on the next line, instead of only printing half the + character. +* Font size can no longer be reduced to negative values + ([#38](https://codeberg.org/dnkl/foot/issues/38)). + + +## 1.3.0 + +### Added + +* User configurable key- and mouse bindings. See `man 5 foot` and the + example `footrc` ([#1](https://codeberg.org/dnkl/foot/issues/1)) +* **initial-window-mode** option to `footrc`, that lets you control + the initial mode for each newly spawned window: _windowed_, + _maximized_ or _fullscreen_. +* **app-id** option to `footrc` and `--app-id` command line option, + that sets the _app-id_ property on the Wayland window. +* **title** option to `footrc` and `--title` command line option, that + sets the initial window title. +* Right mouse button extends the current selection. +* `CSI Ps ; Ps ; Ps t` escape sequences for the following parameters: + `11t`, `13t`, `13;2t`, `14t`, `14;2t`, `15t`, `19t`. +* Unicode combining characters. + + +### Changed + +* Spaces no longer removed from zsh font name completions. +* Default key binding for _spawn-terminal_ to ctrl+shift+n. +* Renderer is now much faster with interactive scrolling + ([#4](https://codeberg.org/dnkl/foot/issues/4)) +* memfd sealing failures are no longer fatal errors. +* Selection to no longer be cleared on resize. +* The current monitor's subpixel order (RGB/BGR/V-RGB/V-BGR) is + preferred over FontConfig's `rgba` property. Only if the monitor's + subpixel order is `unknown` is FontConfig's `rgba` property used. If + the subpixel order is `none`, then grayscale antialiasing is + used. The subpixel order is ignored if antialiasing has been + disabled. +* The four primary font variants (normal, bold, italic, bold italic) + are now loaded in parallel. This speeds up both the initial startup + time, as well as DPI changes. +* Command line parsing no longer tries to parse arguments following + the command-to-execute. This means one can now write `foot sh -c + true` instead of `foot -- sh -c true`. + + +### Removed + +* Keyboard/pointer handler workarounds for Sway 1.2. + + +### Fixed + +* Sixel images moved or deleted on window resize. +* Cursor sometimes incorrectly restored on exit from alternate screen. +* 'Underline' cursor being invisible on underlined text. +* Restored cursor position in 'normal' screen when window was resized + while in 'alt' screen. +* Hostname in OSC 7 URI not being validated. +* OSC 4 with multiple `c;spec` pairs. +* Alt+Return to emit "ESC \r". +* Trackpad sloooow scrolling to eventually scroll a line. +* Memory leak in terminal reset. +* Translation of cursor coordinates on resize +* Scaling color specifiers in OSC sequences. +* `OSC 12 ?` to return the cursor color, not the cursor's text color. +* `OSC 12;#000000` to configure the cursor to use inverted + foreground/background colors. +* Call `ioctl(TIOCSCTTY)` on the pts fd in the slave process. + + +## 1.2.3 + +### Fixed +* Forgot to version bump 1.2.2 + + +## 1.2.2 + +### Changed + +* Changed icon name in `foot.desktop` and `foot-server.desktop` from + _terminal_ to _utilities-terminal_. +* `XDG_SESSION_ID` is now included in the server/daemon default socket + path. + + +### Fixed + +* Window size doubling when moving window between outputs with + different scaling factors ([#3](https://codeberg.org/dnkl/foot/issues/3)). +* Font being too small on monitors with fractional scaling + ([#5](https://codeberg.org/dnkl/foot/issues/5)). + + +## 1.2.1 + +### Fixed + +* Building AUR package + + +## 1.2.0 + +### Added + +* Run-time text resize using ctrl-+, ctrl+- and ctrl+0 +* Font size adjusts dynamically to outputs' DPI +* Reflow text when resizing window +* **pad** option to `footrc` +* **login-shell** option to `footrc` and `--login-shell` command line + option +* Client side decorations (CSDs). This finally makes foot usable on + GNOME. +* Sixel graphics support +* OSC 12 and 112 escape sequences (set/reset text cursor color) +* REP CSI escape sequence +* `oc` to terminfo +* foot-server.desktop file +* Window and cell size reporting escape sequences +* `--hold` command line option +* `--print-pid=FILE|FD` command line option + + +### Changed + +* Subpixel antialiasing is only enabled when background is opaque +* Meta/alt ESC prefix can now be disabled with `\E[?1036l`. In this + mode, the 8:th bit is set and the result is UTF-8 encoded. This can + also be disabled with `\E[1024l` (in which case the Alt key is + effectively being ignored). +* terminfo now uses ST instead of BEL as OSC terminator +* Logging to print to stderr, not stdout +* Backspace now emits DEL (^?), and ctrl+backspace emits BS (^H) + + +### Removed + +* '28' from DA response diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..26ab32a --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,83 @@ +# Foot Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +Participants in the foot community are expected to uphold the described +standards not only in official community spaces (issue trackers, IRC channels, +etc.) but in all public spaces. The Code of Conduct however does acknowledge +that people are fallible and that it is possible to truly correct a past +pattern of unacceptable behavior. That is to say, the scope of the Code of +Conduct does not necessarily extend into the distant past. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior +may be reported to the community leaders responsible for enforcement +at [daniel@ekloef.se](mailto:daniel@ekloef.se). All complaints will +be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +The consequences for Code of Conduct violations will be decided upon and +enforced by community leaders. These may include a formal warning, a temporary +ban from community spaces, a permanent ban from community spaces, etc. + +There are no hard and fast rules for exactly what behavior in which space will +result in what consequences, it is up to the community leaders to enforce the +Code of Conduct in a way that they believe best promotes a healthy community. + +## Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org/), +version 2.1, available at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..7df8d0b --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,476 @@ +# Installing + +1. [Overview](#overview) +1. [Requirements](#requirements) + 1. [Running](#running) + 1. [Building](#building) +1. [Other](#other) + 1. [Setup](#setup) + 1. [Options](#options) + 1. [Release build](#release-build) + 1. [Size optimized](#size-optimized) + 1. [Performance optimized, non-PGO](#performance-optimized-non-pgo) + 1. [Performance optimized, PGO](#performance-optimized-pgo) + 1. [Partial PGO](#partial-pgo) + 1. [Full PGO](#full-pgo) + 1. [Use the generated PGO data](#use-the-generated-pgo-data) + 1. [Profile Guided Optimization](#profile-guided-optimization) + 1. [Debug build](#debug-build) + 1. [Terminfo](#terminfo) + 1. [Running the new build](#running-the-new-build) + + +## Overview + +foot makes use of a couple of libraries I have developed: +[tllist](https://codeberg.org/dnkl/tllist) and +[fcft](https://codeberg.org/dnkl/fcft). As such, they will most likely +not have been installed already. You can either install them as system +libraries or build them as _subprojects_ in foot. + +When building foot, they will first be searched for as system +libraries. If **found**, foot will link dynamically against them. +If **not** found, meson will attempt to download and build them as +subprojects. + + +## Requirements + +### Running + +* UTF-8 locale +* fontconfig +* freetype +* pixman +* wayland (_client_ and _cursor_ libraries) +* xkbcommon +* utf8proc (_optional_, needed for grapheme clustering) +* libutempter (_optional_, needed for utmp logging on Linux) +* ulog (_optional_, needed for utmp logging on FreeBSD) +* [fcft](https://codeberg.org/dnkl/fcft) [^1] + +[^1]: can also be built as subprojects, in which case they are + statically linked. + +If you are packaging foot, you may also want to consider adding the +following **optional** dependencies: + +* libnotify: desktop notifications by default uses `notify-send`. +* xdg-utils: URLs are by default launched with `xdg-open`. +* bash-completion: If you want completion for positional arguments. + +### Building + +In addition to the dev variant of the packages above, you need: + +* meson +* ninja +* wayland protocols +* ncurses (needed to generate terminfo) +* scdoc (for man page generation, not needed if documentation is disabled) +* llvm (for PGO builds with Clang) +* [tllist](https://codeberg.org/dnkl/tllist) [^1] +* systemd (optional, foot will install systemd unit files if detected) + +A note on compilers; in general, foot runs **much** faster when +compiled with gcc instead of clang. A profile-guided gcc build can be +more than twice as fast as a clang build. + +**Note** GCC 10.1 has a performance regression that severely affects +foot when doing PGO builds and building with `-O2`; it is about 30-40% +slower compared to GCC 9.3. + +The work around is simple: make sure you build with `-O3`. This is the +default with `meson --buildtype=release`, but e.g. `makepkg` can +override it (`makepkg` uses `-O2` by default). + +## Other + +Foot uses _meson_. If you are unfamiliar with it, the official +[tutorial](https://mesonbuild.com/Tutorial.html) might be a good +starting point. + +A note on terminfo; the terminfo database exposes terminal +capabilities to the applications running inside the terminal. As such, +it is important that the terminfo used reflects the actual +terminal. Using the `xterm-256color` terminfo will, in many cases, +work, but I still recommend using foot's own terminfo. There are two +reasons for this: + +* foot's terminfo contains a couple of non-standard capabilities, + used by e.g. tmux. +* New capabilities added to the `xterm-256color` terminfo could + potentially break foot. +* There may be future additions or changes to foot's terminfo. + +As of ncurses 2021-07-31, ncurses includes a version of foot's +terminfo. **The recommendation is to use those**, and only install the +terminfo definitions from this git repo if the system's ncurses +predates 2021-07-31. + +But, note that the foot terminfo definitions in ncurses' lack the +non-standard capabilities. This mostly affects tmux; without them, +`terminal-overrides` must be configured to enable truecolor +support. For this reason, it _is_ possible to install "our" terminfo +definitions as well, either in a non-default location, or under a +different name. + +Both have their set of issues. When installing to a non-default +location, foot will set the environment variable `TERMINFO` in the +child process. However, there are many situations where this simply +does not work. See https://codeberg.org/dnkl/foot/issues/695 for +details. + +Installing them under a different name generally works well, but will +break applications that check if `$TERM == foot`. + +Hence the recommendation to simply use ncurses' terminfo definitions +if available. + +If packaging "our" terminfo definitions, I recommend doing that as a +separate package, to allow them to be installed on remote systems +without having to install foot itself. + + +### Setup + +To build, first, create a build directory, and switch to it: +```sh +mkdir -p bld/release && cd bld/release +``` + +### Options + +Available compile-time options: + +| Option | Type | Default | Description | Extra dependencies | +|--------------------------------------|---------|-------------------------|---------------------------------------------------------------------------------|---------------------| +| `-Ddocs` | feature | `auto` | Builds and install documentation | scdoc | +| `-Dtests` | bool | `true` | Build tests (adds a `ninja test` build target) | None | +| `-Dime` | bool | `true` | Enables IME support | None | +| `-Dgrapheme-clustering` | feature | `auto` | Enables grapheme clustering | libutf8proc | +| `-Dterminfo` | feature | `enabled` | Build and install terminfo files | tic (ncurses) | +| `-Ddefault-terminfo` | string | `foot` | Default value of `TERM` | None | +| `-Dterminfo-base-name` | string | `-Ddefault-terminfo` | Base name of the generated terminfo files | None | +| `-Dcustom-terminfo-install-location` | string | `${datadir}/terminfo` | Value to set `TERMINFO` to | None | +| `-Dsystemd-units-dir` | string | `${systemduserunitdir}` | Where to install the systemd service files (absolute) | None | +| `-Dutmp-backend` | combo | `auto` | Which utmp backend to use (`none`, `libutempter`, `ulog` or `auto`) | libutempter or ulog | +| `-Dutmp-default-helper-path` | string | `auto` | Default path to utmp helper binary. `auto` selects path based on `utmp-backend` | None | + +Documentation includes the man pages, readme, changelog and license +files. + +`-Ddefault-terminfo`: I strongly recommend leaving the default +value. Use this option if you plan on installing the terminfo files +under a different name. Setting this changes the default value of +`$TERM`, and the names of the terminfo files (if +`-Dterminfo=enabled`). + +If you want foot to use the terminfo files from ncurses, but still +package foot's own terminfo files under a different name, you can use +the `-Dterminfo-base-name` option. Many distributions use the name +`foot-extra`, and thus it might be a good idea to reuse that: + +```sh +meson ... -Ddefault-terminfo=foot -Dterminfo-base-name=foot-extra +``` +(or just leave out `-Ddefault-terminfo`, since it defaults to `foot` anyway). + +Finally, `-Dcustom-terminfo-install-location` enables foot's terminfo +to co-exist with ncurses' version, without changing the terminfo +names. The idea is that you install foot's terminfo to a non-standard +location, for example `/usr/share/foot/terminfo`. Use +`-Dcustom-terminfo-install-location` to tell foot where the terminfo +is. Foot will set the environment variable `TERMINFO` to this value +(with `${prefix}` added). The value is **relative to ${prefix}**. + +Note that there are several issues with this approach: +https://codeberg.org/dnkl/foot/issues/695. + +If left unset, foot will **not** set or modify `TERMINFO`. + +`-Dterminfo` can be used to disable building the terminfo definitions +in the meson build. It does **not** change the default value of +`TERM`, and it does **not** disable `TERMINFO`, if +`-Dcustom-terminfo-install-location` has been set. Use this if +packaging the terminfo definitions in a separate package (and the +build script isn't shared with the 'foot' package). + +Example: + +```sh +meson --prefix=/usr -Dcustom-terminfo-install-location=lib/foot/terminfo +``` + +The above tells foot its terminfo definitions will be installed to +`/usr/lib/foot/terminfo`. This is the value foot will set the +`TERMINFO` environment variable to. + +If `-Dterminfo` is enabled (the default), then the terminfo files will +be built as part of the regular build process, and installed to the +specified location. + +Packagers may want to set `-Dterminfo=disabled`, and manually build +and [install the terminfo](#terminfo) files instead. + + +### Release build + +Below are instructions for building foot either [size +optimized](#size-optimized), [performance +optimized](performance-optimized-non-pgo), or performance +optimized using [PGO](#performance-optimized-pgo). + +PGO - _Profile Guided Optimization_ - is a way to optimize a program +better than `-O3` can, and is done by compiling foot twice: first to +generate an instrumented version which is used to run a payload that +exercises the performance critical parts of foot, and then a second +time to rebuild foot using the generated profiling data to guide +optimization. + +In addition to being faster, PGO builds also tend to be smaller than +regular `-O3` builds. + + +#### Size optimized + +To optimize for size (i.e. produce a small binary): + +```sh +export CFLAGS="$CFLAGS -Os" +meson --buildtype=release --prefix=/usr -Db_lto=true ../.. +ninja +ninja test +ninja install +``` + +#### Performance optimized, non-PGO + +To do a regular, non-PGO build optimized for performance: + +```sh +export CFLAGS="$CFLAGS -O3" +meson --buildtype=release --prefix=/usr -Db_lto=true ../.. +ninja +ninja test +ninja install +``` + +Use `-O2` instead of `-O3` if you prefer a slightly smaller (and +slower!) binary. + + +#### Performance optimized, PGO + +There are a lot more steps involved in a PGO build, and for this +reason there are a number of helper scripts available. + +`pgo/pgo.sh` is a standalone script that pieces together the other +scripts in the `pgo` directory to do a complete PGO build. This script +is intended to be used when doing manual builds. + +Note that all "full" PGO builds (which `auto` will prefer, if +possible) **require** `LC_CTYPE` to be set to an UTF-8 locale. This is +**not** done automatically. + +Example: + +```sh +cd foot +./pgo/pgo.sh auto . /tmp/foot-pgo-build-output +``` + +(run `./pgo/pgo.sh` to get help on usage) + +It supports a couple of different PGO builds; partial (covered in +detail below), full (also covered in detail below), and (full) +headless builds using Sway or cage. + +Packagers may want to use it as inspiration, but may choose to support +only a specific build type; e.g. full/headless with Sway. + +To do a manual PGO build, instead of using the script(s) mentioned +above, detailed instructions follows: + +First, configure the build directory: + +```sh +export CFLAGS="$CFLAGS -O3" +meson --buildtype=release --prefix=/usr -Db_lto=true ../.. +``` + +It is **very** important `-O3` is being used here, as GCC-10.1.x and +later have a regression where PGO with `-O2` is **much** slower. + +Clang users **must** add `-Wno-ignored-optimization-argument` to +`CFLAGS`. + +Then, tell meson we want to _generate_ profiling data, and build: + +```sh +meson configure -Db_pgo=generate +ninja +ninja test +``` + +Next, we need to actually generate the profiling data. + +There are two ways to do this: a [partial PGO build using a PGO +helper](#partial-pgo) binary, or a [full PGO build](#full-pgo) by +running the real foot binary. The latter has slightly better results +(i.e. results in a faster binary), but must be run in a Wayland +session. + +A full PGO build also tends to be smaller than a partial build. + + +##### Partial PGO + +This method uses a PGO helper binary that links against the VT parser +only. It is similar to a mock test; it instantiates a dummy terminal +instance and then directly calls the VT parser with stimuli. + +It explicitly does **not** include the Wayland backend and as such, it +does not require a running Wayland session. The downside is that not +all code paths in foot is exercised. In particular, the **rendering** +code is not. As a result, the final binary built using this method is +slightly slower than when doing a [full PGO](#full-pgo) build. + +We will use the `pgo` binary along with input corpus generated by +`scripts/generate-alt-random-writes.py`: + +```sh +./utils/xtgettcap +./footclient --version +./foot --version +tmp_file=$(mktemp) +../../scripts/generate-alt-random-writes \ + --rows=67 \ + --cols=135 \ + --scroll \ + --scroll-region \ + --colors-regular \ + --colors-bright \ + --colors-256 \ + --colors-rgb \ + --attr-bold \ + --attr-italic \ + --attr-underline \ + --sixel \ + ${tmp_file} +./pgo ${tmp_file} ${tmp_file} ${tmp_file} +rm ${tmp_file} +``` + +The first step, running `./foot --version` and `./footclient +--version` etc, might seem unnecessary, but is needed to ensure we +have _some_ profiling data for functions not covered by the PGO helper +binary, for **all** binaries. Without this, the final link phase will +fail. + +The snippet above then creates an (empty) temporary file. Then, it +runs a script that generates random escape sequences (if you cat +`${tmp_file}` in a terminal, you'll see random colored characters all +over the screen). Finally, we feed the randomly generated escape +sequences to the PGO helper. This is what generates the profiling data +used in the next step. + +You are now ready to [use the generated PGO +data](#use-the-generated-pgo-data). + + +##### Full PGO + +This method requires a running Wayland session. + +We will use the script `scripts/generate-alt-random-writes.py`: + +```sh +./utils/xtgettcap +./footclient --version +foot_tmp_file=$(mktemp) +./foot \ + --config=/dev/null \ + --override tweak.grapheme-shaping=no \ + --term=xterm \ + sh -c " --scroll --scroll-region --colors-regular --colors-bright --colors-256 --colors-rgb --attr-bold --attr-italic --attr-underline --sixel ${foot_tmp_file} && cat ${foot_tmp_file}" +rm ${foot_tmp_file} +``` + +You should see a foot window open up, with random colored text. The +window should close after ~1-2s. + +The first step, `./utils/xtgettcap && ./footclient --version` +might seem unnecessary, but is needed to ensure we have _some_ +profiling data for **all** binaries we build. Without this, the final +link phase will fail. + + +##### Use the generated PGO data + +Now that we have _generated_ PGO data, we need to rebuild foot. This +time telling meson (and ultimately gcc/clang) to _use_ the PGO data. + +If using Clang, now do (this requires _llvm_ to have been installed): + +```sh +llvm-profdata merge default_*profraw --output=default.profdata +``` + +Next, tell meson to _use_ the profile data we just generated, and rebuild: + +```sh +meson configure -Db_pgo=use +ninja +ninja test +``` + +Continue reading in [Running the new build](#running-the-new-build) + + +### Debug build + +```sh +meson --buildtype=debug ../.. +ninja +ninja test +``` + +### Terminfo + +By default, building foot also builds the terminfo files. If packaging +the terminfo files in a separate package, it might be easier to simply +disable the terminfo files in the regular build, and compile the +terminfo files manually instead. + +To build the terminfo files, run: + +```sh +sed 's/@default_terminfo@/foot/g' foot.info | \ + tic -o -x -e foot,foot-direct - +``` + +Where _"output-directory"_ **must** match the value passed to +`-Dcustom-terminfo-install-location` in the foot build. If +`-Dcustom-terminfo-install-location` has not been set, `-o +` can simply be omitted. + +Or, if packaging: + +```sh +tic -o ${DESTDIR}/usr/share/terminfo ... +``` + + +### Running the new build + +You can now run it directly from the build directory: +```sh +./foot +``` + +Or, if you did not install the terminfo definitions: + +```sh +./foot --term xterm-256color +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a915c5b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Daniel Eklöf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0c775a --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +toes diff --git a/async.c b/async.c new file mode 100644 index 0000000..3ad2a8c --- /dev/null +++ b/async.c @@ -0,0 +1,35 @@ +#include "async.h" + +#include +#include +#include + +#define LOG_MODULE "async" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +enum async_write_status +async_write(int fd, const void *_data, size_t len, size_t *idx) +{ + const uint8_t *const data = _data; + size_t left = len - *idx; + + while (left > 0) { + ssize_t ret = write(fd, &data[*idx], left); + + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return ASYNC_WRITE_REMAIN; + + return ASYNC_WRITE_ERR; + } + + LOG_DBG("wrote %zd bytes of %zu (%zu left) to FD=%d", + ret, left, left - ret, fd); + + *idx += ret; + left -= ret; + } + + return ASYNC_WRITE_DONE; +} diff --git a/async.h b/async.h new file mode 100644 index 0000000..876368d --- /dev/null +++ b/async.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +enum async_write_status {ASYNC_WRITE_DONE, ASYNC_WRITE_REMAIN, ASYNC_WRITE_ERR}; + +/* + * Primitive that writes data to a NONBLOCK:ing FD. + * + * _data: points to the beginning of the buffer + * len: total size of the data buffer + * idx: pointer to byte offset into data buffer - writing starts here. + * + * Thus, the total amount of data to write is (len - *idx). *idx is + * updated such that it points to the next unwritten byte in the data + * buffer. + * + * I.e. if the return value is: + * - ASYNC_WRITE_DONE, then the *idx == len. + * - ASYNC_WRITE_REMAIN, then *idx < len + * - ASYNC_WRITE_ERR, there was an error, and no data was written + */ +enum async_write_status async_write( + int fd, const void *data, size_t len, size_t *idx); diff --git a/base64.c b/base64.c new file mode 100644 index 0000000..db697cb --- /dev/null +++ b/base64.c @@ -0,0 +1,172 @@ +#include "base64.h" + +#include +#include +#include +#include +#include + +#define LOG_MODULE "base64" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" + +enum { + P = 1 << 6, // Padding byte (=) + I = 1 << 7, // Invalid byte ([^A-Za-z0-9+/=]) +}; + +static const uint8_t reverse_lookup[256] = { + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, 62, I, I, I, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, I, I, I, P, I, I, + I, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, I, I, I, I, I, + I, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I +}; + +static const char lookup[64] = { + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/" +}; + +char * +base64_decode(const char *s, size_t *size) +{ + const size_t len = strlen(s); + if (unlikely(len % 4 != 0)) { + errno = EINVAL; + return NULL; + } + + char *ret = malloc(len / 4 * 3 + 1); + if (unlikely(ret == NULL)) + return NULL; + + if (unlikely(size != NULL)) + *size = len / 4 * 3; + + for (size_t i = 0, o = 0; i < len; i += 4, o += 3) { + unsigned a = reverse_lookup[(unsigned char)s[i + 0]]; + unsigned b = reverse_lookup[(unsigned char)s[i + 1]]; + unsigned c = reverse_lookup[(unsigned char)s[i + 2]]; + unsigned d = reverse_lookup[(unsigned char)s[i + 3]]; + + unsigned u = a | b | c | d; + if (unlikely(u & I)) + goto invalid; + + if (unlikely(u & P)) { + if (unlikely(i + 4 != len || (a | b) & P || (c & P && !(d & P)))) + goto invalid; + + if (unlikely(size != NULL)) { + if (c & P) + *size = len / 4 * 3 - 2; + else + *size = len / 4 * 3 - 1; + } + + c &= 63; + d &= 63; + } + + uint32_t v = a << 18 | b << 12 | c << 6 | d << 0; + char x = (v >> 16) & 0xff; + char y = (v >> 8) & 0xff; + char z = (v >> 0) & 0xff; + + LOG_DBG("%c%c%c", x, y, z); + ret[o + 0] = x; + ret[o + 1] = y; + ret[o + 2] = z; + } + + ret[len / 4 * 3] = '\0'; + return ret; + +invalid: + free(ret); + errno = EINVAL; + return NULL; +} + +char * +base64_encode(const uint8_t *data, size_t size) +{ + xassert(size % 3 == 0); + if (unlikely(size % 3 != 0)) + return NULL; + + char *ret = malloc(size / 3 * 4 + 1); + if (unlikely(ret == NULL)) + return NULL; + + for (size_t i = 0, o = 0; i < size; i += 3, o += 4) { + int x = data[i + 0]; + int y = data[i + 1]; + int z = data[i + 2]; + + uint32_t v = x << 16 | y << 8 | z << 0; + + unsigned a = (v >> 18) & 0x3f; + unsigned b = (v >> 12) & 0x3f; + unsigned c = (v >> 6) & 0x3f; + unsigned d = (v >> 0) & 0x3f; + + char c0 = lookup[a]; + char c1 = lookup[b]; + char c2 = lookup[c]; + char c3 = lookup[d]; + + ret[o + 0] = c0; + ret[o + 1] = c1; + ret[o + 2] = c2; + ret[o + 3] = c3; + + LOG_DBG("base64: encode: %c%c%c%c", c0, c1, c2, c3); + } + + ret[size / 3 * 4] = '\0'; + return ret; +} + +void +base64_encode_final(const uint8_t *data, size_t size, char result[4]) +{ + xassert(size > 0); + xassert(size < 3); + + uint32_t v = 0; + if (size >= 1) + v |= data[0] << 16; + if (size >= 2) + v |= data[1] << 8; + + unsigned a = (v >> 18) & 0x3f; + unsigned b = (v >> 12) & 0x3f; + unsigned c = (v >> 6) & 0x3f; + + char c0 = lookup[a]; + char c1 = lookup[b]; + char c2 = size == 2 ? lookup[c] : '='; + char c3 = '='; + + result[0] = c0; + result[1] = c1; + result[2] = c2; + result[3] = c3; + + LOG_DBG("base64: encode: %c%c%c%c", c0, c1, c2, c3); +} diff --git a/base64.h b/base64.h new file mode 100644 index 0000000..3fa3d07 --- /dev/null +++ b/base64.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +char *base64_decode(const char *s, size_t *out_len); +char *base64_encode(const uint8_t *data, size_t size); +void base64_encode_final(const uint8_t *data, size_t size, char result[4]); diff --git a/box-drawing.c b/box-drawing.c new file mode 100644 index 0000000..e69d964 --- /dev/null +++ b/box-drawing.c @@ -0,0 +1,3450 @@ +#include "box-drawing.h" + +#include +#include +#include + +#define LOG_MODULE "box-drawing" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "config.h" +#include "macros.h" +#include "stride.h" +#include "terminal.h" +#include "util.h" +#include "xmalloc.h" + +#define clamp(x, lower, upper) (min(upper, max(x, lower))) + +enum thickness { + LIGHT, + HEAVY, +}; + +struct buf { + uint8_t *data; + pixman_image_t *pix; + pixman_format_code_t format; + int width; + int height; + int stride; + bool solid_shades; + + int thickness[2]; + + /* For octants, sextants and wedges */ + int x_halfs[2]; + int y_thirds[2]; + + /* For octants */ + int y_quads[3]; +}; + +static const pixman_color_t white = {0xffff, 0xffff, 0xffff, 0xffff}; + +static void +change_buffer_format(struct buf *buf, pixman_format_code_t new_format) +{ + int stride = stride_for_format_and_width(new_format, buf->width); + uint8_t *new_data = xcalloc(buf->height * stride, 1); + pixman_image_t *new_pix = pixman_image_create_bits_no_clear( + new_format, buf->width, buf->height, (uint32_t *)new_data, stride); + + if (new_pix == NULL) { + errno = ENOMEM; + perror(__func__); + abort(); + } + + pixman_image_unref(buf->pix); + free(buf->data); + + buf->data = new_data; + buf->pix = new_pix; + buf->format = new_format; + buf->stride = stride; +} + +static int NOINLINE +_thickness(int base_thickness, enum thickness thick) +{ + int multiplier = thick * 2 + 1; + + xassert(base_thickness >= 1); + xassert((thick == LIGHT && multiplier == 1) || + (thick == HEAVY && multiplier == 3)); + + return base_thickness * multiplier; +} +#define thickness(thick) buf->thickness[thick] + +static void NOINLINE +_hline(struct buf *buf, int x1, int x2, int y, int thick) +{ + pixman_box32_t box = { + .x1 = min(max(x1, 0), buf->width), + .x2 = min(max(x2, 0), buf->width), + .y1 = min(max(y, 0), buf->height), + .y2 = min(max(y + thick, 0), buf->height), + }; + pixman_image_fill_boxes(PIXMAN_OP_SRC, buf->pix, &white, 1, &box); +} + +#define hline(x1, x2, y, thick) _hline(buf, x1, x2, y, thick) + +static void NOINLINE +_vline(struct buf *buf, int y1, int y2, int x, int thick) +{ + pixman_box32_t box = { + .x1 = min(max(x, 0), buf->width), + .x2 = min(max(x + thick, 0), buf->width), + .y1 = min(max(y1, 0), buf->height), + .y2 = min(max(y2, 0), buf->height), + }; + pixman_image_fill_boxes(PIXMAN_OP_SRC, buf->pix, &white, 1, &box); +} + +#define vline(y1, y2, x, thick) _vline(buf, y1, y2, x, thick) + +static void NOINLINE +_rect(struct buf *buf, int x1, int y1, int x2, int y2) +{ + pixman_box32_t box = { + .x1 = min(max(x1, 0), buf->width), + .y1 = min(max(y1, 0), buf->height), + .x2 = min(max(x2, 0), buf->width), + .y2 = min(max(y2, 0), buf->height), + }; + pixman_image_fill_boxes(PIXMAN_OP_SRC, buf->pix, &white, 1, &box); +} + +#define rect(x1, y1, x2, y2) _rect(buf, x1, y1, x2, y2) + +static void NOINLINE +_hline_middle(struct buf *buf, enum thickness _thick) +{ + int thick = thickness(_thick); + hline(0, buf->width, (buf->height - thick) / 2, thick); +} + +static void NOINLINE +_hline_middle_left(struct buf *buf, enum thickness _vthick, enum thickness _hthick) +{ + int vthick = thickness(_vthick); + int hthick = thickness(_hthick); + _hline(buf, 0, (buf->width + vthick) / 2, (buf->height - hthick) / 2, hthick); +} + +static void NOINLINE +_hline_middle_right(struct buf *buf, enum thickness _vthick, enum thickness _hthick) +{ + int vthick = thickness(_vthick); + int hthick = thickness(_hthick); + hline((buf->width - vthick) / 2, buf->width, (buf->height - hthick) / 2, hthick); +} + +static void NOINLINE +_vline_middle(struct buf *buf, enum thickness _thick) +{ + int thick = thickness(_thick); + vline(0, buf->height, (buf->width - thick) / 2, thick); +} + +static void NOINLINE +_vline_middle_up(struct buf *buf, enum thickness _vthick, enum thickness _hthick) +{ + int vthick = thickness(_vthick); + int hthick = thickness(_hthick); + vline(0, (buf->height + hthick) / 2, (buf->width - vthick) / 2, vthick); +} + +static void NOINLINE +_vline_middle_down(struct buf *buf, enum thickness _vthick, enum thickness _hthick) +{ + int vthick = thickness(_vthick); + int hthick = thickness(_hthick); + vline((buf->height - hthick) / 2, buf->height, (buf->width - vthick) / 2, vthick); +} + +#define hline_middle(thick) _hline_middle(buf, thick) +#define hline_middle_left(thick) _hline_middle_left(buf, thick, thick) +#define hline_middle_right(thick) _hline_middle_right(buf, thick, thick) +#define hline_middle_left_mixed(vthick, hthick) _hline_middle_left(buf, vthick, hthick) +#define hline_middle_right_mixed(vthick, hthick) _hline_middle_right(buf, vthick, hthick) +#define vline_middle(thick) _vline_middle(buf, thick) +#define vline_middle_up(thick) _vline_middle_up(buf, thick, thick) +#define vline_middle_down(thick) _vline_middle_down(buf, thick, thick) +#define vline_middle_up_mixed(vthick, hthick) _vline_middle_up(buf, vthick, hthick) +#define vline_middle_down_mixed(vthick, hthick) _vline_middle_down(buf, vthick, hthick) + +static void +draw_box_drawings_light_horizontal(struct buf *buf) +{ + hline_middle(LIGHT); +} + +static void +draw_box_drawings_heavy_horizontal(struct buf *buf) +{ + hline_middle(HEAVY); +} + +static void +draw_box_drawings_light_vertical(struct buf *buf) +{ + vline_middle(LIGHT); +} + +static void +draw_box_drawings_heavy_vertical(struct buf *buf) +{ + vline_middle(HEAVY); +} + +static void +draw_box_drawings_dash_horizontal(struct buf *buf, int count, int thick, int gap) +{ + int width = buf->width; + int height = buf->height; + + xassert(count >= 2 && count <= 4); + const int gap_count = count - 1; + + int dash_width = (width - (gap_count * gap)) / count; + while (dash_width <= 0 && gap > 1) { + gap--; + dash_width = (width - (gap_count * gap)) / count; + } + + if (dash_width <= 0) { + hline_middle(LIGHT); + return; + } + + xassert(count * dash_width + gap_count * gap <= width); + + int remaining = width - count * dash_width - gap_count * gap; + + int x[4] = {0}; + int w[4] = {dash_width, dash_width, dash_width, dash_width}; + + x[0] = 0; + + x[1] = x[0] + w[0] + gap; + if (count == 2) + w[1] = width - x[1]; + else if (count == 3) + w[1] += remaining; + else + w[1] += remaining / 2; + + if (count >= 3) { + x[2] = x[1] + w[1] + gap; + if (count == 3) + w[2] = width - x[2]; + else + w[2] += remaining - remaining / 2; + } + + if (count >= 4) { + x[3] = x[2] + w[2] + gap; + w[3] = width - x[3]; + } + + hline(x[0], x[0] + w[0], (height - thick) / 2, thick); + hline(x[1], x[1] + w[1], (height - thick) / 2, thick); + if (count >= 3) + hline(x[2], x[2] + w[2], (height - thick) / 2, thick); + if (count >= 4) + hline(x[3], x[3] + w[3], (height - thick) / 2, thick); +} + +static void +draw_box_drawings_dash_vertical(struct buf *buf, int count, int thick, int gap) +{ + int width = buf->width; + int height = buf->height; + + xassert(count >= 2 && count <= 4); + const int gap_count = count - 1; + + int dash_height = (height - (gap_count * gap)) / count; + while (dash_height <= 0 && gap > 1) { + gap--; + dash_height = (height - (gap_count * gap)) / count; + } + + if (dash_height <= 0) { + vline_middle(LIGHT); + return; + } + + xassert(count * dash_height + gap_count * gap <= height); + + int remaining = height - count * dash_height - gap_count * gap; + + int y[4] = {0}; + int h[4] = {dash_height, dash_height, dash_height, dash_height}; + + y[0] = 0; + + y[1] = y[0] + h[0] + gap; + if (count == 2) + h[1] = height - y[1]; + else if (count == 3) + h[1] += remaining; + else + h[1] += remaining / 2; + + if (count >= 3) { + y[2] = y[1] + h[1] + gap; + if (count == 3) + h[2] = height - y[2]; + else + h[2] += remaining - remaining / 2; + } + + if (count >= 4) { + y[3] = y[2] + h[2] + gap; + h[3] = height - y[3]; + } + + vline(y[0], y[0] + h[0], (width - thick) / 2, thick); + vline(y[1], y[1] + h[1], (width - thick) / 2, thick); + if (count >= 3) + vline(y[2], y[2] + h[2], (width - thick) / 2, thick); + if (count >= 4) + vline(y[3], y[3] + h[3], (width - thick) / 2, thick); +} + +static void +draw_box_drawings_light_triple_dash_horizontal(struct buf *buf) +{ + draw_box_drawings_dash_horizontal(buf, 3, thickness(LIGHT), thickness(LIGHT)); +} + +static void +draw_box_drawings_heavy_triple_dash_horizontal(struct buf *buf) +{ + draw_box_drawings_dash_horizontal(buf, 3, thickness(HEAVY), thickness(LIGHT)); +} + +static void +draw_box_drawings_light_triple_dash_vertical(struct buf *buf) +{ + draw_box_drawings_dash_vertical(buf, 3, thickness(LIGHT), thickness(HEAVY)); +} + +static void +draw_box_drawings_heavy_triple_dash_vertical(struct buf *buf) +{ + draw_box_drawings_dash_vertical(buf, 3, thickness(HEAVY), thickness(HEAVY)); +} + +static void +draw_box_drawings_light_quadruple_dash_horizontal(struct buf *buf) +{ + draw_box_drawings_dash_horizontal(buf, 4, thickness(LIGHT), thickness(LIGHT)); +} + +static void +draw_box_drawings_heavy_quadruple_dash_horizontal(struct buf *buf) +{ + draw_box_drawings_dash_horizontal(buf, 4, thickness(HEAVY), thickness(LIGHT)); +} + +static void +draw_box_drawings_light_quadruple_dash_vertical(struct buf *buf) +{ + draw_box_drawings_dash_vertical(buf, 4, thickness(LIGHT), thickness(LIGHT)); +} + +static void +draw_box_drawings_heavy_quadruple_dash_vertical(struct buf *buf) +{ + draw_box_drawings_dash_vertical(buf, 4, thickness(HEAVY), thickness(LIGHT)); +} + +static void +draw_box_drawings_light_down_and_right(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_light_and_right_heavy(struct buf *buf) +{ + hline_middle_right_mixed(LIGHT, HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_heavy_and_right_light(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle_down_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_heavy_down_and_right(struct buf *buf) +{ + hline_middle_right(HEAVY); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_light_down_and_left(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_light_and_left_heavy(struct buf *buf) +{ + hline_middle_left_mixed(LIGHT, HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_heavy_and_left_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle_down_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_heavy_down_and_left(struct buf *buf) +{ + hline_middle_left(HEAVY); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_light_up_and_right(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_up_light_and_right_heavy(struct buf *buf) +{ + hline_middle_right_mixed(LIGHT, HEAVY); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_up_heavy_and_right_light(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle_up_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_heavy_up_and_right(struct buf *buf) +{ + hline_middle_right(HEAVY); + vline_middle_up(HEAVY); +} + +static void +draw_box_drawings_light_up_and_left(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_up_light_and_left_heavy(struct buf *buf) +{ + hline_middle_left_mixed(LIGHT, HEAVY); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_up_heavy_and_left_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle_up_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_heavy_up_and_left(struct buf *buf) +{ + hline_middle_left(HEAVY); + vline_middle_up(HEAVY); +} + +static void +draw_box_drawings_light_vertical_and_right(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_vertical_light_and_right_heavy(struct buf *buf) +{ + hline_middle_right_mixed(LIGHT, HEAVY); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_up_heavy_and_right_down_light(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle_up_mixed(HEAVY, LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_heavy_and_right_up_light(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle_up(LIGHT); + vline_middle_down_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_vertical_heavy_and_right_light(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_down_light_and_right_up_heavy(struct buf *buf) +{ + hline_middle_right(HEAVY); + vline_middle_up(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_up_light_and_right_down_heavy(struct buf *buf) +{ + hline_middle_right(HEAVY); + vline_middle_up(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_heavy_vertical_and_right(struct buf *buf) +{ + hline_middle_right(HEAVY); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_light_vertical_and_left(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_vertical_light_and_left_heavy(struct buf *buf) +{ + hline_middle_left_mixed(LIGHT, HEAVY); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_up_heavy_and_left_down_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle_up_mixed(HEAVY, LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_heavy_and_left_up_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle_up(LIGHT); + vline_middle_down_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_vertical_heavy_and_left_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_down_light_and_left_up_heavy(struct buf *buf) +{ + hline_middle_left(HEAVY); + vline_middle_up(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_up_light_and_left_down_heavy(struct buf *buf) +{ + hline_middle_left(HEAVY); + vline_middle_up(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_heavy_vertical_and_left(struct buf *buf) +{ + hline_middle_left(HEAVY); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_light_down_and_horizontal(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_left_heavy_and_right_down_light(struct buf *buf) +{ + hline_middle_left_mixed(LIGHT, HEAVY); + hline_middle_right(LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_right_heavy_and_left_down_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right_mixed(LIGHT, HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_light_and_horizontal_heavy(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_heavy_and_horizontal_light(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle_down_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_right_light_and_left_down_heavy(struct buf *buf) +{ + hline_middle_left(HEAVY); + hline_middle_right(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_left_light_and_right_down_heavy(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right(HEAVY); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_heavy_down_and_horizontal(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_light_up_and_horizontal(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_left_heavy_and_right_up_light(struct buf *buf) +{ + hline_middle_left_mixed(LIGHT, HEAVY); + hline_middle_right(LIGHT); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_right_heavy_and_left_up_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right_mixed(LIGHT, HEAVY); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_up_light_and_horizontal_heavy(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_up_heavy_and_horizontal_light(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle_up_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_right_light_and_left_up_heavy(struct buf *buf) +{ + hline_middle_left(HEAVY); + hline_middle_right(LIGHT); + vline_middle_up(HEAVY); +} + +static void +draw_box_drawings_left_light_and_right_up_heavy(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right(HEAVY); + vline_middle_up(HEAVY); +} + +static void +draw_box_drawings_heavy_up_and_horizontal(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle_up(HEAVY); +} + +static void +draw_box_drawings_light_vertical_and_horizontal(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_left_heavy_and_right_vertical_light(struct buf *buf) +{ + hline_middle_left_mixed(LIGHT, HEAVY); + hline_middle_right(LIGHT); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_right_heavy_and_left_vertical_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right_mixed(LIGHT, HEAVY); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_vertical_light_and_horizontal_heavy(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_up_heavy_and_down_horizontal_light(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle_up_mixed(HEAVY, LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_heavy_and_up_horizontal_light(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle_up(LIGHT); + vline_middle_down_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_vertical_heavy_and_horizontal_light(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_left_up_heavy_and_right_down_light(struct buf *buf) +{ + hline_middle_left(HEAVY); + hline_middle_right(LIGHT); + vline_middle_up(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_right_up_heavy_and_left_down_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right(HEAVY); + vline_middle_up(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_left_down_heavy_and_right_up_light(struct buf *buf) +{ + hline_middle_left(HEAVY); + hline_middle_right(LIGHT); + vline_middle_up(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_right_down_heavy_and_left_up_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right(HEAVY); + vline_middle_up(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_down_light_and_up_horizontal_heavy(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle_up(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_up_light_and_down_horizontal_heavy(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle_up(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_right_light_and_left_vertical_heavy(struct buf *buf) +{ + hline_middle_left(HEAVY); + hline_middle_right(LIGHT); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_left_light_and_right_vertical_heavy(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right(HEAVY); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_heavy_vertical_and_horizontal(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_light_double_dash_horizontal(struct buf *buf) +{ + draw_box_drawings_dash_horizontal(buf, 2, thickness(LIGHT), thickness(LIGHT)); +} + +static void +draw_box_drawings_heavy_double_dash_horizontal(struct buf *buf) +{ + draw_box_drawings_dash_horizontal(buf, 2, thickness(HEAVY), thickness(LIGHT)); +} + +static void +draw_box_drawings_light_double_dash_vertical(struct buf *buf) +{ + draw_box_drawings_dash_vertical(buf, 2, thickness(LIGHT), thickness(HEAVY)); +} + +static void +draw_box_drawings_heavy_double_dash_vertical(struct buf *buf) +{ + draw_box_drawings_dash_vertical(buf, 2, thickness(HEAVY), thickness(HEAVY)); +} + +static void +draw_box_drawings_double_horizontal(struct buf *buf) +{ + int thick = thickness(LIGHT); + int mid = (buf->height - thick * 3) / 2; + + hline(0, buf->width, mid, thick); + hline(0, buf->width, mid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_vertical(struct buf *buf) +{ + int thick = thickness(LIGHT); + int mid = (buf->width - thick * 3) / 2; + + vline(0, buf->height, mid, thick); + vline(0, buf->height, mid + 2 * thick, thick); +} + +static void +draw_box_drawings_down_single_and_right_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick) / 2; + + vline_middle_down(LIGHT); + + hline(vmid, buf->width, hmid, thick); + hline(vmid, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_down_double_and_right_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline_middle_right(LIGHT); + + vline(hmid, buf->height, vmid, thick); + vline(hmid, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_down_and_right(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(hmid, buf->height, vmid, thick); + vline(hmid + 2 * thick, buf->height, vmid + 2 * thick, thick); + + hline(vmid, buf->width, hmid, thick); + hline(vmid + 2 * thick, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_down_single_and_left_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width + thick) / 2; + + vline_middle_down(LIGHT); + + hline(0, vmid, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_down_double_and_left_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline_middle_left(LIGHT); + + vline(hmid, buf->height, vmid, thick); + vline(hmid, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_down_and_left(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(hmid + 2 * thick, buf->height, vmid, thick); + vline(hmid, buf->height, vmid + 2 * thick, thick); + + hline(0, vmid + 2 * thick, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_up_single_and_right_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick) / 2; + + vline_middle_up(LIGHT); + + hline(vmid, buf->width, hmid, thick); + hline(vmid, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_up_double_and_right_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height + thick) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline_middle_right(LIGHT); + + vline(0, hmid, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_up_and_right(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(0, hmid + 2 * thick, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); + + hline(vmid + 2 * thick, buf->width, hmid, thick); + hline(vmid, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_up_single_and_left_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width + thick) / 2; + + vline_middle_up(LIGHT); + + hline(0, vmid, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_up_double_and_left_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height + thick) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline_middle_left(LIGHT); + + vline(0, hmid, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_up_and_left(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(0, hmid + 0 * thick + thick, vmid, thick); + vline(0, hmid + 2 * thick + thick, vmid + 2 * thick, thick); + + hline(0, vmid, hmid, thick); + hline(0, vmid + 2 * thick, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_vertical_single_and_right_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick) / 2; + + vline_middle(LIGHT); + + hline(vmid, buf->width, hmid, thick); + hline(vmid, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_vertical_double_and_right_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int vmid = (buf->width - thick * 3) / 2; + + hline(vmid + 2 * thick, buf->width, (buf->height - thick) / 2, thick); + + vline(0, buf->height, vmid, thick); + vline(0, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_vertical_and_right(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(0, buf->height, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); + vline(hmid + 2 * thick, buf->height, vmid + 2 * thick, thick); + + hline(vmid + 2 * thick, buf->width, hmid, thick); + hline(vmid + 2 * thick, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_vertical_single_and_left_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width + thick) / 2; + + vline_middle(LIGHT); + + hline(0, vmid, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_vertical_double_and_left_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int vmid = (buf->width - thick * 3) / 2; + + hline(0, vmid, (buf->height - thick) / 2, thick); + + vline(0, buf->height, vmid, thick); + vline(0, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_vertical_and_left(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(0, buf->height, vmid + 2 * thick, thick); + vline(0, hmid, vmid, thick); + vline(hmid + 2 * thick, buf->height, vmid, thick); + + hline(0, vmid + thick, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_down_single_and_horizontal_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + + vline(hmid + 2 * thick, buf->height, (buf->width - thick) / 2, thick); + + hline(0, buf->width, hmid, thick); + hline(0, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_down_double_and_horizontal_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline_middle(LIGHT); + + vline(hmid, buf->height, vmid, thick); + vline(hmid, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_down_and_horizontal(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline(0, buf->width, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); + hline(vmid + 2 * thick, buf->width, hmid + 2 * thick, thick); + + vline(hmid + 2 * thick, buf->height, vmid, thick); + vline(hmid + 2 * thick, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_up_single_and_horizontal_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick) / 2; + + vline(0, hmid, vmid, thick); + + hline(0, buf->width, hmid, thick); + hline(0, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_up_double_and_horizontal_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline_middle(LIGHT); + + vline(0, hmid, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_up_and_horizontal(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(0, hmid, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); + + hline(0, vmid + thick, hmid, thick); + hline(vmid + 2 * thick, buf->width, hmid, thick); + hline(0, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_vertical_single_and_horizontal_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + + vline_middle(LIGHT); + + hline(0, buf->width, hmid, thick); + hline(0, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_vertical_double_and_horizontal_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int vmid = (buf->width - thick * 3) / 2; + + hline_middle(LIGHT); + + vline(0, buf->height, vmid, thick); + vline(0, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_vertical_and_horizontal(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline(0, vmid, hmid, thick); + hline(vmid + 2 * thick, buf->width, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); + hline(vmid + 2 * thick, buf->width, hmid + 2 * thick, thick); + + vline(0, hmid + thick, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); + vline(hmid + 2 * thick, buf->height, vmid, thick); + vline(hmid + 2 * thick, buf->height, vmid + 2 * thick, thick); +} + +static inline void +set_a1_bit(uint8_t *data, size_t ofs, size_t bit_no) +{ +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + data[ofs] |= 1 << bit_no; +#else + data[ofs] |= 1 << (7 - bit_no); +#endif +} + +static void NOINLINE +draw_box_drawings_light_arc(struct buf *buf, char32_t wc) +{ + const pixman_format_code_t fmt = buf->format; + const int supersample = fmt == PIXMAN_a8 ? 4 : 1; + const int height = buf->height * supersample; + const int width = buf->width * supersample; + const int stride = fmt == PIXMAN_a8 + ? stride_for_format_and_width(PIXMAN_a8, width) : buf->stride; + uint8_t *data = supersample > 1 ? xcalloc(height * stride, 1) : buf->data; + + const int thick = thickness(LIGHT) * supersample; + + const int height_pixels = buf->height; + const int width_pixels = buf->width; + const int thick_pixels = thickness(LIGHT); + + /* + * The general idea here is to connect the two incoming lines using a + * circle, which is extended to the box-edges with vertical/horizontal + * lines. + * + * The radius of the quartercircle should be as big as possible, with some + * restrictions: The radius should be the same for all of ╭ ╮ ╯ ╰ at a + * given box-size (which won't be the case if we choose the biggest + * possible radius for a given box, consider the following:) + * + * + * ▕ ███ ▏ + * ▕a │ d▔▔ + * ▕ │ x + * ▕ │ + * ▕ │ ██ + * ▕ ╰────██ + * ▕ ██ + * ▕ + * ▕ + * ▕b c + * ▔▔▔▔▔▔▔▔▔▔ + * for ╰ it would be possible to center the circle on the upper right + * corner of d, but we have set it on x instead because ╯ can only use a + * 2px inner radius: + * + * ▕ ███ ▏ + * ▔▔a │ d▏ + * x │ ▏ + * │ ▏ + * ██ │ ▏ + * ██───╯ ▏ + * ██ ▏ + * ▏ + * ▏ + * b c▏ + * ▔▔▔▔▔▔▔▔▔▔ + * As the incoming lines always exactly fill pixels, and are rounded down + * (via float->int), we can use this to get the radius of the inner + * (connecting the left/upper edge of the lines) quartercircle. + */ + int circle_inner_edge = (min(width_pixels, height_pixels) - thick_pixels) / 2; + + /* + * We want to draw the quartercircle by filling small circles (with r = + * thickness/2.) whose centers are on its edge. This means to get the + * radius of the quartercircle, we add the exact half thickness to the + * radius of the inner circle. + */ + double c_r = circle_inner_edge + thick_pixels/2.; + + /* + * We need to draw short lines from the end of the quartercircle to the + * box-edges, store one endpoint (the other is the edge of the + * quartercircle) in these vars. + */ + int vert_to = 0, + hor_to = 0; + + /* Coordinates of the circle-center. */ + int c_x = 0, + c_y = 0; + + /* + * For a given y there are up to two solutions for the circle-equation. + * Set to -1 for the left, and 1 for the right hemisphere. + */ + int circle_hemisphere = 0; + + /* + * The quarter circle only has to be evaluated for a small range of + * y-values. + */ + int y_min = 0, + y_max = 0; + + switch (wc) { + case U'╭': { + /* + * Don't use supersampled coordinates yet, we want to align actual + * pixels. + * + * pixel-coordinates of the lower edge of the right line and the + * right edge of the bottom line. + */ + int right_bottom_edge = (height_pixels + thick_pixels) / 2; + int bottom_right_edge = (width_pixels + thick_pixels) / 2; + + /* find coordinates of circle-center. */ + c_y = right_bottom_edge + circle_inner_edge; + c_x = bottom_right_edge + circle_inner_edge; + + /* we want to render the left, not the right hemisphere of the circle. */ + circle_hemisphere = -1; + + /* don't evaluate beyond c_y, the vertical line is drawn there. */ + y_min = 0; + y_max = c_y; + + /* + * the vertical line should extend to the bottom of the box, the + * horizontal to the right. + */ + vert_to = height_pixels; + hor_to = width_pixels; + + break; + } + case U'╮': { + int left_bottom_edge = (height_pixels + thick_pixels) / 2; + int bottom_left_edge = (width_pixels - thick_pixels) / 2; + + c_y = left_bottom_edge + circle_inner_edge; + c_x = bottom_left_edge - circle_inner_edge; + + circle_hemisphere = 1; + + y_min = 0; + y_max = c_y; + + vert_to = height_pixels; + hor_to = 0; + + break; + } + case U'╰': { + int right_top_edge = (height_pixels - thick_pixels) / 2; + int top_right_edge = (width_pixels + thick_pixels) / 2; + + c_y = right_top_edge - circle_inner_edge; + c_x = top_right_edge + circle_inner_edge; + + circle_hemisphere = -1; + + y_min = c_y; + y_max = height_pixels; + + vert_to = 0; + hor_to = width_pixels; + + break; + } + case U'╯': { + int left_top_edge = (height_pixels - thick_pixels) / 2; + int top_left_edge = (width_pixels - thick_pixels) / 2; + + c_y = left_top_edge - circle_inner_edge; + c_x = top_left_edge - circle_inner_edge; + + circle_hemisphere = 1; + + y_min = c_y; + y_max = height_pixels; + + vert_to = 0; + hor_to = 0; + + break; + } + } + + /* store for horizontal+vertical line. */ + int c_x_pixels = c_x; + int c_y_pixels = c_y; + + /* Bring coordinates from pixel-grid to supersampled grid. */ + c_r *= supersample; + c_x *= supersample; + c_y *= supersample; + + y_min *= supersample; + y_max *= supersample; + + double c_r2 = c_r * c_r; + + /* + * To prevent gaps in the circle, each pixel is sampled multiple times. + * As the quartercircle ends (vertically) in the middle of a pixel, an + * uneven number helps hit that exactly. + */ + for (double i = y_min*16; i <= y_max*16; i++) { + errno = 0; + + double y = i / 16.; + double x = circle_hemisphere * sqrt(c_r2 - (y - c_y) * (y - c_y)) + c_x; + + /* See math_error(7) */ + if (errno != 0) + { + continue; + } + + const int row = round(y); + const int col = round(x); + + if (col < 0) + continue; + + /* rectangle big enough to fit entire circle with radius thick/2. */ + int row1 = row - (thick/2+1); + int row2 = row + (thick/2+1); + int col1 = col - (thick/2+1); + int col2 = col + (thick/2+1); + + int row_start = min(row1, row2), + row_end = max(row1, row2), + col_start = min(col1, col2), + col_end = max(col1, col2); + + xassert(row_end > row_start); + xassert(col_end > col_start); + + /* + * draw circle with radius thick/2 around x,y. + * this is accomplished by rejecting pixels where the distance from + * their center to x,y is greater than thick/2. + */ + for (int r = max(row_start, 0); r < max(min(row_end, height), 0); r++) { + double r_midpoint = r + 0.5; + for (int c = max(col_start, 0); c < max(min(col_end, width), 0); c++) { + double c_midpoint = c + 0.5; + + /* vector from point on quartercircle to midpoint of the current pixel. */ + double center_midpoint_x = c_midpoint - x; + double center_midpoint_y = r_midpoint - y; + + /* distance from current point to circle-center. */ + double dist = sqrt(center_midpoint_x * center_midpoint_x + center_midpoint_y * center_midpoint_y); + /* skip if midpoint of pixel is outside the circle. */ + if (dist > thick/2.) + continue; + if (fmt == PIXMAN_a1) { + size_t idx = c / 8; + size_t bit_no = c % 8; + set_a1_bit(data, r * stride + idx, bit_no); + } else + data[r * stride + c] = 0xff; + } + } + } + + if (fmt == PIXMAN_a8) { + xassert(data != buf->data); + + /* Downsample */ + for (size_t r = 0; r < buf->height; r++) { + for (size_t c = 0; c < buf->width; c++) { + uint32_t total = 0; + for (size_t i = 0; i < supersample; i++) { + for (size_t j = 0; j < supersample; j++) + total += data[(r * supersample + i) * stride + c * supersample + j]; + } + uint8_t average = min(total / (supersample * supersample), 0xff); + buf->data[r * buf->stride + c] = average; + } + } + + free(data); + } + + /* draw vertical/horizontal lines from quartercircle-edge to box-edge. */ + vline(min(c_y_pixels, vert_to), max(c_y_pixels, vert_to), (width_pixels - thick_pixels) / 2, thick_pixels); + hline(min(c_x_pixels, hor_to), max(c_x_pixels, hor_to), (height_pixels - thick_pixels) / 2, thick_pixels); +} + +static void NOINLINE +draw_box_drawings_light_diagonal_upper_right_to_lower_left(struct buf *buf) +{ + pixman_trapezoid_t trap = { + .top = pixman_int_to_fixed(0), + .bottom = pixman_int_to_fixed(buf->height), + .left = { + .p1 = { + .x = pixman_double_to_fixed(buf->width - thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(0), + }, + .p2 = { + .x = pixman_double_to_fixed(0 - thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(buf->height), + }, + }, + .right = { + .p1 = { + .x = pixman_double_to_fixed(buf->width + thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(0), + }, + .p2 = { + .x = pixman_double_to_fixed(0 + thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(buf->height), + }, + }, + }; + + pixman_rasterize_trapezoid(buf->pix, &trap, 0, 0); +} + +static void NOINLINE +draw_box_drawings_light_diagonal_upper_left_to_lower_right(struct buf *buf) +{ + pixman_trapezoid_t trap = { + .top = pixman_int_to_fixed(0), + .bottom = pixman_int_to_fixed(buf->height), + .left = { + .p1 = { + .x = pixman_double_to_fixed(0 - thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(0), + }, + .p2 = { + .x = pixman_double_to_fixed(buf->width - thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(buf->height), + }, + }, + .right = { + .p1 = { + .x = pixman_double_to_fixed(0 + thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(0), + }, + .p2 = { + .x = pixman_double_to_fixed(buf->width + thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(buf->height), + }, + }, + }; + + pixman_rasterize_trapezoid(buf->pix, &trap, 0, 0); +} + +static void +draw_box_drawings_light_diagonal_cross(struct buf *buf) +{ + draw_box_drawings_light_diagonal_upper_right_to_lower_left(buf); + draw_box_drawings_light_diagonal_upper_left_to_lower_right(buf); +} + +static void +draw_box_drawings_light_left(struct buf *buf) +{ + hline_middle_left(LIGHT); +} + +static void +draw_box_drawings_light_up(struct buf *buf) +{ + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_light_right(struct buf *buf) +{ + hline_middle_right(LIGHT); +} + +static void +draw_box_drawings_light_down(struct buf *buf) +{ + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_heavy_left(struct buf *buf) +{ + hline_middle_left(HEAVY); +} + +static void +draw_box_drawings_heavy_up(struct buf *buf) +{ + vline_middle_up(HEAVY); +} + +static void +draw_box_drawings_heavy_right(struct buf *buf) +{ + hline_middle_right(HEAVY); +} + +static void +draw_box_drawings_heavy_down(struct buf *buf) +{ + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_light_left_and_heavy_right(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right(HEAVY); +} + +static void +draw_box_drawings_light_up_and_heavy_down(struct buf *buf) +{ + vline_middle_up(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_heavy_left_and_light_right(struct buf *buf) +{ + hline_middle_left(HEAVY); + hline_middle_right(LIGHT); +} + +static void +draw_box_drawings_heavy_up_and_light_down(struct buf *buf) +{ + vline_middle_up(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_upper_half_block(struct buf *buf) +{ + rect(0, 0, buf->width, round(buf->height / 2.)); +} + +static void +draw_lower_one_eighth_block(struct buf *buf) +{ + rect(0, buf->height - round(buf->height / 8.), buf->width, buf->height); +} + +static void +draw_lower_one_quarter_block(struct buf *buf) +{ + rect(0, buf->height - round(buf->height / 4.), buf->width, buf->height); +} + +static void +draw_lower_three_eighths_block(struct buf *buf) +{ + rect(0, buf->height - round(3. * buf->height / 8.), buf->width, buf->height); +} + +static void +draw_lower_half_block(struct buf *buf) +{ + rect(0, buf->height - round(buf->height / 2.), buf->width, buf->height); +} + +static void +draw_lower_five_eighths_block(struct buf *buf) +{ + rect(0, buf->height - round(5. * buf->height / 8.), buf->width, buf->height); +} + +static void +draw_lower_three_quarters_block(struct buf *buf) +{ + rect(0, buf->height - round(3. * buf->height / 4.), buf->width, buf->height); +} + +static void +draw_lower_seven_eighths_block(struct buf *buf) +{ + rect(0, buf->height - round(7. * buf->height / 8.), buf->width, buf->height); +} + +static void +draw_upper_one_quarter_block(struct buf *buf) +{ + rect(0, 0, buf->width, round(buf->height / 4.)); +} + +static void +draw_upper_three_eighths_block(struct buf *buf) +{ + rect(0, 0, buf->width, round(3. * buf->height / 8.)); +} + +static void +draw_upper_five_eighths_block(struct buf *buf) +{ + rect(0, 0, buf->width, round(5. * buf->height / 8.)); +} + +static void +draw_upper_three_quarters_block(struct buf *buf) +{ + rect(0, 0, buf->width, round(3. * buf->height / 4.)); +} + +static void +draw_upper_seven_eighths_block(struct buf *buf) +{ + rect(0, 0, buf->width, round(7. * buf->height / 8.)); +} + +static void +draw_full_block(struct buf *buf) +{ + rect(0, 0, buf->width, buf->height); +} + +static void +draw_left_seven_eighths_block(struct buf *buf) +{ + rect(0, 0, round(7. * buf->width / 8.), buf->height); +} + +static void +draw_left_three_quarters_block(struct buf *buf) +{ + rect(0, 0, round(3. * buf->width / 4.), buf->height); +} + +static void +draw_left_five_eighths_block(struct buf *buf) +{ + rect(0, 0, round(5. * buf->width / 8.), buf->height); +} + +static void +draw_left_half_block(struct buf *buf) +{ + rect(0, 0, round(buf->width / 2.), buf->height); +} + +static void +draw_left_three_eighths_block(struct buf *buf) +{ + rect(0, 0, round(3. * buf->width / 8.), buf->height); +} + +static void +draw_left_one_quarter_block(struct buf *buf) +{ + rect(0, 0, round(buf->width / 4.), buf->height); +} + +static void +draw_vertical_one_eighth_block_n(struct buf *buf, int n) +{ + double x = round((double)n * buf->width / 8.); + double w = round(buf->width / 8.); + rect(x, 0, x + w, buf->height); +} + +static void +draw_left_one_eighth_block(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 0); +} + +static void +draw_vertical_one_eighth_block_2(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 1); +} + +static void +draw_vertical_one_eighth_block_3(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 2); +} + +static void +draw_vertical_one_eighth_block_4(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 3); +} + +static void +draw_vertical_one_eighth_block_5(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 4); +} + +static void +draw_vertical_one_eighth_block_6(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 5); +} + +static void +draw_vertical_one_eighth_block_7(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 6); +} + +static void +draw_right_half_block(struct buf *buf) +{ + rect(round(buf->width / 2.), 0, buf->width, buf->height); +} + +static void NOINLINE +draw_pixman_shade(struct buf *buf, uint16_t v) +{ + pixman_color_t shade = {.red = 0, .green = 0, .blue = 0, .alpha = v}; + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix, &shade, 1, + (pixman_rectangle16_t []){{0, 0, buf->width, buf->height}}); +} + +static void +draw_light_shade(struct buf *buf) +{ + pixman_format_code_t fmt = buf->format; + + if (buf->solid_shades && fmt == PIXMAN_a1) + change_buffer_format(buf, PIXMAN_a8); + else if (!buf->solid_shades && fmt == PIXMAN_a8) + change_buffer_format(buf, PIXMAN_a1); + + if (buf->solid_shades) + draw_pixman_shade(buf, 0x4000); + else { + for (size_t row = 0; row < buf->height; row += 2) { + for (size_t col = 0; col < buf->width; col += 2) { + size_t idx = col / 8; + size_t bit_no = col % 8; + set_a1_bit(buf->data, row * buf->stride + idx, bit_no); + } + } + } +} + +static void +draw_medium_shade(struct buf *buf) +{ + pixman_format_code_t fmt = buf->format; + + if (buf->solid_shades && fmt == PIXMAN_a1) + change_buffer_format(buf, PIXMAN_a8); + else if (!buf->solid_shades && fmt == PIXMAN_a8) + change_buffer_format(buf, PIXMAN_a1); + + if (buf->solid_shades) + draw_pixman_shade(buf, 0x8000); + else { + for (size_t row = 0; row < buf->height; row++) { + for (size_t col = row % 2; col < buf->width; col += 2) { + size_t idx = col / 8; + size_t bit_no = col % 8; + set_a1_bit(buf->data, row * buf->stride + idx, bit_no); + } + } + } +} + +static void +draw_dark_shade(struct buf *buf) +{ + pixman_format_code_t fmt = buf->format; + + if (buf->solid_shades && fmt == PIXMAN_a1) + change_buffer_format(buf, PIXMAN_a8); + else if (!buf->solid_shades && fmt == PIXMAN_a8) + change_buffer_format(buf, PIXMAN_a1); + + if (buf->solid_shades) + draw_pixman_shade(buf, 0xc000); + else { + for (size_t row = 0; row < buf->height; row++) { + for (size_t col = 0; col < buf->width; col += 1 + row % 2) { + size_t idx = col / 8; + size_t bit_no = col % 8; + set_a1_bit(buf->data, row * buf->stride + idx, bit_no); + } + } + } +} + +static void NOINLINE +draw_horizontal_one_eighth_block_n(struct buf *buf, int n) +{ + double y = round((double)n * buf->height / 8.); + double h = round(buf->height / 8.); + rect(0, y, buf->width, y + h); +} + +static void +draw_upper_one_eighth_block(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 0); +} + +static void +draw_horizontal_one_eighth_block_2(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 1); +} + +static void +draw_horizontal_one_eighth_block_3(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 2); +} + +static void +draw_horizontal_one_eighth_block_4(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 3); +} + +static void +draw_horizontal_one_eighth_block_5(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 4); +} + +static void +draw_horizontal_one_eighth_block_6(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 5); +} + +static void +draw_horizontal_one_eighth_block_7(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 6); +} + +static void +draw_right_one_eighth_block(struct buf *buf) +{ + rect(buf->width - round(buf->width / 8.), 0, buf->width, buf->height); +} + +static void +quad_upper_left(struct buf *buf) +{ + rect(0, 0, ceil(buf->width / 2.), ceil(buf->height / 2.)); +} + +static void +quad_upper_right(struct buf *buf) +{ + rect(floor(buf->width / 2.), 0, buf->width, ceil(buf->height / 2.)); +} + +static void +quad_lower_left(struct buf *buf) +{ + rect(0, floor(buf->height / 2.), ceil(buf->width / 2.), buf->height); +} + +static void +quad_lower_right(struct buf *buf) +{ + rect(floor(buf->width / 2.), floor(buf->height / 2.), buf->width, buf->height); +} + +static void NOINLINE +draw_quadrant(struct buf *buf, char32_t wc) +{ + enum { + UPPER_LEFT = 1 << 0, + UPPER_RIGHT = 1 << 1, + LOWER_LEFT = 1 << 2, + LOWER_RIGHT = 1 << 3, + }; + + static const uint8_t matrix[10] = { + LOWER_LEFT, + LOWER_RIGHT, + UPPER_LEFT, + UPPER_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | LOWER_RIGHT, + UPPER_RIGHT, + UPPER_RIGHT | LOWER_LEFT, + UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, + }; + + xassert(wc >= 0x2596 && wc <= 0x259f); + const size_t idx = wc - 0x2596; + + xassert(idx < ALEN(matrix)); + uint8_t encoded = matrix[idx]; + + if (encoded & UPPER_LEFT) + quad_upper_left(buf); + + if (encoded & UPPER_RIGHT) + quad_upper_right(buf); + + if (encoded & LOWER_LEFT) + quad_lower_left(buf); + + if (encoded & LOWER_RIGHT) + quad_lower_right(buf); +} + +static void NOINLINE +draw_braille(struct buf *buf, char32_t wc) +{ + int w = min(buf->width / 4, buf->height / 8); + int x_spacing = buf->width / 4; + int y_spacing = buf->height / 8; + int x_margin = x_spacing / 2; + int y_margin = y_spacing / 2; + + int x_px_left = buf->width - 2 * x_margin - x_spacing - 2 * w; + int y_px_left = buf->height - 2 * y_margin - 3 * y_spacing - 4 * w; + + LOG_DBG( + "braille: before adjusting: " + "cell: %dx%d, margin=%dx%d, spacing=%dx%d, width=%d, left=%dx%d", + buf->width, buf->height, x_margin, y_margin, x_spacing, y_spacing, + w, x_px_left, y_px_left); + + /* First, try hard to ensure the DOT width is non-zero */ + if (x_px_left >= 2 && y_px_left >= 4 && w == 0) { + w++; + x_px_left -= 2; + y_px_left -= 4; + } + + /* Second, prefer a non-zero margin */ + if (x_px_left >= 2 && x_margin == 0) { x_margin = 1; x_px_left -= 2; } + if (y_px_left >= 2 && y_margin == 0) { y_margin = 1; y_px_left -= 2; } + + /* Third, increase spacing */ + if (x_px_left >= 1) { x_spacing++; x_px_left--; } + if (y_px_left >= 3) { y_spacing++; y_px_left -= 3; } + + /* Fourth, margins ("spacing", but on the sides) */ + if (x_px_left >= 2) { x_margin++; x_px_left -= 2; } + if (y_px_left >= 2) { y_margin++; y_px_left -= 2; } + + /* Last - increase dot width */ + if (x_px_left >= 2 && y_px_left >= 4) { + w++; + x_px_left -= 2; + y_px_left -= 4; + } + + LOG_DBG( + "braille: after adjusting: " + "cell: %dx%d, margin=%dx%d, spacing=%dx%d, width=%d, left=%dx%d", + buf->width, buf->height, x_margin, y_margin, x_spacing, y_spacing, + w, x_px_left, y_px_left); + + xassert(x_px_left <= 1 || y_px_left <= 1); + xassert(2 * x_margin + 2 * w + x_spacing <= buf->width); + xassert(2 * y_margin + 4 * w + 3 * y_spacing <= buf->height); + + int x[2], y[4]; + x[0] = x_margin; + x[1] = x_margin + w + x_spacing; + y[0] = y_margin; + y[1] = y[0] + w + y_spacing; + y[2] = y[1] + w + y_spacing; + y[3] = y[2] + w + y_spacing; + + assert(wc >= 0x2800); + assert(wc <= 0x28ff); + uint8_t sym = wc - 0x2800; + + /* Left side */ + if (sym & 1) + rect(x[0], y[0], x[0] + w, y[0] + w); + if (sym & 2) + rect(x[0], y[1], x[0] + w, y[1] + w); + if (sym & 4) + rect(x[0], y[2], x[0] + w, y[2] + w); + + /* Right side */ + if (sym & 8) + rect(x[1], y[0], x[1] + w, y[0] + w); + if (sym & 16) + rect(x[1], y[1], x[1] + w, y[1] + w); + if (sym & 32) + rect(x[1], y[2], x[1] + w, y[2] + w); + + /* 8-dot patterns */ + if (sym & 64) + rect(x[0], y[3], x[0] + w, y[3] + w); + if (sym & 128) + rect(x[1], y[3], x[1] + w, y[3] + w); +} + +static void +sextant_upper_left(struct buf *buf) +{ + rect(0, 0, buf->x_halfs[0], buf->y_thirds[0]); +} + +static void +sextant_middle_left(struct buf *buf) +{ + rect(0, buf->y_thirds[0], buf->x_halfs[0], buf->y_thirds[1]); +} + +static void +sextant_lower_left(struct buf *buf) +{ + rect(0, buf->y_thirds[1], buf->x_halfs[0], buf->height); +} + +static void +sextant_upper_right(struct buf *buf) +{ + rect(buf->x_halfs[1], 0, buf->width, buf->y_thirds[0]); +} + +static void +sextant_middle_right(struct buf *buf) +{ + rect(buf->x_halfs[1], buf->y_thirds[0], buf->width, buf->y_thirds[1]); +} + +static void +sextant_lower_right(struct buf *buf) +{ + rect(buf->x_halfs[1], buf->y_thirds[1], buf->width, buf->height); +} + +static void NOINLINE +draw_sextant(struct buf *buf, char32_t wc) +{ + /* + * Each byte encodes one sextant: + * + * Bit sextant + * 0 upper left + * 1 middle left + * 2 lower left + * 3 upper right + * 4 middle right + * 5 lower right + */ + enum { + UPPER_LEFT = 1 << 0, + MIDDLE_LEFT = 1 << 1, + LOWER_LEFT = 1 << 2, + UPPER_RIGHT = 1 << 3, + MIDDLE_RIGHT = 1 << 4, + LOWER_RIGHT = 1 << 5, + }; + + /* TODO: move this to a separate file? */ + static const uint8_t matrix[60] = { + /* U+1fb00 - U+1fb0f */ + UPPER_LEFT, + UPPER_RIGHT, + UPPER_LEFT | UPPER_RIGHT, + MIDDLE_LEFT, + UPPER_LEFT | MIDDLE_LEFT, + UPPER_RIGHT | MIDDLE_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT, + MIDDLE_RIGHT, + UPPER_LEFT | MIDDLE_RIGHT, + UPPER_RIGHT | MIDDLE_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_RIGHT, + MIDDLE_LEFT | MIDDLE_RIGHT, + UPPER_LEFT | MIDDLE_LEFT | MIDDLE_RIGHT, + UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT, + LOWER_LEFT, + + /* U+1fb10 - U+1fb1f */ + UPPER_LEFT | LOWER_LEFT, + UPPER_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT, + MIDDLE_LEFT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | LOWER_LEFT, + MIDDLE_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_RIGHT | LOWER_LEFT, + MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT, + LOWER_RIGHT, + UPPER_LEFT | LOWER_RIGHT, + + /* U+1fb20 - U+1fb2f */ + UPPER_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | LOWER_RIGHT, + MIDDLE_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | LOWER_RIGHT, + MIDDLE_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_RIGHT | LOWER_RIGHT, + MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, + LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1fb30 - U+1fb3b */ + UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + }; + + xassert(wc >= 0x1fb00 && wc <= 0x1fb3b); + const size_t idx = wc - 0x1fb00; + + xassert(idx < ALEN(matrix)); + uint8_t encoded = matrix[idx]; + + if (encoded & UPPER_LEFT) + sextant_upper_left(buf); + + if (encoded & MIDDLE_LEFT) + sextant_middle_left(buf); + + if (encoded & LOWER_LEFT) + sextant_lower_left(buf); + + if (encoded & UPPER_RIGHT) + sextant_upper_right(buf); + + if (encoded & MIDDLE_RIGHT) + sextant_middle_right(buf); + + if (encoded & LOWER_RIGHT) + sextant_lower_right(buf); +} + +static void +octant_upper_left(struct buf *buf) +{ + rect(0, 0, buf->x_halfs[0], buf->y_quads[0]); +} + +static void +octant_middle_up_left(struct buf *buf) +{ + rect(0, buf->y_quads[0], buf->x_halfs[0], buf->y_quads[1]); +} + +static void +octant_middle_down_left(struct buf *buf) +{ + rect(0, buf->y_quads[1], buf->x_halfs[0], buf->y_quads[2]); +} + +static void +octant_lower_left(struct buf *buf) +{ + rect(0, buf->y_quads[2], buf->x_halfs[0], buf->height); +} + +static void +octant_upper_right(struct buf *buf) +{ + rect(buf->x_halfs[1], 0, buf->width, buf->y_quads[0]); +} + +static void +octant_middle_up_right(struct buf *buf) +{ + rect(buf->x_halfs[1], buf->y_quads[0], buf->width, buf->y_quads[1]); +} + +static void +octant_middle_down_right(struct buf *buf) +{ + rect(buf->x_halfs[1], buf->y_quads[1], buf->width, buf->y_quads[2]); +} + +static void +octant_lower_right(struct buf *buf) +{ + rect(buf->x_halfs[1], buf->y_quads[2], buf->width, buf->height); +} + +static void NOINLINE +draw_octant(struct buf *buf, char32_t wc) +{ + /* + * Each byte encodes one octant: + * + * Bit octant part + * 0 upper left + * 1 middle, upper left + * 2 middle, lower left + * 3 lower, left + * 4 upper right + * 5 middle, upper right + * 6 middle, lower right + * 7 lower right + */ + enum { + UPPER_LEFT = 1 << 0, + MIDDLE_UP_LEFT = 1 << 1, + MIDDLE_DOWN_LEFT = 1 << 2, + LOWER_LEFT = 1 << 3, + UPPER_RIGHT = 1 << 4, + MIDDLE_UP_RIGHT = 1 << 5, + MIDDLE_DOWN_RIGHT = 1 << 6, + LOWER_RIGHT = 1 << 7, + }; + + /* TODO: move this to a separate file */ + static const uint8_t matrix[230] = { + /* U+1CD00 - U+1CD0F */ + MIDDLE_UP_LEFT, + MIDDLE_UP_LEFT | UPPER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | UPPER_RIGHT, + MIDDLE_UP_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT, + MIDDLE_DOWN_LEFT, + UPPER_LEFT | MIDDLE_DOWN_LEFT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT, + + /* U+1CD10 - U+1CD1F */ + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, + + /* U+1CD20 - U+1CD2F */ + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + + /* U+1CD30 - U+1CD3F */ + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | LOWER_LEFT, + UPPER_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT, + MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT, + + /* U+1CD40 - U+1CD4F */ + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + + /* U+1CD50 - U+1CD5F */ + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + + /* U+1CD60 - U+1CD6F */ + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + + /* U+1CD70 - U+1CD7F */ + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + + /* U+1CD80 - U+1CD8F */ + MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + + /* U+1CD90 - U+1CD9F */ + UPPER_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + + /* U+1CDA0 - U+1CDAF */ + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1CDB0 - U+1CDBF */ + UPPER_LEFT | MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1CDC0 - U+1CDCF */ + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1CDD0 - U+1CDDF */ + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1CDE0 - U+1CDE5 */ + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + }; + + _Static_assert(ALEN(matrix) == 230, "incorrect number of codepoints"); + +#if defined(_DEBUG) + const size_t last_implemented = 0x1cde5; + for (size_t i = 0; i < sizeof(matrix) / sizeof(matrix[0]); i++) { + if (i + 0x1cd00 > last_implemented) + break; + + for (size_t j = 0; j < sizeof(matrix) / sizeof(matrix[0]); j++) { + if (j + 0x1cd00 > last_implemented) + break; + + if (i == j) + continue; + + if (matrix[i] == matrix[j]) { + BUG("octant U+%05x (idx=%zu) is the same as U+%05x (idx=%zu)", + matrix[i], i, matrix[j], j); + } + } + } +#endif + + xassert(wc >= 0x1cd00 && wc <= 0x1cde5); + const size_t idx = wc - 0x1cd00; + + xassert(idx < ALEN(matrix)); + uint8_t encoded = matrix[idx]; + + if (encoded & UPPER_LEFT) + octant_upper_left(buf); + + if (encoded & MIDDLE_UP_LEFT) + octant_middle_up_left(buf); + + if (encoded & MIDDLE_DOWN_LEFT) + octant_middle_down_left(buf); + + if (encoded & LOWER_LEFT) + octant_lower_left(buf); + + if (encoded & UPPER_RIGHT) + octant_upper_right(buf); + + if (encoded & MIDDLE_UP_RIGHT) + octant_middle_up_right(buf); + + if (encoded & MIDDLE_DOWN_RIGHT) + octant_middle_down_right(buf); + + if (encoded & LOWER_RIGHT) + octant_lower_right(buf); +} + +static void NOINLINE +draw_wedge_triangle(struct buf *buf, char32_t wc) +{ + const int width = buf->width; + const int height = buf->height; + + int halfs0 = buf->x_halfs[0]; + int halfs1 = buf->x_halfs[1]; + int thirds0 = buf->y_thirds[0]; + int thirds1 = buf->y_thirds[1]; + + int p1_x, p1_y, p2_x, p2_y, p3_x, p3_y; + + switch (wc) { + case 0x1fb3c: /* 🬼 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb52: /* 🭒 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb3d: /* 🬽 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb53: /* 🭓 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb3e: /* 🬾 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb54: /* 🭔 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb3f: /* 🬿 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb55: /* 🭕 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb40: /* 🭀 */ + case 0x1fb56: /* 🭖 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = 0; p2_y = p3_y = height; + break; + + case 0x1fb47: /* 🭇 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb5d: /* 🭝 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb48: /* 🭈 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb5e: /* 🭞 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb49: /* 🭉 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb5f: /* 🭟 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb4a: /* 🭊 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb60: /* 🭠 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb4b: /* 🭋 */ + case 0x1fb61: /* 🭡 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = 0; p2_y = p3_y = height; + break; + + case 0x1fb57: /* 🭗 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb41: /* 🭁 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb58: /* 🭘 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb42: /* 🭂 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb59: /* 🭙 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb43: /* 🭃 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb5a: /* 🭚 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb44: /* 🭄 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb5b: /* 🭛 */ + case 0x1fb45: /* 🭅 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = p3_y = 0; p2_y = height; + break; + + case 0x1fb62: /* 🭢 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb4c: /* 🭌 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb63: /* 🭣 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb4d: /* 🭍 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb64: /* 🭤 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb4e: /* 🭎 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb65: /* 🭥 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb4f: /* 🭏 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb66: /* 🭦 */ + case 0x1fb50: /* 🭐 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = p3_y = 0; p2_y = height; + break; + + case 0x1fb46: /* 🭆 */ + p1_x = 0; p1_y = thirds1; + p2_x = width; p2_y = thirds0; + p3_x = width; p3_y = p1_y; + break; + + case 0x1fb51: /* 🭑 */ + p1_x = 0; p1_y = thirds0; + p2_x = 0; p2_y = thirds1; + p3_x = width; p3_y = p2_y; + break; + + case 0x1fb5c: /* 🭜 */ + p1_x = 0; p1_y = thirds0; + p2_x = 0; p2_y = thirds1; + p3_x = width; p3_y = p1_y; + break; + + case 0x1fb67: /* 🭧 */ + p1_x = 0; p1_y = thirds0; + p2_x = width; p2_y = p1_y; + p3_x = width; p3_y = thirds1; + break; + + case 0x1fb6c: /* 🭬 */ + case 0x1fb68: /* 🭨 */ + p1_x = 0; p1_y = 0; + p2_x = halfs0; p2_y = height / 2; + p3_x = 0; p3_y = height; + break; + + case 0x1fb6d: /* 🭭 */ + case 0x1fb69: /* 🭩 */ + p1_x = 0; p1_y = 0; + p2_x = halfs1; p2_y = height / 2; + p3_x = width; p3_y = 0; + break; + + case 0x1fb6e: /* 🭮 */ + case 0x1fb6a: /* 🭪 */ + p1_x = width; p1_y = 0; + p2_x = halfs1; p2_y = height / 2; + p3_x = width; p3_y = height; + break; + + case 0x1fb6f: /* 🭯 */ + case 0x1fb6b: /* 🭫 */ + p1_x = 0; p1_y = height; + p2_x = halfs1; p2_y = height / 2; + p3_x = width; p3_y = height; + break; + + default: + BUG("unimplemented Unicode codepoint"); + break; + } + + const pixman_triangle_t tri = { + .p1 = {.x = pixman_int_to_fixed(p1_x), .y = pixman_int_to_fixed(p1_y)}, + .p2 = {.x = pixman_int_to_fixed(p2_x), .y = pixman_int_to_fixed(p2_y)}, + .p3 = {.x = pixman_int_to_fixed(p3_x), .y = pixman_int_to_fixed(p3_y)}, + }; + + pixman_image_t *src = pixman_image_create_solid_fill(&white); + pixman_composite_triangles( + PIXMAN_OP_OVER, src, buf->pix, buf->format, 0, 0, 0, 0, 1, &tri); + pixman_image_unref(src); +} + +static void NOINLINE +draw_wedge_triangle_inverted(struct buf *buf, char32_t wc) +{ + draw_wedge_triangle(buf, wc); + + pixman_image_t *src = pixman_image_create_solid_fill(&white); + pixman_image_composite(PIXMAN_OP_OUT, src, NULL, buf->pix, 0, 0, 0, 0, 0, 0, buf->width, buf->height); + pixman_image_unref(src); +} + +static void NOINLINE +draw_wedge_triangle_and_box(struct buf *buf, char32_t wc) +{ + draw_wedge_triangle(buf, wc); + + const int width = buf->width; + const int height = buf->height; + + pixman_box32_t box; + + switch (wc) { + case 0x1fb46: + case 0x1fb51: + box = (pixman_box32_t){ + .x1 = 0, .y1 = buf->y_thirds[1], + .x2 = width, .y2 = height, + }; + break; + + case 0x1fb5c: + case 0x1fb67: + box = (pixman_box32_t){ + .x1 = 0, .y1 = 0, + .x2 = width, .y2 = buf->y_thirds[0], + }; + break; + } + + pixman_image_fill_boxes(PIXMAN_OP_SRC, buf->pix, &white, 1, &box); +} + +static void +draw_left_and_lower_one_eighth_block(struct buf *buf) +{ + draw_left_one_eighth_block(buf); + draw_lower_one_eighth_block(buf); +} + +static void +draw_left_and_upper_one_eighth_block(struct buf *buf) +{ + draw_left_one_eighth_block(buf); + draw_upper_one_eighth_block(buf); +} + +static void +draw_right_and_upper_one_eighth_block(struct buf *buf) +{ + draw_right_one_eighth_block(buf); + draw_upper_one_eighth_block(buf); +} + +static void +draw_right_and_lower_one_eighth_block(struct buf *buf) +{ + draw_right_one_eighth_block(buf); + draw_lower_one_eighth_block(buf); +} + +static void +draw_upper_and_lower_one_eighth_block(struct buf *buf) +{ + draw_upper_one_eighth_block(buf); + draw_lower_one_eighth_block(buf); +} + +static void +draw_horizontal_one_eighth_1358_block(struct buf *buf) +{ + draw_upper_one_eighth_block(buf); + draw_horizontal_one_eighth_block_3(buf); + draw_horizontal_one_eighth_block_5(buf); + draw_lower_one_eighth_block(buf); +} + +static void +draw_right_one_quarter_block(struct buf *buf) +{ + rect(buf->width - round(buf->width / 4.), 0, buf->width, buf->height); +} + +static void +draw_right_three_eighths_block(struct buf *buf) +{ + rect(buf->width - round(3. * buf->width / 8.), 0, buf->width, buf->height); +} + +static void +draw_right_five_eighths_block(struct buf *buf) +{ + rect(buf->width - round(5. * buf->width / 8.), 0, buf->width, buf->height); +} + +static void +draw_right_three_quarters_block(struct buf *buf) +{ + rect(buf->width - round(3. * buf->width / 4.), 0, buf->width, buf->height); +} + +static void +draw_right_seven_eighths_block(struct buf *buf) +{ + rect(buf->width - round(7. * buf->width / 8.), 0, buf->width, buf->height); +} + +static void +draw_glyph(struct buf *buf, char32_t wc) +{ + IGNORE_WARNING("-Wpedantic") + + switch (wc) { + case 0x2500: draw_box_drawings_light_horizontal(buf); break; + case 0x2501: draw_box_drawings_heavy_horizontal(buf); break; + case 0x2502: draw_box_drawings_light_vertical(buf); break; + case 0x2503: draw_box_drawings_heavy_vertical(buf); break; + case 0x2504: draw_box_drawings_light_triple_dash_horizontal(buf); break; + case 0x2505: draw_box_drawings_heavy_triple_dash_horizontal(buf); break; + case 0x2506: draw_box_drawings_light_triple_dash_vertical(buf); break; + case 0x2507: draw_box_drawings_heavy_triple_dash_vertical(buf); break; + case 0x2508: draw_box_drawings_light_quadruple_dash_horizontal(buf); break; + case 0x2509: draw_box_drawings_heavy_quadruple_dash_horizontal(buf); break; + case 0x250a: draw_box_drawings_light_quadruple_dash_vertical(buf); break; + case 0x250b: draw_box_drawings_heavy_quadruple_dash_vertical(buf); break; + case 0x250c: draw_box_drawings_light_down_and_right(buf); break; + case 0x250d: draw_box_drawings_down_light_and_right_heavy(buf); break; + case 0x250e: draw_box_drawings_down_heavy_and_right_light(buf); break; + case 0x250f: draw_box_drawings_heavy_down_and_right(buf); break; + + case 0x2510: draw_box_drawings_light_down_and_left(buf); break; + case 0x2511: draw_box_drawings_down_light_and_left_heavy(buf); break; + case 0x2512: draw_box_drawings_down_heavy_and_left_light(buf); break; + case 0x2513: draw_box_drawings_heavy_down_and_left(buf); break; + case 0x2514: draw_box_drawings_light_up_and_right(buf); break; + case 0x2515: draw_box_drawings_up_light_and_right_heavy(buf); break; + case 0x2516: draw_box_drawings_up_heavy_and_right_light(buf); break; + case 0x2517: draw_box_drawings_heavy_up_and_right(buf); break; + case 0x2518: draw_box_drawings_light_up_and_left(buf); break; + case 0x2519: draw_box_drawings_up_light_and_left_heavy(buf); break; + case 0x251a: draw_box_drawings_up_heavy_and_left_light(buf); break; + case 0x251b: draw_box_drawings_heavy_up_and_left(buf); break; + case 0x251c: draw_box_drawings_light_vertical_and_right(buf); break; + case 0x251d: draw_box_drawings_vertical_light_and_right_heavy(buf); break; + case 0x251e: draw_box_drawings_up_heavy_and_right_down_light(buf); break; + case 0x251f: draw_box_drawings_down_heavy_and_right_up_light(buf); break; + + case 0x2520: draw_box_drawings_vertical_heavy_and_right_light(buf); break; + case 0x2521: draw_box_drawings_down_light_and_right_up_heavy(buf); break; + case 0x2522: draw_box_drawings_up_light_and_right_down_heavy(buf); break; + case 0x2523: draw_box_drawings_heavy_vertical_and_right(buf); break; + case 0x2524: draw_box_drawings_light_vertical_and_left(buf); break; + case 0x2525: draw_box_drawings_vertical_light_and_left_heavy(buf); break; + case 0x2526: draw_box_drawings_up_heavy_and_left_down_light(buf); break; + case 0x2527: draw_box_drawings_down_heavy_and_left_up_light(buf); break; + case 0x2528: draw_box_drawings_vertical_heavy_and_left_light(buf); break; + case 0x2529: draw_box_drawings_down_light_and_left_up_heavy(buf); break; + case 0x252a: draw_box_drawings_up_light_and_left_down_heavy(buf); break; + case 0x252b: draw_box_drawings_heavy_vertical_and_left(buf); break; + case 0x252c: draw_box_drawings_light_down_and_horizontal(buf); break; + case 0x252d: draw_box_drawings_left_heavy_and_right_down_light(buf); break; + case 0x252e: draw_box_drawings_right_heavy_and_left_down_light(buf); break; + case 0x252f: draw_box_drawings_down_light_and_horizontal_heavy(buf); break; + + case 0x2530: draw_box_drawings_down_heavy_and_horizontal_light(buf); break; + case 0x2531: draw_box_drawings_right_light_and_left_down_heavy(buf); break; + case 0x2532: draw_box_drawings_left_light_and_right_down_heavy(buf); break; + case 0x2533: draw_box_drawings_heavy_down_and_horizontal(buf); break; + case 0x2534: draw_box_drawings_light_up_and_horizontal(buf); break; + case 0x2535: draw_box_drawings_left_heavy_and_right_up_light(buf); break; + case 0x2536: draw_box_drawings_right_heavy_and_left_up_light(buf); break; + case 0x2537: draw_box_drawings_up_light_and_horizontal_heavy(buf); break; + case 0x2538: draw_box_drawings_up_heavy_and_horizontal_light(buf); break; + case 0x2539: draw_box_drawings_right_light_and_left_up_heavy(buf); break; + case 0x253a: draw_box_drawings_left_light_and_right_up_heavy(buf); break; + case 0x253b: draw_box_drawings_heavy_up_and_horizontal(buf); break; + case 0x253c: draw_box_drawings_light_vertical_and_horizontal(buf); break; + case 0x253d: draw_box_drawings_left_heavy_and_right_vertical_light(buf); break; + case 0x253e: draw_box_drawings_right_heavy_and_left_vertical_light(buf); break; + case 0x253f: draw_box_drawings_vertical_light_and_horizontal_heavy(buf); break; + + case 0x2540: draw_box_drawings_up_heavy_and_down_horizontal_light(buf); break; + case 0x2541: draw_box_drawings_down_heavy_and_up_horizontal_light(buf); break; + case 0x2542: draw_box_drawings_vertical_heavy_and_horizontal_light(buf); break; + case 0x2543: draw_box_drawings_left_up_heavy_and_right_down_light(buf); break; + case 0x2544: draw_box_drawings_right_up_heavy_and_left_down_light(buf); break; + case 0x2545: draw_box_drawings_left_down_heavy_and_right_up_light(buf); break; + case 0x2546: draw_box_drawings_right_down_heavy_and_left_up_light(buf); break; + case 0x2547: draw_box_drawings_down_light_and_up_horizontal_heavy(buf); break; + case 0x2548: draw_box_drawings_up_light_and_down_horizontal_heavy(buf); break; + case 0x2549: draw_box_drawings_right_light_and_left_vertical_heavy(buf); break; + case 0x254a: draw_box_drawings_left_light_and_right_vertical_heavy(buf); break; + case 0x254b: draw_box_drawings_heavy_vertical_and_horizontal(buf); break; + case 0x254c: draw_box_drawings_light_double_dash_horizontal(buf); break; + case 0x254d: draw_box_drawings_heavy_double_dash_horizontal(buf); break; + case 0x254e: draw_box_drawings_light_double_dash_vertical(buf); break; + case 0x254f: draw_box_drawings_heavy_double_dash_vertical(buf); break; + + case 0x2550: draw_box_drawings_double_horizontal(buf); break; + case 0x2551: draw_box_drawings_double_vertical(buf); break; + case 0x2552: draw_box_drawings_down_single_and_right_double(buf); break; + case 0x2553: draw_box_drawings_down_double_and_right_single(buf); break; + case 0x2554: draw_box_drawings_double_down_and_right(buf); break; + case 0x2555: draw_box_drawings_down_single_and_left_double(buf); break; + case 0x2556: draw_box_drawings_down_double_and_left_single(buf); break; + case 0x2557: draw_box_drawings_double_down_and_left(buf); break; + case 0x2558: draw_box_drawings_up_single_and_right_double(buf); break; + case 0x2559: draw_box_drawings_up_double_and_right_single(buf); break; + case 0x255a: draw_box_drawings_double_up_and_right(buf); break; + case 0x255b: draw_box_drawings_up_single_and_left_double(buf); break; + case 0x255c: draw_box_drawings_up_double_and_left_single(buf); break; + case 0x255d: draw_box_drawings_double_up_and_left(buf); break; + case 0x255e: draw_box_drawings_vertical_single_and_right_double(buf); break; + case 0x255f: draw_box_drawings_vertical_double_and_right_single(buf); break; + + case 0x2560: draw_box_drawings_double_vertical_and_right(buf); break; + case 0x2561: draw_box_drawings_vertical_single_and_left_double(buf); break; + case 0x2562: draw_box_drawings_vertical_double_and_left_single(buf); break; + case 0x2563: draw_box_drawings_double_vertical_and_left(buf); break; + case 0x2564: draw_box_drawings_down_single_and_horizontal_double(buf); break; + case 0x2565: draw_box_drawings_down_double_and_horizontal_single(buf); break; + case 0x2566: draw_box_drawings_double_down_and_horizontal(buf); break; + case 0x2567: draw_box_drawings_up_single_and_horizontal_double(buf); break; + case 0x2568: draw_box_drawings_up_double_and_horizontal_single(buf); break; + case 0x2569: draw_box_drawings_double_up_and_horizontal(buf); break; + case 0x256a: draw_box_drawings_vertical_single_and_horizontal_double(buf); break; + case 0x256b: draw_box_drawings_vertical_double_and_horizontal_single(buf); break; + case 0x256c: draw_box_drawings_double_vertical_and_horizontal(buf); break; + case 0x256d ... 0x2570: draw_box_drawings_light_arc(buf, wc); break; + + case 0x2571: draw_box_drawings_light_diagonal_upper_right_to_lower_left(buf); break; + case 0x2572: draw_box_drawings_light_diagonal_upper_left_to_lower_right(buf); break; + case 0x2573: draw_box_drawings_light_diagonal_cross(buf); break; + case 0x2574: draw_box_drawings_light_left(buf); break; + case 0x2575: draw_box_drawings_light_up(buf); break; + case 0x2576: draw_box_drawings_light_right(buf); break; + case 0x2577: draw_box_drawings_light_down(buf); break; + case 0x2578: draw_box_drawings_heavy_left(buf); break; + case 0x2579: draw_box_drawings_heavy_up(buf); break; + case 0x257a: draw_box_drawings_heavy_right(buf); break; + case 0x257b: draw_box_drawings_heavy_down(buf); break; + case 0x257c: draw_box_drawings_light_left_and_heavy_right(buf); break; + case 0x257d: draw_box_drawings_light_up_and_heavy_down(buf); break; + case 0x257e: draw_box_drawings_heavy_left_and_light_right(buf); break; + case 0x257f: draw_box_drawings_heavy_up_and_light_down(buf); break; + + case 0x2580: draw_upper_half_block(buf); break; + case 0x2581: draw_lower_one_eighth_block(buf); break; + case 0x2582: draw_lower_one_quarter_block(buf); break; + case 0x2583: draw_lower_three_eighths_block(buf); break; + case 0x2584: draw_lower_half_block(buf); break; + case 0x2585: draw_lower_five_eighths_block(buf); break; + case 0x2586: draw_lower_three_quarters_block(buf); break; + case 0x2587: draw_lower_seven_eighths_block(buf); break; + case 0x2588: draw_full_block(buf); break; + case 0x2589: draw_left_seven_eighths_block(buf); break; + case 0x258a: draw_left_three_quarters_block(buf); break; + case 0x258b: draw_left_five_eighths_block(buf); break; + case 0x258c: draw_left_half_block(buf); break; + case 0x258d: draw_left_three_eighths_block(buf); break; + case 0x258e: draw_left_one_quarter_block(buf); break; + case 0x258f: draw_left_one_eighth_block(buf); break; + + case 0x2590: draw_right_half_block(buf); break; + case 0x2591: draw_light_shade(buf); break; + case 0x2592: draw_medium_shade(buf); break; + case 0x2593: draw_dark_shade(buf); break; + case 0x2594: draw_upper_one_eighth_block(buf); break; + case 0x2595: draw_right_one_eighth_block(buf); break; + case 0x2596 ... 0x259f: draw_quadrant(buf, wc); break; + + case 0x2800 ... 0x28ff: draw_braille(buf, wc); break; + + case 0x1cd00 ... 0x1cde5: draw_octant(buf, wc); break; + case 0x1fb00 ... 0x1fb3b: draw_sextant(buf, wc); break; + + case 0x1fb3c ... 0x1fb40: + case 0x1fb47 ... 0x1fb4b: + case 0x1fb57 ... 0x1fb5b: + case 0x1fb62 ... 0x1fb66: + case 0x1fb6c ... 0x1fb6f: + draw_wedge_triangle(buf, wc); + break; + + case 0x1fb41 ... 0x1fb45: + case 0x1fb4c ... 0x1fb50: + case 0x1fb52 ... 0x1fb56: + case 0x1fb5d ... 0x1fb61: + case 0x1fb68 ... 0x1fb6b: + draw_wedge_triangle_inverted(buf, wc); + break; + + case 0x1fb46: + case 0x1fb51: + case 0x1fb5c: + case 0x1fb67: + draw_wedge_triangle_and_box(buf, wc); + break; + + case 0x1fb9a: + draw_wedge_triangle(buf, 0x1fb6d); + draw_wedge_triangle(buf, 0x1fb6f); + break; + + case 0x1fb9b: + draw_wedge_triangle(buf, 0x1fb6c); + draw_wedge_triangle(buf, 0x1fb6e); + break; + + case 0x1fb70: draw_vertical_one_eighth_block_2(buf); break; + case 0x1fb71: draw_vertical_one_eighth_block_3(buf); break; + case 0x1fb72: draw_vertical_one_eighth_block_4(buf); break; + case 0x1fb73: draw_vertical_one_eighth_block_5(buf); break; + case 0x1fb74: draw_vertical_one_eighth_block_6(buf); break; + case 0x1fb75: draw_vertical_one_eighth_block_7(buf); break; + + case 0x1fb76: draw_horizontal_one_eighth_block_2(buf); break; + case 0x1fb77: draw_horizontal_one_eighth_block_3(buf); break; + case 0x1fb78: draw_horizontal_one_eighth_block_4(buf); break; + case 0x1fb79: draw_horizontal_one_eighth_block_5(buf); break; + case 0x1fb7a: draw_horizontal_one_eighth_block_6(buf); break; + case 0x1fb7b: draw_horizontal_one_eighth_block_7(buf); break; + + case 0x1fb82: draw_upper_one_quarter_block(buf); break; + case 0x1fb83: draw_upper_three_eighths_block(buf); break; + case 0x1fb84: draw_upper_five_eighths_block(buf); break; + case 0x1fb85: draw_upper_three_quarters_block(buf); break; + case 0x1fb86: draw_upper_seven_eighths_block(buf); break; + + case 0x1fb7c: draw_left_and_lower_one_eighth_block(buf); break; + case 0x1fb7d: draw_left_and_upper_one_eighth_block(buf); break; + case 0x1fb7e: draw_right_and_upper_one_eighth_block(buf); break; + case 0x1fb7f: draw_right_and_lower_one_eighth_block(buf); break; + case 0x1fb80: draw_upper_and_lower_one_eighth_block(buf); break; + case 0x1fb81: draw_horizontal_one_eighth_1358_block(buf); break; + + case 0x1fb87: draw_right_one_quarter_block(buf); break; + case 0x1fb88: draw_right_three_eighths_block(buf); break; + case 0x1fb89: draw_right_five_eighths_block(buf); break; + case 0x1fb8a: draw_right_three_quarters_block(buf); break; + case 0x1fb8b: draw_right_seven_eighths_block(buf); break; + } + + UNIGNORE_WARNINGS +} + +struct fcft_glyph * COLD +box_drawing(const struct terminal *term, char32_t wc) +{ + int width = term->cell_width; + int height = term->cell_height; + + pixman_format_code_t fmt = + term->fonts[0]->antialias ? PIXMAN_a8 : PIXMAN_a1; + + int stride = stride_for_format_and_width(fmt, width); + uint8_t *data = xcalloc(height * stride, 1); + + pixman_image_t *pix = pixman_image_create_bits_no_clear( + fmt, width, height, (uint32_t*)data, stride); + + if (pix == NULL) { + errno = ENOMEM; + perror(__func__); + abort(); + } + + double dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + double scale = term->font_is_sized_by_dpi ? 1. : term->scale; + double cell_size = sqrt(pow(term->cell_width, 2) + pow(term->cell_height, 2)); + + int base_thickness = + (double)term->conf->tweak.box_drawing_base_thickness * scale * cell_size * dpi / 72.0; + base_thickness = max(base_thickness, 1); + + int y_third_0 = 0, y_third_1 = 0; + switch (height % 3) { + case 0: + y_third_0 = height / 3; + y_third_1 = 2 * height / 3; + break; + + case 1: + y_third_0 = height / 3; + y_third_1 = 2 * height / 3 + 1; + break; + + case 2: + y_third_0 = height / 3 + 1; + y_third_1 = y_third_0 + height / 3; + break; + } + + /* TODO */ + int y_quad_0 = 0, y_quad_1 = 0, y_quad_2 = 0; + switch (height % 4) { + case 0: + y_quad_0 = height / 4; + y_quad_1 = height / 2; + y_quad_2 = 3 * height / 4; + break; + + case 1: + y_quad_0 = height / 4; + y_quad_1 = height / 2; + y_quad_2 = 3 * height / 4; + break; + case 2: + y_quad_0 = height / 4; + y_quad_1 = height / 2; + y_quad_2 = 3 * height / 4; + break; + + case 3: + y_quad_0 = height / 4; + y_quad_1 = height / 2; + y_quad_2 = 3 * height / 4; + break; + } + + struct buf buf = { + .data = data, + .pix = pix, + .format = fmt, + .width = width, + .height = height, + .stride = stride, + .solid_shades = term->conf->tweak.box_drawing_solid_shades, + + .thickness = { + [LIGHT] = _thickness(base_thickness, LIGHT), + [HEAVY] = _thickness(base_thickness, HEAVY), + }, + + /* Overlap when width is odd */ + .x_halfs = { + round(width / 2.), /* Endpoint first half */ + width / 2, /* Startpoint second half */ + }, + + .y_thirds = { + y_third_0, /* Endpoint first third, start point second third */ + y_third_1, /* Endpoint second third, start point last third */ + }, + + .y_quads = { + y_quad_0, + y_quad_1, + y_quad_2, + }, + }; + + LOG_DBG("LIGHT=%d, HEAVY=%d", buf.thickness[LIGHT], buf.thickness[HEAVY]); + + draw_glyph(&buf, wc); + + struct fcft_glyph *glyph = xmalloc(sizeof(*glyph)); + *glyph = (struct fcft_glyph){ + .cp = wc, + .cols = 1, + .pix = buf.pix, + .x = -term->font_x_ofs, + .y = term->font_baseline, + .width = width, + .height = height, + .advance = { + .x = width, + .y = height, + }, + }; + return glyph; +} diff --git a/box-drawing.h b/box-drawing.h new file mode 100644 index 0000000..d7413bd --- /dev/null +++ b/box-drawing.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include + +struct terminal; +struct fcft_glyph *box_drawing(const struct terminal *term, char32_t wc); diff --git a/char32.c b/char32.c new file mode 100644 index 0000000..be5bf22 --- /dev/null +++ b/char32.c @@ -0,0 +1,432 @@ +#include "char32.h" + +#include +#include +#include + +#include +#include + +#if defined __has_include + #if __has_include () + #include + #endif +#endif + +#define LOG_MODULE "char32" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "macros.h" +#include "xmalloc.h" + +/* + * For now, assume we can map directly to the corresponding wchar_t + * functions. This is true if: + * + * - both data types have the same size + * - both use the same encoding (though we require that encoding to be UTF-32) + */ + +_Static_assert( + sizeof(wchar_t) == sizeof(char32_t), "wchar_t vs. char32_t size mismatch"); + +#if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__ + #error "char32_t does not use UTF-32" +#endif +#if (!defined(__STDC_ISO_10646__) || !__STDC_ISO_10646__) && !defined(__FreeBSD__) && !defined(__OpenBSD__) + #error "wchar_t does not use UTF-32" +#endif + +UNITTEST +{ + xassert(c32len(U"") == 0); + xassert(c32len(U"foobar") == 6); +} + +UNITTEST +{ + xassert(c32cmp(U"foobar", U"foobar") == 0); + xassert(c32cmp(U"foo", U"foobar") < 0); + xassert(c32cmp(U"foobar", U"foo") > 0); + xassert(c32cmp(U"a", U"b") < 0); + xassert(c32cmp(U"b", U"a") > 0); +} + +UNITTEST +{ + xassert(c32ncmp(U"foo", U"foot", 3) == 0); + xassert(c32ncmp(U"foot", U"FOOT", 4) > 0); + xassert(c32ncmp(U"a", U"b", 1) < 0); + xassert(c32ncmp(U"bb", U"aa", 2) > 0); +} + +UNITTEST +{ + char32_t copy[16]; + char32_t *ret = c32ncpy(copy, U"foobar", 16); + + xassert(ret == copy); + xassert(copy[0] == U'f'); + xassert(copy[1] == U'o'); + xassert(copy[2] == U'o'); + xassert(copy[3] == U'b'); + xassert(copy[4] == U'a'); + xassert(copy[5] == U'r'); + + unsigned char zeroes[(16 - 6) * sizeof(copy[0])] = {0}; + xassert(memcmp(©[6], zeroes, sizeof(zeroes)) == 0); +} + +UNITTEST +{ + char32_t copy[16]; + memset(copy, 0x55, sizeof(copy)); + + char32_t *ret = c32cpy(copy, U"foobar"); + + xassert(ret == copy); + xassert(copy[0] == U'f'); + xassert(copy[1] == U'o'); + xassert(copy[2] == U'o'); + xassert(copy[3] == U'b'); + xassert(copy[4] == U'a'); + xassert(copy[5] == U'r'); + xassert(copy[6] == U'\0'); + + unsigned char fives[(16 - 6 - 1) * sizeof(copy[0])]; + memset(fives, 0x55, sizeof(fives)); + xassert(memcmp(©[7], fives, sizeof(fives)) == 0); +} + +UNITTEST +{ + xassert(c32casecmp(U"foobar", U"FOOBAR") == 0); + xassert(c32casecmp(U"foo", U"FOOO") < 0); + xassert(c32casecmp(U"FOOO", U"foo") > 0); + xassert(c32casecmp(U"a", U"B") < 0); + xassert(c32casecmp(U"B", U"a") > 0); +} + +UNITTEST +{ + xassert(c32ncasecmp(U"foo", U"FOObar", 3) == 0); + xassert(c32ncasecmp(U"foo", U"FOOO", 4) < 0); + xassert(c32ncasecmp(U"FOOO", U"foo", 4) > 0); + xassert(c32ncasecmp(U"a", U"BB", 1) < 0); + xassert(c32ncasecmp(U"BB", U"a", 1) > 0); +} + +UNITTEST +{ + char32_t dst[32] = U"foobar"; + char32_t *ret = c32ncat(dst, U"12345678XXXXXXXXX", 8); + + xassert(ret == dst); + xassert(c32cmp(dst, U"foobar12345678") == 0); +} + +UNITTEST +{ + char32_t dst[32] = U"foobar"; + char32_t *ret = c32cat(dst, U"12345678"); + + xassert(ret == dst); + xassert(c32cmp(dst, U"foobar12345678") == 0); +} + +UNITTEST +{ + xassert(!isc32upper(U'a')); + xassert(isc32upper(U'A')); + xassert(!isc32upper(U'a')); +} + +UNITTEST +{ + xassert(hasc32upper(U"abc1A")); + xassert(!hasc32upper(U"abc1_aaa")); + xassert(!hasc32upper(U"")); +} + +UNITTEST +{ + char32_t *c = xc32dup(U"foobar"); + xassert(c32cmp(c, U"foobar") == 0); + free(c); + + c = xc32dup(U""); + xassert(c32cmp(c, U"") == 0); + free(c); +} + +size_t +mbsntoc32(char32_t *dst, const char *src, size_t nms, size_t len) +{ + mbstate_t ps = {0}; + + char32_t *out = dst; + const char *in = src; + + size_t consumed = 0; + size_t chars = 0; + size_t rc; + + while ((out == NULL || chars < len) && + consumed < nms && + (rc = mbrtoc32(out, in, nms - consumed, &ps)) != 0) + { + switch (rc) { + case 0: + goto done; + + case (size_t)-1: + case (size_t)-2: + case (size_t)-3: + goto err; + } + + in += rc; + consumed += rc; + chars++; + + if (out != NULL) + out++; + } + +done: + return chars; + +err: + return (size_t)-1; +} + +UNITTEST +{ + const char input[] = "foobarzoo"; + char32_t c32[32]; + + size_t ret = mbsntoc32(NULL, input, sizeof(input), 0); + xassert(ret == 9); + + memset(c32, 0x55, sizeof(c32)); + ret = mbsntoc32(c32, input, sizeof(input), 32); + + xassert(ret == 9); + xassert(c32[0] == U'f'); + xassert(c32[1] == U'o'); + xassert(c32[2] == U'o'); + xassert(c32[3] == U'b'); + xassert(c32[4] == U'a'); + xassert(c32[5] == U'r'); + xassert(c32[6] == U'z'); + xassert(c32[7] == U'o'); + xassert(c32[8] == U'o'); + xassert(c32[9] == U'\0'); + xassert(c32[10] == 0x55555555); + + memset(c32, 0x55, sizeof(c32)); + ret = mbsntoc32(c32, input, 1, 32); + + xassert(ret == 1); + xassert(c32[0] == U'f'); + xassert(c32[1] == 0x55555555); + + memset(c32, 0x55, sizeof(c32)); + ret = mbsntoc32(c32, input, sizeof(input), 1); + + xassert(ret == 1); + xassert(c32[0] == U'f'); + xassert(c32[1] == 0x55555555); +} + +UNITTEST +{ + const char input[] = "foobarzoo"; + char32_t c32[32]; + + size_t ret = mbstoc32(NULL, input, 0); + xassert(ret == 9); + + memset(c32, 0x55, sizeof(c32)); + ret = mbstoc32(c32, input, 32); + + xassert(ret == 9); + xassert(c32[0] == U'f'); + xassert(c32[1] == U'o'); + xassert(c32[2] == U'o'); + xassert(c32[3] == U'b'); + xassert(c32[4] == U'a'); + xassert(c32[5] == U'r'); + xassert(c32[6] == U'z'); + xassert(c32[7] == U'o'); + xassert(c32[8] == U'o'); + xassert(c32[9] == U'\0'); + xassert(c32[10] == 0x55555555); + + memset(c32, 0x55, sizeof(c32)); + ret = mbstoc32(c32, input, 1); + + xassert(ret == 1); + xassert(c32[0] == U'f'); + xassert(c32[1] == 0x55555555); +} + + +char32_t * +ambstoc32(const char *src) +{ + if (src == NULL) + return NULL; + + const size_t src_len = strlen(src); + + char32_t *ret = xmalloc((src_len + 1) * sizeof(ret[0])); + mbstate_t ps = {0}; + + char32_t *out = ret; + const char *in = src; + const char *const end = src + src_len + 1; + + size_t chars = 0; + size_t rc; + + while ((rc = mbrtoc32(out, in, end - in, &ps)) != 0) { + switch (rc) { + case (size_t)-1: + case (size_t)-2: + case (size_t)-3: + goto err; + } + + in += rc; + out++; + chars++; + } + + *out = U'\0'; + + ret = xrealloc(ret, (chars + 1) * sizeof(ret[0])); + return ret; + +err: + free(ret); + return NULL; +} + +UNITTEST +{ + const char* locale = setlocale(LC_CTYPE, "en_US.UTF-8"); + if (!locale) + locale = setlocale(LC_CTYPE, "C.UTF-8"); + if (!locale) + return; + + char32_t *hello = ambstoc32(u8"hello"); + xassert(hello != NULL); + xassert(hello[0] == U'h'); + xassert(hello[1] == U'e'); + xassert(hello[2] == U'l'); + xassert(hello[3] == U'l'); + xassert(hello[4] == U'o'); + xassert(hello[5] == U'\0'); + free(hello); + + char32_t *swedish = ambstoc32(u8"åäö"); + xassert(swedish != NULL); + xassert(swedish[0] == U'å'); + xassert(swedish[1] == U'ä'); + xassert(swedish[2] == U'ö'); + xassert(swedish[3] == U'\0'); + free(swedish); + + char32_t *emoji = ambstoc32(u8"👨‍👩‍👧‍👦"); + xassert(emoji != NULL); + xassert(emoji[0] == U'👨'); + xassert(emoji[1] == U'‍'); + xassert(emoji[2] == U'👩'); + xassert(emoji[3] == U'‍'); + xassert(emoji[4] == U'👧'); + xassert(emoji[5] == U'‍'); + xassert(emoji[6] == U'👦'); + xassert(emoji[7] == U'\0'); + free(emoji); + + xassert(ambstoc32(NULL) == NULL); + xassert(setlocale(LC_CTYPE, "C") != NULL); +} + +char * +ac32tombs(const char32_t *src) +{ + if (src == NULL) + return NULL; + + const size_t src_len = c32len(src); + + size_t allocated = src_len + 1; + char *ret = xmalloc(allocated); + mbstate_t ps = {0}; + + char *out = ret; + const char32_t *const end = src + src_len + 1; + + size_t bytes = 0; + + char mb[MB_CUR_MAX]; + + for (const char32_t *in = src; in < end; in++) { + size_t rc = c32rtomb(mb, *in, &ps); + + switch (rc) { + case (size_t)-1: + goto err; + } + + if (bytes + rc > allocated) { + allocated *= 2; + ret = xrealloc(ret, allocated); + out = &ret[bytes]; + } + + for (size_t i = 0; i < rc; i++, out++) + *out = mb[i]; + + bytes += rc; + } + + xassert(ret[bytes - 1] == '\0'); + ret = xrealloc(ret, bytes); + return ret; + +err: + free(ret); + return NULL; +} + +UNITTEST +{ + const char* locale = setlocale(LC_CTYPE, "en_US.UTF-8"); + if (!locale) + locale = setlocale(LC_CTYPE, "C.UTF-8"); + if (!locale) + return; + + char *s = ac32tombs(U"foobar"); + xassert(s != NULL); + xassert(strcmp(s, "foobar") == 0); + free(s); + + s = ac32tombs(U"åäö"); + xassert(s != NULL); + xassert(strcmp(s, u8"åäö") == 0); + free(s); + + s = ac32tombs(U"👨‍👩‍👧‍👦"); + xassert(s != NULL); + xassert(strcmp(s, u8"👨‍👩‍👧‍👦") == 0); + free(s); + + xassert(ac32tombs(NULL) == NULL); + xassert(setlocale(LC_CTYPE, "C") != NULL); +} diff --git a/char32.h b/char32.h new file mode 100644 index 0000000..dcb412c --- /dev/null +++ b/char32.h @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#if defined(FOOT_GRAPHEME_CLUSTERING) + #include +#endif + +static inline size_t c32len(const char32_t *s) { + return wcslen((const wchar_t *)s); +} + +static inline int c32cmp(const char32_t *s1, const char32_t *s2) { + return wcscmp((const wchar_t *)s1, (const wchar_t *)s2); +} + +static inline int c32ncmp(const char32_t *s1, const char32_t *s2, size_t n) { + return wcsncmp((const wchar_t *)s1, (const wchar_t *)s2, n); +} + +static inline char32_t *c32ncpy(char32_t *dst, const char32_t *src, size_t n) { + return (char32_t *)wcsncpy((wchar_t *)dst, (const wchar_t *)src, n); +} + +static inline char32_t *c32cpy(char32_t *dst, const char32_t *src) { + return (char32_t *)wcscpy((wchar_t *)dst, (const wchar_t *)src); +} + +static inline char32_t *c32ncat(char32_t *dst, const char32_t *src, size_t n) { + return (char32_t *)wcsncat((wchar_t *)dst, (const wchar_t *)src, n); +} + +static inline char32_t *c32cat(char32_t *dst, const char32_t *src) { + return (char32_t *)wcscat((wchar_t *)dst, (const wchar_t *)src); +} + +static inline char32_t *c32dup(const char32_t *s) { + return (char32_t *)wcsdup((const wchar_t *)s); +} + +static inline char32_t *c32chr(const char32_t *s, char32_t c) { + return (char32_t *)wcschr((const wchar_t *)s, c); +} + +static inline int c32casecmp(const char32_t *s1, const char32_t *s2) { + return wcscasecmp((const wchar_t *)s1, (const wchar_t *)s2); +} + +static inline int c32ncasecmp(const char32_t *s1, const char32_t *s2, size_t n) { + return wcsncasecmp((const wchar_t *)s1, (const wchar_t *)s2, n); +} + +static inline char32_t toc32lower(char32_t c) { + return (char32_t)towlower((wint_t)c); +} + +static inline char32_t toc32upper(char32_t c) { + return (char32_t)towupper((wint_t)c); +} + +static inline bool isc32upper(char32_t c32) { + return iswupper((wint_t)c32); +} + +static inline bool isc32space(char32_t c32) { + return iswspace((wint_t)c32); +} + +static inline bool isc32print(char32_t c32) { + return iswprint((wint_t)c32); +} + +static inline bool isc32graph(char32_t c32) { + return iswgraph((wint_t)c32); +} + +static inline bool hasc32upper(const char32_t *s) { + for (int i = 0; s[i] != '\0'; i++) { + if (isc32upper(s[i])) return true; + } + return false; +} + +static inline int c32width(char32_t c) { +#if defined(FOOT_GRAPHEME_CLUSTERING) + return utf8proc_charwidth((utf8proc_int32_t)c); +#else + return wcwidth((wchar_t)c); +#endif +} + +static inline int c32swidth(const char32_t *s, size_t n) { +#if defined(FOOT_GRAPHEME_CLUSTERING) + int width = 0; + for (size_t i = 0; i < n; i++) + width += utf8proc_charwidth((utf8proc_int32_t)s[i]); + return width; +#else + return wcswidth((const wchar_t *)s, n); +#endif +} + +size_t mbsntoc32(char32_t *dst, const char *src, size_t nms, size_t len); +char32_t *ambstoc32(const char *src); +char *ac32tombs(const char32_t *src); + +static inline size_t mbstoc32(char32_t *dst, const char *src, size_t len) { + return mbsntoc32(dst, src, strlen(src) + 1, len); +} diff --git a/client-protocol.h b/client-protocol.h new file mode 100644 index 0000000..efd601d --- /dev/null +++ b/client-protocol.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +struct client_string { + uint16_t len; + /* char str[static len]; */ +}; + +struct client_data { + bool hold:1; + bool no_wait:1; + bool xdga_token:1; + uint8_t reserved:5; + + uint8_t token_len; + uint16_t cwd_len; + uint16_t override_count; + uint16_t argc; + uint16_t env_count; + + /* char cwd[static cwd_len]; */ + /* char token[static token_len]; */ + /* struct client_string overrides[static override_count]; */ + /* struct client_string argv[static argc]; */ + /* struct client_string envp[static env_count]; */ +} __attribute__((packed)); + +_Static_assert(sizeof(struct client_data) == 10, "protocol struct size error"); + +enum client_ipc_code { + FOOT_IPC_SIGUSR, +}; + +struct client_ipc_hdr { + enum client_ipc_code ipc_code; + uint8_t size; +} __attribute__((packed)); + + +struct client_ipc_sigusr { + int signo; +} __attribute__((packed)); diff --git a/client.c b/client.c new file mode 100644 index 0000000..befd3ab --- /dev/null +++ b/client.c @@ -0,0 +1,604 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#define LOG_MODULE "foot-client" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "client-protocol.h" +#include "debug.h" +#include "foot-features.h" +#include "macros.h" +#include "util.h" +#include "xmalloc.h" + +extern char **environ; + +struct string { + size_t len; + char *str; +}; +typedef tll(struct string) string_list_t; + +static volatile sig_atomic_t aborted = 0; +static volatile sig_atomic_t sigusr = 0; + +static void +sigint_handler(int signo) +{ + aborted = 1; +} + +static void +sigusr_handler(int signo) +{ + sigusr = signo; +} + +static ssize_t +sendall(int sock, const void *_buf, size_t len) +{ + const uint8_t *buf = _buf; + size_t left = len; + + while (left > 0) { + ssize_t r = send(sock, buf, left, MSG_NOSIGNAL); + if (r < 0) { + if (errno == EINTR) + continue; + return r; + } + + buf += r; + left -= r; + } + + return len; +} + +static void +print_usage(const char *prog_name) +{ + static const char options[] = + "\nOptions:\n" + " -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n" + " -T,--title=TITLE initial window title (foot)\n" + " -a,--app-id=ID window application ID (foot)\n" + " --toplevel-tag=TAG set a custom toplevel tag\n" + " -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n" + " -W,--window-size-chars=WIDTHxHEIGHT initial width and height, in characters\n" + " -m,--maximized start in maximized mode\n" + " -F,--fullscreen start in fullscreen mode\n" + " -L,--login-shell start shell as a login shell\n" + " -D,--working-directory=DIR directory to start in (CWD)\n" + " -s,--server-socket=PATH path to the server UNIX domain socket (default=$XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock)\n" + " -H,--hold remain open after child process exits\n" + " -N,--no-wait detach the client process from the running terminal, exiting immediately\n" + " -o,--override=[section.]key=value override configuration option\n" + " -E, --client-environment exec shell using footclient's environment, instead of the server's\n" + " -d,--log-level={info|warning|error|none} log level (warning)\n" + " -l,--log-colorize=[{never|always|auto}] enable/disable colorization of log output on stderr\n" + " -v,--version show the version number and quit\n" + " -e ignored (for compatibility with xterm -e)\n"; + + printf("Usage: %s [OPTIONS...]\n", prog_name); + printf("Usage: %s [OPTIONS...] command [ARGS...]\n", prog_name); + puts(options); +} + +static bool NOINLINE +push_string(string_list_t *string_list, const char *s, uint64_t *total_len) +{ + size_t len = strlen(s) + 1; + if (len >= 1 << (8 * sizeof(uint16_t))) { + LOG_ERR("string length overflow"); + return false; + } + + struct string o = {len, xstrdup(s)}; + tll_push_back(*string_list, o); + *total_len += sizeof(struct client_string) + o.len; + return true; +} + +static void +free_string_list(string_list_t *string_list) +{ + tll_foreach(*string_list, it) { + free(it->item.str); + tll_remove(*string_list, it); + } +} + +static bool +send_string_list(int fd, const string_list_t *string_list) +{ + tll_foreach(*string_list, it) { + const struct client_string s = {it->item.len}; + if (sendall(fd, &s, sizeof(s)) < 0 || + sendall(fd, it->item.str, s.len) < 0) + { + LOG_ERRNO("failed to send setup packet to server"); + return false; + } + } + + return true; +} + +enum { + TOPLEVEL_TAG_OPTION = CHAR_MAX + 1, +}; + +int +main(int argc, char *const *argv) +{ + /* Custom exit code, to enable users to differentiate between foot + * itself failing, and the client application failing */ + static const int foot_exit_failure = -36; + int ret = foot_exit_failure; + + const char *const prog_name = argc > 0 ? argv[0] : ""; + + static const struct option longopts[] = { + {"term", required_argument, NULL, 't'}, + {"title", required_argument, NULL, 'T'}, + {"app-id", required_argument, NULL, 'a'}, + {"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION}, + {"window-size-pixels", required_argument, NULL, 'w'}, + {"window-size-chars", required_argument, NULL, 'W'}, + {"maximized", no_argument, NULL, 'm'}, + {"fullscreen", no_argument, NULL, 'F'}, + {"login-shell", no_argument, NULL, 'L'}, + {"working-directory", required_argument, NULL, 'D'}, + {"server-socket", required_argument, NULL, 's'}, + {"hold", no_argument, NULL, 'H'}, + {"no-wait", no_argument, NULL, 'N'}, + {"override", required_argument, NULL, 'o'}, + {"client-environment", no_argument, NULL, 'E'}, + {"log-level", required_argument, NULL, 'd'}, + {"log-colorize", optional_argument, NULL, 'l'}, + {"version", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {NULL, no_argument, NULL, 0}, + }; + + const char *custom_cwd = NULL; + const char *server_socket_path = NULL; + enum log_class log_level = LOG_CLASS_WARNING; + enum log_colorize log_colorize = LOG_COLORIZE_AUTO; + bool hold = false; + bool client_environment = false; + + /* Used to format overrides */ + bool no_wait = false; + + /* For XDG activation */ + const char *token = getenv("XDG_ACTIVATION_TOKEN"); + bool xdga_token = token != NULL; + size_t token_len = xdga_token ? strlen(token) + 1 : 0; + + char buf[1024]; + + /* Total packet length, not (yet) including overrides or argv[] */ + uint64_t total_len = 0; + + /* malloc:ed and needs to be in scope of all goto's */ + int fd = -1; + char *_cwd = NULL; + struct client_string *cargv = NULL; + string_list_t overrides = tll_init(); + string_list_t envp = tll_init(); + + while (true) { + int c = getopt_long(argc, argv, "+t:T:a:w:W:mFLD:s:HNo:Ed:l::veh", longopts, NULL); + if (c == -1) + break; + + switch (c) { + case 't': + snprintf(buf, sizeof(buf), "term=%s", optarg); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + + case 'T': + snprintf(buf, sizeof(buf), "title=%s", optarg); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + + case 'a': + snprintf(buf, sizeof(buf), "app-id=%s", optarg); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + + case TOPLEVEL_TAG_OPTION: + snprintf(buf, sizeof(buf), "toplevel-tag=%s", optarg); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + + case 'L': + if (!push_string(&overrides, "login-shell=yes", &total_len)) + goto err; + break; + + case 'D': { + struct stat st; + if (stat(optarg, &st) < 0 || !(st.st_mode & S_IFDIR)) { + fprintf(stderr, "error: %s: not a directory\n", optarg); + goto err; + } + custom_cwd = optarg; + break; + } + + case 'w': { + unsigned width, height; + if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { + fprintf(stderr, "error: invalid window-size-pixels: %s\n", optarg); + goto err; + } + + snprintf(buf, sizeof(buf), "initial-window-size-pixels=%ux%u", width, height); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + } + + case 'W': { + unsigned width, height; + if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { + fprintf(stderr, "error: invalid window-size-chars: %s\n", optarg); + goto err; + } + + snprintf(buf, sizeof(buf), "initial-window-size-chars=%ux%u", width, height); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + } + + case 'm': + if (!push_string(&overrides, "initial-window-mode=maximized", &total_len)) + goto err; + break; + + case 'F': + if (!push_string(&overrides, "initial-window-mode=fullscreen", &total_len)) + goto err; + break; + + case 's': + server_socket_path = optarg; + break; + + case 'H': + hold = true; + break; + + case 'N': + no_wait = true; + break; + + case 'o': + if (!push_string(&overrides, optarg, &total_len)) + goto err; + break; + + case 'E': + client_environment = true; + break; + + case 'd': { + int lvl = log_level_from_string(optarg); + if (unlikely(lvl < 0)) { + fprintf( + stderr, + "-d,--log-level: %s: argument must be one of %s\n", + optarg, + log_level_string_hint()); + goto err; + } + log_level = lvl; + break; + } + + case 'l': + if (optarg == NULL || streq(optarg, "auto")) + log_colorize = LOG_COLORIZE_AUTO; + else if (streq(optarg, "never")) + log_colorize = LOG_COLORIZE_NEVER; + else if (streq(optarg, "always")) + log_colorize = LOG_COLORIZE_ALWAYS; + else { + fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg); + goto err; + } + break; + + case 'v': + print_version_and_features("footclient "); + ret = EXIT_SUCCESS; + goto err; + + case 'h': + print_usage(prog_name); + ret = EXIT_SUCCESS; + goto err; + + case 'e': + break; + + case '?': + goto err; + } + } + + if (argc > 0) { + argc -= optind; + argv += optind; + } + + log_init(log_colorize, false, LOG_FACILITY_USER, log_level); + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) { + LOG_ERRNO("failed to create socket"); + goto err; + } + + struct sockaddr_un addr = {.sun_family = AF_UNIX}; + + if (server_socket_path != NULL) { + strncpy(addr.sun_path, server_socket_path, sizeof(addr.sun_path) - 1); + if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { + LOG_ERR("%s: failed to connect (is 'foot --server' running?)", server_socket_path); + goto err; + } + } else { + bool connected = false; + + const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); + if (xdg_runtime != NULL) { + const char *wayland_display = getenv("WAYLAND_DISPLAY"); + if (wayland_display != NULL) { + snprintf(addr.sun_path, sizeof(addr.sun_path), + "%s/foot-%s.sock", xdg_runtime, wayland_display); + connected = (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0); + } + if (!connected) { + LOG_WARN("%s: failed to connect, will now try %s/foot.sock", + addr.sun_path, xdg_runtime); + snprintf(addr.sun_path, sizeof(addr.sun_path), + "%s/foot.sock", xdg_runtime); + connected = (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0); + } + if (!connected) + LOG_WARN("%s: failed to connect, will now try /tmp/foot.sock", addr.sun_path); + } + + if (!connected) { + strncpy(addr.sun_path, "/tmp/foot.sock", sizeof(addr.sun_path) - 1); + if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { + LOG_ERRNO("failed to connect (is 'foot --server' running?)"); + goto err; + } + } + } + + const char *cwd = custom_cwd; + if (cwd == NULL) { + size_t buf_len = 1024; + do { + _cwd = xrealloc(_cwd, buf_len); + errno = 0; + if (getcwd(_cwd, buf_len) == NULL && errno != ERANGE) { + LOG_ERRNO("failed to get current working directory"); + goto err; + } + buf_len *= 2; + } while (errno == ERANGE); + cwd = _cwd; + } + + const char *pwd = getenv("PWD"); + if (pwd != NULL) { + char *resolved_path_cwd = realpath(cwd, NULL); + char *resolved_path_pwd = realpath(pwd, NULL); + + if (resolved_path_cwd != NULL && + resolved_path_pwd != NULL && + streq(resolved_path_cwd, resolved_path_pwd)) + { + /* + * The resolved path of $PWD matches the resolved path of + * the *actual* working directory - use $PWD. + * + * This makes a difference when $PWD refers to a symlink. + */ + cwd = pwd; + } + + free(resolved_path_cwd); + free(resolved_path_pwd); + } + + if (client_environment) { + for (char **e = environ; *e != NULL; e++) { + if (!push_string(&envp, *e, &total_len)) + goto err; + } + } + + /* String lengths, including NULL terminator */ + const size_t cwd_len = strlen(cwd) + 1; + const size_t override_count = tll_length(overrides); + const size_t env_count = tll_length(envp); + + const struct client_data data = { + .hold = hold, + .no_wait = no_wait, + .xdga_token = xdga_token, + .token_len = token_len, + .cwd_len = cwd_len, + .override_count = override_count, + .argc = argc, + .env_count = env_count, + }; + + /* Total packet length, not (yet) including argv[] */ + total_len += sizeof(data) + cwd_len + token_len; + + /* Add argv[] size to total packet length */ + cargv = xmalloc(argc * sizeof(cargv[0])); + for (size_t i = 0; i < argc; i++) { + const size_t arg_len = strlen(argv[i]) + 1; + + if (arg_len >= 1 << (8 * sizeof(cargv[i].len))) { + LOG_ERR("argv length overflow"); + goto err; + } + + cargv[i].len = arg_len; + total_len += sizeof(cargv[i]) + cargv[i].len; + } + + /* Check for size overflows */ + if (total_len >= 1llu << (8 * sizeof(uint32_t)) || + cwd_len >= 1 << (8 * sizeof(data.cwd_len)) || + token_len >= 1 << (8 * sizeof(data.token_len)) || + override_count > (size_t)(unsigned int)data.override_count || + argc > (int)(unsigned int)data.argc || + env_count > (size_t)(unsigned int)data.env_count) + { + LOG_ERR("size overflow"); + goto err; + } + + /* Send everything except argv[] */ + if (sendall(fd, &(uint32_t){total_len}, sizeof(uint32_t)) < 0 || + sendall(fd, &data, sizeof(data)) < 0 || + sendall(fd, cwd, cwd_len) < 0) + { + LOG_ERRNO("failed to send setup packet to server"); + goto err; + } + + /* Send XDGA token, if we have one */ + if (xdga_token) { + if (sendall(fd, token, token_len) != token_len) + { + LOG_ERRNO("failed to send xdg activation token to server"); + goto err; + } + } + + /* Send overrides */ + if (!send_string_list(fd, &overrides)) + goto err; + + /* Send argv[] */ + for (size_t i = 0; i < argc; i++) { + if (sendall(fd, &cargv[i], sizeof(cargv[i])) < 0 || + sendall(fd, argv[i], cargv[i].len) < 0) + { + LOG_ERRNO("failed to send setup packet (argv) to server"); + goto err; + } + } + + /* Send environment */ + if (!send_string_list(fd, &envp)) + goto err; + + struct sigaction sa_int = {.sa_handler = &sigint_handler}; + struct sigaction sa_usr = {.sa_handler = &sigusr_handler}; + sigemptyset(&sa_int.sa_mask); + sigemptyset(&sa_usr.sa_mask); + + if (sigaction(SIGINT, &sa_int, NULL) < 0 || + sigaction(SIGTERM, &sa_int, NULL) < 0 || + sigaction(SIGUSR1, &sa_usr, NULL) < 0 || + sigaction(SIGUSR2, &sa_usr, NULL) < 0) + { + LOG_ERRNO("failed to register signal handlers"); + goto err; + } + + int exit_code; + ssize_t rcvd = -1; + + while (true) { + rcvd = recv(fd, &exit_code, sizeof(exit_code), 0); + + const int got_sigusr = sigusr; + sigusr = 0; + + if (rcvd < 0 && errno == EINTR) { + if (aborted) + break; + else if (got_sigusr != 0) { + LOG_DBG("sending sigusr %d to server", got_sigusr); + + struct { + struct client_ipc_hdr hdr; + struct client_ipc_sigusr sigusr; + } ipc = { + .hdr = { + .ipc_code = FOOT_IPC_SIGUSR, + .size = sizeof(struct client_ipc_sigusr), + }, + .sigusr = { + .signo = got_sigusr, + }, + }; + + ssize_t count = send(fd, &ipc, sizeof(ipc), 0); + if (count < 0) { + LOG_ERRNO("failed to send SIGUSR IPC to server"); + goto err; + } else if ((size_t)count != sizeof(ipc)) { + LOG_ERR("failed to send SIGUSR IPC to server"); + goto err; + } + } + + continue; + } + + break; + } + + if (rcvd == -1 && errno == EINTR) + xassert(aborted); + else if (rcvd != sizeof(exit_code)) + LOG_ERRNO("failed to read server response"); + else + ret = exit_code; + +err: + free_string_list(&envp); + free_string_list(&overrides); + free(cargv); + free(_cwd); + if (fd != -1) + close(fd); + log_deinit(); + return ret; +} diff --git a/commands.c b/commands.c new file mode 100644 index 0000000..a3e4845 --- /dev/null +++ b/commands.c @@ -0,0 +1,115 @@ +#include "commands.h" + +#define LOG_MODULE "commands" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "grid.h" +#include "render.h" +#include "selection.h" +#include "terminal.h" +#include "url-mode.h" +#include "util.h" + +void +cmd_scrollback_up(struct terminal *term, int rows) +{ + if (term->grid == &term->alt) + return; + if (urls_mode_is_active(term)) + return; + + const struct grid *grid = term->grid; + const int view = grid->view; + const int grid_rows = grid->num_rows; + + /* The view row number in scrollback relative coordinates. This is + * the maximum number of rows we're allowed to scroll */ + int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); + int view_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, view); + + rows = min(rows, view_sb_rel); + if (rows == 0) + return; + + int new_view = (view + grid_rows) - rows; + new_view &= grid_rows - 1; + + xassert(new_view != view); + xassert(grid->rows[new_view] != NULL); +#if defined(_DEBUG) + for (int r = 0; r < term->rows; r++) + xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); +#endif + + LOG_DBG("scrollback UP: %d -> %d (offset = %d, rows = %d)", + view, new_view, offset, grid_rows); + + selection_view_up(term, new_view); + term->grid->view = new_view; + + if (rows < term->rows) { + term_damage_scroll( + term, DAMAGE_SCROLL_REVERSE_IN_VIEW, + (struct scroll_region){0, term->rows}, rows); + term_damage_rows_in_view(term, 0, rows - 1); + } else + term_damage_view(term); + + render_refresh_urls(term); + render_refresh(term); +} + +void +cmd_scrollback_down(struct terminal *term, int rows) +{ + if (term->grid == &term->alt) + return; + if (urls_mode_is_active(term)) + return; + + const struct grid *grid = term->grid; + const int offset = grid->offset; + const int view = grid->view; + const int grid_rows = grid->num_rows; + const int screen_rows = term->rows; + + const int scrollback_end = offset; + + /* Number of rows to scroll, without going past the scrollback end */ + int max_rows = 0; + if (view <= scrollback_end) + max_rows = scrollback_end - view; + else + max_rows = offset + (grid_rows - view); + + rows = min(rows, max_rows); + if (rows == 0) + return; + + int new_view = (view + rows) & (grid_rows - 1); + + xassert(new_view != view); + xassert(grid->rows[new_view] != NULL); +#if defined(_DEBUG) + for (int r = 0; r < term->rows; r++) + xassert(grid->rows[(new_view + r) & (grid_rows - 1)] != NULL); +#endif + + LOG_DBG("scrollback DOWN: %d -> %d (offset = %d, rows = %d)", + view, new_view, offset, grid_rows); + + selection_view_down(term, new_view); + term->grid->view = new_view; + + if (rows < term->rows) { + term_damage_scroll( + term, DAMAGE_SCROLL_IN_VIEW, + (struct scroll_region){0, term->rows}, rows); + term_damage_rows_in_view(term, term->rows - rows, screen_rows - 1); + } else + term_damage_view(term); + + render_refresh_urls(term); + render_refresh(term); +} diff --git a/commands.h b/commands.h new file mode 100644 index 0000000..644523b --- /dev/null +++ b/commands.h @@ -0,0 +1,6 @@ +#pragma once + +#include "terminal.h" + +void cmd_scrollback_up(struct terminal *term, int rows); +void cmd_scrollback_down(struct terminal *term, int rows); diff --git a/completions/bash/foot b/completions/bash/foot new file mode 100644 index 0000000..e27be2f --- /dev/null +++ b/completions/bash/foot @@ -0,0 +1,90 @@ +# Bash completion script for foot +_foot() +{ + COMPREPLY=() + + local cur prev flags word commands match previous_words i offset + flags=( + "--app-id" + "--toplevel-tag" + "--check-config" + "--config" + "--font" + "--fullscreen" + "--help" + "--hold" + "--log-colorize" + "--log-level" + "--log-no-syslog" + "--login-shell" + "--maximized" + "--override" + "--print-pid" + "--pty" + "--server" + "--term" + "--title" + "--version" + "--window-size-pixels" + "--window-size-chars" + "--working-directory" + ) + flags="${flags[@]}" + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + + # Check if positional argument is completed + previous_words=( "${COMP_WORDS[@]}" ) + unset previous_words[-1] + commands=$(compgen -c | grep -vFx "$(compgen -k)" | grep -vE '^([.:[]|foot)$' | sort -u) + i=0 + for word in "${previous_words[@]}" ; do + match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null) + if [[ ! -z "$match" ]] ; then + if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--toplevel-tag|--config|--font|--log-level|--pty|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then + (( i++ )) + continue + fi + # Positional argument found + offset=$i + fi + (( i++ )) + done + + if [[ ! -z "$offset" ]] ; then + # Depends on bash_completion being available + declare -F _command_offset >/dev/null || return 1 + _command_offset $offset + return 0 + elif [[ ${cur} == --* ]] ; then + COMPREPLY=( $(compgen -W "${flags}" -- ${cur}) ) + return 0 + fi + + case "$prev" in + --config|--print-pid|--server|-[cps]) + compopt -o default ;; + --working-directory|-D) + compopt -o dirnames ;; + --term|-t) + command -v toe > /dev/null || return 1 + COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 !~ /[+]/ {print $1}')" -- ${cur}) ) ;; + --font|-f) + command -v fc-list > /dev/null || return 1 + COMPREPLY=( $(compgen -W "$(fc-list : family | sed 's/,/\n/g' | uniq | tr -d ' ')" -- ${cur}) ) ;; + --log-level|-d) + COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; + --log-colorize|-l) + COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; + --app-id|--toplevel-tag|--help|--override|--pty|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) + # Don't autocomplete for these flags + : ;; + *) + # Complete commands from $PATH + COMPREPLY=( $(compgen -c -- ${cur}) ) ;; + esac + + return 0 +} + +complete -F _foot foot diff --git a/completions/bash/footclient b/completions/bash/footclient new file mode 100644 index 0000000..c7f1df4 --- /dev/null +++ b/completions/bash/footclient @@ -0,0 +1,82 @@ +# Bash completion script for footclient +_footclient() +{ + COMPREPLY=() + + local cur prev flags word commands match previous_words i offset + flags=( + "--app-id" + "--toplevel-tag" + "--fullscreen" + "--help" + "--hold" + "--login-shell" + "--log-level" + "--log-colorize" + "--maximized" + "--override" + "--client-environment" + "--server-socket" + "--term" + "--title" + "--version" + "--window-size-pixels" + "--window-size-chars" + "--working-directory" + ) + flags="${flags[@]}" + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + + # Check if positional argument is completed + previous_words=( "${COMP_WORDS[@]}" ) + unset previous_words[-1] + commands=$(compgen -c | grep -vFx "$(compgen -k)" | grep -vE '^([.:[]|footclient)$' | sort -u) + i=0 + for word in "${previous_words[@]}" ; do + match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null) + if [[ ! -z "$match" ]] ; then + if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--toplevel-tag|--log-level|--server-socket|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then + (( i++ )) + continue + fi + # Positional argument found + offset=$i + fi + (( i++ )) + done + + if [[ ! -z "$offset" ]] ; then + # Depends on bash_completion being available + declare -F _command_offset >/dev/null || return 1 + _command_offset $offset + return 0 + elif [[ ${cur} == --* ]] ; then + COMPREPLY=( $(compgen -W "${flags}" -- ${cur}) ) + return 0 + fi + + case "$prev" in + --server-socket|-s) + compopt -o default ;; + --working-directory|-D) + compopt -o dirnames ;; + --term|-t) + command -v toe > /dev/null || return 1 + COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 ~ /[+]/ {next}; {print $1}')" -- ${cur}) ) ;; + --log-level|-d) + COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; + --log-colorize|-l) + COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; + --app-id|--toplevel-tag|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|-[ahoTvWw]) + # Don't autocomplete for these flags + : ;; + *) + # Complete commands from $PATH + COMPREPLY=( $(compgen -c -- ${cur}) ) ;; + esac + + return 0 +} + +complete -F _footclient footclient diff --git a/completions/fish/foot.fish b/completions/fish/foot.fish new file mode 100644 index 0000000..21b42d3 --- /dev/null +++ b/completions/fish/foot.fish @@ -0,0 +1,24 @@ +complete -c foot -x -a "(__fish_complete_subcommand)" +complete -c foot -r -s c -l config -d "path to configuration file (XDG_CONFIG_HOME/foot/foot.ini)" +complete -c foot -s C -l check-config -d "verify configuration and exit with 0 if ok, otherwise exit with 1" +complete -c foot -x -s o -l override -d "configuration option to override, in form SECTION.KEY=VALUE" +complete -c foot -x -s f -l font -a "(fc-list : family | sed 's/,/\n/g' | sort | uniq)" -d "font name and style in fontconfig format (monospace)" +complete -c foot -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)" +complete -c foot -x -s T -l title -d "initial window title" +complete -c foot -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)" +complete -c foot -x -l toplevel-tag -d "value to set the toplevel-tag property on the Wayland window to" +complete -c foot -s m -l maximized -d "start in maximized mode" +complete -c foot -s F -l fullscreen -d "start in fullscreen mode" +complete -c foot -s L -l login-shell -d "start shell as a login shell" +complete -c foot -F -s D -l working-directory -d "initial working directory for the client application (CWD)" +complete -c foot -x -s w -l window-size-pixels -d "window WIDTHxHEIGHT, in pixels (700x500)" +complete -c foot -x -s W -l window-size-chars -d "window WIDTHxHEIGHT, in characters (not set)" +complete -c foot -F -s s -l server -d "run as server; open terminals by running footclient" +complete -c foot -s H -l hold -d "remain open after child process exits" +complete -c foot -r -s p -l print-pid -d "print PID to this file or FD when up and running (server mode only)" +complete -c foot -x -s d -l log-level -a "info warning error none" -d "log-level (warning)" +complete -c foot -x -s l -l log-colorize -a "always never auto" -d "enable or disable colorization of log output on stderr" +complete -c foot -s S -l log-no-syslog -d "disable syslog logging (server mode only)" +complete -c foot -r -l pty -d "display an existing pty instead of creating one" +complete -c foot -s v -l version -d "show the version number and quit" +complete -c foot -s h -l help -d "show help message and quit" diff --git a/completions/fish/footclient.fish b/completions/fish/footclient.fish new file mode 100644 index 0000000..0362479 --- /dev/null +++ b/completions/fish/footclient.fish @@ -0,0 +1,20 @@ +complete -c footclient -x -a "(__fish_complete_subcommand)" +complete -c footclient -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)" +complete -c footclient -x -s T -l title -d "initial window title" +complete -c footclient -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)" +complete -c footclient -x -l toplevel-tag -d "value to set the toplevel-tag property on the Wayland window to" +complete -c footclient -s m -l maximized -d "start in maximized mode" +complete -c footclient -s F -l fullscreen -d "start in fullscreen mode" +complete -c footclient -s L -l login-shell -d "start shell as a login shell" +complete -c footclient -F -s D -l working-directory -d "initial working directory for the client application (CWD)" +complete -c footclient -x -s w -l window-size-pixels -d "window WIDTHxHEIGHT, in pixels (700x500)" +complete -c footclient -x -s W -l window-size-chars -d "window WIDTHxHEIGHT, in characters (not set)" +complete -c footclient -F -s s -l server-socket -d "override the default path to the foot server socket ($XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock)" +complete -c footclient -s H -l hold -d "remain open after child process exits" +complete -c footclient -s N -l no-wait -d "detach the client process from the running terminal, exiting immediately" +complete -c footclient -x -s o -l override -d "configuration option to override, in form SECTION.KEY=VALUE" +complete -c footclient -s E -l client-environment -d "child process inherits footclient's environment, instead of the server's" +complete -c footclient -x -s d -l log-level -a "info warning error none" -d "log-level (info)" +complete -c footclient -x -s l -l log-colorize -a "always never auto" -d "enable or disable colorization of log output on stderr" +complete -c footclient -s v -l version -d "show the version number and quit" +complete -c footclient -s h -l help -d "show help message and quit" diff --git a/completions/meson.build b/completions/meson.build new file mode 100644 index 0000000..b0da945 --- /dev/null +++ b/completions/meson.build @@ -0,0 +1,9 @@ +zsh_install_dir = join_paths(get_option('datadir'), 'zsh', 'site-functions') +fish_install_dir = join_paths(get_option('datadir'), 'fish', 'vendor_completions.d') +bash_install_dir = join_paths(get_option('datadir'), 'bash-completion', 'completions') +install_data('zsh/_foot', install_dir: zsh_install_dir) +install_data('zsh/_footclient', install_dir: zsh_install_dir) +install_data('fish/foot.fish', install_dir: fish_install_dir) +install_data('fish/footclient.fish', install_dir: fish_install_dir) +install_data('bash/foot', install_dir: bash_install_dir) +install_data('bash/footclient', install_dir: bash_install_dir) diff --git a/completions/zsh/_foot b/completions/zsh/_foot new file mode 100644 index 0000000..0fd83b3 --- /dev/null +++ b/completions/zsh/_foot @@ -0,0 +1,41 @@ +#compdef foot + +_arguments \ + -s -S -C \ + '(-c --config)'{-c,--config}'[path to configuration file (XDG_CONFIG_HOME/foot/foot.ini)]:config:_files' \ + '(-C --check-config)'{-C,--check-config}'[verify configuration and exit with 0 if ok, otherwise exit with 1]' \ + '(-o --override)'{-o,--override}'[configuration option to override, in form SECTION.KEY=VALUE]:()' \ + '(-f --font)'{-f,--font}'[font name and style in fontconfig format (monospace)]:font:->fonts' \ + '(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \ + '(-T --title)'{-T,--title}'[initial window title]:()' \ + '(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \ + '--toplevel-tag=[value to set the toplevel-tag property on the Wayland window to]:()' \ + '(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \ + '(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \ + '(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \ + '(-D --working-directory)'{-D,--working-directory}'[initial working directory for the client application (CWD)]:working_directory:_files' \ + '(-w --window-size-pixels)'{-w,--window-size-pixels}'[window WIDTHxHEIGHT, in pixels (700x500)]:size_pixels:()' \ + '(-W --window-size-chars)'{-W,--window-size-chars}'[window WIDTHxHEIGHT, in characters (not set)]:size_chars:()' \ + '(-s --server)'{-s,--server}'[run as server; open terminals by running footclient]:server:_files' \ + '(-H --hold)'{-H,--hold}'[remain open after child process exits]' \ + '(-p --print-pid)'{-p,--print-pid}'[print PID to this file or FD when up and running (server mode only)]:pidfile:_files' \ + '--pty=[display an existing pty instead of creating one]:pty:_files' \ + '(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \ + '(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \ + '(-S --log-no-syslog)'{-s,--log-no-syslog}'[disable syslog logging (server mode only)]' \ + '(-v --version)'{-v,--version}'[show the version number and quit]' \ + '(-h --help)'{-h,--help}'[show help message and quit]' \ + ':command: _command_names -e' \ + '*::command arguments: _dispatch ${words[1]} ${words[1]}' + +case ${state} in + fonts) + IFS=$'\n' + _values -s , 'font families' $(fc-list : family | sed 's/,/\n/g' | sort | uniq) + unset IFS + ;; + + terms) + _values 'terminal definitions' /usr/share/terminfo/**/*(.:t) + ;; +esac diff --git a/completions/zsh/_footclient b/completions/zsh/_footclient new file mode 100644 index 0000000..12f29d7 --- /dev/null +++ b/completions/zsh/_footclient @@ -0,0 +1,31 @@ +#compdef footclient + +_arguments \ + -s -S -C \ + '(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \ + '(-T --title)'{-T,--title}'[initial window title]:()' \ + '(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \ + '--toplevel-tag=[value to set the toplevel-tag property on the Wayland window to]:()' \ + '(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \ + '(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \ + '(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \ + '(-D --working-directory)'{-D,--working-directory}'[initial working directory for the client application (CWD)]:working_directory:_files' \ + '(-w --window-size-pixels)'{-w,--window-size-pixels}'[window WIDTHxHEIGHT, in pixels (700x500)]:size_pixels:()' \ + '(-W --window-size-chars)'{-W,--window-size-chars}'[window WIDTHxHEIGHT, in characters (not set)]:size_chars:()' \ + '(-s --server-socket)'{-s,--server-socket}'[override the default path to the foot server socket ($XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock)]:server:_files' \ + '(-H --hold)'{-H,--hold}'[remain open after child process exits]' \ + '(-N --no-wait)'{-N,--no-wait}'[detach the client process from the running terminal, exiting immediately]' \ + '(-o --override)'{-o,--override}'[configuration option to override, in form SECTION.KEY=VALUE]:()' \ + '(-E --client-environment)'{-E,--client-environment}"[child process inherits footclient's environment, instead of the server's]" \ + '(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \ + '(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \ + '(-v --version)'{-v,--version}'[show the version number and quit]' \ + '(-h --help)'{-h,--help}'[show help message and quit]' \ + ':command: _command_names -e' \ + '*::command arguments: _dispatch ${words[1]} ${words[1]}' + +case ${state} in + terms) + _values 'terminal definitions' /usr/share/terminfo/**/*(.:t) + ;; +esac diff --git a/composed.c b/composed.c new file mode 100644 index 0000000..fc7dfa0 --- /dev/null +++ b/composed.c @@ -0,0 +1,149 @@ +#include "composed.h" + +#include +#include + +#include "debug.h" +#include "terminal.h" + +uint32_t +composed_key_from_chars(const uint32_t chars[], size_t count) +{ + if (count == 0) + return 0; + + uint32_t key = chars[0]; + for (size_t i = 1; i < count; i++) + key = composed_key_from_key(key, chars[i]); + + return key; +} + +uint32_t +composed_key_from_key(uint32_t prev_key, uint32_t next_char) +{ + unsigned bits = 32 - __builtin_clz(CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO); + + /* Rotate old key 8 bits */ + uint32_t new_key = (prev_key << 8) | (prev_key >> (bits - 8)); + + /* xor with new char */ + new_key ^= next_char; + + /* Multiply with magic hash constant */ + new_key *= 2654435761ul; + + /* And mask, to ensure the new value is within range */ + new_key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; + return new_key; +} + +UNITTEST +{ + const char32_t chars[] = U"abcdef"; + + uint32_t k1 = composed_key_from_key(chars[0], chars[1]); + uint32_t k2 = composed_key_from_chars(chars, 2); + xassert(k1 == k2); + + uint32_t k3 = composed_key_from_key(k2, chars[2]); + uint32_t k4 = composed_key_from_chars(chars, 3); + xassert(k3 == k4); +} + +const struct composed * +composed_lookup(struct composed *root, uint32_t key) +{ + struct composed *node = root; + + while (node != NULL) { + if (key == node->key) + return node; + + node = key < node->key ? node->left : node->right; + } + + return NULL; +} + +const struct composed * +composed_lookup_without_collision(struct composed *root, uint32_t *key, + const char32_t *prefix_text, size_t prefix_len, + char32_t wc, int forced_width) +{ + while (true) { + const struct composed *cc = composed_lookup(root, *key); + if (cc == NULL) + return NULL; + + bool match = cc->count == prefix_len + 1 && + cc->forced_width == forced_width && + cc->chars[prefix_len] == wc; + + if (match) { + for (size_t i = 0; i < prefix_len; i++) { + if (cc->chars[i] != prefix_text[i]) { + match = false; + break; + } + } + } + + if (match) + return cc; + + (*key)++; + *key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; + + /* TODO: this will loop infinitely if the composed table is full */ + } + + return NULL; +} + +void +composed_insert(struct composed **root, struct composed *node) +{ + node->left = node->right = NULL; + + if (*root == NULL) { + *root = node; + return; + } + + uint32_t key = node->key; + + struct composed *prev = NULL; + struct composed *n = *root; + + while (n != NULL) { + xassert(n->key != node->key); + + prev = n; + n = key < n->key ? n->left : n->right; + } + + xassert(prev != NULL); + xassert(n == NULL); + + if (key < prev->key) { + xassert(prev->left == NULL); + prev->left = node; + } else { + xassert(prev->right == NULL); + prev->right = node; + } +} + +void +composed_free(struct composed *root) +{ + if (root == NULL) + return; + + composed_free(root->left); + composed_free(root->right); + + free(root->chars); + free(root); +} diff --git a/composed.h b/composed.h new file mode 100644 index 0000000..18afb14 --- /dev/null +++ b/composed.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +struct composed { + char32_t *chars; + struct composed *left; + struct composed *right; + uint32_t key; + uint8_t count; + uint8_t width; + uint8_t forced_width; +}; + +uint32_t composed_key_from_chars(const uint32_t chars[], size_t count); +uint32_t composed_key_from_key(uint32_t prev_key, uint32_t next_char); + +const struct composed *composed_lookup(struct composed *root, uint32_t key); +const struct composed *composed_lookup_without_collision( + struct composed *root, uint32_t *key, + const char32_t *prefix, size_t prefix_len, char32_t wc, int forced_width); +void composed_insert(struct composed **root, struct composed *node); + +void composed_free(struct composed *root); diff --git a/config.c b/config.c new file mode 100644 index 0000000..c02f5b7 --- /dev/null +++ b/config.c @@ -0,0 +1,4366 @@ +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#define LOG_MODULE "config" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" +#include "debug.h" +#include "input.h" +#include "key-binding.h" +#include "macros.h" +#include "tokenize.h" +#include "util.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +static const uint32_t default_foreground = 0xffffff; +static const uint32_t default_background = 0x242424; + +static const size_t min_csd_border_width = 5; + +#define cube6(r, g) \ + r|g|0x00, r|g|0x5f, r|g|0x87, r|g|0xaf, r|g|0xd7, r|g|0xff + +#define cube36(r) \ + cube6(r, 0x0000), \ + cube6(r, 0x5f00), \ + cube6(r, 0x8700), \ + cube6(r, 0xaf00), \ + cube6(r, 0xd700), \ + cube6(r, 0xff00) + +static const uint32_t default_color_table[256] = { + // Regular + 0x242424, + 0xf62b5a, + 0x47b413, + 0xe3c401, + 0x24acd4, + 0xf2affd, + 0x13c299, + 0xe6e6e6, + + // Bright + 0x616161, + 0xff4d51, + 0x35d450, + 0xe9e836, + 0x5dc5f8, + 0xfeabf2, + 0x24dfc4, + 0xffffff, + + // 6x6x6 RGB cube + // (color channels = i ? i*40+55 : 0, where i = 0..5) + cube36(0x000000), + cube36(0x5f0000), + cube36(0x870000), + cube36(0xaf0000), + cube36(0xd70000), + cube36(0xff0000), + + // 24 shades of gray + // (color channels = i*10+8, where i = 0..23) + 0x080808, 0x121212, 0x1c1c1c, 0x262626, + 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e, + 0x585858, 0x626262, 0x6c6c6c, 0x767676, + 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, + 0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, + 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee +}; + +/* VT330/VT340 Programmer Reference Manual - Table 2-3 VT340 Default Color Map */ +static const uint32_t default_sixel_colors[16] = { + 0xff000000, + 0xff3333cc, + 0xffcc2121, + 0xff33cc33, + 0xffcc33cc, + 0xff33cccc, + 0xffcccc33, + 0xff878787, + 0xff424242, + 0xff545499, + 0xff994242, + 0xff549954, + 0xff995499, + 0xff549999, + 0xff999954, + 0xffcccccc, +}; + +static const char *const binding_action_map[] = { + [BIND_ACTION_NONE] = NULL, + [BIND_ACTION_NOOP] = "noop", + [BIND_ACTION_SCROLLBACK_UP_PAGE] = "scrollback-up-page", + [BIND_ACTION_SCROLLBACK_UP_HALF_PAGE] = "scrollback-up-half-page", + [BIND_ACTION_SCROLLBACK_UP_LINE] = "scrollback-up-line", + [BIND_ACTION_SCROLLBACK_DOWN_PAGE] = "scrollback-down-page", + [BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE] = "scrollback-down-half-page", + [BIND_ACTION_SCROLLBACK_DOWN_LINE] = "scrollback-down-line", + [BIND_ACTION_SCROLLBACK_HOME] = "scrollback-home", + [BIND_ACTION_SCROLLBACK_END] = "scrollback-end", + [BIND_ACTION_CLIPBOARD_COPY] = "clipboard-copy", + [BIND_ACTION_CLIPBOARD_PASTE] = "clipboard-paste", + [BIND_ACTION_PRIMARY_PASTE] = "primary-paste", + [BIND_ACTION_SEARCH_START] = "search-start", + [BIND_ACTION_FONT_SIZE_UP] = "font-increase", + [BIND_ACTION_FONT_SIZE_DOWN] = "font-decrease", + [BIND_ACTION_FONT_SIZE_RESET] = "font-reset", + [BIND_ACTION_SPAWN_TERMINAL] = "spawn-terminal", + [BIND_ACTION_MINIMIZE] = "minimize", + [BIND_ACTION_MAXIMIZE] = "maximize", + [BIND_ACTION_FULLSCREEN] = "fullscreen", + [BIND_ACTION_PIPE_SCROLLBACK] = "pipe-scrollback", + [BIND_ACTION_PIPE_VIEW] = "pipe-visible", + [BIND_ACTION_PIPE_SELECTED] = "pipe-selected", + [BIND_ACTION_PIPE_COMMAND_OUTPUT] = "pipe-command-output", + [BIND_ACTION_SHOW_URLS_COPY] = "show-urls-copy", + [BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch", + [BIND_ACTION_SHOW_URLS_PERSISTENT] = "show-urls-persistent", + [BIND_ACTION_TEXT_BINDING] = "text-binding", + [BIND_ACTION_PROMPT_PREV] = "prompt-prev", + [BIND_ACTION_PROMPT_NEXT] = "prompt-next", + [BIND_ACTION_UNICODE_INPUT] = "unicode-input", + [BIND_ACTION_QUIT] = "quit", + [BIND_ACTION_REGEX_LAUNCH] = "regex-launch", + [BIND_ACTION_REGEX_COPY] = "regex-copy", + [BIND_ACTION_THEME_SWITCH_1] = "color-theme-switch-1", + [BIND_ACTION_THEME_SWITCH_2] = "color-theme-switch-2", + [BIND_ACTION_THEME_SWITCH_DARK] = "color-theme-switch-dark", + [BIND_ACTION_THEME_SWITCH_LIGHT] = "color-theme-switch-light", + [BIND_ACTION_THEME_TOGGLE] = "color-theme-toggle", + + /* Tab actions */ + [BIND_ACTION_TAB_NEW] = "tab-new", + [BIND_ACTION_TAB_CLOSE] = "tab-close", + [BIND_ACTION_TAB_NEXT] = "tab-next", + [BIND_ACTION_TAB_PREV] = "tab-prev", + [BIND_ACTION_TAB_1] = "tab-1", + [BIND_ACTION_TAB_2] = "tab-2", + [BIND_ACTION_TAB_3] = "tab-3", + [BIND_ACTION_TAB_4] = "tab-4", + [BIND_ACTION_TAB_5] = "tab-5", + [BIND_ACTION_TAB_6] = "tab-6", + [BIND_ACTION_TAB_7] = "tab-7", + [BIND_ACTION_TAB_8] = "tab-8", + [BIND_ACTION_TAB_9] = "tab-9", + [BIND_ACTION_TAB_OVERVIEW] = "tab-overview", + + /* Mouse-specific actions */ + [BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse", + [BIND_ACTION_SCROLLBACK_DOWN_MOUSE] = "scrollback-down-mouse", + [BIND_ACTION_SELECT_BEGIN] = "select-begin", + [BIND_ACTION_SELECT_BEGIN_BLOCK] = "select-begin-block", + [BIND_ACTION_SELECT_EXTEND] = "select-extend", + [BIND_ACTION_SELECT_EXTEND_CHAR_WISE] = "select-extend-character-wise", + [BIND_ACTION_SELECT_WORD] = "select-word", + [BIND_ACTION_SELECT_WORD_WS] = "select-word-whitespace", + [BIND_ACTION_SELECT_QUOTE] = "select-quote", + [BIND_ACTION_SELECT_ROW] = "select-row", +}; + +static const char *const search_binding_action_map[] = { + [BIND_ACTION_SEARCH_NONE] = NULL, + [BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE] = "scrollback-up-page", + [BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE] = "scrollback-up-half-page", + [BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE] = "scrollback-up-line", + [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE] = "scrollback-down-page", + [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE] = "scrollback-down-half-page", + [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE] = "scrollback-down-line", + [BIND_ACTION_SEARCH_SCROLLBACK_HOME] = "scrollback-home", + [BIND_ACTION_SEARCH_SCROLLBACK_END] = "scrollback-end", + [BIND_ACTION_SEARCH_CANCEL] = "cancel", + [BIND_ACTION_SEARCH_COMMIT] = "commit", + [BIND_ACTION_SEARCH_FIND_PREV] = "find-prev", + [BIND_ACTION_SEARCH_FIND_NEXT] = "find-next", + [BIND_ACTION_SEARCH_EDIT_LEFT] = "cursor-left", + [BIND_ACTION_SEARCH_EDIT_LEFT_WORD] = "cursor-left-word", + [BIND_ACTION_SEARCH_EDIT_RIGHT] = "cursor-right", + [BIND_ACTION_SEARCH_EDIT_RIGHT_WORD] = "cursor-right-word", + [BIND_ACTION_SEARCH_EDIT_HOME] = "cursor-home", + [BIND_ACTION_SEARCH_EDIT_END] = "cursor-end", + [BIND_ACTION_SEARCH_DELETE_PREV] = "delete-prev", + [BIND_ACTION_SEARCH_DELETE_PREV_WORD] = "delete-prev-word", + [BIND_ACTION_SEARCH_DELETE_NEXT] = "delete-next", + [BIND_ACTION_SEARCH_DELETE_NEXT_WORD] = "delete-next-word", + [BIND_ACTION_SEARCH_DELETE_TO_START] = "delete-to-start", + [BIND_ACTION_SEARCH_DELETE_TO_END] = "delete-to-end", + [BIND_ACTION_SEARCH_EXTEND_CHAR] = "extend-char", + [BIND_ACTION_SEARCH_EXTEND_WORD] = "extend-to-word-boundary", + [BIND_ACTION_SEARCH_EXTEND_WORD_WS] = "extend-to-next-whitespace", + [BIND_ACTION_SEARCH_EXTEND_LINE_DOWN] = "extend-line-down", + [BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR] = "extend-backward-char", + [BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD] = "extend-backward-to-word-boundary", + [BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS] = "extend-backward-to-next-whitespace", + [BIND_ACTION_SEARCH_EXTEND_LINE_UP] = "extend-line-up", + [BIND_ACTION_SEARCH_CLIPBOARD_PASTE] = "clipboard-paste", + [BIND_ACTION_SEARCH_PRIMARY_PASTE] = "primary-paste", + [BIND_ACTION_SEARCH_UNICODE_INPUT] = "unicode-input", + [BIND_ACTION_SEARCH_TOGGLE_CASE] = "toggle-case", + [BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD] = "toggle-whole-word", + [BIND_ACTION_SEARCH_TOGGLE_REGEX] = "toggle-regex", + [BIND_ACTION_SEARCH_HISTORY_PREV] = "history-prev", + [BIND_ACTION_SEARCH_HISTORY_NEXT] = "history-next", + [BIND_ACTION_SEARCH_COMMIT_LINE] = "commit-line", +}; + +static const char *const url_binding_action_map[] = { + [BIND_ACTION_URL_NONE] = NULL, + [BIND_ACTION_URL_CANCEL] = "cancel", + [BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL] = "toggle-url-visible", +}; + +static_assert(ALEN(binding_action_map) == BIND_ACTION_COUNT, + "binding action map size mismatch"); +static_assert(ALEN(search_binding_action_map) == BIND_ACTION_SEARCH_COUNT, + "search binding action map size mismatch"); +static_assert(ALEN(url_binding_action_map) == BIND_ACTION_URL_COUNT, + "URL binding action map size mismatch"); + +struct context { + struct config *conf; + const char *section; + const char *section_suffix; + const char *key; + const char *value; + + const char *path; + unsigned lineno; + + bool errors_are_fatal; +}; + +static const enum user_notification_kind log_class_to_notify_kind[LOG_CLASS_COUNT] = { + [LOG_CLASS_WARNING] = USER_NOTIFICATION_WARNING, + [LOG_CLASS_ERROR] = USER_NOTIFICATION_ERROR, +}; + +static void NOINLINE VPRINTF(5) +log_and_notify_va(struct config *conf, enum log_class log_class, + const char *file, int lineno, const char *fmt, va_list va) +{ + xassert(log_class < ALEN(log_class_to_notify_kind)); + enum user_notification_kind kind = log_class_to_notify_kind[log_class]; + + if (kind == 0) { + BUG("unsupported log class: %d", (int)log_class); + return; + } + + char *formatted_msg = xvasprintf(fmt, va); + log_msg(log_class, LOG_MODULE, file, lineno, "%s", formatted_msg); + user_notification_add(&conf->notifications, kind, formatted_msg); +} + +static void NOINLINE PRINTF(5) +log_and_notify(struct config *conf, enum log_class log_class, + const char *file, int lineno, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + log_and_notify_va(conf, log_class, file, lineno, fmt, va); + va_end(va); +} + +static void NOINLINE PRINTF(5) +log_contextual(struct context *ctx, enum log_class log_class, + const char *file, int lineno, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + char *formatted_msg = xvasprintf(fmt, va); + va_end(va); + + const bool print_dot = ctx->key != NULL; + const bool print_colon = ctx->value != NULL; + const bool print_section_suffix = ctx->section_suffix != NULL; + + if (!print_dot) + ctx->key = ""; + + if (!print_colon) + ctx->value = ""; + + if (!print_section_suffix) + ctx->section_suffix = ""; + + log_and_notify( + ctx->conf, log_class, file, lineno, "%s:%d: [%s%s%s]%s%s%s%s: %s", + ctx->path, ctx->lineno, ctx->section, + print_section_suffix ? ":" : "", ctx->section_suffix, + print_dot ? "." : "", ctx->key, print_colon ? ": " : "", + ctx->value, formatted_msg); + free(formatted_msg); +} + + +static void NOINLINE VPRINTF(4) +log_and_notify_errno_va(struct config *conf, const char *file, int lineno, + const char *fmt, va_list va) +{ + int errno_copy = errno; + char *formatted_msg = xvasprintf(fmt, va); + log_and_notify( + conf, LOG_CLASS_ERROR, file, lineno, + "%s: %s", formatted_msg, strerror(errno_copy)); + free(formatted_msg); +} + +static void NOINLINE PRINTF(4) +log_and_notify_errno(struct config *conf, const char *file, int lineno, + const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + log_and_notify_errno_va(conf, file, lineno, fmt, va); + va_end(va); +} + +static void NOINLINE PRINTF(4) +log_contextual_errno(struct context *ctx, const char *file, int lineno, + const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + char *formatted_msg = xvasprintf(fmt, va); + va_end(va); + + bool print_dot = ctx->key != NULL; + bool print_colon = ctx->value != NULL; + + if (!print_dot) + ctx->key = ""; + + if (!print_colon) + ctx->value = ""; + + log_and_notify_errno( + ctx->conf, file, lineno, "%s:%d: [%s]%s%s%s%s: %s", + ctx->path, ctx->lineno, ctx->section, print_dot ? "." : "", + ctx->key, print_colon ? ": " : "", ctx->value, formatted_msg); + + free(formatted_msg); +} + +#define LOG_CONTEXTUAL_ERR(...) \ + log_contextual(ctx, LOG_CLASS_ERROR, __FILE__, __LINE__, __VA_ARGS__) + +#define LOG_CONTEXTUAL_WARN(...) \ + log_contextual(ctx, LOG_CLASS_WARNING, __FILE__, __LINE__, __VA_ARGS__) + +#define LOG_CONTEXTUAL_ERRNO(...) \ + log_contextual_errno(ctx, __FILE__, __LINE__, __VA_ARGS__) + +#define LOG_AND_NOTIFY_ERR(...) \ + log_and_notify(conf, LOG_CLASS_ERROR, __FILE__, __LINE__, __VA_ARGS__) + +#define LOG_AND_NOTIFY_WARN(...) \ + log_and_notify(conf, LOG_CLASS_WARNING, __FILE__, __LINE__, __VA_ARGS__) + +#define LOG_AND_NOTIFY_ERRNO(...) \ + log_and_notify_errno(conf, __FILE__, __LINE__, __VA_ARGS__) + +static char * +get_shell(void) +{ + const char *shell = getenv("SHELL"); + + if (shell == NULL) { + struct passwd *passwd = getpwuid(getuid()); + if (passwd == NULL) { + LOG_ERRNO("failed to lookup user: falling back to 'sh'"); + shell = "sh"; + } else + shell = passwd->pw_shell; + } + + LOG_DBG("user's shell: %s", shell); + return xstrdup(shell); +} + +struct config_file { + char *path; /* Full, absolute, path */ + int fd; /* FD of file, O_RDONLY */ +}; + +static struct config_file +open_config(void) +{ + char *path = NULL; + struct config_file ret = {.path = NULL, .fd = -1}; + + const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); + const char *xdg_config_dirs = getenv("XDG_CONFIG_DIRS"); + const char *home_dir = getenv("HOME"); + char *xdg_config_dirs_copy = NULL; + + /* First, check XDG_CONFIG_HOME (or .config, if unset) */ + if (xdg_config_home != NULL && xdg_config_home[0] != '\0') + path = xstrjoin(xdg_config_home, "/foot/foot.ini"); + else if (home_dir != NULL) + path = xstrjoin(home_dir, "/.config/foot/foot.ini"); + + if (path != NULL) { + LOG_DBG("checking for %s", path); + int fd = open(path, O_RDONLY | O_CLOEXEC); + + if (fd >= 0) { + ret = (struct config_file) {.path = path, .fd = fd}; + path = NULL; + goto done; + } + } + + xdg_config_dirs_copy = xdg_config_dirs != NULL && xdg_config_dirs[0] != '\0' + ? strdup(xdg_config_dirs) + : strdup("/etc/xdg"); + + if (xdg_config_dirs_copy == NULL || xdg_config_dirs_copy[0] == '\0') + goto done; + + for (const char *conf_dir = strtok(xdg_config_dirs_copy, ":"); + conf_dir != NULL; + conf_dir = strtok(NULL, ":")) + { + free(path); + path = xstrjoin(conf_dir, "/foot/foot.ini"); + + LOG_DBG("checking for %s", path); + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd >= 0) { + ret = (struct config_file){.path = path, .fd = fd}; + path = NULL; + goto done; + } + } + +done: + free(xdg_config_dirs_copy); + free(path); + return ret; +} + +static bool +str_has_prefix(const char *str, const char *prefix) +{ + return strncmp(str, prefix, strlen(prefix)) == 0; +} + +static bool NOINLINE +value_to_bool(struct context *ctx, bool *res) +{ + static const char *const yes[] = {"on", "true", "yes", "1"}; + static const char *const no[] = {"off", "false", "no", "0"}; + + for (size_t i = 0; i < ALEN(yes); i++) { + if (strcasecmp(ctx->value, yes[i]) == 0) { + *res = true; + return true; + } + } + + for (size_t i = 0; i < ALEN(no); i++) { + if (strcasecmp(ctx->value, no[i]) == 0) { + *res = false; + return true; + } + } + + LOG_CONTEXTUAL_ERR("invalid boolean value"); + return false; +} + + +static bool NOINLINE +str_to_ulong(const char *s, int base, unsigned long *res) +{ + if (s == NULL) + return false; + + errno = 0; + char *end = NULL; + + unsigned long v = strtoul(s, &end, base); + if (!(errno == 0 && *end == '\0')) + return false; + + *res = v; + return true; +} + +static bool NOINLINE +str_to_uint32(const char *s, int base, uint32_t *res) +{ + unsigned long v; + bool ret = str_to_ulong(s, base, &v); + if (v > UINT32_MAX) + return false; + *res = v; + return ret; +} + +static bool NOINLINE +str_to_uint16(const char *s, int base, uint16_t *res) +{ + unsigned long v; + bool ret = str_to_ulong(s, base, &v); + if (v > UINT16_MAX) + return false; + *res = v; + return ret; +} + +static bool NOINLINE +value_to_uint16(struct context *ctx, int base, uint16_t *res) +{ + if (!str_to_uint16(ctx->value, base, res)) { + LOG_CONTEXTUAL_ERR( + "invalid integer value, or outside range 0-%u", UINT16_MAX); + return false; + } + return true; +} + +static bool NOINLINE +value_to_uint32(struct context *ctx, int base, uint32_t *res) +{ + if (!str_to_uint32(ctx->value, base, res)){ + LOG_CONTEXTUAL_ERR( + "invalid integer value, or outside range 0-%u", UINT32_MAX); + return false; + } + return true; +} + +static bool NOINLINE +value_to_dimensions(struct context *ctx, uint32_t *x, uint32_t *y) +{ + if (sscanf(ctx->value, "%ux%u", x, y) != 2) { + LOG_CONTEXTUAL_ERR("invalid dimensions (must be in the form AxB)"); + return false; + } + + return true; +} + +static bool NOINLINE +value_to_float(struct context *ctx, float *res) +{ + const char *s = ctx->value; + + if (s == NULL) + return false; + + errno = 0; + char *end = NULL; + + float v = strtof(s, &end); + if (!(errno == 0 && *end == '\0')) { + LOG_CONTEXTUAL_ERR("invalid decimal value"); + return false; + } + + *res = v; + return true; +} + +static bool NOINLINE +value_to_str(struct context *ctx, char **res) +{ + char *copy = xstrdup(ctx->value); + char *end = copy + strlen(copy) - 1; + + /* Un-quote + * + * Note: this is very simple; we only support the *entire* value + * being quoted. That is, no mid-value quotes. Both double and + * single quotes are supported. + * + * - key="value" OK + * - key=abc "quote" def NOT OK + * - key='value' OK + * + * Finally, we support escaping the quote character, and the + * escape character itself: + * + * - key="value \"quotes\"" + * - key="backslash: \\" + * + * ONLY the "current" quote character can be escaped: + * + * key="value \'" NOt OK (both backslash and single quote is kept) + */ + + if ((copy[0] == '"' && *end == '"') || + (copy[0] == '\'' && *end == '\'')) + { + const char quote = copy[0]; + *end = '\0'; + + memmove(copy, copy + 1, end - copy); + + /* Un-escape */ + for (char *p = copy; *p != '\0'; p++) { + if (p[0] == '\\' && (p[1] == '\\' || p[1] == quote)) { + memmove(p, p + 1, end - p); + } + } + } + + free(*res); + *res = copy; + return true; +} + +static bool NOINLINE +value_to_wchars(struct context *ctx, char32_t **res) +{ + char32_t *s = ambstoc32(ctx->value); + if (s == NULL) { + LOG_CONTEXTUAL_ERR("not a valid string value"); + return false; + } + + free(*res); + *res = s; + return true; +} + +static bool NOINLINE +value_to_enum(struct context *ctx, const char **value_map, int *res) +{ + size_t str_len = 0; + size_t count = 0; + + for (; value_map[count] != NULL; count++) { + if (strcasecmp(value_map[count], ctx->value) == 0) { + *res = count; + return true; + } + str_len += strlen(value_map[count]); + } + + const size_t size = str_len + count * 4 + 1; + char valid_values[512]; + size_t idx = 0; + xassert(size < sizeof(valid_values)); + + for (size_t i = 0; i < count; i++) + idx += xsnprintf(&valid_values[idx], size - idx, "'%s', ", value_map[i]); + + if (count > 0) + valid_values[idx - 2] = '\0'; + + LOG_CONTEXTUAL_ERR("not one of %s", valid_values); + return false; +} + +static bool NOINLINE +value_to_color(struct context *ctx, uint32_t *result, bool allow_alpha) +{ + uint32_t color; + const size_t len = strlen(ctx->value); + const size_t component_count = len / 2; + + if (!(len == 6 || (allow_alpha && len == 8)) || + !str_to_uint32(ctx->value, 16, &color)) + { + if (allow_alpha) { + LOG_CONTEXTUAL_ERR("color must be in either RGB or ARGB format"); + } else { + LOG_CONTEXTUAL_ERR("color must be in RGB format"); + } + + return false; + } + + if (allow_alpha && component_count == 3) { + /* If user left out the alpha component, assume non-transparency */ + color |= 0xff000000; + } + + *result = color; + return true; +} + +static bool NOINLINE +value_to_two_colors(struct context *ctx, + uint32_t *first, uint32_t *second, bool allow_alpha) +{ + bool ret = false; + const char *original_value = ctx->value; + + /* TODO: do this without strdup() */ + char *value_copy = xstrdup(ctx->value); + const char *first_as_str = strtok(value_copy, " "); + const char *second_as_str = strtok(NULL, " "); + + if (first_as_str == NULL || second_as_str == NULL) { + LOG_CONTEXTUAL_ERR("invalid double color value"); + goto out; + } + + uint32_t a, b; + + ctx->value = first_as_str; + if (!value_to_color(ctx, &a, allow_alpha)) + goto out; + + ctx->value = second_as_str; + if (!value_to_color(ctx, &b, allow_alpha)) + goto out; + + *first = a; + *second = b; + ret = true; + +out: + free(value_copy); + ctx->value = original_value; + return ret; +} + +static bool NOINLINE +value_to_pt_or_px(struct context *ctx, struct pt_or_px *res) +{ + const char *s = ctx->value; + + size_t len = s != NULL ? strlen(s) : 0; + if (len >= 2 && s[len - 2] == 'p' && s[len - 1] == 'x') { + errno = 0; + char *end = NULL; + + long value = strtol(s, &end, 10); + if (!(len > 2 && errno == 0 && end == s + len - 2)) { + LOG_CONTEXTUAL_ERR("invalid px value (must be in the form 12px)"); + return false; + } + res->pt = 0; + res->px = value; + } else { + float value; + if (!value_to_float(ctx, &value)) + return false; + res->pt = value; + res->px = 0; + } + + return true; +} + +static struct config_font_list NOINLINE +value_to_fonts(struct context *ctx) +{ + size_t count = 0; + size_t size = 0; + struct config_font *fonts = NULL; + + char *copy = xstrdup(ctx->value); + for (const char *font = strtok(copy, ","); + font != NULL; + font = strtok(NULL, ",")) + { + /* Trim spaces, strictly speaking not necessary, but looks nice :) */ + while (isspace(font[0])) + font++; + + if (font[0] == '\0') + continue; + + struct config_font font_data; + if (!config_font_parse(font, &font_data)) { + ctx->value = font; + LOG_CONTEXTUAL_ERR("invalid font specification"); + goto err; + } + + if (count + 1 > size) { + size += 4; + fonts = xrealloc(fonts, size * sizeof(fonts[0])); + } + + xassert(count + 1 <= size); + fonts[count++] = font_data; + } + + free(copy); + return (struct config_font_list){.arr = fonts, .count = count}; + +err: + free(copy); + free(fonts); + return (struct config_font_list){.arr = NULL, .count = 0}; +} + +static void NOINLINE +free_argv(struct argv *argv) +{ + if (argv->args == NULL) + return; + for (char **a = argv->args; *a != NULL; a++) + free(*a); + free(argv->args); + argv->args = NULL; +} + +static void NOINLINE +clone_argv(struct argv *dst, const struct argv *src) +{ + if (src->args == NULL) { + dst->args = NULL; + return; + } + + size_t count = 0; + for (char **args = src->args; *args != NULL; args++) + count++; + + dst->args = xmalloc((count + 1) * sizeof(dst->args[0])); + for (char **args_src = src->args, **args_dst = dst->args; + *args_src != NULL; args_src++, + args_dst++) + { + *args_dst = xstrdup(*args_src); + } + dst->args[count] = NULL; +} + +static void +spawn_template_free(struct config_spawn_template *template) +{ + free_argv(&template->argv); +} + +static void +spawn_template_clone(struct config_spawn_template *dst, + const struct config_spawn_template *src) +{ + clone_argv(&dst->argv, &src->argv); +} + +static bool NOINLINE +value_to_spawn_template(struct context *ctx, + struct config_spawn_template *template) +{ + spawn_template_free(template); + + char **argv = NULL; + + if (ctx->value[0] == '"' && ctx->value[1] == '"' && ctx->value[2] == '\0') { + template->argv.args = NULL; + return true; + } + + if (!tokenize_cmdline(ctx->value, &argv)) { + LOG_CONTEXTUAL_ERR("syntax error in command line"); + return false; + } + + template->argv.args = argv; + return true; +} + +static bool parse_config_file( + FILE *f, struct config *conf, const char *path, bool errors_are_fatal); + +static bool +parse_section_main(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + const char *value = ctx->value; + bool errors_are_fatal = ctx->errors_are_fatal; + + if (streq(key, "include")) { + char *_include_path = NULL; + const char *include_path = NULL; + + if (value[0] == '~' && value[1] == '/') { + const char *home_dir = getenv("HOME"); + + if (home_dir == NULL) { + LOG_CONTEXTUAL_ERRNO("failed to expand '~'"); + return false; + } + + _include_path = xstrjoin3(home_dir, "/", value + 2); + include_path = _include_path; + } else + include_path = value; + + if (include_path[0] != '/') { + LOG_CONTEXTUAL_ERR("not an absolute path"); + free(_include_path); + return false; + } + + FILE *include = fopen(include_path, "r"); + + if (include == NULL) { + LOG_CONTEXTUAL_ERRNO("failed to open"); + free(_include_path); + return false; + } + + bool ret = parse_config_file( + include, conf, include_path, errors_are_fatal); + fclose(include); + + LOG_INFO("imported sub-configuration from %s", include_path); + free(_include_path); + return ret; + } + + else if (streq(key, "term")) + return value_to_str(ctx, &conf->term); + + else if (streq(key, "shell")) + return value_to_str(ctx, &conf->shell); + + else if (streq(key, "login-shell")) + return value_to_bool(ctx, &conf->login_shell); + + else if (streq(key, "title")) + return value_to_str(ctx, &conf->title); + + else if (streq(key, "locked-title")) + return value_to_bool(ctx, &conf->locked_title); + + else if (streq(key, "app-id")) + return value_to_str(ctx, &conf->app_id); + + else if (streq(key, "toplevel-tag")) + return value_to_str(ctx, &conf->toplevel_tag); + + else if (streq(key, "initial-window-size-pixels")) { + if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height)) + return false; + + conf->size.type = CONF_SIZE_PX; + return true; + } + + else if (streq(key, "initial-window-size-chars")) { + if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height)) + return false; + + conf->size.type = CONF_SIZE_CELLS; + return true; + } + + else if (streq(key, "pad")) { + unsigned x, y, left, top, right, bottom; + char mode[64] = {0}; + int ret = sscanf(value, "%ux%ux%ux%u %63s", &left, &top, &right, &bottom, mode); + enum center_when center = CENTER_NEVER; + + if (ret == 5) { + if (strcasecmp(mode, "center") == 0) + center = CENTER_ALWAYS; + else if (strcasecmp(mode, "center-when-fullscreen") == 0) + center = CENTER_FULLSCREEN; + else if (strcasecmp(mode, "center-when-maximized-and-fullscreen") == 0) + center = CENTER_MAXIMIZED_AND_FULLSCREEN; + else + center = CENTER_INVALID; + } else if (ret < 4) { + ret = sscanf(value, "%ux%u %63s", &x, &y, mode); + if (ret >= 2) { + left = right = x; + top = bottom = y; + if (ret == 3) { + if (strcasecmp(mode, "center") == 0) + center = CENTER_ALWAYS; + else if (strcasecmp(mode, "center-when-fullscreen") == 0) + center = CENTER_FULLSCREEN; + else if (strcasecmp(mode, "center-when-maximized-and-fullscreen") == 0) + center = CENTER_MAXIMIZED_AND_FULLSCREEN; + else + center = CENTER_INVALID; + } + } + } + + if ((ret < 2 || ret > 5) || center == CENTER_INVALID) { + LOG_CONTEXTUAL_ERR( + "invalid padding (must be in the form RIGHTxTOPxLEFTxBOTTOM or XxY " + "[center|" + "center-when-fullscreen|" + "center-when-maximized-and-fullscreen])"); + return false; + } + + conf->pad_left = left; + conf->pad_top = top; + conf->pad_right = right; + conf->pad_bottom = bottom; + conf->center_when = (ret == 4 || ret == 2) ? CENTER_NEVER : center; + return true; + } + + else if (streq(key, "resize-delay-ms")) + return value_to_uint16(ctx, 10, &conf->resize_delay_ms); + + else if (streq(key, "resize-by-cells")) + return value_to_bool(ctx, &conf->resize_by_cells); + + else if (streq(key, "resize-keep-grid")) + return value_to_bool(ctx, &conf->resize_keep_grid); + + else if (streq(key, "bold-text-in-bright")) { + if (streq(value, "palette-based")) { + conf->bold_in_bright.enabled = true; + conf->bold_in_bright.palette_based = true; + } else { + if (!value_to_bool(ctx, &conf->bold_in_bright.enabled)) + return false; + conf->bold_in_bright.palette_based = false; + } + return true; + } + + else if (streq(key, "initial-window-mode")) { + _Static_assert(sizeof(conf->startup_mode) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"windowed", "maximized", "fullscreen", NULL}, + (int *)&conf->startup_mode); + } + + else if (streq(key, "font") || + streq(key, "font-bold") || + streq(key, "font-italic") || + streq(key, "font-bold-italic")) + + { + size_t idx = + streq(key, "font") ? 0 : + streq(key, "font-bold") ? 1 : + streq(key, "font-italic") ? 2 : 3; + + struct config_font_list new_list = value_to_fonts(ctx); + if (new_list.arr == NULL) + return false; + + config_font_list_destroy(&conf->fonts[idx]); + conf->fonts[idx] = new_list; + return true; + } + + else if (streq(key, "font-size-adjustment")) { + const size_t len = strlen(ctx->value); + if (len >= 1 && ctx->value[len - 1] == '%') { + errno = 0; + char *end = NULL; + + float percent = strtof(ctx->value, &end); + if (!(len > 1 && errno == 0 && end == ctx->value + len - 1)) { + LOG_CONTEXTUAL_ERR( + "invalid percent value (must be in the form 10.5%%)"); + return false; + } + + conf->font_size_adjustment.percent = percent / 100.; + conf->font_size_adjustment.pt_or_px.pt = 0; + conf->font_size_adjustment.pt_or_px.px = 0; + return true; + } else { + bool ret = value_to_pt_or_px(ctx, &conf->font_size_adjustment.pt_or_px); + if (ret) + conf->font_size_adjustment.percent = 0.; + return ret; + } + } + + else if (streq(key, "line-height")) + return value_to_pt_or_px(ctx, &conf->line_height); + + else if (streq(key, "letter-spacing")) + return value_to_pt_or_px(ctx, &conf->letter_spacing); + + else if (streq(key, "horizontal-letter-offset")) + return value_to_pt_or_px(ctx, &conf->horizontal_letter_offset); + + else if (streq(key, "vertical-letter-offset")) + return value_to_pt_or_px(ctx, &conf->vertical_letter_offset); + + else if (streq(key, "underline-offset")) { + if (!value_to_pt_or_px(ctx, &conf->underline_offset)) + return false; + conf->use_custom_underline_offset = true; + return true; + } + + else if (streq(key, "underline-thickness")) + return value_to_pt_or_px(ctx, &conf->underline_thickness); + + else if (streq(key, "strikeout-thickness")) + return value_to_pt_or_px(ctx, &conf->strikeout_thickness); + + else if (streq(key, "dpi-aware")) + return value_to_bool(ctx, &conf->dpi_aware); + + else if (streq(key, "workers")) + return value_to_uint16(ctx, 10, &conf->render_worker_count); + + else if (streq(key, "word-delimiters")) + return value_to_wchars(ctx, &conf->word_delimiters); + + else if (streq(key, "selection-target")) { + _Static_assert(sizeof(conf->selection_target) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"none", "primary", "clipboard", "both", NULL}, + (int *)&conf->selection_target); + } + + else if (streq(key, "box-drawings-uses-font-glyphs")) + return value_to_bool(ctx, &conf->box_drawings_uses_font_glyphs); + + else if (streq(key, "utmp-helper")) { + if (!value_to_str(ctx, &conf->utmp_helper_path)) + return false; + + if (streq(conf->utmp_helper_path, "none")) { + free(conf->utmp_helper_path); + conf->utmp_helper_path = NULL; + } + + return true; + } + + else if (streq(key, "gamma-correct-blending")) + return value_to_bool(ctx, &conf->gamma_correct); + + else if (streq(key, "initial-color-theme")) { + _Static_assert( + sizeof(conf->initial_color_theme) == sizeof(int), + "enum is not 32-bit"); + + if (!value_to_enum(ctx, (const char*[]){ + "dark", "light", "1", "2", NULL}, + (int *)&conf->initial_color_theme)) + return false; + + if (streq(ctx->value, "1")) { + LOG_WARN("%s:%d: [main].initial-color-theme=1 deprecated, " + "use [main].initial-color-theme=dark instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[main].initial-color-theme=1: " + "use [main].initial-color-theme=dark instead")); + + conf->initial_color_theme = COLOR_THEME_DARK; + } + + else if (streq(ctx->value, "2")) { + LOG_WARN("%s:%d: [main].initial-color-theme=2 deprecated, " + "use [main].initial-color-theme=light instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[main].initial-color-theme=2: " + "use [main].initial-color-theme=light instead")); + + conf->initial_color_theme = COLOR_THEME_LIGHT; + } + + return true; + } + + else if (streq(key, "uppercase-regex-insert")) + return value_to_bool(ctx, &conf->uppercase_regex_insert); + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_security(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "osc52")) { + _Static_assert(sizeof(conf->security.osc52) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum( + ctx, + (const char *[]){"disabled", "copy-enabled", "paste-enabled", "enabled", NULL}, + (int *)&conf->security.osc52); + } else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_bell(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "urgent")) + return value_to_bool(ctx, &conf->bell.urgent); + else if (streq(key, "notify")) + return value_to_bool(ctx, &conf->bell.notify); + else if (streq(key, "system")) + return value_to_bool(ctx, &conf->bell.system_bell); + else if (streq(key, "visual")) + return value_to_bool(ctx, &conf->bell.flash); + else if (streq(key, "command")) + return value_to_spawn_template(ctx, &conf->bell.command); + else if (streq(key, "command-focused")) + return value_to_bool(ctx, &conf->bell.command_focused); + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_desktop_notifications(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "command")) + return value_to_spawn_template( + ctx, &conf->desktop_notifications.command); + else if (streq(key, "command-action-argument")) + return value_to_spawn_template( + ctx, &conf->desktop_notifications.command_action_arg); + else if (streq(key, "close")) + return value_to_spawn_template( + ctx, &conf->desktop_notifications.close); + else if (streq(key, "inhibit-when-focused")) + return value_to_bool( + ctx, &conf->desktop_notifications.inhibit_when_focused); + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_scrollback(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + const char *value = ctx->value; + + if (streq(key, "lines")) + return value_to_uint32(ctx, 10, &conf->scrollback.lines); + + else if (streq(key, "indicator-position")) { + _Static_assert( + sizeof(conf->scrollback.indicator.position) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"none", "fixed", "relative", NULL}, + (int *)&conf->scrollback.indicator.position); + } + + else if (streq(key, "indicator-format")) { + if (streq(value, "percentage")) { + conf->scrollback.indicator.format + = SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE; + return true; + } else if (streq(value, "line")) { + conf->scrollback.indicator.format + = SCROLLBACK_INDICATOR_FORMAT_LINENO; + return true; + } else + return value_to_wchars(ctx, &conf->scrollback.indicator.text); + } + + else if (streq(key, "multiplier")) + return value_to_float(ctx, &conf->scrollback.multiplier); + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_url(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "launch")) + return value_to_spawn_template(ctx, &conf->url.launch); + + else if (streq(key, "label-letters")) + return value_to_wchars(ctx, &conf->url.label_letters); + + else if (streq(key, "style")) { + _Static_assert(sizeof(conf->url.style) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum( + ctx, (const char *[]){"none", "single", "double", "curly", "dotted", "dashed", NULL}, + (int *)&conf->url.style); + } + + else if (streq(key, "osc8-underline")) { + _Static_assert(sizeof(conf->url.osc8_underline) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"url-mode", "always", NULL}, + (int *)&conf->url.osc8_underline); + } + + else if (streq(key, "regex")) { + const char *regex = ctx->value; + regex_t preg; + + int r = regcomp(&preg, regex, REG_EXTENDED); + + if (r != 0) { + char err_buf[128]; + regerror(r, &preg, err_buf, sizeof(err_buf)); + LOG_CONTEXTUAL_ERR("invalid regex: %s", err_buf); + return false; + } + + if (preg.re_nsub == 0) { + LOG_CONTEXTUAL_ERR("invalid regex: no marked subexpression(s)"); + regfree(&preg); + return false; + } + + regfree(&conf->url.preg); + free(conf->url.regex); + + conf->url.regex = xstrdup(regex); + conf->url.preg = preg; + return true; + } + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_regex(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + const char *regex_name = + ctx->section_suffix != NULL ? ctx->section_suffix : ""; + + struct custom_regex *regex = NULL; + tll_foreach(conf->custom_regexes, it) { + if (streq(it->item.name, regex_name)) { + regex = &it->item; + break; + } + } + + if (streq(key, "regex")) { + const char *regex_string = ctx->value; + regex_t preg; + + int r = regcomp(&preg, regex_string, REG_EXTENDED); + + if (r != 0) { + char err_buf[128]; + regerror(r, &preg, err_buf, sizeof(err_buf)); + LOG_CONTEXTUAL_ERR("invalid regex: %s", err_buf); + return false; + } + + if (preg.re_nsub == 0) { + LOG_CONTEXTUAL_ERR("invalid regex: no marked subexpression(s)"); + regfree(&preg); + return false; + } + + if (regex == NULL) { + tll_push_back(conf->custom_regexes, + ((struct custom_regex){.name = xstrdup(regex_name)})); + regex = &tll_back(conf->custom_regexes); + } + + regfree(®ex->preg); + free(regex->regex); + + regex->regex = xstrdup(regex_string); + regex->preg = preg; + return true; + } + + else if (streq(key, "launch")) { + struct config_spawn_template launch = {NULL}; + if (!value_to_spawn_template(ctx, &launch)) + return false; + + if (regex == NULL) { + tll_push_back(conf->custom_regexes, + ((struct custom_regex){.name = xstrdup(regex_name)})); + regex = &tll_back(conf->custom_regexes); + } + + spawn_template_free(®ex->launch); + regex->launch = launch; + return true; + } + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool NOINLINE +parse_color_theme(struct context *ctx, struct color_theme *theme) +{ + const char *key = ctx->key; + + size_t key_len = strlen(key); + uint8_t last_digit = (unsigned char)key[key_len - 1] - '0'; + uint32_t *color = NULL; + + if (isdigit(key[0])) { + unsigned long index; + if (!str_to_ulong(key, 0, &index) || index >= ALEN(theme->table)) { + LOG_CONTEXTUAL_ERR( + "invalid color palette index: %s (not in range 0-%zu)", + key, ALEN(theme->table)); + return false; + } + color = &theme->table[index]; + } + + else if (key_len == 8 && str_has_prefix(key, "regular") && last_digit < 8) + color = &theme->table[last_digit]; + + else if (key_len == 7 && str_has_prefix(key, "bright") && last_digit < 8) + color = &theme->table[8 + last_digit]; + + else if (key_len == 4 && str_has_prefix(key, "dim") && last_digit < 8) { + if (!value_to_color(ctx, &theme->dim[last_digit], false)) + return false; + + theme->use_custom.dim |= 1 << last_digit; + return true; + } + + else if (str_has_prefix(key, "sixel") && + ((key_len == 6 && last_digit < 10) || + (key_len == 7 && key[5] == '1' && last_digit < 6))) + { + size_t idx = key_len == 6 ? last_digit : 10 + last_digit; + return value_to_color(ctx, &theme->sixel[idx], false); + } + + else if (streq(key, "flash")) color = &theme->flash; + else if (streq(key, "foreground")) color = &theme->fg; + else if (streq(key, "background")) color = &theme->bg; + else if (streq(key, "selection-foreground")) color = &theme->selection_fg; + else if (streq(key, "selection-background")) color = &theme->selection_bg; + + else if (streq(key, "jump-labels")) { + if (!value_to_two_colors( + ctx, + &theme->jump_label.fg, + &theme->jump_label.bg, + false)) + { + return false; + } + + theme->use_custom.jump_label = true; + return true; + } + + else if (streq(key, "scrollback-indicator")) { + if (!value_to_two_colors( + ctx, + &theme->scrollback_indicator.fg, + &theme->scrollback_indicator.bg, + false)) + { + return false; + } + + theme->use_custom.scrollback_indicator = true; + return true; + } + + else if (streq(key, "search-box-no-match")) { + if (!value_to_two_colors( + ctx, + &theme->search_box.no_match.fg, + &theme->search_box.no_match.bg, + false)) + { + return false; + } + + theme->use_custom.search_box_no_match = true; + return true; + } + + else if (streq(key, "search-box-match")) { + if (!value_to_two_colors( + ctx, + &theme->search_box.match.fg, + &theme->search_box.match.bg, + false)) + { + return false; + } + + theme->use_custom.search_box_match = true; + return true; + } + + else if (streq(key, "cursor")) { + if (!value_to_two_colors( + ctx, + &theme->cursor.text, + &theme->cursor.cursor, + false)) + { + return false; + } + + theme->use_custom.cursor = true; + return true; + } + + else if (streq(key, "urls")) { + if (!value_to_color(ctx, &theme->url, false)) + return false; + + theme->use_custom.url = true; + return true; + } + + else if (streq(key, "alpha")) { + float alpha; + if (!value_to_float(ctx, &alpha)) + return false; + + if (alpha < 0. || alpha > 1.) { + LOG_CONTEXTUAL_ERR("not in range 0.0-1.0"); + return false; + } + + theme->alpha = alpha * 65535.; + return true; + } + + else if (streq(key, "flash-alpha")) { + float alpha; + if (!value_to_float(ctx, &alpha)) + return false; + + if (alpha < 0. || alpha > 1.) { + LOG_CONTEXTUAL_ERR("not in range 0.0-1.0"); + return false; + } + + theme->flash_alpha = alpha * 65535.; + return true; + } + + else if (streq(key, "alpha-mode")) { + _Static_assert(sizeof(theme->alpha_mode) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"default", "matching", "all", NULL}, + (int *)&theme->alpha_mode); + } + + else if (streq(key, "dim-blend-towards")) { + _Static_assert(sizeof(theme->dim_blend_towards) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"black", "white", NULL}, + (int *)&theme->dim_blend_towards); + } + + else if (streq(key, "blur")) + return value_to_bool(ctx, &theme->blur); + + else { + LOG_CONTEXTUAL_ERR("not valid option"); + return false; + } + + uint32_t color_value; + if (!value_to_color(ctx, &color_value, false)) + return false; + + *color = color_value; + return true; +} + +static bool +parse_section_colors_dark(struct context *ctx) +{ + return parse_color_theme(ctx, &ctx->conf->colors_dark); +} + +static bool +parse_section_colors_light(struct context *ctx) +{ + return parse_color_theme(ctx, &ctx->conf->colors_light); +} + +static bool +parse_section_colors(struct context *ctx) +{ + LOG_WARN("%s:%d: [colors]: deprecated; use [colors-dark] instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[colors]: use [colors-dark] instead")); + + return parse_color_theme(ctx, &ctx->conf->colors_dark); +} + +static bool +parse_section_colors2(struct context *ctx) +{ + LOG_WARN("%s:%d: [colors2]: deprecated; use [colors-light] instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[colors2]: use [colors-light] instead")); + + return parse_color_theme(ctx, &ctx->conf->colors_light); +} + +static bool +parse_section_cursor(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "style")) { + _Static_assert(sizeof(conf->cursor.style) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"block", "underline", "beam", "hollow", NULL}, + (int *)&conf->cursor.style); + } + + else if (streq(key, "unfocused-style")) { + _Static_assert(sizeof(conf->cursor.unfocused_style) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"unchanged", "hollow", "none", NULL}, + (int *)&conf->cursor.unfocused_style); + } + + else if (streq(key, "blink")) + return value_to_bool(ctx, &conf->cursor.blink.enabled); + + else if (streq(key, "blink-rate")) + return value_to_uint32(ctx, 10, &conf->cursor.blink.rate_ms); + + else if (streq(key, "beam-thickness")) + return value_to_pt_or_px(ctx, &conf->cursor.beam_thickness); + + else if (streq(key, "underline-thickness")) + return value_to_pt_or_px(ctx, &conf->cursor.underline_thickness); + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_mouse(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "hide-when-typing")) + return value_to_bool(ctx, &conf->mouse.hide_when_typing); + + else if (streq(key, "alternate-scroll-mode")) + return value_to_bool(ctx, &conf->mouse.alternate_scroll_mode); + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_csd(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "preferred")) { + _Static_assert(sizeof(conf->csd.preferred) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"none", "server", "client", NULL}, + (int *)&conf->csd.preferred); + } + + else if (streq(key, "font")) { + struct config_font_list new_list = value_to_fonts(ctx); + if (new_list.arr == NULL) + return false; + + config_font_list_destroy(&conf->csd.font); + conf->csd.font = new_list; + return true; + } + + else if (streq(key, "color")) { + uint32_t color; + if (!value_to_color(ctx, &color, true)) + return false; + + conf->csd.color.title_set = true; + conf->csd.color.title = color; + return true; + } + + else if (streq(key, "size")) + return value_to_uint16(ctx, 10, &conf->csd.title_height); + + else if (streq(key, "button-width")) + return value_to_uint16(ctx, 10, &conf->csd.button_width); + + else if (streq(key, "button-color")) { + if (!value_to_color(ctx, &conf->csd.color.buttons, true)) + return false; + + conf->csd.color.buttons_set = true; + return true; + } + + else if (streq(key, "button-minimize-color")) { + if (!value_to_color(ctx, &conf->csd.color.minimize, true)) + return false; + + conf->csd.color.minimize_set = true; + return true; + } + + else if (streq(key, "button-maximize-color")) { + if (!value_to_color(ctx, &conf->csd.color.maximize, true)) + return false; + + conf->csd.color.maximize_set = true; + return true; + } + + else if (streq(key, "button-close-color")) { + if (!value_to_color(ctx, &conf->csd.color.quit, true)) + return false; + + conf->csd.color.close_set = true; + return true; + } + + else if (streq(key, "border-color")) { + if (!value_to_color(ctx, &conf->csd.color.border, true)) + return false; + + conf->csd.color.border_set = true; + return true; + } + + else if (streq(key, "border-width")) + return value_to_uint16(ctx, 10, &conf->csd.border_width_visible); + + else if (streq(key, "hide-when-maximized")) + return value_to_bool(ctx, &conf->csd.hide_when_maximized); + + else if (streq(key, "double-click-to-maximize")) + return value_to_bool(ctx, &conf->csd.double_click_to_maximize); + + else { + LOG_CONTEXTUAL_ERR("not a valid action: %s", key); + return false; + } +} + +static bool +parse_section_tabs(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "enabled")) + return value_to_bool(ctx, &conf->tabs.enabled); + + else if (streq(key, "position")) { + _Static_assert(sizeof(conf->tabs.position) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum( + ctx, + (const char *[]){"top", "bottom", NULL}, + (int *)&conf->tabs.position); + } + + else if (streq(key, "style")) { + _Static_assert(sizeof(conf->tabs.style) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum( + ctx, + (const char *[]){"rounded", "square", NULL}, + (int *)&conf->tabs.style); + } + + else if (streq(key, "layout")) { + _Static_assert(sizeof(conf->tabs.layout) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum( + ctx, + (const char *[]){"span", "floating", NULL}, + (int *)&conf->tabs.layout); + } + + else if (streq(key, "height")) + return value_to_uint16(ctx, 10, &conf->tabs.height); + + else if (streq(key, "tab-width")) + return value_to_uint16(ctx, 10, &conf->tabs.tab_width); + + else if (streq(key, "tab-padding")) + return value_to_uint16(ctx, 10, &conf->tabs.tab_padding); + + else if (streq(key, "label-padding")) + return value_to_uint16(ctx, 10, &conf->tabs.label_padding); + + else if (streq(key, "margin")) + return value_to_uint16(ctx, 10, &conf->tabs.margin); + + else if (streq(key, "corner-radius")) + return value_to_uint16(ctx, 10, &conf->tabs.corner_radius); + + else if (streq(key, "background")) + return value_to_color(ctx, &conf->tabs.colors.bg, false); + + else if (streq(key, "foreground")) + return value_to_color(ctx, &conf->tabs.colors.fg, false); + + else if (streq(key, "active-background")) + return value_to_color(ctx, &conf->tabs.colors.active_bg, false); + + else if (streq(key, "active-foreground")) + return value_to_color(ctx, &conf->tabs.colors.active_fg, false); + + else if (streq(key, "unread-color")) + return value_to_color(ctx, &conf->tabs.colors.unread_fg, false); + + else if (streq(key, "unread-indicator")) { + free(conf->tabs.unread_indicator); + conf->tabs.unread_indicator = xstrdup(ctx->value); + return true; + } + + else if (streq(key, "inherit-cwd")) + return value_to_bool(ctx, &conf->tabs.inherit_cwd); + + else { + LOG_CONTEXTUAL_ERR("not a valid tabs option: %s", key); + return false; + } +} + +static void +free_binding_aux(struct binding_aux *aux) +{ + if (!aux->master_copy) + return; + + switch (aux->type) { + case BINDING_AUX_NONE: break; + case BINDING_AUX_PIPE: free_argv(&aux->pipe); break; + case BINDING_AUX_TEXT: free(aux->text.data); break; + case BINDING_AUX_REGEX: free(aux->regex_name); break; + } +} + +static void +free_key_binding(struct config_key_binding *binding) +{ + free_binding_aux(&binding->aux); + tll_free_and_free(binding->modifiers, free); +} + +static void NOINLINE +free_key_binding_list(struct config_key_binding_list *bindings) +{ + struct config_key_binding *binding = &bindings->arr[0]; + + for (size_t i = 0; i < bindings->count; i++, binding++) + free_key_binding(binding); + free(bindings->arr); + + bindings->arr = NULL; + bindings->count = 0; +} + +static void NOINLINE +parse_modifiers(const char *text, size_t len, config_modifier_list_t *modifiers) +{ + tll_free_and_free(*modifiers, free); + + /* Handle "none" separately because e.g. none+shift is nonsense */ + if (strncmp(text, "none", len) == 0) + return; + + char *copy = xstrndup(text, len); + + for (char *ctx = NULL, *key = strtok_r(copy, "+", &ctx); + key != NULL; + key = strtok_r(NULL, "+", &ctx)) + { + tll_push_back(*modifiers, xstrdup(key)); + } + + free(copy); + tll_sort(*modifiers, strcmp); +} + +static int NOINLINE +argv_compare(const struct argv *argv1, const struct argv *argv2) +{ + if (argv1->args == NULL && argv2->args == NULL) + return 0; + + if (argv1->args == NULL) + return -1; + if (argv2->args == NULL) + return 1; + + for (size_t i = 0; ; i++) { + if (argv1->args[i] == NULL && argv2->args[i] == NULL) + return 0; + if (argv1->args[i] == NULL) + return -1; + if (argv2->args[i] == NULL) + return 1; + + int ret = strcmp(argv1->args[i], argv2->args[i]); + if (ret != 0) + return ret; + } + + BUG("unexpected loop break"); + return 1; +} + +static bool NOINLINE +binding_aux_equal(const struct binding_aux *a, + const struct binding_aux *b) +{ + if (a->type != b->type) + return false; + + switch (a->type) { + case BINDING_AUX_NONE: + return true; + + case BINDING_AUX_PIPE: + return argv_compare(&a->pipe, &b->pipe) == 0; + + case BINDING_AUX_TEXT: + return a->text.len == b->text.len && + memcmp(a->text.data, b->text.data, a->text.len) == 0; + + case BINDING_AUX_REGEX: + return streq(a->regex_name, b->regex_name); + } + + BUG("invalid AUX type: %d", a->type); + return false; +} + +static void NOINLINE +remove_from_key_bindings_list(struct config_key_binding_list *bindings, + int action, const struct binding_aux *aux) +{ + size_t remove_first_idx = 0; + size_t remove_count = 0; + + for (size_t i = 0; i < bindings->count; i++) { + struct config_key_binding *binding = &bindings->arr[i]; + + if (binding->action != action) + continue; + + if (binding_aux_equal(&binding->aux, aux)) { + if (remove_count++ == 0) + remove_first_idx = i; + + xassert(remove_first_idx + remove_count - 1 == i); + free_key_binding(binding); + } + } + + if (remove_count == 0) + return; + + size_t move_count = bindings->count - (remove_first_idx + remove_count); + + memmove( + &bindings->arr[remove_first_idx], + &bindings->arr[remove_first_idx + remove_count], + move_count * sizeof(bindings->arr[0])); + bindings->count -= remove_count; +} + +static const struct { + const char *name; + int code; +} button_map[] = { + /* System defined */ + {"BTN_LEFT", BTN_LEFT}, + {"BTN_RIGHT", BTN_RIGHT}, + {"BTN_MIDDLE", BTN_MIDDLE}, + {"BTN_SIDE", BTN_SIDE}, + {"BTN_EXTRA", BTN_EXTRA}, + {"BTN_FORWARD", BTN_FORWARD}, + {"BTN_BACK", BTN_BACK}, + {"BTN_TASK", BTN_TASK}, + + /* Foot custom, to be able to map scroll events to mouse bindings */ + {"BTN_WHEEL_BACK", BTN_WHEEL_BACK}, + {"BTN_WHEEL_FORWARD", BTN_WHEEL_FORWARD}, + {"BTN_WHEEL_LEFT", BTN_WHEEL_LEFT}, + {"BTN_WHEEL_RIGHT", BTN_WHEEL_RIGHT}, +}; + +static int +mouse_button_name_to_code(const char *name) +{ + for (size_t i = 0; i < ALEN(button_map); i++) { + if (streq(button_map[i].name, name)) + return button_map[i].code; + } + return -1; +} + +static const char* +mouse_button_code_to_name(int code) +{ + for (size_t i = 0; i < ALEN(button_map); i++) { + if (code == button_map[i].code) + return button_map[i].name; + } + + return NULL; +} + +static bool NOINLINE +value_to_key_combos(struct context *ctx, int action, + struct binding_aux *aux, + struct config_key_binding_list *bindings, + enum key_binding_type type) +{ + if (strcasecmp(ctx->value, "none") == 0) { + remove_from_key_bindings_list(bindings, action, aux); + return true; + } + + /* Count number of combinations */ + size_t combo_count = 1; + size_t used_combos = 1; /* For error handling */ + for (const char *p = strchr(ctx->value, ' '); + p != NULL; + p = strchr(p + 1, ' ')) + { + combo_count++; + } + + struct config_key_binding new_combos[combo_count]; + + char *copy = xstrdup(ctx->value); + size_t idx = 0; + + for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx); + combo != NULL; + combo = strtok_r(NULL, " ", &tok_ctx), + idx++, used_combos++) + { + struct config_key_binding *new_combo = &new_combos[idx]; + new_combo->action = action; + new_combo->aux = *aux; + new_combo->aux.master_copy = idx == 0; +#if 0 + new_combo->aux.type = BINDING_AUX_PIPE; + new_combo->aux.master_copy = idx == 0; + new_combo->aux.pipe = *argv; +#endif + memset(&new_combo->modifiers, 0, sizeof(new_combo->modifiers)); + new_combo->path = ctx->path; + new_combo->lineno = ctx->lineno; + + char *key = strrchr(combo, '+'); + + if (key == NULL) { + /* No modifiers */ + key = combo; + } else { + *key = '\0'; + parse_modifiers(combo, key - combo, &new_combo->modifiers); + key++; /* Skip past the '+' */ + } + + switch (type) { + case KEY_BINDING: + /* Translate key name to symbol */ + new_combo->k.sym = xkb_keysym_from_name(key, 0); + if (new_combo->k.sym == XKB_KEY_NoSymbol) { + LOG_CONTEXTUAL_ERR("not a valid XKB key name: %s", key); + goto err; + } + break; + + case MOUSE_BINDING: { + new_combo->m.count = 1; + + char *_count = strrchr(key, '-'); + if (_count != NULL) { + *_count = '\0'; + _count++; + + errno = 0; + char *end; + unsigned long value = strtoul(_count, &end, 10); + if (_count[0] == '\0' || *end != '\0' || errno != 0) { + if (errno != 0) + LOG_CONTEXTUAL_ERRNO("invalid click count: %s", _count); + else + LOG_CONTEXTUAL_ERR("invalid click count: %s", _count); + goto err; + } + + new_combo->m.count = value; + } + + new_combo->m.button = mouse_button_name_to_code(key); + if (new_combo->m.button < 0) { + LOG_CONTEXTUAL_ERR("invalid mouse button name: %s", key); + goto err; + } + + break; + } + } + + } + + if (idx == 0) { + LOG_CONTEXTUAL_ERR( + "empty binding not allowed (set to 'none' to unmap)"); + free(copy); + return false; + } + + remove_from_key_bindings_list(bindings, action, aux); + + bindings->arr = xrealloc( + bindings->arr, + (bindings->count + combo_count) * sizeof(bindings->arr[0])); + + memcpy(&bindings->arr[bindings->count], + new_combos, + combo_count * sizeof(bindings->arr[0])); + bindings->count += combo_count; + + free(copy); + return true; + +err: + for (size_t i = 0; i < used_combos; i++) + free_key_binding(&new_combos[i]); + free(copy); + return false; +} + +static bool +modifiers_equal(const config_modifier_list_t *mods1, + const config_modifier_list_t *mods2) +{ + if (tll_length(*mods1) != tll_length(*mods2)) + return false; + + size_t count = 0; + tll_foreach(*mods1, it1) { + size_t skip = count; + tll_foreach(*mods2, it2) { + if (skip > 0) { + skip--; + continue; + } + + if (strcmp(it1->item, it2->item) != 0) + return false; + break; + } + + count++; + } + + return true; + /* + * bool shift = mods1->shift == mods2->shift; + * bool alt = mods1->alt == mods2->alt; + * bool ctrl = mods1->ctrl == mods2->ctrl; + * bool super = mods1->super == mods2->super; + * return shift && alt && ctrl && super; + */ +} + +UNITTEST +{ + config_modifier_list_t mods1 = tll_init(); + config_modifier_list_t mods2 = tll_init(); + + tll_push_back(mods1, xstrdup("foo")); + tll_push_back(mods1, xstrdup("bar")); + + tll_push_back(mods2, xstrdup("foo")); + xassert(!modifiers_equal(&mods1, &mods2)); + + tll_push_back(mods2, xstrdup("zoo")); + xassert(!modifiers_equal(&mods1, &mods2)); + + free(tll_pop_back(mods2)); + tll_push_back(mods2, xstrdup("bar")); + xassert(modifiers_equal(&mods1, &mods2)); + + tll_free_and_free(mods1, free); + tll_free_and_free(mods2, free); +} + +static bool +modifiers_disjoint(const config_modifier_list_t *mods1, + const config_modifier_list_t *mods2) +{ + return !modifiers_equal(mods1, mods2); +} + +static char * NOINLINE +modifiers_to_str(const config_modifier_list_t *mods, bool strip_last_plus) +{ + size_t len = tll_length(*mods); /* '+' separator */ + tll_foreach(*mods, it) + len += strlen(it->item); + + char *ret = xmalloc(len + 1); + size_t idx = 0; + tll_foreach(*mods, it) { + idx += snprintf(&ret[idx], len - idx, "%s", it->item); + ret[idx++] = '+'; + } + + if (strip_last_plus) + idx--; + + ret[idx] = '\0'; + return ret; +} + +/* + * Parses a key binding value in the form + * "[cmd-to-exec arg1 arg2] Mods+Key" + * + * and extracts 'cmd-to-exec' and its arguments. + * + * Input: + * - value: raw string, in the form mentioned above + * - cmd: pointer to string to will be allocated and filled with + * 'cmd-to-exec arg1 arg2' + * - argv: point to array of string. Array will be allocated. Will be + * filled with {'cmd-to-exec', 'arg1', 'arg2', NULL} + * + * Returns: + * - ssize_t, number of bytes that were stripped from 'value' to remove the '[]' + * enclosed cmd and its arguments, including any subsequent + * whitespace characters. I.e. if 'value' is "[cmd] BTN_RIGHT", the + * return value is 6 (strlen("[cmd] ")). + * - cmd: allocated string containing "cmd arg1 arg2...". Caller frees. + * - argv: allocated array containing {"cmd", "arg1", "arg2", NULL}. Caller frees. + */ +static ssize_t NOINLINE +pipe_argv_from_value(struct context *ctx, struct argv *argv) +{ + argv->args = NULL; + + if (ctx->value[0] != '[') + return 0; + + const char *pipe_cmd_end = strrchr(ctx->value, ']'); + if (pipe_cmd_end == NULL) { + LOG_CONTEXTUAL_ERR("unclosed '['"); + return -1; + } + + size_t pipe_len = pipe_cmd_end - ctx->value - 1; + char *cmd = xstrndup(&ctx->value[1], pipe_len); + + if (!tokenize_cmdline(cmd, &argv->args)) { + LOG_CONTEXTUAL_ERR("syntax error in command line"); + free(cmd); + return -1; + } + + ssize_t remove_len = pipe_cmd_end + 1 - ctx->value; + ctx->value = pipe_cmd_end + 1; + while (isspace(*ctx->value)) { + ctx->value++; + remove_len++; + } + + free(cmd); + return remove_len; +} + +static ssize_t NOINLINE +regex_name_from_value(struct context *ctx, char **regex_name) +{ + *regex_name = NULL; + + if (ctx->value[0] != '[') + return 0; + + const char *regex_end = strrchr(ctx->value, ']'); + if (regex_end == NULL) { + LOG_CONTEXTUAL_ERR("unclosed '['"); + return -1; + } + + size_t regex_len = regex_end - ctx->value - 1; + *regex_name = xstrndup(&ctx->value[1], regex_len); + + ssize_t remove_len = regex_end + 1 - ctx->value; + ctx->value = regex_end + 1; + while (isspace(*ctx->value)) { + ctx->value++; + remove_len++; + } + + return remove_len; +} + + +static bool NOINLINE +parse_key_binding_section(struct context *ctx, + int action_count, + const char *const action_map[static action_count], + struct config_key_binding_list *bindings) +{ + for (int action = 0; action < action_count; action++) { + if (action_map[action] == NULL) + continue; + + if (!streq(ctx->key, action_map[action])) + continue; + + struct binding_aux aux = {.type = BINDING_AUX_NONE, .master_copy = true}; + + /* TODO: this is ugly... */ + if (action_map == binding_action_map && + action >= BIND_ACTION_PIPE_SCROLLBACK && + action <= BIND_ACTION_PIPE_COMMAND_OUTPUT) + { + ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &aux.pipe); + if (pipe_remove_len <= 0) + return false; + + aux.type = BINDING_AUX_PIPE; + aux.master_copy = true; + } else if (action_map == binding_action_map && + action >= BIND_ACTION_REGEX_LAUNCH && + action <= BIND_ACTION_REGEX_COPY) + { + char *regex_name = NULL; + ssize_t regex_remove_len = regex_name_from_value(ctx, ®ex_name); + if (regex_remove_len <= 0) + return false; + + aux.type = BINDING_AUX_REGEX; + aux.master_copy = true; + aux.regex_name = regex_name; + } + + if (action_map == binding_action_map && + action >= BIND_ACTION_THEME_SWITCH_1 && + action <= BIND_ACTION_THEME_SWITCH_2) + { + const char *use_instead = + action_map[action == BIND_ACTION_THEME_SWITCH_1 + ? BIND_ACTION_THEME_SWITCH_DARK + : BIND_ACTION_THEME_SWITCH_LIGHT]; + + const char *notif = action == BIND_ACTION_THEME_SWITCH_1 + ? "[key-bindings].color-theme-switch-1: use [key-bindings].color-theme-switch-dark instead" + : "[key-bindings].color-theme-switch-2: use [key-bindings].color-theme-switch-light instead"; + + LOG_WARN("%s:%d: [key-bindings].%s: deprecated, use %s instead", + ctx->path, ctx->lineno, + action_map[action], use_instead); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup(notif)); + } + + if (!value_to_key_combos(ctx, action, &aux, bindings, KEY_BINDING)) { + free_binding_aux(&aux); + return false; + } + + return true; + } + + LOG_CONTEXTUAL_ERR("not a valid action: %s", ctx->key); + return false; +} + +UNITTEST +{ + enum test_actions { + TEST_ACTION_NONE, + TEST_ACTION_FOO, + TEST_ACTION_BAR, + TEST_ACTION_COUNT, + }; + + const char *const map[] = { + [TEST_ACTION_NONE] = NULL, + [TEST_ACTION_FOO] = "foo", + [TEST_ACTION_BAR] = "bar", + }; + + struct config conf = {0}; + struct config_key_binding_list bindings = {0}; + + struct context ctx = { + .conf = &conf, + .section = "", + .key = "foo", + .value = "Escape", + .path = "", + }; + + /* + * ADD foo=Escape + * + * This verifies we can bind a single key combo to an action. + */ + xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); + xassert(bindings.count == 1); + xassert(bindings.arr[0].action == TEST_ACTION_FOO); + xassert(bindings.arr[0].k.sym == XKB_KEY_Escape); + + /* + * ADD bar=Control+g Control+Shift+x + * + * This verifies we can bind multiple key combos to an action. + */ + ctx.key = "bar"; + ctx.value = "Control+g Control+Shift+x"; + xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); + xassert(bindings.count == 3); + xassert(bindings.arr[0].action == TEST_ACTION_FOO); + xassert(bindings.arr[1].action == TEST_ACTION_BAR); + xassert(bindings.arr[1].k.sym == XKB_KEY_g); + xassert(tll_length(bindings.arr[1].modifiers) == 1); + xassert(strcmp(tll_front(bindings.arr[1].modifiers), XKB_MOD_NAME_CTRL) == 0); + xassert(bindings.arr[2].action == TEST_ACTION_BAR); + xassert(bindings.arr[2].k.sym == XKB_KEY_x); + xassert(tll_length(bindings.arr[2].modifiers) == 2); + xassert(strcmp(tll_front(bindings.arr[2].modifiers), XKB_MOD_NAME_CTRL) == 0); + xassert(strcmp(tll_back(bindings.arr[2].modifiers), XKB_MOD_NAME_SHIFT) == 0); + + /* + * REPLACE foo with foo=Mod+v Shift+q + * + * This verifies we can update a single-combo action with multiple + * key combos. + */ + ctx.key = "foo"; + ctx.value = "Mod1+v Shift+q"; + xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); + xassert(bindings.count == 4); + xassert(bindings.arr[0].action == TEST_ACTION_BAR); + xassert(bindings.arr[1].action == TEST_ACTION_BAR); + xassert(bindings.arr[2].action == TEST_ACTION_FOO); + xassert(bindings.arr[2].k.sym == XKB_KEY_v); + xassert(tll_length(bindings.arr[2].modifiers) == 1); + xassert(strcmp(tll_front(bindings.arr[2].modifiers), XKB_MOD_NAME_ALT) == 0); + xassert(bindings.arr[3].action == TEST_ACTION_FOO); + xassert(bindings.arr[3].k.sym == XKB_KEY_q); + xassert(tll_length(bindings.arr[3].modifiers) == 1); + xassert(strcmp(tll_front(bindings.arr[3].modifiers), XKB_MOD_NAME_SHIFT) == 0); + + /* + * REMOVE bar + */ + ctx.key = "bar"; + ctx.value = "none"; + xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); + xassert(bindings.count == 2); + xassert(bindings.arr[0].action == TEST_ACTION_FOO); + xassert(bindings.arr[1].action == TEST_ACTION_FOO); + + /* + * REMOVE foo + */ + ctx.key = "foo"; + ctx.value = "none"; + xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); + xassert(bindings.count == 0); + + free(bindings.arr); +} + +static bool +parse_section_key_bindings(struct context *ctx) +{ + return parse_key_binding_section( + ctx, + BIND_ACTION_KEY_COUNT, binding_action_map, + &ctx->conf->bindings.key); +} + +static bool +parse_section_search_bindings(struct context *ctx) +{ + return parse_key_binding_section( + ctx, + BIND_ACTION_SEARCH_COUNT, search_binding_action_map, + &ctx->conf->bindings.search); +} + +static bool +parse_section_url_bindings(struct context *ctx) +{ + return parse_key_binding_section( + ctx, + BIND_ACTION_URL_COUNT, url_binding_action_map, + &ctx->conf->bindings.url); +} + +static bool NOINLINE +resolve_key_binding_collisions(struct config *conf, const char *section_name, + const char *const action_map[], + struct config_key_binding_list *bindings, + enum key_binding_type type) +{ + bool ret = true; + + for (size_t i = 1; i < bindings->count; i++) { + enum {COLLISION_NONE, + COLLISION_OVERRIDE, + COLLISION_BINDING} collision_type = COLLISION_NONE; + const struct config_key_binding *collision_binding = NULL; + + struct config_key_binding *binding1 = &bindings->arr[i]; + xassert(binding1->action != BIND_ACTION_NONE); + + const config_modifier_list_t *mods1 = &binding1->modifiers; + + /* Does our modifiers collide with the selection override mods? */ + if (type == MOUSE_BINDING && + !modifiers_disjoint( + mods1, &conf->mouse.selection_override_modifiers)) + { + collision_type = COLLISION_OVERRIDE; + } + + /* Does our binding collide with another binding? */ + for (ssize_t j = i - 1; + collision_type == COLLISION_NONE && j >= 0; + j--) + { + const struct config_key_binding *binding2 = &bindings->arr[j]; + xassert(binding2->action != BIND_ACTION_NONE); + + if (binding2->action == binding1->action && + binding_aux_equal(&binding1->aux, &binding2->aux)) + { + continue; + } + + const config_modifier_list_t *mods2 = &binding2->modifiers; + + bool mods_equal = modifiers_equal(mods1, mods2); + bool sym_equal; + + switch (type) { + case KEY_BINDING: + sym_equal = binding1->k.sym == binding2->k.sym; + break; + + case MOUSE_BINDING: + sym_equal = (binding1->m.button == binding2->m.button && + binding1->m.count == binding2->m.count); + break; + + default: + BUG("unhandled key binding type"); + } + + if (!mods_equal || !sym_equal) + continue; + + collision_binding = binding2; + collision_type = COLLISION_BINDING; + break; + } + + if (collision_type != COLLISION_NONE) { + char *modifier_names = modifiers_to_str(mods1, false); + char sym_name[64]; + + switch (type){ + case KEY_BINDING: + xkb_keysym_get_name(binding1->k.sym, sym_name, sizeof(sym_name)); + break; + + case MOUSE_BINDING: { + const char *button_name = + mouse_button_code_to_name(binding1->m.button); + + if (binding1->m.count > 1) { + snprintf(sym_name, sizeof(sym_name), "%s-%d", + button_name, binding1->m.count); + } else + strcpy(sym_name, button_name); + break; + } + } + + switch (collision_type) { + case COLLISION_NONE: + break; + + case COLLISION_BINDING: { + bool has_pipe = collision_binding->aux.type == BINDING_AUX_PIPE; + LOG_AND_NOTIFY_ERR( + "%s:%d: [%s].%s: %s%s already mapped to '%s%s%s%s'", + binding1->path, binding1->lineno, section_name, + action_map[binding1->action], + modifier_names, sym_name, + action_map[collision_binding->action], + has_pipe ? " [" : "", + has_pipe ? collision_binding->aux.pipe.args[0] : "", + has_pipe ? "]" : ""); + ret = false; + break; + } + + case COLLISION_OVERRIDE: { + char *override_names = modifiers_to_str( + &conf->mouse.selection_override_modifiers, true); + + if (override_names[0] != '\0') + override_names[strlen(override_names) - 1] = '\0'; + + LOG_AND_NOTIFY_ERR( + "%s:%d: [%s].%s: %s%s: " + "modifiers conflict with 'selection-override-modifiers=%s'", + binding1->path != NULL ? binding1->path : "(default)", + binding1->lineno, section_name, + action_map[binding1->action], + modifier_names, sym_name, override_names); + ret = false; + free(override_names); + break; + } + } + + free(modifier_names); + + if (binding1->aux.master_copy && i + 1 < bindings->count) { + struct config_key_binding *next = &bindings->arr[i + 1]; + + if (next->action == binding1->action && + binding_aux_equal(&binding1->aux, &next->aux)) + { + /* Transfer ownership to next binding */ + next->aux.master_copy = true; + binding1->aux.master_copy = false; + } + } + + free_key_binding(binding1); + + /* Remove the most recent binding */ + size_t move_count = bindings->count - (i + 1); + memmove(&bindings->arr[i], &bindings->arr[i + 1], + move_count * sizeof(bindings->arr[0])); + bindings->count--; + + i--; + } + } + + return ret; +} + +static bool +parse_section_mouse_bindings(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + const char *value = ctx->value; + + if (streq(key, "selection-override-modifiers")) { + parse_modifiers( + ctx->value, strlen(value), + &conf->mouse.selection_override_modifiers); + return true; + } + + struct binding_aux aux; + + ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &aux.pipe); + if (pipe_remove_len < 0) + return false; + + aux.type = pipe_remove_len == 0 ? BINDING_AUX_NONE : BINDING_AUX_PIPE; + aux.master_copy = true; + + for (enum bind_action_normal action = 0; + action < BIND_ACTION_COUNT; + action++) + { + if (binding_action_map[action] == NULL) + continue; + + if (!streq(key, binding_action_map[action])) + continue; + + if (!value_to_key_combos( + ctx, action, &aux, &conf->bindings.mouse, MOUSE_BINDING)) + { + free_binding_aux(&aux); + return false; + } + + return true; + } + + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + free_binding_aux(&aux); + return false; +} + +static bool +parse_section_text_bindings(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + const size_t key_len = strlen(key); + + uint8_t *data = xmalloc(key_len + 1); + size_t data_len = 0; + bool esc = false; + + for (size_t i = 0; i < key_len; i++) { + if (key[i] == '\\') { + if (i + 1 >= key_len) { + ctx->value = ""; + LOG_CONTEXTUAL_ERR("trailing backslash"); + goto err; + } + + esc = true; + } + + else if (esc) { + if (key[i] != 'x') { + ctx->value = ""; + LOG_CONTEXTUAL_ERR("invalid escaped character: %c", key[i]); + goto err; + } + if (i + 2 >= key_len) { + ctx->value = ""; + LOG_CONTEXTUAL_ERR("\\x sequence too short"); + goto err; + } + + const uint8_t nib1 = hex2nibble(key[i + 1]); + const uint8_t nib2 = hex2nibble(key[i + 2]); + + if (nib1 >= HEX_DIGIT_INVALID || nib2 >= HEX_DIGIT_INVALID) { + ctx->value = ""; + LOG_CONTEXTUAL_ERR("invalid \\x sequence: \\x%c%c", + key[i + 1], key[i + 2]); + goto err; + } + + data[data_len++] = nib1 << 4 | nib2; + esc = false; + i += 2; + } + + else + data[data_len++] = key[i]; + } + + struct binding_aux aux = { + .type = BINDING_AUX_TEXT, + .text = { + .data = data, /* data is now owned by value_to_key_combos() */ + .len = data_len, + }, + }; + + if (!value_to_key_combos(ctx, BIND_ACTION_TEXT_BINDING, &aux, + &conf->bindings.key, KEY_BINDING)) + { + /* Do *not* free(data) - it is handled by value_to_key_combos() */ + return false; + } + + return true; + +err: + free(data); + return false; +} + +static bool +parse_section_environment(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + /* Check for pre-existing env variable */ + tll_foreach(conf->env_vars, it) { + if (streq(it->item.name, key)) + return value_to_str(ctx, &it->item.value); + } + + /* + * No pre-existing variable - allocate a new one + */ + + char *value = NULL; + if (!value_to_str(ctx, &value)) + return false; + + tll_push_back(conf->env_vars, ((struct env_var){xstrdup(key), value})); + return true; +} + +static bool +parse_section_tweak(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "scaling-filter")) { + static const char *filters[] = { + [FCFT_SCALING_FILTER_NONE] = "none", + [FCFT_SCALING_FILTER_NEAREST] = "nearest", + [FCFT_SCALING_FILTER_BILINEAR] = "bilinear", + + [FCFT_SCALING_FILTER_IMPULSE] = "impulse", + [FCFT_SCALING_FILTER_BOX] = "box", + [FCFT_SCALING_FILTER_LINEAR] = "linear", + [FCFT_SCALING_FILTER_CUBIC] = "cubic", + [FCFT_SCALING_FILTER_GAUSSIAN] = "gaussian", + [FCFT_SCALING_FILTER_LANCZOS2] = "lanczos2", + [FCFT_SCALING_FILTER_LANCZOS3] = "lanczos3", + [FCFT_SCALING_FILTER_LANCZOS3_STRETCHED] = "lanczos3-stretched", + NULL, + }; + + _Static_assert(sizeof(conf->tweak.fcft_filter) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum(ctx, filters, (int *)&conf->tweak.fcft_filter); + } + + else if (streq(key, "overflowing-glyphs")) + return value_to_bool(ctx, &conf->tweak.overflowing_glyphs); + + else if (streq(key, "damage-whole-window")) + return value_to_bool(ctx, &conf->tweak.damage_whole_window); + + else if (streq(key, "grapheme-shaping")) { + if (!value_to_bool(ctx, &conf->tweak.grapheme_shaping)) + return false; + +#if !defined(FOOT_GRAPHEME_CLUSTERING) + if (conf->tweak.grapheme_shaping) { + LOG_CONTEXTUAL_WARN( + "foot was not compiled with support for grapheme shaping"); + conf->tweak.grapheme_shaping = false; + } +#endif + + if (conf->tweak.grapheme_shaping && !conf->can_shape_grapheme) { + LOG_WARN( + "fcft was not compiled with support for grapheme shaping"); + + /* Keep it enabled though - this will cause us to do + * grapheme-clustering at least */ + } + + return true; + } + + else if (streq(key, "grapheme-width-method")) { + _Static_assert(sizeof(conf->tweak.grapheme_width_method) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"wcswidth", "double-width", "max", NULL}, + (int *)&conf->tweak.grapheme_width_method); + } + + else if (streq(key, "render-timer")) { + _Static_assert(sizeof(conf->tweak.render_timer) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"none", "osd", "log", "both", NULL}, + (int *)&conf->tweak.render_timer); + } + + else if (streq(key, "delayed-render-lower")) { + uint32_t ns; + if (!value_to_uint32(ctx, 10, &ns)) + return false; + + if (ns > 16666666) { + LOG_CONTEXTUAL_ERR("timeout must not exceed 16ms"); + return false; + } + + conf->tweak.delayed_render_lower_ns = ns; + return true; + } + + else if (streq(key, "delayed-render-upper")) { + uint32_t ns; + if (!value_to_uint32(ctx, 10, &ns)) + return false; + + if (ns > 16666666) { + LOG_CONTEXTUAL_ERR("timeout must not exceed 16ms"); + return false; + } + + conf->tweak.delayed_render_upper_ns = ns; + return true; + } + + else if (streq(key, "max-shm-pool-size-mb")) { + uint32_t mb; + if (!value_to_uint32(ctx, 10, &mb)) + return false; + + conf->tweak.max_shm_pool_size = min((int32_t)mb * 1024 * 1024, INT32_MAX); + return true; + } + + else if (streq(key, "box-drawing-base-thickness")) + return value_to_float(ctx, &conf->tweak.box_drawing_base_thickness); + + else if (streq(key, "box-drawing-solid-shades")) + return value_to_bool(ctx, &conf->tweak.box_drawing_solid_shades); + + else if (streq(key, "font-monospace-warn")) + return value_to_bool(ctx, &conf->tweak.font_monospace_warn); + + else if (streq(key, "sixel")) + return value_to_bool(ctx, &conf->tweak.sixel); + + else if (streq(key, "dim-amount")) + return value_to_float(ctx, &conf->dim.amount); + + else if (streq(key, "bold-text-in-bright-amount")) + return value_to_float(ctx, &conf->bold_in_bright.amount); + + else if (streq(key, "surface-bit-depth")) { + _Static_assert(sizeof(conf->tweak.surface_bit_depth) == sizeof(int), + "enum is not 32-bit"); + +#if defined(HAVE_PIXMAN_RGBA_16) + return value_to_enum( + ctx, + (const char *[]){"auto", "8-bit", "10-bit", "16-bit", NULL}, + (int *)&conf->tweak.surface_bit_depth); +#else + return value_to_enum( + ctx, + (const char *[]){"auto", "8-bit", "10-bit", NULL}, + (int *)&conf->tweak.surface_bit_depth); +#endif + } + + else if (streq(key, "min-stride-alignment")) + return value_to_uint32(ctx, 10, &conf->tweak.min_stride_alignment); + + else if (streq(key, "pre-apply-damage")) + return value_to_bool(ctx, &conf->tweak.preapply_damage); + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_touch(struct context *ctx) { + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "long-press-delay")) + return value_to_uint32(ctx, 10, &conf->touch.long_press_delay); + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_key_value(char *kv, char **section, const char **key, const char **value) +{ + bool section_is_needed = section != NULL; + + /* Strip leading whitespace */ + while (isspace(kv[0])) + ++kv; + + if (section_is_needed) + *section = "main"; + + if (kv[0] == '=') + return false; + + *key = kv; + *value = NULL; + + size_t kvlen = strlen(kv); + + /* Strip trailing whitespace */ + while (isspace(kv[kvlen - 1])) + kvlen--; + kv[kvlen] = '\0'; + + for (size_t i = 0; i < kvlen; ++i) { + if (kv[i] == '.' && section_is_needed) { + section_is_needed = false; + *section = kv; + kv[i] = '\0'; + if (i == kvlen - 1 || kv[i + 1] == '=') { + *key = NULL; + return false; + } + *key = &kv[i + 1]; + } else if (kv[i] == '=') { + kv[i] = '\0'; + if (i != kvlen - 1) + *value = &kv[i + 1]; + break; + } + } + + if (*value == NULL) + return false; + + /* Strip trailing whitespace from key (leading stripped earlier) */ + { + xassert(!isspace(*key[0])); + + char *end = (char *)*key + strlen(*key) - 1; + while (isspace(end[0])) + end--; + end[1] = '\0'; + } + + /* Strip leading whitespace from value (trailing stripped earlier) */ + while (isspace(*value[0])) + ++*value; + + return true; +} + +enum section { + SECTION_MAIN, + SECTION_SECURITY, + SECTION_BELL, + SECTION_DESKTOP_NOTIFICATIONS, + SECTION_SCROLLBACK, + SECTION_URL, + SECTION_REGEX, + SECTION_COLORS_DARK, + SECTION_COLORS_LIGHT, + SECTION_CURSOR, + SECTION_MOUSE, + SECTION_CSD, + SECTION_KEY_BINDINGS, + SECTION_SEARCH_BINDINGS, + SECTION_URL_BINDINGS, + SECTION_MOUSE_BINDINGS, + SECTION_TEXT_BINDINGS, + SECTION_ENVIRONMENT, + SECTION_TWEAK, + SECTION_TOUCH, + SECTION_TABS, + + /* Deprecated */ + SECTION_COLORS, + SECTION_COLORS2, + + SECTION_COUNT, +}; + +/* Function pointer, called for each key/value line */ +typedef bool (*parser_fun_t)(struct context *ctx); + +static const struct { + parser_fun_t fun; + const char *name; + bool allow_colon_suffix; +} section_info[] = { + [SECTION_MAIN] = {&parse_section_main, "main"}, + [SECTION_SECURITY] = {&parse_section_security, "security"}, + [SECTION_BELL] = {&parse_section_bell, "bell"}, + [SECTION_DESKTOP_NOTIFICATIONS] = {&parse_section_desktop_notifications, "desktop-notifications"}, + [SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"}, + [SECTION_URL] = {&parse_section_url, "url"}, + [SECTION_REGEX] = {&parse_section_regex, "regex", true}, + [SECTION_COLORS_DARK] = {&parse_section_colors_dark, "colors-dark"}, + [SECTION_COLORS_LIGHT] = {&parse_section_colors_light, "colors-light"}, + [SECTION_CURSOR] = {&parse_section_cursor, "cursor"}, + [SECTION_MOUSE] = {&parse_section_mouse, "mouse"}, + [SECTION_CSD] = {&parse_section_csd, "csd"}, + [SECTION_KEY_BINDINGS] = {&parse_section_key_bindings, "key-bindings"}, + [SECTION_SEARCH_BINDINGS] = {&parse_section_search_bindings, "search-bindings"}, + [SECTION_URL_BINDINGS] = {&parse_section_url_bindings, "url-bindings"}, + [SECTION_MOUSE_BINDINGS] = {&parse_section_mouse_bindings, "mouse-bindings"}, + [SECTION_TEXT_BINDINGS] = {&parse_section_text_bindings, "text-bindings"}, + [SECTION_ENVIRONMENT] = {&parse_section_environment, "environment"}, + [SECTION_TWEAK] = {&parse_section_tweak, "tweak"}, + [SECTION_TOUCH] = {&parse_section_touch, "touch"}, + [SECTION_TABS] = {&parse_section_tabs, "tabs"}, + + /* Deprecated */ + [SECTION_COLORS] = {&parse_section_colors, "colors"}, + [SECTION_COLORS2] = {&parse_section_colors2, "colors2"}, +}; + +static_assert(ALEN(section_info) == SECTION_COUNT, "section info array size mismatch"); + +static enum section +str_to_section(char *str, char **suffix) +{ + *suffix = NULL; + + for (enum section section = SECTION_MAIN; section < SECTION_COUNT; ++section) { + const char *name = section_info[section].name; + + if (streq(str, name)) + return section; + + else if (section_info[section].allow_colon_suffix) { + const size_t str_len = strlen(str); + const size_t name_len = strlen(name); + + /* At least "section:" chars? */ + if (str_len > name_len + 1) { + if (strncmp(str, name, name_len) == 0 && str[name_len] == ':') { + str[name_len] = '\0'; + *suffix = &str[name_len + 1]; + return section; + } + } + } + } + return SECTION_COUNT; +} + +static bool +parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_are_fatal) +{ + enum section section = SECTION_MAIN; + + char *_line = NULL; + size_t count = 0; + bool ret = true; + +#define error_or_continue() \ + { \ + if (errors_are_fatal) { \ + ret = false; \ + goto done; \ + } else \ + continue; \ + } + + char *section_name = xstrdup("main"); + char *section_suffix = NULL; + + struct context context = { + .conf = conf, + .section = section_name, + .section_suffix = section_suffix, + .path = path, + .lineno = 0, + .errors_are_fatal = errors_are_fatal, + }; + struct context *ctx = &context; /* For LOG_AND_*() */ + + errno = 0; + ssize_t len; + + while ((len = getline(&_line, &count, f)) != -1) { + context.key = NULL; + context.value = NULL; + context.lineno++; + + char *line = _line; + + /* Strip leading whitespace */ + while (isspace(line[0])) { + line++; + len--; + } + + /* Empty line, or comment */ + if (line[0] == '\0' || line[0] == '#') + continue; + + /* Strip the trailing newline - may be absent on the last line */ + if (line[len - 1] == '\n') + line[--len] = '\0'; + + /* Split up into key/value pair + trailing comment separated by blank */ + char *key_value = line; + char *kv_trailing = &line[len - 1]; + char *comment = &line[1]; + while (comment[1] != '\0') { + if (isblank(comment[0]) && comment[1] == '#') { + comment[1] = '\0'; /* Terminate key/value pair */ + kv_trailing = comment++; + break; + } + comment++; + } + comment++; + + /* Strip trailing whitespace */ + while (isspace(kv_trailing[0])) + kv_trailing--; + kv_trailing[1] = '\0'; + + /* Check for new section */ + if (key_value[0] == '[') { + key_value++; + + if (key_value[0] == ']') { + LOG_CONTEXTUAL_ERR("empty section name"); + section = SECTION_COUNT; + error_or_continue(); + } + + char *end = strchr(key_value, ']'); + + if (end == NULL) { + context.section = key_value; + LOG_CONTEXTUAL_ERR("syntax error: no closing ']'"); + context.section = section_name; + section = SECTION_COUNT; + error_or_continue(); + } + + end[0] = '\0'; + + if (end[1] != '\0') { + context.section = key_value; + LOG_CONTEXTUAL_ERR("section declaration contains trailing " + "characters"); + context.section = section_name; + section = SECTION_COUNT; + error_or_continue(); + } + + char *maybe_section_suffix; + section = str_to_section(key_value, &maybe_section_suffix); + if (section == SECTION_COUNT) { + context.section = key_value; + LOG_CONTEXTUAL_ERR("invalid section name: %s", key_value); + context.section = section_name; + error_or_continue(); + } + + free(section_name); + free(section_suffix); + section_name = xstrdup(key_value); + section_suffix = maybe_section_suffix != NULL ? xstrdup(maybe_section_suffix) : NULL; + context.section = section_name; + context.section_suffix = section_suffix; + + /* Process next line */ + continue; + } + + if (section >= SECTION_COUNT) { + /* Last section name was invalid; ignore all keys in it */ + continue; + } + + if (!parse_key_value(key_value, NULL, &context.key, &context.value)) { + LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no %s", + context.key == NULL ? "key" : "value"); + error_or_continue(); + } + + LOG_DBG("section=%s, key='%s', value='%s', comment='%s'", + section_info[section].name, context.key, context.value, comment); + + xassert(section >= 0 && section < SECTION_COUNT); + + parser_fun_t section_parser = section_info[section].fun; + xassert(section_parser != NULL); + + if (!section_parser(ctx)) + error_or_continue(); + + /* For next iteration of getline() */ + errno = 0; + } + + if (errno != 0) { + LOG_AND_NOTIFY_ERRNO("failed to read from configuration"); + if (errors_are_fatal) + ret = false; + } + +done: + free(section_name); + free(section_suffix); + free(_line); + return ret; +} + +static char * +get_server_socket_path(void) +{ + const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); + if (xdg_runtime == NULL) + return xstrdup("/tmp/foot.sock"); + + const char *wayland_display = getenv("WAYLAND_DISPLAY"); + if (wayland_display == NULL) { + return xstrjoin(xdg_runtime, "/foot.sock"); + } + + return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display); +} + +static config_modifier_list_t +m(const char *text) +{ + config_modifier_list_t ret = tll_init(); + parse_modifiers(text, strlen(text), &ret); + return ret; +} + +static void +add_default_key_bindings(struct config *conf) +{ + const struct config_key_binding bindings[] = { + {BIND_ACTION_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Prior}}}, + {BIND_ACTION_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Prior}}}, + {BIND_ACTION_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Next}}}, + {BIND_ACTION_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Next}}}, + {BIND_ACTION_CLIPBOARD_COPY, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_c}}}, + {BIND_ACTION_CLIPBOARD_COPY, m("none"), {{XKB_KEY_XF86Copy}}}, + {BIND_ACTION_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_v}}}, + {BIND_ACTION_CLIPBOARD_PASTE, m("none"), {{XKB_KEY_XF86Paste}}}, + {BIND_ACTION_PRIMARY_PASTE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Insert}}}, + {BIND_ACTION_SEARCH_START, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_r}}}, + {BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_plus}}}, + {BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_equal}}}, + {BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_Add}}}, + {BIND_ACTION_FONT_SIZE_DOWN, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_minus}}}, + {BIND_ACTION_FONT_SIZE_DOWN, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_Subtract}}}, + {BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_0}}}, + {BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_0}}}, + {BIND_ACTION_SPAWN_TERMINAL, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_n}}}, + {BIND_ACTION_TAB_NEW, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_t}}}, + {BIND_ACTION_TAB_CLOSE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_w}}}, + {BIND_ACTION_TAB_NEXT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Tab}}}, + {BIND_ACTION_TAB_PREV, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Tab}}}, + {BIND_ACTION_TAB_OVERVIEW, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_space}}}, + {BIND_ACTION_SHOW_URLS_LAUNCH, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_o}}}, + {BIND_ACTION_UNICODE_INPUT, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_u}}}, + {BIND_ACTION_PROMPT_PREV, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_z}}}, + {BIND_ACTION_PROMPT_NEXT, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_x}}}, + }; + + conf->bindings.key.count = ALEN(bindings); + conf->bindings.key.arr = xmemdup(bindings, sizeof(bindings)); +} + + +static void +add_default_search_bindings(struct config *conf) +{ + const struct config_key_binding bindings[] = { + {BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Prior}}}, + {BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Prior}}}, + {BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Next}}}, + {BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Next}}}, + {BIND_ACTION_SEARCH_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_c}}}, + {BIND_ACTION_SEARCH_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_g}}}, + {BIND_ACTION_SEARCH_CANCEL, m("none"), {{XKB_KEY_Escape}}}, + {BIND_ACTION_SEARCH_COMMIT, m("none"), {{XKB_KEY_Return}}}, + {BIND_ACTION_SEARCH_COMMIT, m("none"), {{XKB_KEY_KP_Enter}}}, + {BIND_ACTION_SEARCH_FIND_PREV, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_r}}}, + {BIND_ACTION_SEARCH_FIND_NEXT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_s}}}, + {BIND_ACTION_SEARCH_EDIT_LEFT, m("none"), {{XKB_KEY_Left}}}, + {BIND_ACTION_SEARCH_EDIT_LEFT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_b}}}, + {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Left}}}, + {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_b}}}, + {BIND_ACTION_SEARCH_EDIT_RIGHT, m("none"), {{XKB_KEY_Right}}}, + {BIND_ACTION_SEARCH_EDIT_RIGHT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_f}}}, + {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Right}}}, + {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_f}}}, + {BIND_ACTION_SEARCH_EDIT_HOME, m("none"), {{XKB_KEY_Home}}}, + {BIND_ACTION_SEARCH_EDIT_HOME, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_a}}}, + {BIND_ACTION_SEARCH_EDIT_END, m("none"), {{XKB_KEY_End}}}, + {BIND_ACTION_SEARCH_EDIT_END, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_e}}}, + {BIND_ACTION_SEARCH_DELETE_PREV, m("none"), {{XKB_KEY_BackSpace}}}, + {BIND_ACTION_SEARCH_DELETE_PREV_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_BackSpace}}}, + {BIND_ACTION_SEARCH_DELETE_PREV_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_BackSpace}}}, + {BIND_ACTION_SEARCH_DELETE_NEXT, m("none"), {{XKB_KEY_Delete}}}, + {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Delete}}}, + {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_d}}}, + {BIND_ACTION_SEARCH_DELETE_TO_START, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_u}}}, + {BIND_ACTION_SEARCH_DELETE_TO_END, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_k}}}, + {BIND_ACTION_SEARCH_EXTEND_CHAR, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Right}}}, + {BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Right}}}, + {BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_w}}}, + {BIND_ACTION_SEARCH_EXTEND_WORD_WS, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_w}}}, + {BIND_ACTION_SEARCH_EXTEND_LINE_DOWN, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Down}}}, + {BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Left}}}, + {BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Left}}}, + {BIND_ACTION_SEARCH_EXTEND_LINE_UP, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Up}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_v}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_v}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_y}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m("none"), {{XKB_KEY_XF86Paste}}}, + {BIND_ACTION_SEARCH_PRIMARY_PASTE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Insert}}}, + {BIND_ACTION_SEARCH_TOGGLE_CASE, m(XKB_MOD_NAME_ALT), {{XKB_KEY_c}}}, + {BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_w}}}, + {BIND_ACTION_SEARCH_TOGGLE_REGEX, m(XKB_MOD_NAME_ALT), {{XKB_KEY_r}}}, + {BIND_ACTION_SEARCH_HISTORY_PREV, m("none"), {{XKB_KEY_Up}}}, + {BIND_ACTION_SEARCH_HISTORY_NEXT, m("none"), {{XKB_KEY_Down}}}, + {BIND_ACTION_SEARCH_COMMIT_LINE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Return}}}, + }; + + conf->bindings.search.count = ALEN(bindings); + conf->bindings.search.arr = xmemdup(bindings, sizeof(bindings)); +} + +static void +add_default_url_bindings(struct config *conf) +{ + const struct config_key_binding bindings[] = { + {BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_c}}}, + {BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_g}}}, + {BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_d}}}, + {BIND_ACTION_URL_CANCEL, m("none"), {{XKB_KEY_Escape}}}, + {BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, m("none"), {{XKB_KEY_t}}}, + }; + + conf->bindings.url.count = ALEN(bindings); + conf->bindings.url.arr = xmemdup(bindings, sizeof(bindings)); +} + +static void +add_default_mouse_bindings(struct config *conf) +{ + const struct config_key_binding bindings[] = { + {BIND_ACTION_SCROLLBACK_UP_MOUSE, m("none"), {.m = {BTN_WHEEL_BACK, 1}}}, + {BIND_ACTION_SCROLLBACK_DOWN_MOUSE, m("none"), {.m = {BTN_WHEEL_FORWARD, 1}}}, + {BIND_ACTION_PRIMARY_PASTE, m("none"), {.m = {BTN_MIDDLE, 1}}}, + {BIND_ACTION_SELECT_BEGIN, m("none"), {.m = {BTN_LEFT, 1}}}, + {BIND_ACTION_SELECT_BEGIN_BLOCK, m(XKB_MOD_NAME_CTRL), {.m = {BTN_LEFT, 1}}}, + {BIND_ACTION_SELECT_EXTEND, m("none"), {.m = {BTN_RIGHT, 1}}}, + {BIND_ACTION_SELECT_EXTEND_CHAR_WISE, m(XKB_MOD_NAME_CTRL), {.m = {BTN_RIGHT, 1}}}, + {BIND_ACTION_SELECT_WORD, m("none"), {.m = {BTN_LEFT, 2}}}, + {BIND_ACTION_SELECT_WORD_WS, m(XKB_MOD_NAME_CTRL), {.m = {BTN_LEFT, 2}}}, + {BIND_ACTION_SELECT_QUOTE, m("none"), {.m = {BTN_LEFT, 3}}}, + {BIND_ACTION_SELECT_ROW, m("none"), {.m = {BTN_LEFT, 4}}}, + {BIND_ACTION_FONT_SIZE_UP, m("Control"), {.m = {BTN_WHEEL_BACK, 1}}}, + {BIND_ACTION_FONT_SIZE_DOWN, m("Control"), {.m = {BTN_WHEEL_FORWARD, 1}}}, + }; + + conf->bindings.mouse.count = ALEN(bindings); + conf->bindings.mouse.arr = xmemdup(bindings, sizeof(bindings)); +} + +static void NOINLINE +config_font_list_clone(struct config_font_list *dst, + const struct config_font_list *src) +{ + dst->count = src->count; + dst->arr = xmalloc(dst->count * sizeof(dst->arr[0])); + + for (size_t j = 0; j < dst->count; j++) { + dst->arr[j].pt_size = src->arr[j].pt_size; + dst->arr[j].px_size = src->arr[j].px_size; + dst->arr[j].pattern = xstrdup(src->arr[j].pattern); + } +} + +bool +config_load(struct config *conf, const char *conf_path, + user_notifications_t *initial_user_notifications, + config_override_t *overrides, bool errors_are_fatal, + bool as_server) +{ + bool ret = true; + enum fcft_capabilities fcft_caps = fcft_capabilities(); + + *conf = (struct config) { + .conf_path = (conf_path ? xstrdup(conf_path) : NULL), + .term = xstrdup(FOOT_DEFAULT_TERM), + .shell = get_shell(), + .title = xstrdup("foot"), + .app_id = (as_server ? xstrdup("footclient") : xstrdup("foot")), + .toplevel_tag = xstrdup(""), + .word_delimiters = xc32dup(U",│`|:\"'()[]{}<>"), + .size = { + .type = CONF_SIZE_PX, + .width = 700, + .height = 500, + }, + .pad_left = 0, + .pad_top = 0, + .pad_right = 0, + .pad_bottom = 0, + .center_when = CENTER_MAXIMIZED_AND_FULLSCREEN, + .resize_by_cells = true, + .resize_keep_grid = true, + .resize_delay_ms = 100, + .dim = { .amount = 1.5 }, + .bold_in_bright = { + .enabled = false, + .palette_based = false, + .amount = 1.3, + }, + .startup_mode = STARTUP_WINDOWED, + .fonts = {{0}}, + .font_size_adjustment = {.percent = 0., .pt_or_px = {.pt = 0.5, .px = 0}}, + .line_height = {.pt = 0, .px = -1}, + .letter_spacing = {.pt = 0, .px = 0}, + .horizontal_letter_offset = {.pt = 0, .px = 0}, + .vertical_letter_offset = {.pt = 0, .px = 0}, + .use_custom_underline_offset = false, + .box_drawings_uses_font_glyphs = false, + .underline_thickness = {.pt = 0., .px = -1}, + .strikeout_thickness = {.pt = 0., .px = -1}, + .dpi_aware = false, + .gamma_correct = false, + .uppercase_regex_insert = true, + .security = { + .osc52 = OSC52_ENABLED, + }, + .bell = { + .urgent = false, + .notify = false, + .flash = false, + .system_bell = true, + .command = { + .argv = {.args = NULL}, + }, + .command_focused = false, + }, + .url = { + .label_letters = xc32dup(U"sadfjklewcmpgh"), + .osc8_underline = OSC8_UNDERLINE_URL_MODE, + .style = UNDERLINE_DOTTED, + }, + .custom_regexes = tll_init(), + .can_shape_grapheme = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING, + .scrollback = { + .lines = 1000, + .indicator = { + .position = SCROLLBACK_INDICATOR_POSITION_RELATIVE, + .format = SCROLLBACK_INDICATOR_FORMAT_TEXT, + .text = xc32dup(U""), + }, + .multiplier = 3., + }, + .colors_dark = { + .fg = default_foreground, + .bg = default_background, + .flash = 0x7f7f00, + .flash_alpha = 0x7fff, + .alpha = 0xffff, + .alpha_mode = ALPHA_MODE_DEFAULT, + .dim_blend_towards = DIM_BLEND_TOWARDS_BLACK, + .selection_fg = 0x80000000, /* Use default bg */ + .selection_bg = 0x80000000, /* Use default fg */ + .cursor = { + .text = 0, + .cursor = 0, + }, + .use_custom = { + .jump_label = false, + .scrollback_indicator = false, + .url = false, + }, + .blur = false, + }, + .initial_color_theme = COLOR_THEME_DARK, + .cursor = { + .style = CURSOR_BLOCK, + .unfocused_style = CURSOR_UNFOCUSED_HOLLOW, + .blink = { + .enabled = false, + .rate_ms = 500, + }, + .beam_thickness = {.pt = 1.5}, + .underline_thickness = {.pt = 0., .px = -1}, + }, + .mouse = { + .hide_when_typing = false, + .alternate_scroll_mode = true, + .selection_override_modifiers = tll_init(), + }, + .csd = { + .preferred = CONF_CSD_PREFER_SERVER, + .font = {0}, + .hide_when_maximized = false, + .double_click_to_maximize = true, + .title_height = 26, + .border_width = 5, + .border_width_visible = 0, + .button_width = 26, + }, + + .render_worker_count = sysconf(_SC_NPROCESSORS_ONLN), + .server_socket_path = get_server_socket_path(), + .presentation_timings = false, + .selection_target = SELECTION_TARGET_PRIMARY, + .hold_at_exit = false, + .desktop_notifications = { + .command = { + .argv = {.args = NULL}, + }, + .command_action_arg = { + .argv = {.args = NULL}, + }, + .close = { + .argv = {.args = NULL}, + }, + .inhibit_when_focused = true, + }, + + .tweak = { + .fcft_filter = FCFT_SCALING_FILTER_LANCZOS3, + .overflowing_glyphs = true, +#if defined(FOOT_GRAPHEME_CLUSTERING) && FOOT_GRAPHEME_CLUSTERING + .grapheme_shaping = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING, +#endif + .grapheme_width_method = GRAPHEME_WIDTH_DOUBLE, + .delayed_render_lower_ns = 500000, /* 0.5ms */ + .delayed_render_upper_ns = 16666666 / 2, /* half a frame period (60Hz) */ + .max_shm_pool_size = 512 * 1024 * 1024, + .render_timer = RENDER_TIMER_NONE, + .damage_whole_window = false, + .box_drawing_base_thickness = 0.04, + .box_drawing_solid_shades = true, + .font_monospace_warn = true, + .sixel = true, + .surface_bit_depth = SHM_BITS_AUTO, + .min_stride_alignment = 256, + .preapply_damage = true, + }, + + .touch = { + .long_press_delay = 400, + }, + + .tabs = { + .enabled = false, + .inherit_cwd = false, + .position = CONF_TABS_POSITION_TOP, + .style = CONF_TABS_STYLE_ROUNDED, + .layout = CONF_TABS_LAYOUT_SPAN, + .height = 26, + .tab_width = 200, + .tab_padding = 8, + .label_padding = 8, + .margin = 4, + .corner_radius = 6, + .unread_indicator = NULL, /* set below to a strdup'd default */ + .colors = { + .bg = 0x1c1c1c, + .fg = 0xb0b0b0, + .active_bg = 0x3a3a3a, + .active_fg = 0xffffff, + .unread_fg = 0xfabd2f, /* warm yellow */ + }, + }, + + .env_vars = tll_init(), +#if defined(UTMP_DEFAULT_HELPER_PATH) + .utmp_helper_path = ((strlen(UTMP_DEFAULT_HELPER_PATH) > 0 && + access(UTMP_DEFAULT_HELPER_PATH, X_OK) == 0) + ? xstrdup(UTMP_DEFAULT_HELPER_PATH) + : NULL), +#endif + .notifications = tll_init(), + }; + + conf->tabs.unread_indicator = xstrdup("●"); + + memcpy(conf->colors_dark.table, default_color_table, sizeof(default_color_table)); + memcpy(conf->colors_dark.sixel, default_sixel_colors, sizeof(default_sixel_colors)); + memcpy(&conf->colors_light, &conf->colors_dark, sizeof(conf->colors_dark)); + conf->colors_light.dim_blend_towards = DIM_BLEND_TOWARDS_WHITE; + + parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); + + tokenize_cmdline( + "notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --hint BOOLEAN:suppress-sound:${muted} --hint STRING:sound-name:${sound-name} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body}", + &conf->desktop_notifications.command.argv.args); + tokenize_cmdline("--action ${action-name}=${action-label}", &conf->desktop_notifications.command_action_arg.argv.args); + tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); + + { + const char *url_regex_string = + "(" + "(" + "(https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)" + "|" + "www\\." + ")" + "(" + /* Safe + reserved + some unsafe characters parenthesis and double quotes omitted (we only allow them when balanced) */ + "[0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]+" + "|" + /* Balanced "(...)". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\([]\\[\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\)" + "|" + /* Balanced "[...]". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\[[\\(\\)\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\]" + "|" + /* Balanced '"..."'. Content is same as above, plus all _other_ characters we require to be balanced */ + "\"[]\\[\\(\\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\"" + "|" + /* Balanced "'...'". Content is same as above, plus all _other_ characters we require to be balanced */ + "'[]\\[\\(\\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]*'" + ")+" + "(" + /* Same as above, except :?!,;. are excluded */ + "[0-9a-zA-Z/#@$&*+=~_%^\\-]" + "|" + /* Balanced "(...)". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\([]\\[\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\)" + "|" + /* Balanced "[...]". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\[[\\(\\)\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\]" + "|" + /* Balanced '"..."'. Content is same as above, plus all _other_ characters we require to be balanced */ + "\"[]\\[\\(\\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\"" + "|" + /* Balanced "'...'". Content is same as above, plus all _other_ characters we require to be balanced */ + "'[]\\[\\(\\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]*'" + ")" + ")"; + + int r = regcomp(&conf->url.preg, url_regex_string, REG_EXTENDED); + xassert(r == 0); + conf->url.regex = xstrdup(url_regex_string); + xassert(conf->url.preg.re_nsub >= 1); + } + + tll_foreach(*initial_user_notifications, it) { + tll_push_back(conf->notifications, it->item); + tll_remove(*initial_user_notifications, it); + } + + add_default_key_bindings(conf); + add_default_search_bindings(conf); + add_default_url_bindings(conf); + add_default_mouse_bindings(conf); + + struct config_file conf_file = {.path = NULL, .fd = -1}; + if (conf_path != NULL) { + int fd = open(conf_path, O_RDONLY); + if (fd < 0) { + LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_path); + ret = !errors_are_fatal; + } else { + conf_file.path = xstrdup(conf_path); + conf_file.fd = fd; + } + } else { + conf_file = open_config(); + if (conf_file.fd < 0) { + LOG_WARN("no configuration found, using defaults"); + ret = !errors_are_fatal; + } + } + + if (conf_file.path && conf_file.fd >= 0) { + LOG_INFO("loading configuration from %s", conf_file.path); + + FILE *f = fdopen(conf_file.fd, "r"); + if (f == NULL) { + LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_file.path); + ret = !errors_are_fatal; + } else { + if (!parse_config_file(f, conf, conf_file.path, errors_are_fatal)) + ret = !errors_are_fatal; + + fclose(f); + conf_file.fd = -1; + } + } + + if (!config_override_apply(conf, overrides, errors_are_fatal)) + ret = !errors_are_fatal; + + if (ret && conf->fonts[0].count == 0) { + struct config_font font; + if (!config_font_parse("monospace", &font)) { + LOG_ERR("failed to load font 'monospace' - no fonts installed?"); + ret = false; + } else { + conf->fonts[0].count = 1; + conf->fonts[0].arr = xmalloc(sizeof(font)); + conf->fonts[0].arr[0] = font; + } + } + + if (ret && conf->csd.font.count == 0) + config_font_list_clone(&conf->csd.font, &conf->fonts[0]); + +#if defined(_DEBUG) + for (size_t i = 0; i < conf->bindings.key.count; i++) + xassert(conf->bindings.key.arr[i].action != BIND_ACTION_NONE); + for (size_t i = 0; i < conf->bindings.search.count; i++) + xassert(conf->bindings.search.arr[i].action != BIND_ACTION_SEARCH_NONE); + for (size_t i = 0; i < conf->bindings.url.count; i++) + xassert(conf->bindings.url.arr[i].action != BIND_ACTION_URL_NONE); +#endif + + free(conf_file.path); + if (conf_file.fd >= 0) + close(conf_file.fd); + + return ret; +} + +bool +config_override_apply(struct config *conf, config_override_t *overrides, + bool errors_are_fatal) +{ + char *section_name = NULL; + + struct context context = { + .conf = conf, + .path = "override", + .lineno = 0, + .errors_are_fatal = errors_are_fatal, + }; + struct context *ctx = &context; + + tll_foreach(*overrides, it) { + context.lineno++; + + if (!parse_key_value(it->item, §ion_name, &context.key, &context.value)) + { + LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no %s", + context.key == NULL ? "key" : "value"); + if (errors_are_fatal) + return false; + continue; + } + + if (section_name[0] == '\0') { + LOG_CONTEXTUAL_ERR("empty section name"); + if (errors_are_fatal) + return false; + continue; + } + + char *maybe_section_suffix = NULL; + enum section section = str_to_section(section_name, &maybe_section_suffix); + + context.section = section_name; + context.section_suffix = maybe_section_suffix; + + if (section == SECTION_COUNT) { + LOG_CONTEXTUAL_ERR("invalid section name: %s", section_name); + if (errors_are_fatal) + return false; + continue; + } + + parser_fun_t section_parser = section_info[section].fun; + xassert(section_parser != NULL); + + if (!section_parser(ctx)) { + if (errors_are_fatal) + return false; + continue; + } + } + + conf->csd.border_width = max( + min_csd_border_width, conf->csd.border_width_visible); + + return + resolve_key_binding_collisions( + conf, section_info[SECTION_KEY_BINDINGS].name, + binding_action_map, &conf->bindings.key, KEY_BINDING) && + resolve_key_binding_collisions( + conf, section_info[SECTION_SEARCH_BINDINGS].name, + search_binding_action_map, &conf->bindings.search, KEY_BINDING) && + resolve_key_binding_collisions( + conf, section_info[SECTION_URL_BINDINGS].name, + url_binding_action_map, &conf->bindings.url, KEY_BINDING) && + resolve_key_binding_collisions( + conf, section_info[SECTION_MOUSE_BINDINGS].name, + binding_action_map, &conf->bindings.mouse, MOUSE_BINDING); +} + +static void NOINLINE +key_binding_list_clone(struct config_key_binding_list *dst, + const struct config_key_binding_list *src) +{ + struct argv *last_master_argv = NULL; + uint8_t *last_master_text_data = NULL; + size_t last_master_text_len = 0; + char *last_master_regex_name = NULL; + + dst->count = src->count; + dst->arr = xmalloc(src->count * sizeof(dst->arr[0])); + + for (size_t i = 0; i < src->count; i++) { + const struct config_key_binding *old = &src->arr[i]; + struct config_key_binding *new = &dst->arr[i]; + + *new = *old; + memset(&new->modifiers, 0, sizeof(new->modifiers)); + tll_foreach(old->modifiers, it) + tll_push_back(new->modifiers, xstrdup(it->item)); + + switch (old->aux.type) { + case BINDING_AUX_NONE: + last_master_argv = NULL; + last_master_text_data = NULL; + last_master_text_len = 0; + break; + + case BINDING_AUX_PIPE: + if (old->aux.master_copy) { + clone_argv(&new->aux.pipe, &old->aux.pipe); + last_master_argv = &new->aux.pipe; + } else { + xassert(last_master_argv != NULL); + new->aux.pipe = *last_master_argv; + } + last_master_text_data = NULL; + last_master_text_len = 0; + break; + + case BINDING_AUX_TEXT: + if (old->aux.master_copy) { + const size_t len = old->aux.text.len; + new->aux.text.len = len; + new->aux.text.data = xmemdup(old->aux.text.data, len); + + last_master_text_len = len; + last_master_text_data = new->aux.text.data; + } else { + xassert(last_master_text_data != NULL); + new->aux.text.len = last_master_text_len; + new->aux.text.data = last_master_text_data; + } + last_master_argv = NULL; + break; + + case BINDING_AUX_REGEX: + if (old->aux.master_copy) { + new->aux.regex_name = xstrdup(old->aux.regex_name); + last_master_regex_name = new->aux.regex_name; + } else { + xassert(last_master_regex_name != NULL); + new->aux.regex_name = last_master_regex_name; + } + break; + } + } +} + +struct config * +config_clone(const struct config *old) +{ + struct config *conf = xmalloc(sizeof(*conf)); + *conf = *old; + + conf->conf_path = (old->conf_path ? xstrdup(old->conf_path) : NULL); + conf->term = xstrdup(old->term); + conf->shell = xstrdup(old->shell); + conf->title = xstrdup(old->title); + conf->app_id = xstrdup(old->app_id); + conf->toplevel_tag = xstrdup(old->toplevel_tag); + conf->word_delimiters = xc32dup(old->word_delimiters); + conf->scrollback.indicator.text = xc32dup(old->scrollback.indicator.text); + conf->server_socket_path = xstrdup(old->server_socket_path); + spawn_template_clone(&conf->bell.command, &old->bell.command); + spawn_template_clone(&conf->desktop_notifications.command, + &old->desktop_notifications.command); + spawn_template_clone(&conf->desktop_notifications.command_action_arg, + &old->desktop_notifications.command_action_arg); + spawn_template_clone(&conf->desktop_notifications.close, + &old->desktop_notifications.close); + + for (size_t i = 0; i < ALEN(conf->fonts); i++) + config_font_list_clone(&conf->fonts[i], &old->fonts[i]); + config_font_list_clone(&conf->csd.font, &old->csd.font); + + conf->url.label_letters = xc32dup(old->url.label_letters); + spawn_template_clone(&conf->url.launch, &old->url.launch); + conf->url.regex = xstrdup(old->url.regex); + regcomp(&conf->url.preg, conf->url.regex, REG_EXTENDED); + + memset(&conf->custom_regexes, 0, sizeof(conf->custom_regexes)); + tll_foreach(old->custom_regexes, it) { + const struct custom_regex *old_regex = &it->item; + + tll_push_back(conf->custom_regexes, + ((struct custom_regex){.name = xstrdup(old_regex->name), + .regex = xstrdup(old_regex->regex)})); + + + struct custom_regex *new_regex = &tll_back(conf->custom_regexes); + regcomp(&new_regex->preg, new_regex->regex, REG_EXTENDED); + spawn_template_clone(&new_regex->launch, &old_regex->launch); + } + + key_binding_list_clone(&conf->bindings.key, &old->bindings.key); + key_binding_list_clone(&conf->bindings.search, &old->bindings.search); + key_binding_list_clone(&conf->bindings.url, &old->bindings.url); + key_binding_list_clone(&conf->bindings.mouse, &old->bindings.mouse); + + conf->env_vars.length = 0; + conf->env_vars.head = conf->env_vars.tail = NULL; + + memset(&conf->mouse.selection_override_modifiers, 0, sizeof(conf->mouse.selection_override_modifiers)); + tll_foreach(old->mouse.selection_override_modifiers, it) + tll_push_back(conf->mouse.selection_override_modifiers, xstrdup(it->item)); + + tll_foreach(old->env_vars, it) { + struct env_var copy = { + .name = xstrdup(it->item.name), + .value = xstrdup(it->item.value), + }; + tll_push_back(conf->env_vars, copy); + } + + conf->utmp_helper_path = + old->utmp_helper_path != NULL ? xstrdup(old->utmp_helper_path) : NULL; + + conf->notifications.length = 0; + conf->notifications.head = conf->notifications.tail = 0; + tll_foreach(old->notifications, it) { + char *text = xstrdup(it->item.text); + user_notification_add(&conf->notifications, it->item.kind, text); + } + + return conf; +} + +UNITTEST +{ + struct config original; + user_notifications_t nots = tll_init(); + config_override_t overrides = tll_init(); + + fcft_init(FCFT_LOG_COLORIZE_NEVER, false, FCFT_LOG_CLASS_NONE); + + bool ret = config_load(&original, "/dev/null", ¬s, &overrides, false, false); + xassert(ret); + + //struct config *clone = config_clone(&original); + //xassert(clone != NULL); + //xassert(clone != &original); + + config_free(&original); + //config_free(clone); + //free(clone); + + fcft_fini(); + + tll_free(overrides); + tll_free(nots); +} + +void +config_free(struct config *conf) +{ + free(conf->conf_path); + free(conf->term); + free(conf->shell); + free(conf->title); + free(conf->app_id); + free(conf->toplevel_tag); + free(conf->word_delimiters); + spawn_template_free(&conf->bell.command); + free(conf->scrollback.indicator.text); + spawn_template_free(&conf->desktop_notifications.command); + spawn_template_free(&conf->desktop_notifications.command_action_arg); + spawn_template_free(&conf->desktop_notifications.close); + for (size_t i = 0; i < ALEN(conf->fonts); i++) + config_font_list_destroy(&conf->fonts[i]); + free(conf->server_socket_path); + + config_font_list_destroy(&conf->csd.font); + + free(conf->url.label_letters); + spawn_template_free(&conf->url.launch); + regfree(&conf->url.preg); + free(conf->url.regex); + + tll_foreach(conf->custom_regexes, it) { + struct custom_regex *regex = &it->item; + free(regex->name); + free(regex->regex); + regfree(®ex->preg); + spawn_template_free(®ex->launch); + tll_remove(conf->custom_regexes, it); + } + + free_key_binding_list(&conf->bindings.key); + free_key_binding_list(&conf->bindings.search); + free_key_binding_list(&conf->bindings.url); + free_key_binding_list(&conf->bindings.mouse); + tll_free_and_free(conf->mouse.selection_override_modifiers, free); + + tll_foreach(conf->env_vars, it) { + free(it->item.name); + free(it->item.value); + tll_remove(conf->env_vars, it); + } + + free(conf->utmp_helper_path); + free(conf->tabs.unread_indicator); + user_notifications_free(&conf->notifications); +} + +bool +config_font_parse(const char *pattern, struct config_font *font) +{ + FcPattern *pat = FcNameParse((const FcChar8 *)pattern); + if (pat == NULL) + return false; + + /* + * First look for user specified {pixel}size option + * e.g. "font-name:size=12" + */ + + double pt_size = -1.0; + FcResult have_pt_size = FcPatternGetDouble(pat, FC_SIZE, 0, &pt_size); + + int px_size = -1; + FcResult have_px_size = FcPatternGetInteger(pat, FC_PIXEL_SIZE, 0, &px_size); + + if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) { + /* + * Apply fontconfig config. Can't do that until we've first + * checked for a user provided size, since we may end up with + * both "size" and "pixelsize" being set, and we don't know + * which one takes priority. + */ + FcConfig *fc_conf = FcConfigCreate(); + FcPattern *pat_copy = FcPatternDuplicate(pat); + if (pat_copy == NULL || + !FcConfigSubstitute(fc_conf, pat_copy, FcMatchPattern)) + { + LOG_WARN("%s: failed to do config substitution", pattern); + } else { + have_pt_size = FcPatternGetDouble(pat_copy, FC_SIZE, 0, &pt_size); + have_px_size = FcPatternGetInteger(pat_copy, FC_PIXEL_SIZE, 0, &px_size); + } + + FcPatternDestroy(pat_copy); + FcConfigDestroy(fc_conf); + + if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) + pt_size = 8.0; + } + + FcPatternRemove(pat, FC_SIZE, 0); + FcPatternRemove(pat, FC_PIXEL_SIZE, 0); + + char *stripped_pattern = (char *)FcNameUnparse(pat); + FcPatternDestroy(pat); + + LOG_DBG("%s: pt-size=%.2f, px-size=%d", stripped_pattern, pt_size, px_size); + + if (stripped_pattern == NULL) { + LOG_ERR("failed to convert font pattern to string"); + return false; + } + + *font = (struct config_font){ + .pattern = stripped_pattern, + .pt_size = pt_size, + .px_size = px_size + }; + return true; +} + +void +config_font_list_destroy(struct config_font_list *font_list) +{ + for (size_t i = 0; i < font_list->count; i++) + free(font_list->arr[i].pattern); + free(font_list->arr); + font_list->count = 0; + font_list->arr = NULL; +} + + +bool +check_if_font_is_monospaced(const char *pattern, + user_notifications_t *notifications) +{ + struct fcft_font *f = fcft_from_name( + 1, (const char *[]){pattern}, ":size=8"); + + if (f == NULL) + return true; + + static const char32_t chars[] = {U'a', U'i', U'l', U'M', U'W'}; + + bool is_monospaced = true; + int last_width = -1; + + for (size_t i = 0; i < sizeof(chars) / sizeof(chars[0]); i++) { + const struct fcft_glyph *g = fcft_rasterize_char_utf32( + f, chars[i], FCFT_SUBPIXEL_NONE); + + if (g == NULL) + continue; + + if (last_width >= 0 && g->advance.x != last_width) { + const char *font_name = f->name != NULL + ? f->name + : pattern; + + LOG_WARN("%s: font does not appear to be monospace; " + "check your config, or disable this warning by " + "setting [tweak].font-monospace-warn=no", + font_name); + + static const char fmt[] = + "%s: font does not appear to be monospace; " + "check your config, or disable this warning by " + "setting \033[1m[tweak].font-monospace-warn=no\033[22m"; + + user_notification_add_fmt( + notifications, USER_NOTIFICATION_WARNING, fmt, font_name); + + is_monospaced = false; + break; + } + + last_width = g->advance.x; + } + + fcft_destroy(f); + return is_monospaced; +} + +#if 0 +xkb_mod_mask_t +conf_modifiers_to_mask(const struct seat *seat, + const struct config_key_modifiers *modifiers) +{ + xkb_mod_mask_t mods = 0; + if (seat->kbd.mod_shift != XKB_MOD_INVALID) + mods |= modifiers->shift << seat->kbd.mod_shift; + if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) + mods |= modifiers->ctrl << seat->kbd.mod_ctrl; + if (seat->kbd.mod_alt != XKB_MOD_INVALID) + mods |= modifiers->alt << seat->kbd.mod_alt; + if (seat->kbd.mod_super != XKB_MOD_INVALID) + mods |= modifiers->super << seat->kbd.mod_super; + return mods; +} +#endif diff --git a/config.h b/config.h new file mode 100644 index 0000000..33ea725 --- /dev/null +++ b/config.h @@ -0,0 +1,523 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#include "user-notification.h" + +#define DEFINE_LIST(type) \ + type##_list { \ + size_t count; \ + type *arr; \ + } + +/* If px != 0 then px is valid, otherwise pt is valid */ +struct pt_or_px { + int16_t px; + float pt; +}; + +struct font_size_adjustment { + struct pt_or_px pt_or_px; + float percent; +}; + +enum cursor_style { CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BEAM, CURSOR_HOLLOW }; +enum cursor_unfocused_style { + CURSOR_UNFOCUSED_UNCHANGED, + CURSOR_UNFOCUSED_HOLLOW, + CURSOR_UNFOCUSED_NONE +}; + +enum conf_size_type {CONF_SIZE_PX, CONF_SIZE_CELLS}; + +struct config_font { + char *pattern; + float pt_size; + int px_size; +}; +DEFINE_LIST(struct config_font); + +#if 0 +struct config_key_modifiers { + bool shift; + bool alt; + bool ctrl; + bool super; +}; +#endif + +struct argv { + char **args; +}; + +enum binding_aux_type { + BINDING_AUX_NONE, + BINDING_AUX_PIPE, + BINDING_AUX_TEXT, + BINDING_AUX_REGEX, +}; + +struct binding_aux { + enum binding_aux_type type; + bool master_copy; + + union { + struct argv pipe; + + struct { + uint8_t *data; + size_t len; + } text; + + char *regex_name; + }; +}; + +enum key_binding_type { + KEY_BINDING, + MOUSE_BINDING, +}; + +typedef tll(char *) config_modifier_list_t; + +struct config_key_binding { + int action; /* One of the various bind_action_* enums from wayland.h */ + //struct config_key_modifiers modifiers; + config_modifier_list_t modifiers; + union { + /* Key bindings */ + struct { + xkb_keysym_t sym; + } k; + + /* Mouse bindings */ + struct { + int button; + int count; + } m; + }; + + struct binding_aux aux; + + /* For error messages in collision handling */ + const char *path; + int lineno; +}; +DEFINE_LIST(struct config_key_binding); + +typedef tll(char *) config_override_t; + +struct config_spawn_template { + struct argv argv; +}; + +struct env_var { + char *name; + char *value; +}; +typedef tll(struct env_var) env_var_list_t; + +struct custom_regex { + char *name; + char *regex; + regex_t preg; + struct config_spawn_template launch; +}; + +struct color_theme { + uint32_t fg; + uint32_t bg; + uint32_t flash; + uint32_t flash_alpha; + uint32_t table[256]; + uint16_t alpha; + uint32_t selection_fg; + uint32_t selection_bg; + uint32_t url; + + uint32_t dim[8]; + uint32_t sixel[16]; + + enum { + DIM_BLEND_TOWARDS_BLACK, + DIM_BLEND_TOWARDS_WHITE, + } dim_blend_towards; + + enum { + ALPHA_MODE_DEFAULT, + ALPHA_MODE_MATCHING, + ALPHA_MODE_ALL + } alpha_mode; + + struct { + uint32_t text; + uint32_t cursor; + } cursor; + + struct { + uint32_t fg; + uint32_t bg; + } jump_label; + + struct { + uint32_t fg; + uint32_t bg; + } scrollback_indicator; + + struct { + struct { + uint32_t fg; + uint32_t bg; + } no_match; + + struct { + uint32_t fg; + uint32_t bg; + } match; + } search_box; + + struct { + bool cursor:1; + bool jump_label:1; + bool scrollback_indicator:1; + bool url:1; + bool search_box_no_match:1; + bool search_box_match:1; + uint8_t dim; + } use_custom; + + bool blur; +}; + +enum which_color_theme { + COLOR_THEME_DARK, + COLOR_THEME_LIGHT, + COLOR_THEME_1, /* Deprecated */ + COLOR_THEME_2, /* Deprecated */ +}; + +enum shm_bit_depth { + SHM_BITS_AUTO, + SHM_BITS_8, + SHM_BITS_10, + SHM_BITS_16, +}; + +enum center_when { + CENTER_INVALID, + CENTER_NEVER, + CENTER_FULLSCREEN, + CENTER_MAXIMIZED_AND_FULLSCREEN, + CENTER_ALWAYS, +}; + +enum underline_style { + UNDERLINE_NONE, + UNDERLINE_SINGLE, /* Legacy underline */ + UNDERLINE_DOUBLE, + UNDERLINE_CURLY, + UNDERLINE_DOTTED, + UNDERLINE_DASHED, +}; + +struct config { + char *conf_path; + char *term; + char *shell; + char *title; + char *app_id; + char *toplevel_tag; + char32_t *word_delimiters; + bool login_shell; + bool locked_title; + + struct { + enum conf_size_type type; + uint32_t width; + uint32_t height; + } size; + + unsigned pad_left; + unsigned pad_top; + unsigned pad_right; + unsigned pad_bottom; + enum center_when center_when; + + bool resize_by_cells; + bool resize_keep_grid; + + uint16_t resize_delay_ms; + + struct { + float amount; + } dim; + + struct { + bool enabled; + bool palette_based; + float amount; + } bold_in_bright; + + enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode; + + bool dpi_aware; + bool gamma_correct; + bool uppercase_regex_insert; + struct config_font_list fonts[4]; + struct font_size_adjustment font_size_adjustment; + + /* Custom font metrics (-1 = use real font metrics) */ + struct pt_or_px line_height; + struct pt_or_px letter_spacing; + + /* Adjusted letter x/y offsets */ + struct pt_or_px horizontal_letter_offset; + struct pt_or_px vertical_letter_offset; + + bool use_custom_underline_offset; + struct pt_or_px underline_offset; + struct pt_or_px underline_thickness; + + struct pt_or_px strikeout_thickness; + + bool box_drawings_uses_font_glyphs; + bool can_shape_grapheme; + + struct { + enum { + OSC52_DISABLED, + OSC52_COPY_ENABLED, + OSC52_PASTE_ENABLED, + OSC52_ENABLED, + } osc52; + } security; + + struct { + bool urgent; + bool notify; + bool flash; + bool system_bell; + struct config_spawn_template command; + bool command_focused; + } bell; + + struct { + uint32_t lines; + + struct { + enum { + SCROLLBACK_INDICATOR_POSITION_NONE, + SCROLLBACK_INDICATOR_POSITION_FIXED, + SCROLLBACK_INDICATOR_POSITION_RELATIVE + } position; + + enum { + SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE, + SCROLLBACK_INDICATOR_FORMAT_LINENO, + SCROLLBACK_INDICATOR_FORMAT_TEXT, + } format; + + char32_t *text; + } indicator; + float multiplier; + } scrollback; + + struct { + char32_t *label_letters; + struct config_spawn_template launch; + enum { + OSC8_UNDERLINE_URL_MODE, + OSC8_UNDERLINE_ALWAYS, + } osc8_underline; + enum underline_style style; + + char *regex; + regex_t preg; + } url; + + tll(struct custom_regex) custom_regexes; + + struct color_theme colors_dark; + struct color_theme colors_light; + enum which_color_theme initial_color_theme; + + struct { + enum cursor_style style; + enum cursor_unfocused_style unfocused_style; + struct { + bool enabled; + uint32_t rate_ms; + } blink; + struct pt_or_px beam_thickness; + struct pt_or_px underline_thickness; + } cursor; + + struct { + bool hide_when_typing; + bool alternate_scroll_mode; + //struct config_key_modifiers selection_override_modifiers; + config_modifier_list_t selection_override_modifiers; + } mouse; + + struct { + /* Bindings for "normal" mode */ + struct config_key_binding_list key; + struct config_key_binding_list mouse; + + /* + * Special modes + */ + + /* While searching (not - action to *start* a search is in the + * 'key' bindings above */ + struct config_key_binding_list search; + + /* While showing URL jump labels */ + struct config_key_binding_list url; + } bindings; + + struct { + enum { CONF_CSD_PREFER_NONE, CONF_CSD_PREFER_SERVER, CONF_CSD_PREFER_CLIENT } preferred; + + uint16_t title_height; + uint16_t border_width; + uint16_t border_width_visible; + uint16_t button_width; + + bool hide_when_maximized; + bool double_click_to_maximize; + + struct { + bool title_set:1; + bool buttons_set:1; + bool minimize_set:1; + bool maximize_set:1; + bool close_set:1; + bool border_set:1; + uint32_t title; + uint32_t buttons; + uint32_t minimize; + uint32_t maximize; + uint32_t quit; /* 'close' collides with #define in epoll-shim */ + uint32_t border; + } color; + + struct config_font_list font; + } csd; + + uint16_t render_worker_count; + char *server_socket_path; + bool presentation_timings; + bool hold_at_exit; + enum { + SELECTION_TARGET_NONE, + SELECTION_TARGET_PRIMARY, + SELECTION_TARGET_CLIPBOARD, + SELECTION_TARGET_BOTH + } selection_target; + + struct { + struct config_spawn_template command; + struct config_spawn_template command_action_arg; + struct config_spawn_template close; + bool inhibit_when_focused; + } desktop_notifications; + + env_var_list_t env_vars; + + char *utmp_helper_path; + + struct { + enum fcft_scaling_filter fcft_filter; + bool overflowing_glyphs; + bool grapheme_shaping; + enum { + GRAPHEME_WIDTH_WCSWIDTH, + GRAPHEME_WIDTH_DOUBLE, + GRAPHEME_WIDTH_MAX, + } grapheme_width_method; + enum { + RENDER_TIMER_NONE, + RENDER_TIMER_OSD, + RENDER_TIMER_LOG, + RENDER_TIMER_BOTH + } render_timer; + bool damage_whole_window; + uint32_t delayed_render_lower_ns; + uint32_t delayed_render_upper_ns; + off_t max_shm_pool_size; + float box_drawing_base_thickness; + bool box_drawing_solid_shades; + bool font_monospace_warn; + bool sixel; + enum shm_bit_depth surface_bit_depth; + uint32_t min_stride_alignment; + bool preapply_damage; + } tweak; + + struct { + uint32_t long_press_delay; + } touch; + + struct { + bool enabled; + enum { + CONF_TABS_POSITION_TOP, + CONF_TABS_POSITION_BOTTOM, + } position; + enum { + CONF_TABS_STYLE_ROUNDED, + CONF_TABS_STYLE_SQUARE, + } style; + enum { + CONF_TABS_LAYOUT_SPAN, + CONF_TABS_LAYOUT_FLOATING, + } layout; + uint16_t height; /* pill height; bar = height + margin in floating mode */ + uint16_t tab_width; /* max tab width in floating mode */ + uint16_t tab_padding; /* gap between tabs in floating mode */ + uint16_t label_padding; /* horizontal padding around the label inside each tab pill */ + uint16_t margin; /* edge gap in floating mode (added to bar height) */ + uint16_t corner_radius; + bool inherit_cwd; + char *unread_indicator; /* UTF-8 string drawn before label when unread; NULL or empty disables */ + struct { + uint32_t bg; + uint32_t fg; + uint32_t active_bg; + uint32_t active_fg; + uint32_t unread_fg; + } colors; + } tabs; + + user_notifications_t notifications; +}; + +bool config_override_apply(struct config *conf, config_override_t *overrides, + bool errors_are_fatal); +bool config_load( + struct config *conf, const char *path, + user_notifications_t *initial_user_notifications, + config_override_t *overrides, bool errors_are_fatal, + bool as_server); +void config_free(struct config *conf); +struct config *config_clone(const struct config *old); + +bool config_font_parse(const char *pattern, struct config_font *font); +void config_font_list_destroy(struct config_font_list *font_list); + +#if 0 +struct seat; +xkb_mod_mask_t +conf_modifiers_to_mask( + const struct seat *seat, const struct config_key_modifiers *modifiers); +#endif +bool check_if_font_is_monospaced( + const char *pattern, user_notifications_t *notifications); diff --git a/csi.c b/csi.c new file mode 100644 index 0000000..87af215 --- /dev/null +++ b/csi.c @@ -0,0 +1,2234 @@ +#include "csi.h" + +#include +#include +#include + +#if defined(_DEBUG) + #include +#endif + +#include + +#define LOG_MODULE "csi" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" +#include "config.h" +#include "debug.h" +#include "grid.h" +#include "selection.h" +#include "sixel.h" +#include "util.h" +#include "version.h" +#include "vt.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +#define UNHANDLED() LOG_DBG("unhandled: %s", csi_as_string(term, final, -1)) +#define UNHANDLED_SGR(idx) LOG_DBG("unhandled: %s", csi_as_string(term, 'm', idx)) + +static void +sgr_reset(struct terminal *term) +{ + term->vt.attrs = (struct attributes){0}; + term->vt.underline = (struct underline_range_data){0}; + + term->bits_affecting_ascii_printer.underline_style = false; + term->bits_affecting_ascii_printer.underline_color = false; + term_update_ascii_printer(term); +} + +static const char * +csi_as_string(struct terminal *term, uint8_t final, int idx) +{ + static char msg[1024]; + int c = snprintf(msg, sizeof(msg), "CSI: "); + + for (size_t i = idx >= 0 ? idx : 0; + i < (idx >= 0 ? idx + 1 : term->vt.params.idx); + i++) + { + c += snprintf(&msg[c], sizeof(msg) - c, "%u", + term->vt.params.v[i].value); + + for (size_t j = 0; j < term->vt.params.v[i].sub.idx; j++) { + c += snprintf(&msg[c], sizeof(msg) - c, ":%u", + term->vt.params.v[i].sub.value[j]); + } + + c += snprintf(&msg[c], sizeof(msg) - c, "%s", + i == term->vt.params.idx - 1 ? "" : ";"); + } + + for (size_t i = 0; i < sizeof(term->vt.private); i++) { + char value = (term->vt.private >> (i * 8)) & 0xff; + if (value == 0) + break; + c += snprintf(&msg[c], sizeof(msg) - c, "%c", value); + } + + snprintf(&msg[c], sizeof(msg) - c, "%c (%u parameters)", + final, idx >= 0 ? 1 : term->vt.params.idx); + return msg; +} + +static void +csi_sgr(struct terminal *term) +{ + if (term->vt.params.idx == 0) { + sgr_reset(term); + return; + } + + for (size_t i = 0; i < term->vt.params.idx; i++) { + const int param = term->vt.params.v[i].value; + + switch (param) { + case 0: + sgr_reset(term); + break; + + case 1: term->vt.attrs.bold = true; break; + case 2: term->vt.attrs.dim = true; break; + case 3: term->vt.attrs.italic = true; break; + case 4: { + term->vt.attrs.underline = true; + term->vt.underline.style = UNDERLINE_SINGLE; + + if (unlikely(term->vt.params.v[i].sub.idx == 1)) { + enum underline_style style = term->vt.params.v[i].sub.value[0]; + + switch (style) { + default: + case UNDERLINE_NONE: + term->vt.attrs.underline = false; + term->vt.underline.style = UNDERLINE_NONE; + term->bits_affecting_ascii_printer.underline_style = false; + break; + + case UNDERLINE_SINGLE: + case UNDERLINE_DOUBLE: + case UNDERLINE_CURLY: + case UNDERLINE_DOTTED: + case UNDERLINE_DASHED: + term->vt.underline.style = style; + term->bits_affecting_ascii_printer.underline_style = + style > UNDERLINE_SINGLE; + break; + } + } else + term->bits_affecting_ascii_printer.underline_style = false; + term_update_ascii_printer(term); + break; + } + case 5: term->vt.attrs.blink = true; break; + case 6: LOG_WARN("ignored: rapid blink"); break; + case 7: term->vt.attrs.reverse = true; break; + case 8: term->vt.attrs.conceal = true; break; + case 9: term->vt.attrs.strikethrough = true; break; + + case 21: + term->vt.attrs.underline = true; + term->vt.underline.style = UNDERLINE_DOUBLE; + term->bits_affecting_ascii_printer.underline_style = true; + term_update_ascii_printer(term); + break; + + case 22: term->vt.attrs.bold = term->vt.attrs.dim = false; break; + case 23: term->vt.attrs.italic = false; break; + case 24: { + term->vt.attrs.underline = false; + term->vt.underline.style = UNDERLINE_NONE; + term->bits_affecting_ascii_printer.underline_style = false; + term_update_ascii_printer(term); + break; + } + case 25: term->vt.attrs.blink = false; break; + case 26: break; /* rapid blink, ignored */ + case 27: term->vt.attrs.reverse = false; break; + case 28: term->vt.attrs.conceal = false; break; + case 29: term->vt.attrs.strikethrough = false; break; + + /* Regular foreground colors */ + case 30: + case 31: + case 32: + case 33: + case 34: + case 35: + case 36: + case 37: + term->vt.attrs.fg_src = COLOR_BASE16; + term->vt.attrs.fg = param - 30; + break; + + case 38: + case 48: + case 58: { + uint32_t color; + enum color_source src; + + /* Indexed: 38;5; */ + if (term->vt.params.idx - i - 1 >= 2 && + term->vt.params.v[i + 1].value == 5) + { + src = COLOR_BASE256; + color = min(term->vt.params.v[i + 2].value, + ALEN(term->colors.table) - 1); + i += 2; + } + + /* RGB: 38;2;;; */ + else if (term->vt.params.idx - i - 1 >= 4 && + term->vt.params.v[i + 1].value == 2) + { + uint8_t r = term->vt.params.v[i + 2].value; + uint8_t g = term->vt.params.v[i + 3].value; + uint8_t b = term->vt.params.v[i + 4].value; + src = COLOR_RGB; + color = r << 16 | g << 8 | b; + i += 4; + } + + /* Indexed: 38:5: */ + else if (term->vt.params.v[i].sub.idx >= 2 && + term->vt.params.v[i].sub.value[0] == 5) + { + src = COLOR_BASE256; + color = min(term->vt.params.v[i].sub.value[1], + ALEN(term->colors.table) - 1); + } + + /* + * RGB: 38:2::r:g:b[:ignored:tolerance:tolerance-color-space] + * RGB: 38:2:r:g:b + * + * The second version is a "bastard" version - many + * programs "forget" the color space ID + * parameter... *sigh* + */ + else if (term->vt.params.v[i].sub.idx >= 4 && + term->vt.params.v[i].sub.value[0] == 2) + { + const struct vt_param *param = &term->vt.params.v[i]; + bool have_color_space_id = param->sub.idx >= 5; + + /* 0 - color space (ignored) */ + int r_idx = 2 - !have_color_space_id; + int g_idx = 3 - !have_color_space_id; + int b_idx = 4 - !have_color_space_id; + /* 5 - unused */ + /* 6 - CS tolerance */ + /* 7 - color space associated with tolerance */ + + uint8_t r = param->sub.value[r_idx]; + uint8_t g = param->sub.value[g_idx]; + uint8_t b = param->sub.value[b_idx]; + + src = COLOR_RGB; + color = r << 16 | g << 8 | b; + } + + /* Transparent: 38:1 */ + /* CMY: 38:3::c:m:y[:tolerance:tolerance-color-space] */ + /* CMYK: 38:4::c:m:y:k[:tolerance:tolerance-color-space] */ + + /* Unrecognized */ + else { + UNHANDLED_SGR(i); + break; + } + + if (unlikely(param == 58)) { + term->vt.underline.color_src = src; + term->vt.underline.color = color; + term->bits_affecting_ascii_printer.underline_color = true; + term_update_ascii_printer(term); + } else if (param == 38) { + term->vt.attrs.fg_src = src; + term->vt.attrs.fg = color; + } else { + xassert(param == 48); + term->vt.attrs.bg_src = src; + term->vt.attrs.bg = color; + } + break; + } + + case 39: + term->vt.attrs.fg_src = COLOR_DEFAULT; + break; + + /* Regular background colors */ + case 40: + case 41: + case 42: + case 43: + case 44: + case 45: + case 46: + case 47: + term->vt.attrs.bg_src = COLOR_BASE16; + term->vt.attrs.bg = param - 40; + break; + + case 49: + term->vt.attrs.bg_src = COLOR_DEFAULT; + break; + + case 59: + term->vt.underline.color_src = COLOR_DEFAULT; + term->vt.underline.color = 0; + term->bits_affecting_ascii_printer.underline_color = false; + term_update_ascii_printer(term); + break; + + /* Bright foreground colors */ + case 90: + case 91: + case 92: + case 93: + case 94: + case 95: + case 96: + case 97: + term->vt.attrs.fg_src = COLOR_BASE16; + term->vt.attrs.fg = param - 90 + 8; + break; + + /* Bright background colors */ + case 100: + case 101: + case 102: + case 103: + case 104: + case 105: + case 106: + case 107: + term->vt.attrs.bg_src = COLOR_BASE16; + term->vt.attrs.bg = param - 100 + 8; + break; + + default: + UNHANDLED_SGR(i); + break; + } + } +} + +static void +decset_decrst(struct terminal *term, unsigned param, bool enable) +{ +#if defined(_DEBUG) + /* For UNHANDLED() */ + int UNUSED final = enable ? 'h' : 'l'; +#endif + + /* Note: update XTSAVE/XTRESTORE if adding/removing things here */ + + switch (param) { + case 1: + /* DECCKM */ + term->cursor_keys_mode = + enable ? CURSOR_KEYS_APPLICATION : CURSOR_KEYS_NORMAL; + break; + + case 5: + /* DECSCNM */ + term->reverse = enable; + term_damage_all(term); + term_damage_margins(term); + break; + + case 6: { + /* DECOM */ + term->origin = enable ? ORIGIN_RELATIVE : ORIGIN_ABSOLUTE; + term_cursor_home(term); + break; + } + + case 7: + /* DECAWM */ + term->auto_margin = enable; + term->grid->cursor.lcf = false; + break; + + case 9: + if (enable) + LOG_WARN("unimplemented: X10 mouse tracking mode"); +#if 0 + else if (term->mouse_tracking == MOUSE_X10) + term->mouse_tracking = MOUSE_NONE; +#endif + break; + + case 12: + term->cursor_blink.decset = enable; + term_cursor_blink_update(term); + break; + + case 25: + /* DECTCEM */ + term->hide_cursor = !enable; + break; + + case 45: + term->reverse_wrap = enable; + break; + + case 66: + /* DECNKM */ + term->keypad_keys_mode = enable ? KEYPAD_APPLICATION : KEYPAD_NUMERICAL; + break; + + case 67: + if (enable) + LOG_WARN("unimplemented: DECBKM"); + break; + + case 80: + term->sixel.scrolling = !enable; + break; + + case 1000: + if (enable) + term->mouse_tracking = MOUSE_CLICK; + else if (term->mouse_tracking == MOUSE_CLICK) + term->mouse_tracking = MOUSE_NONE; + term_xcursor_update(term); + break; + + case 1001: + if (enable) + LOG_WARN("unimplemented: highlight mouse tracking"); + break; + + case 1002: + if (enable) + term->mouse_tracking = MOUSE_DRAG; + else if (term->mouse_tracking == MOUSE_DRAG) + term->mouse_tracking = MOUSE_NONE; + term_xcursor_update(term); + break; + + case 1003: + if (enable) + term->mouse_tracking = MOUSE_MOTION; + else if (term->mouse_tracking == MOUSE_MOTION) + term->mouse_tracking = MOUSE_NONE; + term_xcursor_update(term); + break; + + case 1004: + term->focus_events = enable; + if (enable) + term_to_slave(term, term->kbd_focus ? "\033[I" : "\033[O", 3); + break; + + case 1005: + if (enable) + LOG_WARN("unimplemented: mouse reporting mode: UTF-8"); +#if 0 + else if (term->mouse_reporting == MOUSE_UTF8) + term->mouse_reporting = MOUSE_NONE; +#endif + break; + + case 1006: + if (enable) + term->mouse_reporting = MOUSE_SGR; + else if (term->mouse_reporting == MOUSE_SGR) + term->mouse_reporting = MOUSE_NORMAL; + break; + + case 1007: + term->alt_scrolling = enable; + break; + + case 1015: + if (enable) + term->mouse_reporting = MOUSE_URXVT; + else if (term->mouse_reporting == MOUSE_URXVT) + term->mouse_reporting = MOUSE_NORMAL; + break; + + case 1016: + if (enable) + term->mouse_reporting = MOUSE_SGR_PIXELS; + else if (term->mouse_reporting == MOUSE_SGR_PIXELS) + term->mouse_reporting = MOUSE_NORMAL; + break; + + case 1034: + /* smm */ + LOG_DBG("%s 8-bit meta mode", enable ? "enabling" : "disabling"); + term->meta.eight_bit = enable; + break; + + case 1035: + /* numLock */ + LOG_DBG("%s Num Lock modifier", enable ? "enabling" : "disabling"); + term->num_lock_modifier = enable; + break; + + case 1036: + /* metaSendsEscape */ + LOG_DBG("%s meta-sends-escape", enable ? "enabling" : "disabling"); + term->meta.esc_prefix = enable; + break; + + case 1042: + term->bell_action_enabled = enable; + break; + +#if 0 + case 1043: + LOG_WARN("unimplemented: raise window on ctrl-g"); + break; +#endif + + case 1048: + if (enable) + term_save_cursor(term); + else + term_restore_cursor(term, &term->grid->saved_cursor); + break; + + case 47: + case 1047: + case 1049: + if (enable && term->grid != &term->alt) { + selection_cancel(term); + + if (param == 1049) + term_save_cursor(term); + + term->grid = &term->alt; + + /* Cursor retains its position from the normal grid */ + term_cursor_to( + term, + min(term->normal.cursor.point.row, term->rows - 1), + min(term->normal.cursor.point.col, term->cols - 1)); + + tll_free(term->normal.scroll_damage); + term_erase(term, 0, 0, term->rows - 1, term->cols - 1); + } + + else if (!enable && term->grid == &term->alt) { + selection_cancel(term); + + term->grid = &term->normal; + + /* Cursor retains its position from the alt grid */ + term_cursor_to( + term, min(term->alt.cursor.point.row, term->rows - 1), + min(term->alt.cursor.point.col, term->cols - 1)); + + if (param == 1049) + term_restore_cursor(term, &term->grid->saved_cursor); + + /* Delete all sixel images on the alt screen */ + tll_foreach(term->alt.sixel_images, it) { + sixel_destroy(&it->item); + tll_remove(term->alt.sixel_images, it); + } + + tll_free(term->alt.scroll_damage); + term_damage_view(term); + } + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; + term_update_ascii_printer(term); + break; + + case 1070: + term->sixel.use_private_palette = enable; + break; + + case 2004: + term->bracketed_paste = enable; + break; + + case 2026: + if (enable) + term_enable_app_sync_updates(term); + else + term_disable_app_sync_updates(term); + break; + + case 2027: +#if defined(FOOT_GRAPHEME_CLUSTERING) + term->grapheme_shaping = enable; +#endif + break; + + case 2031: + term->report_theme_changes = enable; + break; + + case 2048: + if (enable) + term_enable_size_notifications(term); + else + term_disable_size_notifications(term); + break; + + case 8452: + term->sixel.cursor_right_of_graphics = enable; + break; + + case 737769: + if (enable) + term_ime_enable(term); + else { + term_ime_disable(term); + term->ime_reenable_after_url_mode = false; + } + break; + + default: + UNHANDLED(); + break; + } +} + +static void +decset(struct terminal *term, unsigned param) +{ + decset_decrst(term, param, true); +} + +static void +decrst(struct terminal *term, unsigned param) +{ + decset_decrst(term, param, false); +} + +/* + * These values represent the current state of a DEC private mode, + * as returned in the DECRPM reply to a DECRQM query. + */ +enum decrpm_status { + DECRPM_NOT_RECOGNIZED = 0, + DECRPM_SET = 1, + DECRPM_RESET = 2, + DECRPM_PERMANENTLY_SET = 3, + DECRPM_PERMANENTLY_RESET = 4, +}; + +static enum decrpm_status +decrpm(bool enabled) +{ + return enabled ? DECRPM_SET : DECRPM_RESET; +} + +static enum decrpm_status +decrqm(const struct terminal *term, unsigned param) +{ + switch (param) { + case 1: return decrpm(term->cursor_keys_mode == CURSOR_KEYS_APPLICATION); + case 5: return decrpm(term->reverse); + case 6: return decrpm(term->origin); + case 7: return decrpm(term->auto_margin); + case 9: return DECRPM_PERMANENTLY_RESET; /* term->mouse_tracking == MOUSE_X10; */ + case 12: return decrpm(term->cursor_blink.decset); + case 25: return decrpm(!term->hide_cursor); + case 45: return decrpm(term->reverse_wrap); + case 66: return decrpm(term->keypad_keys_mode == KEYPAD_APPLICATION); + case 67: return DECRPM_PERMANENTLY_RESET; /* https://vt100.net/docs/vt510-rm/DECBKM */ + case 80: return decrpm(!term->sixel.scrolling); + case 1000: return decrpm(term->mouse_tracking == MOUSE_CLICK); + case 1001: return DECRPM_PERMANENTLY_RESET; + case 1002: return decrpm(term->mouse_tracking == MOUSE_DRAG); + case 1003: return decrpm(term->mouse_tracking == MOUSE_MOTION); + case 1004: return decrpm(term->focus_events); + case 1005: return DECRPM_PERMANENTLY_RESET; /* term->mouse_reporting == MOUSE_UTF8; */ + case 1006: return decrpm(term->mouse_reporting == MOUSE_SGR); + case 1007: return decrpm(term->alt_scrolling); + case 1015: return decrpm(term->mouse_reporting == MOUSE_URXVT); + case 1016: return decrpm(term->mouse_reporting == MOUSE_SGR_PIXELS); + case 1034: return decrpm(term->meta.eight_bit); + case 1035: return decrpm(term->num_lock_modifier); + case 1036: return decrpm(term->meta.esc_prefix); + case 1042: return decrpm(term->bell_action_enabled); + case 47: /* FALLTHROUGH */ + case 1047: /* FALLTHROUGH */ + case 1049: return decrpm(term->grid == &term->alt); + case 1070: return decrpm(term->sixel.use_private_palette); + case 2004: return decrpm(term->bracketed_paste); + case 2026: return decrpm(term->render.app_sync_updates.enabled); + case 2027: return term->conf->tweak.grapheme_width_method != GRAPHEME_WIDTH_DOUBLE + ? DECRPM_PERMANENTLY_RESET + : decrpm(term->grapheme_shaping); + case 2031: return decrpm(term->report_theme_changes); + case 2048: return decrpm(term->size_notifications); + case 8452: return decrpm(term->sixel.cursor_right_of_graphics); + case 737769: return decrpm(term_ime_is_enabled(term)); + } + + return DECRPM_NOT_RECOGNIZED; +} + +static void +xtsave(struct terminal *term, unsigned param) +{ + switch (param) { + case 1: term->xtsave.application_cursor_keys = term->cursor_keys_mode == CURSOR_KEYS_APPLICATION; break; + case 5: term->xtsave.reverse = term->reverse; break; + case 6: term->xtsave.origin = term->origin; break; + case 7: term->xtsave.auto_margin = term->auto_margin; break; + case 9: /* term->xtsave.mouse_x10 = term->mouse_tracking == MOUSE_X10; */ break; + case 12: term->xtsave.cursor_blink = term->cursor_blink.decset; break; + case 25: term->xtsave.show_cursor = !term->hide_cursor; break; + case 45: term->xtsave.reverse_wrap = term->reverse_wrap; break; + case 47: term->xtsave.alt_screen = term->grid == &term->alt; break; + case 66: term->xtsave.application_keypad_keys = term->keypad_keys_mode == KEYPAD_APPLICATION; break; + case 67: break; + case 80: term->xtsave.sixel_display_mode = !term->sixel.scrolling; break; + case 1000: term->xtsave.mouse_click = term->mouse_tracking == MOUSE_CLICK; break; + case 1001: break; + case 1002: term->xtsave.mouse_drag = term->mouse_tracking == MOUSE_DRAG; break; + case 1003: term->xtsave.mouse_motion = term->mouse_tracking == MOUSE_MOTION; break; + case 1004: term->xtsave.focus_events = term->focus_events; break; + case 1005: /* term->xtsave.mouse_utf8 = term->mouse_reporting == MOUSE_UTF8; */ break; + case 1006: term->xtsave.mouse_sgr = term->mouse_reporting == MOUSE_SGR; break; + case 1007: term->xtsave.alt_scrolling = term->alt_scrolling; break; + case 1015: term->xtsave.mouse_urxvt = term->mouse_reporting == MOUSE_URXVT; break; + case 1016: term->xtsave.mouse_sgr_pixels = term->mouse_reporting == MOUSE_SGR_PIXELS; break; + case 1034: term->xtsave.meta_eight_bit = term->meta.eight_bit; break; + case 1035: term->xtsave.num_lock_modifier = term->num_lock_modifier; break; + case 1036: term->xtsave.meta_esc_prefix = term->meta.esc_prefix; break; + case 1042: term->xtsave.bell_action_enabled = term->bell_action_enabled; break; + case 1047: term->xtsave.alt_screen = term->grid == &term->alt; break; + case 1048: term_save_cursor(term); break; + case 1049: term->xtsave.alt_screen = term->grid == &term->alt; break; + case 1070: term->xtsave.sixel_private_palette = term->sixel.use_private_palette; break; + case 2004: term->xtsave.bracketed_paste = term->bracketed_paste; break; + case 2026: term->xtsave.app_sync_updates = term->render.app_sync_updates.enabled; break; + case 2027: term->xtsave.grapheme_shaping = term->grapheme_shaping; break; + case 2031: term->xtsave.report_theme_changes = term->report_theme_changes; break; + case 2048: term->xtsave.size_notifications = term->size_notifications; break; + case 8452: term->xtsave.sixel_cursor_right_of_graphics = term->sixel.cursor_right_of_graphics; break; + case 737769: term->xtsave.ime = term_ime_is_enabled(term); break; + } +} + +static void +xtrestore(struct terminal *term, unsigned param) +{ + bool enable; + switch (param) { + case 1: enable = term->xtsave.application_cursor_keys; break; + case 5: enable = term->xtsave.reverse; break; + case 6: enable = term->xtsave.origin; break; + case 7: enable = term->xtsave.auto_margin; break; + case 9: /* enable = term->xtsave.mouse_x10; break; */ return; + case 12: enable = term->xtsave.cursor_blink; break; + case 25: enable = term->xtsave.show_cursor; break; + case 45: enable = term->xtsave.reverse_wrap; break; + case 47: enable = term->xtsave.alt_screen; break; + case 66: enable = term->xtsave.application_keypad_keys; break; + case 67: return; + case 80: enable = term->xtsave.sixel_display_mode; break; + case 1000: enable = term->xtsave.mouse_click; break; + case 1001: return; + case 1002: enable = term->xtsave.mouse_drag; break; + case 1003: enable = term->xtsave.mouse_motion; break; + case 1004: enable = term->xtsave.focus_events; break; + case 1005: /* enable = term->xtsave.mouse_utf8; break; */ return; + case 1006: enable = term->xtsave.mouse_sgr; break; + case 1007: enable = term->xtsave.alt_scrolling; break; + case 1015: enable = term->xtsave.mouse_urxvt; break; + case 1016: enable = term->xtsave.mouse_sgr_pixels; break; + case 1034: enable = term->xtsave.meta_eight_bit; break; + case 1035: enable = term->xtsave.num_lock_modifier; break; + case 1036: enable = term->xtsave.meta_esc_prefix; break; + case 1042: enable = term->xtsave.bell_action_enabled; break; + case 1047: enable = term->xtsave.alt_screen; break; + case 1048: enable = true; break; + case 1049: enable = term->xtsave.alt_screen; break; + case 1070: enable = term->xtsave.sixel_private_palette; break; + case 2004: enable = term->xtsave.bracketed_paste; break; + case 2026: enable = term->xtsave.app_sync_updates; break; + case 2027: enable = term->xtsave.grapheme_shaping; break; + case 2031: enable = term->xtsave.report_theme_changes; break; + case 2048: enable = term->xtsave.size_notifications; break; + case 8452: enable = term->xtsave.sixel_cursor_right_of_graphics; break; + case 737769: enable = term->xtsave.ime; break; + + default: return; + } + + decset_decrst(term, param, enable); +} + +static bool +params_to_rectangular_area(const struct terminal *term, int first_idx, + int *top, int *left, int *bottom, int *right) +{ + int rel_top = vt_param_get(term, first_idx + 0, 1) - 1; + *left = min(vt_param_get(term, first_idx + 1, 1) - 1, term->cols - 1); + int rel_bottom = vt_param_get(term, first_idx + 2, term->rows) - 1; + *right = min(vt_param_get(term, first_idx + 3, term->cols) - 1, term->cols - 1); + + if (rel_top > rel_bottom || *left > *right) + return false; + + *top = term_row_rel_to_abs(term, rel_top); + *bottom = term_row_rel_to_abs(term, rel_bottom); + + return true; +} + +void +csi_dispatch(struct terminal *term, uint8_t final) +{ + LOG_DBG("%s (%08x)", csi_as_string(term, final, -1), term->vt.private); + + switch (term->vt.private) { + case 0: { + switch (final) { + case 'b': + if (term->vt.last_printed != 0) { + /* + * Note: we never reset 'last-printed'. According to + * ECMA-48, the behaviour is undefined if REP was + * _not_ preceded by a graphical character. + */ + int count = vt_param_get(term, 0, 1); + LOG_DBG("REP: '%lc' %d times", (wint_t)term->vt.last_printed, count); + + int width; + + if (term->vt.last_printed >= CELL_COMB_CHARS_LO) { + const struct composed *comp = composed_lookup( + term->composed, term->vt.last_printed - CELL_COMB_CHARS_LO); + + xassert(comp != NULL); + width = comp->forced_width > 0 ? comp->forced_width : comp->width; + } else + width = c32width(term->vt.last_printed); + + if (width > 0) { + for (int i = 0; i < count; i++) + term_print(term, term->vt.last_printed, width, false); + } + } + break; + + case 'c': { + if (vt_param_get(term, 0, 0) != 0) { + UNHANDLED(); + break; + } + + /* Send Device Attributes (Primary DA) */ + + /* + * Responses: + * - CSI?1;2c vt100 with advanced video option + * - CSI?1;0c vt101 with no options + * - CSI?6c vt102 + * - CSI?62;c vt220 + * - CSI?63;c vt320 + * - CSI?64;c vt420 + * + * Ps (response may contain multiple): + * - 1 132 columns + * - 2 Printer. + * - 3 ReGIS graphics. + * - 4 Sixel graphics. + * - 6 Selective erase. + * - 8 User-defined keys. + * - 9 National Replacement Character sets. + * - 15 Technical characters. + * - 16 Locator port. + * - 17 Terminal state interrogation. + * - 18 User windows. + * - 21 Horizontal scrolling. + * - 22 ANSI color, e.g., VT525. + * - 28 Rectangular editing. + * - 29 ANSI text locator (i.e., DEC Locator mode). + * - 52 Clipboard access + * + * Note: we report ourselves as a VT220, mainly to be able + * to pass parameters, to indicate we support sixel, and + * ANSI colors. + * + * The VT level must be synchronized with the secondary DA + * response. + * + * Note: tertiary DA responds with "FOOT". + */ + char reply[32]; + + int len = snprintf( + reply, sizeof(reply), "\033[?62%s;22;28%sc", + term->conf->tweak.sixel ? ";4" : "", + (term->conf->security.osc52 == OSC52_ENABLED || + term->conf->security.osc52 == OSC52_COPY_ENABLED ? ";52" : "")); + + term_to_slave(term, reply, len); + break; + } + + case 'd': { + /* VPA - vertical line position absolute */ + int rel_row = vt_param_get(term, 0, 1) - 1; + int row = term_row_rel_to_abs(term, rel_row); + term_cursor_to(term, row, term->grid->cursor.point.col); + break; + } + + case 'm': + csi_sgr(term); + break; + + case 'A': + term_cursor_up(term, vt_param_get(term, 0, 1)); + break; + + case 'e': + case 'B': + term_cursor_down(term, vt_param_get(term, 0, 1)); + break; + + case 'a': + case 'C': + term_cursor_right(term, vt_param_get(term, 0, 1)); + break; + + case 'D': + term_cursor_left(term, vt_param_get(term, 0, 1)); + break; + + case 'E': + /* CNL - Cursor Next Line */ + term_cursor_down(term, vt_param_get(term, 0, 1)); + term_cursor_left(term, term->grid->cursor.point.col); + break; + + case 'F': + /* CPL - Cursor Previous Line */ + term_cursor_up(term, vt_param_get(term, 0, 1)); + term_cursor_left(term, term->grid->cursor.point.col); + break; + + case 'g': { + int param = vt_param_get(term, 0, 0); + switch (param) { + case 0: + /* Clear tab stop at *current* column */ + tll_foreach(term->tab_stops, it) { + if (it->item == term->grid->cursor.point.col) + tll_remove(term->tab_stops, it); + else if (it->item > term->grid->cursor.point.col) + break; + } + + break; + + case 3: + /* Clear *all* tabs */ + tll_free(term->tab_stops); + break; + + default: + UNHANDLED(); + break; + } + break; + } + + case '`': + case 'G': { + /* Cursor horizontal absolute */ + int col = min(vt_param_get(term, 0, 1), term->cols) - 1; + term_cursor_col(term, col); + break; + } + + case 'f': + case 'H': { + /* Move cursor */ + int rel_row = vt_param_get(term, 0, 1) - 1; + int row = term_row_rel_to_abs(term, rel_row); + int col = min(vt_param_get(term, 1, 1), term->cols) - 1; + term_cursor_to(term, row, col); + break; + } + + case 'J': { + /* Erase screen */ + + int param = vt_param_get(term, 0, 0); + switch (param) { + case 0: { + /* From cursor to end of screen */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase( + term, + cursor->row, cursor->col, + term->rows - 1, term->cols - 1); + term->grid->cursor.lcf = false; + break; + } + + case 1: { + /* From start of screen to cursor */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, 0, 0, cursor->row, cursor->col); + term->grid->cursor.lcf = false; + break; + } + + case 2: + /* Erase entire screen */ + term_erase(term, 0, 0, term->rows - 1, term->cols - 1); + term->grid->cursor.lcf = false; + break; + + case 3: { + /* Erase scrollback */ + term_erase_scrollback(term); + break; + } + + default: + UNHANDLED(); + break; + } + break; + } + + case 'K': { + /* Erase line */ + + int param = vt_param_get(term, 0, 0); + switch (param) { + case 0: { + /* From cursor to end of line */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase( + term, + cursor->row, cursor->col, + cursor->row, term->cols - 1); + term->grid->cursor.lcf = false; + break; + } + + case 1: { + /* From start of line to cursor */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, cursor->row, 0, cursor->row, cursor->col); + term->grid->cursor.lcf = false; + break; + } + + case 2: { + /* Entire line */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, cursor->row, 0, cursor->row, term->cols - 1); + term->grid->cursor.lcf = false; + break; + } + + default: + UNHANDLED(); + break; + } + + break; + } + + case 'L': { /* IL */ + if (term->grid->cursor.point.row < term->scroll_region.start || + term->grid->cursor.point.row >= term->scroll_region.end) + break; + + int count = min( + vt_param_get(term, 0, 1), + term->scroll_region.end - term->grid->cursor.point.row); + + term_scroll_reverse_partial( + term, + (struct scroll_region){ + .start = term->grid->cursor.point.row, + .end = term->scroll_region.end}, + count); + term->grid->cursor.lcf = false; + term->grid->cursor.point.col = 0; + break; + } + + case 'M': { /* DL */ + if (term->grid->cursor.point.row < term->scroll_region.start || + term->grid->cursor.point.row >= term->scroll_region.end) + break; + + int count = min( + vt_param_get(term, 0, 1), + term->scroll_region.end - term->grid->cursor.point.row); + + term_scroll_partial( + term, + (struct scroll_region){ + .start = term->grid->cursor.point.row, + .end = term->scroll_region.end}, + count); + term->grid->cursor.lcf = false; + term->grid->cursor.point.col = 0; + break; + } + + case 'P': { + /* DCH: Delete character(s) */ + + /* Number of characters to delete */ + int count = min( + vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); + + /* Number of characters left after deletion (on current line) */ + int remaining = term->cols - (term->grid->cursor.point.col + count); + + /* 'Delete' characters by moving the remaining ones */ + memmove(&term->grid->cur_row->cells[term->grid->cursor.point.col], + &term->grid->cur_row->cells[term->grid->cursor.point.col + count], + remaining * sizeof(term->grid->cur_row->cells[0])); + + for (size_t c = 0; c < remaining; c++) + term->grid->cur_row->cells[term->grid->cursor.point.col + c].attrs.clean = 0; + term->grid->cur_row->dirty = true; + + /* Erase the remainder of the line */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase( + term, + cursor->row, cursor->col + remaining, + cursor->row, term->cols - 1); + term->grid->cursor.lcf = false; + break; + } + + case '@': { + /* ICH: insert character(s) */ + + /* Number of characters to insert */ + int count = min( + vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); + + /* Characters to move */ + int remaining = term->cols - (term->grid->cursor.point.col + count); + + /* Push existing characters */ + memmove(&term->grid->cur_row->cells[term->grid->cursor.point.col + count], + &term->grid->cur_row->cells[term->grid->cursor.point.col], + remaining * sizeof(term->grid->cur_row->cells[0])); + for (size_t c = 0; c < remaining; c++) + term->grid->cur_row->cells[term->grid->cursor.point.col + count + c].attrs.clean = 0; + term->grid->cur_row->dirty = true; + + /* Erase (insert space characters) */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase( + term, + cursor->row, cursor->col, + cursor->row, cursor->col + count - 1); + term->grid->cursor.lcf = false; + break; + } + + case 'S': { + const struct scroll_region *r = &term->scroll_region; + int amount = min(vt_param_get(term, 0, 1), r->end - r->start); + term_scroll(term, amount); + break; + } + + case 'T': { + const struct scroll_region *r = &term->scroll_region; + int amount = min(vt_param_get(term, 0, 1), r->end - r->start); + term_scroll_reverse(term, amount); + break; + } + + case 'X': { + /* Erase chars */ + int count = min( + vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); + + const struct coord *cursor = &term->grid->cursor.point; + term_erase( + term, + cursor->row, cursor->col, + cursor->row, cursor->col + count - 1); + term->grid->cursor.lcf = false; + break; + } + + case 'I': { + /* CHT - Tab Forward (param is number of tab stops to move through) */ + for (int i = 0; i < vt_param_get(term, 0, 1); i++) { + int new_col = term->cols - 1; + tll_foreach(term->tab_stops, it) { + if (it->item > term->grid->cursor.point.col) { + new_col = it->item; + break; + } + } + xassert(new_col >= term->grid->cursor.point.col); + + bool lcf = term->grid->cursor.lcf; + term_cursor_right(term, new_col - term->grid->cursor.point.col); + term->grid->cursor.lcf = lcf; + } + break; + } + + case 'Z': + /* CBT - Back tab (param is number of tab stops to move back through) */ + for (int i = 0; i < vt_param_get(term, 0, 1); i++) { + int new_col = 0; + tll_rforeach(term->tab_stops, it) { + if (it->item < term->grid->cursor.point.col) { + new_col = it->item; + break; + } + } + xassert(term->grid->cursor.point.col >= new_col); + term_cursor_left(term, term->grid->cursor.point.col - new_col); + } + break; + + case 'h': + case 'l': { + /* Set/Reset Mode (SM/RM) */ + int param = vt_param_get(term, 0, 0); + bool sm = final == 'h'; + if (param == 4) { + /* Insertion Replacement Mode (IRM) */ + term->insert_mode = sm; + term->bits_affecting_ascii_printer.insert_mode = sm; + term_update_ascii_printer(term); + break; + } + + /* + * ECMA-48 defines modes 1-22, all of which were optional + * (§7.1; "may have one state only") and are considered + * deprecated (§7.1) in the latest (5th) edition. xterm only + * documents modes 2, 4, 12 and 20, the last of which was + * outright removed (§8.3.106) in 5th edition ECMA-48. + */ + if (sm) { + LOG_WARN("SM with unimplemented mode: %d", param); + } + break; + } + + case 'r': { + int start = vt_param_get(term, 0, 1); + int end = min(vt_param_get(term, 1, term->rows), term->rows); + + if (end > start) { + + /* 1-based */ + term->scroll_region.start = start - 1; + term->scroll_region.end = end; + term_cursor_home(term); + + LOG_DBG("scroll region: %d-%d", + term->scroll_region.start, + term->scroll_region.end); + } + break; + } + + case 's': + term_save_cursor(term); + break; + + case 'u': + term_restore_cursor(term, &term->grid->saved_cursor); + break; + + case 't': { + /* + * Window operations + */ + + const unsigned param = vt_param_get(term, 0, 0); + + switch (param) { + case 1: LOG_WARN("unimplemented: de-iconify"); break; + case 2: LOG_WARN("unimplemented: iconify"); break; + case 3: LOG_WARN("unimplemented: move window to pixel position"); break; + case 4: LOG_WARN("unimplemented: resize window in pixels"); break; + case 5: LOG_WARN("unimplemented: raise window to front of stack"); break; + case 6: LOG_WARN("unimplemented: raise window to back of stack"); break; + case 7: LOG_WARN("unimplemented: refresh window"); break; + case 8: LOG_WARN("unimplemented: resize window in chars"); break; + case 9: LOG_WARN("unimplemented: maximize/unmaximize window"); break; + case 10: LOG_WARN("unimplemented: to/from full screen"); break; + case 20: LOG_WARN("unimplemented: report icon label"); break; + case 24: LOG_WARN("unimplemented: resize window (DECSLPP)"); break; + + case 11: /* report if window is iconified */ + /* We don't know - always report *not* iconified */ + /* 1=not iconified, 2=iconified */ + term_to_slave(term, "\033[1t", 4); + break; + + case 13: { /* report window position */ + /* We don't know our position - always report (0,0) */ + static const char reply[] = "\033[3;0;0t"; + switch (vt_param_get(term, 1, 0)) { + case 0: /* window position */ + case 2: /* text area position */ + term_to_slave(term, reply, sizeof(reply) - 1); + break; + + default: + UNHANDLED(); + break; + } + + break; + } + + case 14: { /* report window size in pixels */ + int width = -1; + int height = -1; + + switch (vt_param_get(term, 1, 0)) { + case 0: + /* text area size */ + width = term->width - term->margins.left - term->margins.right; + height = term->height - term->margins.top - term->margins.bottom; + break; + + case 2: + /* window size */ + width = term->width; + height = term->height; + break; + + default: + UNHANDLED(); + break; + } + + if (width >= 0 && height >= 0) { + char reply[64]; + size_t n = xsnprintf( + reply, sizeof(reply), "\033[4;%d;%dt", height, width); + term_to_slave(term, reply, n); + } + break; + } + + case 15: /* report screen size in pixels */ + tll_foreach(term->window->on_outputs, it) { + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[5;%d;%dt", + it->item->dim.px_real.height, + it->item->dim.px_real.width); + term_to_slave(term, reply, n); + break; + } + + if (tll_length(term->window->on_outputs) == 0) + term_to_slave(term, "\033[5;0;0t", 8); + break; + + case 16: { /* report cell size in pixels */ + char reply[64]; + size_t n = xsnprintf( + reply, sizeof(reply), "\033[6;%d;%dt", + term->cell_height, term->cell_width); + term_to_slave(term, reply, n); + break; + } + + case 18: { /* text area size in chars */ + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[8;%d;%dt", + term->rows, term->cols); + term_to_slave(term, reply, n); + break; + } + + case 19: { /* report screen size in chars */ + tll_foreach(term->window->on_outputs, it) { + char reply[64]; + size_t n = xsnprintf( + reply, sizeof(reply), "\033[9;%d;%dt", + it->item->dim.px_real.height / term->cell_height, + it->item->dim.px_real.width / term->cell_width); + term_to_slave(term, reply, n); + break; + } + + if (tll_length(term->window->on_outputs) == 0) + term_to_slave(term, "\033[9;0;0t", 8); + break; + } + + case 21: { +#if 0 /* Disabled for now, see #1894 */ + char reply[3 + strlen(term->window_title) + 2 + 1]; + int chars = xsnprintf( + reply, sizeof(reply), "\033]l%s\033\\", term->window_title); + term_to_slave(term, reply, chars); +#else + LOG_WARN("CSI 21 t (report window title) ignored"); +#endif + break; + } + + case 22: { /* push window title */ + /* 0 - icon + title, 1 - icon, 2 - title */ + unsigned what = vt_param_get(term, 1, 0); + if (what == 0 || what == 2) { + tll_push_back( + term->window_title_stack, xstrdup(term->window_title)); + } + break; + } + + case 23: { /* pop window title */ + /* 0 - icon + title, 1 - icon, 2 - title */ + unsigned what = vt_param_get(term, 1, 0); + if (what == 0 || what == 2) { + if (tll_length(term->window_title_stack) > 0) { + char *title = tll_pop_back(term->window_title_stack); + term_set_window_title(term, title); + free(title); + } + } + break; + } + + case 1001: { + } + + default: + LOG_DBG("ignoring %s", csi_as_string(term, final, -1)); + break; + } + break; + } + + case 'n': { + if (term->vt.params.idx > 0) { + int param = vt_param_get(term, 0, 0); + switch (param) { + case 5: + /* Query device status */ + term_to_slave(term, "\x1b[0n", 4); /* "Device OK" */ + break; + + case 6: { + /* u7 - cursor position query */ + + int row = term->origin == ORIGIN_ABSOLUTE + ? term->grid->cursor.point.row + : term->grid->cursor.point.row - term->scroll_region.start; + + /* TODO: we use 0-based position, while the xterm + * terminfo says the receiver of the reply should + * decrement, hence we must add 1 */ + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\x1b[%d;%dR", + row + 1, term->grid->cursor.point.col + 1); + term_to_slave(term, reply, n); + break; + } + + default: + UNHANDLED(); + break; + } + } else + UNHANDLED(); + + break; + } + + default: + UNHANDLED(); + break; + } + + break; /* private[0] == 0 */ + } + + case '?': { + switch (final) { + case 'h': + /* DECSET - DEC private mode set */ + for (size_t i = 0; i < term->vt.params.idx; i++) + decset(term, term->vt.params.v[i].value); + break; + + case 'l': + /* DECRST - DEC private mode reset */ + for (size_t i = 0; i < term->vt.params.idx; i++) + decrst(term, term->vt.params.v[i].value); + break; + + case 's': + for (size_t i = 0; i < term->vt.params.idx; i++) + xtsave(term, term->vt.params.v[i].value); + break; + + case 'r': + for (size_t i = 0; i < term->vt.params.idx; i++) + xtrestore(term, term->vt.params.v[i].value); + break; + + case 'S': { + if (!term->conf->tweak.sixel) { + UNHANDLED(); + break; + } + + unsigned target = vt_param_get(term, 0, 0); + unsigned operation = vt_param_get(term, 1, 0); + + switch (target) { + case 1: + switch (operation) { + case 1: sixel_colors_report_current(term); break; + case 2: sixel_colors_reset(term); break; + case 3: sixel_colors_set(term, vt_param_get(term, 2, 0)); break; + case 4: sixel_colors_report_max(term); break; + default: UNHANDLED(); break; + } + break; + + case 2: + switch (operation) { + case 1: sixel_geometry_report_current(term); break; + case 2: sixel_geometry_reset(term); break; + case 3: sixel_geometry_set(term, vt_param_get(term, 2, 0), vt_param_get(term, 3, 0)); break; + case 4: sixel_geometry_report_max(term); break; + default: UNHANDLED(); break; + } + break; + + default: + UNHANDLED(); + break; + } + + break; + } + + case 'm': { + int resource = vt_param_get(term, 0, 0); + int value = -1; + + switch (resource) { + case 0: /* modifyKeyboard */ + value = 0; + break; + + case 1: /* modifyCursorKeys */ + case 2: /* modifyFunctionKeys */ + value = 1; + break; + + case 4: /* modifyOtherKeys */ + value = term->modify_other_keys_2 ? 2 : 1; + break; + + default: + LOG_WARN("XTQMODKEYS: invalid resource '%d' in '%s'", + resource, csi_as_string(term, final, -1)); + break; + } + + if (value >= 0) { + char reply[16] = {0}; + int chars = snprintf(reply, sizeof(reply), + "\033[>%d;%dm", resource, value); + term_to_slave(term, reply, chars); + } + break; + } + + case 'n': { + const int param = vt_param_get(term, 0, 0); + + switch (param) { + case 996: { /* Query current theme mode (see private mode 2031) */ + /* + * 1 - dark mode + * 2 - light mode + * + * In foot, the themes aren't necessarily light/dark, + * but by convention, the primary theme is dark, and + * the alternative theme is light. + */ + char reply[16] = {0}; + int chars = snprintf( + reply, sizeof(reply), + "\033[?997;%dn", + term->colors.active_theme == COLOR_THEME_DARK ? 1 : 2); + + term_to_slave(term, reply, chars); + break; + } + } + break; + } + + case 'p': { + /* + * Request status of ECMA-48/"ANSI" private mode (DECRQM + * for SM/RM modes; see private="?$" case further below for + * DECSET/DECRST modes) + */ + unsigned param = vt_param_get(term, 0, 0); + unsigned status = DECRPM_NOT_RECOGNIZED; + if (param == 4) { + status = decrpm(term->insert_mode); + } + char reply[32]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[%u;%u$y", param, status); + term_to_slave(term, reply, n); + break; + } + + case 'u': { + enum kitty_kbd_flags flags = + term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx]; + + char reply[8]; + int chars = snprintf(reply, sizeof(reply), "\033[?%uu", flags); + term_to_slave(term, reply, chars); + break; + } + + default: + UNHANDLED(); + break; + } + + break; /* private[0] == '?' */ + } + + case '>': { + switch (final) { + case 'c': + /* Send Device Attributes (Secondary DA) */ + if (vt_param_get(term, 0, 0) != 0) { + UNHANDLED(); + break; + } + + /* + * Param 1 - terminal type: + * 0 - vt100 + * 1 - vt220 + * 2 - vt240 + * 18 - vt330 + * 19 - vt340 + * 24 - vt320 + * 41 - vt420 + * 61 - vt510 + * 64 - vt520 + * 65 - vt525 + * + * Param 2 - firmware version xterm uses its version + * number. We do to, in the format "MAJORMINORPATCH", + * where all three version numbers are always two + * digits. So e.g. 1.25.0 is reported as 012500. + * + * We report ourselves as a VT220. This must be + * synchronized with the primary DA response. + * + * Note: tertiary DA replies with "FOOT". + */ + + static_assert(FOOT_MAJOR < 100, "Major version must not exceed 99"); + static_assert(FOOT_MINOR < 100, "Minor version must not exceed 99"); + static_assert(FOOT_PATCH < 100, "Patch version must not exceed 99"); + + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[>1;%02u%02u%02u;0c", + FOOT_MAJOR, FOOT_MINOR, FOOT_PATCH); + + term_to_slave(term, reply, n); + break; + + case 'm': + if (term->vt.params.idx == 0) { + /* Reset all */ + } else { + int resource = vt_param_get(term, 0, 0); + int value = vt_param_get(term, 1, -1); + + switch (resource) { + case 0: /* modifyKeyboard */ + break; + + case 1: /* modifyCursorKeys */ + case 2: /* modifyFunctionKeys */ + /* Ignored, we always report modifiers */ + if (value != 2 && value != -1) { + LOG_WARN( + "unimplemented: %s = %d", + resource == 1 ? "modifyCursorKeys" : + resource == 2 ? "modifyFunctionKeys" : "", + value); + } + break; + + case 4: /* modifyOtherKeys */ + term->modify_other_keys_2 = value == 2; + LOG_DBG("modifyOtherKeys=%d", value); + break; + + default: + LOG_WARN("XTMODKEYS: invalid resource '%d' in '%s'", + resource, csi_as_string(term, final, -1)); + break; + } + } + break; /* final == 'm' */ + + case 'n': { + int resource = vt_param_get(term, 0, 2); /* Default is modifyFunctionKeys */ + switch (resource) { + case 0: /* modifyKeyboard */ + case 1: /* modifyCursorKeys */ + case 2: /* modifyFunctionKeys */ + break; + + case 4: /* modifyOtherKeys */ + /* We don't support fully disabling modifyOtherKeys, + * but simply revert back to mode '1' */ + term->modify_other_keys_2 = false; + LOG_DBG("modifyOtherKeys=1"); + break; + } + break; + } + + case 'u': { + int flags = vt_param_get(term, 0, 0) & KITTY_KBD_SUPPORTED; + + struct grid *grid = term->grid; + uint8_t idx = grid->kitty_kbd.idx; + + if (idx + 1 >= ALEN(grid->kitty_kbd.flags)) { + /* Stack full, evict oldest by wrapping around */ + idx = 0; + } else + idx++; + + grid->kitty_kbd.flags[idx] = flags; + grid->kitty_kbd.idx = idx; + + LOG_DBG("kitty kbd: pushed new flags: 0x%03x", flags); + break; + } + + case 'q': { + /* XTVERSION */ + if (vt_param_get(term, 0, 0) != 0) { + UNHANDLED(); + break; + } + + char reply[64]; + size_t n = xsnprintf( + reply, sizeof(reply), "\033P>|foot(%u.%u.%u%s%s)\033\\", + FOOT_MAJOR, FOOT_MINOR, FOOT_PATCH, + FOOT_EXTRA[0] != '\0' ? "-" : "", FOOT_EXTRA); + term_to_slave(term, reply, n); + break; + } + + default: + UNHANDLED(); + break; + } + + break; /* private[0] == '>' */ + } + + case '<': { + switch (final) { + case 'u': { + int count = vt_param_get(term, 0, 1); + LOG_DBG("kitty kbd: popping %d levels of flags", count); + + struct grid *grid = term->grid; + uint8_t idx = grid->kitty_kbd.idx; + + for (int i = 0; i < count; i++) { + /* Reset flags. This ensures we get flags=0 when + * over-popping */ + grid->kitty_kbd.flags[idx] = 0; + + if (idx == 0) + idx = ALEN(grid->kitty_kbd.flags) - 1; + else + idx--; + } + + grid->kitty_kbd.idx = idx; + + LOG_DBG("kitty kbd: flags after pop: 0x%03x", + term->grid->kitty_kbd.flags[idx]); + break; + } + } + break; /* private[0] == '<' */ + } + + case ' ': { + switch (final) { + case 'q': { + int param = vt_param_get(term, 0, 0); + switch (param) { + case 0: /* blinking block, but we use it to reset to configured default */ + term->cursor_style = term->conf->cursor.style; + term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; + term_cursor_blink_update(term); + break; + + case 1: /* blinking block */ + case 2: /* steady block */ + term->cursor_style = term->conf->cursor.style == CURSOR_HOLLOW + ? CURSOR_HOLLOW : CURSOR_BLOCK; + break; + + case 3: /* blinking underline */ + case 4: /* steady underline */ + term->cursor_style = CURSOR_UNDERLINE; + break; + + case 5: /* blinking bar */ + case 6: /* steady bar */ + term->cursor_style = CURSOR_BEAM; + break; + + default: + UNHANDLED(); + break; + } + + if (param > 0 && param <= 6) { + term->cursor_blink.deccsusr = param & 1; + term_cursor_blink_update(term); + } + break; + } + + default: + UNHANDLED(); + break; + } + break; /* private[0] == ' ' */ + } + + case '!': { + if (final == 'p') { + term_reset(term, false); + break; + } + + UNHANDLED(); + break; /* private[0] == '!' */ + } + + case '=': { + switch (final) { + case 'c': + if (vt_param_get(term, 0, 0) != 0) { + UNHANDLED(); + break; + } + + /* + * Send Device Attributes (Tertiary DA) + * + * Reply format is "DCS ! | DDDDDDDD ST" + * + * D..D is the unit ID of the terminal, consisting of four + * hexadecimal pairs. The first pair represents the + * manufacturing site code. This code can be any + * hexadecimal value from 00 through FF. + */ + + term_to_slave(term, "\033P!|464f4f54\033\\", 14); /* FOOT */ + break; + + case 'u': { + int flag_set = vt_param_get(term, 0, 0) & KITTY_KBD_SUPPORTED; + int mode = vt_param_get(term, 1, 1); + + struct grid *grid = term->grid; + uint8_t idx = grid->kitty_kbd.idx; + + switch (mode) { + case 1: + /* set bits are set, unset bits are reset */ + grid->kitty_kbd.flags[idx] = flag_set; + break; + + case 2: + /* set bits are set, unset bits are left unchanged */ + grid->kitty_kbd.flags[idx] |= flag_set; + break; + + case 3: + /* set bits are reset, unset bits are left unchanged */ + grid->kitty_kbd.flags[idx] &= ~flag_set; + break; + + default: + UNHANDLED(); + break; + } + + LOG_DBG("kitty kbd: flags after update: 0x%03x", + grid->kitty_kbd.flags[idx]); + break; + } + + default: + UNHANDLED(); + break; + } + break; /* private[0] == '=' */ + } + + case '$': { + switch (final) { + case 'r': { /* DECCARA */ + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 0, &top, &left, &bottom, &right)) + { + break; + } + + for (int r = top; r <= bottom; r++) { + struct row *row = grid_row(term->grid, r); + row->dirty = true; + + for (int c = left; c <= right; c++) { + struct attributes *a = &row->cells[c].attrs; + a->clean = 0; + + for (size_t i = 4; i < term->vt.params.idx; i++) { + const int param = term->vt.params.v[i].value; + + /* DECCARA only supports a sub-set of SGR parameters */ + switch (param) { + case 0: + a->bold = false; + a->underline = false; + a->blink = false; + a->reverse = false; + break; + + case 1: a->bold = true; break; + case 4: a->underline = true; break; + case 5: a->blink = true; break; + case 7: a->reverse = true; break; + + case 22: a->bold = false; break; + case 24: a->underline = false; break; + case 25: a->blink = false; break; + case 27: a->reverse = false; break; + } + } + } + } + break; + } + + case 't': { /* DECRARA */ + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 0, &top, &left, &bottom, &right)) + { + break; + } + + for (int r = top; r <= bottom; r++) { + struct row *row = grid_row(term->grid, r); + row->dirty = true; + + for (int c = left; c <= right; c++) { + struct attributes *a = &row->cells[c].attrs; + a->clean = 0; + + for (size_t i = 4; i < term->vt.params.idx; i++) { + const int param = term->vt.params.v[i].value; + + /* DECRARA only supports a sub-set of SGR parameters */ + switch (param) { + case 0: + a->bold = !a->bold; + a->underline = !a->underline; + a->blink = !a->blink; + a->reverse = !a->reverse; + break; + + case 1: a->bold = !a->bold; break; + case 4: a->underline = !a->underline; break; + case 5: a->blink = !a->blink; break; + case 7: a->reverse = !a->reverse; break; + } + } + } + } + break; + } + + case 'v': { /* DECCRA */ + int src_top, src_left, src_bottom, src_right; + if (!params_to_rectangular_area( + term, 0, &src_top, &src_left, &src_bottom, &src_right)) + { + break; + } + + int src_page = vt_param_get(term, 4, 1); + + int dst_rel_top = vt_param_get(term, 5, 1) - 1; + int dst_left = vt_param_get(term, 6, 1) - 1; + int dst_page = vt_param_get(term, 7, 1); + + if (unlikely(src_page != 1 || dst_page != 1)) { + /* We don’t support “pages” */ + break; + } + + int dst_rel_bottom = dst_rel_top + (src_bottom - src_top); + int dst_right = min(dst_left + (src_right - src_left), term->cols - 1); + + int dst_top = term_row_rel_to_abs(term, dst_rel_top); + int dst_bottom = term_row_rel_to_abs(term, dst_rel_bottom); + + /* Target area outside the screen is clipped */ + const size_t row_count = min(src_bottom - src_top, + dst_bottom - dst_top) + 1; + const size_t cell_count = min(src_right - src_left, + dst_right - dst_left) + 1; + + sixel_overwrite_by_rectangle( + term, dst_top, dst_left, row_count, cell_count); + + /* + * Copy source area + * + * Note: since source and destination may overlap, we need + * to copy out the entire source region first, and _then_ + * write the destination. I.e. this is similar to how + * memmove() behaves, but adapted to our row/cell + * structure. + */ + struct cell **copy = xmalloc(row_count * sizeof(copy[0])); + for (int r = 0; r < row_count; r++) { + copy[r] = xmalloc(cell_count * sizeof(copy[r][0])); + + const struct row *row = grid_row(term->grid, src_top + r); + const struct cell *cell = &row->cells[src_left]; + memcpy(copy[r], cell, cell_count * sizeof(copy[r][0])); + } + + /* Paste into destination area */ + for (int r = 0; r < row_count; r++) { + struct row *row = grid_row(term->grid, dst_top + r); + row->dirty = true; + + struct cell *cell = &row->cells[dst_left]; + memcpy(cell, copy[r], cell_count * sizeof(copy[r][0])); + free(copy[r]); + + for (;cell < &row->cells[dst_left + cell_count]; cell++) + cell->attrs.clean = 0; + + if (unlikely(row->extra != NULL)) { + /* TODO: technically, we should copy the source URIs... */ + grid_row_uri_range_erase(row, dst_left, dst_right); + } + } + free(copy); + break; + } + + case 'x': { /* DECFRA */ + const uint8_t c = vt_param_get(term, 0, 0); + + if (unlikely(!((c >= 32 && c < 126) || c >= 160))) + break; + + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 1, &top, &left, &bottom, &right)) + { + break; + } + + /* Erase the entire region at once (MUCH cheaper than + * doing it row by row, or even character by + * character). */ + sixel_overwrite_by_rectangle( + term, top, left, bottom - top + 1, right - left + 1); + + for (int r = top; r <= bottom; r++) + term_fill(term, r, left, c, right - left + 1, true); + + break; + } + + case 'z': { /* DECERA */ + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 0, &top, &left, &bottom, &right)) + { + break; + } + + /* + * Note: term_erase() _also_ erases sixels, but since + * we’re forced to erase one row at a time, erasing the + * entire sixel here is more efficient. + */ + sixel_overwrite_by_rectangle( + term, top, left, bottom - top + 1, right - left + 1); + + for (int r = top; r <= bottom; r++) + term_erase(term, r, left, r, right); + break; + } + } + + break; /* private[0] == ‘$’ */ + } + + case '#': { + switch (final) { + case 'P': { /* XTPUSHCOLORS */ + int slot = vt_param_get(term, 0, 0); + + /* Pm == 0, "push" (what xterm does is take take the + *current* slot + 1, even if that's in the middle of the + stack, and overwrites whatever is already in that + slot) */ + if (slot == 0) + slot = term->color_stack.idx + 1; + + if (term->color_stack.size < slot) { + const size_t new_size = slot; + term->color_stack.stack = xrealloc( + term->color_stack.stack, + new_size * sizeof(term->color_stack.stack[0])); + + /* Initialize new slots (except the selected slot, + which is done below) */ + xassert(new_size > 0); + for (size_t i = term->color_stack.size; i < new_size - 1; i++) { + memcpy(&term->color_stack.stack[i], &term->colors, + sizeof(term->colors)); + } + term->color_stack.size = new_size; + } + + xassert(slot > 0); + xassert(slot <= term->color_stack.size); + term->color_stack.idx = slot; + memcpy(&term->color_stack.stack[slot - 1], &term->colors, + sizeof(term->colors)); + break; + } + + case 'Q': { /* XTPOPCOLORS */ + int slot = vt_param_get(term, 0, 0); + + /* Pm == 0, "pop" (what xterm does is copy colors from the + *current* slot, *and* decrease the current slot index, + even if that's in the middle of the stack) */ + if (slot == 0) + slot = term->color_stack.idx; + + if (slot > 0 && slot <= term->color_stack.size) { + memcpy(&term->colors, &term->color_stack.stack[slot - 1], + sizeof(term->colors)); + term->color_stack.idx = slot - 1; + + /* Assume a full palette switch *will* affect almost + all cells. The alternative is to call + term_damage_color() for all 256 palette entries + *and* the default fg/bg (256 + 2 calls in total) */ + term_damage_view(term); + term_damage_margins(term); + } else if (slot == 0) { + LOG_ERR("XTPOPCOLORS: cannot pop beyond the first element"); + } else { + LOG_ERR( + "XTPOPCOLORS: invalid color slot: %d " + "(stack has %zu slots, current slot is %zu)", + vt_param_get(term, 0, 0), + term->color_stack.size, term->color_stack.idx); + } + break; + } + + case 'R': { /* XTREPORTCOLORS */ + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[?%zu;%zu#Q", + term->color_stack.idx, term->color_stack.size); + term_to_slave(term, reply, n); + break; + } + } + break; /* private[0] == '#' */ + } + + case 0x243f: /* ?$ */ + switch (final) { + case 'p': { + unsigned param = vt_param_get(term, 0, 0); + + /* + * Request DEC private mode (DECRQM) + * Reply: + * 0 - not recognized + * 1 - set + * 2 - reset + * 3 - permanently set + * 4 - permantently reset + */ + unsigned status = decrqm(term, param); + char reply[32]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[?%u;%u$y", param, status); + term_to_slave(term, reply, n); + break; + + } + + default: + UNHANDLED(); + break; + } + + break; /* private[0] == '?' && private[1] == '$' */ + + default: + UNHANDLED(); + break; + } +} diff --git a/csi.h b/csi.h new file mode 100644 index 0000000..12ae67e --- /dev/null +++ b/csi.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include "terminal.h" + +void csi_dispatch(struct terminal *term, uint8_t final); diff --git a/cursor-shape.c b/cursor-shape.c new file mode 100644 index 0000000..e68411c --- /dev/null +++ b/cursor-shape.c @@ -0,0 +1,130 @@ +#include +#include + +#define LOG_MODULE "cursor-shape" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "cursor-shape.h" +#include "debug.h" +#include "util.h" + +const char *const * +cursor_shape_to_string(enum cursor_shape shape) +{ + static const char *const table[][CURSOR_SHAPE_COUNT]= { + [CURSOR_SHAPE_NONE] = {NULL}, + [CURSOR_SHAPE_HIDDEN] = {"hidden", NULL}, + [CURSOR_SHAPE_LEFT_PTR] = {"default", "left_ptr", NULL}, + [CURSOR_SHAPE_POINTER] = {"pointer", "hand1", NULL}, + [CURSOR_SHAPE_TEXT] = {"text", "xterm", NULL}, + [CURSOR_SHAPE_TOP_LEFT_CORNER] = {"nw-resize", "top_left_corner", NULL}, + [CURSOR_SHAPE_TOP_RIGHT_CORNER] = {"ne-resize", "top_right_corner", NULL}, + [CURSOR_SHAPE_BOTTOM_LEFT_CORNER] = {"sw-resize", "bottom_left_corner", NULL}, + [CURSOR_SHAPE_BOTTOM_RIGHT_CORNER] = {"se-resize", "bottom_right_corner", NULL}, + [CURSOR_SHAPE_LEFT_SIDE] = {"w-resize", "left_side", NULL}, + [CURSOR_SHAPE_RIGHT_SIDE] = {"e-resize", "right_side", NULL}, + [CURSOR_SHAPE_TOP_SIDE] = {"n-resize", "top_side", NULL}, + [CURSOR_SHAPE_BOTTOM_SIDE] = {"s-resize", "bottom_side", NULL}, + + }; + + xassert(shape <= ALEN(table)); + return table[shape]; +} + +enum wp_cursor_shape_device_v1_shape +cursor_shape_to_server_shape(enum cursor_shape shape) +{ + static const enum wp_cursor_shape_device_v1_shape table[CURSOR_SHAPE_COUNT] = { + [CURSOR_SHAPE_LEFT_PTR] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT, + [CURSOR_SHAPE_POINTER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER, + [CURSOR_SHAPE_TEXT] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT, + [CURSOR_SHAPE_TOP_LEFT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NW_RESIZE, + [CURSOR_SHAPE_TOP_RIGHT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NE_RESIZE, + [CURSOR_SHAPE_BOTTOM_LEFT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SW_RESIZE, + [CURSOR_SHAPE_BOTTOM_RIGHT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SE_RESIZE, + [CURSOR_SHAPE_LEFT_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_W_RESIZE, + [CURSOR_SHAPE_RIGHT_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_E_RESIZE, + [CURSOR_SHAPE_TOP_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_N_RESIZE, + [CURSOR_SHAPE_BOTTOM_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_S_RESIZE, + }; + + xassert(shape <= ALEN(table)); + xassert(table[shape] != 0); + return table[shape]; +} + +enum wp_cursor_shape_device_v1_shape +cursor_string_to_server_shape(const char *xcursor, int bound_version) +{ + if (xcursor == NULL) + return 0; + + static const char *const table[][2] = { + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT] = {"default", "left_ptr"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CONTEXT_MENU] = {"context-menu"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_HELP] = {"help", "question_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER] = {"pointer", "hand"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_PROGRESS] = {"progress", "left_ptr_watch"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_WAIT] = {"wait", "watch"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CELL] = {"cell"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR] = {"crosshair", "cross"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT] = {"text", "xterm"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_VERTICAL_TEXT] = {"vertical-text"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALIAS] = {"alias", "dnd-link"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_COPY] = {"copy", "dnd-copy"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE] = {"move", "dnd-move"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NO_DROP] = {"no-drop", "dnd-no-drop"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NOT_ALLOWED] = {"not-allowed", "crossed_circle"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRAB] = {"grab", "hand1"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRABBING] = {"grabbing"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_E_RESIZE] = {"e-resize", "right_side"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_N_RESIZE] = {"n-resize", "top_side"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NE_RESIZE] = {"ne-resize", "top_right_corner"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NW_RESIZE] = {"nw-resize", "top_left_corner"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_S_RESIZE] = {"s-resize", "bottom_side"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SE_RESIZE] = {"se-resize", "bottom_right_corner"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SW_RESIZE] = {"sw-resize", "bottom_left_corner"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_W_RESIZE] = {"w-resize", "left_side"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE] = {"ew-resize", "sb_h_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE] = {"ns-resize", "sb_v_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE] = {"nesw-resize", "fd_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE] = {"nwse-resize", "bd_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_COL_RESIZE] = {"col-resize", "sb_h_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ROW_RESIZE] = {"row-resize", "sb_v_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_SCROLL] = {"all-scroll", "fleur"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ZOOM_IN] = {"zoom-in"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ZOOM_OUT] = {"zoom-out"}, +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION) /* 1.42 */ + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK] = {"dnd-ask"}, +#endif +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE_SINCE_VERSION) /* 1.42 */ + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE] = {"all-resize"}, +#endif + }; + + for (size_t i = 0; i < ALEN(table); i++) { +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION) + if (i == WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK && + bound_version < WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION) + { + continue; + } +#endif +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE_SINCE_VERSION) + if (i == WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE && + bound_version < WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE_SINCE_VERSION) + { + continue; + } +#endif + for (size_t j = 0; j < ALEN(table[i]); j++) { + if (table[i][j] != NULL && streq(xcursor, table[i][j])) { + return i; + } + } + } + + return 0; +} diff --git a/cursor-shape.h b/cursor-shape.h new file mode 100644 index 0000000..51a411e --- /dev/null +++ b/cursor-shape.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +enum cursor_shape { + CURSOR_SHAPE_NONE, + CURSOR_SHAPE_CUSTOM, + CURSOR_SHAPE_HIDDEN, + + CURSOR_SHAPE_LEFT_PTR, + CURSOR_SHAPE_POINTER, + CURSOR_SHAPE_TEXT, + CURSOR_SHAPE_TOP_LEFT_CORNER, + CURSOR_SHAPE_TOP_RIGHT_CORNER, + CURSOR_SHAPE_BOTTOM_LEFT_CORNER, + CURSOR_SHAPE_BOTTOM_RIGHT_CORNER, + CURSOR_SHAPE_LEFT_SIDE, + CURSOR_SHAPE_RIGHT_SIDE, + CURSOR_SHAPE_TOP_SIDE, + CURSOR_SHAPE_BOTTOM_SIDE, + + CURSOR_SHAPE_COUNT, +}; + +const char *const *cursor_shape_to_string(enum cursor_shape shape); + +enum wp_cursor_shape_device_v1_shape cursor_shape_to_server_shape( + enum cursor_shape shape); +enum wp_cursor_shape_device_v1_shape cursor_string_to_server_shape( + const char *xcursor, int bound_version); diff --git a/dcs.c b/dcs.c new file mode 100644 index 0000000..376c73b --- /dev/null +++ b/dcs.c @@ -0,0 +1,533 @@ +#include "dcs.h" +#include + +#define LOG_MODULE "dcs" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "foot-terminfo.h" +#include "sixel.h" +#include "util.h" +#include "vt.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +static bool +ensure_size(struct terminal *term, size_t required_size) +{ + if (required_size <= term->vt.dcs.size) + return true; + + uint8_t *new_data = realloc(term->vt.dcs.data, required_size); + if (new_data == NULL) { + LOG_ERRNO("failed to increase size of DCS buffer"); + return false; + } + + term->vt.dcs.data = new_data; + term->vt.dcs.size = required_size; + return true; +} + +/* Decode hex-encoded string *inline*. NULL terminates */ +static char * +hex_decode(const char *s, size_t len) +{ + if (len % 2) + return NULL; + + char *hex = xmalloc(len / 2 + 1); + char *o = hex; + + /* TODO: error checking */ + for (size_t i = 0; i < len; i += 2) { + uint8_t nib1 = hex2nibble(*s); s++; + uint8_t nib2 = hex2nibble(*s); s++; + + if (nib1 == HEX_DIGIT_INVALID || nib2 == HEX_DIGIT_INVALID) + goto err; + + *o = nib1 << 4 | nib2; o++; + } + + *o = '\0'; + return hex; + +err: + free(hex); + return NULL; +} + +UNITTEST +{ + /* Verify table is sorted */ + const char *p = terminfo_capabilities; + size_t left = sizeof(terminfo_capabilities); + + const char *last_cap = NULL; + + while (left > 0) { + const char *cap = p; + const char *val = cap + strlen(cap) + 1; + + size_t size = strlen(cap) + 1 + strlen(val) + 1;; + xassert(size <= left); + p += size; + left -= size; + + if (last_cap != NULL) + xassert(strcmp(last_cap, cap) < 0); + + last_cap = cap; + } +} + +static bool +lookup_capability(const char *name, const char **value) +{ + const char *p = terminfo_capabilities; + size_t left = sizeof(terminfo_capabilities); + + while (left > 0) { + const char *cap = p; + const char *val = cap + strlen(cap) + 1; + + size_t size = strlen(cap) + 1 + strlen(val) + 1;; + xassert(size <= left); + p += size; + left -= size; + + int r = strcmp(cap, name); + if (r == 0) { + *value = val; + return true; + } else if (r > 0) + break; + } + + *value = NULL; + return false; +} + +static void +xtgettcap_reply(struct terminal *term, const char *hex_cap_name, size_t len) +{ + char *name = hex_decode(hex_cap_name, len); + if (name == NULL) { + LOG_WARN("XTGETTCAP: invalid hex encoding, ignoring capability"); + return; + } + + const char *value; + bool valid_capability = lookup_capability(name, &value); + xassert(!valid_capability || value != NULL); + + LOG_DBG("XTGETTCAP: cap=%s (%.*s), value=%s", + name, (int)len, hex_cap_name, + valid_capability ? value : ""); + + if (!valid_capability) + goto err; + + if (value[0] == '\0') { + /* Boolean */ + term_to_slave(term, "\033P1+r", 5); + term_to_slave(term, hex_cap_name, len); + term_to_slave(term, "\033\\", 2); + goto out; + } + + /* + * Reply format: + * \EP 1 + r cap=value \E\\ + * Where 'cap' and 'value are hex encoded ascii strings + */ + char *reply = xmalloc( + 5 + /* DCS 1 + r (\EP1+r) */ + len + /* capability name, hex encoded */ + 1 + /* '=' */ + strlen(value) * 2 + /* capability value, hex encoded */ + 2 + /* ST (\E\\) */ + 1); + + int idx = sprintf(reply, "\033P1+r%.*s=", (int)len, hex_cap_name); + + for (const char *c = value; *c != '\0'; c++) { + uint8_t nib1 = (uint8_t)*c >> 4; + uint8_t nib2 = (uint8_t)*c & 0xf; + + reply[idx] = nib1 >= 0xa ? 'A' + nib1 - 0xa : '0' + nib1; idx++; + reply[idx] = nib2 >= 0xa ? 'A' + nib2 - 0xa : '0' + nib2; idx++; + } + + reply[idx] = '\033'; idx++; + reply[idx] = '\\'; idx++; + term_to_slave(term, reply, idx); + + free(reply); + goto out; + +err: + term_to_slave(term, "\033P0+r", 5); + term_to_slave(term, hex_cap_name, len); + term_to_slave(term, "\033\\", 2); + +out: + free(name); +} + +static void +xtgettcap_put(struct terminal *term, uint8_t c) +{ + struct vt *vt = &term->vt; + + /* Grow buffer expontentially */ + if (vt->dcs.idx >= vt->dcs.size) { + size_t new_size = vt->dcs.size * 2; + if (new_size == 0) + new_size = 128; + + if (!ensure_size(term, new_size)) + return; + } + + vt->dcs.data[vt->dcs.idx++] = c; +} + +static void +xtgettcap_unhook(struct terminal *term) +{ + size_t left = term->vt.dcs.idx; + + const char *const end = (const char *)&term->vt.dcs.data[left]; + const char *p = (const char *)term->vt.dcs.data; + + if (p == NULL) { + /* Request is empty; send an error reply, without any capabilities */ + term_to_slave(term, "\033P0+r\033\\", 7); + return; + } + + while (true) { + const char *sep = memchr(p, ';', left); + size_t cap_len; + + if (sep == NULL) { + /* Last capability */ + cap_len = end - p; + } else { + cap_len = sep - p; + } + + xtgettcap_reply(term, p, cap_len); + + left -= cap_len + 1; + p += cap_len + 1; + + if (sep == NULL) + break; + } +} + +static void NOINLINE +append_sgr_attr_n(char **reply, size_t *len, const char *attr, size_t n) +{ + size_t new_len = *len + n + 1; + *reply = xrealloc(*reply, new_len); + memcpy(&(*reply)[*len], attr, n); + (*reply)[new_len - 1] = ';'; + *len = new_len; +} + +static void +decrqss_put(struct terminal *term, uint8_t c) +{ + /* Largest request we support is two bytes */ + if (!ensure_size(term, 2)) + return; + + struct vt *vt = &term->vt; + if (vt->dcs.idx >= 2) + return; + vt->dcs.data[vt->dcs.idx++] = c; +} + +static void +decrqss_unhook(struct terminal *term) +{ + const uint8_t *query = term->vt.dcs.data; + const size_t n = term->vt.dcs.idx; + + /* + * A note on the Ps parameter in the reply: many DEC manual + * instances (e.g. https://vt100.net/docs/vt510-rm/DECRPSS) claim + * that 0 means "request is valid", and 1 means "request is + * invalid". + * + * However, this appears to be a typo; actual hardware inverts the + * response (as does XTerm and mlterm): + * https://github.com/hackerb9/vt340test/issues/13 + */ + + if (n == 1 && query[0] == 'r') { + /* DECSTBM - Set Top and Bottom Margins */ + char reply[64]; + size_t len = xsnprintf(reply, sizeof(reply), "\033P1$r%d;%dr\033\\", + term->scroll_region.start + 1, + term->scroll_region.end); + term_to_slave(term, reply, len); + } + + else if (n == 1 && query[0] == 'm') { + /* SGR - Set Graphic Rendition */ + char *reply = NULL; + size_t len = 0; + + #define append_sgr_attr(num_as_str) \ + append_sgr_attr_n(&reply, &len, num_as_str, sizeof(num_as_str) - 1) + + /* Always present, both in the example from the VT510 manual + * (https://vt100.net/docs/vt510-rm/DECRPSS), and in XTerm and + * mlterm */ + append_sgr_attr("0"); + + struct attributes *a = &term->vt.attrs; + if (a->bold) + append_sgr_attr("1"); + if (a->dim) + append_sgr_attr("2"); + if (a->italic) + append_sgr_attr("3"); + if (a->underline) { + if (term->vt.underline.style > UNDERLINE_SINGLE) { + char value[4]; + size_t val_len = + xsnprintf(value, sizeof(value), "4:%d", term->vt.underline.style); + append_sgr_attr_n(&reply, &len, value, val_len); + } else + append_sgr_attr("4"); + } + if (a->blink) + append_sgr_attr("5"); + if (a->reverse) + append_sgr_attr("7"); + if (a->conceal) + append_sgr_attr("8"); + if (a->strikethrough) + append_sgr_attr("9"); + + switch (a->fg_src) { + case COLOR_DEFAULT: + break; + + case COLOR_BASE16: { + char value[4]; + size_t val_len = xsnprintf( + value, sizeof(value), "%u", + a->fg >= 8 ? a->fg - 8 + 90 : a->fg + 30); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + + case COLOR_BASE256: { + char value[16]; + size_t val_len = xsnprintf(value, sizeof(value), "38:5:%u", a->fg); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + + case COLOR_RGB: { + uint8_t r = a->fg >> 16; + uint8_t g = a->fg >> 8; + uint8_t b = a->fg >> 0; + + char value[32]; + size_t val_len = xsnprintf( + value, sizeof(value), "38:2::%hhu:%hhu:%hhu", r, g, b); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + } + + switch (a->bg_src) { + case COLOR_DEFAULT: + break; + + case COLOR_BASE16: { + char value[4]; + size_t val_len = xsnprintf( + value, sizeof(value), "%u", + a->bg >= 8 ? a->bg - 8 + 100 : a->bg + 40); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + + case COLOR_BASE256: { + char value[16]; + size_t val_len = xsnprintf(value, sizeof(value), "48:5:%u", a->bg); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + + case COLOR_RGB: { + uint8_t r = a->bg >> 16; + uint8_t g = a->bg >> 8; + uint8_t b = a->bg >> 0; + + char value[32]; + size_t val_len = xsnprintf( + value, sizeof(value), "48:2::%hhu:%hhu:%hhu", r, g, b); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + } + + switch (term->vt.underline.color_src) { + case COLOR_DEFAULT: + case COLOR_BASE16: + break; + + case COLOR_BASE256: { + char value[16]; + size_t val_len = xsnprintf( + value, sizeof(value), "58:5:%u", term->vt.underline.color); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + + case COLOR_RGB: { + uint8_t r = term->vt.underline.color >> 16; + uint8_t g = term->vt.underline.color >> 8; + uint8_t b = term->vt.underline.color >> 0; + + char value[32]; + size_t val_len = xsnprintf( + value, sizeof(value), "58:2::%hhu:%hhu:%hhu", r, g, b); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + } + + #undef append_sgr_attr_n + + reply[len - 1] = 'm'; + + term_to_slave(term, "\033P1$r", 5); + term_to_slave(term, reply, len); + term_to_slave(term, "\033\\", 2); + free(reply); + } + + else if (n == 2 && memcmp(query, " q", 2) == 0) { + /* DECSCUSR - Set Cursor Style */ + int mode; + + switch (term->cursor_style) { + case CURSOR_HOLLOW: /* FALLTHROUGH */ + case CURSOR_BLOCK: mode = 2; break; + case CURSOR_UNDERLINE: mode = 4; break; + case CURSOR_BEAM: mode = 6; break; + default: BUG("invalid cursor style"); break; + } + + if (term->cursor_blink.deccsusr) + mode--; + + char reply[16]; + size_t len = xsnprintf(reply, sizeof(reply), "\033P1$r%d q\033\\", mode); + term_to_slave(term, reply, len); + } + + else { + static const char err[] = "\033P0$r\033\\"; + term_to_slave(term, err, sizeof(err) - 1); + } +} + +void +dcs_hook(struct terminal *term, uint8_t final) +{ + LOG_DBG("hook: %c (intermediate(s): %.2s, param=%d)", final, + (const char *)&term->vt.private, vt_param_get(term, 0, 0)); + + xassert(term->vt.dcs.data == NULL); + xassert(term->vt.dcs.size == 0); + xassert(term->vt.dcs.put_handler == NULL); + xassert(term->vt.dcs.unhook_handler == NULL); + + switch (term->vt.private) { + case 0: + switch (final) { + case 'q': { + if (!term->conf->tweak.sixel) { + break; + } + int p1 = vt_param_get(term, 0, 0); + int p2 = vt_param_get(term, 1, 0); + int p3 = vt_param_get(term, 2, 0); + + term->vt.dcs.put_handler = sixel_init(term, p1, p2, p3); + term->vt.dcs.unhook_handler = &sixel_unhook; + break; + } + } + break; + + case '$': + switch (final) { + case 'q': + term->vt.dcs.put_handler = &decrqss_put; + term->vt.dcs.unhook_handler = &decrqss_unhook; + break; + } + break; + + case '=': + switch (final) { + case 's': + /* BSU/ESU: https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec */ + switch (vt_param_get(term, 0, 0)) { + case 1: + term->vt.dcs.unhook_handler = &term_enable_app_sync_updates; + return; + case 2: + term->vt.dcs.unhook_handler = &term_disable_app_sync_updates; + return; + } + break; + } + break; + + case '+': + switch (final) { + case 'q': /* XTGETTCAP */ + term->vt.dcs.put_handler = &xtgettcap_put; + term->vt.dcs.unhook_handler = &xtgettcap_unhook; + break; + } + break; + } +} + +void +dcs_put(struct terminal *term, uint8_t c) +{ + /* LOG_DBG("PUT: %c", c); */ + + if (term->vt.dcs.put_handler != NULL) + term->vt.dcs.put_handler(term, c); +} + +void +dcs_unhook(struct terminal *term) +{ + if (term->vt.dcs.unhook_handler != NULL) + term->vt.dcs.unhook_handler(term); + + term->vt.dcs.unhook_handler = NULL; + term->vt.dcs.put_handler = NULL; + + free(term->vt.dcs.data); + term->vt.dcs.data = NULL; + term->vt.dcs.size = 0; + term->vt.dcs.idx = 0; +} diff --git a/dcs.h b/dcs.h new file mode 100644 index 0000000..f89de38 --- /dev/null +++ b/dcs.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include "terminal.h" + +void dcs_hook(struct terminal *term, uint8_t final); +void dcs_put(struct terminal *term, uint8_t c); +void dcs_unhook(struct terminal *term); diff --git a/debug.c b/debug.c new file mode 100644 index 0000000..8a86e9d --- /dev/null +++ b/debug.c @@ -0,0 +1,47 @@ +#include "debug.h" + +#include +#include +#include +#include +#include +#include "log.h" + +#if defined(__SANITIZE_ADDRESS__) || HAS_FEATURE(address_sanitizer) +#include +#define ASAN_ENABLED 1 +#endif + +static void +print_stack_trace(void) +{ +#ifdef ASAN_ENABLED + fputs("\nStack trace:\n", stderr); + __sanitizer_print_stack_trace(); +#endif +} + +noreturn void +fatal_error(const char *file, int line, const char *msg, int err) +{ + log_msg(LOG_CLASS_ERROR, "debug", file, line, "%s: %s", msg, strerror(err)); + print_stack_trace(); + fflush(stderr); + abort(); +} + +noreturn void +bug(const char *file, int line, const char *func, const char *fmt, ...) +{ + char buf[4096]; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + + const char *msg = likely(n >= 0) ? buf : "??"; + log_msg(LOG_CLASS_ERROR, "debug", file, line, "BUG in %s(): %s", func, msg); + print_stack_trace(); + fflush(stderr); + abort(); +} diff --git a/debug.h b/debug.h new file mode 100644 index 0000000..92a0e09 --- /dev/null +++ b/debug.h @@ -0,0 +1,32 @@ +#pragma once + +#include "macros.h" + +#define FATAL_ERROR(...) fatal_error(__FILE__, __LINE__, __VA_ARGS__) + +#ifdef NDEBUG + #define BUG(...) UNREACHABLE() +#else + #define BUG(...) bug(__FILE__, __LINE__, __func__, __VA_ARGS__) +#endif + +#define xassert(x) do { \ + IGNORE_WARNING("-Wtautological-compare") \ + if (unlikely(!(x))) { \ + BUG("assertion failed: '%s'", #x); \ + } \ + UNIGNORE_WARNINGS \ +} while (0) + +#ifndef static_assert + #if __STDC_VERSION__ >= 201112L + #define static_assert(x, msg) _Static_assert((x), msg) + #elif GNUC_AT_LEAST(4, 6) || HAS_EXTENSION(c_static_assert) + #define static_assert(x, msg) __extension__ _Static_assert((x), msg) + #else + #define static_assert(x, msg) + #endif +#endif + +noreturn void fatal_error(const char *file, int line, const char *msg, int err) COLD; +noreturn void bug(const char *file, int line, const char *func, const char *fmt, ...) PRINTF(4) COLD; diff --git a/doc/benchmark.md b/doc/benchmark.md new file mode 100644 index 0000000..f77b826 --- /dev/null +++ b/doc/benchmark.md @@ -0,0 +1,81 @@ +# Benchmarks + +## vtebench + +All benchmarks are done using [vtebench](https://github.com/alacritty/vtebench): + +```sh +./target/release/vtebench -b ./benchmarks --dat /tmp/ +``` + +## 2022-05-12 + +### System + +CPU: i9-9900 + +RAM: 64GB + +Graphics: Radeon RX 5500XT + + +### Terminal configuration + +Geometry: 2040x1884 + +Font: Fantasque Sans Mono 10.00pt/23px + +Scrollback: 10000 lines + + +### Results + +| Benchmark (times in ms) | Foot (GCC+PGO) 1.12.1 | Foot 1.12.1 | Alacritty 0.10.1 | URxvt 9.26 | XTerm 372 | +|-------------------------------|----------------------:|------------:|-----------------:|-----------:|----------:| +| cursor motion | 10.40 | 14.07 | 24.97 | 23.38 | 1622.86 | +| dense cells | 29.58 | 45.46 | 97.45 | 10828.00 | 2323.00 | +| light cells | 4.34 | 4.40 | 12.84 | 12.17 | 49.81 | +| scrollling | 135.31 | 116.35 | 121.69 | 108.30 | 4041.33 | +| scrolling bottom region | 118.19 | 109.70 | 105.26 | 118.80 | 3875.00 | +| scrolling bottom small region | 132.41 | 122.11 | 122.83 | 151.30 | 3839.67 | +| scrolling fullscreen | 5.70 | 5.66 | 10.92 | 12.09 | 124.25 | +| scrolling top region | 144.19 | 121.78 | 135.81 | 159.24 | 3858.33 | +| scrolling top small region | 135.95 | 119.01 | 115.46 | 216.55 | 3872.67 | +| unicode | 11.56 | 10.92 | 15.94 | 1012.27 | 4779.33 | + + +## 2022-05-12 + +### System + +CPU: i5-8250U + +RAM: 8GB + +Graphics: Intel UHD Graphics 620 + + +### Terminal configuration + +Geometry: 945x1020 + +Font: Dina:pixelsize=12 + +Scrollback=10000 lines + + +### Results + + +| Benchmark (times in ms) | Foot (GCC+PGO) 1.12.1 | Foot 1.12.1 | Alacritty 0.10.1 | URxvt 9.26 | XTerm 372 | +|-------------------------------|----------------------:|------------:|-----------------:|-----------:|----------:| +| cursor motion | 15.03 | 16.74 | 23.22 | 24.14 | 1381.63 | +| dense cells | 43.56 | 54.10 | 89.43 | 1807.17 | 1945.50 | +| light cells | 7.96 | 9.66 | 20.19 | 21.31 | 122.44 | +| scrollling | 146.02 | 150.47 | 129.22 | 129.84 | 10140.00 | +| scrolling bottom region | 138.36 | 137.42 | 117.06 | 141.87 | 10136.00 | +| scrolling bottom small region | 137.40 | 134.66 | 128.97 | 208.77 | 9930.00 | +| scrolling fullscreen | 11.66 | 12.02 | 19.69 | 21.96 | 315.80 | +| scrolling top region | 143.81 | 133.47 | 132.51 | 475.81 | 10267.00 | +| scrolling top small region | 133.72 | 135.32 | 145.10 | 314.13 | 10074.00 | +| unicode | 20.89 | 21.78 | 26.11 | 5687.00 | 15740.00 | diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd new file mode 100644 index 0000000..40906eb --- /dev/null +++ b/doc/foot-ctlseqs.7.scd @@ -0,0 +1,813 @@ +foot-ctlseqs(7) + +# NAME +foot-ctlseqs - terminal control sequences supported by foot + +# DESCRIPTION + +This document describes all the control sequences supported by foot. + +- Control characters +- Sequences beginning with ESC +- CSI - Control Sequence Introducer + - SGR + - Indexed and RGB colors (256-color palette and 24-bit colors) + - Private modes + - Window manipulation + - Other +- OSC - Operating System Command +- DCS - Device Control String + +# Control characters + +[[ *Sequence* +:[ *Name* +:< *Description* +| \\a +: BEL +: Depends on what *bell* in *foot.ini*(5) is set to. +| \\b +: BS +: Backspace; move the cursor left one step. Wrap if _bw_ is enabled. +| \\t +: HT +: Horizontal tab; move the cursor to the next tab stop. +| \\n +: LF +: Line feed; move the cursor down one step, or scroll content up if + at the bottom line. +| \\v +: VT +: Vertical tab; identical to _LF_. +| \\f +: FF +: Form feed; identical to _LF_. +| \\r +: CR +: Carriage ret; move the cursor to the leftmost column. +| \\x0E +: SO +: Shift out; select the _G1_ character set. +| \\x0F +: SI +: Shift in; select the _G0_ character set. + +# Sequences beginning with ESC + +Note: this table excludes sequences where ESC is part of a 7-bit +equivalent to 8-bit C1 controls. + +[[ *Sequence* +:[ *Name* +:[ *Origin* +:< *Description* +| \\E 7 +: DECSC +: VT100 +: Save cursor position. +| \\E 8 +: DECRC +: VT100 +: Restore cursor position. +| \\E c +: RIS +: VT100 +: Reset terminal to initial state. +| \\E D +: IND +: VT100 +: Line feed; move the cursor down one step, or scroll content up if + at the bottom margin. +| \\E E +: NEL +: VT100 +: Next line; move the cursor down one step, and to the first + column. Content is scrolled up if at the bottom line. +| \\E H +: HTS +: VT100 +: Set one horizontal tab stop at the current position. +| \\E M +: RI +: VT100 +: Reverse index; move the cursor up one step, or scroll content down + if at the top margin. +| \\E N +: SS2 +: VT220 +: Single shift select of G2 character set (affects next character only). +| \\E O +: SS3 +: VT220 +: Single shift select of G3 character set (affects next character only). +| \\E = +: DECKPAM +: VT100 +: Switch keypad to _application_ mode. +| \\E > +: DECKPNM +: VT100 +: Switch keypad to _numeric_ mode. +| \\E ( _C_ +: SCS +: VT100 +: Designate G0 character set. Supported values for _C_ are: *0* (DEC + Special Character and Line Drawing Set), and *B* (USASCII). +| \\E ) _C_ +: SCS +: VT100 +: Designate G1 character set. Same supported values for _C_ as in _G0_. +| \\E \* _C_ +: SCS +: VT220 +: Designate G2 character set. Same supported values for _C_ as in _G0_. +| \\E + _C_ +: SCS +: VT220 +: Designate G3 character set. Same supported values for _C_ as in _G0_. + +# CSI + +All sequences begin with *\\E[*, sometimes abbreviated "CSI". Spaces +are used in the sequence strings to make them easier to read, but are +not actually part of the string (i.e. *\\E[ 1 m* is really *\\E[1m*). + +## SGR + +All SGR sequences are in the form *\\E[* _N_ *m*, where _N_ is a decimal +number - the _parameter_. Multiple parameters can be combined in a +single CSI sequence by separating them with semicolons: *\\E[ 1;2;3 +m*. + +[[ *Parameter* +:< *Description* +| 0 +: Reset all attributes +| 1 +: Bold +| 2 +: Dim +| 3 +: Italic +| 4 +: Underline, including styled underlines +| 5 +: Blink +| 7 +: Reverse video; swap foreground and background colors +| 8 +: Conceal; text is not visible, but is copiable +| 9 +: Crossed-out/strike +| 21 +: Double underline +| 22 +: Disable *bold* and *dim* +| 23 +: Disable italic +| 24 +: Disable underline +| 25 +: Disable blink +| 27 +: Disable reverse video +| 28 +: Disable conceal +| 29 +: Disable crossed-out +| 30-37 +: Select foreground color (using *regularN* in *foot.ini*(5)) +| 38 +: Select foreground color, see "indexed and RGB colors" below +| 39 +: Use the default foreground color (*foreground* in *foot.ini*(5)) +| 40-47 +: Select background color (using *regularN* in *foot.ini*(5)) +| 48 +: Select background color, see "indexed and RGB colors" below +| 49 +: Use the default background color (*background* in *foot.ini*(5)) +| 58 +: Select underline color, see "indexed and RGB colors" below +| 59 +: Use the default underline color +| 90-97 +: Select foreground color (using *brightN* in *foot.ini*(5)) +| 100-107 +: Select background color (using *brightN* in *foot.ini*(5)) + +## Indexed and RGB colors (256-color palette and 24-bit colors) + +Foot supports both the new sub-parameter based variants, and the older +parameter based variants for setting foreground and background colors. + +Indexed colors: + +- *\\E[ 38 : 5 :* _idx_ *m* +- *\\E[ 38 ; 5 ;* _idx_ *m* + +RGB colors: + +- *\\E[ 38 : 2 :* _cs_ *:* _r_ *:* _g_ *:* _b_ *m* +- *\\E[ 38 : 2 :* _r_ *:* _g_ *:* _b_ *m* +- *\\E[ 38 ; 2 ;* _r_ *;* _g_ *;* _b_ *m* + +The first variant is the "correct" one (and foot also recognizes, but +ignores, the optional _tolerance_ parameters). + +The second one is allowed since many programs "forget" the color space +ID, _cs_. + +The sub-parameter based variants are preferred, and are what foot's +*terminfo*(5) entry uses. + +## Private Modes + +There are several Boolean-like "modes" that affect certain aspects +of the terminal's behavior. These modes can be manipulated with the +following 4 escape sequences: + +[[ *Sequence* +:[ *Name* +:< *Description* +| \\E[ ? _Pm_ h +: DECSET +: Enable private mode +| \\E[ ? _Pm_ l +: DECRST +: Disable private mode +| \\E[ ? _Pm_ s +: XTSAVE +: Save private mode +| \\E[ ? _Pm_ r +: XTRESTORE +: Restore private mode + + +The _Pm_ parameter in the above sequences denotes a numerical ID +that corresponds to one of the following modes: + +[[ *Parameter* +:[ *Origin* +:< *Description* +| 1 +: VT100 +: Cursor keys mode (DECCKM) +| 5 +: VT100 +: Reverse video (DECSCNM) +| 6 +: VT100 +: Origin mode (DECOM) +| 7 +: VT100 +: Auto-wrap mode (DECAWM) +| 12 +: AT&T 610 +: Cursor blink +| 25 +: VT220 +: Cursor visibility (DECTCEM) +| 45 +: xterm +: Reverse-wraparound mode +| 47 +: xterm +: Same as 1047 (see below) +| 66 +: VT320 +: Numeric keypad mode (DECNKM); same as DECKPAM/DECKPNM when enabled/disabled +| 1000 +: xterm +: Send mouse x/y on button press/release +| 1001 +: xterm +: Use hilite mouse tracking +| 1002 +: xterm +: Use cell motion mouse tracking +| 1003 +: xterm +: Use all motion mouse tracking +| 1004 +: xterm +: Send FocusIn/FocusOut events +| 1006 +: xterm +: SGR mouse mode +| 1007 +: xterm +: Alternate scroll mode +| 1015 +: urxvt +: urxvt mouse mode +| 1016 +: xterm +: SGR-Pixels mouse mode +| 1034 +: xterm +: 8-bit Meta mode +| 1035 +: xterm +: Num Lock modifier (see xterm numLock option) +| 1036 +: xterm +: Send ESC when Meta modifies a key (see xterm metaSendsEscape option) +| 1042 +: xterm +: Perform action for BEL character (see *bell* in *foot.ini*(5)) +| 1047 +: xterm +: Use alternate screen buffer +| 1048 +: xterm +: Save/restore cursor (DECSET=save, DECRST=restore) +| 1049 +: xterm +: Equivalent to 1048 and 1047 combined +| 1070 +: xterm +: Use private color registers for each sixel +| 2004 +: xterm +: Wrap pasted text with start/end delimiters (bracketed paste mode) +| 2026 +: terminal-wg +: Application synchronized updates mode +| 2027 +: contour +: Grapheme cluster processing +| 2031 +: contour +: Request color theme updates +| 2048 +: TODO +: In-band window resize notifications +| 8452 +: xterm +: Position cursor to the right of sixels, instead of on the next line +| 737769 +: foot +: Input Method Editor (IME) mode + +## Window manipulation + +Foot implements a sub-set of XTerm's (originally dtterm's) window +manipulation sequences. The generic format is: + +*\\E[ *_Ps_* ; *_Ps_* ; *_Ps_* t* + +[[ *Parameter 1* +:[ *Parameter 2* +:< *Description* +| 11 +: - +: Report if window is iconified. Foot always reports *1* - not iconified. +| 13 +: - +: Report window position. Foot always reports (0,0), due to Wayland + limitations. +| 13 +: 2 +: Report text area position. Foot always reports (0,0) due to Wayland + limitations. +| 14 +: - +: Report text area size, in pixels. Foot reports the grid size, + excluding the margins. +| 14 +: 2 +: Report window size, in pixels. Foot reports the grid size plus the + margins. +| 15 +: - +: Report the screen size, in pixels. +| 16 +: - +: Report the cell size, in pixels. +| 18 +: - +: Report text area size, in characters. +| 19 +: - +: Report screen size, in characters. +| 20 +: - +: Report icon label. +| 22 +: - +: Push window title+icon. +| 22 +: 1 +: Push window icon. +| 22 +: 2 +: Push window title. +| 23 +: - +: Pop window title+icon. +| 23 +: 1 +: Pop window icon. +| 23 +: 2 +: Pop window title. + +## Other + +[[ *Parameter* +:[ *Name* +:[ *Origin* +:< *Description* +| \\E[ _Ps_ c +: DA +: VT100 +: Send primary device attributes. Foot responds with "I'm a VT220 with + sixel and ANSI color support". +| \\E[ _Ps_ A +: CUU +: VT100 +: Cursor up - move cursor up _Ps_ times. +| \\E[ _Ps_ B +: CUD +: VT100 +: Cursor down - move cursor down _Ps_ times. +| \\E[ _Ps_ C +: CUF +: VT100 +: Cursor forward - move cursor to the right _Ps_ times. +| \\E[ _Ps_ D +: CUB +: VT100 +: Cursor backward - move cursor to the left _Ps_ times. +| \\E[ _Ps_ g +: TBC +: VT100 +: Tab clear. _Ps_=0 -> clear current column. _Ps_=3 -> clear all. +| \\E[ _Ps_ ; _Ps_ f +: HVP +: VT100 +: Horizontal and vertical position - move cursor to _row_ ; _column_. +| \\E[ _Ps_ ; _Ps_ H +: CUP +: VT100 +: Cursor position - move cursor to _row_ ; _column_. +| \\E[ _Ps_ J +: ED +: VT100 +: Erase in display. _Ps_=0 -> below cursor. _Ps_=1 -> above +| \\E[ _Ps_ K +: EL +: VT100 +: Erase in line. _Ps_=0 -> right of cursor. _Ps_=1 -> left of + cursor. _Ps_=2 -> all. +| \\E[ _Pm_ h +: SM +: VT100 +: Set mode. _Pm_=4 -> enable IRM (Insertion Replacement Mode). All + other values of _Pm_ are unsupported. +| \\E[ _Pm_ l +: RM +: VT100 +: Reset mode. _Pm_=4 -> disable IRM (Insertion Replacement Mode). All + other values of _Pm_ are unsupported. +| \\E[ _Ps_ n +: DSR +: VT100 +: Device status report. _Ps_=5 -> device status. _Ps_=6 -> cursor + position. +| \\E[ _Ps_ L +: IL +: VT220 +: Insert _Ps_ lines. +| \\E[ _Ps_ M +: DL +: VT220 +: Delete _Ps_ lines. +| \\E[ _Ps_ P +: DCH +: VT220 +: Delete _Ps_ characters. +| \\E[ _Ps_ @ +: ICH +: VT220 +: Insert _Ps_ blank characters. +| \\E[ _Ps_ X +: ECH +: VT220 +: Erase _Ps_ characters. +| \\E[ > c +: DA2 +: VT220 +: Send secondary device attributes. Foot responds with "I'm a VT220 + and here's my version number". +| \\E[ ! p +: DECSTR +: VT220 +: Soft terminal reset. +| \\E[ ? _Ps_ $ p +: DECRQM +: VT320 +: Request status of DEC private mode. The _Ps_ parameter corresponds + to one of the values mentioned in the "Private Modes" section above + (as set with DECSET/DECRST). +| \\E[ _Ps_ $ p +: DECRQM +: VT320 +: Request status of ECMA-48/ANSI mode. See the descriptions for SM/RM + above for recognized _Ps_ values. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pm_ $ r +: DECCARA +: VT400 +: Change attributes in rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ + denotes the rectangle, _Pm_ denotes the SGR attributes. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pm_ $ t +: DECRARA +: VT400 +: Invert attributes in rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ + denotes the rectangle, _Pm_ denotes the SGR attributes. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pp_ ; _Pt_ ; _Pl_ ; _Pp_ $ v +: DECCRA +: VT400 +: Copy rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the + rectangle, _Pt_ and _Pl_ denotes the target location. +| \\E[ _Pc_ ; _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ $ x +: DECFRA +: VT420 +: Fill rectangular area. _Pc_ is the character to use, _Pt_, _Pl_, + _Pb_ and _Pr_ denotes the rectangle. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ $ z +: DECERA +: VT400 +: Erase rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the + rectangle. +| \\E[ _Ps_ T +: SD +: VT420 +: Scroll down _Ps_ lines. +| \\E[ s +: SCOSC +: SCO, VT510 +: Save cursor position. +| \\E[ u +: SCORC +: SCO, VT510 +: Restore cursor position. +| \\E[ _Ps_ SP q +: DECSCUSR +: VT510 +: Set cursor style. In foot, _Ps_=0 means "use style from foot.ini". +| \\E[ = _Ps_ c +: DA3 +: VT510 +: Send tertiary device attributes. Foot responds with "FOOT", in + hexadecimal. +| \\E[ _Pm_ d +: VPA +: ECMA-48 +: Line position absolute - move cursor to line _Pm_. +| \\E[ _Pm_ e +: VPR +: ECMA-48 +: Line position relative - move cursor down _Pm_ lines. +| \\E[ _Pm_ a +: HPR +: ECMA-48 +: Character position relative - move cursor to the right _Pm_ times. +| \\E[ _Ps_ E +: CNL +: ECMA-48 +: Cursor next line - move the cursor down _Ps_ times. +| \\E[ _Ps_ F +: CPL +: ECMA-48 +: Cursor preceding line - move the cursor up _Ps_ times. +| \\E[ _Pm_ ` +: HPA +: ECMA-48 +: Character position absolute - move cursor to column _Pm_. +| \\E[ _Ps_ G +: CHA +: ECMA-48 +: Cursor character absolute - move cursor to column _Ps_. + cursor. _Ps_=2 -> all. _Ps_=3 -> saved lines. +| \\E[ _Ps_ S +: SU +: ECMA-48 +: Scroll up _Ps_ lines. +| \\E[ _Ps_ I +: CHT +: ECMA-48 +: Cursor forward tabulation _Ps_ tab stops. +| \\E[ _Ps_ Z +: CBT +: ECMA-48 +: Cursor backward tabulation _Ps_ tab stops. +| \\E[ _Ps_ b +: REP +: ECMA-48 +: Repeat the preceding printable character _Ps_ times. +| \\E[ ? _Pi_ ; _Pa_ ; _Pv_ S +: XTSMGRAPHICS +: xterm +: Set or request sixel attributes. +| \\E[ > _Ps_ q +: XTVERSION +: xterm +: _Ps_=0 -> report terminal name and version, in the form + *\\EP>|foot(version)\\E\\*. +| \\E[ > 4 ; _Pv_ m +: XTMODKEYS +: xterm +: Set level of the _modifyOtherKeys_ property to _Pv_. Note that foot + only supports level 1 and 2, where level 1 is the default setting. +| \\E[ ? _Pp_ m +: XTQMODKEYS +: xterm +: Query key modifier options +| \\E[ > 4 n +: +: xterm +: Resets the _modifyOtherKeys_ property to level 1. Note that in foot, + this sequence does not completely disable _modifyOtherKeys_, since + foot only supports level 1 and level 2 (and not level 0). +| \\E[ ? u +: +: kitty +: Query current values of the Kitty keyboard flags. +| \\E[ > _flags_ u +: +: kitty +: Push a new entry, _flags_, to the Kitty keyboard stack. +| \\E[ < _number_ u +: +: kitty +: Pop _number_ of entries from the Kitty keyboard stack. +| \\E[ = _flags_ ; _mode_ u +: +: kitty +: Update current Kitty keyboard flags, according to _mode_. +| \\E[ # P +: XTPUSHCOLORS +: xterm +: Push current color palette onto stack +| \\E[ # Q +: XTPOPCOLORS +: xterm +: Pop color palette from stack +| \\E[ # R +: XTREPORTCOLORS +: xterm +: Report the current entry on the palette stack, and the number of + palettes stored on the stack. +| \\E[ ? 996 n +: Query the current (color) theme mode +: contour +: The current color theme mode (light or dark) is reported as *CSI ? + 997 ; 1|2 n*, where *1* means dark and *2* light. By convention, the + primary theme in foot is considered dark, and the alternative theme + light. + + +# OSC + +All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. + +[[ *Sequence* +:[ *Origin* +:< *Description* +| \\E] 0 ; _Pt_ \\E\\ +: xterm +: Set window icon and title to _Pt_. +| \\E] 1 ; _Pt_ \\E\\ +: xterm +: Set window icon to _Pt_. +| \\E] 2 ; _Pt_ \\E\\ +: xterm +: Set window title to _Pt_ +| \\E] 4 ; _c_ ; _spec_ \\E\\ +: xterm +: Change color number _c_ to _spec_, where _spec_ is a color in + XParseColor format. foot only supports RGB colors; either + *rgb://*, or the legacy format (*#rgb*). +| \\E] 7 ; _Uri_ \\E\\ +: iTerm2 +: Update the terminal's current working directory. Newly spawned + terminals will launch in this directory. _Uri_ must be in the format + *file:///*. *hostname* must refer to your local host. +| \\E] 8 ; id=_ID_ ; _Uri_ \\E\\ +: VTE+iTerm2 +: Hyperlink (a.k.a HTML-like anchors). id=_ID_ is optional; if assigned, + all URIs with the same _ID_ will be treated as a single + hyperlink. An empty URI closes the hyperlink. +| \\E] 9 ; _msg_ \\E\\ +: iTerm2 +: Desktop notification, uses *notify* in *foot.ini*(5). +| \\E] 10 ; _spec_ \\E\\ +: xterm +: Change the default foreground color to _spec_, a color in + XParseColor format. +| \\E] 11 ; _spec_ \\E\\ +: xterm +: Change the default background color to _spec_, a color in + XParseColor format. Foot implements URxvt's transparency extension; + e.g. _spec_=*[75]#ff00ff* or _spec_=*rgba:ff/00/ff/bf* (pink with + 75% alpha). +| \\E] 12 ; _spec_ \\E\\ +: xterm +: Change cursor color to _spec_, a color in XParseColor format. +| \\E] 17 ; _spec_ \\E\\ +: xterm +: Change selection background color to _spec_, a color in + XParseColor format. +| \\E] 19 ; _spec_ \\E\\ +: xterm +: Change selection foreground color to _spec_, a color in XParseColor + format. +| \\E] 22 ; _xcursor-pointer-name_ \\E\\ +: xterm +: Sets the xcursor pointer. An empty name, or an invalid name resets + it. +| \\E] 52 ; _Pc_ ; ? \\E\\ +: xterm +: Send clipboard data. _Pc_ can be either *c*, *s* or *p*. *c* uses + the clipboard as source, and *s* and *p* uses the primary + selection. The response is *\\E] 52 ; Pc ; + \E\\*, where _Pc_ denotes the source used. +| \\E] 52 ; _Pc_ ; _Pd_ \\E\\ +: xterm +: Copy _Pd_ (base64 encoded text) to the clipboard. _Pc_ denotes the + target: *c* targets the clipboard and *s* and *p* the primary + selection. +| \\E] 66 ; _params_ ; text \\E\\ +: kitty +: Text sizing protocol (only 'w', width, supported) +| \\E] 99 ; _params_ ; _payload_ \\E\\ +: kitty +: Desktop notification; uses *desktop-notifications.command* in + *foot.ini*(5). +| \\E] 104 ; _c_ \\E\\ +: xterm +: Reset color number _c_ (multiple semicolon separated _c_ values may + be provided), or all colors (excluding the default + foreground/background colors) if _c_ is omitted. +| \\E] 110 \\E\\ +: xterm +: Reset default foreground color +| \\E] 111 \\E\\ +: xterm +: Reset default background color +| \\E] 112 \\E\\ +: xterm +: Reset cursor color +| \\E] 117 \\E\\ +: xterm +: Reset selection background color +| \\E] 119 \\E\\ +: xterm +: Reset selection foreground color +| \\E] 133 ; A \\E\\ +: FinalTerm +: Mark start of shell prompt +| \\E] 133 ; C \\E\\ +: FinalTerm +: Mark start of command output +| \\E] 133 ; D \\E\\ +: FinalTerm +: Mark end of command output +| \\E] 176 ; _app-id_ \\E\\ +: foot +: Set app ID. _app-id_ is optional; if assigned, + the terminal window App ID will be set to the value. + An empty App ID resets the value to the default. +| \\E] 555 \\E\\ +: foot +: Flash the entire terminal (foot extension) +| \\E] 777;notify;_title_;_msg_ \\E\\ +: urxvt +: Desktop notification, uses *desktop-notifications.command* in + *foot.ini*(5). + +# DCS + +All _DCS_ sequences begin with *\\EP* (sometimes abbreviated _DCS_), +and are terminated by *\\E\\* (ST). + +[[ *Sequence* +:< *Description* +| \\EP q \\E\\ +: Emit a sixel image at the current cursor position +| \\EP $ q \\E\\ +: Request selection or setting (DECRQSS). Implemented queries: + DECSTBM, SGR and DECSCUSR. +| \\EP = _C_ s \\E\\ +: Begin (_C_=*1*) or end (_C_=*2*) application synchronized updates. + This sequence is supported for compatibility reasons, but it's + recommended to use private mode 2026 (see above) instead. +| \\EP + q \\E\\ +: Query builtin terminfo database (XTGETTCAP) + + +# FOOTNOTE + +Foot does not support 8-bit control characters ("C1"). diff --git a/doc/foot.1.scd b/doc/foot.1.scd new file mode 100644 index 0000000..a190db9 --- /dev/null +++ b/doc/foot.1.scd @@ -0,0 +1,735 @@ +foot(1) + +# NAME + +foot - Wayland terminal emulator + +# SYNOPSIS + +*foot* [_OPTIONS_]++ +*foot* [_OPTIONS_] <_command_> [_COMMAND OPTIONS_] + +All trailing (non-option) arguments are treated as a command, and its +arguments, to execute (instead of the default shell). + +# DESCRIPTION + +*foot* is a Wayland terminal emulator. Running it without arguments +will start a new terminal window with your default shell. + +You can override the default shell by appending a custom command to +the foot command line + + *foot htop* + +# OPTIONS + +*-c*,*--config*=_PATH_ + Path to configuration file, see *foot.ini*(5) for details. + + The configuration file is automatically passed to new terminals + spawned via *spawn-terminal* (see *foot.ini*(5)). + +*-C*,*--check-config* + Verify configuration and then exit with 0 if ok, otherwise exit + with 230 (see *EXIT STATUS*). + +*-o*,*--override*=[_SECTION_.]_KEY_=_VALUE_ + Override an option set in the configuration file. If _SECTION_ is not + given, defaults to _main_. + +*-f*,*--font*=_FONT_ + Comma separated list of fonts to use, in fontconfig format (see + *FONT FORMAT*). + + The first font is the primary font. The remaining fonts are + fallback fonts that will be used whenever a glyph cannot be found + in the primary font. + + The fallback fonts are searched in the order they appear. If a + glyph cannot be found in any of the fallback fonts, the dynamic + fallback list from fontconfig (for the primary font) is + searched. + + Default: _monospace_. + +*-w*,*--window-size-pixels*=_WIDTHxHEIGHT_ + Set initial window width and height, in pixels. Default: _700x500_. + +*-W*,*--window-size-chars*=_WIDTHxHEIGHT_ + Set initial window width and height, in characters. Default: _not set_. + +*-t*,*--term*=_TERM_ + Value to set the environment variable *TERM* to (see *TERMINFO* + and *ENVIRONMENT*). Default: _@default_terminfo@_. + +*-T*,*--title*=_TITLE_ + Initial window title. Default: _foot_. + +*-a*,*--app-id*=_ID_ + Value to set the *app-id* property on the Wayland window + to. Default: _foot_ (normal mode), or _footclient_ (server mode). + +*toplevel-tag*=_TAG_ + Value to set the *toplevel-tag* property on the Wayland window + to. The compositor can use this value for session management, + window rules etc. Default: _not set_ + +*-m*,*--maximized* + Start in maximized mode. If both *--maximized* and *--fullscreen* + are specified, the _last_ one takes precedence. + +*-F*,*--fullscreen* + Start in fullscreen mode. If both *--maximized* and *--fullscreen* + are specified, the _last_ one takes precedence. + +*-L*,*--login-shell* + Start a login shell, by prepending a '-' to argv[0]. + +*--pty* + Display an existing pty instead of creating one. This is useful + for interacting with VM consoles. + + This option is not currently supported in combination with + *-s*,*--server*. + +*-D*,*--working-directory*=_DIR_ + Initial working directory for the client application. Default: + _CWD of foot_. + +*-s*,*--server*[=_PATH_|_FD_] + Run as a server. In this mode, a single foot instance hosts + multiple terminals (windows). Use *footclient*(1) to launch new + terminals. + + This saves some memory since for example fonts and glyph caches + can be shared between the terminals. + + It also saves upstart time since the config has already been + loaded and parsed, and most importantly, fonts have already been + loaded (and their glyph caches are likely to already have been + populated). + + Each terminal will have its own rendering threads, but all Wayland + communication, as well as input/output to the shell, is + multiplexed in the main thread. Thus, this mode might result in + slightly worse performance when multiple terminals are under heavy + load. + + Also be aware that should one terminal crash, it will take all the + others with it. + + The default path is + *$XDG\_RUNTIME\_DIR/foot-$WAYLAND\_DISPLAY.sock*. + + If *$XDG\_RUNTIME\_DIR* is not set, the default path is instead + */tmp/foot.sock*. + + If *$XDG\_RUNTIME\_DIR* is set, but *$WAYLAND\_DISPLAY* is not, + the default path is *$XDG\_RUNTIME\_DIR/foot.sock*. + + Note that if you change the default, you will also need to use the + *--server-socket* option in *footclient*(1) and point it to your + custom socket path. + + If the argument is a number, foot will interpret it as the file descriptor + of a socket provided by a supervision daemon (such as systemd or s6), and + use that socket as it's own. + + Two systemd units (foot-server.{service,socket}) are provided to use that + feature with systemd. To use socket activation, only enable the + socket unit. + + Note that starting *foot --server* as a systemd service will use + the environment of the systemd user instance; thus, you'll need + to import *$WAYLAND_DISPLAY* in it using *systemctl --user + import-environment WAYLAND_DISPLAY*. + +*-H*,*--hold* + Remain open after child process exits. + +*-p*,*--print-pid*=_FILE_|_FD_ + Print PID to this file, or FD, when successfully started. The file + (or FD) is closed immediately after writing the PID. When a _FILE_ + as been specified, the file is unlinked at exit. + + This option can only be used in combination with *-s*,*--server*. + +*-d*,*--log-level*={*info*,*warning*,*error*,*none*} + Log level, used both for log output on stderr as well as + syslog. Default: _warning_. + +*-l*,*--log-colorize*=[{*never*,*always*,*auto*}] + Enables or disables colorization of log output on stderr. Default: + _auto_. + +*-S*,*--log-no-syslog* + Disables syslog logging. Logging is only done on stderr. This + option can only be used in combination with *-s*,*--server*. + +*-v*,*--version* + Show the version number and quit. + +*-e* + Ignored; for compatibility with *xterm -e*. + + This option was added in response to several program launchers + passing *-e* to arbitrary terminals, under the assumption that + they all implement the same semantics for it as *xterm*(1). + Ignoring it allows foot to be invoked as e.g. *foot -e man foot* + with the same results as with xterm, instead of producing an + "invalid option" error. + +# KEYBOARD SHORTCUTS + +The following keyboard shortcuts are available by default. They can be +changed in *foot.ini*(5). There are also more actions (disabled by +default) available; see *foot.ini*(5). + +## NORMAL MODE + +*shift*+*page up*/*page down* + Scroll up/down in history + +*ctrl*+*shift*+*c*, *XF86Copy* + Copy selected text to the _clipboard_ + +*ctrl*+*shift*+*v*, *XF86Paste* + Paste from _clipboard_ + +*shift*+*insert* + Paste from the _primary selection_ + +*ctrl*+*shift*+*r* + Start a scrollback search + +*ctrl*+*+*, *ctrl*+*=* + Increase font size + +*ctrl*+*-* + Decrease font size + +*ctrl*+*0* + Reset font size + +*ctrl*+*shift*+*n* + Spawn a new terminal. If the shell has been configured to emit the + _OSC 7_ escape sequence, the new terminal will start in the + current working directory. + +*ctrl*+*shift*+*o* + Activate URL mode, allowing you to "launch" URLs. + +*ctrl*+*shift*+*u* + Activate Unicode input. + +*ctrl*+*shift*+*z* + Jump to the previous, currently not visible, prompt. Requires + shell integration. + +*ctrl*+*shift*+*x* + Jump to the next prompt. Requires shell integration. + +## SCROLLBACK SEARCH + +These keyboard shortcuts affect the search selection: + +*ctrl*+*r* + Search _backward_ for the next match. If the search string is + empty, the last searched-for string is used. + +*ctrl*+*s* + Search _forward_ for the next match. If the search string is + empty, the last searched-for string is used. + +*shift*+*right* + Extend current selection to the right by one character. + +*shift*+*left* + Extend current selection to the left by one character. + +*ctrl*+*w*, *ctrl*+*shift*+*right* + Extend current selection (and thus the search criteria) to the end + of the word, or the next word if currently at a word separating + character. + +*ctrl*+*shift*+*w* + Same as *ctrl*+*w*, except that the only word separating + characters are whitespace characters. + +*ctrl*+*shift*+*left* + Extend current selection to the left to the last word boundary. + +*shift*+*down* + Extend current selection down one line + +*shift*+*up* + Extend current selection up one line. + +*ctrl*+*v*, *ctrl*+*shift*+*v*, *ctrl*+*y*, *XF86Paste* + Paste from clipboard into the search buffer. + +*shift*+*insert* + Paste from primary selection into the search buffer. + +*escape*, *ctrl*+*g*, *ctrl*+*c* + Cancel the search + +*return* + Finish the search and copy the current match to the primary + selection. The terminal selection is kept, allowing you to press + *ctrl*+*shift*+*c* to copy it to the clipboard. + +These shortcuts affect the search box in scrollback-search mode: + +*ctrl*+*b* + Moves the cursor in the search box one **character** to the left. + +*ctrl*+*left*, *alt*+*b* + Moves the cursor in the search box one **word** to the left. + +*ctrl*+*f* + Moves the cursor in the search box one **character** to the right. + +*ctrl*+*right*, *alt*+*f* + Moves the cursor in the search box one **word** to the right. + +*Home*, *ctrl*+*a* + Moves the cursor in the search box to the beginning of the input. + +*End*, *ctrl*+*e* + Moves the cursor in the search box to the end of the input. + +*alt*+*backspace*, *ctrl*+*backspace* + Deletes the **word before** the cursor. + +*alt*+*delete*, *ctrl*+*delete* + Deletes the **word after** the cursor. + +*ctrl*+*u* + Deletes from the cursor to the start of the input + +*ctrl*+*k* + Deletes from the cursor to the end of the input + +These shortcuts affect scrolling in scrollback-search mode: + +*shift*+*page-up* + Scrolls up/back one page in history. + +*shift*+*page-down* + Scroll down/forward one page in history. + +## URL MODE + +*t* + Toggle URL visibility in jump label. + +*escape*, *ctrl*+*g*, *ctrl*+*c*, *ctrl*+*d* + Exit URL mode without launching a URL. + +## MOUSE SHORTCUTS + +*left*, single-click + Drag to select; when released, the selected text is copied to the + _primary_ selection. This feature is normally *disabled* whenever + the client has enabled _mouse tracking_, but can be forced by + holding *shift*. + + Holding *ctrl* will create a block selection. + +*left*, double-click + Selects the _word_ (separated by spaces, period, comma, + parenthesis etc) under the pointer. Hold *ctrl* to select + everything under the pointer up to, and until, the next space + characters. + +*left*, triple-click + Selects the everything between enclosing quotes, or the entire row + if not inside a quote. + +*left*, quad-click + Selects the entire row + +*middle* + Paste from the _primary_ selection + +*right* + Extend current selection. Clicking immediately extends the + selection, while hold-and-drag allows you to interactively resize + the selection. + +*ctrl*+*right* + Extend the current selection, but force it to be character wise, + rather than depending on the original selection mode. + +*wheel* + Scroll up/down in history + +*ctrl*+*wheel* + Increase/decrease font size + +## TOUCHSCREEN + +*tap* + Emulates mouse left button click. + +*drag* + Scrolls up/down in history. + + Holding for a while before dragging (time delay can be configured) + emulates mouse dragging with left button held. + + +# FONT FORMAT + +The font is specified in FontConfig syntax. That is, a colon-separated +list of font name and font options. + +_Examples_: +- Dina:weight=bold:slant=italic +- Courier New:size=12 + +# URLs + +Foot supports URL detection. But, unlike many other terminal +emulators, where URLs are highlighted when they are hovered and opened +by clicking on them, foot uses a keyboard driven approach. + +Pressing *ctrl*+*shift*+*o* enters _"Open URL mode"_, where all currently +visible URLs are underlined, and is associated with a +_"jump-label"_. The jump-label indicates the _key sequence_ +(e.g. *"AF"*) to use to activate the URL. + +The key binding can, of course, be customized, like all other key +bindings in foot. See *show-urls-launch* and *show-urls-copy* in +*foot.ini*(5). + +*show-urls-launch* by default opens the URL with *xdg-open*. This can +be changed with the *url-launch* option. + +*show-urls-copy* is an alternative to *show-urls-launch*, that changes +what activating a URL _does_; instead of opening it, it copies it to +the clipboard. It is unbound by default. + +Jump label colors, the URL underline color, and the letters used in +the jump label key sequences can be configured. + +# ALT/META CHARACTERS + +By default, foot prefixes meta characters with *ESC*. This corresponds +to XTerm's *metaSendsEscape* option set to *true*. + +This can be disabled programmatically with *\E[?1036l* (and enabled +again with *\E[?1036h*). + +When disabled, foot will instead set the 8:th bit of meta character +and then UTF-8 encode it. This corresponds to XTerm's *eightBitMeta* +option set to *true*. + +This can also be disabled programmatically with *rmm* (Reset Meta Mode, +*\E[?1034l*), and enabled again with *smm* (Set Meta Mode, +*\E[?1034h*). + +# BACKSPACE + +Foot transmits DEL (*^?*) on backspace. This corresponds to XTerm's +*backarrowKey* option set to *false*, and to DECBKM being _reset_. + +To instead transmit BS (*^H*), press *ctrl*+*backspace*. + +Note that foot does *not* implement DECBKM, and that the behavior +described above *cannot* be changed. + +Finally, pressing *alt* will prefix the transmitted byte with ESC. + +# KEYPAD + +By default, *Num Lock* overrides the run-time configuration keypad +mode; when active, the keypad is always considered to be in +_numerical_ mode. This corresponds to XTerm's *numLock* option set to +*true*. + +In this mode, the keypad keys always sends either numbers (Num Lock is +active) or cursor movement keys (up, down, left, right, page up, page +down etc). + +This can be disabled programmatically with *\E[?1035l* (and enabled +again with *\E[?1035h*). + +When disabled, the keypad sends custom escape sequences instead of +numbers, when in _application_ mode. + +# CONFIGURATION + +foot will search for a configuration file in the following locations, +in this order: + + - *XDG_CONFIG_HOME/foot/foot.ini* (defaulting to + *$HOME/.config/foot/foot.ini* if unset) + - *XDG_CONFIG_DIRS/foot/foot.ini* (defaulting to + */etc/xdg/foot/foot.ini* if unset) + +An example configuration file containing all options with their default value +commented out will usually be installed to */etc/xdg/foot/foot.ini*. + +For more information, see *foot.ini*(5). + +# SHELL INTEGRATION + +## Current working directory + +New foot terminal instances (bound to *ctrl*+*shift*+*n* by default) +will open in the current working directory, if the shell in the +"parent" terminal reports directory changes. + +This is done with the OSC-7 escape sequence. Most shells can be +scripted to do this, if they do not support it natively. See the wiki +(https://codeberg.org/dnkl/foot/wiki#user-content-spawning-new-terminal-instances-in-the-current-working-directory) +for details. + + +## Jumping between prompts + +Foot can move the current viewport to focus prompts of already +executed commands (bound to *ctrl*+*shift*+*z*/*x* by default). + +For this to work, the shell needs to emit an OSC-133;A +(*\\E]133;A\\E\\\\*) sequence before each prompt. + +In zsh, one way to do this is to add a _precmd_ hook: + + *precmd() { + print -Pn "\\e]133;A\\e\\\\" + }* + +See the wiki +(https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) +for details, and examples for other shells. + +## Piping last command's output + +The key binding *pipe-command-output* can pipe the last command's +output to an application of your choice (similar to the other +*pipe-\** key bindings): + + *\[key-bindings\]++ +pipe-command-output=[sh -c "f=$(mktemp); cat - > $f; footclient emacsclient -nw $f; rm $f"] Control+Shift+g* + +When pressing *ctrl*+*shift*+*g*, the last command's output is written +to a temporary file, then an emacsclient is started in a new +footclient instance. The temporary file is removed after the +footclient instance has closed. + +For this to work, the shell must emit an OSC-133;C (*\\E]133;C\\E\\\\*) +sequence before command output starts, and an OSC-133;D +(*\\E]133;D\\E\\\\*) when the command output ends. + +In fish, one way to do this is to add _preexec_ and _postexec_ hooks: + + *function foot_cmd_start --on-event fish_preexec + echo -en "\\e]133;C\\e\\\\" + end* + + *function foot_cmd_end --on-event fish_postexec + echo -en "\\e]133;D\\e\\\\" + end* + +See the wiki +(https://codeberg.org/dnkl/foot/wiki#user-content-piping-last-commands-output) +for details, and examples for other shells + +# TERMINFO + +Client applications use the terminfo identifier specified by the +environment variable *TERM* (set by foot) to determine terminal +capabilities. + +Foot has two terminfo definitions: *foot* and *foot-direct*, with +*foot* being the default. + +The difference between the two is in the number of colors they +describe; *foot* describes 256 colors and *foot-direct* 16.7 million +colors (24-bit truecolor). + +Note that using the *foot* terminfo does not limit the number of +usable colors to 256; applications can still use 24-bit RGB colors. In +fact, most applications work best with *foot* (including 24-bit +colors). Using *\*-direct* terminfo entries has been known to crash +some ncurses applications even. + +There are however applications that need a *\*-direct* terminfo entry +for 24-bit support. Emacs is one such example. + +While using either *foot* or *foot-direct* is strongly recommended, it +is possible to use e.g. *xterm-256color* as well. This can be useful +when remoting to a system where foot's terminfo entries cannot easily +be installed. + +Note that terminfo entries can be installed in the user's home +directory. I.e. if you do not have root access, or if there is no +distro package for foot's terminfo entries, you can install foot's +terminfo entries manually, by copying *foot* and *foot-direct* to +*~/.terminfo/f/*. + +# XTGETTCAP + +*XTGETTCAP* is an escape sequence initially introduced by XTerm, and +also implemented (and extended, to some degree) by Kitty. + +It allows querying the terminal for terminfo classic, file-based, +terminfo definition. For example, if all applications used this +feature, you would no longer have to install foot's terminfo on remote +hosts you SSH into. + +XTerm's implementation (as of XTerm-370) only supports querying key +(as in keyboard keys) capabilities, and three custom capabilities: + +- TN - terminal name +- Co - number of colors (alias for the colors capability) +- RGB - number of bits per color channel (different semantics from + the RGB capability in file-based terminfo definitions!). + +Kitty has extended this, and also supports querying all integer and +string capabilities. + +Foot supports this, and extends it even further, to also include +boolean capabilities. This means foot's entire terminfo can be queried +via *XTGETTCAP*. + +Note that both Kitty and foot handles responses to multi-capability +queries slightly differently, compared to XTerm. + +XTerm will send a single DCS reply, with ;-separated +capability/value pairs. There are a couple of issues with this: + +- The success/fail flag in the beginning of the response is always 1 + (success), unless the very first queried capability is invalid. +- XTerm will not respond at all to an invalid capability, unless it's + the first one in the XTGETTCAP query. +- XTerm will end the response at the first invalid capability. + +In other words, if you send a large multi-capability query, you will +only get responses up to, but not including, the first invalid +capability. All subsequent capabilities will be dropped. + +Kitty and foot on the other hand, send one DCS response for each +capability in the multi query. This allows us to send a proper +success/fail flag for each queried capability. Responses for all +queried capabilities are always sent. No queries are ever dropped. + +# EXIT STATUS + +Foot will exit with code 230 if there is a failure in foot itself. + +In all other cases, the exit code is that of the client application +(i.e. the shell). + +# ENVIRONMENT + +## Variables used by foot + +*SHELL* + The default child process to run, when no _command_ argument is + specified and the *shell* option in *foot.ini*(5) is not set. + +*HOME* + Used to determine the location of the configuration file, see + *foot.ini*(5) for details. + +*XDG\_CONFIG\_HOME* + Used to determine the location of the configuration file, see + *foot.ini*(5) for details. + +*XDG\_CONFIG\_DIRS* + Used to determine the location of the configuration file, see + *foot.ini*(5) for details. + +*XDG\_RUNTIME\_DIR* + Used to construct the default _PATH_ for the *--server* + option, when no explicit argument is given (see above). + +*WAYLAND\_DISPLAY* + Used to construct the default _PATH_ for the *--server* + option, when no explicit argument is given (see above). + +*XCURSOR\_THEME* + The name of the *Xcursor*(3) theme to use for pointers (typically + set by the Wayland compositor). + +*XCURSOR\_SIZE* + The size to use for *Xcursor*(3) pointers (typically set by the + Wayland compositor). + +## Variables set in the child process + +*TERM* + terminfo/termcap identifier. This is used by client applications + to determine which capabilities a terminal supports. The value is + set according to either the *--term* command-line option or the + *term* config option in *foot.ini*(5). + +*COLORTERM* + This variable is set to *truecolor*, to indicate to client + applications that 24-bit RGB colors are supported. + +*PWD* + Current working directory (at the time of launching foot) + +*SHELL* + Set to the launched shell, if the shell is valid (it is listed in + */etc/shells*). + +In addition to the variables listed above, custom environment +variables may be defined in *foot.ini*(5). + +## Variables *unset* in the child process + +*TERM_PROGRAM* +*TERM_PROGRAM_VERSION* + These environment variables are set by certain other terminal + emulators. We unset them, to prevent applications from + misdetecting foot. + +In addition to the variables listed above, custom environment +variables to unset may be defined in *foot.ini*(5). + +# Signals + +The following signals have special meaning in foot: + +- SIGUSR1: switch to the dark color theme (*[colors-dark]*). +- SIGUSR2: switch to the light color theme (*[colors-light]*). + +Note: you can send SIGUSR1/SIGUSR2 to a *foot --server* process too, +in which case all client instances will switch theme. Furthermore, all +future client instances will also use the selected theme. + +You can also send SIGUSR1/SIGUSR2 to a footclient instance, see +*footclient*(1) for details. + + +# BUGS + +Please report bugs to https://codeberg.org/dnkl/foot/issues + +Before you open a new issue, please search existing bug reports, both +open and closed ones. Chances are someone else has already reported +the same issue. + +The report should contain the following: + +- Foot version (*foot --version*). +- Log output from foot (run *foot -d info* from another terminal). +- Which Wayland compositor (and version) you are running. +- If reporting a crash, please try to provide a *bt full* backtrace + with symbols. +- Steps to reproduce. The more details the better. + +# IRC + +\#foot on irc.libera.chat + +# SEE ALSO + +*foot.ini*(5), *footclient*(1) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd new file mode 100644 index 0000000..66daaea --- /dev/null +++ b/doc/foot.ini.5.scd @@ -0,0 +1,2166 @@ +foot.ini(5) + +# NAME + +foot.ini - configuration file for *foot*(1) + +# DESCRIPTION + +*foot* uses the standard _unix configuration format_, with section based +key/value pairs. The default section is usually unnamed, i.e. not prefixed +with a _[section]_. However it can also be explicitly named _[main]_, +say if it needs to be reopened after any of the other sections. + +foot will search for a configuration file in the following locations, +in this order: + + - *XDG_CONFIG_HOME/foot/foot.ini* (defaulting to + *$HOME/.config/foot/foot.ini* if unset) + - *XDG_CONFIG_DIRS/foot/foot.ini* (defaulting to + */etc/xdg/foot/foot.ini* if unset) + +An example configuration file containing all options with their default value +commented out will usually be installed to */etc/xdg/foot/foot.ini*. + +Options are set using KEY=VALUE pairs: + + *\[colors-dark\]*++ +*background=000000*++ +*foreground=ffffff* + +Empty values (*KEY=*) are not supported. String options do allow the +empty string to be set, but it must be quoted: *KEY=""* + +# SECTION: main + +*shell* + Executable to launch. Typically a shell. Default: _$SHELL_ if set, + otherwise the user's default shell (as specified in + _/etc/passwd_). You can also pass arguments. For example + */bin/bash --norc*. + +*login-shell* + Boolean. If enabled, the shell will be launched as a login shell, + by prepending a '-' to argv[0]. Default: _no_. + +*term* + Value to set the environment variable *TERM* to. Default: + _@default_terminfo@_ + +*font*, *font-bold*, *font-italic*, *font-bold-italic* + Comma separated list of fonts to use, in fontconfig format. That + is, a font name followed by a list of colon-separated + options. Most noteworthy is *:size=n* (or *:pixelsize=n*), which + is used to set the font size. Note that the font size is also + affected by the *dpi-aware* option. + + Examples: + - Dina:weight=bold:slant=italic + - Courier New:size=12 + - Fantasque Sans Mono:fontfeatures=ss01 + - Iosevka:fontfeatures=cv01=1:fontfeatures=cv06=1 + - Meslo LG S:size=12, Noto Color Emoji:size=12 + - Courier New:pixelsize=8 + + Be aware that, depending on your setup, there may be global + FontConfig options that overrides options set here. If an option + appears to have no effect, ensure there is no global configuration + file that sets the same option with *assign* or *assign_replace*; + use one of the many *append* or possibly *prepend* modes. + + For each option, the first font is the primary font. The remaining + fonts are fallback fonts that will be used whenever a glyph cannot + be found in the primary font. + + The fallback fonts are searched in the order they appear. If a + glyph cannot be found in any of the fallback fonts, the dynamic + fallback list from fontconfig (for the primary font) is + searched. + + *font-bold*, *font-italic* and *font-bold-italic* allow custom + fonts to be used for bold/italic/bold+italic fonts. If left + unconfigured, the bold/italic variants of the regular font(s) + specified in *font* are used. *Note*: you _may_ have to tweak the + size(s) of the custom bold/italic fonts to match the regular font. + + To disable bold and/or italic fonts, set e.g. *font-bold* to + _exactly_ the same value as *font*. + + **size** is in _points_ (as defined by the FontConfig format). To + set a _pixel_ size, use **pixelsize** instead. Note that pixel + sizes are unaffected by DPI aware rendering (see *dpi-aware*), but + are affected by desktop scaling. + + Default: _monospace:size=8_ (*font*), _not set_ (*font-bold*, + *font-italic*, *font-bold-italic*). + +*font-size-adjustment* + Amount, in _points_, _pixels_ or _percent_, to increment/decrement + the font size when zooming in or out. + + Examples: + ``` + font-size-adjustment=0.5 # Adjust by 0.5 points + font-size-adjustment=10px # Adjust by 10 pixels + font-size-adjustment=7.5% # Adjust by 7.5 percent + ``` + + Default: _0.5_ + +*include* + Absolute path to configuration file to import. + + The import file has its own section scope. I.e. the including + configuration is still in the default section after the include, + regardless of which section the included file ends in. + + - The path must be an absolute path, or start with *~/*. + - Multiple include directives are allowed, but only one path per + directive. + - Nested imports are allowed. + + Default: _not set_. + +*line-height* + An absolute value, in _points_, that override line height from the + font metrics. + + You can specify a height in _pixels_ by using the *px* suffix: + e.g. *line-height=12px*. + + *Warning*: when changing the font size at runtime (i.e. zooming in + or out), foot will change the line height by the same + percentage. However, due to rounding, it is possible the line + height will be "too small" for some font sizes, causing + e.g. underscores to "disappear". + + See also: *vertical-letter-offset*. + + Default: _not set_. + +*letter-spacing* + Spacing between letters, in _points_. A positive value will + increase the cell size, and a negative value shrinks it. + + You can specify a letter spacing in _pixels_ by using the *px* + suffix: e.g. *letter-spacing=2px*. + + See also: *horizontal-letter-offset*. + + Default: _0_. + +*horizontal-letter-offset*, *vertical-letter-offset* + Configure the horizontal and vertical offsets used when + positioning glyphs within cells, in _points_, relative to the top + left corner. + + To specify an offset in _pixels_, append *px*: + e.g. *horizontal-letter-offset=2px*. + + Default: _0_. + +*underline-offset* + Use a custom offset for underlines. The offset is, by default, in + _points_ and relative to the font's baseline. A positive value + positions the underline under the baseline, while a negative value + positions it above the baseline. + + To specify an offset in _pixels_, append *px*: + *underline-offset=2px*. + + If left unset (the default), the offset specified in the font is + used, or estimated by foot if the font lacks underline positioning + information. + + Default: _unset_. + +*underline-thickness* + Use a custom thickness (height) for underlines. The thickness is, by + default, in _points_. + + To specify a thickness in _pixels_, append *px*: + *underline-thickness=1px*. + + If left unset (the default), the thickness specified in the font is + used. + + Default: _unset_ + +*strikeout-thickness* + Use a custom thickness (height) for strikeouts. The thickness is, by + default, in _points_. + + To specify a thickness in _pixels_, append *px*: + *strikeout-thickness=1px*. + + If left unset (the default), the thickness specified in the font is + used. + + Default: _unset_ + +*gamma-correct-blending* + Boolean. When enabled, foot will do gamma-correct blending in + linear color space. This is how font glyphs are supposed to be + rendered, but since nearly no applications or toolkits are doing + it on Linux, the result may not look like you are used to. + + Compared to the default (disabled), bright glyphs on a dark + background will appear thicker, and dark glyphs on a light + background will appear thinner. + + FreeType can limit the effect of the latter, with a technique + called stem darkening. It is only available for CFF fonts + (OpenType, .otf) and disabled by default (in FreeType). You can + enable it by setting the environment variable + *FREETYPE_PROPERTIES="cff:no-stem-darkening=0"* before starting + foot. + + Also be aware that many fonts have been developed on systems that + do not do gamma-correct blending, and may therefore look thicker + than intended when rendered with gamma-correct blending, since the + font designer set the font weight based on incorrect rendering. + + In order to represent colors faithfully, higher precision image + buffers are required. By default, foot will use either 16-bit, or + 10-bit color channels, depending on availability, when + gamma-correct blending is enabled. However, the high precision + buffers are slow; if you want to use gamma-correct blending, but + prefer speed (throughput and input latency) over accurate colors, + you can force 8-bit color channels by setting + *tweak.surface-bit-depth=8-bit*. + + Default: _no_. + +*uppercase-regex-insert* + Boolean. When enabled, inputting an uppercase hint character in + *show-urls-copy* or *regex-copy* mode will insert the selected + text into the prompt in addition to copying it to the clipboard. + + Default: _yes_ + +*box-drawings-uses-font-glyphs* + Boolean. When disabled, foot generates box/line drawing characters + itself. There are several advantages to doing this instead of using + font glyphs: + + - No antialiasing effects where e.g. line endpoints appear + dimmed down, or blurred. + - Line- and box characters are guaranteed to span the entire cell, + resulting in a gap-less appearance. + - No alignment issues, i.e. lines are centered when they should be. + - Many fonts lack some, or all, of the line- and box drawing + characters, causing fallback fonts to be used, which results + in out-of-place looking glyphs (for example, badly sized). + + When enabled, box/line drawing characters are rendered using font + glyphs. This may result in a more uniform look, in some use cases. + + When disabled, foot will render the following Unicode codepoints + by itself: + + - U+02500 - U+0259F + - U+02800 - U+028FF + - U+1CD00 - U+1CDE5 + - U+1Fb00 - U+1FB9B + + Default: _no_. + +*dpi-aware* + Boolean. + + When set to *yes*, fonts are sized using the monitor's DPI, making + a font of a given size have the same physical size, regardless of + monitor. In other words, if you drag a foot window between + different monitors, the font size remains the same. + + In this mode, the monitor's scaling factor is ignored; doubling + the scaling factor will *not* double the font size. + + When set to *no*, the monitor's DPI is ignored. The font is + instead sized using the monitor's scaling factor; doubling the + scaling factor *does* double the font size. + + Note that this option typically does not work with bitmap fonts, + which only contain a pre-defined set of sizes, and cannot be + dynamically scaled. Whichever size (of the available ones) that + best matches the DPI or scaling factor, will be used. + + Also note that if the font size has been specified in pixels + (*:pixelsize=*_N_, instead of *:size=*_N_), DPI scaling + (*dpi-aware=yes*) will have no effect (the specified pixel size + will be used as is). But, if the monitor's scaling factor is used + to size the font (*dpi-aware=no*), the font's pixel size will be + multiplied with the scaling factor. + + Default: _no_ + +*pad* + Padding between border and glyphs, in pixels (subject to output + scaling), in the form + + ``` + _XxY_ [center | center-when-fullscreen | center-when-maximized-and-fullscreen] + ``` + or + ``` + RIGHTxTOPxLEFTxBOTTOM [center | center-when-fullscreen | center-when-maximized-and-fullscreen] + ``` + + - `_XxY_` adds _at least_: + - X pixels on the left and right sides. + - Y pixels on the top and bottom sides. + + - `LEFTxTOPxRIGHTxBOTTOM` adds **at least**: + - LEFT pixels to the left + - TOP pixels to the top + - RIGHT pixels to the right + - BOTTOM pixels to the bottom + + When no centering is specified, the grid content is anchored to + the top left corner. I.e. if the window manager forces an odd + window size on foot, the additional pixels will be added to the + right and bottom sides. + + If *center* is specified, the grid content is instead + centered. This may cause "jumpiness" when resizing the window. + + With *center-when-fullscreen* and + *center-when-maximized-and-fullscreen*, the grid is anchored to + the top left corner, unless the window is maximized, or + fullscreened. + + Default: _0x0_ center-when-maximized-and-fullscreen. + +*resize-delay-ms* + + Time, in milliseconds, of "idle time" before foot performs text + reflow, and sends the new window dimensions to the client + application while doing an interactive resize of a foot + window. Idle time in this context is a period of time where the + window size is not changing. + + In other words, while you are fiddling with the window size, foot + does not send the updated dimensions to the client. It also does a + fast "truncating" resize of the grid, instead of actually + reflowing the contents. Only when you pause the fiddling for + *resize-delay-ms* milliseconds is the client updated, and the + contents properly reflowed. + + Emphasis is on _while_ here; as soon as the interactive resize + ends (i.e. when you let go of the window border), the final + dimensions are sent to the client, without any delays. + + Setting it to 0 disables the delay completely. + + Default: _100_. + +*resize-by-cells* + Boolean. + + When set to *yes*, the window size will be constrained to multiples + of the cell size (plus any configured padding). When set to *no*, + the window size will be unconstrained, and padding may be adjusted + as necessary to accommodate window sizes that are not multiples of + the cell size. + + This option only applies to floating windows. Sizes of maximized, tiled + or fullscreen windows will not be constrained to multiples of the cell + size. + + Default: _yes_ + +*resize-keep-grid* + Boolean. + + When set to *yes*, the window size will be adjusted with changes in font + size to preserve the dimensions of the text grid. When set to *no*, the + window size will remain constant and the text grid will be adjusted as + necessary to fit the window. + + This option only applies to floating windows. + + Default: _yes_ + +*initial-color-theme* + Selects which color theme to use, *dark*, or *light*. + + *dark* uses the colors defined in the *colors-dark* section, while + *light* uses the colors from the *colors-light* section. + + Use the *color-theme-switch-dark*, *color-theme-switch-light* and + *color-theme-toggle* key bindings to switch between the two themes + at runtime, or send SIGUSR1/SIGUSR2 to the foot process (see + *foot*(1) for details). + + Default: _dark_ + +*initial-window-size-pixels* + Initial window width and height in _pixels_ (subject to output + scaling), in the form _WIDTHxHEIGHT_. The height _includes_ the + titlebar when using CSDs. Mutually exclusive to + *initial-window-size-chars*. + + Note that this option may not work as expected if fractional + scaling is being used, due to the fact that many compositors do + not report the correct scaling factor until after a window has + been mapped. + + Default: _700x500_. + +*initial-window-size-chars* + Initial window width and height in _characters_, in the form + _WIDTHxHEIGHT_. Mutually exclusive to + *initial-window-size-pixels*. + + Note that if you have a multi-monitor setup, with different + scaling factors, there is a possibility the window size will not + be set correctly. If that is the case, use + *initial-window-size-pixels* instead. + + And, just like *initial-window-size-pixels*, this option may not + work as expected if fractional scaling is being used (see + *initial-window-size-pixels* for details). + + Default: _not set_. + +*initial-window-mode* + Initial window mode for each newly spawned window: *windowed*, + *maximized* or *fullscreen*. Default: _windowed_. + +*title* + Initial window title. Default: _foot_. + +*locked-title* + Boolean. If enabled, applications are not allowed to change the + title at run-time. Default: _no_. + +*app-id* + Value to set the *app-id* property on the Wayland window to. The + compositor can use this value to e.g. group multiple windows, or + apply window management rules. Default: _foot_ (normal mode), or + _footclient_ (server mode). + +*toplevel-tag* + Value to set the *toplevel-tag* property on the Wayland window + to. The compositor can use this value for session management, + window rules etc. Default: _not set_ + +*bold-text-in-bright* + Semi-boolean. When enabled, bold text is rendered in a brighter + color (in addition to using a bold font). The color is brightened + by blending it with white. + + If set to *palette-based*, rather than a simple *yes|true*, colors + matching one of the 8 regular palette colors will be brightened + using the corresponding bright palette color. Other colors will + not be brightened. + + Default: _no_. + +*word-delimiters* + String of characters that act as word delimiters when selecting + text. Note that whitespace characters are _always_ word + delimiters, regardless of this setting. Default: _,│`|:"'()[]{}<>_ + +*selection-target* + Clipboard target to automatically copy selected text to. One of + *none*, *primary*, *clipboard* or *both*. Default: _primary_. + +*workers* + Number of threads to use for rendering. Set to 0 to disable + multithreading. Default: the number of available logical CPUs + (including SMT). Note that this is not always the best value. In + some cases, the number of physical _cores_ is better. + + In case you have a ridiculous amount of cores and/or threads, + consider limiting the number of *workers*, since foot cannot + parallelize more than the number of visible rows. + +*utmp-helper* + Path to utmp logging helper binary. + + When starting foot, an utmp record is created by launching the + helper binary with the following arguments: + + ``` + @utmp_add_args@ + ``` + + When foot is closed, the utmp record is removed by launching the + helper binary with the following arguments: + + ``` + @utmp_del_args@ + ``` + + Set to *none* to disable utmp records. Default: _@utmp_helper_path@_. + +# SECTION: environment + +This section is used to define environment variables that will be set +in the client application, in addition to the variables inherited from +the terminal process itself. + +The format is simply: + +*name*=_value_ + +Note: do not set *TERM* here; use the *term* option in the main +(default) section instead. + +# SECTION: security + +*osc52* + + Whether OSC-52 (clipboard access) is enabled or disabled. One of + *disabled*, *copy-enabled*, *paste-enabled* or *enabled*. + + OSC-52 gives terminal application access to the host clipboard + (i.e. the Wayland clipboard). This is normally not a security + issue, since all applications can access the clipboard directly + over the Wayland socket. + + However, when SSH:ing into a remote system, or accessing a + container etc, the terminal applications may be untrusted, and you + might consider disabling the host clipboard access. + + - *disabled*: disables all clipboard access + - *copy-enabled*: applications can write to the clipboard, but not + read from it. + - *paste-enabled*: applications can read from the clipboard, but + not write to it. + - *enabled*: all applications have full access to the host + clipboard. This is the default. + + Default: _enabled_ + + +# SECTION: bell + +*system* + Boolean, when set to _yes_, ring the system bell. The bell is rung + independent of whether the foot window has keyboard focus or + not. Exact behavior is compositor dependent. + + Default: _yes_ + +*urgent* + Boolean, when set to _yes_, foot will signal urgency to the + compositor through the XDG activation protocol whenever *BEL* is + received, and the window does NOT have keyboard focus. + + If the compositor does not implement this protocol, the margins + will be painted in red instead. + + Applications can enable/disable this feature programmatically with + the *CSI ? 1042 h* and *CSI ? 1042 l* escape sequences. + + Default: _no_ + +*notify* + Boolean, when set to _yes_, foot will emit a desktop notification + using the command specified in the *notify* option whenever *BEL* + is received. By default, bell notifications are shown only when + the window does *not* have keyboard focus. See + _desktop-notifications.inhibit-when-focused_. + + Default: _no_ + +*visual* + Boolean, when set to _yes_, foot will flash the terminal + window. Default: _no_ + +*command* + When set, foot will execute this command when *BEL* is received. + Default: none + +*command-focused* + Boolean, whether to run the command on *BEL* even while + focused. Default: _no_ + +# SECTION: desktop-notifications + +*command* + Command to execute to display a notification. + + Template arguments + _${title}_ and _${body}_ will be replaced with the + notification's actual _title_ and _body_ (message content). + + _${app-id}_ is replaced with the value of the command line + option _--app-id_, and defaults to *foot* (normal mode), or + *footclient* (server mode). + + _${window-title}_ is replaced with the current window title. + + _${icon}_ is replaced by the icon specified in the + notification request, or the empty string if no icon was + specified. Can be used with e.g. notify-send's *--icon* + option, or preferably, by setting the *image-path* hint (with + e.g. notify-send's *--hint* option). + + _${category}_ is replaced by the notification's category. Can + be used together with e.g. notify-send's *--category* option. + + _${urgency}_ is replaced with the notifications urgency; + *low*, *normal* or *critical*. Can be used together with + e.g. notify-send's *--urgency* option. + + _${expire-time}_ is replaced with the notification specified + notification timeout. Can be used together with + e.g. notify-send's *--expire-time* option. + + _${replace-id}_ is replaced by the notification daemon + assigned ID that the notification replaces/updates. For this + to work, foot needs to know the externally assigned IDs of + previously emitted notifications, see the 'stdout' section + below. Can be used together with e.g. notify-send's + *--replace-id* option. + + _${muted}_ is replaced by either *true* or *false*, depending + on whether the notification has requested all notification + sounds be muted. It is intended to set the *suppress-sound* + hint (with e.g. notify-send's *--hint* option). + + _${sound-name}_ is replaced by sound-name requested by the + notification. This should be a name from the freedesktop sound + naming specification, but this is not something that foot + enforces. It is intended to set the *sound-name* hint (with + e.g. notify-send's *--hint* option). + + _${action-argument}_ will be expanded to the + *command-action-argument* option, for each notification + action. There will always be at least one action, the + "default" action. Foot uses this to enable window focusing, + and reporting notification activation to applications that + requested such events. + + Applications can also define their own custom notification + actions. See the *command-action-argument* option for details. + + Ways to trigger notifications + Applications can trigger notifications in the following ways: + + - OSC 777: *\\e]777;notify;;<body>\\e\\\\* + - OSC 99: *\\e]99;;<title>\\e\\\\* (this is just a bare bones + example; this protocol has lots of features, see + https://sw.kovidgoyal.net/kitty/desktop-notifications) + + By default, notifications are *inhibited* if the foot window + has keyboard focus. See + _desktop-notifications.inhibit-when-focused_. + + Window activation (focusing) + Foot can focus the window when the notification is + 'activated'. It can also send an event back to the client + application, notifying it that the notification has been + 'activated', This typically happens when the default action is + invoked, and/or when the notification is clicked, but exact + behavior depends on the notification daemon in use, and how it + has been configured. + + For this to work, foot needs to know when the notification was + activated (as opposed to just dismissed), and it needs an XDG + activation token. + + There are two parts to handle this. First, the notification + must define an action. For this purpose, foot will add a + "default" action to the notification (see the + *command-action-argument* option). + + Second, foot needs to know when the notification is activated, + and it needs to get hold of the XDG activation token. + + Both are expected to be printed on stdout. + + Foot expects the action name (not label) to be printed on a + single line. No prefix, no postfix. + + Foot expects the activation token to be printed on a single + line, prefixed with *xdgtoken=*. + + Example: + default++ +xdgtoken=18179adf579a7a904ce73754964b1ec3 + + The expected format of stdout may change at any time. Please + read the changelog when upgrading foot. + + *Note*: notify-send does not, out of the box, support + reporting the XDG activation token in any way. This means + window activation will not work by default. + + Stdout + Foot recognizes the following things from the notification + helper's stdout: + + - _id_: integer in base 10, daemon assigned notification ID + - *id=*_id_: same as plain _nnn_. + - *default*: the 'default' action was triggered + - *action=*_default_: same as _default_ + - *action=*_n_: application custom action _n_ triggered + - _n_: integer in base 10, appearing after the ID; application + custom action _n_ triggered + - *xdgtoken=*_xyz_: XDG activation token. + + Example #1: + 17++ +action=default++ +xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 + + Foot recognizes this as: + - notification has the daemon assigned ID 17 + - the user triggered the default action + - the notification sent an XDG activation token + + Example #2: + 17++ +1 + + Foot recognizes this as: + - notification has the daemon assigned ID 17 + - the user triggered the first custom action, "1" + + Example #3: + id=17++ +1 + + Foot recognizes this as: + - notification has the daemon assigned ID 17 + - the user triggered the first custom action, "1" + + Default: _notify-send++ + --wait++ + --app-name ${app-id}++ + --icon ${app-id}++ + --category ${category}++ + --urgency ${urgency}++ + --expire-time ${expire-time}++ + --hint STRING:image-path:${icon}++ + --hint BOOLEAN:suppress-sound:${muted}++ + --hint STRING:sound-name:${sound-name}++ + --replace-id ${replace-id}++ + ${action-argument}++ + --print-id++ + -- ${title} ${body}_. + +*command-action-argument* + String to use with *command* to enable passing action/button names + to the notification helper. + + Foot will always configure a "default" action that can be used to + "activate" the notification, which in turn can cause the foot + window to be focused, or an escape to be sent to the terminal + application (depending on how the application generated the + notification). + + Furthermore, the OSC-99 notifications protocol allows applications + to define their own actions. Foot uses a combination of the + *command* option, and the *command-action-argument* option to pass + the names of the actions to the notification helper. + + This option has the following template arguments: + + - _${action-name}_: the name of the action; *default* for the + default action configured by foot, and _n_, where _n_ is an + integer >= 1, for application defined actions. + - _${action-label}_: *Activate* for the default action, and a + free-form string for application defined actions. + + For each notification action (remember, there will always be at + least one), *command-action-argument* will be expanded with the + action's name and label. + + Then, _${action-argument}_ is expanded in *command* to the full list + of actions. + + If *command-action-argument* is set to the empty string, no + actions will be passed to *command*. That is, _${action-argument}_ + will be replaced with the empty string. + + Example: + + *command-action-argument=--action ${action-name}=${action-label}*++ +*command=notify-send ${action-argument} ...* + + Assume the application defined two custom actions: *OK* and + *Cancel*. + + Given the above, foot will execute: + + notify-send++ + --action default='Click to activate'++ + --action 1=OK++ + --action 2=Cancel++ + ... + + Default: _--action ${action-name}=${action-label}_ + +*close* + Command to execute to close an existing notification. + + _${id}_ is expanded to the ID of the notification that should be + closed. For example: + + fyi --close ${id} + + Closing a notification is only supported by the Kitty Desktop + Notification protocol, OSC-99. + + If set to the empty string (the default), foot will instead try to + close the notification by sending SIGINT to the notification + helper process. For example, *notify-send --wait* (libnotify >= + 0.8.0) responds to SIGINT by closing the notification. + + Default: _not set_ + +*inhibit-when-focused* + Boolean. If enabled, foot will not display notifications if the + terminal window has keyboard focus. + + Default: _yes_ + +# SECTION: scrollback + +*lines* + Number of scrollback lines. The maximum number of allocated lines + will be this value plus the number of visible lines, rounded up to + the nearest power of 2. Default: _1000_. + +*multiplier* + Amount to multiply mouse scrolling with. It is a decimal number, + i.e. fractions are allowed. Default: _3.0_. + +*indicator-position* + Configures the style of the scrollback position indicator. One of + *none*, *fixed* or *relative*. *none* disables the indicator + completely. *fixed* always renders the indicator near the top of + the window, and *relative* renders the indicator at the position + corresponding to the current scrollback position. Default: + _relative_. + +*indicator-format* + Which format to use when displaying the scrollback position + indicator. Either _percentage_, _line_, or a custom fixed + string. This option is ignored if + *indicator-position=none*. Default: _empty string_. + +# SECTION: url + +Note that you can also add custom regular expressions, see the 'regex' +section. + +*launch* + Command to execute when opening URLs. _${url}_ will be replaced + with the actual URL. Default: _xdg-open ${url}_. + +*osc8-underline* + When to underline OSC-8 URLs. Possible values are *url-mode* and + *always*. + + When set to *url-mode*, OSC-8 URLs are only highlighted in URL + mode, just like auto-detected URLs. + + When set to *always*, OSC-8 URLs are always highlighted, + regardless of their other attributes (bold, italic etc). Note that + this does _not_ make them clickable. + + Default: _url-mode_ + +*style* + The underline style to use when rendering URL underlines. This + applies to both OSC-8 underlines when *osc8-underline=always*, and + all detected URLs in URL mode. One of *none*, *single*, *double*, + *curly*, *dotted* or *dashed*. + + Default: _dotted_ + +*label-letters* + String of characters to use when generating key sequences for URL + jump labels. + + If you change this option to include the letter *t*, you should + also change the default *[url-bindings].toggle-url-visible* key + binding to avoid a clash. + + Default: _sadfjklewcmpgh_. + +*regex* + Regular expression to use when auto-detecting URLs. The format is + "POSIX-Extended Regular Expressions". Note that the first marked + subexpression is used as the URL. In other words, if you want the + whole regex match to be used as an URL, surround all of it with + parenthesis: *(regex-pattern)*. + + Default: _(((https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)|www\.)([0-9a-zA-Z:/?#@!$&\*+,;=.~\_%^\-]+|\([]\["0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&\*+,;=.~\_%^\-]\*')+([0-9a-zA-Z/#@$&\*+=~\_%^\-]|\([]\["0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&\*+,;=.~\_%^\-]\*'))_ + +# SECTION: regex + +Similar to the 'url' mode, but with custom defined regular expressions +(and launchers). + +To use a custom defined regular expression, you also need to add a key +binding for it. This is done in the *key-binding* section, see below +for details. For example, a regex to detect hash digests (e.g. git +commit hashes) could look like: + +``` +[regex:hashes] +regex=([a-fA-F0-9]{7,128}) +launch=path-to-script-or-application ${match} + +[key-bindings] +regex-launch=[hashes] Control+Shift+q +regex-copy=[hashes] Control+Mod1+Shift+q +``` + +*launch* + Command to execute when "launching" a regex match. _${match}_ will + be replaced with the actual URL. Default: _not set_. + +*regex* + Regular expression to use when matching text. The format is + "POSIX-Extended Regular Expressions". Note that the first marked + subexpression is used as the match. In other words, if you want + the whole regex match to be used, surround all of it with + parenthesis: *(regex-pattern)*. + + Default: _not set_. + + +# SECTION: cursor + +This section controls the cursor style and color. Note that +applications can change these at runtime. + +*style* + Configures the default cursor style, and is one of: *block*, + *beam*, *underline* or *hollow*. Note that this can be overridden + by applications. Default: _block_. + +*unfocused-style* + Configures how the cursor is rendered when the terminal window is + unfocused. Possible values are: + + - unchanged: render cursor in exactly the same way as when the + window has focus. + - hollow: render a block cursor, but hollowed out. + - none: do not display any cursor at all. + +*blink* + Boolean. Enables blinking cursor. Note that this can be overridden + by applications. Related option: *blink-rate*. Default: _no_. + +*blink-rate* + The rate at which the cursor blinks, when cursor blinking has been + enabled. Expressed in milliseconds between each blink. Default: + _500_. + +*beam-thickness* + Thickness (width) of the beam styled cursor. The value is in + points, and its exact value thus depends on the monitor's DPI. To + instead specify a thickness in pixels, use the *px* suffix: + e.g. *beam-thickness=2px*. Default: _1.5_ + +*underline-thickness* + Thickness (height) of the underline styled cursor. The value is in + points, and its exact value thus depends on the monitor's DPI. + + To instead specify a thickness in pixels, use the *px* suffix: + e.g. *underline-thickness=2px*. + + Note that if left unset, the cursor's thickness will scale with + the font size, while if set, the size is fixed. + + Default: _font underline thickness_. + +# SECTION: mouse + +*hide-when-typing* + Boolean. When enabled, the mouse cursor is hidden while + typing. Default: _no_. + +*alternate-scroll-mode* + Boolean. This option controls the initial value for the _alternate + scroll mode_. When this mode is enabled, mouse scroll events are + translated to _up_/_down_ key events when displaying the alternate + screen. + + This lets you scroll with the mouse in e.g. pagers (like _less_) + without enabling native mouse support in them. + + Alternate scrolling is *not* used if the application enables + native mouse support. + + This option can be modified by applications at run-time using the + escape sequences *CSI ? 1007 h* (enable) and *CSI ? 1007 l* + (disable). + + Default: _yes_. + +# SECTION: touch + +*long-press-delay* + Number of milliseconds to distinguish between a short press and + a long press on the touchscreen. + + Default: _400_. + +# SECTION: colors-dark, colors-light + +These two sections controls the 16 ANSI colors, the default foreground +and background colors, and the extended 256 color palette. Note that +applications can change these at runtime. + +The colors are in RRGGBB format (i.e. plain old 6-digit hex values, +without prefix). That is, they do *not* have an alpha component. You +can configure the background transparency with the _alpha_ option. + +*colors-dark* is intended to define a dark color theme, and +*colors-light* is intended to define a light color theme. You can +switch between them using the *color-theme-switch-dark*, +*color-theme-switch-light* and *color-theme-toggle* key bindings, or +by sending SIGUSR1/SIGUSR2 to the foot process. + +The default theme used is *colors-dark*, unless +*initial-color-theme=light* has been set. + +*cursor* + Two space separated RRGGBB values (i.e. plain old 6-digit hex + values, without prefix) specifying the foreground (text) and + background (cursor) colors for the cursor. + + Example: *ff0000 00ff00* (green cursor, red text) + + Default: the regular foreground and background colors, reversed. + +*foreground* + Default foreground color. This is the color used when no ANSI + color is being used. Default: _839496_. + +*background* + Default background color. This is the color used when no ANSI + color is being used. Default: _002b36_. + +*regular0*, *regular1* *..* *regular7* + The eight basic ANSI colors (Black, Red, Green, Yellow, Blue, + Magenta, Cyan, White). Default: _242424_, _f62b5a_, _47b413_, + _e3c401_, _24acd4_, _f2affd_, _13c299_, _e6e6e6_ (starlight + theme, V4). + +*bright0*, *bright1* *..* *bright7* + The eight bright ANSI colors (Black, Red, Green, Yellow, Blue, + Magenta, Cyan, White). Default: _616161_, _ff4d51_, _35d450_, + _e9e836_, _5dc5f8_, _feabf2_, _24dfc4_, _ffffff_ (starlight + theme, V4). + +*dim0*, *dim1* *..* *dim7* + Custom colors to use with dimmed colors. Dimmed colors do not have + an entry in the color palette. Applications emit them by combining + a color value, and a "dim" attribute. + + By default, foot implements this by blending the current color + with black or white, depending on what the *dim-blend-towards* + option is set to . This is a generic approach that applies to both + colors from the 256-color palette, as well as 24-bit RGB colors. + + You can change this behavior by setting the *dimN* options. When + set, foot will match the current color against the color palette, + and if it matches one of the *regularN* colors, the corresponding + *dimN* color will be used. + + If instead the current color matches one of the *brightN* colors, + the corresponding *regularN* color will be used. + + If the current color does not match any known color, it is dimmed + by blending with black (i.e. the same behavior as if the *dimN* + options are unconfigured). 24-bit RGB colors will typically fall + into this category. + + Note that applications can change the *regularN* and *brightN* + colors at runtime. However, they have no way of changing the + *dimN* colors. If an application has changed the *regularN* + colors, foot will still use the corresponding *dimN* color, as + configured in foot.ini. + + Default: _not set_. + +*0* *..* *255* + Arbitrary colors in the 256-color palette. Default: for *0* *..* + *15*, see regular and bright defaults above; see + https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit for an + explanation of the remainder. + +*sixel0* *..* *sixel15* + The default sixel color palette. Default: _000000_, _3333cc_, + _cc2121_, _33cc33_, _cc33cc_, _33cccc_, _cccc33_, _878787_, + _424242_, _545499_, _994242_, _549954_, _995499_, _549999_, + _999954_, _cccccc_. + +*alpha* + Background translucency. A value in the range 0.0-1.0, where 0.0 + means completely transparent, and 1.0 is opaque. Default: _1.0_. + +*alpha-mode* + Specifies when *alpha* is applied. One of *default*, *matching* or + *all*. + + *default* applies *alpha* to cells with the default background + color, excluding cells with the same RGB value as the default + background color. + + *matching* is the same as *default*, but also applies *alpha* to + cells with the same RGB value as the default background color. + + *all* applies *alpha* to all cells, regardless of background color. + + Default: _default_ + +*blur* + Boolean. When enabled, foot will blur the background (main window + only, not CSDs etc), when it is transparent. This feature requires + the compositor to implement the _ext-background-effect-v1_ + protocol (and specifically, the _blur_ effect). + + Default: _no_ + +*dim-blend-towards* + Which color to blend towards when "auto" dimming a color (see + *dim0*..*dim7* above). One of *black* or *white*. Blending towards + black makes the text darker, while blending towards white makes it + whiter (but still dimmer than normal text). + + Default: _black_ (*colors-dark*), _white_ (*colors-light*) + +*selection-foreground*, *selection-background* + Foreground (text) and background color to use in selected + text. Default: _inverse foreground/background_. + +*jump-labels* + Two color values specifying the foreground (text) and background + colors to use when rendering jump labels in URL mode. Default: + _regular0 regular3_. + +*scrollback-indicator* + Two color values specifying the foreground (text) and background + (indicator itself) colors for the scrollback indicator. Default: + _regular0 bright4_. + +*search-box-no-match* + Two color values specifying the foreground (text) and background + colors for the scrollback search box, when there are no + matches. Default: _regular0 regular1_. + +*search-box-match* + Two color values specifying the foreground (text) and background + colors for the scrollback search box, when the search box is + either empty, or there are matches. Default: _regular0 regular3_. + +*urls* + Color to use for the underline used to highlight URLs in URL + mode. Default: _regular3_. + +*flash* + Color to use for the terminal window flash. Default: _7f7f00_. + +*flash-alpha* + Flash translucency. A value in the range 0.0-1.0, where 0.0 means + completely transparent, and 1.0 is opaque. Default: _0.5_. + +# SECTION: csd + +This section controls the look of the _CSDs_ (Client Side +Decorations). Note that the default is to *not* use CSDs, but instead +to use _SSDs_ (Server Side Decorations) when the compositor supports +it. + +Note that unlike the colors defined in the _colors_ section, the color +values here are in AARRGGBB (i.e. plain old 8-digit hex values) +format. I.e. they contain an alpha component - 00 means completely +transparent, and ff fully opaque. + +Examples: + +- ffffffff: white, fully opaque +- ff000000: black, fully opaque +- 7fffffff: white, semi-transparent +- ff00ff00: green, fully opaque + +*preferred* + Which type of window decorations to prefer: *client* (CSD), + *server* (SSD) or *none*. + + Note that this is only a hint to the compositor. Depending on + compositor support, and how it has been configured, it may + instruct foot to use CSDs even though this option has been set to + *server*, or render SSDs despite *client* or *none* being set. + + Default: _server_. + +*size* + Height, in pixels (subject to output scaling), of the + titlebar. Setting it to 0 will hide the titlebar, while still + showing the border (if *border-width* is set to a non-zero + value). Default: _26_. + +*color* + Titlebar color. Default: use the default _foreground_ color. + +*font* + Font to use for the title bar. This is a list of fonts, similar to + the main *font* option. Note that the font will be sized using the + title bar size. That is, all *:size* and *:pixelsize* attributes + will be ignored. Default: _primary font_. + +*hide-when-maximized* + Boolean. When enabled, the CSD titlebar is hidden when the window + is maximized. To completely disable the titlebar, set *size* to 0 + instead. Default: _no_. + +*double-click-to-maximize* + Boolean. When enabled, double-clicking the CSD titlebar will + (un)maximize the window. Default: _yes_. + +*border-width* + Width of the border, in pixels (subject to output scaling). Note + that the border encompasses the entire window, including the title + bar. Default: _0_. + +*border-color* + Color of border. By default, the title bar color is used. If the + title bar color has not been set, the default foreground color + (from the color scheme) is used. Default: _titlebar color_. + +*button-width* + Width, in pixels (subject to output scaling), of the + minimize/maximize/close buttons. Default: _26_. + +*button-color* + Foreground color on the minimize/maximize/close buttons and the + titlebar text. Default: use the default _background_ color. + +*button-minimize-color* + Minimize button's background color. Default: use the default + _regular4_ color (blue). + +*button-maximize-color* + Maximize button's background color. Default: use the default + _regular2_ color (green). + +*button-close-color* + Close button's background color. Default: use the default + _regular1_ color (red). + +# SECTION: key-bindings + +This section lets you override the default key bindings. + +The general format is _action=combo1...comboN_. That is, each action +may have one or more key combinations, space separated. Each +combination is in the form _mod1+mod2+key_. The names of the modifiers +and the key *must* be valid XKB key names. + +Note that if *Shift* is one of the modifiers, the _key_ *must not* be +in upper case. For example, *Control+Shift+V* will never trigger, but +*Control+Shift+v* will. + +The default key bindings all use "real" modifiers (*Mod1*, *Mod4* +etc), but "virtual" modifiers (*Alt*, *Super* etc) are allowed. + +*xkbcli interactive-wayland* can be useful for finding keysym names. + +When matching key presses to key bindings, foot uses a couple of +different approaches. + +As an example, let's say you press ctrl+shift+c (assume plain us ASCII +layout). XKB will tell foot *Control+C* was pressed. Note the lack of +the shift modifier, and the upper case 'C'. Internally, this is called +the "translated" form. + +The "untranslated" form (*Control+Shift+c*) is derived from the +translated form, and is what foot tries to match first. + +If no "untranslated" key bindings can be found, foot proceeds to +checking the "translated" variant. + +This means you can use either form in your foot configuration, and +that *Control+Shift+c* (and similar) has higher priority than +*Control+C*. Also note that while foot normally detects when the same +combination is assigned to multiple actions, it will not detect +*Control+C* vs. *Control+Shift+c* collisions. Call it a known bug... + +Finally, foot tries to match the raw key code. Here, the primary +layout is queried for all key codes that generate a particular XKB +symbol, and the pressed key's code is matched against this. For +example, if you use the layouts *"us,de(neo)"*, the 'r' key generates +the symbol 'c' in the neo layout. I.e. to get a 'c', you press +'r'. The match logic described above will only match 'c' key bindings +(e.g. *Control+Shift+c*). The raw mode however, will match 'r' key +bindings (e.g. *Control+Shift+r*). This is useful for non-latin +layouts, where you would otherwise have to customize all key bindings. + +A key combination can only be mapped to *one* action. Let's say you +want to bind *Control+Shift+R* to *fullscreen*. Since this is the +default shortcut for *search-start*, you first need to unmap the +default binding. This can be done by setting _action=none_; +e.g. *search-start=none*. + +*noop* + All key combinations listed here will not be sent to the + application. Default: _none_. + +*scrollback-up-page* + Scrolls up/back one page in history. Default: _Shift+Page\_Up + Shift+KP\_Page\_Up_. + +*scrollback-up-half-page* + Scrolls up/back half of a page in history. Default: _none_. + +*scrollback-up-line* + Scrolls up/back a single line in history. Default: _none_. + +*scrollback-down-page* + Scroll down/forward one page in history. Default: + _Shift+Page\_Down Shift+KP\_Page\_Down_. + +*scrollback-down-half-page* + Scroll down/forward half of a page in history. Default: _none_. + +*scrollback-down-line* + Scroll down/forward a single line in history. Default: _none_. + +*scrollback-home* + Scroll to the beginning of the scrollback. Default: _none_. + +*scrollback-end* + Scroll to the end (bottom) of the scrollback. Default: _none_. + +*clipboard-copy* + Copies the current selection into the _clipboard_. Default: _Control+Shift+c_ + _XF86Copy_. + +*clipboard-paste* + Pastes from the _clipboard_. Default: _Control+Shift+v_ _XF86Paste_. + +*primary-paste* + Pastes from the _primary selection_. Default: _Shift+Insert_ (also + defined in *mouse-bindings*). + +*search-start* + Starts a scrollback/history search. Default: _Control+Shift+r_. + +*font-increase* + Increases the font size by 0.5pt. Default: _Control+plus + Control+equal Control+KP\_Add_ (also defined in *mouse-bindings*). + +*font-decrease* + Decreases the font size by 0.5pt. Default: _Control+minus + Control+KP\_Subtract_ (also defined in *mouse-bindings*). + +*font-reset* + Resets the font size to the default. Default: _Control+0 Control+KP\_0_. + +*spawn-terminal* + Spawns a new terminal. If the shell has been configured to emit + the OSC 7 escape sequence, the new terminal will start in the + current working directory. Default: _Control+Shift+n_. + +*minimize* + Minimizes the window. Default: _none_. + +*maximize* + Toggle the maximized state. Default: _none_. + +*fullscreen* + Toggles the fullscreen state. Default: _none_. + +*pipe-visible*, *pipe-scrollback*, *pipe-selected*, *pipe-command-output* + Pipes the currently visible text, the entire scrollback, the + currently selected text, or the last command's output to an + external tool. The syntax for this option is a bit special; the + first part of the value is the command to execute enclosed in + "[]", followed by the binding(s). + + You can configure multiple pipes as long as the command strings + are different and the key bindings are unique. + + Note that the command is *not* automatically run inside a shell; + use *sh -c "command line"* if you need that. + + Example #1: + # Extract currently visible URLs, let user choose one (via + fuzzel), then launch firefox with the selected URL++ +*pipe-visible=[sh -c "xurls | uniq | tac | fuzzel | xargs -r firefox"] Control+Print* + + Example #2: + # Open scrollback contents in Emacs running in a new foot instance++ +*pipe-scrollback=[sh -c "f=$(mktemp) && cat - > $f && foot emacsclient -t $f; rm $f"] Control+Shift+Print* + + Default: _none_ + +*show-urls-launch* + Enter URL mode, where all currently visible URLs are tagged with a + jump label with a key sequence that will open the URL (and exit + URL mode). Default: _Control+Shift+o_. + +*show-urls-persistent* + Similar to *show-urls-launch*, but does not automatically exit URL + mode after activating an URL. Default: _none_. + +*show-urls-copy* + Enter URL mode, where all currently visible URLs are tagged with a + jump label with a key sequence that will place the URL in the + clipboard. If the hint is completed with an uppercase character, + the match will also be pasted. Default: _none_. + +*regex-launch* + Enter regex mode. This works exactly the same as URL mode; all + regex matches are tagged with a jump label with a key sequence + that will "launch" to match (and exit regex mode). + + The name of the regex section must be specified in the key + binding: + + ``` + [regex:hashes] + regex=([a-fA-F0-9]{7,128}) + launch=path-to-script-or-application ${match} + + [key-bindings] + regex-launch=[hashes] Control+Shift+q + regex-copy=[hashes] Control+Mod1+Shift+q + ``` + + Default: _none_. + +*regex-copy* + Same as *regex-launch*, but the match is placed in the clipboard, + instead of "launched", upon activation. If the hint is completed + with an uppercase character, the match will also be pasted. + Default: _none_. + +*prompt-prev* + Jump to the previous, currently not visible, prompt (requires + shell integration, see *foot*(1)). Default: _Control+Shift+z_. + +*prompt-next* + Jump to the next prompt (requires shell integration, see + *foot*(1)). Default: _Control+Shift+x_. + +*unicode-input* + Input a Unicode character by typing its codepoint in hexadecimal, + followed by *Enter* or *Space*. + + For example, to input the character _ö_ (LATIN SMALL LETTER O WITH + DIAERESIS, Unicode codepoint 0xf6), you would first activate this + key binding, then type: *f*, *6*, *Enter*. + + Another example: to input 😍 (SMILING FACE WITH HEART-SHAPED EYES, + Unicode codepoint 0x1f60d), activate this key binding, then type: + *1*, *f*, *6*, *0*, *d*, *Enter*. + + Recognized key bindings in Unicode input mode: + + - Enter, Space: commit the Unicode character, then exit this mode. + - Escape, q, Ctrl+c, Ctrl+d, Ctrl+g: abort input, then exit this mode. + - 0-9, a-f: append next digit to the Unicode's codepoint. + - Backspace: undo the last digit. + + Note that there is no visual feedback while in this mode. This is + by design; foot's Unicode input mode is considered to be a + fallback. The preferred way of entering Unicode characters, emojis + etc is by using an IME. + + Default: _Control+Shift+u_. + +*color-theme-switch-dark*, *color-theme-switch-light*, *color-theme-toggle* + Switch between the dark color theme (defined in the *colors-dark* + section), and the light color theme (defined in the *colors-light* + section). + + *color-theme-switch-dark* applies the dark color theme regardless + of which color theme is currently active. + + *color-theme-switch-light* applies the light color theme + regardless of which color theme is currently active. + + *color-theme-toggle* toggles between the primary and alternative + color themes. + + Note: you can also send SIGUSR1/SIGUSR2 to the foot process to + change the theme (see *foot*(1) for details.) + + Default: _none_ + +*quit* + Quit foot. Default: _none_. + +# SECTION: search-bindings + +This section lets you override the default key bindings used in +scrollback search mode. The syntax is exactly the same as the regular +**key-bindings**. + +*cancel* + Aborts the search. The viewport is restored and the _primary + selection_ is **not** updated. Default: _Control+g Control+c + Escape_. + +*commit* + Exit search mode and copy current selection into the _primary + selection_. Viewport is **not** restored. To copy the selection to + the regular _clipboard_, use *Control+Shift+c*. Default: _Return + KP_Enter_. + +*find-prev* + Search **backwards** in the scrollback history for the next + match. Default: _Control+r_. + +*find-next* + Searches **forwards** in the scrollback history for the next + match. Default: _Control+s_. + +*cursor-left* + Moves the cursor in the search box one **character** to the + left. Default: _Left Control+b_. + +*cursor-left-word* + Moves the cursor in the search box one **word** to the + left. Default: _Control+Left Mod1+b_. + +*cursor-right* + Moves the cursor in the search box one **character** to the + right. Default: _Right Control+f_. + +*cursor-right-word* + Moves the cursor in the search box one **word** to the + right. Default: _Control+Right Mod1+f_. + +*cursor-home* + Moves the cursor in the search box to the beginning of the + input. Default: _Home Control+a_. + +*cursor-end* + Moves the cursor in the search box to the end of the + input. Default: _End Control+e_. + +*delete-prev* + Deletes the **character before** the cursor. Default: _BackSpace_. + +*delete-prev-word* + Deletes the **word before** the cursor. Default: _Mod1+BackSpace + Control+BackSpace_. + +*delete-next* + Deletes the **character after** the cursor. Default: _Delete_. + +*delete-next-word* + Deletes the **word after** the cursor. Default: _Mod1+d + Control+Delete_. + +*delete-to-start* + Deletes search input before the cursor. Default: _Ctrl+u_. + +*delete-to-end* + Deletes search input after the cursor. Default: _Ctrl+k_. + +*extend-char* + Extend current selection to the right, by one character. Default: + _Shift+Right_. + +*extend-to-word-boundary* + Extend current selection to the right, to the next word + boundary. Default: _Control+w Control+Shift+Right_. + +*extend-to-next-whitespace* + Extend the current selection to the right, to the next + whitespace. Default: _Control+Shift+w_. + +*extend-line-down* + Extend current selection down one line. Default: _Shift+Down_. + +*extend-backward-char* + Extend current selection to the left, by one character. Default: + _Shift+Left_. + +*extend-backward-to-word-boundary* + Extend current selection to the left, to the next word + boundary. Default: _Control+Shift+Left_. + +*extend-backward-to-next-whitespace* + Extend the current selection to the left, to the next + whitespace. Default: _none_. + +*extend-line-up* + Extend current selection up one line. Default: _Shift+Up_. + +*clipboard-paste* + Paste from the _clipboard_ into the search buffer. Default: + _Control+v Control+y Control+Shift+v XF86Paste_. + +*primary-paste* + Paste from the _primary selection_ into the search + buffer. Default: _Shift+Insert_. + +*unicode-input* + Unicode input mode. See _key-bindings.unicode-input_ for + details. Default: _none_. + +*scrollback-up-page* + Scrolls up/back one page in history. Default: _Shift+Page\_Up + Shift+KP\_Page\_Up_. + +*scrollback-up-half-page* + Scrolls up/back half of a page in history. Default: _none_. + +*scrollback-up-line* + Scrolls up/back a single line in history. Default: _none_. + +*scrollback-down-page* + Scroll down/forward one page in history. Default: + _Shift+Page\_Down Shift+KP\_Page\_Down_. + +*scrollback-down-half-page* + Scroll down/forward half of a page in history. Default: _none_. + +*scrollback-down-line* + Scroll down/forward a single line in history. Default: _none_. + +*scrollback-home* + Scroll to the beginning of the scrollback. Default: _none_. + +*scrollback-end* + Scroll to the end (bottom) of the scrollback. Default: _none_. + +# SECTION: url-bindings + +This section lets you override the default key bindings used in URL +mode. The syntax is exactly the same as the regular **key-bindings**. + +Be careful; do not use single-letter keys that are also used in +*[url].label-letters*, as doing so will make some URLs inaccessible. + +*cancel* + Exits URL mode without opening a URL. Default: _Control+g + Control+c Control+d Escape_. + +*toggle-url-visible* + By default, the jump label only shows the key sequence required to + activate it. This is fine as long as the URL is visible in the + original text. + + But with e.g. OSC-8 URLs (the terminal version of HTML anchors, + i.e. "links"), the text on the screen can be something completely + different than the URL. + + This action toggles between showing and hiding the URL on the jump + label. + + Default: _t_. + +# SECTION: text-bindings + +This section lets you remap key combinations to custom escape +sequences. + +The format is _text=combo1...comboN_. That is, the string to emit may +have one or more key combinations, space separated. Each combination +is in the form _mod1+mod2+key_. The names of the modifiers and the key +*must* be valid XKB key names. + +The text string specifies the characters, or bytes, to emit when the +associated key combination(s) are pressed. There are two ways to +specify a character: + +- Normal, printable characters are written as-is: *abcdef*. +- Bytes (e.g. ESC) are written as two-digit hexadecimal numbers, with + a *\\x* prefix: *\\x1b*. + +Example: you would like to remap _Super+k_ to the _Up_ key. + +The escape sequence for the Up key is _ESC [ A_ (without the +spaces). Thus, we need to specify this in foot.ini (*Mod4* is the XKB +name for the Super/logo key): + +*\\x1b[A = Mod4+k* + +Another example: to remap _Super+c_ to _Control+c_: + +*\\x03 = Mod4+c* + +# SECTION: mouse-bindings + +This section lets you override the default mouse bindings. + +The general format is _action=combo1...comboN_. That is, each action +may have one or more key combinations, space separated. Each +combination is in the form _mod1+mod2+BTN\_<name>[-COUNT]_. The names +of the modifiers *must* be valid XKB key names, and the button name +*must* be a valid libinput name. You can find the button names using +*libinput debug-events*. + +The trailing *COUNT* (number of times the button has to be clicked) is +optional and specifies the click count required to trigger the +binding. The default if *COUNT* is omitted is _1_. + +To map wheel events (i.e. scrolling), use the button names +*BTN_WHEEL_BACK* (up) and *BTN_WHEEL_FORWARD* (down). Note that these +events never generate a *COUNT* larger than 1. That is, +*BTN_WHEEL_BACK+2*, for example, will never trigger. + +Foot also recognizes tiltable wheels; to map these, use +*BTN_WHEEL_LEFT* and *BTN_WHEEL_RIGHT*. + +A modifier+button combination can only be mapped to *one* action. Let's +say you want to bind *BTN\_MIDDLE* to *fullscreen*. Since +*BTN\_MIDDLE* is the default binding for *primary-paste*, you first +need to unmap the default binding. This can be done by setting +_action=none_; e.g. *primary-paste=none*. + +*selection-override-modifiers* + The modifiers set in this set (which may be set to any combination + of modifiers, e.g. _mod1+mod2+mod3_, as well as _none_) are used + to enable selecting text with the mouse irrespective of whether a + client application currently has the mouse grabbed. + These modifiers cannot be used as modifiers in mouse bindings. + Because the order of bindings is significant, it is best to set + this prior to any other mouse bindings that might use modifiers in + the default set. + Default: _Shift_ + +The actions to which mouse combos can be bound are listed below. All +actions listed under *key-bindings* can be used here as well. + +*scrollback-up-mouse* + Normal screen: scrolls up the contents. + + Alt screen: send fake _KeyUP_ events to the client application, if + alternate scroll mode is enabled. + + Default: _BTN\_WHEEL\_BACK_ + +*scrollback-down-mouse* + Normal screen: scrolls down the contents. + + Alt screen: send fake _KeyDOWN_ events to the client application, if + alternate scroll mode is enabled. + + Default: _BTN\_WHEEL\_FORWARD_ + +*select-begin* + Begin an interactive selection. The selection is finalized, and + copied to the _primary selection_, when the button is + released. Default: _BTN\_LEFT_. + +*select-begin-block* + Begin an interactive block selection. The selection is finalized, + and copied to the _primary selection_, when the button is + released. Default: _Control+BTN\_LEFT_. + +*select-word* + Begin an interactive word-wise selection, where words are + separated by whitespace and all characters defined by the + *word-delimiters* option. The selection is finalized, and copied + to the _primary selection_, when the button is released. Default: + _BTN\_LEFT-2_. + +*select-word-whitespace* + Same as *select-word*, but the characters in the *word-delimiters* + option are ignored. I.e only whitespace characters act as + delimiters. The selection is finalized, and copied to the _primary + selection_, when the button is released. Default: + _Control+BTN\_LEFT-2_. + +*select-quote* + Begin an interactive "quote" selection. This is similar to + *select-word*, except an entire quote is selected (that is, + everything inside the quote, excluding the quote + characters). Recognized quote characters are: *"* and *'*. + + If a complete quote cannot be found on the current logical row + (only one quote character, or none are found), the entire row is + selected. + + The selection is finalized, and copied to the _primary selection_, + when the button is released. + + After the initial selection has been made, it behaves like a + normal word, or row selection, depending on whether a quote was + found or not. This affects what happens when, for example, + extending the selection. + + Notes: + - Escaped quote characters are not supported (*"foo \\"bar"* will + match *'foo \\'*, not *'foo "bar'*). + - Foot does not try to handle mismatched quote characters; they + will simply not match. + - Nested quotes (using different quote characters) are supported. + + Default: _BTN\_LEFT-3_. + +*select-row* + Begin an interactive row-wise selection. The selection is + finalized, and copied to the _primary selection_, when the button + is released. Default: _BTN\_LEFT-4_. + +*select-extend* + Interactively extend an existing selection, using the original + selection mode (normal, block, word-wise or row-wise). The + selection is finalized, and copied to the _primary selection_, + when the button is released. Default: _BTN\_RIGHT_. + +*select-extend-character-wise* + Same as *select-extend*, but forces the selection mode to _normal_ + (i.e. character wise). Note that this causes subsequent + *select-extend* operations to be character wise. This action is + ignored for block selections. Default: _Control+BTN\_RIGHT_. + +*primary-paste* + Pastes from the _primary selection_. Default: _BTN\_MIDDLE_. + +*font-increase* + Increases the font size by 0.5pt. Default: + _Control+BTN\_WHEEL\_BACK_ (also defined in *key-bindings*). + +*font-decrease* + Decreases the font size by 0.5pt. Default: + _Control+BTN\_WHEEL\_FORWARD_ (also defined in *key-bindings*). + + +# TWEAK + +This section is for advanced users and describes configuration options +that can be used to tweak foot's low-level behavior. + +These options are *not* included in the example configuration. You +should not change these unless you understand what they do. + +Note that these options may change, or be removed at any time, without +prior notice. + +When reporting bugs, please mention if, and to what, you have changed +any of these options. + +*scaling-filter* + Overrides the default scaling filter used when down-scaling bitmap + fonts (e.g. emoji fonts). Possible values are *none*, *nearest*, + *bilinear*, *impulse*, *box*, *linear*, *cubic*, *gaussian*, + *lanczos2*, *lanczos3* or *lanczos3-stretched*. + + Default: _lanczos3_. + +*overflowing-glyphs* + Boolean. When enabled, glyphs wider than their cell(s) are allowed + to render into one additional neighbouring cell. + + One use case for this are fonts with wide italic characters that + "bend" into the next cell. Without this option, such glyphs will + appear "cut off". + + Another use case are fonts with "icon" characters in the Unicode + private usage area, e.g. Nerd Fonts, or Powerline Fonts and legacy + emoji characters like *WHITE FROWNING FACE*. + + Note: might impact performance depending on the font used. + Especially small font sizes can cause many overflowing glyphs + because of subpixel rendering. + + Default: _yes_. + +*render-timer* + Enables a frame rendering timer, that prints the time it takes to + render each frame, in microseconds, either on-screen, to stderr, + or both. Valid values are *none*, *osd*, *log* and + *both*. Default: _none_. + +*box-drawing-base-thickness* + Line thickness to use for *LIGHT* box drawing line characters, in + points. This value is converted to pixels using the monitor's DPI, + and then multiplied with the cell size. The end result is that a + larger font (and thus larger cells) result in thicker + lines. Default: _0.04_. + +*box-drawing-solid-shades* + Boolean. When enabled, box drawing "shades" (e.g. LIGHT SHADE, + MEDIUM SHADE and DARK SHADE) are rendered as solid blocks using a + darker variant of the current foreground color. + + When disabled, they are instead rendered as checker box pattern, + using the current foreground color as is. + + Default: _yes_. + +*delayed-render-lower*, *delayed-render-upper* + These two values control the timeouts (in nanoseconds) that are + used to mitigate screen flicker caused by clients writing large, + non-atomic screen updates. + + If a client splits up a screen update over multiple *write*(3) + calls, we may end up rendering an intermediate frame, quickly + followed by another frame with the final screen content. For + example, the client may erase part of the screen (or scroll) in + one write, and then write new content in one or more subsequent + writes. Rendering the frame when the screen has been erased, but + not yet filled with new content will be perceived as screen + flicker. + + The *real* solution to this is _Application Synchronized Updates_ + (https://gitlab.freedesktop.org/terminal-wg/specifications/-/merge_requests/2). + + The problem with this is twofold - first, it has not yet been + standardized, and thus there are not many terminal emulators that + implement it (foot *does* implement it), and second, applications + must be patched to use it. + + Until this has happened, foot offers an interim workaround; an + attempt to mitigate the screen flicker *without* affecting either + performance or latency. + + It is based on the fact that the screen is updated at a fixed + interval (typically 60Hz). For us, this means it does not matter + if we render a new frame at the *beginning* of a frame interval, + or at the *end*. Thus, the goal is to introduce a delay between + receiving client data and rendering the resulting state, but + without causing a frame skip. + + While it should be possible to estimate the amount of time left + until the next frame, foot's algorithm is currently not that + advanced, but is based on statistics I guess you could say - the + delay we introduce is so small that the risk of pushing the frame + over to the next frame interval is also very small. + + Now, that was a lot of text. But what is it foot actually does? + + When receiving client data, it schedules a timer, the + *delayed-render-lower*. If we do not receive any more client data + before the timer has run out, we render the frame. If however, we + do receive more data, the timer is re-scheduled. That is, each + time we receive client data, frame rendering is delayed another + *delayed-render-lower* nanoseconds. + + Now, while this works very well with most clients, it would be + possible to construct a malicious client that keeps writing data + at a slow pace. To the user, this would look like foot has frozen + as we never get to render a new frame. To prevent this, an upper + limit is set - *delayed-render-upper*. If this timer runs out, we + render the frame regardless of what the client is doing. + + If changing these values, note that the lower timeout *must* be + set lower than the upper timeout, but that this is not verified by + foot. Furthermore, both values must be less than 16ms (that is, + 16000000 nanoseconds). + + You can disable the feature altogether by setting either value to + 0. In this case, frames are rendered "as soon as possible". + + Default: lower=_500000_ (0.5ms), upper=_8333333_ (8.3ms - half a + frame interval). + +*damage-whole-window* + Boolean. When enabled, foot will 'damage' the entire window each + time a frame has been rendered. This forces the compositor to + redraw the entire window. If disabled, foot will only 'damage' + updated rows. + + There is normally *no* reason to enable this. However, it has been + seen to workaround an issue with _fractional scaling_ in _Gnome_. + + Note that enabling this option is likely to increase CPU and/or + GPU usage (by the compositor, not by foot), and may have a + negative impact on battery life. + + Default: _no_. + +*grapheme-shaping* + Boolean. When enabled, foot will use _utf8proc_ to do grapheme + cluster segmentation while parsing "printed" text. Then, when + rendering, it will use _fcft_ (if compiled with _HarfBuzz_ + support) to shape the grapheme clusters. + + This is required to render e.g. flag (emoji) sequences, keycap + sequences, modifier sequences, zero-width-joiner (ZWJ) sequences + and emoji tag sequences. It might also improve rendering of + composed characters, depending on font. + + - foot must have been compiled with utf8proc support + - fcft must have been compiled with HarfBuzz support + + This option can also be set runtime with DECSET/DECRST 2027. + + See also: *grapheme-width-method*. + + Default: _yes_ + +*grapheme-width-method* + Selects which method to use when calculating the width + (i.e. number of columns) of a grapheme cluster. One of + *wcswidth*, *double-width* and *max*. + + *wcswidth* simply adds together the individual width of all + codepoints making up the cluster. + + *double-width* does the same, but limits the maximum number of + columns to 2. This is more correct, but may break some + applications since applications typically use *wcswidth*(3) + internally to calculate the width. This results in cursor + de-synchronization issues. + + *max* uses the width of the largest codepoint in the cluster. + + Default: _double-width_ + +*font-monospace-warn* + Boolean. When enabled, foot will use heuristics to try to verify + the primary font is a monospace font, and warn if it is not. + + Disable this if you still want to use the font, even if foot + thinks it is not monospaced. + + You may also want to disable it to get slightly faster startup times. + + Default: _yes_ + +*max-shm-pool-size-mb* + This option controls the amount of virtual address space used by + the pixmap memory to which the terminal screen content is + rendered. + + It does not change how much physical memory foot uses. + + Foot uses a memory mapping trick to implement fast rendering of + interactive scrolling (typically, but applies to "slow" scrolling + in general). Example: holding down the 'up' or 'down' arrow key to + scroll in a text editor. + + For this to work, it needs a large amount of virtual address + space. Again, note that this is not physical memory. + + On a normal x64 based computer, each process has 128TB of virtual + address space, and newer ones have 64PB. This is an insane amount + and most applications do not use anywhere near that amount. + + Each foot terminal window can allocate up to 2GB of virtual + address space. With 128TB of address space, that means a maximum + of 65536 windows in server/daemon mode (for 2GB). That should be + enough, yes? + + However, the Wayland compositor also needs to allocate the same + amount of virtual address space. Thus, it has a slightly higher + chance of running out of address space since it needs to host + all running Wayland clients in the same way, at the same time. + + In the off chance that this becomes a problem for you, you can + reduce the amount used with this option. + + Or, for optimal performance, you can increase it to the maximum + allowed value, 2GB (but note that you most likely will not notice + any difference compared to the default value). + + Setting it to 0 disables the feature. + + Limitations: + - only supported on 64-bit architectures + - only supported on Linux + + Default: _512_. Maximum allowed: _2048_ (2GB). + +*min-stride-alignment* + This option controls the minimum stride alignment, in bytes, when + allocating SHM buffers. + + In some circumstances, a compositor can import foot's SHM buffers + directly to the GPU, without copying the buffer to GPU memory + (typically on integrated graphics). Different drivers have + different requirements for this, and one of those requirements is + typically the stride alignment. At the time of writing, AMD GPUs + require 256-byte alignment. + + Note that doing a direct import typically disables immediate + buffer release (if the compositor supports that), which means foot + has to double buffer. This adds a performance penalty in foot, but + the overall system performance should still be better. + + If you are not using integrated graphics, or if the compositor + does not support GPU direct imports, this option has close to zero + impact. You can save a small amount of memory by setting this to + 0. + + Ultimately, it is up to the compositor to decide whether to do + immediate buffer releases, or try to optimize GPU imports. + + Default: _256_ + +*sixel* + Boolean. When enabled, foot will process sixel images. Default: + _yes_ + +*dim-amount* + Amount by which dimmed text is darkened. Default: _1.5_. + +*bold-text-in-bright-amount* + Amount by which bold fonts are brightened when + *bold-text-in-bright* is set to *yes* (the *palette-based* variant + is not affected by this option). Default: _1.3_. + +*surface-bit-depth* + Selects which RGB bit depth to use for image buffers. One of + *auto*, *8-bit*, *10-bit* or *16-bit*. + + *auto* chooses bit depth depending on other settings, and + availability. + + *8-bit*, uses 8 bits for each color channel, alpha included. This + is the default when *gamma-correct-blending=no*. + + *10-bit* uses 10 bits for each RGB channel, and 2 bits for the + alpha channel. Thus, it provides higher precision color channels, + but a lower precision alpha channel. + + *16-bit* 16 bits for each color channel, alpha included. If + available, this is the default when *gamma-correct-blending=yes*. + + Note that both *10-bit* and *16-bit* are much slower than *8-bit*; + if you want to use gamma-correct blending, and if you prefer speed + (throughput and input latency) over accurate colors, you can set + *surface-bit-depth=8-bit* explicitly. + + Default: _auto_ + +*pre-apply-damage* + Boolean. When enabled, foot will attempt to "pre-apply" the damage + from the last frame when foot is forced to double-buffer + (i.e. when the compositor does not release SHM buffers + immediately). All text after this assumes the compositor is not + releasing buffers immediately. + + When this option is disabled, each time foot needs to render a + frame, it has to first copy over areas that changed in the last + frame (i.e. all changes between the last two frames). This is + basically a *memcpy*(3), which can be slow if the changed area is + large. It is also done on the main thread, which means foot cannot + do anything else at the same time; no other rendering, no VT + parsing. After the changes have been brought over to the new + frame, foot proceeds with rendering the cells that has changed + between the last frame and the new frame. + + When this option is enabled, the changes between the last two frames + are brought over to what will become the next frame before foot + starts rendering the next frame. As soon as the compositor + releases the previous buffer (typically right after foot has + pushed a new frame), foot kicks off a thread that copies over the + changes to the newly released buffer. Since this is done in a + thread, foot can continue processing input at the same + time. Later, when it is time to render a new frame, the changes + have already been transferred, and foot can immediately start with + the actual rendering. + + Thus, having this option enabled improves both performance + (copying the last two frames' changes is threaded), and improves + input latency (rendering the next frame no longer has to first bring + over the changes between the last two frames). + + Default: _yes_ + +# SEE ALSO + +*foot*(1), *footclient*(1) diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd new file mode 100644 index 0000000..ad86591 --- /dev/null +++ b/doc/footclient.1.scd @@ -0,0 +1,214 @@ +footclient(1) + +# NAME +footclient - start new terminals in a foot server + +# SYNOPSIS +*footclient* [_OPTIONS_]++ +*footclient* [_OPTIONS_] <_command_> [_COMMAND OPTIONS_] + +All trailing (non-option) arguments are treated as a command, and its +arguments, to execute (instead of the default shell). + +# DESCRIPTION + +*footclient* is used together with *foot*(1) in *--server* +mode. + +Running it without arguments will open a new terminal window (hosted +in the foot server), with your default shell. The exit code will be +that of the terminal. I.e *footclient* does not exit until the +terminal has terminated. + +# OPTIONS + +*-t*,*--term*=_TERM_ + Value to set the environment variable *TERM* to (see *TERMINFO* + and *ENVIRONMENT*). Default: _@default_terminfo@_. + +*-T*,*--title*=_TITLE_ + Initial window title. Default: _foot_. + +*-a*,*--app-id*=_ID_ + Value to set the *app-id* property on the Wayland window + to. Default: _foot_ (normal mode), or _footclient_ (server mode). + +*toplevel-tag*=_TAG_ + Value to set the *toplevel-tag* property on the Wayland window + to. The compositor can use this value for session management, + window rules etc. Default: _not set_ + +*-w*,*--window-size-pixels*=_WIDTHxHEIGHT_ + Set initial window width and height, in pixels. Default: _700x500_. + +*-W*,*--window-size-chars*=_WIDTHxHEIGHT_ + Set initial window width and height, in characters. Default: _not set_. + +*-m*,*--maximized* + Start in maximized mode. If both *--maximized* and *--fullscreen* + are specified, the _last_ one takes precedence. + +*-F*,*--fullscreen* + Start in fullscreen mode. If both *--maximized* and *--fullscreen* + are specified, the _last_ one takes precedence. + +*-L*,*--login-shell* + Start a login shell, by prepending a '-' to argv[0]. + +*-D*,*--working-directory*=_DIR_ + Initial working directory for the client application. Default: + _CWD of footclient_. + +*-s*,*--server-socket*=_PATH_ + Connect to _PATH_ instead of + *$XDG\_RUNTIME\_DIR/foot-$WAYLAND\_DISPLAY.sock*. + +*-H*,*--hold* + Remain open after child process exits. + +*-N*,*--no-wait* + Detach the client process from the running terminal, exiting + immediately. + +*-o*,*--override*=[_SECTION_.]_KEY_=_VALUE_ + Override an option set in the configuration file. If _SECTION_ is not + given, defaults to _main_. + +*-E*,*--client-environment* + The child process in the new terminal instance will use + footclient's environment, instead of the server's. + + Environment variables listed in the *Variables set in the child + process* section will be overwritten by the foot server. For + example, the new terminal will use *TERM* from the configuration, + not footclient's environment. + +*-d*,*--log-level*={*info*,*warning*,*error*,*none*} + Log level, used both for log output on stderr as well as + syslog. Default: _warning_. + +*-l*,*--log-colorize*=[{*never*,*always*,*auto*}] + Enables or disables colorization of log output on stderr. + +*-v*,*--version* + Show the version number and quit + +*-e* + Ignored; for compatibility with *xterm -e*. See *foot*(1) for more + details. + +# EXIT STATUS + +Footclient will exit with code 220 if there is a failure in footclient +itself (for example, the server socket does not exist). + +If *-N*,*--no-wait* is used, footclient exits with code 0 as soon as +the foot server has been instructed to open a new window. + +If not, footclient may also exit with code 230. This indicates a +failure in the foot server. + +In all other cases the exit code is that of the client application +(i.e. the shell). + +# TERMINFO + +Client applications use the terminfo identifier specified by the +environment variable *TERM* (set by foot) to determine terminal +capabilities. + +Foot has two terminfo definitions: *foot* and *foot-direct*, with +*foot* being the default. + +The difference between the two is in the number of colors they +describe; *foot* describes 256 colors and *foot-direct* 16.7 million +colors (24-bit truecolor). + +Note that using the *foot* terminfo does not limit the number of +usable colors to 256; applications can still use 24-bit RGB colors. In +fact, most applications work best with *foot* (including 24-bit +colors)). Using *\*-direct* terminfo entries has been known to crash +some ncurses applications even. + +There are however applications that need a *\*-direct* terminfo entry +for 24-bit support. Emacs is one such example. + +While using either *foot* or *foot-direct* is strongly recommended, it +is possible to use e.g. *xterm-256color* as well. This can be useful +when remoting to a system where foot's terminfo entries cannot easily +be installed. + +Note that terminfo entries can be installed in the user's home +directory. I.e. if you do not have root access, or if there is no +distro package for foot's terminfo entries, you can install foot's +terminfo entries manually, by copying *foot* and *foot-direct* to +*~/.terminfo/f/*. + +# ENVIRONMENT + +## Variables used by footclient + +*XDG\_RUNTIME\_DIR* + Used to construct the default _PATH_ for the *--server-socket* + option, when no explicit argument is given (see above). + +*WAYLAND\_DISPLAY* + Used to construct the default _PATH_ for the *--server-socket* + option, when no explicit argument is given (see above). + +If the socket at default _PATH_ does not exist, *footclient* will +fallback to the less specific path, with the following priority: +*$XDG\_RUNTIME\_DIR/foot-$WAYLAND\_DISPLAY.sock*, +*$XDG\_RUNTIME\_DIR/foot.sock*, */tmp/foot.sock*. + +## Variables set in the child process + +*TERM* + terminfo/termcap identifier. This is used by client applications + to determine which capabilities a terminal supports. The value is + set according to either the *--term* command-line option or the + *term* config option in *foot.ini*(5). + +*COLORTERM* + This variable is set to *truecolor*, to indicate to client + applications that 24-bit RGB colors are supported. + +*PWD* + Current working directory (at the time of launching foot) + +*SHELL* + Set to the launched shell, if the shell is valid (it is listed in + */etc/shells*). + +In addition to the variables listed above, custom environment +variables may be defined in *foot.ini*(5). + +## Variables *unset* in the child process + +*TERM_PROGRAM* +*TERM_PROGRAM_VERSION* + These environment variables are set by certain other terminal + emulators. We unset them, to prevent applications from + misdetecting foot. + +In addition to the variables listed above, custom environment +variables to unset may be defined in *foot.ini*(5). + +# Signals + +The following signals have special meaning in footclient: + +- SIGUSR1: switch to the dark color theme (*[colors-dark]*). +- SIGUSR2: switch to the light color theme (*[colors-light]*). + +When sending SIGUSR1/SIGUSR2 to a footclient instance, the theme is +changed in that instance only. This is different from when you send +SIGUSR1/SIGUSR2 to the server process, where all instances change the +theme. + +Note: for obvious reasons, this is not supported when footclient is +started with *--no-wait*. + +# SEE ALSO + +*foot*(1) diff --git a/doc/meson.build b/doc/meson.build new file mode 100644 index 0000000..3797265 --- /dev/null +++ b/doc/meson.build @@ -0,0 +1,49 @@ +scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) + +if utmp_backend != 'none' + utmp_add_args = '@0@ $WAYLAND_DISPLAY'.format(utmp_add) + utmp_del_args = (utmp_del_have_argument + ? '@0@ $WAYLAND_DISPLAY'.format(utmp_del) + : '@0@'.format(utmp_del)) + utmp_path = utmp_default_helper_path +else + utmp_add_args = '<no utmp support in foot>' + utmp_del_args = '<no utmp support in foot>' + utmp_path = 'none' +endif + + +conf_data = configuration_data( + { + 'default_terminfo': get_option('default-terminfo'), + 'utmp_backend': utmp_backend, + 'utmp_add_args': utmp_add_args, + 'utmp_del_args': utmp_del_args, + 'utmp_helper_path': utmp_path, + } +) + +foreach man_src : [{'name': 'foot', 'section' : 1}, + {'name': 'foot.ini', 'section': 5}, + {'name': 'footclient', 'section': 1}, + {'name': 'foot-ctlseqs', 'section': 7}] + name = man_src['name'] + section = man_src['section'] + out = '@0@.@1@'.format(name, section) + + preprocessed = configure_file( + input: '@0@.@1@.scd'.format(name, section), + output: '@0@.preprocessed'.format(out), + configuration: conf_data, + ) + + custom_target( + out, + output: out, + input: preprocessed, + command: scdoc_prog.full_path(), + capture: true, + feed: true, + install: true, + install_dir: join_paths(get_option('mandir'), 'man@0@'.format(section))) +endforeach diff --git a/doc/sixel-tux-foot.png b/doc/sixel-tux-foot.png new file mode 100644 index 0000000..ce30fe8 Binary files /dev/null and b/doc/sixel-tux-foot.png differ diff --git a/doc/tux-foot-ok.png b/doc/tux-foot-ok.png new file mode 100644 index 0000000..5d81462 Binary files /dev/null and b/doc/tux-foot-ok.png differ diff --git a/extract.c b/extract.c new file mode 100644 index 0000000..cd9a0c9 --- /dev/null +++ b/extract.c @@ -0,0 +1,271 @@ +#include "extract.h" +#include <string.h> + +#define LOG_MODULE "extract" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" + +struct extraction_context { + char32_t *buf; + size_t size; + size_t idx; + size_t tab_spaces_left; + size_t empty_count; + size_t newline_count; + bool strip_trailing_empty; + bool failed; + const struct row *last_row; + const struct cell *last_cell; + enum selection_kind selection_kind; +}; + +struct extraction_context * +extract_begin(enum selection_kind kind, bool strip_trailing_empty) +{ + struct extraction_context *ctx = malloc(sizeof(*ctx)); + if (unlikely(ctx == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } + + *ctx = (struct extraction_context){ + .selection_kind = kind, + .strip_trailing_empty = strip_trailing_empty, + }; + return ctx; +} + +static bool +ensure_size(struct extraction_context *ctx, size_t additional_chars) +{ + while (ctx->size < ctx->idx + additional_chars) { + size_t new_size = ctx->size == 0 ? 512 : ctx->size * 2; + char32_t *new_buf = realloc(ctx->buf, new_size * sizeof(new_buf[0])); + + if (new_buf == NULL) + return false; + + ctx->buf = new_buf; + ctx->size = new_size; + } + + xassert(ctx->size >= ctx->idx + additional_chars); + return true; +} + +bool +extract_finish_wide(struct extraction_context *ctx, char32_t **text, size_t *len) +{ + if (text == NULL) + return false; + + *text = NULL; + if (len != NULL) + *len = 0; + + if (ctx->failed) + goto err; + + if (!ctx->strip_trailing_empty) { + /* Insert pending newlines, and replace empty cells with spaces */ + if (!ensure_size(ctx, ctx->newline_count + ctx->empty_count)) + goto err; + + for (size_t i = 0; i < ctx->newline_count; i++) + ctx->buf[ctx->idx++] = U'\n'; + + for (size_t i = 0; i < ctx->empty_count; i++) + ctx->buf[ctx->idx++] = U' '; + } + + if (ctx->idx == 0) { + /* Selection of empty cells only */ + if (!ensure_size(ctx, 1)) + goto err; + ctx->buf[ctx->idx++] = U'\0'; + } else { + xassert(ctx->idx > 0); + xassert(ctx->idx <= ctx->size); + + switch (ctx->selection_kind) { + default: + if (ctx->buf[ctx->idx - 1] == U'\n') + ctx->buf[ctx->idx - 1] = U'\0'; + break; + + case SELECTION_LINE_WISE: + if (ctx->buf[ctx->idx - 1] != U'\n') { + if (!ensure_size(ctx, 1)) + goto err; + ctx->buf[ctx->idx++] = U'\n'; + } + break; + + } + + if (ctx->buf[ctx->idx - 1] != U'\0') { + if (!ensure_size(ctx, 1)) + goto err; + ctx->buf[ctx->idx++] = U'\0'; + } + } + + *text = ctx->buf; + if (len != NULL) + *len = ctx->idx - 1; + free(ctx); + return true; + +err: + free(ctx->buf); + free(ctx); + return false; +} + +bool +extract_finish(struct extraction_context *ctx, char **text, size_t *len) +{ + if (text == NULL) + return false; + if (len != NULL) + *len = 0; + + char32_t *wtext; + if (!extract_finish_wide(ctx, &wtext, NULL)) + return false; + + bool ret = false; + + *text = ac32tombs(wtext); + if (*text == NULL) { + LOG_ERR("failed to convert selection to UTF-8"); + goto out; + } + + if (len != NULL) + *len = strlen(*text); + ret = true; + +out: + free(wtext); + return ret; +} + +bool +extract_one(const struct terminal *term, const struct row *row, + const struct cell *cell, int col, void *context) +{ + struct extraction_context *ctx = context; + + if (cell->wc >= CELL_SPACER) + return true; + + if (ctx->last_row != NULL && row != ctx->last_row) { + /* New row - determine if we should insert a newline or not */ + + if (ctx->selection_kind != SELECTION_BLOCK) { + if (ctx->last_row->linebreak || + ctx->empty_count > 0 || + cell->wc == 0) + { + /* Row has a hard linebreak, or either last cell or + * current cell is empty */ + + /* Don't emit newline just yet - only if there are + * non-empty cells following it */ + ctx->newline_count++; + + if (!ctx->strip_trailing_empty) { + if (!ensure_size(ctx, ctx->empty_count)) + goto err; + for (size_t i = 0; i < ctx->empty_count; i++) + ctx->buf[ctx->idx++] = U' '; + } + ctx->empty_count = 0; + } + } else { + /* Always insert a linebreak */ + if (!ensure_size(ctx, 1)) + goto err; + + ctx->buf[ctx->idx++] = U'\n'; + + if (!ctx->strip_trailing_empty) { + if (!ensure_size(ctx, ctx->empty_count)) + goto err; + for (size_t i = 0; i < ctx->empty_count; i++) + ctx->buf[ctx->idx++] = U' '; + } + ctx->empty_count = 0; + } + + ctx->tab_spaces_left = 0; + } + + if (cell->wc == U' ' && ctx->tab_spaces_left > 0) { + ctx->tab_spaces_left--; + return true; + } + + ctx->tab_spaces_left = 0; + + if (cell->wc == 0) { + ctx->empty_count++; + ctx->last_row = row; + ctx->last_cell = cell; + return true; + } + + /* Insert pending newlines, and replace empty cells with spaces */ + if (!ensure_size(ctx, ctx->newline_count + ctx->empty_count)) + goto err; + + for (size_t i = 0; i < ctx->newline_count; i++) + ctx->buf[ctx->idx++] = U'\n'; + + for (size_t i = 0; i < ctx->empty_count; i++) + ctx->buf[ctx->idx++] = U' '; + + ctx->newline_count = 0; + ctx->empty_count = 0; + + if (cell->wc >= CELL_COMB_CHARS_LO && cell->wc <= CELL_COMB_CHARS_HI) + { + const struct composed *composed = composed_lookup( + term->composed, cell->wc - CELL_COMB_CHARS_LO); + + if (!ensure_size(ctx, composed->count)) + goto err; + + for (size_t i = 0; i < composed->count; i++) + ctx->buf[ctx->idx++] = composed->chars[i]; + } + + else { + if (!ensure_size(ctx, 1)) + goto err; + ctx->buf[ctx->idx++] = cell->wc; + + if (cell->wc == U'\t') { + int next_tab_stop = term->cols - 1; + tll_foreach(term->tab_stops, it) { + if (it->item > col) { + next_tab_stop = it->item; + break; + } + } + + if (next_tab_stop > col) + ctx->tab_spaces_left = next_tab_stop - col - 1; + } + } + + ctx->last_row = row; + ctx->last_cell = cell; + return true; + +err: + ctx->failed = true; + return false; +} diff --git a/extract.h b/extract.h new file mode 100644 index 0000000..30bec49 --- /dev/null +++ b/extract.h @@ -0,0 +1,21 @@ +#pragma once + +#include <stddef.h> +#include <stdbool.h> +#include <uchar.h> + +#include "terminal.h" + +struct extraction_context; + +struct extraction_context *extract_begin( + enum selection_kind kind, bool strip_trailing_empty); + +bool extract_one( + const struct terminal *term, const struct row *row, const struct cell *cell, + int col, void *context); + +bool extract_finish( + struct extraction_context *context, char **text, size_t *len); +bool extract_finish_wide( + struct extraction_context *context, char32_t **text, size_t *len); diff --git a/fdm.c b/fdm.c new file mode 100644 index 0000000..4822cd9 --- /dev/null +++ b/fdm.c @@ -0,0 +1,496 @@ +#include "fdm.h" + +#include <stdlib.h> +#include <stdbool.h> +#include <inttypes.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <signal.h> + +#include <sys/epoll.h> + +#include <tllist.h> + +#define LOG_MODULE "fdm" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "xmalloc.h" + +#if !defined(SIGABBREV_NP) +#include <stdio.h> + +static const char * +sigabbrev_np(int sig) +{ + static char buf[16]; + snprintf(buf, sizeof(buf), "<%d>", sig); + return buf; +} +#endif + +struct fd_handler { + int fd; + int events; + fdm_fd_handler_t callback; + void *callback_data; + bool deleted; +}; + +struct sig_handler { + fdm_signal_handler_t callback; + void *callback_data; +}; + +struct hook { + fdm_hook_t callback; + void *callback_data; +}; + +typedef tll(struct hook) hooks_t; + +struct fdm { + int epoll_fd; + bool is_polling; + tll(struct fd_handler *) fds; + tll(struct fd_handler *) deferred_delete; + + sigset_t sigmask; + struct sig_handler *signal_handlers; + + hooks_t hooks_low; + hooks_t hooks_normal; + hooks_t hooks_high; +}; + +static volatile sig_atomic_t got_signal = false; +static volatile sig_atomic_t *received_signals = NULL; + +struct fdm * +fdm_init(void) +{ + sigset_t sigmask; + if (sigprocmask(0, NULL, &sigmask) < 0) { + LOG_ERRNO("failed to get process signal mask"); + return NULL; + } + + int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + if (epoll_fd == -1) { + LOG_ERRNO("failed to create epoll FD"); + return NULL; + } + + xassert(received_signals == NULL); /* Only one FDM instance supported */ + received_signals = xcalloc(SIGRTMAX, sizeof(received_signals[0])); + got_signal = false; + + struct fdm *fdm = malloc(sizeof(*fdm)); + if (unlikely(fdm == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } + + struct sig_handler *sig_handlers = calloc(SIGRTMAX, sizeof(sig_handlers[0])); + + if (sig_handlers == NULL) { + LOG_ERRNO("failed to allocate signal handler array"); + free(fdm); + return NULL; + } + + *fdm = (struct fdm){ + .epoll_fd = epoll_fd, + .is_polling = false, + .fds = tll_init(), + .deferred_delete = tll_init(), + .sigmask = sigmask, + .signal_handlers = sig_handlers, + .hooks_low = tll_init(), + .hooks_normal = tll_init(), + .hooks_high = tll_init(), + }; + return fdm; +} + +void +fdm_destroy(struct fdm *fdm) +{ + if (fdm == NULL) + return; + + if (tll_length(fdm->fds) > 0) + LOG_WARN("FD list not empty"); + + for (int i = 0; i < SIGRTMAX; i++) { + if (fdm->signal_handlers[i].callback != NULL) + LOG_WARN("handler for signal %d (SIG%s) not removed", + i, sigabbrev_np(i)); + } + + if (tll_length(fdm->hooks_low) > 0 || + tll_length(fdm->hooks_normal) > 0 || + tll_length(fdm->hooks_high) > 0) + { + LOG_WARN("hook list not empty"); + } + + xassert(tll_length(fdm->fds) == 0); + xassert(tll_length(fdm->deferred_delete) == 0); + xassert(tll_length(fdm->hooks_low) == 0); + xassert(tll_length(fdm->hooks_normal) == 0); + xassert(tll_length(fdm->hooks_high) == 0); + + sigprocmask(SIG_SETMASK, &fdm->sigmask, NULL); + free(fdm->signal_handlers); + + tll_free(fdm->fds); + tll_free(fdm->deferred_delete); + tll_free(fdm->hooks_low); + tll_free(fdm->hooks_normal); + tll_free(fdm->hooks_high); + close(fdm->epoll_fd); + free(fdm); + + free((void *)received_signals); + received_signals = NULL; +} + +bool +fdm_add(struct fdm *fdm, int fd, int events, fdm_fd_handler_t cb, void *data) +{ +#if defined(_DEBUG) + tll_foreach(fdm->fds, it) { + if (it->item->fd == fd) { + BUG("FD=%d already registered", fd); + } + } +#endif + + struct fd_handler *handler = malloc(sizeof(*handler)); + if (unlikely(handler == NULL)) { + LOG_ERRNO("malloc() failed"); + return false; + } + + *handler = (struct fd_handler) { + .fd = fd, + .events = events, + .callback = cb, + .callback_data = data, + .deleted = false, + }; + + tll_push_back(fdm->fds, handler); + + struct epoll_event ev = { + .events = events, + .data = {.ptr = handler}, + }; + + if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) { + LOG_ERRNO("failed to register FD=%d with epoll", fd); + free(handler); + tll_pop_back(fdm->fds); + return false; + } + + return true; +} + +static bool +fdm_del_internal(struct fdm *fdm, int fd, bool close_fd) +{ + if (fd == -1) + return true; + + tll_foreach(fdm->fds, it) { + if (it->item->fd != fd) + continue; + + if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_DEL, fd, NULL) < 0) + LOG_ERRNO("failed to unregister FD=%d from epoll", fd); + + if (close_fd) + close(it->item->fd); + + it->item->deleted = true; + if (fdm->is_polling) + tll_push_back(fdm->deferred_delete, it->item); + else + free(it->item); + + tll_remove(fdm->fds, it); + return true; + } + + LOG_ERR("no such FD: %d", fd); + close(fd); + return false; +} + +bool +fdm_del(struct fdm *fdm, int fd) +{ + return fdm_del_internal(fdm, fd, true); +} + +bool +fdm_del_no_close(struct fdm *fdm, int fd) +{ + return fdm_del_internal(fdm, fd, false); +} + +static bool +event_modify(struct fdm *fdm, struct fd_handler *fd, int new_events) +{ + if (new_events == fd->events) + return true; + + struct epoll_event ev = { + .events = new_events, + .data = {.ptr = fd}, + }; + + if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_MOD, fd->fd, &ev) < 0) { + LOG_ERRNO("failed to modify FD=%d with epoll (events 0x%08x -> 0x%08x)", + fd->fd, fd->events, new_events); + return false; + } + + fd->events = new_events; + return true; +} + +bool +fdm_event_add(struct fdm *fdm, int fd, int events) +{ + tll_foreach(fdm->fds, it) { + if (it->item->fd != fd) + continue; + + return event_modify(fdm, it->item, it->item->events | events); + } + + LOG_ERR("FD=%d not registered with the FDM", fd); + return false; +} + +bool +fdm_event_del(struct fdm *fdm, int fd, int events) +{ + tll_foreach(fdm->fds, it) { + if (it->item->fd != fd) + continue; + + return event_modify(fdm, it->item, it->item->events & ~events); + } + + LOG_ERR("FD=%d not registered with the FDM", fd); + return false; +} + +static hooks_t * +hook_priority_to_list(struct fdm *fdm, enum fdm_hook_priority priority) +{ + switch (priority) { + case FDM_HOOK_PRIORITY_LOW: return &fdm->hooks_low; + case FDM_HOOK_PRIORITY_NORMAL: return &fdm->hooks_normal; + case FDM_HOOK_PRIORITY_HIGH: return &fdm->hooks_high; + } + + BUG("unhandled priority type"); + return NULL; +} + +bool +fdm_hook_add(struct fdm *fdm, fdm_hook_t hook, void *data, + enum fdm_hook_priority priority) +{ + hooks_t *hooks = hook_priority_to_list(fdm, priority); + +#if defined(_DEBUG) + tll_foreach(*hooks, it) { + if (it->item.callback == hook) { + LOG_ERR("hook=0x%" PRIxPTR " already registered", (uintptr_t)hook); + return false; + } + } +#endif + + tll_push_back(*hooks, ((struct hook){hook, data})); + return true; +} + +bool +fdm_hook_del(struct fdm *fdm, fdm_hook_t hook, enum fdm_hook_priority priority) +{ + hooks_t *hooks = hook_priority_to_list(fdm, priority); + + tll_foreach(*hooks, it) { + if (it->item.callback != hook) + continue; + + tll_remove(*hooks, it); + return true; + } + + LOG_WARN("hook=0x%" PRIxPTR " not registered", (uintptr_t)hook); + return false; +} + +static void +signal_handler(int signo) +{ + got_signal = true; + received_signals[signo] = true; +} + +bool +fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *data) +{ + if (fdm->signal_handlers[signo].callback != NULL) { + LOG_ERR("signal %d (SIG%s) already has a handler", + signo, sigabbrev_np(signo)); + return false; + } + + sigset_t mask, original; + sigemptyset(&mask); + sigaddset(&mask, signo); + + if (sigprocmask(SIG_BLOCK, &mask, &original) < 0) { + LOG_ERRNO("failed to block signal %d (SIG%s)", + signo, sigabbrev_np(signo)); + return false; + } + + struct sigaction action = {.sa_handler = &signal_handler}; + sigemptyset(&action.sa_mask); + if (sigaction(signo, &action, NULL) < 0) { + LOG_ERRNO("failed to set signal handler for signal %d (SIG%s)", + signo, sigabbrev_np(signo)); + sigprocmask(SIG_SETMASK, &original, NULL); + return false; + } + + received_signals[signo] = false; + fdm->signal_handlers[signo].callback = handler; + fdm->signal_handlers[signo].callback_data = data; + return true; +} + +bool +fdm_signal_del(struct fdm *fdm, int signo) +{ + if (fdm->signal_handlers[signo].callback == NULL) + return false; + + struct sigaction action = {.sa_handler = SIG_DFL}; + sigemptyset(&action.sa_mask); + if (sigaction(signo, &action, NULL) < 0) { + LOG_ERRNO("failed to restore signal handler for signal %d (SIG%s)", + signo, sigabbrev_np(signo)); + return false; + } + + received_signals[signo] = false; + fdm->signal_handlers[signo].callback = NULL; + fdm->signal_handlers[signo].callback_data = NULL; + + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, signo); + if (sigprocmask(SIG_UNBLOCK, &mask, NULL) < 0) { + LOG_ERRNO("failed to unblock signal %d (SIG%s)", + signo, sigabbrev_np(signo)); + return false; + } + + return true; +} + +bool +fdm_poll(struct fdm *fdm) +{ + xassert(!fdm->is_polling && "nested calls to fdm_poll() not allowed"); + if (fdm->is_polling) { + LOG_ERR("nested calls to fdm_poll() not allowed"); + return false; + } + + tll_foreach(fdm->hooks_high, it) { + LOG_DBG( + "executing high priority hook 0x%" PRIxPTR" (fdm=%p, data=%p)", + (uintptr_t)it->item.callback, (void *)fdm, + (void *)it->item.callback_data); + it->item.callback(fdm, it->item.callback_data); + } + tll_foreach(fdm->hooks_normal, it) { + LOG_DBG( + "executing normal priority hook 0x%" PRIxPTR " (fdm=%p, data=%p)", + (uintptr_t)it->item.callback, (void *)fdm, + (void *)it->item.callback_data); + it->item.callback(fdm, it->item.callback_data); + } + tll_foreach(fdm->hooks_low, it) { + LOG_DBG( + "executing low priority hook 0x%" PRIxPTR " (fdm=%p, data=%p)", + (uintptr_t)it->item.callback, (void *)fdm, + (void *)it->item.callback_data); + it->item.callback(fdm, it->item.callback_data); + } + + struct epoll_event events[tll_length(fdm->fds)]; + + int r = epoll_pwait( + fdm->epoll_fd, events, tll_length(fdm->fds), -1, &fdm->sigmask); + + int errno_copy = errno; + + if (unlikely(got_signal)) { + got_signal = false; + + for (int i = 0; i < SIGRTMAX; i++) { + if (received_signals[i]) { + received_signals[i] = false; + struct sig_handler *handler = &fdm->signal_handlers[i]; + + xassert(handler->callback != NULL); + if (!handler->callback(fdm, i, handler->callback_data)) + return false; + } + } + } + + if (unlikely(r < 0)) { + if (errno_copy == EINTR) + return true; + + LOG_ERRNO_P(errno_copy, "failed to epoll"); + return false; + } + + bool ret = true; + + fdm->is_polling = true; + for (int i = 0; i < r; i++) { + struct fd_handler *fd = events[i].data.ptr; + if (fd->deleted) + continue; + + if (!fd->callback(fdm, fd->fd, events[i].events, fd->callback_data)) { + ret = false; + break; + } + } + fdm->is_polling = false; + + tll_foreach(fdm->deferred_delete, it) { + free(it->item); + tll_remove(fdm->deferred_delete, it); + } + + return ret; +} diff --git a/fdm.h b/fdm.h new file mode 100644 index 0000000..2ccff95 --- /dev/null +++ b/fdm.h @@ -0,0 +1,34 @@ +#pragma once + +#include <stdbool.h> + +struct fdm; + +typedef bool (*fdm_fd_handler_t)(struct fdm *fdm, int fd, int events, void *data); +typedef bool (*fdm_signal_handler_t)(struct fdm *fdm, int signo, void *data); +typedef void (*fdm_hook_t)(struct fdm *fdm, void *data); + +enum fdm_hook_priority { + FDM_HOOK_PRIORITY_LOW, + FDM_HOOK_PRIORITY_NORMAL, + FDM_HOOK_PRIORITY_HIGH +}; + +struct fdm *fdm_init(void); +void fdm_destroy(struct fdm *fdm); + +bool fdm_add(struct fdm *fdm, int fd, int events, fdm_fd_handler_t handler, void *data); +bool fdm_del(struct fdm *fdm, int fd); +bool fdm_del_no_close(struct fdm *fdm, int fd); + +bool fdm_event_add(struct fdm *fdm, int fd, int events); +bool fdm_event_del(struct fdm *fdm, int fd, int events); + +bool fdm_hook_add(struct fdm *fdm, fdm_hook_t hook, void *data, + enum fdm_hook_priority priority); +bool fdm_hook_del(struct fdm *fdm, fdm_hook_t hook, enum fdm_hook_priority priority); + +bool fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *data); +bool fdm_signal_del(struct fdm *fdm, int signo); + +bool fdm_poll(struct fdm *fdm); diff --git a/foot-features.c b/foot-features.c new file mode 100644 index 0000000..8e33251 --- /dev/null +++ b/foot-features.c @@ -0,0 +1,42 @@ +#include "foot-features.h" +#include "version.h" + +const char version_and_features[] = + "version: " FOOT_VERSION + +#if defined(FOOT_PGO_ENABLED) && FOOT_PGO_ENABLED + " +pgo" +#else + " -pgo" +#endif + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + " +ime" +#else + " -ime" +#endif + +#if defined(FOOT_GRAPHEME_CLUSTERING) && FOOT_GRAPHEME_CLUSTERING + " +graphemes" +#else + " -graphemes" +#endif + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + " +toplevel-tag" +#else + " -toplevel-tag" +#endif + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + " +blur" +#else + " -blur" +#endif + +#if !defined(NDEBUG) + " +assertions" +#else + " -assertions" +#endif +; diff --git a/foot-features.h b/foot-features.h new file mode 100644 index 0000000..49cc56e --- /dev/null +++ b/foot-features.h @@ -0,0 +1,13 @@ +#pragma once + +#include <stdio.h> + +extern const char version_and_features[]; + +static inline void +print_version_and_features(const char *prefix) +{ + fputs(prefix, stdout); + fputs(version_and_features, stdout); + fputc('\n', stdout); +} diff --git a/foot-server.desktop b/foot-server.desktop new file mode 100644 index 0000000..6e8891c --- /dev/null +++ b/foot-server.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Exec=foot --server +Icon=foot +Terminal=false +Categories=System;TerminalEmulator; +Keywords=shell;prompt;command;commandline; + +Name=Foot Server +GenericName=Terminal +Comment=A wayland native terminal emulator (server) diff --git a/foot-server.service.in b/foot-server.service.in new file mode 100644 index 0000000..118b19a --- /dev/null +++ b/foot-server.service.in @@ -0,0 +1,15 @@ +[Service] +ExecStart=@bindir@/foot --server=3 +UnsetEnvironment=LISTEN_PID LISTEN_FDS LISTEN_FDNAMES +NonBlocking=true + +[Unit] +Requires=%N.socket +Description=Foot terminal server mode +Documentation=man:foot(1) +PartOf=graphical-session.target +After=graphical-session.target +ConditionEnvironment=WAYLAND_DISPLAY + +[Install] +WantedBy=graphical-session.target diff --git a/foot-server.socket b/foot-server.socket new file mode 100644 index 0000000..0c7c1b8 --- /dev/null +++ b/foot-server.socket @@ -0,0 +1,10 @@ +[Socket] +ListenStream=%t/foot.sock + +[Unit] +PartOf=graphical-session.target +After=graphical-session.target +ConditionEnvironment=WAYLAND_DISPLAY + +[Install] +WantedBy=graphical-session.target diff --git a/foot.desktop b/foot.desktop new file mode 100644 index 0000000..f072568 --- /dev/null +++ b/foot.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Exec=foot +Icon=foot +Terminal=false +Categories=System;TerminalEmulator; +Keywords=shell;prompt;command;commandline; + +Name=Foot +GenericName=Terminal +Comment=A wayland native terminal emulator diff --git a/foot.info b/foot.info new file mode 100644 index 0000000..13f4403 --- /dev/null +++ b/foot.info @@ -0,0 +1,285 @@ +@default_terminfo@|foot terminal emulator, + use=@default_terminfo@+base, + colors#256, + setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48\:5\:%p1%d%;m, + setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38\:5\:%p1%d%;m, + +@default_terminfo@-direct|foot with direct color indexing, + use=@default_terminfo@+base, + colors#16777216, + RGB, + setab=\E[%?%p1%{8}%<%t4%p1%d%e48\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, + setaf=\E[%?%p1%{8}%<%t3%p1%d%e38\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, + +@default_terminfo@+base|foot base fragment, + AX, + Su, + Tc, + XF, + XT, + am, + bce, + bw, + ccc, + hs, + mir, + msgr, + npc, + xenl, + cols#80, + it#8, + lines#24, + pairs#0x10000, + BD=\E[?2004l, + BE=\E[?2004h, + Cr=\E]112\E\\, + Cs=\E]12;%p1%s\E\\, + E3=\E[3J, + Ms=\E]52;%p1%s;%p2%s\E\\, + PE=\E[201~, + PS=\E[200~, + RV=\E[>c, + Rect=\E[%p1%d;%p2%d;%p3%d;%p4%d;%p5%d$x, + Se=\E[ q, + Setulc=\E[58\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, + Smulx=\E[4:%p1%dm, + Ss=\E[%p1%d q, + Sync=\E[?2026%?%p1%{1}%-%tl%eh%;, + TS=\E]2;, + XM=\E[?1006;1000%?%p1%{1}%=%th%el%;, + XR=\E[>0q, + acsc=``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, + bel=^G, + blink=\E[5m, + bold=\E[1m, + cbt=\E[Z, + civis=\E[?25l, + clear=\E[H\E[2J, + cnorm=\E[?12l\E[?25h, + cr=\r, + csr=\E[%i%p1%d;%p2%dr, + cub1=^H, + cub=\E[%p1%dD, + cud1=\n, + cud=\E[%p1%dB, + cuf1=\E[C, + cuf=\E[%p1%dC, + cup=\E[%i%p1%d;%p2%dH, + cuu1=\E[A, + cuu=\E[%p1%dA, + cvvis=\E[?12;25h, + dch1=\E[P, + dch=\E[%p1%dP, + dim=\E[2m, + dl1=\E[M, + dl=\E[%p1%dM, + dsl=\E]2;\E\\, + ech=\E[%p1%dX, + ed=\E[J, + el1=\E[1K, + el=\E[K, + fd=\E[?1004l, + fe=\E[?1004h, + flash=\E]555\E\\, + fsl=\E\\, + home=\E[H, + hpa=\E[%i%p1%dG, + ht=^I, + hts=\EH, + ich=\E[%p1%d@, + il1=\E[L, + il=\E[%p1%dL, + ind=\n, + indn=\E[%p1%dS, + initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\, + invis=\E[8m, + is2=\E[!p\E[4l\E>, + kDC3=\E[3;3~, + kDC4=\E[3;4~, + kDC5=\E[3;5~, + kDC6=\E[3;6~, + kDC7=\E[3;7~, + kDC=\E[3;2~, + kDN3=\E[1;3B, + kDN4=\E[1;4B, + kDN5=\E[1;5B, + kDN6=\E[1;6B, + kDN7=\E[1;7B, + kDN=\E[1;2B, + kEND3=\E[1;3F, + kEND4=\E[1;4F, + kEND5=\E[1;5F, + kEND6=\E[1;6F, + kEND7=\E[1;7F, + kEND=\E[1;2F, + kHOM3=\E[1;3H, + kHOM4=\E[1;4H, + kHOM5=\E[1;5H, + kHOM6=\E[1;6H, + kHOM7=\E[1;7H, + kHOM=\E[1;2H, + kIC3=\E[2;3~, + kIC4=\E[2;4~, + kIC5=\E[2;5~, + kIC6=\E[2;6~, + kIC7=\E[2;7~, + kIC=\E[2;2~, + kLFT3=\E[1;3D, + kLFT4=\E[1;4D, + kLFT5=\E[1;5D, + kLFT6=\E[1;6D, + kLFT7=\E[1;7D, + kLFT=\E[1;2D, + kNXT3=\E[6;3~, + kNXT4=\E[6;4~, + kNXT5=\E[6;5~, + kNXT6=\E[6;6~, + kNXT7=\E[6;7~, + kNXT=\E[6;2~, + kPRV3=\E[5;3~, + kPRV4=\E[5;4~, + kPRV5=\E[5;5~, + kPRV6=\E[5;6~, + kPRV7=\E[5;7~, + kPRV=\E[5;2~, + kRIT3=\E[1;3C, + kRIT4=\E[1;4C, + kRIT5=\E[1;5C, + kRIT6=\E[1;6C, + kRIT7=\E[1;7C, + kRIT=\E[1;2C, + kUP3=\E[1;3A, + kUP4=\E[1;4A, + kUP5=\E[1;5A, + kUP6=\E[1;6A, + kUP7=\E[1;7A, + kUP=\E[1;2A, + kbs=^?, + kcbt=\E[Z, + kcub1=\EOD, + kcud1=\EOB, + kcuf1=\EOC, + kcuu1=\EOA, + kdch1=\E[3~, + kend=\EOF, + kf10=\E[21~, + kf11=\E[23~, + kf12=\E[24~, + kf13=\E[1;2P, + kf14=\E[1;2Q, + kf15=\E[1;2R, + kf16=\E[1;2S, + kf17=\E[15;2~, + kf18=\E[17;2~, + kf19=\E[18;2~, + kf1=\EOP, + kf20=\E[19;2~, + kf21=\E[20;2~, + kf22=\E[21;2~, + kf23=\E[23;2~, + kf24=\E[24;2~, + kf25=\E[1;5P, + kf26=\E[1;5Q, + kf27=\E[1;5R, + kf28=\E[1;5S, + kf29=\E[15;5~, + kf2=\EOQ, + kf30=\E[17;5~, + kf31=\E[18;5~, + kf32=\E[19;5~, + kf33=\E[20;5~, + kf34=\E[21;5~, + kf35=\E[23;5~, + kf36=\E[24;5~, + kf37=\E[1;6P, + kf38=\E[1;6Q, + kf39=\E[1;6R, + kf3=\EOR, + kf40=\E[1;6S, + kf41=\E[15;6~, + kf42=\E[17;6~, + kf43=\E[18;6~, + kf44=\E[19;6~, + kf45=\E[20;6~, + kf46=\E[21;6~, + kf47=\E[23;6~, + kf48=\E[24;6~, + kf49=\E[1;3P, + kf4=\EOS, + kf50=\E[1;3Q, + kf51=\E[1;3R, + kf52=\E[1;3S, + kf53=\E[15;3~, + kf54=\E[17;3~, + kf55=\E[18;3~, + kf56=\E[19;3~, + kf57=\E[20;3~, + kf58=\E[21;3~, + kf59=\E[23;3~, + kf5=\E[15~, + kf60=\E[24;3~, + kf61=\E[1;4P, + kf62=\E[1;4Q, + kf63=\E[1;4R, + kf6=\E[17~, + kf7=\E[18~, + kf8=\E[19~, + kf9=\E[20~, + khome=\EOH, + kich1=\E[2~, + kind=\E[1;2B, + kmous=\E[<, + knp=\E[6~, + kpp=\E[5~, + kri=\E[1;2A, + kxIN=\E[I, + kxOUT=\E[O, + nel=\EE, + oc=\E]104\E\\, + op=\E[39;49m, + rc=\E8, + rep=%p1%c\E[%p2%{1}%-%db, + rev=\E[7m, + ri=\EM, + rin=\E[%p1%dT, + ritm=\E[23m, + rmacs=\E(B, + rmam=\E[?7l, + rmcup=\E[?1049l\E[23;0;0t, + rmir=\E[4l, + rmkx=\E[?1l\E>, + rmm=\E[?1036h\E[?1034l, + rmso=\E[27m, + rmul=\E[24m, + rmxx=\E[29m, + rs1=\Ec, + rs2=\E[!p\E[4l\E>, + rv=\E\\[>1;[0-9][0-9][0-9][0-9][0-9][0-9];0c, + sc=\E7, + setal=\E[58\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, + setrgbb=\E[48\:2\:\:%p1%d\:%p2%d\:%p3%dm, + setrgbf=\E[38\:2\:\:%p1%d\:%p2%d\:%p3%dm, + sgr0=\E(B\E[m, + sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p5%t;2%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m, + sitm=\E[3m, + smacs=\E(0, + smam=\E[?7h, + smcup=\E[?1049h\E[22;0;0t, + smir=\E[4h, + smkx=\E[?1h\E=, + smm=\E[?1036l\E[?1034h, + smso=\E[7m, + smul=\E[4m, + smxx=\E[9m, + tbc=\E[3g, + tsl=\E]2;, + u6=\E[%i%d;%dR, + u7=\E[6n, + u8=\E[?%[;0123456789]c, + u9=\E[c, + vpa=\E[%i%p1%dd, + xm=\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;, + xr=\EP>\\|foot\\([0-9]+\\.[0-9]+\\.[0-9]+(-[0-9]+-g[a-f[0-9]+)?\\)?\E\\\\, + +# XT, +# AX, diff --git a/foot.ini b/foot.ini new file mode 100644 index 0000000..1722de0 --- /dev/null +++ b/foot.ini @@ -0,0 +1,319 @@ +# -*- conf -*- + +# shell=$SHELL (if set, otherwise user's default shell from /etc/passwd) +# term=foot (or xterm-256color if built with -Dterminfo=disabled) +# login-shell=no + +# app-id=foot # globally set wayland app-id. Default values are "foot" and "footclient" for desktop and server mode +# title=foot +# locked-title=no + +# font=monospace:size=8 +# font-bold=<bold variant of regular font> +# font-italic=<italic variant of regular font> +# font-bold-italic=<bold+italic variant of regular font> +# font-size-adjustment=0.5 +# line-height=<font metrics> +# letter-spacing=0 +# horizontal-letter-offset=0 +# vertical-letter-offset=0 +# underline-offset=<font metrics> +# underline-thickness=<font underline thickness> +# strikeout-thickness=<font strikeout thickness> +# box-drawings-uses-font-glyphs=no +# dpi-aware=no +# gamma-correct-blending=no + +# initial-color-theme=dark +# initial-window-size-pixels=700x500 # Or, +# initial-window-size-chars=<COLSxROWS> +# initial-window-mode=windowed +# pad=0x0 center-when-maximized-and-fullscreen +# resize-by-cells=yes +# resize-keep-grid=yes +# resize-delay-ms=100 + +# bold-text-in-bright=no +# word-delimiters=,│`|:"'()[]{}<> +# selection-target=primary +# workers=<number of logical CPUs> +# utmp-helper=/usr/lib/utempter/utempter # When utmp backend is ‘libutempter’ (Linux) +# utmp-helper=/usr/libexec/ulog-helper # When utmp backend is ‘ulog’ (FreeBSD) + +# uppercase-regex-insert=yes + +[environment] +# name=value + +[security] +# osc52=enabled # disabled|copy-enabled|paste-enabled|enabled + +[bell] +# system=yes +# urgent=no +# notify=no +# visual=no +# command= +# command-focused=no + +[desktop-notifications] +# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --hint BOOLEAN:suppress-sound:${muted} --hint STRING:sound-name:${sound-name} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body} +# command-action-argument=--action ${action-name}=${action-label} +# close="" +# inhibit-when-focused=yes + + +[scrollback] +# lines=1000 +# multiplier=3.0 +# indicator-position=relative +# indicator-format="" + +[url] +# launch=xdg-open ${url} +# label-letters=sadfjklewcmpgh +# style=dotted (none|single|double|curly|dotted|dashed) +# osc8-underline=url-mode +# regex=(((https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)|www\.)([0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]+|\([]\["0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]*')+([0-9a-zA-Z/#@$&*+=~_%^\-]|\([]\["0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]*')) + +# You can define your own regex's, by adding a section called +# 'regex:<ID>' with a 'regex' and 'launch' key. These can then be tied +# to a key-binding. See foot.ini(5) for details + +# [regex:your-fancy-name] +# regex=<a POSIX-Extended Regular Expression> +# launch=<path to script or application> ${match} +# +# [key-bindings] +# regex-launch=[your-fancy-name] Control+Shift+q +# regex-copy=[your-fancy-name] Control+Alt+Shift+q + +[cursor] +# style=block +# blink=no +# blink-rate=500 +# beam-thickness=1.5 +# underline-thickness=<font underline thickness> + +[mouse] +# hide-when-typing=no +# alternate-scroll-mode=yes + +[touch] +# long-press-delay=400 + +[colors-dark] +# alpha=1.0 +# alpha-mode=default # Can be `default`, `matching` or `all` +# background=242424 +# foreground=ffffff +# flash=7f7f00 +# flash-alpha=0.5 + +# cursor=<inverse foreground/background> + +## Normal/regular colors (color palette 0-7) +# regular0=242424 # black +# regular1=f62b5a # red +# regular2=47b413 # green +# regular3=e3c401 # yellow +# regular4=24acd4 # blue +# regular5=f2affd # magenta +# regular6=13c299 # cyan +# regular7=e6e6e6 # white + +## Bright colors (color palette 8-15) +# bright0=616161 # bright black +# bright1=ff4d51 # bright red +# bright2=35d450 # bright green +# bright3=e9e836 # bright yellow +# bright4=5dc5f8 # bright blue +# bright5=feabf2 # bright magenta +# bright6=24dfc4 # bright cyan +# bright7=ffffff # bright white + +## dimmed colors (see foot.ini(5) man page) +# dim-blend-towards=black +# dim0=<not set> +# ... +# dim7=<not-set> + +## The remaining 256-color palette +# 16 = <256-color palette #16> +# ... +# 255 = <256-color palette #255> + +## Sixel colors +# sixel0 = 000000 +# sixel1 = 3333cc +# sixel2 = cc2121 +# sixel3 = 33cc33 +# sixel4 = cc33cc +# sixel5 = 33cccc +# sixel6 = cccc33 +# sixel7 = 878787 +# sixel8 = 424242 +# sixel9 = 545499 +# sixel10 = 994242 +# sixel11 = 549954 +# sixel12 = 995499 +# sixel13 = 549999 +# sixel14 = 999954 +# sixel15 = cccccc + +## Misc colors +# selection-foreground=<inverse foreground/background> +# selection-background=<inverse foreground/background> +# jump-labels=<regular0> <regular3> # black-on-yellow +# scrollback-indicator=<regular0> <bright4> # black-on-bright-blue +# search-box-no-match=<regular0> <regular1> # black-on-red +# search-box-match=<regular0> <regular3> # black-on-yellow +# urls=<regular3> + +[colors-light] +# Alternative color theme, see man page foot.ini(5) +# Same builtin defaults as [color], except for: +# dim-blend-towards=white + +[tabs] +enabled=yes +position=bottom +style=rounded +layout=floating +height=26 +# tab-width=200 (max width per tab in floating mode) +# tab-padding=8 (gap between tabs in floating mode) +# label-padding=8 (horizontal padding around the label inside each tab pill) +# margin=4 (edge gap; auto-added to bar height, does not squish pill) +# corner-radius=6 (corner rounding in pixels) +# background=1c1c1c +# foreground=b0b0b0 +# active-background=3a3a3a +# active-foreground=ffffff +# inherit-cwd=no (new tabs open in the active tab's cwd; requires OSC 7 shell support) +# unread-indicator=● (string drawn before label when tab has unseen output; empty disables) +# unread-color=fabd2f (color of the unread-indicator) + +[csd] +# preferred=server +# size=26 +# font=<primary font> +# color=<foreground color> +# hide-when-maximized=no +# double-click-to-maximize=yes +# border-width=0 +# border-color=<csd.color> +# button-width=26 +# button-color=<background color> +# button-minimize-color=<regular4> +# button-maximize-color=<regular2> +# button-close-color=<regular1> + +[key-bindings] +# scrollback-up-page=Shift+Page_Up Shift+KP_Page_Up +# scrollback-up-half-page=none +# scrollback-up-line=none +# scrollback-down-page=Shift+Page_Down Shift+KP_Page_Down +# scrollback-down-half-page=none +# scrollback-down-line=none +# scrollback-home=none +# scrollback-end=none +# clipboard-copy=Control+Shift+c XF86Copy +# clipboard-paste=Control+Shift+v XF86Paste +# primary-paste=Shift+Insert +# search-start=Control+Shift+r +# font-increase=Control+plus Control+equal Control+KP_Add +# font-decrease=Control+minus Control+KP_Subtract +# font-reset=Control+0 Control+KP_0 +# spawn-terminal=Control+Shift+n +# minimize=none +# maximize=none +# fullscreen=none +# pipe-visible=[sh -c "xurls | fuzzel | xargs -r firefox"] none +# pipe-scrollback=[sh -c "xurls | fuzzel | xargs -r firefox"] none +# pipe-selected=[xargs -r firefox] none +# pipe-command-output=[wl-copy] none # Copy last command's output to the clipboard +# show-urls-launch=Control+Shift+o +# show-urls-copy=none +# show-urls-persistent=none +# prompt-prev=Control+Shift+z +# prompt-next=Control+Shift+x +# unicode-input=Control+Shift+u +# color-theme-switch-1=none +# color-theme-switch-2=none +# color-theme-toggle=none +# noop=none +# quit=none +# tab-new=Control+Shift+t +# tab-close=Control+Shift+w +# tab-next=Control+Tab +# tab-prev=Control+Shift+Tab +# tab-overview=Control+Shift+space + +[search-bindings] +# cancel=Control+g Control+c Escape +# commit=Return KP_Enter +# commit-line=Control+Return +# find-prev=Control+r +# find-next=Control+s +# toggle-case=Mod1+c +# toggle-whole-word=Mod1+w +# toggle-regex=Mod1+r +# history-prev=Up +# history-next=Down +# cursor-left=Left Control+b +# cursor-left-word=Control+Left Mod1+b +# cursor-right=Right Control+f +# cursor-right-word=Control+Right Mod1+f +# cursor-home=Home Control+a +# cursor-end=End Control+e +# delete-prev=BackSpace +# delete-prev-word=Mod1+BackSpace Control+BackSpace +# delete-next=Delete +# delete-next-word=Mod1+d Control+Delete +# delete-to-start=Control+u +# delete-to-end=Control+k +# extend-char=Shift+Right +# extend-to-word-boundary=Control+w Control+Shift+Right +# extend-to-next-whitespace=Control+Shift+w +# extend-line-down=Shift+Down +# extend-backward-char=Shift+Left +# extend-backward-to-word-boundary=Control+Shift+Left +# extend-backward-to-next-whitespace=none +# extend-line-up=Shift+Up +# clipboard-paste=Control+v Control+Shift+v Control+y XF86Paste +# primary-paste=Shift+Insert +# unicode-input=none +# scrollback-up-page=Shift+Page_Up Shift+KP_Page_Up +# scrollback-up-half-page=none +# scrollback-up-line=none +# scrollback-down-page=Shift+Page_Down Shift+KP_Page_Down +# scrollback-down-half-page=none +# scrollback-down-line=none +# scrollback-home=none +# scrollback-end=none + +[url-bindings] +# cancel=Control+g Control+c Control+d Escape +# toggle-url-visible=t + +[text-bindings] +# \x03=Mod4+c # Map Super+c -> Ctrl+c + +[mouse-bindings] +# scrollback-up-mouse=BTN_WHEEL_BACK +# scrollback-down-mouse=BTN_WHEEL_FORWARD +# font-increase=Control+BTN_WHEEL_BACK +# font-decrease=Control+BTN_WHEEL_FORWARD +# selection-override-modifiers=Shift +# primary-paste=BTN_MIDDLE +# select-begin=BTN_LEFT +# select-begin-block=Control+BTN_LEFT +# select-extend=BTN_RIGHT +# select-extend-character-wise=Control+BTN_RIGHT +# select-word=BTN_LEFT-2 +# select-word-whitespace=Control+BTN_LEFT-2 +# select-quote = BTN_LEFT-3 +# select-row=BTN_LEFT-4 + +# vim: ft=dosini diff --git a/footclient.desktop b/footclient.desktop new file mode 100644 index 0000000..f82f282 --- /dev/null +++ b/footclient.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Exec=footclient +Icon=foot +Terminal=false +Categories=System;TerminalEmulator; +Keywords=shell;prompt;command;commandline; + +Name=Foot Client +GenericName=Terminal +Comment=A wayland native terminal emulator (client) diff --git a/generate-version.sh b/generate-version.sh new file mode 100755 index 0000000..a030d51 --- /dev/null +++ b/generate-version.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +set -e + +if [ ${#} -ne 3 ]; then + echo "Usage: ${0} <default_version> <src_dir> <out_file>" + exit 1 +fi + +default_version=${1} +src_dir=${2} +out_file=${3} + +# echo "default version: ${default_version}" +# echo "source directory: ${src_dir}" +# echo "output file: ${out_file}" + +if [ -d "${src_dir}/.git" ] && command -v git > /dev/null; then + workdir=$(pwd) + cd "${src_dir}" + + if git describe --tags > /dev/null 2>&1; then + git_version=$(git describe --always --tags) + else + # No tags available, happens in e.g. CI builds + git_version="${default_version}" + fi + + git_branch=$(git rev-parse --abbrev-ref HEAD) + cd "${workdir}" + + new_version="${git_version} ($(date "+%b %d %Y"), branch '${git_branch}')" +else + new_version="${default_version}" + extra="" +fi + +major=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\1/') +minor=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\2/') +patch=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\3/') +extra=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9]+-g[a-z0-9]+) .*)?.*/\5/') + +new_version="#define FOOT_VERSION \"${new_version}\" +#define FOOT_MAJOR ${major} +#define FOOT_MINOR ${minor} +#define FOOT_PATCH ${patch} +#define FOOT_EXTRA \"${extra}\"" + +if [ -f "${out_file}" ]; then + old_version=$(cat "${out_file}") +else + old_version="" +fi + +# echo "old version: ${old_version}" +# echo "new version: ${new_version}" + +if [ "${old_version}" != "${new_version}" ]; then + echo "${new_version}" > "${out_file}" +fi diff --git a/grid.c b/grid.c new file mode 100644 index 0000000..df7ef61 --- /dev/null +++ b/grid.c @@ -0,0 +1,1676 @@ +#include "grid.h" + +#include <limits.h> +#include <stdlib.h> +#include <string.h> + +#define LOG_MODULE "grid" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "macros.h" +#include "sixel.h" +#include "stride.h" +#include "util.h" +#include "xmalloc.h" + +#define TIME_REFLOW 0 + +#if defined(TIME_REFLOW) +#include "misc.h" +#endif + +/* + * "sb" (scrollback relative) coordinates + * + * The scrollback relative row number 0 is the *first*, and *oldest* + * row in the scrollback history (and thus the *first* row to be + * scrolled out). Thus, a higher number means further *down* in the + * scrollback, with the *highest* number being at the bottom of the + * screen, where new input appears. + */ + +int +grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row) +{ + const int scrollback_start = grid->offset + screen_rows; + int rebased_row = abs_row - scrollback_start + grid->num_rows; + + rebased_row &= grid->num_rows - 1; + return rebased_row; +} + +int +grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row) +{ + const int scrollback_start = grid->offset + screen_rows; + int abs_row = sb_rel_row + scrollback_start; + + abs_row &= grid->num_rows - 1; + return abs_row; +} + +int +grid_sb_start_ignore_uninitialized(const struct grid *grid, int screen_rows) +{ + int scrollback_start = grid->offset + screen_rows; + scrollback_start &= grid->num_rows - 1; + + while (grid->rows[scrollback_start] == NULL) { + scrollback_start++; + scrollback_start &= grid->num_rows - 1; + } + + return scrollback_start; +} + +int +grid_row_abs_to_sb_precalc_sb_start(const struct grid *grid, int sb_start, + int abs_row) +{ + int rebased_row = abs_row - sb_start + grid->num_rows; + rebased_row &= grid->num_rows - 1; + return rebased_row; +} + +int +grid_row_sb_to_abs_precalc_sb_start(const struct grid *grid, int sb_start, + int sb_rel_row) +{ + int abs_row = sb_rel_row + sb_start; + abs_row &= grid->num_rows - 1; + return abs_row; +} + +static void +ensure_row_has_extra_data(struct row *row) +{ + if (row->extra == NULL) + row->extra = xcalloc(1, sizeof(*row->extra)); +} + +static void +verify_no_overlapping_ranges_of_type(const struct row_ranges *ranges, + enum row_range_type type) +{ +#if defined(_DEBUG) + for (size_t i = 0; i < ranges->count; i++) { + const struct row_range *r1 = &ranges->v[i]; + + for (size_t j = i + 1; j < ranges->count; j++) { + const struct row_range *r2 = &ranges->v[j]; + xassert(r1 != r2); + + if ((r1->start <= r2->start && r1->end >= r2->start) || + (r1->start <= r2->end && r1->end >= r2->end)) + { + switch (type) { + case ROW_RANGE_URI: + BUG("OSC-8 URI overlap: %s: %d-%d: %s: %d-%d", + r1->uri.uri, r1->start, r1->end, + r2->uri.uri, r2->start, r2->end); + break; + + case ROW_RANGE_UNDERLINE: + BUG("underline overlap: %d-%d, %d-%d", + r1->start, r1->end, r2->start, r2->end); + break; + } + } + } + } +#endif +} + +static void +verify_no_overlapping_ranges(const struct row_data *extra) +{ + verify_no_overlapping_ranges_of_type(&extra->uri_ranges, ROW_RANGE_URI); + verify_no_overlapping_ranges_of_type(&extra->underline_ranges, ROW_RANGE_UNDERLINE); +} + +static void +verify_ranges_of_type_are_sorted(const struct row_ranges *ranges, + enum row_range_type type) +{ +#if defined(_DEBUG) + const struct row_range *last = NULL; + + for (size_t i = 0; i < ranges->count; i++) { + const struct row_range *r = &ranges->v[i]; + + if (last != NULL) { + if (last->start >= r->start || last->end >= r->end) { + switch (type) { + case ROW_RANGE_URI: + BUG("OSC-8 URI not sorted correctly: " + "%s: %d-%d came before %s: %d-%d", + last->uri.uri, last->start, last->end, + r->uri.uri, r->start, r->end); + break; + + case ROW_RANGE_UNDERLINE: + BUG("underline ranges not sorted correctly: " + "%d-%d came before %d-%d", + last->start, last->end, r->start, r->end); + break; + } + } + } + + last = r; + } +#endif +} + +static void +verify_ranges_are_sorted(const struct row_data *extra) +{ + verify_ranges_of_type_are_sorted(&extra->uri_ranges, ROW_RANGE_URI); + verify_ranges_of_type_are_sorted(&extra->underline_ranges, ROW_RANGE_UNDERLINE); +} + +static void +range_ensure_size(struct row_ranges *ranges, int count_to_add) +{ + if (ranges->count + count_to_add > ranges->size) { + ranges->size = ranges->count + count_to_add; + ranges->v = xrealloc(ranges->v, ranges->size * sizeof(ranges->v[0])); + } + + xassert(ranges->count + count_to_add <= ranges->size); +} + +/* + * Be careful! This function may xrealloc() the URI range vector, thus + * invalidating pointers into it. + */ +static void +range_insert(struct row_ranges *ranges, size_t idx, int start, int end, + enum row_range_type type, const union row_range_data *data) +{ + range_ensure_size(ranges, 1); + + xassert(idx <= ranges->count); + + const size_t move_count = ranges->count - idx; + memmove(&ranges->v[idx + 1], + &ranges->v[idx], + move_count * sizeof(ranges->v[0])); + + ranges->count++; + + struct row_range *r = &ranges->v[idx]; + r->start = start; + r->end = end; + + switch (type) { + case ROW_RANGE_URI: + r->uri.id = data->uri.id; + r->uri.uri = xstrdup(data->uri.uri); + break; + + case ROW_RANGE_UNDERLINE: + r->underline = data->underline; + break; + } +} + +static void +range_append_by_ref(struct row_ranges *ranges, int start, int end, + enum row_range_type type, const union row_range_data *data) +{ + range_ensure_size(ranges, 1); + + struct row_range *r = &ranges->v[ranges->count++]; + + r->start = start; + r->end = end; + + switch (type) { + case ROW_RANGE_URI: + r->uri.id = data->uri.id;; + r->uri.uri = data->uri.uri; + break; + + case ROW_RANGE_UNDERLINE: + r->underline = data->underline; + break; + } +} + +static void +range_append(struct row_ranges *ranges, int start, int end, + enum row_range_type type, const union row_range_data *data) +{ + switch (type) { + case ROW_RANGE_URI: + range_append_by_ref( + ranges, start, end, type, + &(union row_range_data){.uri = {.id = data->uri.id, + .uri = xstrdup(data->uri.uri)}}); + break; + + case ROW_RANGE_UNDERLINE: + range_append_by_ref(ranges, start, end, type, data); + break; + } +} + +static void +range_delete(struct row_ranges *ranges, enum row_range_type type, size_t idx) +{ + xassert(idx < ranges->count); + grid_row_range_destroy(&ranges->v[idx], type); + + const size_t move_count = ranges->count - idx - 1; + memmove(&ranges->v[idx], + &ranges->v[idx + 1], + move_count * sizeof(ranges->v[0])); + ranges->count--; +} + +struct grid * +grid_snapshot(const struct grid *grid) +{ + struct grid *clone = xmalloc(sizeof(*clone)); + clone->num_rows = grid->num_rows; + clone->num_cols = grid->num_cols; + clone->offset = grid->offset; + clone->view = grid->view; + clone->cursor = grid->cursor; + clone->saved_cursor = grid->saved_cursor; + clone->kitty_kbd = grid->kitty_kbd; + clone->rows = xcalloc(grid->num_rows, sizeof(clone->rows[0])); + memset(&clone->scroll_damage, 0, sizeof(clone->scroll_damage)); + memset(&clone->sixel_images, 0, sizeof(clone->sixel_images)); + + tll_foreach(grid->scroll_damage, it) + tll_push_back(clone->scroll_damage, it->item); + + for (int r = 0; r < grid->num_rows; r++) { + const struct row *row = grid->rows[r]; + + if (row == NULL) + continue; + + struct row *clone_row = xmalloc(sizeof(*row)); + clone->rows[r] = clone_row; + + clone_row->cells = xmalloc(grid->num_cols * sizeof(clone_row->cells[0])); + clone_row->linebreak = row->linebreak; + clone_row->dirty = row->dirty; + clone_row->shell_integration = row->shell_integration; + + for (int c = 0; c < grid->num_cols; c++) + clone_row->cells[c] = row->cells[c]; + + const struct row_data *extra = row->extra; + + if (extra != NULL) { + struct row_data *clone_extra = xcalloc(1, sizeof(*clone_extra)); + clone_row->extra = clone_extra; + + range_ensure_size(&clone_extra->uri_ranges, extra->uri_ranges.count); + range_ensure_size(&clone_extra->underline_ranges, extra->underline_ranges.count); + + for (int i = 0; i < extra->uri_ranges.count; i++) { + const struct row_range *range = &extra->uri_ranges.v[i]; + range_append( + &clone_extra->uri_ranges, + range->start, range->end, ROW_RANGE_URI, &range->data); + } + + for (int i = 0; i < extra->underline_ranges.count; i++) { + const struct row_range *range = &extra->underline_ranges.v[i]; + range_append_by_ref( + &clone_extra->underline_ranges, range->start, range->end, + ROW_RANGE_UNDERLINE, &range->data); + } + } else + clone_row->extra = NULL; + } + + tll_foreach(grid->sixel_images, it) { + int original_width = it->item.original.width; + int original_height = it->item.original.height; + pixman_image_t *original_pix = it->item.original.pix; + pixman_format_code_t original_pix_fmt = pixman_image_get_format(original_pix); + int original_stride = stride_for_format_and_width(original_pix_fmt, original_width); + + size_t original_size = original_stride * original_height; + void *new_original_data = xmemdup(it->item.original.data, original_size); + + pixman_image_t *new_original_pix = pixman_image_create_bits_no_clear( + original_pix_fmt, original_width, original_height, + new_original_data, original_stride); + + void *new_scaled_data = NULL; + pixman_image_t *new_scaled_pix = NULL; + int scaled_width = -1; + int scaled_height = -1; + + if (it->item.scaled.data != NULL) { + scaled_width = it->item.scaled.width; + scaled_height = it->item.scaled.height; + + pixman_image_t *scaled_pix = it->item.scaled.pix; + pixman_format_code_t scaled_pix_fmt = pixman_image_get_format(scaled_pix); + int scaled_stride = stride_for_format_and_width(scaled_pix_fmt, scaled_width); + + size_t scaled_size = scaled_stride * scaled_height; + new_scaled_data = xmemdup(it->item.scaled.data, scaled_size); + + new_scaled_pix = pixman_image_create_bits_no_clear( + scaled_pix_fmt, scaled_width, scaled_height, new_scaled_data, + scaled_stride); + } + + struct sixel six = { + .pix = (it->item.pix == it->item.original.pix + ? new_original_pix + : (it->item.pix == it->item.scaled.pix + ? new_scaled_pix + : NULL)), + .width = it->item.width, + .height = it->item.height, + .rows = it->item.rows, + .cols = it->item.cols, + .pos = it->item.pos, + .opaque = it->item.opaque, + .cell_width = it->item.cell_width, + .cell_height = it->item.cell_height, + .original = { + .data = new_original_data, + .pix = new_original_pix, + .width = original_width, + .height = original_height, + }, + .scaled = { + .data = new_scaled_data, + .pix = new_scaled_pix, + .width = scaled_width, + .height = scaled_height, + }, + }; + + tll_push_back(clone->sixel_images, six); + } + + return clone; +} + +void +grid_free(struct grid *grid) +{ + if (grid == NULL) + return; + + for (int r = 0; r < grid->num_rows; r++) + grid_row_free(grid->rows[r]); + + tll_foreach(grid->sixel_images, it) { + sixel_destroy(&it->item); + tll_remove(grid->sixel_images, it); + } + + free(grid->rows); + tll_free(grid->scroll_damage); +} + +void +grid_swap_row(struct grid *grid, int row_a, int row_b) +{ + xassert(grid->offset >= 0); + xassert(row_a != row_b); + + int real_a = (grid->offset + row_a) & (grid->num_rows - 1); + int real_b = (grid->offset + row_b) & (grid->num_rows - 1); + + struct row *a = grid->rows[real_a]; + struct row *b = grid->rows[real_b]; + + grid->rows[real_a] = b; + grid->rows[real_b] = a; +} + +struct row * +grid_row_alloc(int cols, bool initialize) +{ + struct row *row = xmalloc(sizeof(*row)); + row->dirty = false; + row->linebreak = true; + row->extra = NULL; + row->shell_integration.prompt_marker = false; + row->shell_integration.cmd_start = -1; + row->shell_integration.cmd_end = -1; + + if (initialize) { + row->cells = xcalloc(cols, sizeof(row->cells[0])); + for (size_t c = 0; c < cols; c++) + row->cells[c].attrs.clean = 1; + } else + row->cells = xmalloc(cols * sizeof(row->cells[0])); + + return row; +} + +void +grid_row_free(struct row *row) +{ + if (row == NULL) + return; + + grid_row_reset_extra(row); + free(row->extra); + free(row->cells); + free(row); +} + +void +grid_resize_without_reflow( + struct grid *grid, int new_rows, int new_cols, + int old_screen_rows, int new_screen_rows) +{ + struct row *const *old_grid = grid->rows; + const int old_rows = grid->num_rows; + const int old_cols = grid->num_cols; + + struct row **new_grid = xcalloc(new_rows, sizeof(new_grid[0])); + + tll(struct sixel) untranslated_sixels = tll_init(); + tll_foreach(grid->sixel_images, it) + tll_push_back(untranslated_sixels, it->item); + tll_free(grid->sixel_images); + + int new_offset = 0; + + /* Copy old lines, truncating them if old rows were longer */ + for (int r = 0, n = min(old_screen_rows, new_screen_rows); r < n; r++) { + const int old_row_idx = (grid->offset + r) & (old_rows - 1); + const int new_row_idx = (new_offset + r) & (new_rows - 1); + + const struct row *old_row = old_grid[old_row_idx]; + xassert(old_row != NULL); + + struct row *new_row = grid_row_alloc(new_cols, false); + new_grid[new_row_idx] = new_row; + + memcpy(new_row->cells, + old_row->cells, + sizeof(struct cell) * min(old_cols, new_cols)); + + new_row->dirty = old_row->dirty; + new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; + new_row->shell_integration.cmd_start = min(old_row->shell_integration.cmd_start, new_cols - 1); + new_row->shell_integration.cmd_end = min(old_row->shell_integration.cmd_end, new_cols - 1); + + if (new_cols > old_cols) { + /* Clear "new" columns */ + memset(&new_row->cells[old_cols], 0, + sizeof(struct cell) * (new_cols - old_cols)); + new_row->dirty = true; + } else if (old_cols > new_cols) { + /* Make sure we don't cut a multi-column character in two */ + for (int i = new_cols; i > 0 && old_row->cells[i].wc > CELL_SPACER; i--) + new_row->cells[i - 1].wc = 0; + } + + /* Map sixels on current "old" row to current "new row" */ + tll_foreach(untranslated_sixels, it) { + if (it->item.pos.row != old_row_idx) + continue; + + struct sixel sixel = it->item; + sixel.pos.row = new_row_idx; + + if (sixel.pos.col < new_cols) + tll_push_back(grid->sixel_images, sixel); + else + sixel_destroy(&it->item); + tll_remove(untranslated_sixels, it); + } + + /* Copy URI ranges, truncating them if necessary */ + const struct row_data *old_extra = old_row->extra; + if (old_extra == NULL) + continue; + + ensure_row_has_extra_data(new_row); + struct row_data *new_extra = new_row->extra; + + range_ensure_size(&new_extra->uri_ranges, old_extra->uri_ranges.count); + range_ensure_size(&new_extra->underline_ranges, old_extra->underline_ranges.count); + + for (int i = 0; i < old_extra->uri_ranges.count; i++) { + const struct row_range *range = &old_extra->uri_ranges.v[i]; + + if (range->start >= new_cols) { + /* The whole range is truncated */ + continue; + } + + const int start = range->start; + const int end = min(range->end, new_cols - 1); + range_append(&new_extra->uri_ranges, start, end, ROW_RANGE_URI, &range->data); + } + + for (int i = 0; i < old_extra->underline_ranges.count; i++) { + const struct row_range *range = &old_extra->underline_ranges.v[i]; + + if (range->start >= new_cols) { + /* The whole range is truncated */ + continue; + } + + const int start = range->start; + const int end = min(range->end, new_cols - 1); + range_append_by_ref(&new_extra->underline_ranges, start, end, ROW_RANGE_UNDERLINE, &range->data); + } +} + + /* Clear "new" lines */ + for (int r = min(old_screen_rows, new_screen_rows); r < new_screen_rows; r++) { + struct row *new_row = grid_row_alloc(new_cols, false); + new_grid[(new_offset + r) & (new_rows - 1)] = new_row; + + memset(new_row->cells, 0, sizeof(struct cell) * new_cols); + new_row->dirty = true; + } + +#if defined(_DEBUG) + for (size_t r = 0; r < new_rows; r++) { + const struct row *row = new_grid[r]; + + if (row == NULL) + continue; + if (row->extra == NULL) + continue; + + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); + } +#endif + + /* Free old grid */ + for (int r = 0; r < grid->num_rows; r++) + grid_row_free(old_grid[r]); + free(grid->rows); + + grid->rows = new_grid; + grid->num_rows = new_rows; + grid->num_cols = new_cols; + + grid->view = grid->offset = new_offset; + + /* Keep cursor at current position, but clamp to new dimensions */ + struct coord cursor = grid->cursor.point; + if (cursor.row == old_screen_rows - 1) { + /* 'less' breaks if the cursor isn't at the bottom */ + cursor.row = new_screen_rows - 1; + } + cursor.row = min(cursor.row, new_screen_rows - 1); + cursor.col = min(cursor.col, new_cols - 1); + grid->cursor.point = cursor; + + struct coord saved_cursor = grid->saved_cursor.point; + if (saved_cursor.row == old_screen_rows - 1) + saved_cursor.row = new_screen_rows - 1; + saved_cursor.row = min(saved_cursor.row, new_screen_rows - 1); + saved_cursor.col = min(saved_cursor.col, new_cols - 1); + grid->saved_cursor.point = saved_cursor; + + grid->cur_row = new_grid[(grid->offset + cursor.row) & (new_rows - 1)]; + xassert(grid->cur_row != NULL); + + grid->cursor.lcf = false; + grid->saved_cursor.lcf = false; + + /* Free sixels we failed to "map" to the new grid */ + tll_foreach(untranslated_sixels, it) + sixel_destroy(&it->item); + tll_free(untranslated_sixels); + +#if defined(_DEBUG) + for (int r = 0; r < new_screen_rows; r++) + grid_row_in_view(grid, r); +#endif +} + +static void +reflow_range_start(struct row_range *range, enum row_range_type type, + struct row *new_row, int new_col_idx) +{ + ensure_row_has_extra_data(new_row); + + struct row_ranges *new_ranges = NULL; + switch (type) { + case ROW_RANGE_URI: new_ranges = &new_row->extra->uri_ranges; break; + case ROW_RANGE_UNDERLINE: new_ranges = &new_row->extra->underline_ranges; break; + } + + if (new_ranges == NULL) + BUG("unhandled range type"); + + range_append_by_ref(new_ranges, new_col_idx, -1, type, &range->data); + + switch (type) { + case ROW_RANGE_URI: range->uri.uri = NULL; break; /* Owned by new_ranges */ + case ROW_RANGE_UNDERLINE: break; + } +} + +static void +reflow_range_end(struct row_range *range, enum row_range_type type, + struct row *new_row, int new_col_idx) +{ + struct row_data *extra = new_row->extra; + struct row_ranges *ranges = NULL; + + switch (type) { + case ROW_RANGE_URI: ranges = &extra->uri_ranges; break; + case ROW_RANGE_UNDERLINE: ranges = &extra->underline_ranges; break; + } + + if (ranges == NULL) + BUG("unhandled range type"); + + xassert(ranges->count > 0); + + struct row_range *new_range = &ranges->v[ranges->count - 1]; + xassert(new_range->end < 0); + + switch (type) { + case ROW_RANGE_URI: + xassert(new_range->uri.id == range->uri.id); + break; + + case ROW_RANGE_UNDERLINE: + xassert(new_range->underline.style == range->underline.style); + xassert(new_range->underline.color_src == range->underline.color_src); + xassert(new_range->underline.color == range->underline.color); + break; + } + + new_range->end = new_col_idx; +} + +static struct row * +_line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, + int *row_idx, int *col_idx, int row_count, int col_count) +{ + *col_idx = 0; + *row_idx = (*row_idx + 1) & (row_count - 1); + + struct row *new_row = new_grid[*row_idx]; + + if (new_row == NULL) { + /* Scrollback not yet full, allocate a completely new row */ + new_row = grid_row_alloc(col_count, false); + new_grid[*row_idx] = new_row; + } else { + /* Scrollback is full, need to reuse a row */ + grid_row_reset_extra(new_row); + new_row->shell_integration.prompt_marker = false; + new_row->shell_integration.cmd_start = -1; + new_row->shell_integration.cmd_end = -1; + + tll_foreach(old_grid->sixel_images, it) { + if (it->item.pos.row == *row_idx) { + sixel_destroy(&it->item); + tll_remove(old_grid->sixel_images, it); + } + } + + /* + * TODO: detect if the reused row is covered by the + * selection. Of so, cancel the selection. The problem: we + * don't know if we've translated the selection coordinates + * yet. + */ + } + + struct row_data *extra = row->extra; + if (extra == NULL) + return new_row; + + /* + * URI ranges are per row. Thus, we need to 'close' the still-open + * ranges on the previous row, and re-open them on the + * next/current row. + */ + if (extra->uri_ranges.count > 0) { + struct row_range *range = + &extra->uri_ranges.v[extra->uri_ranges.count - 1]; + + if (range->end < 0) { + + /* Terminate URI range on the previous row */ + range->end = col_count - 1; + + /* Open a new range on the new/current row */ + ensure_row_has_extra_data(new_row); + range_append(&new_row->extra->uri_ranges, 0, -1, + ROW_RANGE_URI, &range->data); + } + } + + if (extra->underline_ranges.count > 0) { + struct row_range *range = + &extra->underline_ranges.v[extra->underline_ranges.count - 1]; + + if (range->end < 0) { + + /* Terminate URI range on the previous row */ + range->end = col_count - 1; + + /* Open a new range on the new/current row */ + ensure_row_has_extra_data(new_row); + range_append(&new_row->extra->underline_ranges, 0, -1, + ROW_RANGE_UNDERLINE, &range->data); + } + } + + return new_row; +} + +static struct { + int scrollback_start; + int rows; +} tp_cmp_ctx; + +static int +tp_cmp(const void *_a, const void *_b) +{ + const struct coord *a = *(const struct coord **)_a; + const struct coord *b = *(const struct coord **)_b; + + int scrollback_start = tp_cmp_ctx.scrollback_start; + int num_rows = tp_cmp_ctx.rows; + + int a_row = (a->row - scrollback_start + num_rows) & (num_rows - 1); + int b_row = (b->row - scrollback_start + num_rows) & (num_rows - 1); + + xassert(a_row >= 0); + xassert(a_row < num_rows || num_rows == 0); + xassert(b_row >= 0); + xassert(b_row < num_rows || num_rows == 0); + + if (a_row < b_row) + return -1; + if (a_row > b_row) + return 1; + + xassert(a_row == b_row); + + if (a->col < b->col) + return -1; + if (a->col > b->col) + return 1; + + xassert(a->col == b->col); + return 0; +} + +void +grid_resize_and_reflow( + struct grid *grid, const struct terminal *term, int new_rows, int new_cols, + int old_screen_rows, int new_screen_rows, + size_t tracking_points_count, + struct coord *const _tracking_points[static tracking_points_count]) +{ +#if defined(TIME_REFLOW) && TIME_REFLOW + struct timespec start; + clock_gettime(CLOCK_MONOTONIC, &start); +#endif + + struct row *const *old_grid = grid->rows; + const int old_rows = grid->num_rows; + const int old_cols = grid->num_cols; + + /* Is viewpoint tracking current grid offset? */ + const bool view_follows = grid->view == grid->offset; + + int new_col_idx = 0; + int new_row_idx = 0; + + struct row **new_grid = xcalloc(new_rows, sizeof(new_grid[0])); + struct row *new_row = new_grid[new_row_idx]; + + xassert(new_row == NULL); + new_row = grid_row_alloc(new_cols, false); + new_grid[new_row_idx] = new_row; + + /* Start at the beginning of the old grid's scrollback. That is, + * at the output that is *oldest* */ + int offset = grid->offset + old_screen_rows; + + tll(struct sixel) untranslated_sixels = tll_init(); + tll_foreach(grid->sixel_images, it) + tll_push_back(untranslated_sixels, it->item); + tll_free(grid->sixel_images); + + /* Turn cursor coordinates into grid absolute coordinates */ + struct coord cursor = grid->cursor.point; + cursor.row += grid->offset; + cursor.row &= old_rows - 1; + + struct coord saved_cursor = grid->saved_cursor.point; + saved_cursor.row += grid->offset; + saved_cursor.row &= old_rows - 1; + + size_t tp_count = + tracking_points_count + + 1 + /* cursor */ + 1 + /* saved cursor */ + !view_follows + /* viewport */ + 1; /* terminator */ + + struct coord *tracking_points[tp_count]; + memcpy(tracking_points, _tracking_points, tracking_points_count * sizeof(_tracking_points[0])); + tracking_points[tracking_points_count] = &cursor; + tracking_points[tracking_points_count + 1] = &saved_cursor; + + struct coord viewport = {0, grid->view}; + if (!view_follows) + tracking_points[tracking_points_count + 2] = &viewport; + + /* Not thread safe! */ + tp_cmp_ctx.scrollback_start = offset; + tp_cmp_ctx.rows = old_rows; + qsort( + tracking_points, tp_count - 1, sizeof(tracking_points[0]), &tp_cmp); + + /* NULL terminate */ + struct coord terminator = {-1, -1}; + tracking_points[tp_count - 1] = &terminator; + struct coord **next_tp = &tracking_points[0]; + + LOG_DBG("scrollback-start=%d", offset); + for (size_t i = 0; i < tp_count - 1; i++) { + LOG_DBG("TP #%zu: row=%d, col=%d", + i, tracking_points[i]->row, tracking_points[i]->col); + } + + int coalesced_linebreaks = 0; + + /* + * Walk the old grid + */ + for (int r = 0; r < old_rows; r++) { + + const size_t old_row_idx = (offset + r) & (old_rows - 1); + + /* Unallocated (empty) rows we can simply skip */ + const struct row *old_row = old_grid[old_row_idx]; + if (old_row == NULL) + continue; + + /* Map sixels on current "old" row to current "new row" */ + tll_foreach(untranslated_sixels, it) { + if (it->item.pos.row != old_row_idx) + continue; + + struct sixel sixel = it->item; + sixel.pos.row = new_row_idx; + + tll_push_back(grid->sixel_images, sixel); + tll_remove(untranslated_sixels, it); + } + +#define line_wrap() \ + new_row = _line_wrap( \ + grid, new_grid, new_row, &new_row_idx, &new_col_idx, \ + new_rows, new_cols) + + /* Find last non-empty cell */ + int col_count = 0; + for (int c = old_cols - 1; c >= 0; c--) { + const struct cell *cell = &old_row->cells[c]; + if (!(cell->wc == 0 || cell->wc == CELL_SPACER)) { + col_count = c + 1; + break; + } + } + + if (!old_row->linebreak && col_count > 0) { + /* Don't truncate logical lines */ + while (col_count < old_cols && old_row->cells[col_count].wc == 0) + col_count++; + } + + xassert(col_count >= 0 && col_count <= old_cols); + + /* Do we have a (at least one) tracking point on this row */ + struct coord *tp; + if (unlikely((*next_tp)->row == old_row_idx)) { + tp = *next_tp; + + /* Find the *last* tracking point on this row */ + struct coord *last_on_row = tp; + for (struct coord **iter = next_tp; (*iter)->row == old_row_idx; iter++) + last_on_row = *iter; + + /* And make sure its end point is included in the col range */ + xassert(last_on_row->row == old_row_idx); + col_count = max(col_count, last_on_row->col + 1); + } else + tp = NULL; + + /* Does this row have any URIs? */ + struct row_range *uri_range, *uri_range_terminator; + struct row_range *underline_range, *underline_range_terminator; + const struct row_data *extra = old_row->extra; + + if (extra != NULL && extra->uri_ranges.count > 0) { + uri_range = &extra->uri_ranges.v[0]; + uri_range_terminator = &extra->uri_ranges.v[extra->uri_ranges.count]; + + /* Make sure the *last* URI range's end point is included + * in the copy */ + const struct row_range *last_on_row = + &extra->uri_ranges.v[extra->uri_ranges.count - 1]; + col_count = max(col_count, last_on_row->end + 1); + } else + uri_range = uri_range_terminator = NULL; + + if (extra != NULL && extra->underline_ranges.count > 0) { + underline_range = &extra->underline_ranges.v[0]; + underline_range_terminator = &extra->underline_ranges.v[extra->underline_ranges.count]; + + const struct row_range *last_on_row = + &extra->underline_ranges.v[extra->underline_ranges.count - 1]; + col_count = max(col_count, last_on_row->end + 1); + } else + underline_range = underline_range_terminator = NULL; + + if (unlikely(col_count > 0 && coalesced_linebreaks > 0)) { + for (size_t line_no = 0; line_no < coalesced_linebreaks; line_no++) { + /* Erase the remaining cells */ + memset(&new_row->cells[new_col_idx], 0, + (new_cols - new_col_idx) * sizeof(new_row->cells[0])); + new_row->linebreak = true; + line_wrap(); + } + + coalesced_linebreaks = 0; + } + + for (int c = 0; c < col_count;) { + const struct cell *old = &old_row->cells[c]; + + /* Row full, emit newline and get a new, fresh, row */ + xassert(new_col_idx <= new_cols); + if (unlikely(new_col_idx >= new_cols)) + line_wrap(); + + char32_t wc = old->wc; + int width = 1; + + if (unlikely(wc >= CELL_COMB_CHARS_LO && wc <= CELL_COMB_CHARS_HI)) { + const struct composed *composed = + composed_lookup(term->composed, wc - CELL_COMB_CHARS_LO); + + width = composed->forced_width > 0 ? composed->forced_width : composed->width; + } else if (unlikely(c + 1 < col_count && (old + 1)->wc >= CELL_SPACER + 1)) { + /* Wide character, get its width from the next cell's + SPACER value */ + width = (old + 1)->wc - CELL_SPACER + 1; + } + + /* + * Check if character fits, if not, emit spacers, and push + the character to the next row */ + if (unlikely(new_col_idx + width > new_cols && width <= new_cols)) { + for (; new_col_idx < new_cols; new_col_idx++) { + new_row->cells[new_col_idx].wc = CELL_SPACER; + new_row->cells[new_col_idx].attrs = (struct attributes){0}; + } + line_wrap(); + } + + new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; + + for (int i = 0; i < width; i++) { + if (unlikely(uri_range != NULL && uri_range != uri_range_terminator)) { + if (unlikely(uri_range->start == c)) { + reflow_range_start( + uri_range, ROW_RANGE_URI, new_row, new_col_idx); + } + + if (unlikely(uri_range->end == c)) { + reflow_range_end( + uri_range, ROW_RANGE_URI, new_row, new_col_idx); + grid_row_uri_range_destroy(uri_range); + uri_range++; + } + } + + if (unlikely(underline_range != NULL && underline_range != underline_range_terminator)) { + if (unlikely(underline_range->start == c)) { + reflow_range_start( + underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); + } + + if (unlikely(underline_range->end == c)) { + reflow_range_end( + underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); + grid_row_underline_range_destroy(underline_range); + underline_range++; + } + } + + if (unlikely(tp != NULL)) { + if (unlikely(tp->col == c)) { + do { + xassert(tp->row == old_row_idx); + + tp->row = new_row_idx; + tp->col = new_col_idx; + + next_tp++; + tp = *next_tp; + } while (tp->row == old_row_idx && tp->col == c); + + if (tp->row != old_row_idx) + tp = NULL; + + LOG_DBG("next TP (tp=%p): %dx%d", + (void*)tp, (*next_tp)->row, (*next_tp)->col); + } + } + + if (unlikely(old_row->shell_integration.cmd_start == c)) + new_row->shell_integration.cmd_start = new_col_idx; + + if (unlikely(old_row->shell_integration.cmd_end == c)) + new_row->shell_integration.cmd_end = new_col_idx; + + if (unlikely(width > new_cols)) { + /* Wide character no longer fits on a row, replace + it with a single space */ + new_row->cells[new_col_idx++].wc = 0; + c++; + + /* Walk past the SPACER cells */ + for (int i = 1; i < width; i++, c++, old++) + ; + + /* Continue with next character in the *old* grid */ + break; + } + + new_row->cells[new_col_idx++] = *old; + + /* + * TODO: simulate LCF instead? + * + * Rows have linebreak=true by default. This is needed + * for a number of reasons. However, we want non-empty + * rows to have linebreak=false, *until* we reach the + * end of an old row with linebreak=true, at which + * point we set linebreak=true on the new row. + */ + new_row->linebreak = false; + old++; + c++; + } + } + + if (old_row->linebreak) { + if (col_count > 0) { + /* Erase the remaining cells */ + memset(&new_row->cells[new_col_idx], 0, + (new_cols - new_col_idx) * sizeof(new_row->cells[0])); + new_row->linebreak = true; + + if (r + 1 < old_rows) { + /* Not the last (old) row */ + line_wrap(); + } else if (new_row->extra != NULL) { + if (new_row->extra->uri_ranges.count > 0) { + /* + * line_wrap() "closes" still-open URIs. Since + * this is the *last* row, and since we're + * line-breaking due to a hard line-break (rather + * than running out of cells in the "new_row"), + * there shouldn't be an open URI (it would have + * been closed when we reached the end of the URI + * while reflowing the last "old" row). + */ + int last_idx = new_row->extra->uri_ranges.count - 1; + xassert(new_row->extra->uri_ranges.v[last_idx].end >= 0); + } + + if (new_row->extra->underline_ranges.count > 0) { + int last_idx = new_row->extra->underline_ranges.count - 1; + xassert(new_row->extra->underline_ranges.v[last_idx].end >= 0); + } + } + } else { + /* + * rows have linebreak=true by default. But we don't + * want trailing empty lines to result in actual lines + * in the new grid (think: empty window with prompt at + * the top) + */ + coalesced_linebreaks++; + } + } + + grid_row_free(old_grid[old_row_idx]); + grid->rows[old_row_idx] = NULL; + +#undef line_wrap + } + + /* Erase the remaining cells */ + memset(&new_row->cells[new_col_idx], 0, + (new_cols - new_col_idx) * sizeof(new_row->cells[0])); + + for (struct coord **tp = next_tp; *tp != &terminator; tp++) { + LOG_DBG("TP: row=%d, col=%d (old cols: %d, new cols: %d)", + (*tp)->row, (*tp)->col, old_cols, new_cols); + } + xassert(old_rows == 0 || *next_tp == &terminator); + +#if defined(_DEBUG) + /* Verify all URI ranges have been "closed" */ + for (int r = 0; r < new_rows; r++) { + const struct row *row = new_grid[r]; + + if (row == NULL) + continue; + if (row->extra == NULL) + continue; + + for (size_t i = 0; i < row->extra->uri_ranges.count; i++) + xassert(row->extra->uri_ranges.v[i].end >= 0); + for (size_t i = 0; i < row->extra->underline_ranges.count; i++) + xassert(row->extra->underline_ranges.v[i].end >= 0); + + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); + } + + /* Verify all old rows have been free:d */ + for (int i = 0; i < old_rows; i++) + xassert(grid->rows[i] == NULL); +#endif + + /* Set offset such that the last reflowed row is at the bottom */ + grid->offset = new_row_idx - new_screen_rows + 1; + + while (grid->offset < 0) + grid->offset += new_rows; + while (new_grid[grid->offset] == NULL) + grid->offset = (grid->offset + 1) & (new_rows - 1); + + /* Ensure all visible rows have been allocated */ + for (int r = 0; r < new_screen_rows; r++) { + int idx = (grid->offset + r) & (new_rows - 1); + if (new_grid[idx] == NULL) + new_grid[idx] = grid_row_alloc(new_cols, true); + } + + /* Free old grid (rows already free:d) */ + free(grid->rows); + + grid->rows = new_grid; + grid->num_rows = new_rows; + grid->num_cols = new_cols; + + /* + * Set new viewport, making sure it's not too far down. + * + * This is done by using scrollback-start relative cooardinates, + * and bounding the new viewport to (grid_rows - screen_rows). + */ + int sb_view = grid_row_abs_to_sb( + grid, new_screen_rows, view_follows ? grid->offset : viewport.row); + grid->view = grid_row_sb_to_abs( + grid, new_screen_rows, min(sb_view, new_rows - new_screen_rows)); + + /* Convert absolute coordinates to screen relative */ + cursor.row -= grid->offset; + while (cursor.row < 0) + cursor.row += grid->num_rows; + cursor.row = min(cursor.row, new_screen_rows - 1); + cursor.col = min(cursor.col, new_cols - 1); + + saved_cursor.row -= grid->offset; + while (saved_cursor.row < 0) + saved_cursor.row += grid->num_rows; + saved_cursor.row = min(saved_cursor.row, new_screen_rows - 1); + saved_cursor.col = min(saved_cursor.col, new_cols - 1); + + if (grid->cursor.lcf) { + if (cursor.col + 1 < new_cols) { + cursor.col++; + grid->cursor.lcf = false; + } + } + + if (grid->saved_cursor.lcf) { + if (saved_cursor.col + 1 < new_cols) { + saved_cursor.col++; + grid->saved_cursor.lcf = false; + } + } + + grid->cur_row = new_grid[(grid->offset + cursor.row) & (new_rows - 1)]; + xassert(grid->cur_row != NULL); + + grid->cursor.point = cursor; + grid->saved_cursor.point = saved_cursor; + + /* Free sixels we failed to "map" to the new grid */ + tll_foreach(untranslated_sixels, it) + sixel_destroy(&it->item); + tll_free(untranslated_sixels); + +#if defined(TIME_REFLOW) && TIME_REFLOW + struct timespec stop; + clock_gettime(CLOCK_MONOTONIC, &stop); + + struct timespec diff; + timespec_sub(&stop, &start, &diff); + LOG_INFO("reflowed %d -> %d rows in %lds %ldns", + old_rows, new_rows, + (long)diff.tv_sec, + diff.tv_nsec); +#endif +} + +static bool +ranges_match(const struct row_range *r1, const struct row_range *r2, + enum row_range_type type) +{ + switch (type) { + case ROW_RANGE_URI: + /* TODO: also match URI? */ + return r1->uri.id == r2->uri.id; + + case ROW_RANGE_UNDERLINE: + return r1->underline.style == r2->underline.style && + r1->underline.color_src == r2->underline.color_src && + r1->underline.color == r2->underline.color; + } + + BUG("invalid range type"); + return false; +} + +static bool +range_match_data(const struct row_range *r, const union row_range_data *data, + enum row_range_type type) +{ + switch (type) { + case ROW_RANGE_URI: + return r->uri.id == data->uri.id; + + case ROW_RANGE_UNDERLINE: + return r->underline.style == data->underline.style && + r->underline.color_src == data->underline.color_src && + r->underline.color == data->underline.color; + } + + BUG("invalid range type"); + return false; +} + +static void +grid_row_range_put(struct row_ranges *ranges, int col, + const union row_range_data *data, enum row_range_type type) +{ + size_t insert_idx = 0; + bool replace = false; + bool run_merge_pass = false; + + for (int i = ranges->count - 1; i >= 0; i--) { + struct row_range *r = &ranges->v[i]; + + const bool matching = range_match_data(r, data, type); + + if (matching && r->end + 1 == col) { + /* Extend existing range tail */ + r->end++; + return; + } + + else if (r->end < col) { + insert_idx = i + 1; + break; + } + + else if (r->start > col) + continue; + + else { + xassert(r->start <= col); + xassert(r->end >= col); + + if (matching) + return; + + if (r->start == r->end) { + replace = true; + run_merge_pass = true; + insert_idx = i; + } else if (r->start == col) { + run_merge_pass = true; + r->start++; + insert_idx = i; + } else if (r->end == col) { + run_merge_pass = true; + r->end--; + insert_idx = i + 1; + } else { + xassert(r->start < col); + xassert(r->end > col); + + union row_range_data insert_data; + switch (type) { + case ROW_RANGE_URI: insert_data.uri = r->uri; break; + case ROW_RANGE_UNDERLINE: insert_data.underline = r->underline; break; + } + + range_insert(ranges, i + 1, col + 1, r->end, type, &insert_data); + + /* The insertion may xrealloc() the vector, making our + * 'old' pointer invalid */ + r = &ranges->v[i]; + r->end = col - 1; + xassert(r->start <= r->end); + + insert_idx = i + 1; + } + + break; + } + } + + xassert(insert_idx <= ranges->count); + + if (replace) { + grid_row_range_destroy(&ranges->v[insert_idx], type); + ranges->v[insert_idx] = (struct row_range){ + .start = col, + .end = col, + }; + + switch (type) { + case ROW_RANGE_URI: + ranges->v[insert_idx].uri.id = data->uri.id; + ranges->v[insert_idx].uri.uri = xstrdup(data->uri.uri); + break; + + case ROW_RANGE_UNDERLINE: + ranges->v[insert_idx].underline = data->underline; + break; + } + } else + range_insert(ranges, insert_idx, col, col, type, data); + + if (run_merge_pass) { + for (size_t i = 1; i < ranges->count; i++) { + struct row_range *r1 = &ranges->v[i - 1]; + struct row_range *r2 = &ranges->v[i]; + + if (ranges_match(r1, r2, type) && r1->end + 1 == r2->start) { + r1->end = r2->end; + range_delete(ranges, type, i); + i--; + } + } + } +} + +void +grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) +{ + ensure_row_has_extra_data(row); + + grid_row_range_put( + &row->extra->uri_ranges, col, + &(union row_range_data){.uri = {.id = id, .uri = (char *)uri}}, + ROW_RANGE_URI); + + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); +} + +void +grid_row_underline_range_put(struct row *row, int col, struct underline_range_data data) +{ + ensure_row_has_extra_data(row); + + grid_row_range_put( + &row->extra->underline_ranges, col, + &(union row_range_data){.underline = data}, + ROW_RANGE_UNDERLINE); + + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); +} + +UNITTEST +{ + struct row_data row_data = {.uri_ranges = {0}}; + struct row row = {.extra = &row_data}; + +#define verify_range(idx, _start, _end, _id) \ + do { \ + xassert(idx < row_data.uri_ranges.count); \ + xassert(row_data.uri_ranges.v[idx].start == _start); \ + xassert(row_data.uri_ranges.v[idx].end == _end); \ + xassert(row_data.uri_ranges.v[idx].uri.id == _id); \ + } while (0) + + grid_row_uri_range_put(&row, 0, "http://foo.bar", 123); + grid_row_uri_range_put(&row, 1, "http://foo.bar", 123); + grid_row_uri_range_put(&row, 2, "http://foo.bar", 123); + grid_row_uri_range_put(&row, 3, "http://foo.bar", 123); + xassert(row_data.uri_ranges.count == 1); + verify_range(0, 0, 3, 123); + + /* No-op */ + grid_row_uri_range_put(&row, 0, "http://foo.bar", 123); + xassert(row_data.uri_ranges.count == 1); + verify_range(0, 0, 3, 123); + + /* Replace head */ + grid_row_uri_range_put(&row, 0, "http://head", 456); + xassert(row_data.uri_ranges.count == 2); + verify_range(0, 0, 0, 456); + verify_range(1, 1, 3, 123); + + /* Replace tail */ + grid_row_uri_range_put(&row, 3, "http://tail", 789); + xassert(row_data.uri_ranges.count == 3); + verify_range(1, 1, 2, 123); + verify_range(2, 3, 3, 789); + + /* Replace tail + extend head */ + grid_row_uri_range_put(&row, 2, "http://tail", 789); + xassert(row_data.uri_ranges.count == 3); + verify_range(1, 1, 1, 123); + verify_range(2, 2, 3, 789); + + /* Replace + extend tail */ + grid_row_uri_range_put(&row, 1, "http://head", 456); + xassert(row_data.uri_ranges.count == 2); + verify_range(0, 0, 1, 456); + verify_range(1, 2, 3, 789); + + /* Replace + extend, then splice */ + grid_row_uri_range_put(&row, 1, "http://tail", 789); + grid_row_uri_range_put(&row, 2, "http://splice", 000); + xassert(row_data.uri_ranges.count == 4); + verify_range(0, 0, 0, 456); + verify_range(1, 1, 1, 789); + verify_range(2, 2, 2, 000); + verify_range(3, 3, 3, 789); + + for (size_t i = 0; i < row_data.uri_ranges.count; i++) + grid_row_uri_range_destroy(&row_data.uri_ranges.v[i]); + free(row_data.uri_ranges.v); + +#undef verify_range +} + +static void +grid_row_range_erase(struct row_ranges *ranges, enum row_range_type type, + int start, int end) +{ + xassert(start <= end); + + /* Split up, or remove, URI ranges affected by the erase */ + for (int i = ranges->count - 1; i >= 0; i--) { + struct row_range *old = &ranges->v[i]; + + if (old->end < start) + return; + + if (old->start > end) + continue; + + if (start <= old->start && end >= old->end) { + /* Erase range covers URI completely - remove it */ + range_delete(ranges, type, i); + } + + else if (start > old->start && end < old->end) { + /* + * Erase range erases a part in the middle of the URI + * + * Must copy, since range_insert() may xrealloc() (thus + * causing 'old' to be invalid) before it dereferences + * old->data + */ + union row_range_data data = old->data; + range_insert(ranges, i + 1, end + 1, old->end, type, &data); + + /* The insertion may xrealloc() the vector, making our + * 'old' pointer invalid */ + old = &ranges->v[i]; + old->end = start - 1; + return; /* There can be no more URIs affected by the erase range */ + } + + else if (start <= old->start && end >= old->start) { + /* Erase range erases the head of the URI */ + xassert(start <= old->start); + old->start = end + 1; + } + + else if (start <= old->end && end >= old->end) { + /* Erase range erases the tail of the URI */ + xassert(end >= old->end); + old->end = start - 1; + return; /* There can be no more overlapping URIs */ + } + } +} + +void +grid_row_uri_range_erase(struct row *row, int start, int end) +{ + xassert(row->extra != NULL); + grid_row_range_erase(&row->extra->uri_ranges, ROW_RANGE_URI, start, end); +} + +void +grid_row_underline_range_erase(struct row *row, int start, int end) +{ + xassert(row->extra != NULL); + grid_row_range_erase(&row->extra->underline_ranges, ROW_RANGE_UNDERLINE, start, end); +} + +UNITTEST +{ + struct row_data row_data = {.uri_ranges = {0}}; + struct row row = {.extra = &row_data}; + const union row_range_data data = { + .uri = { + .id = 0, + .uri = (char *)"dummy", + }, + }; + + /* Try erasing a row without any URIs */ + grid_row_uri_range_erase(&row, 0, 200); + xassert(row_data.uri_ranges.count == 0); + + range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); + range_append(&row_data.uri_ranges, 11, 20, ROW_RANGE_URI, &data); + xassert(row_data.uri_ranges.count == 2); + xassert(row_data.uri_ranges.v[1].start == 11); + xassert(row_data.uri_ranges.v[1].end == 20); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); + + /* Erase both URis */ + grid_row_uri_range_erase(&row, 1, 20); + xassert(row_data.uri_ranges.count == 0); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); + + /* Two URIs, then erase second half of the first, first half of + the second */ + range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); + range_append(&row_data.uri_ranges, 11, 20, ROW_RANGE_URI, &data); + grid_row_uri_range_erase(&row, 5, 15); + xassert(row_data.uri_ranges.count == 2); + xassert(row_data.uri_ranges.v[0].start == 1); + xassert(row_data.uri_ranges.v[0].end == 4); + xassert(row_data.uri_ranges.v[1].start == 16); + xassert(row_data.uri_ranges.v[1].end == 20); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); + + grid_row_range_destroy(&row_data.uri_ranges.v[0], ROW_RANGE_URI); + grid_row_range_destroy(&row_data.uri_ranges.v[1], ROW_RANGE_URI); + row_data.uri_ranges.count = 0; + + /* One URI, erase middle part of it */ + range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); + grid_row_uri_range_erase(&row, 5, 6); + xassert(row_data.uri_ranges.count == 2); + xassert(row_data.uri_ranges.v[0].start == 1); + xassert(row_data.uri_ranges.v[0].end == 4); + xassert(row_data.uri_ranges.v[1].start == 7); + xassert(row_data.uri_ranges.v[1].end == 10); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); + + grid_row_range_destroy(&row_data.uri_ranges.v[0], ROW_RANGE_URI); + grid_row_range_destroy(&row_data.uri_ranges.v[1], ROW_RANGE_URI); + row_data.uri_ranges.count = 0; + + /* + * Regression test: erasing the middle part of an URI causes us to + * insert a new URI (we split the partly erased URI into two). + * + * The insertion logic typically triggers an xrealloc(), which, in + * some cases, *moves* the entire URI vector to a new base + * address. grid_row_uri_range_erase() did not account for this, + * and tried to update the 'end' member in the URI range we just + * split. This causes foot to crash when the xrealloc() has moved + * the URI range vector. + * + * (note: we're only verifying we don't crash here, hence the lack + * of assertions). + */ + free(row_data.uri_ranges.v); + row_data.uri_ranges.v = NULL; + row_data.uri_ranges.size = 0; + range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); + xassert(row_data.uri_ranges.size == 1); + + grid_row_uri_range_erase(&row, 5, 7); + xassert(row_data.uri_ranges.count == 2); + + grid_row_ranges_destroy(&row_data.uri_ranges, ROW_RANGE_URI); + free(row_data.uri_ranges.v); +} diff --git a/grid.h b/grid.h new file mode 100644 index 0000000..71bdc29 --- /dev/null +++ b/grid.h @@ -0,0 +1,138 @@ +#pragma once + +#include <stddef.h> +#include "debug.h" +#include "terminal.h" + +struct grid *grid_snapshot(const struct grid *grid); +void grid_free(struct grid *grid); + +void grid_swap_row(struct grid *grid, int row_a, int row_b); +struct row *grid_row_alloc(int cols, bool initialize); +void grid_row_free(struct row *row); + +void grid_resize_without_reflow( + struct grid *grid, int new_rows, int new_cols, + int old_screen_rows, int new_screen_rows); + +void grid_resize_and_reflow( + struct grid *grid, const struct terminal *term, int new_rows, int new_cols, + int old_screen_rows, int new_screen_rows, + size_t tracking_points_count, + struct coord *const _tracking_points[static tracking_points_count]); + +/* Convert row numbers between scrollback-relative and absolute coordinates */ +int grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row); +int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row); + +int grid_sb_start_ignore_uninitialized(const struct grid *grid, int screen_rows); +int grid_row_abs_to_sb_precalc_sb_start( + const struct grid *grid, int sb_start, int abs_row); +int grid_row_sb_to_abs_precalc_sb_start( + const struct grid *grid, int sb_start, int sb_rel_row); + +static inline int +grid_row_absolute(const struct grid *grid, int row_no) +{ + return (grid->offset + row_no) & (grid->num_rows - 1); +} + +static inline int +grid_row_absolute_in_view(const struct grid *grid, int row_no) +{ + return (grid->view + row_no) & (grid->num_rows - 1); +} + +static inline struct row * +_grid_row_maybe_alloc(struct grid *grid, int row_no, bool alloc_if_null) +{ + xassert(grid->offset >= 0); + + int real_row = grid_row_absolute(grid, row_no); + struct row *row = grid->rows[real_row]; + + if (row == NULL && alloc_if_null) { + row = grid_row_alloc(grid->num_cols, false); + grid->rows[real_row] = row; + } + + xassert(row != NULL); + return row; +} + +static inline struct row * +grid_row(struct grid *grid, int row_no) +{ + return _grid_row_maybe_alloc(grid, row_no, false); +} + +static inline struct row * +grid_row_and_alloc(struct grid *grid, int row_no) +{ + return _grid_row_maybe_alloc(grid, row_no, true); +} + +static inline struct row * +grid_row_in_view(struct grid *grid, int row_no) +{ + xassert(grid->view >= 0); + + int real_row = grid_row_absolute_in_view(grid, row_no); + struct row *row = grid->rows[real_row]; + + xassert(row != NULL); + return row; +} + +void grid_row_uri_range_put( + struct row *row, int col, const char *uri, uint64_t id); +void grid_row_uri_range_erase(struct row *row, int start, int end); + +void grid_row_underline_range_put( + struct row *row, int col, struct underline_range_data data); +void grid_row_underline_range_erase(struct row *row, int start, int end); + +static inline void +grid_row_uri_range_destroy(struct row_range *range) +{ + free(range->uri.uri); +} + +static inline void +grid_row_underline_range_destroy(struct row_range *range) +{ +} + +static inline void +grid_row_range_destroy(struct row_range *range, enum row_range_type type) +{ + switch (type) { + case ROW_RANGE_URI: grid_row_uri_range_destroy(range); break; + case ROW_RANGE_UNDERLINE: grid_row_underline_range_destroy(range); break; + } +} + +static inline void +grid_row_ranges_destroy(struct row_ranges *ranges, enum row_range_type type) +{ + for (int i = 0; i < ranges->count; i++) { + grid_row_range_destroy(&ranges->v[i], type); + } +} + +static inline void +grid_row_reset_extra(struct row *row) +{ + struct row_data *extra = row->extra; + + if (likely(extra == NULL)) + return; + + grid_row_ranges_destroy(&extra->uri_ranges, ROW_RANGE_URI); + grid_row_ranges_destroy(&extra->underline_ranges, ROW_RANGE_UNDERLINE); + free(extra->uri_ranges.v); + free(extra->underline_ranges.v); + + free(extra); + row->extra = NULL; +} diff --git a/hsl.c b/hsl.c new file mode 100644 index 0000000..1a8c919 --- /dev/null +++ b/hsl.c @@ -0,0 +1,54 @@ +#include "hsl.h" + +#include <math.h> + +uint32_t +hsl_to_rgb(int hue, int sat, int lum) +{ + double L = lum / 100.0; + double S = sat / 100.0; + double C = (1. - fabs(2. * L - 1.)) * S; + + double X = C * (1. - fabs(fmod((double)hue / 60., 2.) - 1.)); + double m = L - C / 2.; + + double r, g, b; + if (hue >= 0 && hue <= 60) { + r = C; + g = X; + b = 0.; + } else if (hue >= 60 && hue <= 120) { + r = X; + g = C; + b = 0.; + } else if (hue >= 120 && hue <= 180) { + r = 0.; + g = C; + b = X; + } else if (hue >= 180 && hue <= 240) { + r = 0.; + g = X; + b = C; + } else if (hue >= 240 && hue <= 300) { + r = X; + g = 0.; + b = C; + } else if (hue >= 300 && hue <= 360) { + r = C; + g = 0.; + b = X; + } else { + r = 0.; + g = 0.; + b = 0.; + } + + r += m; + g += m; + b += m; + + return ( + (uint8_t)round(r * 255.) << 16 | + (uint8_t)round(g * 255.) << 8 | + (uint8_t)round(b * 255.) << 0); +} diff --git a/hsl.h b/hsl.h new file mode 100644 index 0000000..1aaf7e6 --- /dev/null +++ b/hsl.h @@ -0,0 +1,5 @@ +#pragma once + +#include <stdint.h> + +uint32_t hsl_to_rgb(int hue, int sat, int lum); diff --git a/icons/hicolor/48x48/apps/foot.png b/icons/hicolor/48x48/apps/foot.png new file mode 100644 index 0000000..81fa206 Binary files /dev/null and b/icons/hicolor/48x48/apps/foot.png differ diff --git a/icons/hicolor/scalable/apps/foot.svg b/icons/hicolor/scalable/apps/foot.svg new file mode 100644 index 0000000..a87cdc9 --- /dev/null +++ b/icons/hicolor/scalable/apps/foot.svg @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="128" height="128" + inkscape:version="1.0 (4035a4fb49, 2020-05-01)"> + <title>foot logo + + + + + + + image/svg+xml + + + + Lennard Hofmann + + + https://freesvg.org/human-footprints + + foot logo + + + terminal emulator + footprint + + + 2020-06-23 + Black square representing a terminal showing a human footprint as a prompt symbol and an underscore as the cursor + + + + + + + + + + + + + + + + + diff --git a/icons/meson.build b/icons/meson.build new file mode 100644 index 0000000..8838206 --- /dev/null +++ b/icons/meson.build @@ -0,0 +1 @@ +install_subdir('hicolor', install_dir : join_paths(get_option('datadir'), 'icons')) diff --git a/ime.c b/ime.c new file mode 100644 index 0000000..c6ccb47 --- /dev/null +++ b/ime.c @@ -0,0 +1,525 @@ +#include "ime.h" + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + +#include + +#include "text-input-unstable-v3.h" + +#define LOG_MODULE "ime" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" +#include "render.h" +#include "search.h" +#include "terminal.h" +#include "util.h" +#include "wayland.h" +#include "xmalloc.h" + +static void +ime_reset_pending_preedit(struct seat *seat) +{ + free(seat->ime.preedit.pending.text); + seat->ime.preedit.pending.text = NULL; +} + +static void +ime_reset_pending_commit(struct seat *seat) +{ + free(seat->ime.commit.pending.text); + seat->ime.commit.pending.text = NULL; +} + +void +ime_reset_pending(struct seat *seat) +{ + ime_reset_pending_preedit(seat); + ime_reset_pending_commit(seat); +} + +void +ime_reset_preedit(struct seat *seat) +{ + if (seat->ime.preedit.cells == NULL) + return; + + free(seat->ime.preedit.text); + free(seat->ime.preedit.cells); + seat->ime.preedit.text = NULL; + seat->ime.preedit.cells = NULL; + seat->ime.preedit.count = 0; +} + +static void +enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface) +{ + struct seat *seat = data; + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; + + LOG_DBG("enter: seat=%s, term=%p", seat->name, (const void *)term); + + if (seat->kbd_focus != term) { + LOG_WARN("compositor sent ime::enter() event before the " + "corresponding keyboard_enter() event"); + } + + /* The main grid is the *only* input-receiving surface we have */ + seat->ime_focus = term; + + const struct coord *cursor = &term->grid->cursor.point; + + term_ime_set_cursor_rect( + term, + term->margins.left + cursor->col * term->cell_width, + term->margins.top + cursor->row * term->cell_height, + term->cell_width, + term->cell_height); + + ime_enable(seat); +} + +static void +leave(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface) +{ + struct seat *seat = data; + LOG_DBG("leave: seat=%s", seat->name); + + ime_disable(seat); + seat->ime_focus = NULL; +} + +static void +preedit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + const char *text, int32_t cursor_begin, int32_t cursor_end) +{ + LOG_DBG("preedit-string: text=%s, begin=%d, end=%d", text, cursor_begin, cursor_end); + + struct seat *seat = data; + + ime_reset_pending_preedit(seat); + + if (text != NULL) { + seat->ime.preedit.pending.text = xstrdup(text); + seat->ime.preedit.pending.cursor_begin = cursor_begin; + seat->ime.preedit.pending.cursor_end = cursor_end; + } +} + +static void +commit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + const char *text) +{ + LOG_DBG("commit: text=%s", text); + + struct seat *seat = data; + + ime_reset_pending_commit(seat); + + if (text != NULL) + seat->ime.commit.pending.text = xstrdup(text); +} + +static void +delete_surrounding_text(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t before_length, uint32_t after_length) +{ + LOG_DBG("delete-surrounding: before=%d, after=%d", before_length, after_length); + + struct seat *seat = data; + seat->ime.surrounding.pending.before_length = before_length; + seat->ime.surrounding.pending.after_length = after_length; +} + +static void +done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t serial) +{ + /* + * From text-input-unstable-v3.h: + * + * The application must proceed by evaluating the changes in the + * following order: + * + * 1. Replace existing preedit string with the cursor. + * 2. Delete requested surrounding text. + * 3. Insert commit string with the cursor at its end. + * 4. Calculate surrounding text to send. + * 5. Insert new preedit text in cursor position. + * 6. Place cursor inside preedit text. + */ + + LOG_DBG("done: serial=%u", serial); + struct seat *seat = data; + struct terminal *term = seat->ime_focus; + + if (seat->ime.serial != serial) { + LOG_DBG("IME serial mismatch: expected=0x%08x, got 0x%08x", + seat->ime.serial, serial); + return; + } + + if (term == NULL) { + static bool have_warned = false; + if (!have_warned) { + LOG_WARN( + "%s: text-input::done() received on seat that isn't " + "focusing a terminal window", seat->name); + have_warned = true; + } + } + + /* 1. Delete existing pre-edit text */ + if (seat->ime.preedit.cells != NULL) { + ime_reset_preedit(seat); + + if (term != NULL) { + if (term->is_searching) + render_refresh_search(term); + else + render_refresh(term); + } + } + + /* + * 2. Delete requested surrounding text + * + * We don't support deleting surrounding text. But, we also never + * call set_surrounding_text() so hopefully we should never + * receive any requests to delete surrounding text. + */ + + /* 3. Insert commit string */ + if (seat->ime.commit.pending.text != NULL) { + const char *text = seat->ime.commit.pending.text; + size_t len = strlen(text); + + if (term != NULL) { + if (term->is_searching) { + search_add_chars(term, text, len); + render_refresh_search(term); + } else + term_to_slave(term, text, len); + } + ime_reset_pending_commit(seat); + } + + /* 4. Calculate surrounding text to send - not supported */ + + /* 5. Insert new pre-edit text */ + char32_t *allocated_preedit_text = NULL; + + if (seat->ime.preedit.pending.text == NULL || + seat->ime.preedit.pending.text[0] == '\0' || + (allocated_preedit_text = ambstoc32(seat->ime.preedit.pending.text)) == NULL) + { + ime_reset_pending_preedit(seat); + return; + } + + xassert(seat->ime.preedit.pending.text != NULL); + xassert(allocated_preedit_text != NULL); + + seat->ime.preedit.text = allocated_preedit_text; + + size_t wchars = c32len(seat->ime.preedit.text); + + /* Next, count number of cells needed */ + size_t cell_count = 0; + size_t widths[wchars + 1]; + + for (size_t i = 0; i < wchars; i++) { + int width = max(c32width(seat->ime.preedit.text[i]), 1); + widths[i] = width; + cell_count += width; + } + + /* Allocate cells */ + seat->ime.preedit.cells = xmalloc( + cell_count * sizeof(seat->ime.preedit.cells[0])); + seat->ime.preedit.count = cell_count; + + /* Populate cells */ + for (size_t i = 0, cell_idx = 0; i < wchars; i++) { + struct cell *cell = &seat->ime.preedit.cells[cell_idx]; + + int width = widths[i]; + + cell->wc = seat->ime.preedit.text[i]; + cell->attrs = (struct attributes){.clean = 0}; + + for (int j = 1; j < width; j++) { + cell = &seat->ime.preedit.cells[cell_idx + j]; + cell->wc = CELL_SPACER + width - j; + cell->attrs = (struct attributes){.clean = 1}; + } + + cell_idx += width; + } + + const size_t byte_len = strlen(seat->ime.preedit.pending.text); + + /* Pre-edit cursor - hidden */ + if (seat->ime.preedit.pending.cursor_begin == -1 || + seat->ime.preedit.pending.cursor_end == -1) + { + /* Note: docs says *both* begin and end should be -1, + * but what else can we do if only one is -1? */ + LOG_DBG("pre-edit cursor is hidden"); + seat->ime.preedit.cursor.hidden = true; + seat->ime.preedit.cursor.start = -1; + seat->ime.preedit.cursor.end = -1; + } + + else if (seat->ime.preedit.pending.cursor_begin == byte_len && + seat->ime.preedit.pending.cursor_end == byte_len) + { + /* Cursor is *after* the entire pre-edit string */ + seat->ime.preedit.cursor.hidden = false; + seat->ime.preedit.cursor.start = cell_count; + seat->ime.preedit.cursor.end = cell_count; + } + + else { + /* + * Translate cursor position to cell indices + * + * The cursor_begin and cursor_end are counted in + * *bytes*. We want to map them to *cell* indices. + * + * To do this, we use mblen() to step though the utf-8 + * pre-edit string, advancing a unicode character index as + * we go, *and* advancing a *cell* index using c32width() + * of the unicode character. + * + * When we find the matching *byte* index, we at the same + * time know both the unicode *and* cell index. + */ + + int cell_begin = -1, cell_end = -1; + for (size_t byte_idx = 0, wc_idx = 0, cell_idx = 0; + byte_idx < byte_len && + wc_idx < wchars && + cell_idx < cell_count && + (cell_begin < 0 || cell_end < 0); + cell_idx += widths[wc_idx], wc_idx++) + { + if (seat->ime.preedit.pending.cursor_begin == byte_idx) + cell_begin = cell_idx; + if (seat->ime.preedit.pending.cursor_end == byte_idx) + cell_end = cell_idx; + + /* Number of bytes of *next* utf-8 character */ + size_t left = byte_len - byte_idx; + int wc_bytes = mblen(&seat->ime.preedit.pending.text[byte_idx], left); + + if (wc_bytes <= 0) + break; + + byte_idx += wc_bytes; + } + + if (seat->ime.preedit.pending.cursor_end >= byte_len) + cell_end = cell_count; + + /* Bounded by number of screen columns */ + cell_begin = min(max(cell_begin, 0), cell_count - 1); + cell_end = min(max(cell_end, 0), cell_count); + + if (cell_end < cell_begin) + cell_end = cell_begin; + + /* Expand cursor end to end of glyph */ + while (cell_end > cell_begin && cell_end < cell_count && + seat->ime.preedit.cells[cell_end].wc >= CELL_SPACER) + { + cell_end++; + } + + LOG_DBG("pre-edit cursor: begin=%d, end=%d", cell_begin, cell_end); + + xassert(cell_begin >= 0); + xassert(cell_begin < cell_count); + xassert(cell_begin <= cell_end); + xassert(cell_end >= 0); + xassert(cell_end <= cell_count); + + seat->ime.preedit.cursor.hidden = false; + seat->ime.preedit.cursor.start = cell_begin; + seat->ime.preedit.cursor.end = cell_end; + } + + /* Underline pre-edit string that is *not* covered by the cursor */ + bool hidden = seat->ime.preedit.cursor.hidden; + int start = seat->ime.preedit.cursor.start; + int end = seat->ime.preedit.cursor.end; + + for (size_t i = 0, cell_idx = 0; i < wchars; cell_idx += widths[i], i++) { + if (hidden || start == end || cell_idx < start || cell_idx >= end) { + struct cell *cell = &seat->ime.preedit.cells[cell_idx]; + cell->attrs.underline = true; + } + } + + ime_reset_pending_preedit(seat); + + if (term != NULL) { + if (term->is_searching) + render_refresh_search(term); + else + render_refresh(term); + } +} + +static void +ime_send_cursor_rect(struct seat *seat) +{ + if (unlikely(seat->wayl->text_input_manager == NULL)) + return; + + if (seat->ime_focus == NULL) + return; + + struct terminal *term = seat->ime_focus; + + if (!term->ime_enabled) + return; + + if (seat->ime.cursor_rect.pending.x == seat->ime.cursor_rect.sent.x && + seat->ime.cursor_rect.pending.y == seat->ime.cursor_rect.sent.y && + seat->ime.cursor_rect.pending.width == seat->ime.cursor_rect.sent.width && + seat->ime.cursor_rect.pending.height == seat->ime.cursor_rect.sent.height) + { + return; + } + + zwp_text_input_v3_set_cursor_rectangle( + seat->wl_text_input, + seat->ime.cursor_rect.pending.x / term->scale, + seat->ime.cursor_rect.pending.y / term->scale, + seat->ime.cursor_rect.pending.width / term->scale, + seat->ime.cursor_rect.pending.height / term->scale); + + zwp_text_input_v3_commit(seat->wl_text_input); + seat->ime.serial++; + + seat->ime.cursor_rect.sent = seat->ime.cursor_rect.pending; +} + +void +ime_enable(struct seat *seat) +{ + if (unlikely(seat->wayl->text_input_manager == NULL)) + return; + + if (seat->ime_focus == NULL) + return; + + struct terminal *term = seat->ime_focus; + if (term == NULL) + return; + + if (!term->ime_enabled) + return; + + ime_reset_pending(seat); + ime_reset_preedit(seat); + + zwp_text_input_v3_enable(seat->wl_text_input); + zwp_text_input_v3_set_content_type( + seat->wl_text_input, + ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL); + + zwp_text_input_v3_set_cursor_rectangle( + seat->wl_text_input, + seat->ime.cursor_rect.pending.x / term->scale, + seat->ime.cursor_rect.pending.y / term->scale, + seat->ime.cursor_rect.pending.width / term->scale, + seat->ime.cursor_rect.pending.height / term->scale); + + seat->ime.cursor_rect.sent = seat->ime.cursor_rect.pending; + + zwp_text_input_v3_commit(seat->wl_text_input); + seat->ime.serial++; +} + +void +ime_disable(struct seat *seat) +{ + if (unlikely(seat->wayl->text_input_manager == NULL)) + return; + + if (seat->ime_focus == NULL) + return; + + ime_reset_pending(seat); + ime_reset_preedit(seat); + + zwp_text_input_v3_disable(seat->wl_text_input); + zwp_text_input_v3_commit(seat->wl_text_input); + seat->ime.serial++; +} + +void +ime_update_cursor_rect(struct seat *seat) +{ + struct terminal *term = seat->ime_focus; + + /* Set in render_ime_preedit() */ + if (seat->ime.preedit.cells != NULL) + goto update; + + /* Set in render_search_box() */ + if (term->is_searching) + goto update; + + int x, y, width, height; + int col = term->grid->cursor.point.col; + int row = term->grid->cursor.point.row; + row += term->grid->offset; + row -= term->grid->view; + row &= term->grid->num_rows - 1; + x = term->margins.left + col * term->cell_width; + y = term->margins.top + row * term->cell_height; + + if (term->cursor_style == CURSOR_BEAM) + width = 1; + else + width = term->cell_width; + + height = term->cell_height; + + seat->ime.cursor_rect.pending.x = x; + seat->ime.cursor_rect.pending.y = y; + seat->ime.cursor_rect.pending.width = width; + seat->ime.cursor_rect.pending.height = height; + +update: + ime_send_cursor_rect(seat); +} + +const struct zwp_text_input_v3_listener text_input_listener = { + .enter = &enter, + .leave = &leave, + .preedit_string = &preedit_string, + .commit_string = &commit_string, + .delete_surrounding_text = &delete_surrounding_text, + .done = &done, +}; + +#else /* !FOOT_IME_ENABLED */ + +void ime_enable(struct seat *seat) {} +void ime_disable(struct seat *seat) {} +void ime_update_cursor_rect(struct seat *seat) {} + +void ime_reset_pending_preedit(struct seat *seat) {} +void ime_reset_pending_commit(struct seat *seat) {} +void ime_reset_pending(struct seat *seat) {} +void ime_reset_preedit(struct seat *seat) {} + +#endif diff --git a/ime.h b/ime.h new file mode 100644 index 0000000..3127f4d --- /dev/null +++ b/ime.h @@ -0,0 +1,19 @@ +#pragma once + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + +#include "text-input-unstable-v3.h" + +extern const struct zwp_text_input_v3_listener text_input_listener; + +#endif /* FOOT_IME_ENABLED */ + +struct seat; +struct terminal; + +void ime_enable(struct seat *seat); +void ime_disable(struct seat *seat); +void ime_update_cursor_rect(struct seat *seat); + +void ime_reset_pending(struct seat *seat); +void ime_reset_preedit(struct seat *seat); diff --git a/input.c b/input.c new file mode 100644 index 0000000..0e5c031 --- /dev/null +++ b/input.c @@ -0,0 +1,3871 @@ +#include "input.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include + +#define LOG_MODULE "input" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "commands.h" +#include "config.h" +#include "grid.h" +#include "keymap.h" +#include "kitty-keymap.h" +#include "macros.h" +#include "quirks.h" +#include "render.h" +#include "search.h" +#include "selection.h" +#include "spawn.h" +#include "terminal.h" +#include "tokenize.h" +#include "unicode-mode.h" +#include "url-mode.h" +#include "util.h" +#include "vt.h" +#include "xkbcommon-vmod.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +struct pipe_context { + char *text; + size_t idx; + size_t left; +}; + +static bool +fdm_write_pipe(struct fdm *fdm, int fd, int events, void *data) +{ + struct pipe_context *ctx = data; + + if (events & EPOLLHUP) + goto pipe_closed; + + xassert(events & EPOLLOUT); + ssize_t written = write(fd, &ctx->text[ctx->idx], ctx->left); + + if (written < 0) { + LOG_WARN("failed to write to pipe: %s", strerror(errno)); + goto pipe_closed; + } + + xassert(written <= ctx->left); + ctx->idx += written; + ctx->left -= written; + + if (ctx->left == 0) + goto pipe_closed; + + return true; + +pipe_closed: + free(ctx->text); + free(ctx); + fdm_del(fdm, fd); + return true; +} + +static void alternate_scroll(struct seat *seat, int amount, int button); + +static bool +execute_binding(struct seat *seat, struct terminal *term, + const struct key_binding *binding, uint32_t serial, int amount) +{ + const enum bind_action_normal action = binding->action; + + switch (action) { + case BIND_ACTION_NONE: + return true; + + case BIND_ACTION_NOOP: + return true; + + case BIND_ACTION_SCROLLBACK_UP_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->rows); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_UP_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_UP_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, 1); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_UP_MOUSE: + if (term->grid == &term->alt) { + if (term->alt_scrolling) { + alternate_scroll(seat, amount, BTN_BACK); + return true; + } + } else { + cmd_scrollback_up(term, amount); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_DOWN_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->rows); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_DOWN_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, 1); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_DOWN_MOUSE: + if (term->grid == &term->alt) { + if (term->alt_scrolling) { + alternate_scroll(seat, amount, BTN_FORWARD); + return true; + } + } else { + cmd_scrollback_down(term, amount); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_HOME: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_END: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_CLIPBOARD_COPY: + selection_to_clipboard(seat, term, serial); + return true; + + case BIND_ACTION_CLIPBOARD_PASTE: + selection_from_clipboard(seat, term, serial); + term_reset_view(term); + return true; + + case BIND_ACTION_PRIMARY_PASTE: + selection_from_primary(seat, term); + term_reset_view(term); + return true; + + case BIND_ACTION_SEARCH_START: + search_begin(term); + return true; + + case BIND_ACTION_FONT_SIZE_UP: + term_font_size_increase(term); + return true; + + case BIND_ACTION_FONT_SIZE_DOWN: + term_font_size_decrease(term); + return true; + + case BIND_ACTION_FONT_SIZE_RESET: + term_font_size_reset(term); + return true; + + case BIND_ACTION_SPAWN_TERMINAL: + term_spawn_new(term); + return true; + + case BIND_ACTION_MINIMIZE: + xdg_toplevel_set_minimized(term->window->xdg_toplevel); + return true; + + case BIND_ACTION_MAXIMIZE: + if (term->window->is_fullscreen) + xdg_toplevel_unset_fullscreen(term->window->xdg_toplevel); + if (term->window->is_maximized) + xdg_toplevel_unset_maximized(term->window->xdg_toplevel); + else + xdg_toplevel_set_maximized(term->window->xdg_toplevel); + return true; + + case BIND_ACTION_FULLSCREEN: + if (term->window->is_fullscreen) + xdg_toplevel_unset_fullscreen(term->window->xdg_toplevel); + else + xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL); + return true; + + case BIND_ACTION_PIPE_SCROLLBACK: + if (term->grid == &term->alt) + break; + /* FALLTHROUGH */ + case BIND_ACTION_PIPE_VIEW: + case BIND_ACTION_PIPE_SELECTED: + case BIND_ACTION_PIPE_COMMAND_OUTPUT: { + if (binding->aux->type != BINDING_AUX_PIPE) + return true; + + struct pipe_context *ctx = NULL; + + int pipe_fd[2] = {-1, -1}; + int stdout_fd = -1; + int stderr_fd = -1; + + char *text = NULL; + size_t len = 0; + + if (pipe(pipe_fd) < 0) { + LOG_ERRNO("failed to create pipe"); + goto pipe_err; + } + + stdout_fd = open("/dev/null", O_WRONLY); + stderr_fd = open("/dev/null", O_WRONLY); + + if (stdout_fd < 0 || stderr_fd < 0) { + LOG_ERRNO("failed to open /dev/null"); + goto pipe_err; + } + + bool success; + switch (action) { + case BIND_ACTION_PIPE_SCROLLBACK: + success = term_scrollback_to_text(term, &text, &len); + break; + + case BIND_ACTION_PIPE_VIEW: + success = term_view_to_text(term, &text, &len); + break; + + case BIND_ACTION_PIPE_SELECTED: + text = selection_to_text(term); + success = text != NULL; + len = text != NULL ? strlen(text) : 0; + break; + + case BIND_ACTION_PIPE_COMMAND_OUTPUT: + success = term_command_output_to_text(term, &text, &len); + break; + + default: + BUG("Unhandled action type"); + success = false; + break; + } + + if (!success) + goto pipe_err; + + /* Make write-end non-blocking; required by the FDM */ + { + int flags = fcntl(pipe_fd[1], F_GETFL); + if (flags < 0 || + fcntl(pipe_fd[1], F_SETFL, flags | O_NONBLOCK) < 0) + { + LOG_ERRNO("failed to make write-end of pipe non-blocking"); + goto pipe_err; + } + } + + /* Make sure write-end is closed on exec() - or the spawned + * program may not terminate*/ + { + int flags = fcntl(pipe_fd[1], F_GETFD); + if (flags < 0 || + fcntl(pipe_fd[1], F_SETFD, flags | FD_CLOEXEC) < 0) + { + LOG_ERRNO("failed to set FD_CLOEXEC on writeend of pipe"); + goto pipe_err; + } + } + + if (spawn(term->reaper, term->cwd, binding->aux->pipe.args, + pipe_fd[0], stdout_fd, stderr_fd, NULL, NULL, NULL) < 0) + goto pipe_err; + + /* Close read end */ + close(pipe_fd[0]); + + ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct pipe_context){ + .text = text, + .left = len, + }; + + /* Asynchronously write the output to the pipe */ + if (!fdm_add(term->fdm, pipe_fd[1], EPOLLOUT, &fdm_write_pipe, ctx)) + goto pipe_err; + + return true; + + pipe_err: + if (stdout_fd >= 0) + close(stdout_fd); + if (stderr_fd >= 0) + close(stderr_fd); + if (pipe_fd[0] >= 0) + close(pipe_fd[0]); + if (pipe_fd[1] >= 0) + close(pipe_fd[1]); + free(text); + free(ctx); + return true; + } + + case BIND_ACTION_SHOW_URLS_COPY: + case BIND_ACTION_SHOW_URLS_LAUNCH: + case BIND_ACTION_SHOW_URLS_PERSISTENT: { + xassert(!urls_mode_is_active(term)); + + enum url_action url_action = + action == BIND_ACTION_SHOW_URLS_COPY ? URL_ACTION_COPY : + action == BIND_ACTION_SHOW_URLS_LAUNCH ? URL_ACTION_LAUNCH : + URL_ACTION_PERSISTENT; + + urls_collect(term, url_action, &term->conf->url.preg, true, &term->urls); + urls_assign_key_combos(term->conf, &term->urls); + urls_render(term, &term->conf->url.launch); + return true; + } + + case BIND_ACTION_TEXT_BINDING: + xassert(binding->aux->type == BINDING_AUX_TEXT); + term_to_slave(term, binding->aux->text.data, binding->aux->text.len); + return true; + + case BIND_ACTION_PROMPT_PREV: { + if (term->grid != &term->normal) + return false; + + struct grid *grid = term->grid; + const int sb_start = + grid_sb_start_ignore_uninitialized(grid, term->rows); + + /* Check each row from current view-1 (that is, the first + * currently not visible row), up to, and including, the + * scrollback start */ + for (int r_sb_rel = + grid_row_abs_to_sb_precalc_sb_start( + grid, sb_start, grid->view) - 1; + r_sb_rel >= 0; r_sb_rel--) + { + const int r_abs = + grid_row_sb_to_abs_precalc_sb_start(grid, sb_start, r_sb_rel); + + const struct row *row = grid->rows[r_abs]; + xassert(row != NULL); + + if (!row->shell_integration.prompt_marker) + continue; + + grid->view = r_abs; + term_damage_view(term); + render_refresh(term); + break; + } + + return true; + } + + case BIND_ACTION_PROMPT_NEXT: { + if (term->grid != &term->normal) + return false; + + struct grid *grid = term->grid; + const int num_rows = grid->num_rows; + + if (grid->view == grid->offset) { + /* Already at the bottom */ + return true; + } + + /* Check each row from view+1, to the bottom of the scrollback */ + for (int r_abs = (grid->view + 1) & (num_rows - 1); + ; + r_abs = (r_abs + 1) & (num_rows - 1)) + { + const struct row *row = grid->rows[r_abs]; + xassert(row != NULL); + + if (!row->shell_integration.prompt_marker) { + if (r_abs == grid->offset + term->rows - 1) { + /* We've reached the bottom of the scrollback */ + break; + } + continue; + } + + int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); + int ofs_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, grid->offset); + int new_view_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, r_abs); + + new_view_sb_rel = min(ofs_sb_rel, new_view_sb_rel); + grid->view = grid_row_sb_to_abs_precalc_sb_start( + grid, sb_start, new_view_sb_rel); + + term_damage_view(term); + render_refresh(term); + break; + } + + return true; + } + + case BIND_ACTION_UNICODE_INPUT: + unicode_mode_activate(term); + return true; + + case BIND_ACTION_QUIT: + term_shutdown(term); + return true; + + case BIND_ACTION_TAB_NEW: + term_tab_new(term, 0, NULL, NULL, term->shutdown.cb, term->shutdown.cb_data); + return true; + + case BIND_ACTION_TAB_CLOSE: + term_tab_close(term); + return true; + + case BIND_ACTION_TAB_NEXT: { + struct wl_window *win = term->window; + if (win->tab_count > 1) + term_tab_switch(win, (win->active_tab + 1) % win->tab_count); + return true; + } + + case BIND_ACTION_TAB_PREV: { + struct wl_window *win = term->window; + if (win->tab_count > 1) + term_tab_switch(win, + win->active_tab == 0 ? win->tab_count - 1 : win->active_tab - 1); + return true; + } + + case BIND_ACTION_TAB_1: + case BIND_ACTION_TAB_2: + case BIND_ACTION_TAB_3: + case BIND_ACTION_TAB_4: + case BIND_ACTION_TAB_5: + case BIND_ACTION_TAB_6: + case BIND_ACTION_TAB_7: + case BIND_ACTION_TAB_8: + case BIND_ACTION_TAB_9: { + size_t idx = (size_t)(action - BIND_ACTION_TAB_1); + struct wl_window *win = term->window; + if (idx < win->tab_count) + term_tab_switch(win, idx); + return true; + } + + case BIND_ACTION_TAB_OVERVIEW: + tab_overview_toggle(term->window); + return true; + + case BIND_ACTION_REGEX_LAUNCH: + case BIND_ACTION_REGEX_COPY: + if (binding->aux->type != BINDING_AUX_REGEX) + return true; + + tll_foreach(term->conf->custom_regexes, it) { + const struct custom_regex *regex = &it->item; + + if (streq(regex->name, binding->aux->regex_name)) { + xassert(!urls_mode_is_active(term)); + + enum url_action url_action = action == BIND_ACTION_REGEX_LAUNCH + ? URL_ACTION_LAUNCH : URL_ACTION_COPY; + + if (regex->regex == NULL) { + LOG_ERR("regex:%s has no regex defined", regex->name); + return true; + } + if (url_action == URL_ACTION_LAUNCH && regex->launch.argv.args == NULL) { + LOG_ERR("regex:%s has no launch command defined", regex->name); + return true; + } + + urls_collect(term, url_action, ®ex->preg, false, &term->urls); + urls_assign_key_combos(term->conf, &term->urls); + urls_render(term, ®ex->launch); + return true; + } + } + + LOG_ERR( + "no regex section named '%s' defined in the configuration", + binding->aux->regex_name); + + return true; + + case BIND_ACTION_THEME_SWITCH_1: + case BIND_ACTION_THEME_SWITCH_DARK: + term_theme_switch_to_dark(term); + return true; + + case BIND_ACTION_THEME_SWITCH_2: + case BIND_ACTION_THEME_SWITCH_LIGHT: + term_theme_switch_to_light(term); + return true; + + case BIND_ACTION_THEME_TOGGLE: + term_theme_toggle(term); + return true; + + case BIND_ACTION_SELECT_BEGIN: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); + return true; + + case BIND_ACTION_SELECT_BEGIN_BLOCK: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_BLOCK, false); + return true; + + case BIND_ACTION_SELECT_EXTEND: + selection_extend( + seat, term, seat->mouse.col, seat->mouse.row, term->selection.kind); + return true; + + case BIND_ACTION_SELECT_EXTEND_CHAR_WISE: + if (term->selection.kind != SELECTION_BLOCK) { + selection_extend( + seat, term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE); + return true; + } + return false; + + case BIND_ACTION_SELECT_WORD: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, false); + return true; + + case BIND_ACTION_SELECT_WORD_WS: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, true); + return true; + + case BIND_ACTION_SELECT_QUOTE: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_QUOTE_WISE, false); + return true; + + case BIND_ACTION_SELECT_ROW: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_LINE_WISE, false); + return true; + + case BIND_ACTION_COUNT: + BUG("Invalid action type"); + return false; + } + + return false; +} + +static void +keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, + uint32_t format, int32_t fd, uint32_t size) +{ + LOG_DBG("keyboard_keymap: keyboard=%p (format=%u, size=%u)", + (void *)wl_keyboard, format, size); + + struct seat *seat = data; + struct wayland *wayl = seat->wayl; + + /* + * Free old keymap state + */ + + if (seat->kbd.xkb_keymap != NULL) { + xkb_keymap_unref(seat->kbd.xkb_keymap); + seat->kbd.xkb_keymap = NULL; + } + if (seat->kbd.xkb_state != NULL) { + xkb_state_unref(seat->kbd.xkb_state); + seat->kbd.xkb_state = NULL; + } + + key_binding_unload_keymap(wayl->key_binding_manager, seat); + + /* Verify keymap is in a format we understand */ + switch ((enum wl_keyboard_keymap_format)format) { + case WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP: + goto err; + + case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1: + break; + + default: + LOG_WARN("unrecognized keymap format: %u", format); + goto err; + } + + char *map_str = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); + if (map_str == MAP_FAILED) { + LOG_ERRNO("failed to mmap keyboard keymap"); + goto err; + } + + while (map_str[size - 1] == '\0') + size--; + + if (seat->kbd.xkb != NULL) { + seat->kbd.xkb_keymap = xkb_keymap_new_from_buffer( + seat->kbd.xkb, map_str, size, XKB_KEYMAP_FORMAT_TEXT_V1, + XKB_KEYMAP_COMPILE_NO_FLAGS); + + } + + munmap(map_str, size); + + if (seat->kbd.xkb_keymap != NULL) { + seat->kbd.xkb_state = xkb_state_new(seat->kbd.xkb_keymap); + + seat->kbd.mod_shift = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat->kbd.mod_alt = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat->kbd.mod_ctrl = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat->kbd.mod_super = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat->kbd.mod_caps = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat->kbd.mod_num = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat->kbd.legacy_significant = 0; + if (seat->kbd.mod_shift != XKB_MOD_INVALID) + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_shift; + if (seat->kbd.mod_alt != XKB_MOD_INVALID) + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_alt; + if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_ctrl; + if (seat->kbd.mod_super != XKB_MOD_INVALID) + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat->kbd.kitty_significant = seat->kbd.legacy_significant; + if (seat->kbd.mod_caps != XKB_MOD_INVALID) + seat->kbd.kitty_significant |= 1 << seat->kbd.mod_caps; + if (seat->kbd.mod_num != XKB_MOD_INVALID) + seat->kbd.kitty_significant |= 1 << seat->kbd.mod_num; + + + /* + * Create a mask of all "virtual" modifiers. Some compositors + * add these *in addition* to the "real" modifiers (Mod1, + * Mod2, etc). + * + * Since our modifier logic (both for internal shortcut + * processing, and e.g. the kitty keyboard protocol) makes + * very few assumptions on available modifiers, which keys map + * to which modifier etc, the presence of virtual modifiers + * causes various things to break. + * + * For example, if a foot shortcut is Mod1+b (i.e. Alt+b), it + * won't match if the compositor _also_ sets the Alt modifier + * (the corresponding shortcut in foot would be Alt+Mod1+b). + * + * See https://codeberg.org/dnkl/foot/issues/2009 + * + * Mutter (GNOME) is known to set the virtual modifiers in + * addtiion to the real modifiers. + * + * As far as I know, there's no compositor that _only_ sets + * virtual modifiers (don't think that's even legal...?) + */ + { + xkb_mod_index_t alt = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_ALT); + xkb_mod_index_t meta = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_META); + xkb_mod_index_t super = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SUPER); + xkb_mod_index_t hyper = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_HYPER); + xkb_mod_index_t num_lock = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_NUM); + xkb_mod_index_t scroll_lock = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SCROLL); + xkb_mod_index_t level_three = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL3); + xkb_mod_index_t level_five = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL5); + + xkb_mod_index_t ignore = 0; + + if (alt != XKB_MOD_INVALID) ignore |= 1 << alt; + if (meta != XKB_MOD_INVALID) ignore |= 1 << meta; + if (super != XKB_MOD_INVALID) ignore |= 1 << super; + if (hyper != XKB_MOD_INVALID) ignore |= 1 << hyper; + if (num_lock != XKB_MOD_INVALID) ignore |= 1 << num_lock; + if (scroll_lock != XKB_MOD_INVALID) ignore |= 1 << scroll_lock; + if (level_three != XKB_MOD_INVALID) ignore |= 1 << level_three; + if (level_five != XKB_MOD_INVALID) ignore |= 1 << level_five; + + seat->kbd.virtual_modifiers = ignore; + } + + seat->kbd.key_arrow_up = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "UP"); + seat->kbd.key_arrow_down = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "DOWN"); + } + + key_binding_load_keymap(wayl->key_binding_manager, seat); + +err: + close(fd); +} + +static void +keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, + struct wl_surface *surface, struct wl_array *keys) +{ + xassert(surface != NULL); + xassert(serial != 0); + + struct seat *seat = data; + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; + + LOG_DBG("%s: keyboard_enter: keyboard=%p, serial=%u, surface=%p", + seat->name, (void *)wl_keyboard, serial, (void *)surface); + + term_kbd_focus_in(term); + seat->kbd_focus = term; + seat->kbd.serial = serial; +} + +static bool +start_repeater(struct seat *seat, uint32_t key) +{ + if (seat->kbd.repeat.dont_re_repeat) + return true; + + if (seat->kbd.repeat.rate == 0) + return true; + + struct itimerspec t = { + .it_value = {.tv_sec = 0, .tv_nsec = seat->kbd.repeat.delay * 1000000}, + .it_interval = {.tv_sec = 0, .tv_nsec = 1000000000 / seat->kbd.repeat.rate}, + }; + + if (t.it_value.tv_nsec >= 1000000000) { + t.it_value.tv_sec += t.it_value.tv_nsec / 1000000000; + t.it_value.tv_nsec %= 1000000000; + } + if (t.it_interval.tv_nsec >= 1000000000) { + t.it_interval.tv_sec += t.it_interval.tv_nsec / 1000000000; + t.it_interval.tv_nsec %= 1000000000; + } + if (timerfd_settime(seat->kbd.repeat.fd, 0, &t, NULL) < 0) { + LOG_ERRNO("%s: failed to arm keyboard repeat timer", seat->name); + return false; + } + + seat->kbd.repeat.key = key; + return true; +} + +static bool +stop_repeater(struct seat *seat, uint32_t key) +{ + if (key != -1 && key != seat->kbd.repeat.key) + return true; + + if (timerfd_settime(seat->kbd.repeat.fd, 0, &(struct itimerspec){{0}}, NULL) < 0) { + LOG_ERRNO("%s: failed to disarm keyboard repeat timer", seat->name); + return false; + } + + return true; +} + +static void +keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, + struct wl_surface *surface) +{ + struct seat *seat = data; + + LOG_DBG("keyboard_leave: keyboard=%p, serial=%u, surface=%p", + (void *)wl_keyboard, serial, (void *)surface); + + xassert( + seat->kbd_focus == NULL || + surface == NULL || /* Seen on Sway 1.2 */ + ((const struct wl_window *)wl_surface_get_user_data(surface))->term == seat->kbd_focus + ); + + struct terminal *old_focused = seat->kbd_focus; + seat->kbd_focus = NULL; + + stop_repeater(seat, -1); + seat->kbd.shift = false; + seat->kbd.alt = false; + seat->kbd.ctrl = false; + seat->kbd.super = false; + + if (seat->kbd.xkb_compose_state != NULL) + xkb_compose_state_reset(seat->kbd.xkb_compose_state); + + if (seat->kbd.xkb_state != NULL && seat->kbd.xkb_keymap != NULL) { + const xkb_layout_index_t layout_count = xkb_keymap_num_layouts(seat->kbd.xkb_keymap); + + for (xkb_layout_index_t i = 0; i < layout_count; i++) + xkb_state_update_mask(seat->kbd.xkb_state, 0, 0, 0, i, i, i); + } + + if (old_focused != NULL) { + seat->pointer.hidden = false; + term_xcursor_update_for_seat(old_focused, seat); + term_kbd_focus_out(old_focused); + } else { + /* + * Sway bug - under certain conditions we get a + * keyboard_leave() (and keyboard_key()) without first having + * received a keyboard_enter() + */ + LOG_WARN( + "compositor sent keyboard_leave event without a keyboard_enter " + "event: surface=%p", (void *)surface); + } +} + +static const struct key_data * +keymap_data_for_sym(xkb_keysym_t sym, size_t *count) +{ + switch (sym) { + case XKB_KEY_Escape: *count = ALEN(key_escape); return key_escape; + case XKB_KEY_Return: *count = ALEN(key_return); return key_return; + case XKB_KEY_ISO_Left_Tab: *count = ALEN(key_iso_left_tab); return key_iso_left_tab; + case XKB_KEY_Tab: *count = ALEN(key_tab); return key_tab; + case XKB_KEY_BackSpace: *count = ALEN(key_backspace); return key_backspace; + case XKB_KEY_Up: *count = ALEN(key_up); return key_up; + case XKB_KEY_Down: *count = ALEN(key_down); return key_down; + case XKB_KEY_Right: *count = ALEN(key_right); return key_right; + case XKB_KEY_Left: *count = ALEN(key_left); return key_left; + case XKB_KEY_Home: *count = ALEN(key_home); return key_home; + case XKB_KEY_End: *count = ALEN(key_end); return key_end; + case XKB_KEY_Insert: *count = ALEN(key_insert); return key_insert; + case XKB_KEY_Delete: *count = ALEN(key_delete); return key_delete; + case XKB_KEY_Page_Up: *count = ALEN(key_pageup); return key_pageup; + case XKB_KEY_Page_Down: *count = ALEN(key_pagedown); return key_pagedown; + case XKB_KEY_F1: *count = ALEN(key_f1); return key_f1; + case XKB_KEY_F2: *count = ALEN(key_f2); return key_f2; + case XKB_KEY_F3: *count = ALEN(key_f3); return key_f3; + case XKB_KEY_F4: *count = ALEN(key_f4); return key_f4; + case XKB_KEY_F5: *count = ALEN(key_f5); return key_f5; + case XKB_KEY_F6: *count = ALEN(key_f6); return key_f6; + case XKB_KEY_F7: *count = ALEN(key_f7); return key_f7; + case XKB_KEY_F8: *count = ALEN(key_f8); return key_f8; + case XKB_KEY_F9: *count = ALEN(key_f9); return key_f9; + case XKB_KEY_F10: *count = ALEN(key_f10); return key_f10; + case XKB_KEY_F11: *count = ALEN(key_f11); return key_f11; + case XKB_KEY_F12: *count = ALEN(key_f12); return key_f12; + case XKB_KEY_F13: *count = ALEN(key_f13); return key_f13; + case XKB_KEY_F14: *count = ALEN(key_f14); return key_f14; + case XKB_KEY_F15: *count = ALEN(key_f15); return key_f15; + case XKB_KEY_F16: *count = ALEN(key_f16); return key_f16; + case XKB_KEY_F17: *count = ALEN(key_f17); return key_f17; + case XKB_KEY_F18: *count = ALEN(key_f18); return key_f18; + case XKB_KEY_F19: *count = ALEN(key_f19); return key_f19; + case XKB_KEY_F20: *count = ALEN(key_f20); return key_f20; + case XKB_KEY_F21: *count = ALEN(key_f21); return key_f21; + case XKB_KEY_F22: *count = ALEN(key_f22); return key_f22; + case XKB_KEY_F23: *count = ALEN(key_f23); return key_f23; + case XKB_KEY_F24: *count = ALEN(key_f24); return key_f24; + case XKB_KEY_F25: *count = ALEN(key_f25); return key_f25; + case XKB_KEY_F26: *count = ALEN(key_f26); return key_f26; + case XKB_KEY_F27: *count = ALEN(key_f27); return key_f27; + case XKB_KEY_F28: *count = ALEN(key_f28); return key_f28; + case XKB_KEY_F29: *count = ALEN(key_f29); return key_f29; + case XKB_KEY_F30: *count = ALEN(key_f30); return key_f30; + case XKB_KEY_F31: *count = ALEN(key_f31); return key_f31; + case XKB_KEY_F32: *count = ALEN(key_f32); return key_f32; + case XKB_KEY_F33: *count = ALEN(key_f33); return key_f33; + case XKB_KEY_F34: *count = ALEN(key_f34); return key_f34; + case XKB_KEY_F35: *count = ALEN(key_f35); return key_f35; + case XKB_KEY_KP_Up: *count = ALEN(key_kp_up); return key_kp_up; + case XKB_KEY_KP_Down: *count = ALEN(key_kp_down); return key_kp_down; + case XKB_KEY_KP_Right: *count = ALEN(key_kp_right); return key_kp_right; + case XKB_KEY_KP_Left: *count = ALEN(key_kp_left); return key_kp_left; + case XKB_KEY_KP_Begin: *count = ALEN(key_kp_begin); return key_kp_begin; + case XKB_KEY_KP_Home: *count = ALEN(key_kp_home); return key_kp_home; + case XKB_KEY_KP_End: *count = ALEN(key_kp_end); return key_kp_end; + case XKB_KEY_KP_Insert: *count = ALEN(key_kp_insert); return key_kp_insert; + case XKB_KEY_KP_Delete: *count = ALEN(key_kp_delete); return key_kp_delete; + case XKB_KEY_KP_Page_Up: *count = ALEN(key_kp_pageup); return key_kp_pageup; + case XKB_KEY_KP_Page_Down: *count = ALEN(key_kp_pagedown); return key_kp_pagedown; + case XKB_KEY_KP_Enter: *count = ALEN(key_kp_enter); return key_kp_enter; + case XKB_KEY_KP_Divide: *count = ALEN(key_kp_divide); return key_kp_divide; + case XKB_KEY_KP_Multiply: *count = ALEN(key_kp_multiply); return key_kp_multiply; + case XKB_KEY_KP_Subtract: *count = ALEN(key_kp_subtract); return key_kp_subtract; + case XKB_KEY_KP_Add: *count = ALEN(key_kp_add); return key_kp_add; + case XKB_KEY_KP_Separator: *count = ALEN(key_kp_separator); return key_kp_separator; + case XKB_KEY_KP_Decimal: *count = ALEN(key_kp_decimal); return key_kp_decimal; + case XKB_KEY_KP_0: *count = ALEN(key_kp_0); return key_kp_0; + case XKB_KEY_KP_1: *count = ALEN(key_kp_1); return key_kp_1; + case XKB_KEY_KP_2: *count = ALEN(key_kp_2); return key_kp_2; + case XKB_KEY_KP_3: *count = ALEN(key_kp_3); return key_kp_3; + case XKB_KEY_KP_4: *count = ALEN(key_kp_4); return key_kp_4; + case XKB_KEY_KP_5: *count = ALEN(key_kp_5); return key_kp_5; + case XKB_KEY_KP_6: *count = ALEN(key_kp_6); return key_kp_6; + case XKB_KEY_KP_7: *count = ALEN(key_kp_7); return key_kp_7; + case XKB_KEY_KP_8: *count = ALEN(key_kp_8); return key_kp_8; + case XKB_KEY_KP_9: *count = ALEN(key_kp_9); return key_kp_9; + } + + return NULL; +} + +static const struct key_data * +keymap_lookup(struct terminal *term, xkb_keysym_t sym, enum modifier mods) +{ + size_t count; + const struct key_data *info = keymap_data_for_sym(sym, &count); + + if (info == NULL) + return NULL; + + const enum cursor_keys cursor_keys_mode = term->cursor_keys_mode; + const enum keypad_keys keypad_keys_mode + = term->num_lock_modifier ? KEYPAD_NUMERICAL : term->keypad_keys_mode; + + LOG_DBG("keypad mode: %d", keypad_keys_mode); + + for (size_t j = 0; j < count; j++) { + enum modifier modifiers = info[j].modifiers; + + if (modifiers & MOD_MODIFY_OTHER_KEYS_STATE1) { + if (term->modify_other_keys_2) + continue; + modifiers &= ~MOD_MODIFY_OTHER_KEYS_STATE1; + } + if (modifiers & MOD_MODIFY_OTHER_KEYS_STATE2) { + if (!term->modify_other_keys_2) + continue; + modifiers &= ~MOD_MODIFY_OTHER_KEYS_STATE2; + } + + if (modifiers != MOD_ANY && modifiers != mods) + continue; + + if (info[j].cursor_keys_mode != CURSOR_KEYS_DONTCARE && + info[j].cursor_keys_mode != cursor_keys_mode) + continue; + + if (info[j].keypad_keys_mode != KEYPAD_DONTCARE && + info[j].keypad_keys_mode != keypad_keys_mode) + continue; + + return &info[j]; + } + + return NULL; +} + +UNITTEST +{ + struct terminal term = { + .num_lock_modifier = false, + .keypad_keys_mode = KEYPAD_NUMERICAL, + .cursor_keys_mode = CURSOR_KEYS_NORMAL, + }; + + const struct key_data *info = keymap_lookup(&term, XKB_KEY_ISO_Left_Tab, MOD_SHIFT | MOD_CTRL); + xassert(info != NULL); + xassert(streq(info->seq, "\033[27;6;9~")); +} + +UNITTEST +{ + struct terminal term = { + .modify_other_keys_2 = false, + }; + + const struct key_data *info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); + xassert(info != NULL); + xassert(streq(info->seq, "\033\r")); + + term.modify_other_keys_2 = true; + info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); + xassert(info != NULL); + xassert(streq(info->seq, "\033[27;3;13~")); +} + +void +get_current_modifiers(const struct seat *seat, + xkb_mod_mask_t *effective, + xkb_mod_mask_t *consumed, uint32_t key, + bool filter_locked) +{ + if (unlikely(seat->kbd.xkb_state == NULL)) { + if (effective != NULL) + *effective = 0; + if (consumed != NULL) + *consumed = 0; + } + + else { + const xkb_mod_mask_t locked = + xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + + if (effective != NULL) { + *effective = xkb_state_serialize_mods( + seat->kbd.xkb_state, XKB_STATE_MODS_EFFECTIVE); + + if (filter_locked) + *effective &= ~locked; + } + + if (consumed != NULL) { + *consumed = xkb_state_key_get_consumed_mods2( + seat->kbd.xkb_state, key, XKB_CONSUMED_MODE_XKB); + + if (filter_locked) + *consumed &= ~locked; + } + } +} + +struct kbd_ctx { + xkb_layout_index_t layout; + xkb_keycode_t key; + xkb_keysym_t sym; + + struct { + const xkb_keysym_t *syms; + size_t count; + } level0_syms; + + xkb_mod_mask_t mods; + xkb_mod_mask_t consumed; + + struct { + const uint8_t *buf; + size_t count; + } utf8; + uint32_t *utf32; + + enum xkb_compose_status compose_status; + enum wl_keyboard_key_state key_state; +}; + +static bool +legacy_kbd_protocol(struct seat *seat, struct terminal *term, + const struct kbd_ctx *ctx) +{ + if (ctx->key_state != WL_KEYBOARD_KEY_STATE_PRESSED) + return false; + if (ctx->compose_status == XKB_COMPOSE_COMPOSING) + return false; + + enum modifier keymap_mods = MOD_NONE; + keymap_mods |= seat->kbd.shift ? MOD_SHIFT : MOD_NONE; + keymap_mods |= seat->kbd.alt ? MOD_ALT : MOD_NONE; + keymap_mods |= seat->kbd.ctrl ? MOD_CTRL : MOD_NONE; + keymap_mods |= seat->kbd.super ? MOD_META : MOD_NONE; + + const xkb_keysym_t sym = ctx->sym; + const size_t count = ctx->utf8.count; + const uint8_t *const utf8 = ctx->utf8.buf; + + const struct key_data *keymap = keymap_lookup(term, sym, keymap_mods); + if (keymap != NULL) { + term_to_slave(term, keymap->seq, strlen(keymap->seq)); + return true; + } + + if (count == 0) + return false; + +#define is_control_key(x) ((x) >= 0x40 && (x) <= 0x7f) +#define IS_CTRL(x) ((x) < 0x20 || ((x) >= 0x7f && (x) <= 0x9f)) + + //LOG_DBG("term->modify_other_keys=%d, count=%zu, is_ctrl=%d (utf8=0x%02x), sym=%d", + //term->modify_other_keys_2, count, IS_CTRL(utf8[0]), utf8[0], sym); + + bool ctrl_is_in_effect = (keymap_mods & MOD_CTRL) != 0; + bool ctrl_seq = is_control_key(sym) || (count == 1 && IS_CTRL(utf8[0])); + + bool modify_other_keys2_in_effect = false; + + if (term->modify_other_keys_2) { + /* + * Try to mimic XTerm's behavior, when holding shift: + * + * - if other modifiers are pressed (e.g. Alt), emit a CSI escape + * - upper-case symbols A-Z are encoded as an CSI escape + * - other upper-case symbols (e.g 'Ö') or emitted as is + * - non-upper cased symbols are _mostly_ emitted as is (foot + * always emits as is) + * + * Examples (assuming Swedish layout): + * - Shift-a ('A') emits a CSI + * - Shift-, (';') emits ';' + * - Shift-Alt-, (Alt-;) emits a CSI + * - Shift-ö ('Ö') emits 'Ö' + */ + + /* Any modifiers, besides shift active? */ + const xkb_mod_mask_t shift_mask = 1 << seat->kbd.mod_shift; + if ((ctx->mods & ~shift_mask & seat->kbd.legacy_significant) != 0) + modify_other_keys2_in_effect = true; + + else { + const xkb_layout_index_t layout_idx = xkb_state_key_get_layout( + seat->kbd.xkb_state, ctx->key); + + /* + * Get pressed key's base symbol. + * - for 'A' (shift-a), that's 'a' + * - for ';' (shift-,), that's ',' + */ + const xkb_keysym_t *base_syms = NULL; + size_t base_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, ctx->key, layout_idx, 0, &base_syms); + + /* Check if base symbol(s) is a-z. If so, emit CSI */ + const xkb_keysym_t lower_cased_sym = xkb_keysym_to_lower(ctx->sym); + for (size_t i = 0; i < base_count; i++) { + const xkb_keysym_t s = base_syms[i]; + if (lower_cased_sym == s && s >= XKB_KEY_a && s <= XKB_KEY_z) { + modify_other_keys2_in_effect = true; + break; + } + } + } + } + + if (keymap_mods != MOD_NONE && (modify_other_keys2_in_effect || + (ctrl_is_in_effect && !ctrl_seq))) + { + static const int mod_param_map[32] = { + [MOD_SHIFT] = 2, + [MOD_ALT] = 3, + [MOD_SHIFT | MOD_ALT] = 4, + [MOD_CTRL] = 5, + [MOD_SHIFT | MOD_CTRL] = 6, + [MOD_ALT | MOD_CTRL] = 7, + [MOD_SHIFT | MOD_ALT | MOD_CTRL] = 8, + [MOD_META] = 9, + [MOD_META | MOD_SHIFT] = 10, + [MOD_META | MOD_ALT] = 11, + [MOD_META | MOD_SHIFT | MOD_ALT] = 12, + [MOD_META | MOD_CTRL] = 13, + [MOD_META | MOD_SHIFT | MOD_CTRL] = 14, + [MOD_META | MOD_ALT | MOD_CTRL] = 15, + [MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL] = 16, + }; + + xassert(keymap_mods < ALEN(mod_param_map)); + int modify_param = mod_param_map[keymap_mods]; + xassert(modify_param != 0); + + char reply[32]; + size_t n = xsnprintf(reply, sizeof(reply), "\x1b[27;%d;%d~", modify_param, sym); + term_to_slave(term, reply, n); + } + + else if (keymap_mods & MOD_ALT) { + /* + * When the alt modifier is pressed, we do one out of three things: + * + * 1. we prefix the output bytes with ESC + * 2. we set the 8:th bit in the output byte + * 3. we ignore the alt modifier + * + * #1 is configured with \E[?1036, and is on by default + * + * If #1 has been disabled, we use #2, *if* it's a single byte + * we're emitting. Since this is a UTF-8 terminal, we then + * UTF8-encode the 8-bit character. #2 is configured with + * \E[?1034, and is on by default. + * + * Lastly, if both #1 and #2 have been disabled, the alt + * modifier is ignored. + */ + if (term->meta.esc_prefix) { + term_to_slave(term, "\x1b", 1); + term_to_slave(term, utf8, count); + } + + else if (term->meta.eight_bit && count == 1) { + const char32_t wc = 0x80 | utf8[0]; + + char utf8_meta[MB_CUR_MAX]; + size_t chars = c32rtomb(utf8_meta, wc, &(mbstate_t){0}); + + if (chars != (size_t)-1) + term_to_slave(term, utf8_meta, chars); + else + term_to_slave(term, utf8, count); + } + + else { + /* Alt ignored */ + term_to_slave(term, utf8, count); + } + } else + term_to_slave(term, utf8, count); + + return true; +} + +UNITTEST +{ + /* Verify the kitty keymap is sorted */ + xkb_keysym_t last = 0; + for (size_t i = 0; i < ALEN(kitty_keymap); i++) { + const struct kitty_key_data *e = &kitty_keymap[i]; + xassert(e->sym > last); + last = e->sym; + } +} + +static int +kitty_search(const void *_key, const void *_e) +{ + const xkb_keysym_t *key = _key; + const struct kitty_key_data *e = _e; + return *key - e->sym; +} + +static bool +kitty_kbd_protocol(struct seat *seat, struct terminal *term, + const struct kbd_ctx *ctx) +{ + const bool repeating = seat->kbd.repeat.dont_re_repeat; + const bool pressed = ctx->key_state == WL_KEYBOARD_KEY_STATE_PRESSED && !repeating; + const bool released = ctx->key_state == WL_KEYBOARD_KEY_STATE_RELEASED; + const bool composing = ctx->compose_status == XKB_COMPOSE_COMPOSING; + const bool composed = ctx->compose_status == XKB_COMPOSE_COMPOSED; + + const enum kitty_kbd_flags flags = + term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx]; + + const bool disambiguate = flags & KITTY_KBD_DISAMBIGUATE; + const bool report_events = flags & KITTY_KBD_REPORT_EVENT; + const bool report_alternate = flags & KITTY_KBD_REPORT_ALTERNATE; + const bool report_all_as_escapes = flags & KITTY_KBD_REPORT_ALL; + + if (!report_events && released) + return false; + + /* TODO: should we even bother with this, or just say it's not supported? */ + if (!disambiguate && !report_all_as_escapes && pressed) + return legacy_kbd_protocol(seat, term, ctx); + + const xkb_keysym_t sym = ctx->sym; + const uint32_t *utf32 = ctx->utf32; + const uint8_t *const utf8 = ctx->utf8.buf; + const size_t count = ctx->utf8.count; + + /* Lookup sym in the pre-defined keysym table */ + const struct kitty_key_data *info = bsearch( + &sym, kitty_keymap, ALEN(kitty_keymap), sizeof(kitty_keymap[0]), + &kitty_search); + xassert(info == NULL || info->sym == sym); + + xkb_mod_mask_t mods = 0; + xkb_mod_mask_t locked = 0; + xkb_mod_mask_t consumed = ctx->consumed; + + if (info != NULL && info->is_modifier) { + /* + * Special-case modifier keys. + * + * Normally, the "current" XKB state reflects the state + * *before* the current key event. In other words, the + * modifiers for key events that affect the modifier state + * (e.g. one of the control keys, or shift keys etc) does + * *not* include the key itself. + * + * Put another way, if you press "control", the modifier set + * is empty in the key press event, but contains "ctrl" in the + * release event. + * + * The kitty protocol mandates the modifier list contain the + * key itself, in *both* the press and release event. + * + * We handle this by updating the XKB state to *include* the + * current key, retrieve the set of modifiers (including the + * set of consumed modifiers), and then revert the XKB update. + */ + xkb_state_update_key( + seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_DOWN : XKB_KEY_UP); + + get_current_modifiers(seat, &mods, NULL, 0, false); + + locked = xkb_state_serialize_mods( + seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + consumed = xkb_state_key_get_consumed_mods2( + seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_XKB); + +#if 0 + /* + * TODO: according to the XKB docs, state updates should + * always be in pairs: each press should be followed by a + * release. However, doing this just breaks the xkb state. + * + * *Not* pairing the above press/release with a corresponding + * release/press appears to do exactly what we want. + */ + xkb_state_update_key( + seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_UP : XKB_KEY_DOWN); +#endif + } else { + /* Same as ctx->mods, but *without* filtering locked modifiers */ + get_current_modifiers(seat, &mods, NULL, 0, false); + locked = xkb_state_serialize_mods( + seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + } + + mods &= seat->kbd.kitty_significant; + consumed &= seat->kbd.kitty_significant; + + /* + * A note on locked modifiers; they *are* a part of the protocol, + * and *should* be included in the modifier set reported in the + * key event. + * + * However, *only* if the key would result in a CSIu *without* the + * locked modifier being enabled + * + * Translated: if *another* modifier is active, or if + * report-all-keys-as-escapes is enabled, then we include the + * locked modifier in the key event. + * + * But, if the key event would result in plain text output without + * the locked modifier, then we "ignore" the locked modifier and + * emit plain text anyway. + */ + + bool is_text = count > 0 && utf32 != NULL && (mods & ~locked & ~consumed) == 0; + for (size_t i = 0; utf32[i] != U'\0'; i++) { + if (!isc32print(utf32[i])) { + is_text = false; + break; + } + } + + const bool report_associated_text = + (flags & KITTY_KBD_REPORT_ASSOCIATED) && is_text && !released; + + if (composing) { + /* We never emit anything while composing, *except* modifiers + * (and only in report-all-keys-as-escape-codes mode) */ + if (info != NULL && info->is_modifier) + goto emit_escapes; + + return false; + } + + if (report_all_as_escapes) + goto emit_escapes; + + if ((mods & ~locked & ~consumed) == 0) { + switch (sym) { + case XKB_KEY_Return: + if (!released) + term_to_slave(term, "\r", 1); + return true; + + case XKB_KEY_BackSpace: + if (!released) + term_to_slave(term, "\x7f", 1); + return true; + + case XKB_KEY_Tab: + if (!released) + term_to_slave(term, "\t", 1); + return true; + } + } + + /* Plain-text without modifiers, or commposed text, is emitted as-is */ + if (is_text && !released) { + term_to_slave(term, utf8, count); + return true; + } + +emit_escapes: + ; + unsigned int encoded_mods = 0; + if (seat->kbd.mod_shift != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_shift) ? (1 << 0) : 0; + if (seat->kbd.mod_alt != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_alt) ? (1 << 1) : 0; + if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_ctrl) ? (1 << 2) : 0; + if (seat->kbd.mod_super != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_super) ? (1 << 3) : 0; + if (seat->kbd.mod_caps != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_caps) ? (1 << 6) : 0; + if (seat->kbd.mod_num != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_num) ? (1 << 7) : 0; + encoded_mods++; + + /* + * Figure out the main, alternate and base key codes. + * + * The main key is the unshifted version of the generated symbol, + * the alternate key is the shifted version, and base is the + * (unshifted) key assuming the default layout. + * + * For example, the user presses shift+a, then: + * - unshifted = 'a' + * - shifted = 'A' + * - base = 'a' + * + * Base will in many cases be the same as the unshifted key, but + * may differ if the active keyboard layout is non-ASCII (examples + * would be russian, or alternative layouts like neo etc). + * + * The shifted key is what we get from XKB, i.e. the resulting key + * from all active modifiers, plus the pressed key. + */ + int unshifted = -1, shifted = -1, base = -1; + char final; + + if (info != NULL) { + /* Use code from lookup table (cursor keys, enter, tab etc)*/ + if (!info->is_modifier || report_all_as_escapes) { + shifted = info->key; + final = info->final; + } + } else { + /* Use keysym (typically its Unicode codepoint value) */ + + if (composed) + shifted = utf32[0]; /* TODO: what if there are multiple codepoints? */ + else + shifted = xkb_keysym_to_utf32(sym); + + final = 'u'; + } + + if (shifted <= 0) + return false; + + /* Base layout key. I.e the symbol the pressed key produces in + * the base/default layout (layout idx 0) */ + const xkb_keysym_t *base_syms; + int base_sym_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, ctx->key, 0, 0, &base_syms); + + if (base_sym_count > 0) + base = xkb_keysym_to_utf32(base_syms[0]); + + /* + * If the keysym is shifted, use its unshifted codepoint + * instead. In other words, ctrl+a and ctrl+shift+a should both + * use the same value for 'key' (97 - i.a. 'a'). + * + * However, don't do this if a non-significant modifier was used + * to generate the symbol. This is needed since we cannot encode + * non-significant modifiers, and thus the "extra" modifier(s) + * would get lost. + * + * Example: + * + * the Swedish layout has '2', QUOTATION MARK ("double quote"), + * '@', and '²' on the same key. '2' is the base symbol. + * + * Shift+2 results in QUOTATION MARK + * AltGr+2 results in '@' + * AltGr+Shift+2 results in '²' + * + * The kitty kbd protocol can't encode AltGr. So, if we always + * used the base symbol ('2'), Alt+Shift+2 would result in the + * same escape sequence as AltGr+Alt+Shift+2. + * + * (yes, this matches what kitty does, as of 0.23.1) + */ + const bool use_level0_sym = + (ctx->mods & ~seat->kbd.kitty_significant) == 0 && ctx->level0_syms.count > 0; + + unshifted = use_level0_sym ? xkb_keysym_to_utf32(ctx->level0_syms.syms[0]) : 0; + + xassert(encoded_mods >= 1); + + char event[4]; + if (report_events /*&& !pressed*/) { + /* Note: this deviates slightly from Kitty, which omits the + * ":1" subparameter for key press events */ + event[0] = ':'; + event[1] = '0' + (pressed ? 1 : repeating ? 2 : 3); + event[2] = '\0'; + } else + event[0] = '\0'; + + char buf[128], *p = buf; + size_t left = sizeof(buf); + size_t bytes; + + const int key = unshifted > 0 && isc32print(unshifted) && !composed ? unshifted : shifted; + const int alternate = shifted; + + if (final == 'u' || final == '~') { + bytes = snprintf(p, left, "\x1b[%u", key); + p += bytes; left -= bytes; + + if (report_alternate) { + bool emit_alternate = alternate > 0 && alternate != key; + bool emit_base = base > 0 && base != key && base != alternate && isc32print(base); + + if (emit_alternate) { + bytes = snprintf(p, left, ":%u", alternate); + p += bytes; left -= bytes; + } + + if (emit_base) { + bytes = snprintf( + p, left, "%s:%u", !emit_alternate ? ":" : "", base); + p += bytes; left -= bytes; + } + } + + bool emit_mods = encoded_mods > 1 || event[0] != '\0'; + + if (emit_mods) { + bytes = snprintf(p, left, ";%u%s", encoded_mods, event); + p += bytes; left -= bytes; + } + + if (report_associated_text) { + bytes = snprintf(p, left, "%s;%u", !emit_mods ? ";" : "", utf32[0]); + p += bytes; left -= bytes; + + /* Additional text codepoints */ + if (utf32[0] != U'\0') { + for (size_t i = 1; utf32[i] != U'\0'; i++) { + bytes = snprintf(p, left, ":%u", utf32[i]); + p += bytes; left -= bytes; + } + } + } + + bytes = snprintf(p, left, "%c", final); + p += bytes; left -= bytes; + } else { + if (encoded_mods > 1 || event[0] != '\0') { + bytes = snprintf(p, left, "\x1b[1;%u%s%c", encoded_mods, event, final); + p += bytes; left -= bytes; + } else { + bytes = snprintf(p, left, "\x1b[%c", final); + p += bytes; left -= bytes; + } + } + + return term_to_slave(term, buf, sizeof(buf) - left); +} + +/* Copied from libxkbcommon (internal function) */ +static bool +keysym_is_modifier(xkb_keysym_t keysym) +{ + return + (keysym >= XKB_KEY_Shift_L && keysym <= XKB_KEY_Hyper_R) || + /* libX11 only goes up to XKB_KEY_ISO_Level5_Lock. */ + (keysym >= XKB_KEY_ISO_Lock && keysym <= XKB_KEY_ISO_Last_Group_Lock) || + keysym == XKB_KEY_Mode_switch || + keysym == XKB_KEY_Num_Lock; +} + +#if defined(_DEBUG) +static void +modifier_string(xkb_mod_mask_t mods, size_t sz, char mod_str[static sz], const struct seat *seat) +{ + if (sz == 0) + return; + + mod_str[0] = '\0'; + + for (size_t i = 0; i < sizeof(xkb_mod_mask_t) * 8; i++) { + if (!(mods & (1u << i))) + continue; + + strcat(mod_str, xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i)); + strcat(mod_str, "+"); + } + + if (mod_str[0] != '\0') { + /* Strip the last '+' */ + mod_str[strlen(mod_str) - 1] = '\0'; + } + + if (mod_str[0] == '\0') { + strcpy(mod_str, ""); + } +} +#endif + +static void +key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, + uint32_t key, uint32_t state) +{ + xassert(serial != 0); + + seat->kbd.serial = serial; + if (seat->kbd.xkb == NULL || + seat->kbd.xkb_keymap == NULL || + seat->kbd.xkb_state == NULL) + { + return; + } + + const bool pressed = state == WL_KEYBOARD_KEY_STATE_PRESSED; + //const bool repeated = pressed && seat->kbd.repeat.dont_re_repeat; + const bool released = state == WL_KEYBOARD_KEY_STATE_RELEASED; + + if (released) + stop_repeater(seat, key); + + if (pressed) + seat->kbd.last_shortcut_sym = XKB_KEYSYM_MAX + 1; + + bool should_repeat = + pressed && xkb_keymap_key_repeats(seat->kbd.xkb_keymap, key); + + xkb_keysym_t sym = xkb_state_key_get_one_sym(seat->kbd.xkb_state, key); + + if (pressed && term->conf->mouse.hide_when_typing && !keysym_is_modifier(sym)) { + seat->pointer.hidden = true; + term_xcursor_update_for_seat(term, seat); + } + + enum xkb_compose_status compose_status = XKB_COMPOSE_NOTHING; + + if (seat->kbd.xkb_compose_state != NULL) { + if (pressed) + xkb_compose_state_feed(seat->kbd.xkb_compose_state, sym); + compose_status = xkb_compose_state_get_status( + seat->kbd.xkb_compose_state); + } + + const bool composed = compose_status == XKB_COMPOSE_COMPOSED; + + xkb_mod_mask_t mods, consumed; + get_current_modifiers(seat, &mods, &consumed, key, true); + + xkb_layout_index_t layout_idx = + xkb_state_key_get_layout(seat->kbd.xkb_state, key); + + const xkb_keysym_t *raw_syms = NULL; + size_t raw_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, key, layout_idx, 0, &raw_syms); + + const struct key_binding_set *bindings = key_binding_for( + seat->wayl->key_binding_manager, term->conf, seat); + xassert(bindings != NULL); + + if (term->unicode_mode.active) { + if (pressed) + unicode_mode_input(seat, term, sym); + return; + } + + else if (term->is_searching) { + if (pressed) { + if (should_repeat) + start_repeater(seat, key); + + search_input( + seat, term, bindings, key, sym, mods, consumed, + raw_syms, raw_count, serial); + } + return; + } + + else if (urls_mode_is_active(term)) { + if (pressed) { + if (should_repeat) + start_repeater(seat, key); + + urls_input( + seat, term, bindings, key, sym, mods, consumed, + raw_syms, raw_count, serial); + } + return; + } + + /* Tab overview: intercept navigation + activation while open. The + * configured tab-overview binding still fires (handled below), so the + * user can toggle it back off with the same key. */ + if (term->window != NULL && + term->window->tab_overview_state.target_open && pressed) { + struct wl_window *win = term->window; + __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; + const size_t n = win->tab_count; + + bool handled = false; + switch (sym) { + case XKB_KEY_Escape: + tab_overview_toggle(win); + handled = true; + break; + case XKB_KEY_Return: + case XKB_KEY_KP_Enter: + if (st->sel_idx < n && st->sel_idx != win->active_tab) + term_tab_switch(win, st->sel_idx); + tab_overview_close_instant(win); + handled = true; + break; + case XKB_KEY_Left: + if (n > 0) + st->sel_idx = (st->sel_idx == 0) ? n - 1 : st->sel_idx - 1; + render_refresh(term); + handled = true; + break; + case XKB_KEY_Right: + if (n > 0) + st->sel_idx = (st->sel_idx + 1) % n; + render_refresh(term); + handled = true; + break; + case XKB_KEY_Up: + case XKB_KEY_Down: { + /* Step by one row in the cached card layout */ + if (st->card_count >= 2) { + int cur_y = st->cards[st->sel_idx].y; + int cur_x = st->cards[st->sel_idx].x; + int best = -1; + int best_d = INT_MAX; + for (size_t i = 0; i < st->card_count; i++) { + if (i == st->sel_idx) continue; + int dy = st->cards[i].y - cur_y; + if (sym == XKB_KEY_Up && dy >= 0) continue; + if (sym == XKB_KEY_Down && dy <= 0) continue; + int dx = st->cards[i].x - cur_x; + int d = dx * dx + dy * dy; + if (d < best_d) { best_d = d; best = (int)i; } + } + if (best >= 0) st->sel_idx = (size_t)best; + } + render_refresh(term); + handled = true; + break; + } + default: + if (sym >= XKB_KEY_1 && sym <= XKB_KEY_9) { + size_t idx = (size_t)(sym - XKB_KEY_1); + if (idx < n) { + if (idx != win->active_tab) + term_tab_switch(win, idx); + tab_overview_close_instant(win); + } + handled = true; + } + break; + } + + if (handled) { + seat->kbd.last_shortcut_sym = sym; + /* Don't try repeat; navigation is one-shot */ + return; + } + + /* Fall through to binding match so the user's tab-overview key + * can still toggle off, but swallow everything else. */ + } + +#if defined(_DEBUG) && defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + char sym_name[100]; + xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); + + char active_mods_str[256] = {0}; + char consumed_mods_str[256] = {0}; + char locked_mods_str[256] = {0}; + + const xkb_mod_mask_t locked = + xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + + modifier_string(mods, sizeof(active_mods_str), active_mods_str, seat); + modifier_string(consumed, sizeof(consumed_mods_str), consumed_mods_str, seat); + modifier_string(locked, sizeof(locked_mods_str), locked_mods_str, seat); + + LOG_DBG("%s: %s (%u/0x%x), seat=%s, term=%p, serial=%u, " + "mods=%s (0x%08x), consumed=%s (0x%08x), locked=%s (0x%08x), " + "repeats=%d", + pressed ? "pressed" : "released", sym_name, sym, sym, + seat->name, (void *)term, serial, + active_mods_str, mods, consumed_mods_str, consumed, + locked_mods_str, locked, should_repeat); +#endif + + /* + * User configurable bindings + */ + if (pressed) { + /* Match untranslated symbols */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods || bind->mods == 0) + continue; + + for (size_t i = 0; i < raw_count; i++) { + if (bind->k.sym == raw_syms[i] && + execute_binding(seat, term, bind, serial, 1)) + { + seat->kbd.last_shortcut_sym = sym; + goto maybe_repeat; + } + } + } + + /* Match translated symbol */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; + + if (bind->k.sym == sym && + bind->mods == (mods & ~consumed) && + execute_binding(seat, term, bind, serial, 1)) + { + seat->kbd.last_shortcut_sym = sym; + goto maybe_repeat; + } + } + + /* Match raw key code */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods || bind->mods == 0) + continue; + + tll_foreach(bind->k.key_codes, code) { + if (code->item == key && + execute_binding(seat, term, bind, serial, 1)) + { + seat->kbd.last_shortcut_sym = sym; + goto maybe_repeat; + } + } + } + } + + /* Overview is modal: don't pass un-bound keys through to the PTY */ + if (term->window != NULL && + (term->window->tab_overview_state.target_open || + term->window->tab_overview_state.visible)) + { + return; + } + + if (released && seat->kbd.last_shortcut_sym == sym) { + /* + * Don't process a release event, if it corresponds to a + * triggered shortcut. + * + * 1. If we consumed a key (press) event, we shouldn't emit an + * escape for its release event. + * 2. Ignoring the incorrectness of doing so; this also caused + * us to reset the viewport. + * + * Background: if the kitty keyboard protocol was enabled, + * then the viewport was instantly reset to the bottom, after + * scrolling up. + */ + //seat->kbd.last_shortcut_sym = XKB_KEYSYM_MAX + 1; + goto maybe_repeat; + } + + /* + * Keys generating escape sequences + */ + + + /* + * Compose, and maybe emit "normal" character + */ + + xassert(seat->kbd.xkb_compose_state != NULL || !composed); + + if (compose_status == XKB_COMPOSE_CANCELLED) + goto maybe_repeat; + + int count = composed + ? xkb_compose_state_get_utf8(seat->kbd.xkb_compose_state, NULL, 0) + : xkb_state_key_get_utf8(seat->kbd.xkb_state, key, NULL, 0); + + /* Buffer for translated key. Use a static buffer in most cases, + * and use a malloc:ed buffer when necessary */ + uint8_t buf[32]; + uint8_t *utf8 = count < sizeof(buf) ? buf : xmalloc(count + 1); + uint32_t *utf32 = NULL; + + if (composed) { + xkb_compose_state_get_utf8( + seat->kbd.xkb_compose_state, (char *)utf8, count + 1); + + if (count > 0) + utf32 = ambstoc32((const char *)utf8); + } else { + xkb_state_key_get_utf8( + seat->kbd.xkb_state, key, (char *)utf8, count + 1); + + utf32 = xcalloc(2, sizeof(utf32[0])); + utf32[0] = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); + } + + struct kbd_ctx ctx = { + .layout = layout_idx, + .key = key, + .sym = sym, + .level0_syms = { + .syms = raw_syms, + .count = raw_count, + }, + .mods = mods, + .consumed = consumed, + .utf8 = { + .buf = utf8, + .count = count, + }, + .utf32 = utf32, + .compose_status = compose_status, + .key_state = state, + }; + + bool handled = term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx] != 0 + ? kitty_kbd_protocol(seat, term, &ctx) + : legacy_kbd_protocol(seat, term, &ctx); + + if (composed && released) + xkb_compose_state_reset(seat->kbd.xkb_compose_state); + + if (utf8 != buf) + free(utf8); + + if (handled && !keysym_is_modifier(sym)) { + term_reset_view(term); + selection_cancel(term); + } + + free(utf32); + +maybe_repeat: + clock_gettime( + term->wl->presentation_clock_id, &term->render.input_time); + + if (should_repeat) + start_repeater(seat, key); +} + +static void +keyboard_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, + uint32_t time, uint32_t key, uint32_t state) +{ + struct seat *seat = data; + key_press_release(seat, seat->kbd_focus, serial, key + 8, state); +} + +static void +keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, + uint32_t mods_depressed, uint32_t mods_latched, + uint32_t mods_locked, uint32_t group) +{ + struct seat *seat = data; + + mods_depressed &= ~seat->kbd.virtual_modifiers; + mods_latched &= ~seat->kbd.virtual_modifiers; + mods_locked &= ~seat->kbd.virtual_modifiers; + +#if defined(_DEBUG) + char depressed[256]; + char latched[256]; + char locked[256]; + + modifier_string(mods_depressed, sizeof(depressed), depressed, seat); + modifier_string(mods_latched, sizeof(latched), latched, seat); + modifier_string(mods_locked, sizeof(locked), locked, seat); + + LOG_DBG( + "modifiers: depressed=%s (0x%x), latched=%s (0x%x), locked=%s (0x%x), " + "group=%u", + depressed, mods_depressed, latched, mods_latched, locked, mods_locked, + group); +#endif + + if (seat->kbd.xkb_state != NULL) { + xkb_state_update_mask( + seat->kbd.xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group); + + /* Update state of modifiers we're interested in for e.g mouse events */ + seat->kbd.shift = seat->kbd.mod_shift != XKB_MOD_INVALID + ? xkb_state_mod_index_is_active( + seat->kbd.xkb_state, seat->kbd.mod_shift, XKB_STATE_MODS_EFFECTIVE) + : false; + seat->kbd.alt = seat->kbd.mod_alt != XKB_MOD_INVALID + ? xkb_state_mod_index_is_active( + seat->kbd.xkb_state, seat->kbd.mod_alt, XKB_STATE_MODS_EFFECTIVE) + : false; + seat->kbd.ctrl = seat->kbd.mod_ctrl != XKB_MOD_INVALID + ? xkb_state_mod_index_is_active( + seat->kbd.xkb_state, seat->kbd.mod_ctrl, XKB_STATE_MODS_EFFECTIVE) + : false; + seat->kbd.super = seat->kbd.mod_super != XKB_MOD_INVALID + ? xkb_state_mod_index_is_active( + seat->kbd.xkb_state, seat->kbd.mod_super, XKB_STATE_MODS_EFFECTIVE) + : false; + } + + if (seat->kbd_focus && seat->kbd_focus->active_surface == TERM_SURF_GRID) + term_xcursor_update_for_seat(seat->kbd_focus, seat); +} + +UNITTEST +{ + int chan[2]; + xassert(pipe2(chan, O_CLOEXEC) == 0); + + xassert(chan[0] >= 0); + xassert(chan[1] >= 0); + + struct config conf = {0}; + struct grid grid = {0}; + + struct terminal term = { + .conf = &conf, + .grid = &grid, + .ptmx = chan[1], + .selection = { + .coords = { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .auto_scroll = { + .fd = -1, + }, + }, + }; + + struct key_binding_manager *key_binding_manager = key_binding_manager_new(); + + struct wayland wayl = { + .key_binding_manager = key_binding_manager, + .terms = tll_init(), + }; + + struct seat seat = { + .wayl = &wayl, + .name = "unittest", + }; + + tll_push_back(wayl.terms, &term); + term.wl = &wayl; + + seat.kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + xassert(seat.kbd.xkb != NULL); + + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; + + /* Swedish keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "se"}, XKB_KEYMAP_COMPILE_NO_FLAGS); + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_A + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 97 = 'a', alternate: 65 = 'A', base: N/A, mods: 6 = ctrl+shift */ + const char expected_ctrl_shift_a[] = "\033[97:65;6u"; + xassert(count == strlen(expected_ctrl_shift_a)); + xassert(streq(escape, expected_ctrl_shift_a)); + + key_press_release(&seat, &term, 1337, KEY_A + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key;. 50 = '2', alternate: 34 = '"', base: N/A, 4 = alt+shift */ + const char expected_alt_shift_2[] = "\033[50:34;4u"; + xassert(count == strlen(expected_alt_shift_2)); + xassert(streq(escape, expected_alt_shift_2)); + + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_index_t alt_gr = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, "Mod5"); + xassert(alt_gr != XKB_MOD_INVALID); + + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt | 1u << alt_gr; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 178 = '²', alternate: N/A, base: 50 = '2', 4 = alt+shift (AltGr not part of the protocol) */ + const char expected_altgr_alt_shift_2[] = "\033[178::50;4u"; + xassert(count == strlen(expected_altgr_alt_shift_2)); + xassert(streq(escape, expected_altgr_alt_shift_2)); + + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 127 = , alternate: N/A, base: N/A, 3 = alt */ + const char expected_alt_backspace[] = "\033[127;3u"; + xassert(count == strlen(expected_alt_backspace)); + xassert(streq(escape, expected_alt_backspace)); + + key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_ENTER + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 13 = , alternate: N/A, base: N/A, 5 = ctrl */ + const char expected_ctrl_enter[] = "\033[13;5u"; + xassert(count == strlen(expected_ctrl_enter)); + xassert(streq(escape, expected_ctrl_enter)); + + key_press_release(&seat, &term, 1337, KEY_ENTER + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_TAB + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 9 = , alternate: N/A, base: N/A, 5 = ctrl */ + const char expected_ctrl_tab[] = "\033[9;5u"; + xassert(count == strlen(expected_ctrl_tab)); + xassert(streq(escape, expected_ctrl_tab)); + + key_press_release(&seat, &term, 1337, KEY_TAB + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl | 1u << seat.kbd.mod_shift; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_LEFT + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + const char expected_ctrl_shift_left[] = "\033[1;6D"; + xassert(count == strlen(expected_ctrl_shift_left)); + xassert(streq(escape, expected_ctrl_shift_left)); + + key_press_release(&seat, &term, 1337, KEY_LEFT + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + + /* de(neo) keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us,de(neo)"}, + XKB_KEYMAP_COMPILE_NO_FLAGS); + + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + /* + * In the de(neo) layout, the Y key generates 'k'. This + * means we should get a key+alternate that indicates 'k', + * but a base key that is 'y'. + */ + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); + key_press_release(&seat, &term, 1337, KEY_Y + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 107 = 'k', alternate: 75 = 'K', base: 121 = 'y', mods: 4 = alt+shift */ + const char expected_alt_shift_y[] = "\033[107:75:121;4u"; + xassert(count == strlen(expected_alt_shift_y)); + xassert(streq(escape, expected_alt_shift_y)); + + key_press_release(&seat, &term, 1337, KEY_Y + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + + /* us(intl) keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us", .variant = "intl"}, + XKB_KEYMAP_COMPILE_NO_FLAGS); + + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.xkb_compose_table = xkb_compose_table_new_from_locale( + seat.kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); + if (seat.kbd.xkb_compose_table == NULL) + goto no_keymap; + + seat.kbd.xkb_compose_state = xkb_compose_state_new( + seat.kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); + if (seat.kbd.xkb_compose_state == NULL) { + xkb_compose_table_unref(seat.kbd.xkb_compose_table); + goto no_keymap; + } + + seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + /* + * Test the compose sequence "shift+', shift+space" + * + * Should result in a double quote, but a regression + * caused it to instead emit a space. See #1987 + * + * Note: "shift+', space" also results in a double quote, + * but never regressed to a space. + */ + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE; + xkb_compose_state_reset(seat.kbd.xkb_compose_state); + + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); + + key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + + key_press_release(&seat, &term, 1337, KEY_SPACE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 34 = '"', alternate: N/A, base: N/A, mods: 2 = shift */ + const char expected_shift_apostrophe[] = "\033[34;2u"; + xassert(count == strlen(expected_shift_apostrophe)); + xassert(streq(escape, expected_shift_apostrophe)); + + key_press_release(&seat, &term, 1337, KEY_SPACE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; + } + + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_compose_state_unref(seat.kbd.xkb_compose_state); + xkb_compose_table_unref(seat.kbd.xkb_compose_table); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + +no_keymap: + xkb_context_unref(seat.kbd.xkb); + key_binding_manager_destroy(key_binding_manager); + + tll_free(wayl.terms); + close(chan[0]); + close(chan[1]); +} + +static void +keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard, + int32_t rate, int32_t delay) +{ + struct seat *seat = data; + LOG_DBG("keyboard repeat: rate=%d, delay=%d", rate, delay); + seat->kbd.repeat.rate = rate; + seat->kbd.repeat.delay = delay; +} + +const struct wl_keyboard_listener keyboard_listener = { + .keymap = &keyboard_keymap, + .enter = &keyboard_enter, + .leave = &keyboard_leave, + .key = &keyboard_key, + .modifiers = &keyboard_modifiers, + .repeat_info = &keyboard_repeat_info, +}; + +void +input_repeat(struct seat *seat, uint32_t key) +{ + /* Should be cleared as soon as we loose focus */ + xassert(seat->kbd_focus != NULL); + struct terminal *term = seat->kbd_focus; + + key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_DOWN); +} + +static bool +is_top_left(const struct terminal *term, int x, int y) +{ + int csd_border_size = term->conf->csd.border_width; + return ( + (!term->window->is_constrained_top && !term->window->is_constrained_left) && + ((term->active_surface == TERM_SURF_BORDER_LEFT && y < 10 * term->scale) || + (term->active_surface == TERM_SURF_BORDER_TOP && x < (10 + csd_border_size) * term->scale))); +} + +static bool +is_top_right(const struct terminal *term, int x, int y) +{ + int csd_border_size = term->conf->csd.border_width; + return ( + (!term->window->is_constrained_top && !term->window->is_constrained_right) && + ((term->active_surface == TERM_SURF_BORDER_RIGHT && y < 10 * term->scale) || + (term->active_surface == TERM_SURF_BORDER_TOP && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale))); +} + +static bool +is_bottom_left(const struct terminal *term, int x, int y) +{ + int csd_title_size = term->conf->csd.title_height; + int csd_border_size = term->conf->csd.border_width; + return ( + (!term->window->is_constrained_bottom && !term->window->is_constrained_left) && + ((term->active_surface == TERM_SURF_BORDER_LEFT && y > csd_title_size * term->scale + term->height) || + (term->active_surface == TERM_SURF_BORDER_BOTTOM && x < (10 + csd_border_size) * term->scale))); +} + +static bool +is_bottom_right(const struct terminal *term, int x, int y) +{ + int csd_title_size = term->conf->csd.title_height; + int csd_border_size = term->conf->csd.border_width; + return ( + (!term->window->is_constrained_bottom && !term->window->is_constrained_right) && + ((term->active_surface == TERM_SURF_BORDER_RIGHT && y > csd_title_size * term->scale + term->height) || + (term->active_surface == TERM_SURF_BORDER_BOTTOM && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale))); +} + +enum cursor_shape +xcursor_for_csd_border(struct terminal *term, int x, int y) +{ + if (is_top_left(term, x, y)) return CURSOR_SHAPE_TOP_LEFT_CORNER; + else if (is_top_right(term, x, y)) return CURSOR_SHAPE_TOP_RIGHT_CORNER; + else if (is_bottom_left(term, x, y)) return CURSOR_SHAPE_BOTTOM_LEFT_CORNER; + else if (is_bottom_right(term, x, y)) return CURSOR_SHAPE_BOTTOM_RIGHT_CORNER; + + else if (term->active_surface == TERM_SURF_BORDER_LEFT) + return !term->window->is_constrained_left + ? CURSOR_SHAPE_LEFT_SIDE : CURSOR_SHAPE_LEFT_PTR; + + else if (term->active_surface == TERM_SURF_BORDER_RIGHT) + return !term->window->is_constrained_right + ? CURSOR_SHAPE_RIGHT_SIDE : CURSOR_SHAPE_LEFT_PTR; + + else if (term->active_surface == TERM_SURF_BORDER_TOP) + return !term->window->is_constrained_top + ? CURSOR_SHAPE_TOP_SIDE : CURSOR_SHAPE_LEFT_PTR; + + else if (term->active_surface == TERM_SURF_BORDER_BOTTOM) + return !term->window->is_constrained_bottom + ? CURSOR_SHAPE_BOTTOM_SIDE : CURSOR_SHAPE_LEFT_PTR; + + else { + BUG("Unreachable"); + return CURSOR_SHAPE_NONE; + } +} + +static void +mouse_button_state_reset(struct seat *seat) +{ + tll_free(seat->mouse.buttons); + seat->mouse.count = 0; + seat->mouse.last_released_button = 0; + memset(&seat->mouse.last_time, 0, sizeof(seat->mouse.last_time)); +} + +static void +mouse_coord_pixel_to_cell(struct seat *seat, const struct terminal *term, + int x, int y) +{ + /* + * Translate x,y pixel coordinate to a cell coordinate, or -1 + * if the cursor is outside the grid. I.e. if it is inside the + * margins. + */ + if (x < term->margins.left) + seat->mouse.col = 0; + else if (x >= term->width - term->margins.right) + seat->mouse.col = term->cols - 1; + else + seat->mouse.col = (x - term->margins.left) / term->cell_width; + + if (y < term->margins.top) + seat->mouse.row = 0; + else if (y >= term->height - term->margins.bottom) + seat->mouse.row = term->rows - 1; + else + seat->mouse.row = (y - term->margins.top) / term->cell_height; +} + +static bool +touch_is_active(const struct seat *seat) +{ + if (seat->wl_touch == NULL) { + return false; + } + + switch (seat->touch.state) { + case TOUCH_STATE_IDLE: + case TOUCH_STATE_INHIBITED: + return false; + + case TOUCH_STATE_HELD: + case TOUCH_STATE_DRAGGING: + case TOUCH_STATE_SCROLLING: + return true; + } + + BUG("Bad touch state: %d", seat->touch.state); + return false; +} + +static void +wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, struct wl_surface *surface, + wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + if (unlikely(surface == NULL)) { + /* Seen on mutter-3.38 */ + LOG_WARN("compositor sent pointer_enter event with a NULL surface"); + return; + } + + struct seat *seat = data; + + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; + + seat->mouse_focus = term; + term->active_surface = term_surface_kind(term, surface); + + if (touch_is_active(seat)) + return; + + int x = wl_fixed_to_int(surface_x) * term->scale; + int y = wl_fixed_to_int(surface_y) * term->scale; + + seat->pointer.serial = serial; + seat->pointer.hidden = false; + seat->mouse.x = x; + seat->mouse.y = y; + + LOG_DBG("pointer-enter: pointer=%p, serial=%u, surface = %p, new-moused = %p, " + "x=%d, y=%d", + (void *)wl_pointer, serial, (void *)surface, (void *)term, + x, y); + + xassert(tll_length(seat->mouse.buttons) == 0); + + wayl_reload_xcursor_theme(seat, term->scale); /* Scale may have changed */ + term_xcursor_update_for_seat(term, seat); + + switch (term->active_surface) { + case TERM_SURF_GRID: { + mouse_coord_pixel_to_cell(seat, term, x, y); + break; + } + + case TERM_SURF_TITLE: + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + case TERM_SURF_TAB_BAR: + case TERM_SURF_TAB_OVERVIEW: + break; + + case TERM_SURF_BUTTON_MINIMIZE: + case TERM_SURF_BUTTON_MAXIMIZE: + case TERM_SURF_BUTTON_CLOSE: + render_refresh_csd(term); + break; + + case TERM_SURF_NONE: + BUG("Invalid surface type"); + break; + } +} + +static void +wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, struct wl_surface *surface) +{ + struct seat *seat = data; + + if (seat->wl_touch != NULL) { + switch (seat->touch.state) { + case TOUCH_STATE_IDLE: + break; + + case TOUCH_STATE_INHIBITED: + seat->touch.state = TOUCH_STATE_IDLE; + break; + + case TOUCH_STATE_HELD: + case TOUCH_STATE_DRAGGING: + case TOUCH_STATE_SCROLLING: + return; + } + } + + struct terminal *old_moused = seat->mouse_focus; + + LOG_DBG( + "%s: pointer-leave: pointer=%p, serial=%u, surface = %p, old-moused = %p", + seat->name, (void *)wl_pointer, serial, (void *)surface, + (void *)old_moused); + + seat->pointer.hidden = false; + + if (seat->pointer.xcursor_callback != NULL) { + /* A cursor frame callback may never be called if the pointer leaves our surface */ + wl_callback_destroy(seat->pointer.xcursor_callback); + seat->pointer.xcursor_callback = NULL; + seat->pointer.xcursor_pending = false; + } + + /* Reset last-set-xcursor, to ensure we update it on a pointer-enter event */ + seat->pointer.shape = CURSOR_SHAPE_NONE; + + /* Reset mouse state */ + seat->mouse.x = seat->mouse.y = 0; + seat->mouse.col = seat->mouse.row = 0; + mouse_button_state_reset(seat); + for (size_t i = 0; i < ALEN(seat->mouse.aggregated); i++) + seat->mouse.aggregated[i] = 0.0; + seat->mouse.have_discrete = false; + + seat->mouse_focus = NULL; + if (old_moused == NULL) { + LOG_WARN( + "compositor sent pointer_leave event without a pointer_enter " + "event: surface=%p", (void *)surface); + } else { + if (surface != NULL) { + /* Sway 1.4 sends this event with a NULL surface when we destroy the window */ + const struct wl_window UNUSED *win = wl_surface_get_user_data(surface); + xassert(old_moused->window == win); + } + + enum term_surface active_surface = old_moused->active_surface; + + old_moused->active_surface = TERM_SURF_NONE; + + switch (active_surface) { + case TERM_SURF_BUTTON_MINIMIZE: + case TERM_SURF_BUTTON_MAXIMIZE: + case TERM_SURF_BUTTON_CLOSE: + if (old_moused->shutdown.in_progress) + break; + + render_refresh_csd(old_moused); + break; + + case TERM_SURF_GRID: + selection_finalize(seat, old_moused, seat->pointer.serial); + break; + + case TERM_SURF_NONE: + case TERM_SURF_TITLE: + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + case TERM_SURF_TAB_BAR: + case TERM_SURF_TAB_OVERVIEW: + break; + } + + } +} + +static bool +pointer_is_on_button(const struct terminal *term, const struct seat *seat, + enum csd_surface csd_surface) +{ + if (seat->mouse.x < 0) + return false; + if (seat->mouse.y < 0) + return false; + + struct csd_data info = get_csd_data(term, csd_surface); + if (seat->mouse.x > info.width) + return false; + + if (seat->mouse.y > info.height) + return false; + + return true; +} + +static void +wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, + uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + struct seat *seat = data; + + /* Touch-emulated pointer events have wl_pointer == NULL. */ + if (wl_pointer != NULL && touch_is_active(seat)) + return; + + struct wayland *wayl = seat->wayl; + struct terminal *term = seat->mouse_focus; + + if (unlikely(term == NULL)) { + /* Typically happens when the compositor sent a pointer enter + * event with a NULL surface - see wl_pointer_enter(). + * + * In this case, we never set seat->mouse_focus (since we + * can't map the enter event to a specific window). */ + return; + } + + struct wl_window *win = term->window; + + LOG_DBG("pointer_motion: pointer=%p, x=%d, y=%d", (void *)wl_pointer, + wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); + + xassert(term != NULL); + + int x = wl_fixed_to_int(surface_x) * term->scale; + int y = wl_fixed_to_int(surface_y) * term->scale; + + enum term_surface surf_kind = term->active_surface; + int button = 0; + bool send_to_client = false; + bool is_on_button = false; + + /* If current surface is a button, check if pointer was on it + *before* the motion event */ + switch (surf_kind) { + case TERM_SURF_BUTTON_MINIMIZE: + is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE); + break; + + case TERM_SURF_BUTTON_MAXIMIZE: + is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE); + break; + + case TERM_SURF_BUTTON_CLOSE: + is_on_button = pointer_is_on_button(term, seat, CSD_SURF_CLOSE); + break; + + case TERM_SURF_NONE: + case TERM_SURF_GRID: + case TERM_SURF_TITLE: + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + case TERM_SURF_TAB_BAR: + case TERM_SURF_TAB_OVERVIEW: + break; + } + + seat->pointer.hidden = false; + seat->mouse.x = x; + seat->mouse.y = y; + + term_xcursor_update_for_seat(term, seat); + + if (tll_length(seat->mouse.buttons) > 0) { + const struct button_tracker *tracker = &tll_front(seat->mouse.buttons); + surf_kind = tracker->surf_kind; + button = tracker->button; + send_to_client = tracker->send_to_client; + } + + switch (surf_kind) { + case TERM_SURF_NONE: + break; + + case TERM_SURF_BUTTON_MINIMIZE: + if (pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) != is_on_button) + render_refresh_csd(term); + break; + + case TERM_SURF_BUTTON_MAXIMIZE: + if (pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) != is_on_button) + render_refresh_csd(term); + break; + + case TERM_SURF_BUTTON_CLOSE: + if (pointer_is_on_button(term, seat, CSD_SURF_CLOSE) != is_on_button) + render_refresh_csd(term); + break; + + case TERM_SURF_TITLE: + /* We've started a 'move' timer, but user started dragging + * right away - abort the timer and initiate the actual move + * right away */ + if (button == BTN_LEFT && win->csd.move_timeout_fd != -1) { + fdm_del(wayl->fdm, win->csd.move_timeout_fd); + win->csd.move_timeout_fd = -1; + xdg_toplevel_move(win->xdg_toplevel, seat->wl_seat, win->csd.serial); + } + break; + + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + case TERM_SURF_TAB_BAR: + break; + + case TERM_SURF_TAB_OVERVIEW: { + const int new_hover = tab_overview_hit_test(win, x, y); + if (new_hover != win->tab_overview_state.hover_idx) { + win->tab_overview_state.hover_idx = new_hover; + render_refresh(term); + } + break; + } + + case TERM_SURF_GRID: { + int old_col = seat->mouse.col; + int old_row = seat->mouse.row; + + mouse_coord_pixel_to_cell(seat, term, seat->mouse.x, seat->mouse.y); + + xassert(seat->mouse.col >= 0 && seat->mouse.col < term->cols); + xassert(seat->mouse.row >= 0 && seat->mouse.row < term->rows); + + /* Cursor has moved to a different cell since last time */ + bool cursor_is_on_new_cell + = old_col != seat->mouse.col || old_row != seat->mouse.row; + + if (cursor_is_on_new_cell) { + /* Prevent multiple/different mouse bindings from + * triggering if the mouse has moved "too much" (to + * another cell) */ + seat->mouse.count = 0; + } + + /* Cursor is inside the grid, i.e. *not* in the margins */ + const bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; + + enum selection_scroll_direction auto_scroll_direction + = term->selection.coords.end.row < 0 + ? SELECTION_SCROLL_NOT + : y < term->margins.top + ? SELECTION_SCROLL_UP + : y > term->height - term->margins.bottom + ? SELECTION_SCROLL_DOWN + : SELECTION_SCROLL_NOT; + + if (auto_scroll_direction == SELECTION_SCROLL_NOT) + selection_stop_scroll_timer(term); + + /* Update selection */ + if (!term->is_searching) { + if (auto_scroll_direction != SELECTION_SCROLL_NOT) { + /* + * Start 'selection auto-scrolling' + * + * The speed of the scrolling is proportional to the + * distance between the mouse and the grid; the + * further away the mouse is, the faster we scroll. + * + * Note that the speed is measured in 'intervals (in + * ns) between each timed scroll of a single line'. + * + * Thus, the further away the mouse is, the smaller + * interval value we use. + */ + + int distance = auto_scroll_direction == SELECTION_SCROLL_UP + ? term->margins.top - y + : y - (term->height - term->margins.bottom); + + xassert(distance > 0); + int divisor + = distance * term->conf->scrollback.multiplier / term->scale; + + selection_start_scroll_timer( + term, 400000000 / (divisor > 0 ? divisor : 1), + auto_scroll_direction, seat->mouse.col); + } + + if (term->selection.ongoing && + (cursor_is_on_new_cell || + (term->selection.coords.end.row < 0 && + seat->mouse.x >= term->margins.left && + seat->mouse.x < term->width - term->margins.right && + seat->mouse.y >= term->margins.top && + seat->mouse.y < term->height - term->margins.bottom))) + { + selection_update(term, seat->mouse.col, seat->mouse.row); + } + } + + /* Send mouse event to client application */ + if (!term_mouse_grabbed(term, seat) && + (cursor_is_on_new_cell || + term->mouse_reporting == MOUSE_SGR_PIXELS) && + ((button == 0 && cursor_is_on_grid) || + (button != 0 && send_to_client))) + { + xassert(seat->mouse.col < term->cols); + xassert(seat->mouse.row < term->rows); + + term_mouse_motion( + term, button, + seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, + seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); + } + break; + } + } +} + +static bool +fdm_csd_move(struct fdm *fdm, int fd, int events, void *data) +{ + struct seat *seat = data; + fdm_del(fdm, fd); + + if (seat->mouse_focus == NULL) { + LOG_WARN( + "%s: CSD move timeout triggered, but seat's has no mouse focused terminal", + seat->name); + return true; + } + + struct wl_window *win = seat->mouse_focus->window; + + win->csd.move_timeout_fd = -1; + xdg_toplevel_move(win->xdg_toplevel, seat->wl_seat, win->csd.serial); + return true; +} + +static const struct key_binding * +match_mouse_binding(const struct seat *seat, const struct terminal *term, + int button) +{ + if (seat->wl_keyboard != NULL && seat->kbd.xkb_state != NULL) { + /* Seat has keyboard - use mouse bindings *with* modifiers */ + + const struct key_binding_set *bindings = + key_binding_for(term->wl->key_binding_manager, term->conf, seat); + xassert(bindings != NULL); + + xkb_mod_mask_t mods; + get_current_modifiers(seat, &mods, NULL, 0, true); + + /* Ignore selection override modifiers when + * matching modifiers */ + mods &= ~bindings->selection_overrides; + + const struct key_binding *match = NULL; + + tll_foreach(bindings->mouse, it) { + const struct key_binding *binding = &it->item; + + if (binding->m.button != button) { + /* Wrong button */ + continue; + } + + if (binding->mods != mods) { + /* Modifier mismatch */ + continue; + } + + if (binding->m.count > seat->mouse.count) { + /* Not correct click count */ + continue; + } + + if (match == NULL || binding->m.count > match->m.count) + match = binding; + } + + return match; + } + + else { + /* Seat does NOT have a keyboard - use mouse bindings *without* + * modifiers */ + const struct config_key_binding *match = NULL; + const struct config *conf = term->conf; + + for (size_t i = 0; i < conf->bindings.mouse.count; i++) { + const struct config_key_binding *binding = + &conf->bindings.mouse.arr[i]; + + if (binding->m.button != button) { + /* Wrong button */ + continue; + } + + if (binding->m.count > seat->mouse.count) { + /* Incorrect click count */ + continue; + } + + if (tll_length(binding->modifiers) > 0) { + /* Binding has modifiers */ + continue; + } + + if (match == NULL || binding->m.count > match->m.count) + match = binding; + } + + if (match != NULL) { + static struct key_binding bind; + bind.action = match->action; + bind.aux = &match->aux; + return &bind; + } + + return NULL; + } + + BUG("should not get here"); +} + +static void +wl_pointer_button(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, uint32_t time, uint32_t button, uint32_t state) +{ + LOG_DBG("BUTTON: pointer=%p, serial=%u, button=%x, state=%u", + (void *)wl_pointer, serial, button, state); + + xassert(serial != 0); + + struct seat *seat = data; + + /* Touch-emulated pointer events have wl_pointer == NULL. */ + if (wl_pointer != NULL && touch_is_active(seat)) + return; + + struct wayland *wayl = seat->wayl; + struct terminal *term = seat->mouse_focus; + + seat->pointer.serial = serial; + seat->pointer.hidden = false; + + xassert(term != NULL); + + enum term_surface surf_kind = TERM_SURF_NONE; + bool send_to_client = false; + + if (state == WL_POINTER_BUTTON_STATE_PRESSED) { + if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_IDLE) { + seat->touch.state = TOUCH_STATE_INHIBITED; + } + + /* Time since last click */ + struct timespec now, since_last; + clock_gettime(CLOCK_MONOTONIC, &now); + timespec_sub(&now, &seat->mouse.last_time, &since_last); + + if (seat->mouse.last_released_button == button && + since_last.tv_sec == 0 && since_last.tv_nsec <= 300 * 1000 * 1000) + { + seat->mouse.count++; + } else + seat->mouse.count = 1; + + /* + * Workaround GNOME bug + * + * Dragging the window, then stopping the drag (releasing the + * mouse button), *without* moving the mouse, and then + * clicking twice, waiting for the CSD timer, and finally + * clicking once more, results in the following sequence + * (keyboard and other irrelevant events filtered out, unless + * they're needed to prove a point): + * + * dbg: input.c:1551: cancelling drag timer, moving window + * dbg: input.c:759: keyboard_leave: keyboard=0x607000003580, serial=873, surface=0x6070000036d0 + * dbg: input.c:1432: seat0: pointer-leave: pointer=0x607000003660, serial=874, surface = 0x6070000396e0, old-moused = 0x622000006100 + * + * --> drag stopped here + * + * --> LMB clicked first time after the drag (generates the + * enter event on *release*, but no button events) + * dbg: input.c:1360: pointer-enter: pointer=0x607000003660, serial=876, surface = 0x6070000396e0, new-moused = 0x622000006100 + * + * --> LMB clicked, and held until the timer times out, second + * time after the drag + * dbg: input.c:1712: BUTTON: pointer=0x607000003660, serial=877, button=110, state=1 + * dbg: input.c:1806: starting move timer + * dbg: input.c:1692: move timer timed out + * dbg: input.c:759: keyboard_leave: keyboard=0x607000003580, serial=878, surface=0x6070000036d0 + * + * --> NOTE: ^^ no pointer leave event this time, only the + * keyboard leave + * + * --> LMB clicked one last time + * dbg: input.c:697: seat0: keyboard_enter: keyboard=0x607000003580, serial=879, surface=0x6070000036d0 + * dbg: input.c:1712: BUTTON: pointer=0x607000003660, serial=880, button=110, state=1 + * err: input.c:1741: BUG in wl_pointer_button(): assertion failed: 'it->item.button != button' + * + * What are we seeing? + * + * - GNOME does *not* send a pointer *enter* event after the drag + * has stopped + * - The second drag does *not* generate a pointer *leave* event + * - The missing leave event means we're still tracking LMB as + * being held down in our seat struct. + * - This leads to an assert (debug builds) when LMB is clicked + * again (seat's button list already contains LMB). + * + * Note: I've also observed variants of the above + */ + tll_foreach(seat->mouse.buttons, it) { + if (it->item.button == button) { + LOG_WARN("multiple button press events for button %d " + "(compositor bug?)", button); + tll_remove(seat->mouse.buttons, it); + break; + } + } + +#if defined(_DEBUG) + tll_foreach(seat->mouse.buttons, it) + xassert(it->item.button != button); +#endif + + /* + * Remember which surface "owns" this button, so that we can + * send motion and button release events to that surface, even + * if the pointer is no longer over it. + */ + tll_push_back( + seat->mouse.buttons, + ((struct button_tracker){ + .button = button, + .surf_kind = term->active_surface, + .send_to_client = false})); + + seat->mouse.last_time = now; + + surf_kind = term->active_surface; + send_to_client = false; /* For now, may be set to true if a binding consumes the button */ + } else { + bool UNUSED have_button = false; + tll_foreach(seat->mouse.buttons, it) { + if (it->item.button == button) { + have_button = true; + surf_kind = it->item.surf_kind; + send_to_client = it->item.send_to_client; + tll_remove(seat->mouse.buttons, it); + break; + } + } + + if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_INHIBITED) { + if (tll_length(seat->mouse.buttons) == 0) { + seat->touch.state = TOUCH_STATE_IDLE; + } + } + + if (!have_button) { + /* + * Seen on Sway with slurp + * + * 1. Run slurp + * 2. Press, and hold left mouse button + * 3. Press escape, to cancel slurp + * 4. Release mouse button + * 5. BAM! + */ + LOG_WARN("stray button release event (compositor bug?)"); + return; + } + + seat->mouse.last_released_button = button; + } + + switch (surf_kind) { + case TERM_SURF_TITLE: + if (state == WL_POINTER_BUTTON_STATE_PRESSED) { + + struct wl_window *win = term->window; + + /* Toggle maximized state on double-click */ + if (term->conf->csd.double_click_to_maximize && + button == BTN_LEFT && + seat->mouse.count == 2) + { + if (win->is_maximized) + xdg_toplevel_unset_maximized(win->xdg_toplevel); + else + xdg_toplevel_set_maximized(win->xdg_toplevel); + } + + else if (button == BTN_LEFT && win->csd.move_timeout_fd < 0) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 200000000}, + }; + + int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd >= 0 && + timerfd_settime(fd, 0, &timeout, NULL) == 0 && + fdm_add(wayl->fdm, fd, EPOLLIN, &fdm_csd_move, seat)) + { + win->csd.move_timeout_fd = fd; + win->csd.serial = serial; + } else { + LOG_ERRNO("failed to configure XDG toplevel move timer FD"); + if (fd >= 0) + close(fd); + } + } + + if (button == BTN_RIGHT && tll_length(seat->mouse.buttons) == 1) { + const struct csd_data info = get_csd_data(term, CSD_SURF_TITLE); + xdg_toplevel_show_window_menu( + win->xdg_toplevel, + seat->wl_seat, + seat->pointer.serial, + seat->mouse.x + info.x, seat->mouse.y + info.y); + } + } + + else if (state == WL_POINTER_BUTTON_STATE_RELEASED) { + struct wl_window *win = term->window; + if (win->csd.move_timeout_fd >= 0) { + fdm_del(wayl->fdm, win->csd.move_timeout_fd); + win->csd.move_timeout_fd = -1; + } + } + return; + + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: { + if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) { + enum xdg_toplevel_resize_edge resize_type = XDG_TOPLEVEL_RESIZE_EDGE_NONE; + + int x = seat->mouse.x; + int y = seat->mouse.y; + + if (is_top_left(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT; + else if (is_top_right(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT; + else if (is_bottom_left(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT; + else if (is_bottom_right(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT; + else { + if (term->active_surface == TERM_SURF_BORDER_LEFT && + !term->window->is_constrained_left) + { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_LEFT; + } + + else if (term->active_surface == TERM_SURF_BORDER_RIGHT && + !term->window->is_constrained_right) + { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT; + } + + else if (term->active_surface == TERM_SURF_BORDER_TOP && + !term->window->is_constrained_top) + { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP; + } + + else if (term->active_surface == TERM_SURF_BORDER_BOTTOM && + !term->window->is_constrained_bottom) + { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM; + } + } + + if (resize_type != XDG_TOPLEVEL_RESIZE_EDGE_NONE) { + xdg_toplevel_resize( + term->window->xdg_toplevel, seat->wl_seat, serial, resize_type); + } + } + return; + } + + case TERM_SURF_BUTTON_MINIMIZE: + if (button == BTN_LEFT && + pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) && + state == WL_POINTER_BUTTON_STATE_RELEASED) + { + xdg_toplevel_set_minimized(term->window->xdg_toplevel); + } + break; + + case TERM_SURF_BUTTON_MAXIMIZE: + if (button == BTN_LEFT && + pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) && + state == WL_POINTER_BUTTON_STATE_RELEASED) + { + if (term->window->is_maximized) + xdg_toplevel_unset_maximized(term->window->xdg_toplevel); + else + xdg_toplevel_set_maximized(term->window->xdg_toplevel); + } + break; + + case TERM_SURF_BUTTON_CLOSE: + if (button == BTN_LEFT && + pointer_is_on_button(term, seat, CSD_SURF_CLOSE) && + state == WL_POINTER_BUTTON_STATE_RELEASED) + { + term_shutdown(term); + } + break; + + case TERM_SURF_GRID: { + search_cancel(term); + urls_reset(term); + + bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; + + switch (state) { + case WL_POINTER_BUTTON_STATE_PRESSED: { + bool consumed = false; + + if (cursor_is_on_grid && term_mouse_grabbed(term, seat)) { + const struct key_binding *match = + match_mouse_binding(seat, term, button); + + if (match != NULL) + consumed = execute_binding(seat, term, match, serial, 1); + } + + send_to_client = !consumed && cursor_is_on_grid; + + if (send_to_client) + tll_back(seat->mouse.buttons).send_to_client = true; + + if (send_to_client && + !term_mouse_grabbed(term, seat) && + cursor_is_on_grid) + { + term_mouse_down( + term, button, seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, + seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); + } + break; + } + + case WL_POINTER_BUTTON_STATE_RELEASED: + selection_finalize(seat, term, serial); + + if (send_to_client && !term_mouse_grabbed(term, seat)) { + term_mouse_up( + term, button, seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, + seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); + } + break; + } + break; + } + + case TERM_SURF_TAB_BAR: { + if (state != WL_POINTER_BUTTON_STATE_PRESSED) + break; + if (button != BTN_LEFT) + break; + + size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y); + if (idx == SIZE_MAX || idx >= term->window->tab_count) + break; + if (idx == term->window->active_tab) + break; + term_tab_switch(term->window, idx); + break; + } + + case TERM_SURF_TAB_OVERVIEW: { + if (state != WL_POINTER_BUTTON_STATE_PRESSED) + break; + struct wl_window *win = term->window; + if (button == BTN_LEFT) { + int idx = tab_overview_hit_test(win, seat->mouse.x, seat->mouse.y); + if (idx >= 0 && (size_t)idx < win->tab_count) { + if ((size_t)idx != win->active_tab) + term_tab_switch(win, (size_t)idx); + /* Selecting a card snaps closed — no fade-out */ + tab_overview_close_instant(win); + } else { + /* Click on backdrop animates closed */ + tab_overview_toggle(win); + } + } else if (button == BTN_RIGHT) { + tab_overview_toggle(win); + } + break; + } + + case TERM_SURF_NONE: + BUG("Invalid surface type"); + break; + + } +} + +static void +alternate_scroll(struct seat *seat, int amount, int button) +{ + if (seat->wl_keyboard == NULL) + return; + + /* Should be cleared in leave event */ + xassert(seat->mouse_focus != NULL); + struct terminal *term = seat->mouse_focus; + + assert(button == BTN_BACK || button == BTN_FORWARD); + + xkb_keycode_t key = button == BTN_BACK + ? seat->kbd.key_arrow_up : seat->kbd.key_arrow_down; + + for (int i = 0; i < amount; i++) + key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_DOWN); + key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_UP); +} + +static void +mouse_scroll(struct seat *seat, int amount, enum wl_pointer_axis axis) +{ + struct terminal *term = seat->mouse_focus; + xassert(term != NULL); + + int button = axis == WL_POINTER_AXIS_VERTICAL_SCROLL + ? amount < 0 ? BTN_WHEEL_BACK : BTN_WHEEL_FORWARD + : amount < 0 ? BTN_WHEEL_LEFT : BTN_WHEEL_RIGHT; + amount = abs(amount); + + if (term_mouse_grabbed(term, seat)) { + seat->mouse.count = 1; + + const struct key_binding *match = + match_mouse_binding(seat, term, button); + + if (match != NULL) + execute_binding(seat, term, match, seat->pointer.serial, amount); + + seat->mouse.last_released_button = button; + } + + else if (seat->mouse.col >= 0 && seat->mouse.row >= 0) { + xassert(seat->mouse.col < term->cols); + xassert(seat->mouse.row < term->rows); + + for (int i = 0; i < amount; i++) { + term_mouse_down( + term, button, seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, + seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); + } + + term_mouse_up( + term, button, seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, + seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); + } +} + +static double +mouse_scroll_multiplier(const struct terminal *term, const struct seat *seat) +{ + return (term->grid == &term->normal || + (term_mouse_grabbed(term, seat) && term->alt_scrolling)) + ? term->conf->scrollback.multiplier + : 1.0; +} + +static void +wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, + uint32_t time, uint32_t axis, wl_fixed_t value) +{ + struct seat *seat = data; + + if (touch_is_active(seat)) + return; + + if (seat->mouse.have_discrete) + return; + + xassert(seat->mouse_focus != NULL); + xassert(axis < ALEN(seat->mouse.aggregated)); + + const struct terminal *term = seat->mouse_focus; + + /* + * Aggregate scrolled amount until we get at least 1.0 + * + * Without this, very slow scrolling will never actually scroll + * anything. + */ + seat->mouse.aggregated[axis] += + mouse_scroll_multiplier(term, seat) * wl_fixed_to_double(value); + if (fabs(seat->mouse.aggregated[axis]) < seat->mouse_focus->cell_height) + return; + + int lines = seat->mouse.aggregated[axis] / seat->mouse_focus->cell_height; + mouse_scroll(seat, lines, axis); + seat->mouse.aggregated[axis] -= (double)lines * seat->mouse_focus->cell_height; +} + +static void +wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, + enum wl_pointer_axis axis, int32_t discrete) +{ + LOG_DBG("axis_discrete: %d", discrete); + struct seat *seat = data; + + if (touch_is_active(seat)) + return; + + seat->mouse.have_discrete = true; + int amount = discrete; + + if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) { + /* Treat mouse wheel left/right as regular buttons */ + } else + amount *= mouse_scroll_multiplier(seat->mouse_focus, seat); + + mouse_scroll(seat, amount, axis); +} + +#if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION) +static void +wl_pointer_axis_value120(void *data, struct wl_pointer *wl_pointer, + enum wl_pointer_axis axis, int32_t value120) +{ + LOG_DBG("axis_value120: %d -> %.2f", value120, (float)value120 / 120.); + + struct seat *seat = data; + + if (touch_is_active(seat)) + return; + + seat->mouse.have_discrete = true; + + /* + * 120 corresponds to a single "low-res" scroll step. + * + * When doing high-res scrolling, take the scrollback.multiplier, + * and calculate how many degrees there are per line. + * + * For example, with scrollback.multiplier = 3, we have 120 / 3 == 40. + * + * Then, accumulate high-res scroll events, until we have *at + * least* that much. Translate the accumulated value to number of + * lines, and scroll. + * + * Subtract the "used" degrees from the accumulated value, and + * keep what's left (this value will always be less than the + * per-line value). + */ + const double multiplier = mouse_scroll_multiplier(seat->mouse_focus, seat); + const double per_line = 120. / multiplier; + + seat->mouse.aggregated_120[axis] += (double)value120; + + if (fabs(seat->mouse.aggregated_120[axis]) < per_line) + return; + + int lines = (int)(seat->mouse.aggregated_120[axis] / per_line); + mouse_scroll(seat, lines, axis); + seat->mouse.aggregated_120[axis] -= (double)lines * per_line; +} +#endif + +static void +wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) +{ + struct seat *seat = data; + + if (touch_is_active(seat)) + return; + + seat->mouse.have_discrete = false; +} + +static void +wl_pointer_axis_source(void *data, struct wl_pointer *wl_pointer, + uint32_t axis_source) +{ +} + +static void +wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, + uint32_t time, uint32_t axis) +{ + struct seat *seat = data; + + if (touch_is_active(seat)) + return; + + xassert(axis < ALEN(seat->mouse.aggregated)); + seat->mouse.aggregated[axis] = 0.; +} + +const struct wl_pointer_listener pointer_listener = { + .enter = &wl_pointer_enter, + .leave = &wl_pointer_leave, + .motion = &wl_pointer_motion, + .button = &wl_pointer_button, + .axis = &wl_pointer_axis, + .frame = &wl_pointer_frame, + .axis_source = &wl_pointer_axis_source, + .axis_stop = &wl_pointer_axis_stop, + .axis_discrete = &wl_pointer_axis_discrete, +#if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION) + .axis_value120 = &wl_pointer_axis_value120, +#endif +}; + +static bool +touch_to_scroll(struct seat *seat, struct terminal *term, + wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + bool coord_updated = false; + + int y = wl_fixed_to_int(surface_y) * term->scale; + int rows = (y - seat->mouse.y) / term->cell_height; + if (rows != 0) { + mouse_scroll(seat, -rows, WL_POINTER_AXIS_VERTICAL_SCROLL); + seat->mouse.y += rows * term->cell_height; + coord_updated = true; + } + + int x = wl_fixed_to_int(surface_x) * term->scale; + int cols = (x - seat->mouse.x) / term->cell_width; + if (cols != 0) { + mouse_scroll(seat, -cols, WL_POINTER_AXIS_HORIZONTAL_SCROLL); + seat->mouse.x += cols * term->cell_width; + coord_updated = true; + } + + return coord_updated; +} + +static void +wl_touch_down(void *data, struct wl_touch *wl_touch, uint32_t serial, + uint32_t time, struct wl_surface *surface, int32_t id, + wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + struct seat *seat = data; + + if (seat->touch.state != TOUCH_STATE_IDLE) + return; + + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; + + LOG_DBG("touch_down: touch=%p, x=%d, y=%d", (void *)wl_touch, + wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); + + int x = wl_fixed_to_int(surface_x) * term->scale; + int y = wl_fixed_to_int(surface_y) * term->scale; + + seat->mouse.x = x; + seat->mouse.y = y; + mouse_coord_pixel_to_cell(seat, term, x, y); + + seat->touch.state = TOUCH_STATE_HELD; + seat->touch.serial = serial; + seat->touch.time = time + term->conf->touch.long_press_delay; + seat->touch.surface = surface; + seat->touch.surface_kind = term_surface_kind(term, surface); + seat->touch.id = id; +} + +static void +wl_touch_up(void *data, struct wl_touch *wl_touch, uint32_t serial, + uint32_t time, int32_t id) +{ + struct seat *seat = data; + + if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id) + return; + + LOG_DBG("touch_up: touch=%p", (void *)wl_touch); + + struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); + struct terminal *term = win->term; + + struct terminal *old_term = seat->mouse_focus; + enum term_surface old_active_surface = term->active_surface; + seat->mouse_focus = term; + term->active_surface = seat->touch.surface_kind; + + switch (seat->touch.state) { + case TOUCH_STATE_HELD: + wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT, + WL_POINTER_BUTTON_STATE_PRESSED); + /* fallthrough */ + case TOUCH_STATE_DRAGGING: + wl_pointer_button(seat, NULL, serial, time, BTN_LEFT, + WL_POINTER_BUTTON_STATE_RELEASED); + /* fallthrough */ + case TOUCH_STATE_SCROLLING: + term->active_surface = TERM_SURF_NONE; + seat->touch.state = TOUCH_STATE_IDLE; + break; + + case TOUCH_STATE_INHIBITED: + case TOUCH_STATE_IDLE: + BUG("Bad touch state: %d", seat->touch.state); + break; + } + + seat->mouse_focus = old_term; + term->active_surface = old_active_surface; +} + +static void +wl_touch_motion(void *data, struct wl_touch *wl_touch, uint32_t time, + int32_t id, wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + struct seat *seat = data; + if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id) + return; + + LOG_DBG("touch_motion: touch=%p, x=%d, y=%d", (void *)wl_touch, + wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); + + struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); + struct terminal *term = win->term; + + struct terminal *old_term = seat->mouse_focus; + enum term_surface old_active_surface = term->active_surface; + seat->mouse_focus = term; + term->active_surface = seat->touch.surface_kind; + + switch (seat->touch.state) { + case TOUCH_STATE_HELD: + if (time <= seat->touch.time && term->active_surface == TERM_SURF_GRID) { + if (touch_to_scroll(seat, term, surface_x, surface_y)) + seat->touch.state = TOUCH_STATE_SCROLLING; + break; + } else { + wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT, + WL_POINTER_BUTTON_STATE_PRESSED); + seat->touch.state = TOUCH_STATE_DRAGGING; + /* fallthrough */ + } + case TOUCH_STATE_DRAGGING: + wl_pointer_motion(seat, NULL, time, surface_x, surface_y); + break; + case TOUCH_STATE_SCROLLING: + touch_to_scroll(seat, term, surface_x, surface_y); + break; + + case TOUCH_STATE_INHIBITED: + case TOUCH_STATE_IDLE: + BUG("Bad touch state: %d", seat->touch.state); + break; + } + + seat->mouse_focus = old_term; + term->active_surface = old_active_surface; +} + +static void +wl_touch_frame(void *data, struct wl_touch *wl_touch) +{ +} + +static void +wl_touch_cancel(void *data, struct wl_touch *wl_touch) +{ + struct seat *seat = data; + if (seat->touch.state == TOUCH_STATE_INHIBITED) + return; + + seat->touch.state = TOUCH_STATE_IDLE; +} + +const struct wl_touch_listener touch_listener = { + .down = wl_touch_down, + .up = wl_touch_up, + .motion = wl_touch_motion, + .frame = wl_touch_frame, + .cancel = wl_touch_cancel, +}; diff --git a/input.h b/input.h new file mode 100644 index 0000000..34342bb --- /dev/null +++ b/input.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include "cursor-shape.h" +#include "misc.h" +#include "wayland.h" + +/* + * Custom defines for mouse wheel left/right buttons. + * + * Libinput does not define these. On Wayland, all scroll events (both + * vertical and horizontal) are reported not as buttons, as 'axis' + * events. + * + * Libinput _does_ define BTN_BACK and BTN_FORWARD, which is + * what we use for vertical scroll events. But for horizontal scroll + * events, there aren't any pre-defined mouse buttons. + * + * Mouse buttons are in the range 0x110 - 0x11f, with joystick defines + * starting at 0x120. + */ +#define BTN_WHEEL_BACK 0x11c +#define BTN_WHEEL_FORWARD 0x11d +#define BTN_WHEEL_LEFT 0x11e +#define BTN_WHEEL_RIGHT 0x11f + +extern const struct wl_keyboard_listener keyboard_listener; +extern const struct wl_pointer_listener pointer_listener; +extern const struct wl_touch_listener touch_listener; + +void input_repeat(struct seat *seat, uint32_t key); + +void get_current_modifiers(const struct seat *seat, + xkb_mod_mask_t *effective, + xkb_mod_mask_t *consumed, + uint32_t key, bool filter_locked); + +enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y); diff --git a/key-binding.c b/key-binding.c new file mode 100644 index 0000000..a2883ed --- /dev/null +++ b/key-binding.c @@ -0,0 +1,661 @@ +#include "key-binding.h" + +#include + +#define LOG_MODULE "key-binding" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "config.h" +#include "debug.h" +#include "terminal.h" +#include "util.h" +#include "wayland.h" +#include "xkbcommon-vmod.h" +#include "xmalloc.h" + +struct vmod_map { + const char *name; + xkb_mod_mask_t virtual_mask; + xkb_mod_mask_t real_mask; +}; + +struct key_set { + struct key_binding_set public; + + const struct config *conf; + const struct seat *seat; + size_t conf_ref_count; + + /* Virtual to real modifier mappings */ + struct vmod_map vmods[8]; +}; +typedef tll(struct key_set) bind_set_list_t; + +struct key_binding_manager { + struct key_set *last_used_set; + bind_set_list_t binding_sets; +}; + +static void load_keymap(struct key_set *set); +static void unload_keymap(struct key_set *set); + +struct key_binding_manager * +key_binding_manager_new(void) +{ + struct key_binding_manager *mgr = xcalloc(1, sizeof(*mgr)); + return mgr; +} + +void +key_binding_manager_destroy(struct key_binding_manager *mgr) +{ + xassert(tll_length(mgr->binding_sets) == 0); + free(mgr); +} + +static void +initialize_vmod_mappings(struct key_set *set) +{ + if (set->seat == NULL || set->seat->kbd.xkb_keymap == NULL) + return; + + set->vmods[0].name = XKB_VMOD_NAME_ALT; + set->vmods[1].name = XKB_VMOD_NAME_HYPER; + set->vmods[2].name = XKB_VMOD_NAME_LEVEL3; + set->vmods[3].name = XKB_VMOD_NAME_LEVEL5; + set->vmods[4].name = XKB_VMOD_NAME_META; + set->vmods[5].name = XKB_VMOD_NAME_NUM; + set->vmods[6].name = XKB_VMOD_NAME_SCROLL; + set->vmods[7].name = XKB_VMOD_NAME_SUPER; + + struct xkb_state *scratch_state = xkb_state_new(set->seat->kbd.xkb_keymap); + xassert(scratch_state != NULL); + + for (size_t i = 0; i < ALEN(set->vmods); i++) { + xkb_mod_index_t virt_idx = xkb_keymap_mod_get_index( + set->seat->kbd.xkb_keymap, set->vmods[i].name); + + if (virt_idx != XKB_MOD_INVALID) { + xkb_mod_mask_t vmask = 1 << virt_idx; + xkb_state_update_mask(scratch_state, vmask, 0, 0, 0, 0, 0); + set->vmods[i].real_mask = xkb_state_serialize_mods( + scratch_state, XKB_STATE_MODS_DEPRESSED) & ~vmask; + set->vmods[i].virtual_mask = vmask; + + LOG_DBG("%s: 0x%04x -> 0x%04x", + set->vmods[i].name, + set->vmods[i].virtual_mask, + set->vmods[i].real_mask); + } else { + set->vmods[i].virtual_mask = 0; + set->vmods[i].real_mask = 0; + + LOG_DBG("%s: virtual modifier not available", set->vmods[i].name); + } + } + + xkb_state_unref(scratch_state); +} + +void +key_binding_new_for_seat(struct key_binding_manager *mgr, + const struct seat *seat) +{ +#if defined(_DEBUG) + tll_foreach(mgr->binding_sets, it) + xassert(it->item.seat != seat); +#endif + + tll_foreach(seat->wayl->terms, it) { + struct key_set set = { + .public = { + .key = tll_init(), + .search = tll_init(), + .url = tll_init(), + .mouse = tll_init(), + }, + .conf = it->item->conf, + .seat = seat, + .conf_ref_count = 1, + }; + + tll_push_back(mgr->binding_sets, set); + initialize_vmod_mappings(&tll_back(mgr->binding_sets)); + + LOG_DBG("new (seat): set=%p, seat=%p, conf=%p, ref-count=1", + (void *)&tll_back(mgr->binding_sets), + (void *)set.seat, (void *)set.conf); + + load_keymap(&tll_back(mgr->binding_sets)); + } + + LOG_DBG("new (seat): total number of sets: %zu", + tll_length(mgr->binding_sets)); +} + +void +key_binding_new_for_conf(struct key_binding_manager *mgr, + const struct wayland *wayl, const struct config *conf) +{ + tll_foreach(wayl->seats, it) { + struct seat *seat = &it->item; + + struct key_set *existing = + (struct key_set *)key_binding_for(mgr, conf, seat); + + if (existing != NULL) { + existing->conf_ref_count++; + continue; + } + + struct key_set set = { + .public = { + .key = tll_init(), + .search = tll_init(), + .url = tll_init(), + .mouse = tll_init(), + }, + .conf = conf, + .seat = seat, + .conf_ref_count = 1, + }; + + tll_push_back(mgr->binding_sets, set); + initialize_vmod_mappings(&tll_back(mgr->binding_sets)); + + load_keymap(&tll_back(mgr->binding_sets)); + + /* Chances are high this set will be requested next */ + mgr->last_used_set = &tll_back(mgr->binding_sets); + + LOG_DBG("new (conf): set=%p, seat=%p, conf=%p, ref-count=1", + (void *)&tll_back(mgr->binding_sets), + (void *)set.seat, (void *)set.conf); + } + + LOG_DBG("new (conf): total number of sets: %zu", + tll_length(mgr->binding_sets)); +} + +struct key_binding_set * NOINLINE +key_binding_for(struct key_binding_manager *mgr, const struct config *conf, + const struct seat *seat) +{ + struct key_set *last_used = mgr->last_used_set; + if (last_used != NULL && + last_used->conf == conf && + last_used->seat == seat) + { + // LOG_DBG("lookup: last used"); + return &last_used->public; + } + + tll_foreach(mgr->binding_sets, it) { + struct key_set *set = &it->item; + + if (set->conf != conf) + continue; + if (set->seat != seat) + continue; + +#if 0 + LOG_DBG("lookup: set=%p, seat=%p, conf=%p, ref-count=%zu", + (void *)set, (void *)seat, (void *)conf, set->conf_ref_count); +#endif + mgr->last_used_set = set; + return &set->public; + } + + return NULL; +} + +static void +key_binding_set_destroy(struct key_binding_manager *mgr, + struct key_set *set) +{ + unload_keymap(set); + if (mgr->last_used_set == set) + mgr->last_used_set = NULL; + + /* Note: caller must remove from binding_sets */ +} + +void +key_binding_remove_seat(struct key_binding_manager *mgr, + const struct seat *seat) +{ + tll_foreach(mgr->binding_sets, it) { + struct key_set *set = &it->item; + + if (set->seat != seat) + continue; + + key_binding_set_destroy(mgr, set); + tll_remove(mgr->binding_sets, it); + + LOG_DBG("remove seat: set=%p, seat=%p, total number of sets: %zu", + (void *)set, (void *)seat, tll_length(mgr->binding_sets)); + } + + LOG_DBG("remove seat: total number of sets: %zu", + tll_length(mgr->binding_sets)); +} + +void +key_binding_unref(struct key_binding_manager *mgr, const struct config *conf) +{ + tll_foreach(mgr->binding_sets, it) { + struct key_set *set = &it->item; + + if (set->conf != conf) + continue; + + xassert(set->conf_ref_count > 0); + if (--set->conf_ref_count == 0) { + LOG_DBG("unref conf: set=%p, seat=%p, conf=%p", + (void *)set, (void *)set->seat, (void *)conf); + + key_binding_set_destroy(mgr, set); + tll_remove(mgr->binding_sets, it); + } + } + + LOG_DBG("unref conf: total number of sets: %zu", + tll_length(mgr->binding_sets)); +} + +static xkb_keycode_list_t +key_codes_for_xkb_sym(struct xkb_keymap *keymap, xkb_keysym_t sym) +{ + xkb_keycode_list_t key_codes = tll_init(); + + /* + * Find all key codes that map to this symbol. + * + * This allows us to match bindings in other layouts + * too. + */ + struct xkb_state *state = xkb_state_new(keymap); + + for (xkb_keycode_t code = xkb_keymap_min_keycode(keymap); + code <= xkb_keymap_max_keycode(keymap); + code++) + { + if (xkb_state_key_get_one_sym(state, code) == sym) + tll_push_back(key_codes, code); + } + + xkb_state_unref(state); + return key_codes; +} + +static xkb_keysym_t +maybe_repair_key_combo(const struct seat *seat, + xkb_keysym_t sym, xkb_mod_mask_t mods) +{ + /* + * Detect combos containing a shifted symbol and the corresponding + * modifier, and replace the shifted symbol with its unshifted + * variant. + * + * For example, the combo is "Control+Shift+U". In this case, + * Shift is the modifier used to "shift" 'u' to 'U', after which + * 'Shift' will have been "consumed". Since we filter out consumed + * modifiers when matching key combos, this key combo will never + * trigger (we will never be able to match the 'Shift' modifier). + * + * There are two correct variants of the above key combo: + * - "Control+U" (upper case 'U') + * - "Control+Shift+u" (lower case 'u') + * + * What we do here is, for each key *code*, check if there are any + * (shifted) levels where it produces 'sym'. If there are, check + * *which* sets of modifiers are needed to produce it, and compare + * with 'mods'. + * + * If there is at least one common modifier, it means 'sym' is a + * "shifted" symbol, with the corresponding shifting modifier + * explicitly included in the key combo. I.e. the key combo will + * never trigger. + * + * We then proceed and "repair" the key combo by replacing 'sym' + * with the corresponding unshifted symbol. + * + * To reduce the noise, we ignore all key codes where the shifted + * symbol is the same as the unshifted symbol. + */ + + for (xkb_keycode_t code = xkb_keymap_min_keycode(seat->kbd.xkb_keymap); + code <= xkb_keymap_max_keycode(seat->kbd.xkb_keymap); + code++) + { + xkb_layout_index_t layout_idx = + xkb_state_key_get_layout(seat->kbd.xkb_state, code); + + /* Get all unshifted symbols for this key */ + const xkb_keysym_t *base_syms = NULL; + size_t base_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, code, layout_idx, 0, &base_syms); + + if (base_count == 0 || sym == base_syms[0]) { + /* No unshifted symbols, or unshifted symbol is same as 'sym' */ + continue; + } + + /* Name of the unshifted symbol, for logging */ + char base_name[100]; + xkb_keysym_get_name(base_syms[0], base_name, sizeof(base_name)); + + /* Iterate all shift levels */ + for (xkb_level_index_t level_idx = 1; + level_idx < xkb_keymap_num_levels_for_key( + seat->kbd.xkb_keymap, code, layout_idx); + level_idx++) { + + /* Get all symbols for current shift level */ + const xkb_keysym_t *shifted_syms = NULL; + size_t shifted_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, code, + layout_idx, level_idx, &shifted_syms); + + for (size_t i = 0; i < shifted_count; i++) { + if (shifted_syms[i] != sym) + continue; + + /* Get modifier sets that produces the current shift level */ + xkb_mod_mask_t mod_masks[16]; + size_t mod_mask_count = xkb_keymap_key_get_mods_for_level( + seat->kbd.xkb_keymap, code, layout_idx, level_idx, + mod_masks, ALEN(mod_masks)); + + /* Check if key combo's modifier set intersects */ + for (size_t j = 0; j < mod_mask_count; j++) { + if ((mod_masks[j] & mods) != mod_masks[j]) + continue; + + char combo[64] = {0}; + + for (int k = 0; k < sizeof(xkb_mod_mask_t) * 8; k++) { + if (!(mods & (1u << k))) + continue; + + const char *mod_name = xkb_keymap_mod_get_name( + seat->kbd.xkb_keymap, k); + strcat(combo, mod_name); + strcat(combo, "+"); + } + + size_t len = strlen(combo); + xkb_keysym_get_name( + sym, &combo[len], sizeof(combo) - len); + + LOG_WARN( + "%s: combo with both explicit modifier and shifted symbol " + "(level=%d, mod-mask=0x%08x), " + "replacing with %s", + combo, level_idx, mod_masks[j], base_name); + + /* Replace with unshifted symbol */ + return base_syms[0]; + } + } + } + } + + return sym; +} + +static int +key_cmp(struct key_binding a, struct key_binding b) +{ + xassert(a.type == b.type); + + /* + * Sort bindings such that bindings with the same symbol are + * sorted with the binding having the most modifiers comes first. + * + * This fixes an issue where the "wrong" key binding are triggered + * when used with "consumed" modifiers. + * + * For example: if Control+BackSpace is bound before + * Control+Shift+BackSpace, then the latter binding is never + * triggered. + * + * Why? Because Shift is a consumed modifier. This means + * Control+BackSpace is "the same" as Control+Shift+BackSpace. + * + * By sorting bindings with more modifiers first, we work around + * the problem. But note that it is *just* a workaround, and I'm + * not confident there aren't cases where it doesn't work. + * + * See https://codeberg.org/dnkl/foot/issues/1280 + */ + + const int a_mod_count = __builtin_popcount(a.mods); + const int b_mod_count = __builtin_popcount(b.mods); + + switch (a.type) { + case KEY_BINDING: + if (a.k.sym != b.k.sym) + return b.k.sym - a.k.sym; + return b_mod_count - a_mod_count; + + case MOUSE_BINDING: { + if (a.m.button != b.m.button) + return b.m.button - a.m.button; + if (a_mod_count != b_mod_count) + return b_mod_count - a_mod_count; + return b.m.count - a.m.count; + } + } + + BUG("invalid key binding type"); + return 0; +} + +static void NOINLINE +sort_binding_list(key_binding_list_t *list) +{ + tll_sort(*list, key_cmp); +} + +static xkb_mod_mask_t +mods_to_mask(const struct seat *seat, + const struct vmod_map *vmods, size_t vmod_count, + const config_modifier_list_t *mods) +{ + xkb_mod_mask_t mask = 0; + tll_foreach(*mods, it) { + const xkb_mod_index_t idx = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, it->item); + + if (idx == XKB_MOD_INVALID) { + LOG_ERR("%s: invalid modifier name", it->item); + continue; + } + + xkb_mod_mask_t mod = 1 << idx; + + /* Check if this is a virtual modifier, and if so, use the + real modifier it maps to instead */ + for (size_t i = 0; i < vmod_count; i++) { + if (vmods[i].virtual_mask == mod) { + mask |= vmods[i].real_mask; + mod = 0; + + LOG_DBG("%s: virtual modifier, mapped to 0x%04x", + it->item, vmods[i].real_mask); + break; + } + } + + mask |= mod; + } + + return mask; +} + +static void NOINLINE +convert_key_binding(struct key_set *set, + const struct config_key_binding *conf_binding, + key_binding_list_t *bindings) +{ + const struct seat *seat = set->seat; + + xkb_mod_mask_t mods = mods_to_mask( + seat, set->vmods, ALEN(set->vmods), &conf_binding->modifiers); + xkb_keysym_t sym = maybe_repair_key_combo(seat, conf_binding->k.sym, mods); + + struct key_binding binding = { + .type = KEY_BINDING, + .action = conf_binding->action, + .aux = &conf_binding->aux, + .mods = mods, + .k = { + .sym = sym, + .key_codes = key_codes_for_xkb_sym(seat->kbd.xkb_keymap, sym), + }, + }; + tll_push_back(*bindings, binding); + sort_binding_list(bindings); +} + +static void +convert_key_bindings(struct key_set *set) +{ + const struct config *conf = set->conf; + + for (size_t i = 0; i < conf->bindings.key.count; i++) { + const struct config_key_binding *binding = &conf->bindings.key.arr[i]; + convert_key_binding(set, binding, &set->public.key); + } +} + +static void +convert_search_bindings(struct key_set *set) +{ + const struct config *conf = set->conf; + + for (size_t i = 0; i < conf->bindings.search.count; i++) { + const struct config_key_binding *binding = &conf->bindings.search.arr[i]; + convert_key_binding(set, binding, &set->public.search); + } +} + +static void +convert_url_bindings(struct key_set *set) +{ + const struct config *conf = set->conf; + + for (size_t i = 0; i < conf->bindings.url.count; i++) { + const struct config_key_binding *binding = &conf->bindings.url.arr[i]; + convert_key_binding(set, binding, &set->public.url); + } +} + +static void +convert_mouse_binding(struct key_set *set, + const struct config_key_binding *conf_binding) +{ + struct key_binding binding = { + .type = MOUSE_BINDING, + .action = conf_binding->action, + .aux = &conf_binding->aux, + .mods = mods_to_mask(set->seat, set->vmods, ALEN(set->vmods), &conf_binding->modifiers), + .m = { + .button = conf_binding->m.button, + .count = conf_binding->m.count, + }, + }; + tll_push_back(set->public.mouse, binding); + sort_binding_list(&set->public.mouse); +} + +static void +convert_mouse_bindings(struct key_set *set) +{ + const struct config *conf = set->conf; + + for (size_t i = 0; i < conf->bindings.mouse.count; i++) { + const struct config_key_binding *binding = + &conf->bindings.mouse.arr[i]; + convert_mouse_binding(set, binding); + } +} + +static void NOINLINE +load_keymap(struct key_set *set) +{ + LOG_DBG("load keymap: set=%p, seat=%p, conf=%p", + (void *)set, (void *)set->seat, (void *)set->conf); + + if (set->seat->kbd.xkb_state == NULL || + set->seat->kbd.xkb_keymap == NULL) + { + LOG_DBG("no XKB keymap"); + return; + } + + convert_key_bindings(set); + convert_search_bindings(set); + convert_url_bindings(set); + convert_mouse_bindings(set); + + set->public.selection_overrides = mods_to_mask( + set->seat, set->vmods, ALEN(set->vmods), + &set->conf->mouse.selection_override_modifiers); +} + +void +key_binding_load_keymap(struct key_binding_manager *mgr, + const struct seat *seat) +{ + tll_foreach(mgr->binding_sets, it) { + struct key_set *set = &it->item; + + if (set->seat == seat) { + initialize_vmod_mappings(set); + load_keymap(set); + } + } +} + +static void NOINLINE +key_bindings_destroy(key_binding_list_t *bindings) +{ + tll_foreach(*bindings, it) { + struct key_binding *bind = &it->item; + switch (bind->type) { + case KEY_BINDING: tll_free(it->item.k.key_codes); break; + case MOUSE_BINDING: break; + } + + tll_remove(*bindings, it); + } +} + +static void NOINLINE +unload_keymap(struct key_set *set) +{ + key_bindings_destroy(&set->public.key); + key_bindings_destroy(&set->public.search); + key_bindings_destroy(&set->public.url); + key_bindings_destroy(&set->public.mouse); + set->public.selection_overrides = 0; +} + +void +key_binding_unload_keymap(struct key_binding_manager *mgr, + const struct seat *seat) +{ + tll_foreach(mgr->binding_sets, it) { + struct key_set *set = &it->item; + if (set->seat != seat) + continue; + + LOG_DBG("unload keymap: set=%p, seat=%p, conf=%p", + (void *)set, (void *)seat, (void *)set->conf); + + unload_keymap(set); + } +} diff --git a/key-binding.h b/key-binding.h new file mode 100644 index 0000000..45702ee --- /dev/null +++ b/key-binding.h @@ -0,0 +1,200 @@ +#pragma once + +#include + +#include +#include + +#include "config.h" + +enum bind_action_normal { + BIND_ACTION_NONE, + BIND_ACTION_NOOP, + BIND_ACTION_SCROLLBACK_UP_PAGE, + BIND_ACTION_SCROLLBACK_UP_HALF_PAGE, + BIND_ACTION_SCROLLBACK_UP_LINE, + BIND_ACTION_SCROLLBACK_DOWN_PAGE, + BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE, + BIND_ACTION_SCROLLBACK_DOWN_LINE, + BIND_ACTION_SCROLLBACK_HOME, + BIND_ACTION_SCROLLBACK_END, + BIND_ACTION_CLIPBOARD_COPY, + BIND_ACTION_CLIPBOARD_PASTE, + BIND_ACTION_PRIMARY_PASTE, + BIND_ACTION_SEARCH_START, + BIND_ACTION_FONT_SIZE_UP, + BIND_ACTION_FONT_SIZE_DOWN, + BIND_ACTION_FONT_SIZE_RESET, + BIND_ACTION_SPAWN_TERMINAL, + BIND_ACTION_MINIMIZE, + BIND_ACTION_MAXIMIZE, + BIND_ACTION_FULLSCREEN, + BIND_ACTION_PIPE_SCROLLBACK, + BIND_ACTION_PIPE_VIEW, + BIND_ACTION_PIPE_SELECTED, + BIND_ACTION_PIPE_COMMAND_OUTPUT, + BIND_ACTION_SHOW_URLS_COPY, + BIND_ACTION_SHOW_URLS_LAUNCH, + BIND_ACTION_SHOW_URLS_PERSISTENT, + BIND_ACTION_TEXT_BINDING, + BIND_ACTION_PROMPT_PREV, + BIND_ACTION_PROMPT_NEXT, + BIND_ACTION_UNICODE_INPUT, + BIND_ACTION_QUIT, + BIND_ACTION_REGEX_LAUNCH, + BIND_ACTION_REGEX_COPY, + BIND_ACTION_THEME_SWITCH_1, + BIND_ACTION_THEME_SWITCH_2, + BIND_ACTION_THEME_SWITCH_DARK, + BIND_ACTION_THEME_SWITCH_LIGHT, + BIND_ACTION_THEME_TOGGLE, + + /* Tab actions */ + BIND_ACTION_TAB_NEW, + BIND_ACTION_TAB_CLOSE, + BIND_ACTION_TAB_NEXT, + BIND_ACTION_TAB_PREV, + BIND_ACTION_TAB_1, + BIND_ACTION_TAB_2, + BIND_ACTION_TAB_3, + BIND_ACTION_TAB_4, + BIND_ACTION_TAB_5, + BIND_ACTION_TAB_6, + BIND_ACTION_TAB_7, + BIND_ACTION_TAB_8, + BIND_ACTION_TAB_9, + BIND_ACTION_TAB_OVERVIEW, + + /* Mouse specific actions - i.e. they require a mouse coordinate */ + BIND_ACTION_SCROLLBACK_UP_MOUSE, + BIND_ACTION_SCROLLBACK_DOWN_MOUSE, + BIND_ACTION_SELECT_BEGIN, + BIND_ACTION_SELECT_BEGIN_BLOCK, + BIND_ACTION_SELECT_EXTEND, + BIND_ACTION_SELECT_EXTEND_CHAR_WISE, + BIND_ACTION_SELECT_WORD, + BIND_ACTION_SELECT_WORD_WS, + BIND_ACTION_SELECT_QUOTE, + BIND_ACTION_SELECT_ROW, + + BIND_ACTION_KEY_COUNT = BIND_ACTION_TAB_OVERVIEW + 1, + BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, +}; + +enum bind_action_search { + BIND_ACTION_SEARCH_NONE, + BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE, + BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE, + BIND_ACTION_SEARCH_SCROLLBACK_HOME, + BIND_ACTION_SEARCH_SCROLLBACK_END, + BIND_ACTION_SEARCH_CANCEL, + BIND_ACTION_SEARCH_COMMIT, + BIND_ACTION_SEARCH_FIND_PREV, + BIND_ACTION_SEARCH_FIND_NEXT, + BIND_ACTION_SEARCH_EDIT_LEFT, + BIND_ACTION_SEARCH_EDIT_LEFT_WORD, + BIND_ACTION_SEARCH_EDIT_RIGHT, + BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, + BIND_ACTION_SEARCH_EDIT_HOME, + BIND_ACTION_SEARCH_EDIT_END, + BIND_ACTION_SEARCH_DELETE_PREV, + BIND_ACTION_SEARCH_DELETE_PREV_WORD, + BIND_ACTION_SEARCH_DELETE_NEXT, + BIND_ACTION_SEARCH_DELETE_NEXT_WORD, + BIND_ACTION_SEARCH_DELETE_TO_START, + BIND_ACTION_SEARCH_DELETE_TO_END, + BIND_ACTION_SEARCH_EXTEND_CHAR, + BIND_ACTION_SEARCH_EXTEND_WORD, + BIND_ACTION_SEARCH_EXTEND_WORD_WS, + BIND_ACTION_SEARCH_EXTEND_LINE_DOWN, + BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR, + BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD, + BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS, + BIND_ACTION_SEARCH_EXTEND_LINE_UP, + BIND_ACTION_SEARCH_CLIPBOARD_PASTE, + BIND_ACTION_SEARCH_PRIMARY_PASTE, + BIND_ACTION_SEARCH_UNICODE_INPUT, + BIND_ACTION_SEARCH_TOGGLE_CASE, + BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD, + BIND_ACTION_SEARCH_TOGGLE_REGEX, + BIND_ACTION_SEARCH_HISTORY_PREV, + BIND_ACTION_SEARCH_HISTORY_NEXT, + BIND_ACTION_SEARCH_COMMIT_LINE, + BIND_ACTION_SEARCH_COUNT, +}; + +enum bind_action_url { + BIND_ACTION_URL_NONE, + BIND_ACTION_URL_CANCEL, + BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, + BIND_ACTION_URL_COUNT, +}; + +typedef tll(xkb_keycode_t) xkb_keycode_list_t; + +struct key_binding { + enum key_binding_type type; + + int action; /* enum bind_action_* */ + xkb_mod_mask_t mods; + + union { + struct { + xkb_keysym_t sym; + xkb_keycode_list_t key_codes; + } k; + + struct { + uint32_t button; + int count; + } m; + }; + + const struct binding_aux *aux; +}; +typedef tll(struct key_binding) key_binding_list_t; + +struct terminal; +struct seat; +struct wayland; + +struct key_binding_set { + key_binding_list_t key; + key_binding_list_t search; + key_binding_list_t url; + key_binding_list_t mouse; + xkb_mod_mask_t selection_overrides; +}; + +struct key_binding_manager; + +struct key_binding_manager *key_binding_manager_new(void); +void key_binding_manager_destroy(struct key_binding_manager *mgr); + +void key_binding_new_for_seat( + struct key_binding_manager *mgr, const struct seat *seat); + +void key_binding_new_for_conf( + struct key_binding_manager *mgr, const struct wayland *wayl, + const struct config *conf); + +/* Returns the set of key bindings associated with this seat/conf pair */ +struct key_binding_set *key_binding_for( + struct key_binding_manager *mgr, const struct config *conf, + const struct seat *seat); + +/* Remove all key bindings tied to the specified seat */ +void key_binding_remove_seat( + struct key_binding_manager *mgr, const struct seat *seat); + +void key_binding_unref( + struct key_binding_manager *mgr, const struct config *conf); + +void key_binding_load_keymap( + struct key_binding_manager *mgr, const struct seat *seat); +void key_binding_unload_keymap( + struct key_binding_manager *mgr, const struct seat *seat); diff --git a/keymap.h b/keymap.h new file mode 100644 index 0000000..008f0e1 --- /dev/null +++ b/keymap.h @@ -0,0 +1,414 @@ +#pragma once + +#include + +#include "terminal.h" + +enum modifier { + MOD_NONE = 0x0, + MOD_ANY = 0x1, + MOD_SHIFT = 0x2, + MOD_ALT = 0x4, + MOD_CTRL = 0x8, + MOD_META = 0x10, + MOD_MODIFY_OTHER_KEYS_STATE1 = 0x20, + MOD_MODIFY_OTHER_KEYS_STATE2 = 0x40, +}; + +struct key_data { + enum modifier modifiers; + enum cursor_keys cursor_keys_mode; + enum keypad_keys keypad_keys_mode; + const char *seq; +}; + +static const struct key_data key_escape[] = { + {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;27~"}, + {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\033"}, + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;27~"}, + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;27~"}, + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;27~"}, + {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;27~"}, + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;27~"}, + {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;27~"}, + {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;27~"}, + {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;27~"}, + {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;27~"}, + {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;27~"}, + {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;27~"}, + {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;27~"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;27~"}, + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033"}, +}; + +static const struct key_data key_return[] = { + {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;13~"}, + {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\r"}, + {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;13~"}, + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;13~"}, + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;13~"}, + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;13~"}, + {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;13~"}, + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;13~"}, + {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;13~"}, + {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;13~"}, + {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;13~"}, + {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;13~"}, + {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;13~"}, + {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;13~"}, + {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;13~"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;13~"}, + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\r"}, +}; + +/* Tab isn't covered by the regular "modifyOtherKeys" handling */ +static const struct key_data key_tab[] = { + {MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[Z"}, + {MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;9~"}, + {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\t"}, + {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;9~"}, + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;9~"}, + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;9~"}, + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;9~"}, + {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;9~"}, + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;9~"}, + {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;9~"}, + {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;9~"}, + {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;9~"}, + {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;9~"}, + {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;9~"}, + {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;9~"}, + {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;9~"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;9~"}, + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\t"}, +}; + +/* + * Shift+Tab produces ISO_Left_Tab + * + * However, all combos (except Shift+Tab) acts as if we pressed + * mods+shift+tab. + */ +static const struct key_data key_iso_left_tab[] = { + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;9~"}, + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;9~"}, + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;9~"}, + {MOD_SHIFT | MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;9~"}, + {MOD_SHIFT | MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;9~"}, + {MOD_SHIFT | MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;9~"}, + {MOD_SHIFT | MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;9~"}, + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[Z"}, +}; + +static const struct key_data key_backspace[] = { + {MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"}, + {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"}, + {MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"}, + {MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"}, + {MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"}, + {MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"}, + {MOD_META | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"}, + {MOD_META | MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"}, + {MOD_META | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"}, + {MOD_META | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"}, + {MOD_META | MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"}, + {MOD_META | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"}, + + {MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;127~"}, + {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;127~"}, + {MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;127~"}, + {MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;8~"}, + {MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;8~"}, + {MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;8~"}, + {MOD_META | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;127~"}, + {MOD_META | MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;127~"}, + {MOD_META | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;127~"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;127~"}, + {MOD_META | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;8~"}, + {MOD_META | MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;8~"}, + {MOD_META | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;8~"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;8~"}, + + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"}, + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"}, +}; + +#define DEFAULT_MODS_FOR_SINGLE(sym) \ + {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2"#sym}, \ + {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;3"#sym}, \ + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;4"#sym}, \ + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5"#sym}, \ + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;6"#sym}, \ + {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;7"#sym}, \ + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;8"#sym}, \ + {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;9"#sym}, \ + {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;10"#sym}, \ + {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;11"#sym}, \ + {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;12"#sym}, \ + {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;13"#sym}, \ + {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;14"#sym}, \ + {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;15"#sym}, \ + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;16"#sym} + +#define DEFAULT_MODS_FOR_TILDE(sym) \ + {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";2~"}, \ + {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";3~"}, \ + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";4~"}, \ + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";5~"}, \ + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";6~"}, \ + {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";7~"}, \ + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";8~"}, \ + {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";9~"}, \ + {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";10~"}, \ + {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";11~"}, \ + {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";12~"}, \ + {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";13~"}, \ + {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";14~"}, \ + {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";15~"}, \ + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";16~"} + + +static const struct key_data key_up[] = { + DEFAULT_MODS_FOR_SINGLE(A), + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OA"}, + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[A"}, + +}; + +static const struct key_data key_down[] = { + DEFAULT_MODS_FOR_SINGLE(B), + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OB"}, + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[B"}, +}; + +static const struct key_data key_right[] = { + DEFAULT_MODS_FOR_SINGLE(C), + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OC"}, + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[C"}, +}; + +static const struct key_data key_left[] = { + DEFAULT_MODS_FOR_SINGLE(D), + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OD"}, + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[D"}, +}; + +static const struct key_data key_home[] = { + DEFAULT_MODS_FOR_SINGLE(H), + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OH"}, + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[H"}, +}; + +static const struct key_data key_end[] = { + DEFAULT_MODS_FOR_SINGLE(F), + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OF"}, + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[F"}, +}; + +static const struct key_data key_insert[] = { + DEFAULT_MODS_FOR_TILDE(2), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[2~"}, +}; + +static const struct key_data key_delete[] = { + DEFAULT_MODS_FOR_TILDE(3), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[3~"}, +}; + +static const struct key_data key_pageup[] = { + DEFAULT_MODS_FOR_TILDE(5), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[5~"}, +}; + +static const struct key_data key_pagedown[] = { + DEFAULT_MODS_FOR_TILDE(6), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[6~"}, +}; + +static const struct key_data key_f1[] = { + DEFAULT_MODS_FOR_SINGLE(P), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OP"}, +}; + +static const struct key_data key_f2[] = { + DEFAULT_MODS_FOR_SINGLE(Q), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OQ"}, +}; + +static const struct key_data key_f3[] = { + DEFAULT_MODS_FOR_SINGLE(R), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OR"}, +}; + +static const struct key_data key_f4[] = { + DEFAULT_MODS_FOR_SINGLE(S), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OS"}, +}; + +static const struct key_data key_f5[] = { + DEFAULT_MODS_FOR_TILDE(15), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[15~"}, +}; + +static const struct key_data key_f6[] = { + DEFAULT_MODS_FOR_TILDE(17), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[17~"}, +}; +static const struct key_data key_f7[] = { + DEFAULT_MODS_FOR_TILDE(18), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[18~"}, +}; + +static const struct key_data key_f8[] = { + DEFAULT_MODS_FOR_TILDE(19), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[19~"}, +}; + +static const struct key_data key_f9[] = { + DEFAULT_MODS_FOR_TILDE(20), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[20~"}, +}; + +static const struct key_data key_f10[] = { + DEFAULT_MODS_FOR_TILDE(21), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[21~"}, +}; + +static const struct key_data key_f11[] = { + DEFAULT_MODS_FOR_TILDE(23), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[23~"}, +}; + +static const struct key_data key_f12[] = { + DEFAULT_MODS_FOR_TILDE(24), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[24~"}, +}; + +static const struct key_data key_f13[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2P"}}; +static const struct key_data key_f14[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2Q"}}; +static const struct key_data key_f15[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2R"}}; +static const struct key_data key_f16[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2S"}}; +static const struct key_data key_f17[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[15;2~"}}; +static const struct key_data key_f18[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[17;2~"}}; +static const struct key_data key_f19[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[18;2~"}}; +static const struct key_data key_f20[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[19;2~"}}; +static const struct key_data key_f21[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[20;2~"}}; +static const struct key_data key_f22[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[21;2~"}}; +static const struct key_data key_f23[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[23;2~"}}; +static const struct key_data key_f24[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[24;2~"}}; +static const struct key_data key_f25[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5P"}}; +static const struct key_data key_f26[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5Q"}}; +static const struct key_data key_f27[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5R"}}; +static const struct key_data key_f28[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5S"}}; +static const struct key_data key_f29[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[15;5~"}}; +static const struct key_data key_f30[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[17;5~"}}; +static const struct key_data key_f31[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[18;5~"}}; +static const struct key_data key_f32[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[19;5~"}}; +static const struct key_data key_f33[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[20;5~"}}; +static const struct key_data key_f34[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[21;5~"}}; +static const struct key_data key_f35[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[23;5~"}}; + +static const struct key_data key_kp_up[] = { + DEFAULT_MODS_FOR_SINGLE(A), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[A"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OA"}, +}; + +static const struct key_data key_kp_down[] = { + DEFAULT_MODS_FOR_SINGLE(B), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[B"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OB"}, +}; + +static const struct key_data key_kp_right[] = { + DEFAULT_MODS_FOR_SINGLE(C), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[C"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OC"}, +}; + +static const struct key_data key_kp_left[] = { + DEFAULT_MODS_FOR_SINGLE(D), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[D"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OD"}, +}; + +static const struct key_data key_kp_begin[] = { + DEFAULT_MODS_FOR_SINGLE(E), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[E"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OE"}, +}; + +static const struct key_data key_kp_home[] = { + DEFAULT_MODS_FOR_SINGLE(H), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[H"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OH"}, +}; + +static const struct key_data key_kp_end[] = { + DEFAULT_MODS_FOR_SINGLE(F), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[F"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OF"}, +}; + +static const struct key_data key_kp_insert[] = { + DEFAULT_MODS_FOR_TILDE(2), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[2~"}, +}; + +static const struct key_data key_kp_delete[] = { + DEFAULT_MODS_FOR_TILDE(3), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[3~"}, +}; + +static const struct key_data key_kp_pageup[] = { + DEFAULT_MODS_FOR_TILDE(5), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[5~"}, +}; + +static const struct key_data key_kp_pagedown[] = { + DEFAULT_MODS_FOR_TILDE(6), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[6~"}, +}; + +#undef DEFAULT_MODS_FOR_SINGLE +#undef DEFAULT_MODS_FOR_TILDE + +#define DEFAULT_MODS_FOR_KP(sym) \ + {MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O"#sym}, \ + {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O2"#sym}, \ + {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O3"#sym}, \ + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O4"#sym}, \ + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O5"#sym}, \ + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O6"#sym}, \ + {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O7"#sym}, \ + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O8"#sym}, \ + {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O9"#sym}, \ + {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O10"#sym}, \ + {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O11"#sym}, \ + {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O12"#sym}, \ + {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O13"#sym}, \ + {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O14"#sym}, \ + {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O15"#sym}, \ + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O16"#sym} + +static const struct key_data key_kp_enter[] = {DEFAULT_MODS_FOR_KP(M)}; +static const struct key_data key_kp_divide[] = {DEFAULT_MODS_FOR_KP(o)}; +static const struct key_data key_kp_multiply[] = {DEFAULT_MODS_FOR_KP(j)}; +static const struct key_data key_kp_subtract[] = {DEFAULT_MODS_FOR_KP(m)}; +static const struct key_data key_kp_add[] = {DEFAULT_MODS_FOR_KP(k)}; +static const struct key_data key_kp_separator[] = {DEFAULT_MODS_FOR_KP(l)}; +static const struct key_data key_kp_decimal[] = {DEFAULT_MODS_FOR_KP(n)}; +static const struct key_data key_kp_0[] = {DEFAULT_MODS_FOR_KP(p)}; +static const struct key_data key_kp_1[] = {DEFAULT_MODS_FOR_KP(q)}; +static const struct key_data key_kp_2[] = {DEFAULT_MODS_FOR_KP(r)}; +static const struct key_data key_kp_3[] = {DEFAULT_MODS_FOR_KP(s)}; +static const struct key_data key_kp_4[] = {DEFAULT_MODS_FOR_KP(t)}; +static const struct key_data key_kp_5[] = {DEFAULT_MODS_FOR_KP(u)}; +static const struct key_data key_kp_6[] = {DEFAULT_MODS_FOR_KP(v)}; +static const struct key_data key_kp_7[] = {DEFAULT_MODS_FOR_KP(w)}; +static const struct key_data key_kp_8[] = {DEFAULT_MODS_FOR_KP(x)}; +static const struct key_data key_kp_9[] = {DEFAULT_MODS_FOR_KP(y)}; + +#undef DEFAULT_MODS_FOR_KP diff --git a/kitty-keymap.h b/kitty-keymap.h new file mode 100644 index 0000000..3420d01 --- /dev/null +++ b/kitty-keymap.h @@ -0,0 +1,136 @@ +#pragma once + +#include +#include +#include + +struct kitty_key_data { + xkb_keysym_t sym; + uint16_t key; + uint8_t final:7; + bool is_modifier:1; +} __attribute__((packed)); + +_Static_assert(sizeof(struct kitty_key_data) == 7, "bad size"); + +/* Note! *Must* Be kept sorted (on 'sym') */ +static const struct kitty_key_data kitty_keymap[] = { + {XKB_KEY_ISO_Level3_Shift, 57453, 'u', true}, + {XKB_KEY_ISO_Level5_Shift, 57454, 'u', true}, + {XKB_KEY_ISO_Left_Tab, 9, 'u', false}, + + {XKB_KEY_BackSpace, 127, 'u', false}, + {XKB_KEY_Tab, 9, 'u', false}, + {XKB_KEY_Return, 13, 'u', false}, + {XKB_KEY_Pause, 57362, 'u', false}, + {XKB_KEY_Scroll_Lock, 57359, 'u', false}, + {XKB_KEY_Escape, 27, 'u', false}, + {XKB_KEY_Home, 1, 'H', false}, + {XKB_KEY_Left, 1, 'D', false}, + {XKB_KEY_Up, 1, 'A', false}, + {XKB_KEY_Right, 1, 'C', false}, + {XKB_KEY_Down, 1, 'B', false}, + {XKB_KEY_Prior, 5, '~', false}, + {XKB_KEY_Next, 6, '~', false}, + {XKB_KEY_End, 1, 'F', false}, + {XKB_KEY_Print, 57361, 'u', false}, + {XKB_KEY_Insert, 2, '~', false}, + {XKB_KEY_Menu, 57363, 'u', false}, + {XKB_KEY_Num_Lock, 57360, 'u', true}, + + {XKB_KEY_KP_Enter, 57414, 'u', false}, + {XKB_KEY_KP_Home, 57423, 'u', false}, + {XKB_KEY_KP_Left, 57417, 'u', false}, + {XKB_KEY_KP_Up, 57419, 'u', false}, + {XKB_KEY_KP_Right, 57418, 'u', false}, + {XKB_KEY_KP_Down, 57420, 'u', false}, + {XKB_KEY_KP_Prior, 57421, 'u', false}, + {XKB_KEY_KP_Next, 57422, 'u', false}, + {XKB_KEY_KP_End, 57424, 'u', false}, + {XKB_KEY_KP_Begin, 1, 'E', false}, + {XKB_KEY_KP_Insert, 57425, 'u', false}, + {XKB_KEY_KP_Delete, 57426, 'u', false}, + {XKB_KEY_KP_Multiply, 57411, 'u', false}, + {XKB_KEY_KP_Add, 57413, 'u', false}, + {XKB_KEY_KP_Separator, 57416, 'u', false}, + {XKB_KEY_KP_Subtract, 57412, 'u', false}, + {XKB_KEY_KP_Decimal, 57409, 'u', false}, + {XKB_KEY_KP_Divide, 57410, 'u', false}, + {XKB_KEY_KP_0, 57399, 'u', false}, + {XKB_KEY_KP_1, 57400, 'u', false}, + {XKB_KEY_KP_2, 57401, 'u', false}, + {XKB_KEY_KP_3, 57402, 'u', false}, + {XKB_KEY_KP_4, 57403, 'u', false}, + {XKB_KEY_KP_5, 57404, 'u', false}, + {XKB_KEY_KP_6, 57405, 'u', false}, + {XKB_KEY_KP_7, 57406, 'u', false}, + {XKB_KEY_KP_8, 57407, 'u', false}, + {XKB_KEY_KP_9, 57408, 'u', false}, + {XKB_KEY_KP_Equal, 57415, 'u', false}, + + {XKB_KEY_F1, 1, 'P', false}, + {XKB_KEY_F2, 1, 'Q', false}, + {XKB_KEY_F3, 13, '~', false}, + {XKB_KEY_F4, 1, 'S', false}, + {XKB_KEY_F5, 15, '~', false}, + {XKB_KEY_F6, 17, '~', false}, + {XKB_KEY_F7, 18, '~', false}, + {XKB_KEY_F8, 19, '~', false}, + {XKB_KEY_F9, 20, '~', false}, + {XKB_KEY_F10, 21, '~', false}, + {XKB_KEY_F11, 23, '~', false}, + {XKB_KEY_F12, 24, '~', false}, + {XKB_KEY_F13, 57376, 'u', false}, + {XKB_KEY_F14, 57377, 'u', false}, + {XKB_KEY_F15, 57378, 'u', false}, + {XKB_KEY_F16, 57379, 'u', false}, + {XKB_KEY_F17, 57380, 'u', false}, + {XKB_KEY_F18, 57381, 'u', false}, + {XKB_KEY_F19, 57382, 'u', false}, + {XKB_KEY_F20, 57383, 'u', false}, + {XKB_KEY_F21, 57384, 'u', false}, + {XKB_KEY_F22, 57385, 'u', false}, + {XKB_KEY_F23, 57386, 'u', false}, + {XKB_KEY_F24, 57387, 'u', false}, + {XKB_KEY_F25, 57388, 'u', false}, + {XKB_KEY_F26, 57389, 'u', false}, + {XKB_KEY_F27, 57390, 'u', false}, + {XKB_KEY_F28, 57391, 'u', false}, + {XKB_KEY_F29, 57392, 'u', false}, + {XKB_KEY_F30, 57393, 'u', false}, + {XKB_KEY_F31, 57394, 'u', false}, + {XKB_KEY_F32, 57395, 'u', false}, + {XKB_KEY_F33, 57396, 'u', false}, + {XKB_KEY_F34, 57397, 'u', false}, + {XKB_KEY_F35, 57398, 'u', false}, + + {XKB_KEY_Shift_L, 57441, 'u', true}, + {XKB_KEY_Shift_R, 57447, 'u', true}, + {XKB_KEY_Control_L, 57442, 'u', true}, + {XKB_KEY_Control_R, 57448, 'u', true}, + {XKB_KEY_Caps_Lock, 57358, 'u', true}, + {XKB_KEY_Meta_L, 57446, 'u', true}, + {XKB_KEY_Meta_R, 57452, 'u', true}, + {XKB_KEY_Alt_L, 57443, 'u', true}, + {XKB_KEY_Alt_R, 57449, 'u', true}, + {XKB_KEY_Super_L, 57444, 'u', true}, + {XKB_KEY_Super_R, 57450, 'u', true}, + {XKB_KEY_Hyper_L, 57445, 'u', true}, + {XKB_KEY_Hyper_R, 57451, 'u', true}, + + {XKB_KEY_Delete, 3, '~', false}, + + {XKB_KEY_XF86AudioLowerVolume, 57438, 'u', false}, + {XKB_KEY_XF86AudioMute, 57440, 'u', false}, + {XKB_KEY_XF86AudioRaiseVolume, 57439, 'u', false}, + {XKB_KEY_XF86AudioPlay, 57428, 'u', false}, + {XKB_KEY_XF86AudioStop, 57432, 'u', false}, + {XKB_KEY_XF86AudioPrev, 57436, 'u', false}, + {XKB_KEY_XF86AudioNext, 57435, 'u', false}, + {XKB_KEY_XF86AudioRecord, 57437, 'u', false}, + {XKB_KEY_XF86AudioPause, 57429, 'u', false}, + {XKB_KEY_XF86AudioRewind, 57434, 'u', false}, + {XKB_KEY_XF86AudioForward, 57433, 'u', false}, + //{XKB_KEY_XF86AudioPlayPause, 57430, 'u', false}, + //{XKB_KEY_XF86AudioReverse, 57431, 'u', false}, +}; diff --git a/log.c b/log.c new file mode 100644 index 0000000..ebf411e --- /dev/null +++ b/log.c @@ -0,0 +1,231 @@ +#include "log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "debug.h" +#include "util.h" +#include "xsnprintf.h" + +static bool colorize = false; +static bool do_syslog = false; +static enum log_class log_level = LOG_CLASS_NONE; + +static const struct { + const char name[8]; + const char log_prefix[7]; + uint8_t color; + int syslog_equivalent; +} log_level_map[] = { + [LOG_CLASS_NONE] = {"none", "none", 5, -1}, + [LOG_CLASS_ERROR] = {"error", " err", 31, LOG_ERR}, + [LOG_CLASS_WARNING] = {"warning", "warn", 33, LOG_WARNING}, + [LOG_CLASS_INFO] = {"info", "info", 97, LOG_INFO}, + [LOG_CLASS_DEBUG] = {"debug", " dbg", 36, LOG_DEBUG}, +}; + +void +log_init(enum log_colorize _colorize, bool _do_syslog, + enum log_facility syslog_facility, enum log_class _log_level) +{ + static const int facility_map[] = { + [LOG_FACILITY_USER] = LOG_USER, + [LOG_FACILITY_DAEMON] = LOG_DAEMON, + }; + + /* Don't use colors if NO_COLOR is defined and not empty */ + const char *no_color_str = getenv("NO_COLOR"); + const bool no_color = no_color_str != NULL && no_color_str[0] != '\0'; + + colorize = _colorize == LOG_COLORIZE_ALWAYS + || (_colorize == LOG_COLORIZE_AUTO + && !no_color && isatty(STDERR_FILENO)); + do_syslog = _do_syslog; + log_level = _log_level; + + int slvl = log_level_map[_log_level].syslog_equivalent; + if (slvl < 0) + do_syslog = false; + + if (do_syslog) { + openlog(NULL, /*LOG_PID*/0, facility_map[syslog_facility]); + + xassert(slvl >= 0); + setlogmask(LOG_UPTO(slvl)); + } +} + +void +log_deinit(void) +{ + if (do_syslog) + closelog(); +} + +static void +_log(enum log_class log_class, const char *module, const char *file, int lineno, + const char *fmt, int sys_errno, va_list va) +{ + xassert(log_class > LOG_CLASS_NONE); + xassert(log_class < ALEN(log_level_map)); + + if (log_class > log_level) + return; + + const char *prefix = log_level_map[log_class].log_prefix; + unsigned int class_clr = log_level_map[log_class].color; + + char clr[16]; + xsnprintf(clr, sizeof(clr), "\033[%um", class_clr); + fprintf(stderr, "%s%s%s: ", colorize ? clr : "", prefix, colorize ? "\033[0m" : ""); + + if (colorize) + fputs("\033[2m", stderr); + fprintf(stderr, "%s:%d: ", file, lineno); + if (colorize) + fputs("\033[0m", stderr); + + vfprintf(stderr, fmt, va); + + if (sys_errno != 0) + fprintf(stderr, ": %s", strerror(sys_errno)); + + fputc('\n', stderr); +} + +static void +_sys_log(enum log_class log_class, const char *module, + const char UNUSED *file, int UNUSED lineno, + const char *fmt, int sys_errno, va_list va) +{ + xassert(log_class > LOG_CLASS_NONE); + xassert(log_class < ALEN(log_level_map)); + + if (!do_syslog) + return; + + if (log_class > log_level) + return; + + /* Map our log level to syslog's level */ + int level = log_level_map[log_class].syslog_equivalent; + + char msg[4096]; + int n = vsnprintf(msg, sizeof(msg), fmt, va); + xassert(n >= 0); + + if (sys_errno != 0 && (size_t)n < sizeof(msg)) + snprintf(msg + n, sizeof(msg) - n, ": %s", strerror(sys_errno)); + + syslog(level, "%s: %s", module, msg); +} + +void +log_msg_va(enum log_class log_class, const char *module, + const char *file, int lineno, const char *fmt, va_list va) +{ + va_list va2; + va_copy(va2, va); + _log(log_class, module, file, lineno, fmt, 0, va); + _sys_log(log_class, module, file, lineno, fmt, 0, va2); + va_end(va2); +} + +void +log_msg(enum log_class log_class, const char *module, + const char *file, int lineno, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + log_msg_va(log_class, module, file, lineno, fmt, va); + va_end(va); +} + +void +log_errno_va(enum log_class log_class, const char *module, + const char *file, int lineno, + const char *fmt, va_list va) +{ + log_errno_provided_va(log_class, module, file, lineno, errno, fmt, va); +} + +void +log_errno(enum log_class log_class, const char *module, + const char *file, int lineno, + const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + log_errno_va(log_class, module, file, lineno, fmt, va); + va_end(va); +} + +void +log_errno_provided_va(enum log_class log_class, const char *module, + const char *file, int lineno, int errno_copy, + const char *fmt, va_list va) +{ + va_list va2; + va_copy(va2, va); + _log(log_class, module, file, lineno, fmt, errno_copy, va); + _sys_log(log_class, module, file, lineno, fmt, errno_copy, va2); + va_end(va2); +} + +void +log_errno_provided(enum log_class log_class, const char *module, + const char *file, int lineno, int errno_copy, + const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + log_errno_provided_va(log_class, module, file, lineno, errno_copy, fmt, va); + va_end(va); +} + +static size_t +map_len(void) +{ + size_t len = ALEN(log_level_map); +#ifndef _DEBUG + /* Exclude "debug" entry for non-debug builds */ + len--; +#endif + return len; +} + +int +log_level_from_string(const char *str) +{ + if (unlikely(str[0] == '\0')) + return -1; + + for (int i = 0, n = map_len(); i < n; i++) + if (streq(str, log_level_map[i].name)) + return i; + + return -1; +} + +const char * +log_level_string_hint(void) +{ + static char buf[64]; + if (buf[0] != '\0') + return buf; + + for (size_t i = 0, pos = 0, n = map_len(); i < n; i++) { + const char *entry = log_level_map[i].name; + const char *delim = (i + 1 < n) ? ", " : ""; + pos += xsnprintf(buf + pos, sizeof(buf) - pos, "'%s'%s", entry, delim); + } + + return buf; +} diff --git a/log.h b/log.h new file mode 100644 index 0000000..e499cf2 --- /dev/null +++ b/log.h @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include "macros.h" + +enum log_colorize { LOG_COLORIZE_NEVER, LOG_COLORIZE_ALWAYS, LOG_COLORIZE_AUTO }; +enum log_facility { LOG_FACILITY_USER, LOG_FACILITY_DAEMON }; + +enum log_class { + LOG_CLASS_NONE, + LOG_CLASS_ERROR, + LOG_CLASS_WARNING, + LOG_CLASS_INFO, + LOG_CLASS_DEBUG, + LOG_CLASS_COUNT, +}; + +void log_init(enum log_colorize colorize, bool do_syslog, + enum log_facility syslog_facility, enum log_class log_level); +void log_deinit(void); + +void log_msg( + enum log_class log_class, const char *module, + const char *file, int lineno, + const char *fmt, ...) PRINTF(5); + +void log_errno( + enum log_class log_class, const char *module, + const char *file, int lineno, + const char *fmt, ...) PRINTF(5); + +void log_errno_provided( + enum log_class log_class, const char *module, + const char *file, int lineno, int _errno, + const char *fmt, ...) PRINTF(6); + +void log_msg_va( + enum log_class log_class, const char *module, + const char *file, int lineno, const char *fmt, va_list va) VPRINTF(5); +void log_errno_va( + enum log_class log_class, const char *module, + const char *file, int lineno, + const char *fmt, va_list va) VPRINTF(5); +void log_errno_provided_va( + enum log_class log_class, const char *module, + const char *file, int lineno, int _errno, + const char *fmt, va_list va) VPRINTF(6); + + +int log_level_from_string(const char *str); +const char *log_level_string_hint(void); + +#define LOG_ERR(...) \ + log_msg(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) +#define LOG_ERRNO(...) \ + log_errno(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) +#define LOG_ERRNO_P(_errno, ...) \ + log_errno_provided(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, \ + _errno, __VA_ARGS__) +#define LOG_WARN(...) \ + log_msg(LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) +#define LOG_INFO(...) \ + log_msg(LOG_CLASS_INFO, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + #define LOG_DBG(...) \ + log_msg(LOG_CLASS_DEBUG, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) +#else + #define LOG_DBG(...) +#endif diff --git a/macros.h b/macros.h new file mode 100644 index 0000000..2ecda60 --- /dev/null +++ b/macros.h @@ -0,0 +1,211 @@ +#pragma once + +#define PASTE(a, b) a##b +#define XPASTE(a, b) PASTE(a, b) +#define STRLEN(str) (sizeof("" str "") - 1) +#define DO_PRAGMA(x) _Pragma(#x) +#define VERCMP(x, y, cx, cy) ((cx > x) || ((cx == x) && (cy >= y))) + +#if defined(__GNUC__) && defined(__GNUC_MINOR__) + #define GNUC_AT_LEAST(x, y) VERCMP(x, y, __GNUC__, __GNUC_MINOR__) +#else + #define GNUC_AT_LEAST(x, y) 0 +#endif + +#if defined(__clang_major__) && defined(__clang_minor__) + #define CLANG_AT_LEAST(x, y) VERCMP(x, y, __clang_major__, __clang_minor__) +#else + #define CLANG_AT_LEAST(x, y) 0 +#endif + +#ifdef __has_attribute + #define HAS_ATTRIBUTE(x) __has_attribute(x) +#else + #define HAS_ATTRIBUTE(x) 0 +#endif + +#ifdef __has_builtin + #define HAS_BUILTIN(x) __has_builtin(x) +#else + #define HAS_BUILTIN(x) 0 +#endif + +#ifdef __has_include + #define HAS_INCLUDE(x) __has_include(x) +#else + #define HAS_INCLUDE(x) 0 +#endif + +#ifdef __has_feature + #define HAS_FEATURE(x) __has_feature(x) +#else + #define HAS_FEATURE(x) 0 +#endif + +// __has_extension() is a Clang macro used to determine if a feature is +// available even if not standardized in the current "-std" mode. +#ifdef __has_extension + #define HAS_EXTENSION(x) __has_extension(x) +#else + // Clang versions prior to 3.0 only supported __has_feature() + #define HAS_EXTENSION(x) HAS_FEATURE(x) +#endif + +#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(unused) || defined(__TINYC__) + #define UNUSED __attribute__((__unused__)) +#else + #define UNUSED +#endif + +#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(const) + #define CONST __attribute__((__const__)) +#else + #define CONST +#endif + +#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(malloc) + #define MALLOC __attribute__((__malloc__)) +#else + #define MALLOC +#endif + +#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(constructor) + #define CONSTRUCTOR __attribute__((__constructor__)) + #define HAVE_ATTR_CONSTRUCTOR 1 +#else + #define CONSTRUCTOR +#endif + +#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(destructor) + #define DESTRUCTOR __attribute__((__destructor__)) +#else + #define DESTRUCTOR +#endif + +#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(format) + #define PRINTF(x) __attribute__((__format__(__printf__, (x), (x + 1)))) + #define VPRINTF(x) __attribute__((__format__(__printf__, (x), 0))) +#else + #define PRINTF(x) + #define VPRINTF(x) +#endif + +#if (GNUC_AT_LEAST(3, 0) || HAS_BUILTIN(__builtin_expect)) && defined(__OPTIMIZE__) + #define likely(x) __builtin_expect(!!(x), 1) + #define unlikely(x) __builtin_expect(!!(x), 0) +#else + #define likely(x) (x) + #define unlikely(x) (x) +#endif + +#if GNUC_AT_LEAST(3, 1) || HAS_ATTRIBUTE(noinline) + #define NOINLINE __attribute__((__noinline__)) +#else + #define NOINLINE +#endif + +#if GNUC_AT_LEAST(3, 1) || HAS_ATTRIBUTE(always_inline) + #define ALWAYS_INLINE __attribute__((__always_inline__)) +#else + #define ALWAYS_INLINE +#endif + +#if GNUC_AT_LEAST(3, 3) || HAS_ATTRIBUTE(nonnull) + #define NONNULL_ARGS __attribute__((__nonnull__)) + #define NONNULL_ARG(...) __attribute__((__nonnull__(__VA_ARGS__))) +#else + #define NONNULL_ARGS + #define NONNULL_ARG(...) +#endif + +#if GNUC_AT_LEAST(3, 4) || HAS_ATTRIBUTE(warn_unused_result) + #define WARN_UNUSED_RESULT __attribute__((__warn_unused_result__)) +#else + #define WARN_UNUSED_RESULT +#endif + +#if GNUC_AT_LEAST(4, 1) || HAS_ATTRIBUTE(flatten) + #define FLATTEN __attribute__((__flatten__)) +#else + #define FLATTEN +#endif + +#if GNUC_AT_LEAST(4, 3) || HAS_ATTRIBUTE(hot) + #define HOT __attribute__((__hot__)) +#else + #define HOT +#endif + +#if GNUC_AT_LEAST(4, 3) || HAS_ATTRIBUTE(cold) + #define COLD __attribute__((__cold__)) +#else + #define COLD +#endif + +#if GNUC_AT_LEAST(4, 5) || HAS_BUILTIN(__builtin_unreachable) + #define UNREACHABLE() __builtin_unreachable() +#else + #define UNREACHABLE() +#endif + +#if GNUC_AT_LEAST(5, 0) || HAS_ATTRIBUTE(returns_nonnull) + #define RETURNS_NONNULL __attribute__((__returns_nonnull__)) +#else + #define RETURNS_NONNULL +#endif + +#if HAS_ATTRIBUTE(diagnose_if) + #define DIAGNOSE_IF(x) __attribute__((diagnose_if((x), (#x), "error"))) +#else + #define DIAGNOSE_IF(x) +#endif + +#define XMALLOC MALLOC RETURNS_NONNULL WARN_UNUSED_RESULT +#define XSTRDUP XMALLOC NONNULL_ARGS + +#if __STDC_VERSION__ >= 201112L + #define noreturn _Noreturn +#elif GNUC_AT_LEAST(3, 0) + #define noreturn __attribute__((__noreturn__)) +#else + #define noreturn +#endif + +#if CLANG_AT_LEAST(3, 6) + #define UNROLL_LOOP(n) DO_PRAGMA(clang loop unroll_count(n)) +#elif GNUC_AT_LEAST(8, 0) + #define UNROLL_LOOP(n) DO_PRAGMA(GCC unroll (n)) +#else + #define UNROLL_LOOP(n) +#endif + +#ifdef __COUNTER__ + // Supported by GCC 4.3+ and Clang + #define COUNTER_ __COUNTER__ +#else + #define COUNTER_ __LINE__ +#endif + +#if defined(_DEBUG) && defined(HAVE_ATTR_CONSTRUCTOR) + #define UNITTEST static void CONSTRUCTOR XPASTE(unittest_, COUNTER_)(void) +#else + #define UNITTEST static void UNUSED XPASTE(unittest_, COUNTER_)(void) +#endif + +#ifdef __clang__ + #define IGNORE_WARNING(wflag) \ + DO_PRAGMA(clang diagnostic push) \ + DO_PRAGMA(clang diagnostic ignored "-Wunknown-pragmas") \ + DO_PRAGMA(clang diagnostic ignored "-Wunknown-warning-option") \ + DO_PRAGMA(clang diagnostic ignored wflag) + #define UNIGNORE_WARNINGS DO_PRAGMA(clang diagnostic pop) +#elif GNUC_AT_LEAST(4, 6) + #define IGNORE_WARNING(wflag) \ + DO_PRAGMA(GCC diagnostic push) \ + DO_PRAGMA(GCC diagnostic ignored "-Wpragmas") \ + DO_PRAGMA(GCC diagnostic ignored wflag) + #define UNIGNORE_WARNINGS DO_PRAGMA(GCC diagnostic pop) +#else + #define IGNORE_WARNING(wflag) + #define UNIGNORE_WARNINGS +#endif diff --git a/main.c b/main.c new file mode 100644 index 0000000..9db77d0 --- /dev/null +++ b/main.c @@ -0,0 +1,726 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#define LOG_MODULE "main" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "config.h" +#include "fdm.h" +#include "foot-features.h" +#include "key-binding.h" +#include "macros.h" +#include "reaper.h" +#include "render.h" +#include "server.h" +#include "shm.h" +#include "terminal.h" +#include "util.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +#if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__ + #error "char32_t does not use UTF-32" +#endif + +static bool +fdm_sigint(struct fdm *fdm, int signo, void *data) +{ + *(volatile sig_atomic_t *)data = true; + return true; +} + +struct sigusr_context { + struct terminal *term; + struct server *server; +}; + +static bool +fdm_sigusr(struct fdm *fdm, int signo, void *data) +{ + xassert(signo == SIGUSR1 || signo == SIGUSR2); + + struct sigusr_context *ctx = data; + + if (ctx->server != NULL) { + if (signo == SIGUSR1) + server_global_theme_switch_to_dark(ctx->server); + else + server_global_theme_switch_to_light(ctx->server); + } else { + if (signo == SIGUSR1) + term_theme_switch_to_dark(ctx->term); + else + term_theme_switch_to_light(ctx->term); + } + + return true; +} + +static void +print_usage(const char *prog_name) +{ + static const char options[] = + "\nOptions:\n" + " -c,--config=PATH load configuration from PATH ($XDG_CONFIG_HOME/foot/foot.ini)\n" + " -C,--check-config verify configuration, exit with 0 if ok, otherwise exit with 1\n" + " -o,--override=[section.]key=value override configuration option\n" + " -f,--font=FONT comma separated list of fonts in fontconfig format (monospace)\n" + " -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n" + " -T,--title=TITLE initial window title (foot)\n" + " -a,--app-id=ID window application ID (foot)\n" + " --toplevel-tag=TAG set a custom toplevel tag\n" + " -m,--maximized start in maximized mode\n" + " -F,--fullscreen start in fullscreen mode\n" + " -L,--login-shell start shell as a login shell\n" + " --pty=PATH display an existing PTY instead of creating one\n" + " -D,--working-directory=DIR directory to start in (CWD)\n" + " -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n" + " -W,--window-size-chars=WIDTHxHEIGHT initial width and height, in characters\n" + " -s,--server[=PATH] run as a server (use 'footclient' to start terminals).\n" + " Without PATH, $XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock will be used.\n" + " -H,--hold remain open after child process exits\n" + " -p,--print-pid=FILE|FD print PID to file or FD (only applicable in server mode)\n" + " -d,--log-level={info|warning|error|none} log level (warning)\n" + " -l,--log-colorize=[{never|always|auto}] enable/disable colorization of log output on stderr\n" + " -S,--log-no-syslog disable syslog logging (only applicable in server mode)\n" + " -v,--version show the version number and quit\n" + " -e ignored (for compatibility with xterm -e)\n"; + + printf("Usage: %s [OPTIONS...]\n", prog_name); + printf("Usage: %s [OPTIONS...] command [ARGS...]\n", prog_name); + puts(options); +} + +bool +locale_is_utf8(void) +{ + static const char u8[] = u8"ö"; + xassert(strlen(u8) == 2); + + char32_t w; + if (mbrtoc32(&w, u8, 2, &(mbstate_t){0}) != 2) + return false; + + return w == U'ö'; +} + +struct shutdown_context { + struct terminal **term; + int exit_code; +}; + +static void +term_shutdown_cb(void *data, int exit_code) +{ + struct shutdown_context *ctx = data; + *ctx->term = NULL; + ctx->exit_code = exit_code; +} + +static bool +print_pid(const char *pid_file, bool *unlink_at_exit) +{ + LOG_DBG("printing PID to %s", pid_file); + + errno = 0; + char *end; + int pid_fd = strtoul(pid_file, &end, 10); + + if (errno != 0 || *end != '\0') { + if ((pid_fd = open(pid_file, + O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) < 0) { + LOG_ERRNO("%s: failed to open", pid_file); + return false; + } else + *unlink_at_exit = true; + } + + if (pid_fd >= 0) { + char pid[32]; + size_t n = xsnprintf(pid, sizeof(pid), "%u\n", getpid()); + + ssize_t bytes = write(pid_fd, pid, n); + close(pid_fd); + + if (bytes < 0) { + LOG_ERRNO("failed to write PID to FD=%u", pid_fd); + return false; + } + + LOG_DBG("wrote %zd bytes to FD=%d", bytes, pid_fd); + return true; + } else + return false; +} + +static void +sanitize_signals(void) +{ + sigset_t mask; + sigemptyset(&mask); + sigprocmask(SIG_SETMASK, &mask, NULL); + + struct sigaction dfl = {.sa_handler = SIG_DFL}; + sigemptyset(&dfl.sa_mask); + + for (int i = 1; i < SIGRTMAX; i++) + sigaction(i, &dfl, NULL); +} + +enum { + PTY_OPTION = CHAR_MAX + 1, + TOPLEVEL_TAG_OPTION = CHAR_MAX + 2, +}; + +int +main(int argc, char *const *argv) +{ + /* Custom exit code, to enable users to differentiate between foot + * itself failing, and the client application failing */ + static const int foot_exit_failure = -26; + int ret = foot_exit_failure; + + sanitize_signals(); + + /* XDG startup notifications */ + const char *token = getenv("XDG_ACTIVATION_TOKEN"); + unsetenv("XDG_ACTIVATION_TOKEN"); + + /* Startup notifications; we don't support it, but must ensure we + * don't pass this on to programs launched by us */ + unsetenv("DESKTOP_STARTUP_ID"); + + const char *const prog_name = argc > 0 ? argv[0] : ""; + + static const struct option longopts[] = { + {"config", required_argument, NULL, 'c'}, + {"check-config", no_argument, NULL, 'C'}, + {"override", required_argument, NULL, 'o'}, + {"term", required_argument, NULL, 't'}, + {"title", required_argument, NULL, 'T'}, + {"app-id", required_argument, NULL, 'a'}, + {"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION}, + {"login-shell", no_argument, NULL, 'L'}, + {"working-directory", required_argument, NULL, 'D'}, + {"font", required_argument, NULL, 'f'}, + {"window-size-pixels", required_argument, NULL, 'w'}, + {"window-size-chars", required_argument, NULL, 'W'}, + {"server", optional_argument, NULL, 's'}, + {"hold", no_argument, NULL, 'H'}, + {"maximized", no_argument, NULL, 'm'}, + {"fullscreen", no_argument, NULL, 'F'}, + {"presentation-timings", no_argument, NULL, 'P'}, /* Undocumented */ + {"pty", required_argument, NULL, PTY_OPTION}, + {"print-pid", required_argument, NULL, 'p'}, + {"log-level", required_argument, NULL, 'd'}, + {"log-colorize", optional_argument, NULL, 'l'}, + {"log-no-syslog", no_argument, NULL, 'S'}, + {"version", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {NULL, no_argument, NULL, 0}, + }; + + bool check_config = false; + const char *conf_path = NULL; + const char *custom_cwd = NULL; + const char *pty_path = NULL; + bool as_server = false; + const char *conf_server_socket_path = NULL; + bool presentation_timings = false; + bool hold = false; + bool unlink_pid_file = false; + const char *pid_file = NULL; + enum log_class log_level = LOG_CLASS_WARNING; + enum log_colorize log_colorize = LOG_COLORIZE_AUTO; + bool log_syslog = true; + user_notifications_t user_notifications = tll_init(); + config_override_t overrides = tll_init(); + + while (true) { + int c = getopt_long(argc, argv, "+c:Co:t:T:a:LD:f:w:W:s::HmFPp:d:l::Sveh", longopts, NULL); + + if (c == -1) + break; + + switch (c) { + case 'c': + conf_path = optarg; + break; + + case 'C': + check_config = true; + break; + + case 'o': + tll_push_back(overrides, xstrdup(optarg)); + break; + + case 't': + tll_push_back(overrides, xstrjoin("term=", optarg)); + break; + + case 'L': + tll_push_back(overrides, xstrdup("login-shell=yes")); + break; + + case 'T': + tll_push_back(overrides, xstrjoin("title=", optarg)); + break; + + case 'a': + tll_push_back(overrides, xstrjoin("app-id=", optarg)); + break; + + case TOPLEVEL_TAG_OPTION: + tll_push_back(overrides, xstrjoin("toplevel-tag=", optarg)); + break; + + case 'D': { + struct stat st; + if (stat(optarg, &st) < 0 || !(st.st_mode & S_IFDIR)) { + fprintf(stderr, "error: %s: not a directory\n", optarg); + return ret; + } + custom_cwd = optarg; + break; + } + + case 'f': { + char *font_override = xstrjoin("font=", optarg); + tll_push_back(overrides, font_override); + break; + } + + case 'w': { + unsigned width, height; + if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { + fprintf(stderr, "error: invalid window-size-pixels: %s\n", optarg); + return ret; + } + + tll_push_back( + overrides, xasprintf("initial-window-size-pixels=%ux%u", + width, height)); + break; + } + + case 'W': { + unsigned width, height; + if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { + fprintf(stderr, "error: invalid window-size-chars: %s\n", optarg); + return ret; + } + + tll_push_back( + overrides, xasprintf("initial-window-size-chars=%ux%u", + width, height)); + break; + } + + case 's': + as_server = true; + if (optarg != NULL) + conf_server_socket_path = optarg; + break; + + case PTY_OPTION: + pty_path = optarg; + break; + + case 'P': + presentation_timings = true; + break; + + case 'H': + hold = true; + break; + + case 'm': + tll_push_back(overrides, xstrdup("initial-window-mode=maximized")); + break; + + case 'F': + tll_push_back(overrides, xstrdup("initial-window-mode=fullscreen")); + break; + + case 'p': + pid_file = optarg; + break; + + case 'd': { + int lvl = log_level_from_string(optarg); + if (unlikely(lvl < 0)) { + fprintf( + stderr, + "-d,--log-level: %s: argument must be one of %s\n", + optarg, + log_level_string_hint()); + return ret; + } + log_level = lvl; + break; + } + + case 'l': + if (optarg == NULL || streq(optarg, "auto")) + log_colorize = LOG_COLORIZE_AUTO; + else if (streq(optarg, "never")) + log_colorize = LOG_COLORIZE_NEVER; + else if (streq(optarg, "always")) + log_colorize = LOG_COLORIZE_ALWAYS; + else { + fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg); + return ret; + } + break; + + case 'S': + log_syslog = false; + break; + + case 'v': + print_version_and_features("foot "); + return EXIT_SUCCESS; + + case 'h': + print_usage(prog_name); + return EXIT_SUCCESS; + + case 'e': + break; + + case '?': + return ret; + } + } + + if (as_server && pty_path) { + fputs("error: --pty is incompatible with server mode\n", stderr); + return ret; + } + + log_init(log_colorize, as_server && log_syslog, + as_server ? LOG_FACILITY_DAEMON : LOG_FACILITY_USER, log_level); + + if (argc > 0) { + argc -= optind; + argv += optind; + } + + LOG_INFO("%s", version_and_features); + + { + struct utsname name; + if (uname(&name) < 0) + LOG_ERRNO("uname() failed"); + else + LOG_INFO("arch: %s %s/%zu-bit", + name.sysname, name.machine, sizeof(void *) * 8); + } + + srand(time(NULL)); + + const char *locale = setlocale(LC_CTYPE, ""); + if (locale == NULL) { + /* + * If the user has configured an invalid locale, or a name of a locale + * that does not exist on this system, then the above call may return + * NULL. We should just continue with the fallback method below. + */ + LOG_ERR("setlocale() failed. The most common cause is that the " + "configured locale is not available, or has been misspelled"); + } + + LOG_INFO("locale: %s", locale != NULL ? locale : ""); + + bool bad_locale = locale == NULL || !locale_is_utf8(); + if (bad_locale) { + static const char fallback_locales[][12] = { + "C.UTF-8", + "en_US.UTF-8", + }; + char *saved_locale = locale != NULL ? xstrdup(locale) : NULL; + + /* + * Try to force an UTF-8 locale. If we succeed, launch the + * user's shell as usual, but add a user-notification saying + * the locale has been changed. + */ + for (size_t i = 0; i < ALEN(fallback_locales); i++) { + const char *const fallback_locale = fallback_locales[i]; + + if (setlocale(LC_CTYPE, fallback_locale) != NULL) { + if (saved_locale != NULL) { + LOG_WARN( + "'%s' is not a UTF-8 locale, falling back to '%s'", + saved_locale, fallback_locale); + + user_notification_add_fmt( + &user_notifications, USER_NOTIFICATION_WARNING, + "'%s' is not a UTF-8 locale, falling back to '%s'", + saved_locale, fallback_locale); + + } else { + LOG_WARN( + "invalid locale, falling back to '%s'", fallback_locale); + user_notification_add_fmt( + &user_notifications, USER_NOTIFICATION_WARNING, + "invalid locale, falling back to '%s'", fallback_locale); + } + + bad_locale = false; + break; + } + } + + if (bad_locale) { + if (saved_locale != NULL) { + LOG_ERR( + "'%s' is not a UTF-8 locale, and failed to find a fallback", + saved_locale); + + user_notification_add_fmt( + &user_notifications, USER_NOTIFICATION_ERROR, + "'%s' is not a UTF-8 locale, and failed to find a fallback", + saved_locale); + } else { + LOG_ERR("invalid locale, and failed to find a fallback"); + + user_notification_add_fmt( + &user_notifications, USER_NOTIFICATION_ERROR, + "invalid locale, and failed to find a fallback"); + } + } + free(saved_locale); + } + + struct config conf = {NULL}; + bool conf_successful = config_load( + &conf, conf_path, &user_notifications, &overrides, check_config, as_server); + + tll_free_and_free(overrides, free); + if (!conf_successful) { + config_free(&conf); + return ret; + } + + if (check_config) { + config_free(&conf); + return EXIT_SUCCESS; + } + + _Static_assert((int)LOG_CLASS_ERROR == (int)FCFT_LOG_CLASS_ERROR, + "fcft log level enum offset"); + _Static_assert((int)LOG_COLORIZE_ALWAYS == (int)FCFT_LOG_COLORIZE_ALWAYS, + "fcft colorize enum mismatch"); + fcft_init( + (enum fcft_log_colorize)log_colorize, + as_server && log_syslog, + (enum fcft_log_class)log_level); + + if (conf_server_socket_path != NULL) { + free(conf.server_socket_path); + conf.server_socket_path = xstrdup(conf_server_socket_path); + } + conf.presentation_timings = presentation_timings; + conf.hold_at_exit = hold; + + if (conf.tweak.font_monospace_warn && conf.fonts[0].count > 0) { + check_if_font_is_monospaced( + conf.fonts[0].arr[0].pattern, &conf.notifications); + } + + + if (bad_locale) { + static char *const bad_locale_fake_argv[] = {"/bin/sh", "-c", "", NULL}; + argc = 1; + argv = bad_locale_fake_argv; + conf.hold_at_exit = true; + } + + struct fdm *fdm = NULL; + struct reaper *reaper = NULL; + struct key_binding_manager *key_binding_manager = NULL; + struct wayland *wayl = NULL; + struct renderer *renderer = NULL; + struct terminal *term = NULL; + struct server *server = NULL; + struct shutdown_context shutdown_ctx = {.term = &term, .exit_code = foot_exit_failure}; + + const char *cwd = custom_cwd; + char *_cwd = NULL; + + if (cwd == NULL) { + size_t buf_len = 1024; + do { + _cwd = xrealloc(_cwd, buf_len); + errno = 0; + if (getcwd(_cwd, buf_len) == NULL && errno != ERANGE) { + LOG_ERRNO("failed to get current working directory"); + goto out; + } + buf_len *= 2; + } while (errno == ERANGE); + cwd = _cwd; + } + + const char *pwd = getenv("PWD"); + if (pwd != NULL) { + char *resolved_path_cwd = realpath(cwd, NULL); + char *resolved_path_pwd = realpath(pwd, NULL); + + if (resolved_path_cwd != NULL && + resolved_path_pwd != NULL && + streq(resolved_path_cwd, resolved_path_pwd)) + { + /* + * The resolved path of $PWD matches the resolved path of + * the *actual* working directory - use $PWD. + * + * This makes a difference when $PWD refers to a symlink. + */ + cwd = pwd; + } + + free(resolved_path_cwd); + free(resolved_path_pwd); + } + + shm_set_max_pool_size(conf.tweak.max_shm_pool_size); + shm_set_min_stride_alignment(conf.tweak.min_stride_alignment); + + if ((fdm = fdm_init()) == NULL) + goto out; + + if ((reaper = reaper_init(fdm)) == NULL) + goto out; + + if ((key_binding_manager = key_binding_manager_new()) == NULL) + goto out; + + if ((wayl = wayl_init( + fdm, key_binding_manager, conf.presentation_timings)) == NULL) + { + goto out; + } + + if ((renderer = render_init(fdm, wayl)) == NULL) + goto out; + + if (!as_server && (term = term_init( + &conf, fdm, reaper, wayl, "foot", cwd, token, pty_path, + argc, argv, NULL, + &term_shutdown_cb, &shutdown_ctx)) == NULL) { + goto out; + } + free(_cwd); + _cwd = NULL; + + if (as_server && (server = server_init(&conf, fdm, reaper, wayl)) == NULL) + goto out; + + volatile sig_atomic_t aborted = false; + if (!fdm_signal_add(fdm, SIGINT, &fdm_sigint, (void *)&aborted) || + !fdm_signal_add(fdm, SIGTERM, &fdm_sigint, (void *)&aborted)) + { + goto out; + } + + struct sigusr_context sigusr_context = { + .term = term, + .server = server, + }; + + if (!fdm_signal_add(fdm, SIGUSR1, &fdm_sigusr, &sigusr_context) || + !fdm_signal_add(fdm, SIGUSR2, &fdm_sigusr, &sigusr_context)) + { + goto out; + } + + struct sigaction sig_ign = {.sa_handler = SIG_IGN}; + sigemptyset(&sig_ign.sa_mask); + if (sigaction(SIGHUP, &sig_ign, NULL) < 0 || + sigaction(SIGPIPE, &sig_ign, NULL) < 0) + { + LOG_ERRNO("failed to ignore SIGHUP+SIGPIPE"); + goto out; + } + + if (as_server) + LOG_INFO("running as server; launch terminals by running footclient"); + + if (as_server && pid_file != NULL) { + if (!print_pid(pid_file, &unlink_pid_file)) + goto out; + } + + ret = EXIT_SUCCESS; + while (likely(!aborted && (as_server || tll_length(wayl->terms) > 0))) { + if (unlikely(!fdm_poll(fdm))) { + ret = foot_exit_failure; + break; + } + } + +out: + free(_cwd); + server_destroy(server); + term_destroy(term); + + shm_fini(); + render_destroy(renderer); + wayl_destroy(wayl); + key_binding_manager_destroy(key_binding_manager); + reaper_destroy(reaper); + fdm_signal_del(fdm, SIGUSR1); + fdm_signal_del(fdm, SIGUSR2); + fdm_signal_del(fdm, SIGTERM); + fdm_signal_del(fdm, SIGINT); + fdm_destroy(fdm); + + config_free(&conf); + + if (unlink_pid_file) + unlink(pid_file); + + LOG_INFO("goodbye"); + fcft_fini(); + log_deinit(); + return ret == EXIT_SUCCESS && !as_server ? shutdown_ctx.exit_code : ret; +} + +UNITTEST +{ + char *s = xstrjoin("foo", "bar"); + xassert(streq(s, "foobar")); + free(s); + + s = xstrjoin3("foo", " ", "bar"); + xassert(streq(s, "foo bar")); + free(s); + + s = xstrjoin3("foo", ",", "bar"); + xassert(streq(s, "foo,bar")); + free(s); + + s = xstrjoin3("foo", "bar", "baz"); + xassert(streq(s, "foobarbaz")); + free(s); +} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..a0e602b --- /dev/null +++ b/meson.build @@ -0,0 +1,454 @@ +project('foot', 'c', + version: '1.26.1', + license: 'MIT', + meson_version: '>=0.59.0', + default_options: [ + 'c_std=c11', + 'warning_level=1', + 'werror=true', + 'b_ndebug=if-release']) + +is_debug_build = get_option('buildtype').startswith('debug') + +cc = meson.get_compiler('c') + +# Newer clang versions warns when using __COUNTER__ without -std=c2y +if cc.has_argument('-Wc2y-extensions') + add_project_arguments('-Wno-c2y-extensions', language: 'c') +endif + +if cc.has_function('memfd_create', + args: ['-D_GNU_SOURCE'], + prefix: '#include ') + add_project_arguments('-DMEMFD_CREATE', language: 'c') +endif + +# Missing on DragonFly, FreeBSD < 14.1 +if cc.has_function('execvpe', + args: ['-D_GNU_SOURCE'], + prefix: '#include ') + add_project_arguments('-DEXECVPE', language: 'c') +endif + +if cc.has_function('sigabbrev_np', + args: ['-D_GNU_SOURCE'], + prefix: '#include ') + add_project_arguments('-DSIGABBREV_NP', language: 'c') +endif + +utmp_backend = get_option('utmp-backend') +if utmp_backend == 'auto' + host_os = host_machine.system() + if host_os == 'linux' + utmp_backend = 'libutempter' + elif host_os == 'freebsd' + utmp_backend = 'ulog' + else + utmp_backend = 'none' + endif +endif + +utmp_default_helper_path = get_option('utmp-default-helper-path') + +if utmp_backend == 'none' + utmp_add = '' + utmp_del = '' + utmp_del_have_argument = false + utmp_default_helper_path = '' +elif utmp_backend == 'libutempter' + utmp_add = 'add' + utmp_del = 'del' + utmp_del_have_argument = false + if utmp_default_helper_path == 'auto' + utmp_default_helper_path = join_paths('/usr', get_option('libdir'), 'utempter', 'utempter') + endif +elif utmp_backend == 'ulog' + utmp_add = 'login' + utmp_del = 'logout' + utmp_del_have_argument = false + if utmp_default_helper_path == 'auto' + utmp_default_helper_path = join_paths('/usr', get_option('libexecdir'), 'ulog-helper') + endif +else + error('invalid utmp backend') +endif + +add_project_arguments( + ['-D_GNU_SOURCE=200809L', + '-DFOOT_DEFAULT_TERM="@0@"'.format(get_option('default-terminfo'))] + + (utmp_backend != 'none' + ? ['-DUTMP_ADD="@0@"'.format(utmp_add), + '-DUTMP_DEL="@0@"'.format(utmp_del), + '-DUTMP_DEFAULT_HELPER_PATH="@0@"'.format(utmp_default_helper_path)] + : []) + + (utmp_del_have_argument + ? ['-DUTMP_DEL_HAVE_ARGUMENT=1'] + : []) + + (is_debug_build + ? ['-D_DEBUG'] + : [cc.get_supported_arguments('-fno-asynchronous-unwind-tables')]) + + (get_option('ime') + ? ['-DFOOT_IME_ENABLED=1'] + : []) + + (get_option('b_pgo') == 'use' + ? ['-DFOOT_PGO_ENABLED=1'] + : []) + + cc.get_supported_arguments( + ['-pedantic', + '-fstrict-aliasing', + '-Wstrict-aliasing']), + language: 'c', +) + +terminfo_install_location = get_option('custom-terminfo-install-location') + +if terminfo_install_location != '' + add_project_arguments( + ['-DFOOT_TERMINFO_PATH="@0@"'.format( + join_paths(get_option('prefix'), terminfo_install_location))], + language: 'c') +else + terminfo_install_location = join_paths(get_option('datadir'), 'terminfo') +endif + +# Compute the relative path used by compiler invocations. +source_root = meson.current_source_dir().split('/') +build_root = meson.global_build_root().split('/') +relative_dir_parts = [] +i = 0 +in_prefix = true +foreach p : build_root + if i >= source_root.length() or not in_prefix or p != source_root[i] + in_prefix = false + relative_dir_parts += '..' + endif + i += 1 +endforeach +i = 0 +in_prefix = true +foreach p : source_root + if i >= build_root.length() or not in_prefix or build_root[i] != p + in_prefix = false + relative_dir_parts += p + endif + i += 1 +endforeach +relative_dir = join_paths(relative_dir_parts) + '/' + +if cc.has_argument('-fmacro-prefix-map=/foo=') + add_project_arguments('-fmacro-prefix-map=@0@='.format(relative_dir), language: 'c') +endif + +math = cc.find_library('m') +threads = [dependency('threads'), cc.find_library('stdthreads', required: false)] +libepoll = dependency('epoll-shim', required: false) +pixman = dependency('pixman-1') +wayland_protocols = dependency('wayland-protocols', version: '>=1.41', + fallback: 'wayland-protocols', + default_options: ['tests=false']) +wayland_client = dependency('wayland-client') +wayland_cursor = dependency('wayland-cursor') +xkb = dependency('xkbcommon', version: '>=1.0.0') +fontconfig = dependency('fontconfig') +utf8proc = dependency('libutf8proc', required: get_option('grapheme-clustering')) + +if utf8proc.found() + add_project_arguments('-DFOOT_GRAPHEME_CLUSTERING=1', language: 'c') +endif + +if pixman.version().version_compare('>=0.46.0') + add_project_arguments('-DHAVE_PIXMAN_RGBA_16', language: 'c') +endif + +tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist') +fcft = dependency('fcft', version: ['>=3.3.1', '<4.0.0'], fallback: 'fcft') + +wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir') + +wscanner = dependency('wayland-scanner', native: true) +wscanner_prog = find_program( + wscanner.get_variable('wayland_scanner'), native: true) + +wl_proto_headers = [] +wl_proto_src = [] +wl_proto_xml = [ + wayland_protocols_datadir / 'stable/xdg-shell/xdg-shell.xml', + wayland_protocols_datadir / 'unstable/xdg-decoration/xdg-decoration-unstable-v1.xml', + wayland_protocols_datadir / 'unstable/xdg-output/xdg-output-unstable-v1.xml', + wayland_protocols_datadir / 'unstable/primary-selection/primary-selection-unstable-v1.xml', + wayland_protocols_datadir / 'stable/presentation-time/presentation-time.xml', + wayland_protocols_datadir / 'unstable/text-input/text-input-unstable-v3.xml', + wayland_protocols_datadir / 'staging/xdg-activation/xdg-activation-v1.xml', + wayland_protocols_datadir / 'stable/viewporter/viewporter.xml', + wayland_protocols_datadir / 'staging/fractional-scale/fractional-scale-v1.xml', + wayland_protocols_datadir / 'unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 + wayland_protocols_datadir / 'staging/cursor-shape/cursor-shape-v1.xml', + wayland_protocols_datadir / 'staging/single-pixel-buffer/single-pixel-buffer-v1.xml', + wayland_protocols_datadir / 'staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml', + wayland_protocols_datadir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml', + wayland_protocols_datadir / 'staging/color-management/color-management-v1.xml', +] + +if (wayland_protocols.version().version_compare('>=1.43')) + wl_proto_xml += [wayland_protocols_datadir / 'staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml'] + add_project_arguments('-DHAVE_XDG_TOPLEVEL_TAG=1', language: 'c') +endif +if (wayland_protocols.version().version_compare('>=1.45')) + wl_proto_xml += [wayland_protocols_datadir / 'staging/ext-background-effect/ext-background-effect-v1.xml'] + add_project_arguments('-DHAVE_EXT_BACKGROUND_EFFECT=1', language: 'c') +endif + +foreach prot : wl_proto_xml + wl_proto_headers += custom_target( + prot.underscorify() + '-client-header', + output: '@BASENAME@.h', + input: prot, + command: [wscanner_prog, 'client-header', '@INPUT@', '@OUTPUT@']) + + wl_proto_src += custom_target( + prot.underscorify() + '-private-code', + output: '@BASENAME@.c', + input: prot, + command: [wscanner_prog, 'private-code', '@INPUT@', '@OUTPUT@']) +endforeach + +env = find_program('env', native: true) +generate_version_sh = files('generate-version.sh') +version = custom_target( + 'generate_version', + build_always_stale: true, + output: 'version.h', + command: [env, 'LC_ALL=C', generate_version_sh, meson.project_version(), '@CURRENT_SOURCE_DIR@', '@OUTPUT@']) + +python = find_program('python3', native: true) +generate_builtin_terminfo_py = files('scripts/generate-builtin-terminfo.py') +foot_terminfo = files('foot.info') +builtin_terminfo = custom_target( + 'generate_builtin_terminfo', + output: 'foot-terminfo.h', + command: [python, generate_builtin_terminfo_py, + '@default_terminfo@', foot_terminfo, 'foot', '@OUTPUT@'] +) + +generate_emoji_variation_sequences = files('scripts/generate-emoji-variation-sequences.py') +emoji_variation_sequences = custom_target( + 'generate_emoji_variation_sequences', + input: 'unicode/emoji-variation-sequences.txt', + output: 'emoji-variation-sequences.h', + command: [python, generate_emoji_variation_sequences, '@INPUT@', '@OUTPUT@'] +) + +generate_srgb_funcs = files('scripts/srgb.py') +srgb_funcs = custom_target( + 'generate_srgb_funcs', + output: ['srgb.c', 'srgb.h'], + command: [python, generate_srgb_funcs, '@OUTPUT0@', '@OUTPUT1@'] +) + +common = static_library( + 'common', + 'log.c', 'log.h', + 'char32.c', 'char32.h', + 'debug.c', 'debug.h', + 'macros.h', + 'xmalloc.c', 'xmalloc.h', + 'xsnprintf.c', 'xsnprintf.h', + dependencies: [utf8proc] +) + +misc = static_library( + 'misc', + 'hsl.c', 'hsl.h', + 'macros.h', + 'misc.c', 'misc.h', + 'uri.c', 'uri.h', + dependencies: [utf8proc], + link_with: [common] +) + +vtlib = static_library( + 'vtlib', + 'base64.c', 'base64.h', + 'composed.c', 'composed.h', + 'cursor-shape.c', 'cursor-shape.h', + 'csi.c', 'csi.h', + 'dcs.c', 'dcs.h', + 'macros.h', + 'osc.c', 'osc.h', + 'sixel.c', 'sixel.h', + 'vt.c', 'vt.h', + builtin_terminfo, srgb_funcs, + wl_proto_src + wl_proto_headers, + version, + dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc], + link_with: [common, misc], +) + +pgolib = static_library( + 'pgolib', + 'grid.c', 'grid.h', + 'selection.c', 'selection.h', + 'terminal.c', 'terminal.h', + emoji_variation_sequences, + wl_proto_src + wl_proto_headers, + dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc], + link_with: vtlib, +) + +tokenize = static_library( + 'tokenizelib', + 'tokenize.c', + dependencies: [utf8proc], + link_with: [common], +) + +if get_option('b_pgo') == 'generate' + executable( + 'pgo', + 'pgo/pgo.c', + wl_proto_src + wl_proto_headers, + dependencies: [math, threads, libepoll, pixman, wayland_client, xkb, utf8proc, fcft, tllist], + link_with: pgolib, + ) +endif + +executable( + 'foot', + 'async.c', 'async.h', + 'box-drawing.c', 'box-drawing.h', + 'config.c', 'config.h', + 'commands.c', 'commands.h', + 'extract.c', 'extract.h', + 'fdm.c', 'fdm.h', + 'foot-features.c', 'foot-features.h', + 'ime.c', 'ime.h', + 'input.c', 'input.h', + 'key-binding.c', 'key-binding.h', + 'main.c', + 'notify.c', 'notify.h', + 'quirks.c', 'quirks.h', + 'reaper.c', 'reaper.h', + 'render.c', 'render.h', + 'search.c', 'search.h', + 'server.c', 'server.h', 'client-protocol.h', + 'shm.c', 'shm.h', + 'slave.c', 'slave.h', + 'spawn.c', 'spawn.h', + 'tokenize.c', 'tokenize.h', + 'unicode-mode.c', 'unicode-mode.h', + 'url-mode.c', 'url-mode.h', + 'user-notification.c', 'user-notification.h', + 'wayland.c', 'wayland.h', 'shm-formats.h', + 'xkbcommon-vmod.h', + srgb_funcs, wl_proto_src + wl_proto_headers, version, + dependencies: [math, threads, libepoll, pixman, wayland_client, wayland_cursor, xkb, fontconfig, utf8proc, + tllist, fcft], + link_with: pgolib, + install: true) + +executable( + 'footclient', + 'client.c', 'client-protocol.h', + 'foot-features.c', 'foot-features.h', + 'macros.h', + 'util.h', + version, + dependencies: [tllist, utf8proc], + link_with: common, + install: true) + +install_data( + 'foot.desktop', 'foot-server.desktop', 'footclient.desktop', + install_dir: join_paths(get_option('datadir'), 'applications')) + +systemd = dependency('systemd', required: false) +custom_systemd_units_dir = get_option('systemd-units-dir') + +if systemd.found() or custom_systemd_units_dir != '' + configuration = configuration_data() + configuration.set('bindir', join_paths(get_option('prefix'), get_option('bindir'))) + + if (custom_systemd_units_dir == '') + systemd_units_dir = systemd.get_variable('systemduserunitdir') + else + systemd_units_dir = custom_systemd_units_dir + endif + + configure_file( + configuration: configuration, + input: 'foot-server.service.in', + output: '@BASENAME@', + install_dir: systemd_units_dir + ) + + install_data( + 'foot-server.socket', + install_dir: systemd_units_dir) +endif + +scdoc = dependency('scdoc', native: true, required: get_option('docs')) +install_data('foot.ini', install_dir: join_paths(get_option('sysconfdir'), 'xdg', 'foot')) +if scdoc.found() + install_data( + 'LICENSE', 'README.md', 'CHANGELOG.md', + install_dir: join_paths(get_option('datadir'), 'doc', 'foot')) + subdir('doc') +endif + +if get_option('themes') + install_subdir('themes', install_dir: join_paths(get_option('datadir'), 'foot')) +endif + +terminfo_base_name = get_option('terminfo-base-name') +if terminfo_base_name == '' + terminfo_base_name = get_option('default-terminfo') +endif + +tic = find_program('tic', native: true, required: get_option('terminfo')) +if tic.found() + conf_data = configuration_data( + { + 'default_terminfo': terminfo_base_name + } + ) + + preprocessed = configure_file( + input: 'foot.info', + output: 'foot.info.preprocessed', + configuration: conf_data, + ) + custom_target( + 'terminfo', + output: terminfo_base_name[0], + input: preprocessed, + command: [tic, '-x', '-o', '@OUTDIR@', '-e', '@0@,@0@-direct'.format(terminfo_base_name), '@INPUT@'], + install: true, + install_dir: terminfo_install_location + ) +endif + +subdir('completions') +subdir('icons') +subdir('utils') + +if (get_option('tests')) + subdir('tests') +endif + +summary( + { + 'Documentation': scdoc.found(), + 'Themes': get_option('themes'), + 'IME': get_option('ime'), + 'Grapheme clustering': utf8proc.found(), + 'utmp backend': utmp_backend, + 'utmp helper default path': utmp_default_helper_path, + 'Build terminfo': tic.found(), + 'Terminfo base name': terminfo_base_name, + 'Terminfo install location': terminfo_install_location, + 'Default TERM': get_option('default-terminfo'), + 'Set TERMINFO': get_option('custom-terminfo-install-location') != '', + 'Build tests': get_option('tests'), + }, + bool_yn: true +) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..ab7a07b --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,29 @@ +option('docs', type: 'feature', + description: 'Build and install documentation (man pages, example foot.ini, readme, changelog, license etc).') + +option('themes', type: 'boolean', value: true, + description: 'Install themes (predefined color schemes)') + +option('ime', type: 'boolean', value: true, + description: 'IME (Input Method Editor) support') + +option('grapheme-clustering', type: 'feature', + description: 'Enables grapheme clustering using libutf8proc. Requires fcft with harfbuzz support to be useful.') + +option('tests', type: 'boolean', value: true, description: 'Build tests') + +option('terminfo', type: 'feature', value: 'enabled', description: 'Build and install foot\'s terminfo files.') +option('default-terminfo', type: 'string', value: 'foot', + description: 'Default value of the "term" option in foot.ini.') +option('terminfo-base-name', type: 'string', + description: 'Base name of the generated terminfo files. Defaults to the value of the \'default-terminfo\' meson option') +option('custom-terminfo-install-location', type: 'string', value: '', + description: 'Path to foot\'s terminfo, relative to ${prefix}. If set, foot will set $TERMINFO to this value in the client process.') + +option('systemd-units-dir', type: 'string', value: '', + description: 'Where to install the systemd service files (absolute path). Default: ${systemduserunitdir}') + +option('utmp-backend', type: 'combo', value: 'auto', choices: ['none', 'libutempter', 'ulog', 'auto'], + description: 'Which utmp logging backend to use. This affects how (with what arguments) the utmp helper binary (see \'utmp-default-helper-path\')is called. Default: auto (linux=libutempter, freebsd=ulog, others=none)') +option('utmp-default-helper-path', type: 'string', value: 'auto', + description: 'Default path to the utmp helper binary. Default: auto-detect') diff --git a/misc.c b/misc.c new file mode 100644 index 0000000..1369df0 --- /dev/null +++ b/misc.c @@ -0,0 +1,63 @@ +#include "misc.h" +#include "char32.h" +#include + +bool +isword(char32_t wc, bool spaces_only, const char32_t *delimiters) +{ + if (spaces_only) + return isc32graph(wc); + + if (c32chr(delimiters, wc) != NULL) + return false; + + return isc32graph(wc); +} + +void +timespec_add(const struct timespec *a, const struct timespec *b, + struct timespec *res) +{ + const long one_sec_in_ns = 1000000000; + + res->tv_sec = a->tv_sec + b->tv_sec; + res->tv_nsec = a->tv_nsec + b->tv_nsec; + /* tv_nsec may be negative */ + if (res->tv_nsec >= one_sec_in_ns) { + res->tv_sec++; + res->tv_nsec -= one_sec_in_ns; + } +} + +void +timespec_sub(const struct timespec *a, const struct timespec *b, + struct timespec *res) +{ + const long one_sec_in_ns = 1000000000; + + res->tv_sec = a->tv_sec - b->tv_sec; + res->tv_nsec = a->tv_nsec - b->tv_nsec; + /* tv_nsec may be negative */ + if (res->tv_nsec < 0) { + res->tv_sec--; + res->tv_nsec += one_sec_in_ns; + } +} + +bool +is_valid_utf8_and_printable(const char *value) +{ + char32_t *wide = ambstoc32(value); + if (wide == NULL) + return false; + + for (const char32_t *c = wide; *c != U'\0'; c++) { + if (!isc32print(*c)) { + free(wide); + return false; + } + } + + free(wide); + return true; +} diff --git a/misc.h b/misc.h new file mode 100644 index 0000000..6c77c48 --- /dev/null +++ b/misc.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include + +bool isword(char32_t wc, bool spaces_only, const char32_t *delimiters); + +void timespec_add(const struct timespec *a, const struct timespec *b, struct timespec *res); +void timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res); + +bool is_valid_utf8_and_printable(const char *value); diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..345516c --- /dev/null +++ b/notes.txt @@ -0,0 +1,6 @@ +1. uses wrong cursor when switching to another tab, only first tab shows +the correct cursor. +2. if i resize a tab the next tab will be the same +size it was previously until i resize the window, but then i will have +to do that for each consecutive window. if i dont resize the window at +all the tabs look fine. diff --git a/notify.c b/notify.c new file mode 100644 index 0000000..e454b03 --- /dev/null +++ b/notify.c @@ -0,0 +1,765 @@ +#include "notify.h" + +#include +#include +#include +#include + +#include +#include +#include + +#define LOG_MODULE "notify" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "config.h" +#include "spawn.h" +#include "terminal.h" +#include "util.h" +#include "wayland.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +void +notify_free(struct terminal *term, struct notification *notif) +{ + if (notif->pid > 0) + fdm_del(term->fdm, notif->stdout_fd); + + free(notif->id); + free(notif->title); + free(notif->body); + free(notif->category); + free(notif->app_id); + free(notif->icon_cache_id); + free(notif->icon_symbolic_name); + free(notif->icon_data); + free(notif->sound_name); + free(notif->xdg_token); + free(notif->stdout_data); + + tll_free_and_free(notif->actions, free); + + if (notif->icon_path != NULL) { + unlink(notif->icon_path); + free(notif->icon_path); + + if (notif->icon_fd >= 0) + close(notif->icon_fd); + } + + memset(notif, 0, sizeof(*notif)); +} + +static bool +write_icon_file(const void *data, size_t data_sz, int *fd, char **filename, + char **symbolic_name) +{ + xassert(*filename == NULL); + xassert(*symbolic_name == NULL); + + char name[64] = "/tmp/foot-notification-icon-XXXXXX"; + + *filename = NULL; + *symbolic_name = NULL; + *fd = mkostemp(name, O_CLOEXEC); + + if (*fd < 0) { + LOG_ERRNO("failed to create temporary file for icon cache"); + return false; + } + + if (write(*fd, data, data_sz) != (ssize_t)data_sz) { + LOG_ERRNO("failed to write icon data to temporary file"); + close(*fd); + *fd = -1; + return false; + } + + LOG_DBG("wrote icon data to %s", name); + *filename = xstrdup(name); + *symbolic_name = xstrjoin("file://", *filename); + return true; +} + +static bool +to_integer(const char *line, size_t len, uint32_t *res) +{ + bool is_id = true; + uint32_t maybe_id = 0; + + for (size_t i = 0; i < len; i++) { + char digit = line[i]; + if (digit < '0' || digit > '9') { + is_id = false; + break; + } + + maybe_id *= 10; + maybe_id += digit - '0'; + } + + *res = maybe_id; + return is_id; +} + +static void +consume_stdout(struct notification *notif, bool eof) +{ + char *data = notif->stdout_data; + const char *line = data; + size_t left = notif->stdout_sz; + + /* Process stdout, line-by-line */ + while (left > 0) { + line = data; + size_t len = left; + char *eol = (char *)memchr(line, '\n', left); + + if (eol != NULL) { + *eol = '\0'; + len = strlen(line); + data = eol + 1; + } else if (!eof) + break; + + uint32_t maybe_id = 0; + uint32_t maybe_button_nr = 0; + + /* Check for daemon assigned ID, either '123', or 'id=123' */ + if ((notif->external_id == 0 && to_integer(line, len, &maybe_id)) || + (len > 3 && memcmp(line, "id=", 3) == 0 && + to_integer(&line[3], len - 3, &maybe_id))) + { + notif->external_id = maybe_id; + LOG_DBG("external ID: %u", notif->external_id); + } + + /* Check for triggered action, either 'default' or 'action=default' */ + else if ((len == 7 && memcmp(line, "default", 7) == 0) || + (len == 7 + 7 && memcmp(line, "action=default", 7 + 7) == 0)) + { + notif->activated = true; + LOG_DBG("notification's default action was triggered"); + } + + else if (len > 7 && memcmp(line, "action=", 7) == 0) { + notif->activated = true; + + if (to_integer(&line[7], len - 7, &maybe_button_nr)) { + notif->activated_button = maybe_button_nr; + LOG_DBG("custom action %u triggered", notif->activated_button); + } else { + LOG_DBG("unrecognized action triggered: %.*s", + (int)(len - 7), &line[7]); + } + } + + else if (notif->external_id > 0 && + to_integer(line, len, &maybe_button_nr) && + maybe_button_nr > 0 && + maybe_button_nr <= notif->button_count) + { + /* Single integer, appearing *after* the ID, and is within + the custom button/action range */ + notif->activated = true; + notif->activated_button = maybe_button_nr; + LOG_DBG("custom action %u triggered", notif->activated_button); + } + + /* Check for XDG activation token, 'xdgtoken=xyz' */ + else if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) { + notif->xdg_token = xstrndup(&line[9], len - 9); + LOG_DBG("XDG token: \"%s\"", notif->xdg_token); + } + + left -= len + (eol != NULL ? 1 : 0); + } + + if (left > 0) + memmove(notif->stdout_data, data, left); + + notif->stdout_sz = left; +} + +static bool +fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data) +{ + const struct terminal *term = data; + struct notification *notif = NULL; + + /* Find notification */ + tll_foreach(term->active_notifications, it) { + if (it->item.stdout_fd == fd) { + notif = &it->item; + break; + } + } + + if (events & EPOLLIN) { + char buf[512]; + ssize_t count = read(fd, buf, sizeof(buf) - 1); + + if (count < 0) { + if (errno == EINTR) + return true; + + LOG_ERRNO("failed to read notification activation token"); + return false; + } + + if (count > 0 && notif != NULL) { + if (notif->stdout_data == NULL) { + xassert(notif->stdout_sz == 0); + notif->stdout_data = xmemdup(buf, count); + } else { + notif->stdout_data = xrealloc(notif->stdout_data, notif->stdout_sz + count); + memcpy(¬if->stdout_data[notif->stdout_sz], buf, count); + } + + notif->stdout_sz += count; + consume_stdout(notif, false); + } + } + + if (events & EPOLLHUP) { + fdm_del(fdm, fd); + if (notif != NULL) { + notif->stdout_fd = -1; + consume_stdout(notif, true); + } + } + + return true; +} + +static void +notif_done(struct reaper *reaper, pid_t pid, int status, void *data) +{ + struct terminal *term = data; + + tll_foreach(term->active_notifications, it) { + struct notification *notif = &it->item; + if (notif->pid != pid) + continue; + + LOG_DBG("notification %s closed", + notif->id != NULL ? notif->id : ""); + + if (notif->activated && notif->focus) { + LOG_DBG("focus window on notification activation: \"%s\"", + notif->xdg_token); + + if (notif->xdg_token == NULL) + LOG_WARN("cannot focus window: no activation token available"); + else + wayl_activate(term->wl, term->window, notif->xdg_token); + } + + if (notif->activated && notif->report_activated) { + LOG_DBG("sending notification activation event to client"); + + const char *id = notif->id != NULL ? notif->id : "0"; + + char button_nr[16] = {0}; + if (notif->activated_button > 0) { + xsnprintf( + button_nr, sizeof(button_nr), "%u", notif->activated_button); + } + + char reply[7 + strlen(id) + 1 + strlen(button_nr) + 2 + 1]; + size_t n = xsnprintf( + reply, sizeof(reply), "\033]99;i=%s;%s\033\\", id, button_nr); + term_to_slave(term, reply, n); + } + + if (notif->report_closed) { + LOG_DBG("sending notification close event to client"); + + const char *id = notif->id != NULL ? notif->id : "0"; + char reply[7 + strlen(id) + 1 + 7 + 1 + 2 + 1]; + size_t n = xsnprintf( + reply, sizeof(reply), "\033]99;i=%s:p=close;\033\\", id); + term_to_slave(term, reply, n); + } + + notify_free(term, notif); + tll_remove(term->active_notifications, it); + return; + } +} + +static bool +expand_action_to_argv(struct terminal *term, const char *name, const char *label, + size_t *argc, char ***argv) +{ + char **expanded = NULL; + size_t count = 0; + + if (!spawn_expand_template( + &term->conf->desktop_notifications.command_action_arg, 2, + (const char *[]){"action-name", "action-label"}, + (const char *[]){name, label}, + &count, &expanded)) + { + return false; + } + + /* Append to the "global" actions argv */ + *argv = xrealloc(*argv, (*argc + count) * sizeof((*argv)[0])); + memcpy(&(*argv)[*argc], expanded, count * sizeof(expanded[0])); + *argc += count; + + free(expanded); + return true; +} + +bool +notify_notify(struct terminal *term, struct notification *notif) +{ + xassert(notif->xdg_token == NULL); + xassert(notif->external_id == 0); + xassert(notif->pid == 0); + xassert(notif->stdout_fd <= 0); + xassert(notif->stdout_data == NULL); + xassert(notif->icon_path == NULL); + xassert(notif->icon_fd <= 0); + + notif->pid = -1; + notif->stdout_fd = -1; + notif->icon_fd = -1; + + if (term->conf->desktop_notifications.command.argv.args == NULL) + return false; + + if ((term->conf->desktop_notifications.inhibit_when_focused || + notif->when != NOTIFY_ALWAYS) + && term->kbd_focus) + { + /* No notifications while we're focused */ + return false; + } + + const char *app_id = notif->app_id != NULL + ? notif->app_id + : term->app_id != NULL + ? term->app_id + : term->conf->app_id; + const char *title = notif->title != NULL ? notif->title : notif->body; + const char *body = notif->title != NULL && notif->body != NULL ? notif->body : ""; + + /* Icon: symbolic name if present, otherwise a filename */ + const char *icon_name_or_path = ""; + + if (notif->icon_cache_id != NULL) { + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + const struct notification_icon *icon = &term->notification_icons[i]; + + if (icon->id != NULL && streq(icon->id, notif->icon_cache_id)) { + /* For now, we set the symbolic name to 'file:///path' + * when using a file based icon. */ + xassert(icon->symbolic_name != NULL); + icon_name_or_path = icon->symbolic_name; + + LOG_DBG("using icon from cache (cache ID: %s): %s", + icon->id, icon_name_or_path); + break; + } + } + } else if (notif->icon_symbolic_name != NULL) { + icon_name_or_path = notif->icon_symbolic_name; + LOG_DBG("using symbolic icon from notification: %s", icon_name_or_path); + } else if (notif->icon_data_sz > 0) { + xassert(notif->icon_data != NULL); + + if (write_icon_file( + notif->icon_data, notif->icon_data_sz, + ¬if->icon_fd, + ¬if->icon_path, + ¬if->icon_symbolic_name)) + icon_name_or_path = notif->icon_symbolic_name; + + LOG_DBG("using icon data from notification: %s", icon_name_or_path); + } + + bool track_notification = notif->focus || + notif->report_activated || + notif->may_be_programatically_closed; + + uint32_t replaces_id = 0; + if (notif->id != NULL) { + tll_foreach(term->active_notifications, it) { + struct notification *existing = &it->item; + + if (existing->id == NULL) + continue; + + /* + * When replacing/updating a notification, we may have + * *multiple* notification helpers running for the "same" + * notification. Make sure only the *last* notification's + * report closed/activated are honored, to avoid sending + * multiple reports. + * + * This also means we cannot 'break' out of the loop - we + * must check *all* notifications. + */ + if (existing->external_id != 0 && streq(existing->id, notif->id)) { + replaces_id = existing->external_id; + existing->report_activated = false; + existing->report_closed = false; + } + } + } + + char replaces_id_str[16]; + xsnprintf(replaces_id_str, sizeof(replaces_id_str), "%u", replaces_id); + + const char *urgency_str = + notif->urgency == NOTIFY_URGENCY_LOW + ? "low" + : notif->urgency == NOTIFY_URGENCY_NORMAL + ? "normal" : "critical"; + + LOG_DBG("notify: title=\"%s\", body=\"%s\", app-id=\"%s\", category=\"%s\", " + "urgency=\"%s\", icon=\"%s\", expires=%d, replaces=%u, muted=%s, " + "sound-name=%s (tracking: %s)", + title, body, app_id, notif->category, urgency_str, icon_name_or_path, + notif->expire_time, replaces_id, + notif->muted ? "yes" : "no", notif->sound_name, + track_notification ? "yes" : "no"); + + xassert(title != NULL); + if (title == NULL) + return false; + + char **argv = NULL; + size_t argc = 0; + char **action_argv = NULL; + size_t action_argc = 0; + + char expire_time[16]; + xsnprintf(expire_time, sizeof(expire_time), "%d", notif->expire_time); + + if (term->conf->desktop_notifications.command_action_arg.argv.args) { + if (!expand_action_to_argv( + term, "default", "Activate", &action_argc, &action_argv)) + { + return false; + } + + size_t action_idx = 1; + tll_foreach(notif->actions, it) { + + /* Custom actions use a numerical name, starting at 1 */ + char name[16]; + xsnprintf(name, sizeof(name), "%zu", action_idx++); + + if (!expand_action_to_argv( + term, name, it->item, &action_argc, &action_argv)) + { + for (size_t i = 0; i < action_argc; i++) + free(action_argv[i]); + free(action_argv); + return false; + } + } + } + + if (!spawn_expand_template( + &term->conf->desktop_notifications.command, 12, + (const char *[]){ + "app-id", "window-title", "icon", "title", "body", "category", + "urgency", "muted", "sound-name", "expire-time", "replace-id", + "action-argument"}, + (const char *[]){ + app_id, term->window_title, icon_name_or_path, title, + body != NULL ? body : "", + notif->category != NULL ? notif->category : "", urgency_str, + notif->muted ? "true" : "false", + notif->sound_name != NULL ? notif->sound_name : "", + expire_time, replaces_id_str, + + /* Custom expansion below, since we need to expand to multiple arguments */ + "${action-argument}"}, + &argc, &argv)) + { + return false; + } + + /* Post-process the expanded argv, and patch in all the --action + arguments we expanded earlier */ + for (size_t i = 0; i < argc; i++) { + if (!streq(argv[i], "${action-argument}")) + continue; + + if (action_argc == 0) { + free(argv[i]); + + /* Remove ${command-argument}, but include terminating NULL */ + memmove(&argv[i], &argv[i + 1], (argc - i) * sizeof(argv[0])); + argc--; + break; + } + + /* Remove the "${action-argument}" entry, add all actions argument + from earlier, but include terminating NULL */ + argv = xrealloc(argv, (argc + action_argc) * sizeof(argv[0])); + + /* Move remaining arguments to after the action arguments */ + memmove(&argv[i + action_argc], + &argv[i + 1], + (argc - i) * sizeof(argv[0])); /* Include terminating NULL */ + + free(argv[i]); /* Free xstrdup("${action-argument}"); */ + + /* Insert the action arguments */ + for (size_t j = 0; j < action_argc; j++) { + argv[i + j] = action_argv[j]; + action_argv[j] = NULL; + } + + argc += action_argc; + argc--; /* The ${action-argument} option has been removed */ + break; + } + + LOG_DBG("notify command:"); + for (size_t i = 0; i < argc; i++) + LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]); + xassert(argv[argc] == NULL); + + int stdout_fds[2] = {-1, -1}; + if (track_notification) { + if (pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) { + LOG_WARN("failed to create stdout pipe"); + track_notification = false; + /* Non-fatal */ + } else { + tll_push_back(term->active_notifications, *notif); + + /* We've taken over ownership of all data; clear, so that + notify_free() doesn't double free */ + notif->id = NULL; + notif->title = NULL; + notif->body = NULL; + notif->category = NULL; + notif->app_id = NULL; + notif->icon_cache_id = NULL; + notif->icon_symbolic_name = NULL; + notif->icon_data = NULL; + notif->icon_data_sz = 0; + notif->icon_path = NULL; + notif->sound_name = NULL; + notif->icon_fd = -1; + notif->stdout_fd = -1; + struct notification *new_notif = &tll_back(term->active_notifications); + + /* We don't need these anymore. They'll be free:d by the caller */ + new_notif->button_count = tll_length(notif->actions); + memset(&new_notif->actions, 0, sizeof(new_notif->actions)); + notif = new_notif; + } + } + + if (stdout_fds[0] >= 0) { + fdm_add(term->fdm, stdout_fds[0], EPOLLIN, + &fdm_notify_stdout, (void *)term); + } + + /* Redirect stdin to /dev/null, but ignore failure to open */ + int devnull = open("/dev/null", O_RDONLY); + pid_t pid = spawn( + term->reaper, NULL, argv, devnull, stdout_fds[1], -1, + track_notification ? ¬if_done : NULL, (void *)term, NULL); + + if (stdout_fds[1] >= 0) { + /* Close write-end of stdout pipe */ + close(stdout_fds[1]); + } + + if (pid < 0 && stdout_fds[0] >= 0) { + /* Remove FDM callback if we failed to spawn */ + fdm_del(term->fdm, stdout_fds[0]); + } + + if (devnull >= 0) + close(devnull); + + for (size_t i = 0; i < argc; i++) + free(argv[i]); + free(argv); + for (size_t i = 0; i < action_argc; i++) + free(action_argv[i]); + free(action_argv); + + notif->pid = pid; + notif->stdout_fd = stdout_fds[0]; + return true; +} + +void +notify_close(struct terminal *term, const char *id) +{ + xassert(id != NULL); + LOG_DBG("close notification %s", id); + + tll_foreach(term->active_notifications, it) { + const struct notification *notif = &it->item; + if (notif->id == NULL || !streq(notif->id, id)) + continue; + + if (term->conf->desktop_notifications.close.argv.args == NULL) { + LOG_DBG( + "trying to close notification \"%s\" by sending SIGINT to %u", + id, notif->pid); + + if (notif->pid == 0) { + LOG_WARN( + "cannot close notification \"%s\": no helper process running", + id); + } else { + /* Best-effort... */ + kill(notif->pid, SIGINT); + } + } else { + LOG_DBG( + "trying to close notification \"%s\" " + "by running user defined command", id); + + if (notif->external_id == 0) { + LOG_WARN("cannot close notification \"%s\": " + "no daemon assigned notification ID available", id); + return; + } + + char **argv = NULL; + size_t argc = 0; + + char external_id[16]; + xsnprintf(external_id, sizeof(external_id), "%u", notif->external_id); + + if (!spawn_expand_template( + &term->conf->desktop_notifications.close, 1, + (const char *[]){"id"}, + (const char *[]){external_id}, + &argc, &argv)) + { + return; + } + + int devnull = open("/dev/null", O_RDONLY); + spawn( + term->reaper, NULL, argv, devnull, -1, -1, + NULL, (void *)term, NULL); + + if (devnull >= 0) + close(devnull); + + for (size_t i = 0; i < argc; i++) + free(argv[i]); + free(argv); + } + + return; + } + + LOG_WARN("cannot close notification \"%s\": no such notification", id); +} + +static void +add_icon(struct notification_icon *icon, const char *id, const char *symbolic_name, + const uint8_t *data, size_t data_sz) +{ + icon->id = xstrdup(id); + icon->symbolic_name = symbolic_name != NULL ? xstrdup(symbolic_name) : NULL; + icon->tmp_file_name = NULL; + icon->tmp_file_fd = -1; + + /* + * Dump in-line data to a temporary file. This allows us to pass + * the filename as a parameter to notification helpers + * (i.e. notify-send -i ). + * + * Optimization: since we always prefer (i.e. use) the symbolic + * name if present, there's no need to create a file on disk if we + * have a symbolic name. + */ + if (symbolic_name == NULL && data_sz > 0) { + write_icon_file( + data, data_sz, + &icon->tmp_file_fd, + &icon->tmp_file_name, + &icon->symbolic_name); + } + + LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s", + icon->id, icon->symbolic_name, icon->tmp_file_name); +} + +void +notify_icon_add(struct terminal *term, const char *id, + const char *symbolic_name, const uint8_t *data, size_t data_sz) +{ +#if defined(_DEBUG) + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + struct notification_icon *icon = &term->notification_icons[i]; + if (icon->id != NULL && streq(icon->id, id)) { + BUG("notification icon cache already contains \"%s\"", id); + } + } +#endif + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + struct notification_icon *icon = &term->notification_icons[i]; + if (icon->id == NULL) { + add_icon(icon, id, symbolic_name, data, data_sz); + return; + } + } + + /* Cache full - throw out first entry, add new entry last */ + notify_icon_free(&term->notification_icons[0]); + memmove(&term->notification_icons[0], + &term->notification_icons[1], + ((ALEN(term->notification_icons) - 1) * + sizeof(term->notification_icons[0]))); + + add_icon( + &term->notification_icons[ALEN(term->notification_icons) - 1], + id, symbolic_name, data, data_sz); +} + +void +notify_icon_del(struct terminal *term, const char *id) +{ + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + struct notification_icon *icon = &term->notification_icons[i]; + + if (icon->id == NULL || !streq(icon->id, id)) + continue; + + LOG_DBG("expelled %s from the notification icon cache", icon->id); + notify_icon_free(icon); + return; + } +} + +void +notify_icon_free(struct notification_icon *icon) +{ + if (icon->tmp_file_name != NULL) { + unlink(icon->tmp_file_name); + if (icon->tmp_file_fd >= 0) + close(icon->tmp_file_fd); + } + + free(icon->id); + free(icon->symbolic_name); + free(icon->tmp_file_name); + + icon->id = NULL; + icon->symbolic_name = NULL; + icon->tmp_file_name = NULL; + icon->tmp_file_fd = -1; +} diff --git a/notify.h b/notify.h new file mode 100644 index 0000000..89b5123 --- /dev/null +++ b/notify.h @@ -0,0 +1,95 @@ +#pragma once +#include +#include +#include + +#include + +struct terminal; + +enum notify_when { + /* First, so that it can be left out of initializer and still be + the default */ + NOTIFY_ALWAYS, + + NOTIFY_UNFOCUSED, + NOTIFY_INVISIBLE +}; + +enum notify_urgency { + /* First, so that it can be left out of initializer and still be + the default */ + NOTIFY_URGENCY_NORMAL, + + NOTIFY_URGENCY_LOW, + NOTIFY_URGENCY_CRITICAL, +}; + +struct notification { + /* + * Set by caller of notify_notify() + */ + char *id; /* Internal notification ID */ + + char *app_id; /* Custom app-id, overrides the terminal's app-id if set */ + char *title; /* Required */ + char *body; + char *category; + + enum notify_when when; + enum notify_urgency urgency; + int32_t expire_time; + + tll(char *) actions; + + char *icon_cache_id; + char *icon_symbolic_name; + uint8_t *icon_data; + size_t icon_data_sz; + + bool focus; /* Focus the foot window when notification is activated */ + bool may_be_programatically_closed; /* OSC-99: notification may be programmatically closed by the client */ + bool report_activated; /* OSC-99: report notification activation to client */ + bool report_closed; /* OSC-99: report notification closed to client */ + + bool muted; /* Explicitly mute the notification */ + char *sound_name; /* Should be set to NULL if muted == true */ + + /* + * Used internally by notify + */ + + uint32_t external_id; /* Daemon assigned notification ID */ + bool activated; /* User 'activated' the notification */ + uint32_t button_count; /* Number of buttons (custom actions) in notification */ + uint32_t activated_button; /* User activated one of the custom actions */ + char *xdg_token; /* XDG activation token, from daemon */ + + pid_t pid; /* Notifier command PID */ + int stdout_fd; /* Notifier command's stdout */ + + char *stdout_data; /* Data we've reado from command's stdout */ + size_t stdout_sz; + + /* Used when notification provides raw icon data, and it's + bypassing the icon cache */ + char *icon_path; + int icon_fd; +}; + +struct notification_icon { + char *id; + char *symbolic_name; + char *tmp_file_name; + int tmp_file_fd; +}; + +bool notify_notify(struct terminal *term, struct notification *notif); +void notify_close(struct terminal *term, const char *id); +void notify_free(struct terminal *term, struct notification *notif); + +void notify_icon_add(struct terminal *term, const char *id, + const char *symbolic_name, const uint8_t *data, + size_t data_sz); +void notify_icon_del(struct terminal *term, const char *id); +void notify_icon_free(struct notification_icon *icon); diff --git a/osc.c b/osc.c new file mode 100644 index 0000000..90704b7 --- /dev/null +++ b/osc.c @@ -0,0 +1,1735 @@ +#include "osc.h" + +#include +#include +#include +#include + +#include + +#define LOG_MODULE "osc" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "base64.h" +#include "config.h" +#include "macros.h" +#include "notify.h" +#include "selection.h" +#include "render.h" +#include "terminal.h" +#include "uri.h" +#include "util.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +#define UNHANDLED() LOG_DBG("unhandled: OSC: %.*s", (int)term->vt.osc.idx, term->vt.osc.data) + +static void +osc_to_clipboard(struct terminal *term, const char *target, + const char *base64_data) +{ + bool to_clipboard = false; + bool to_primary = false; + + if (target[0] == '\0') + to_clipboard = true; + + for (const char *t = target; *t != '\0'; t++) { + switch (*t) { + case 'c': + to_clipboard = true; + break; + + case 's': + case 'p': + to_primary = true; + break; + + default: + LOG_WARN("unimplemented: clipboard target '%c'", *t); + break; + } + } + + /* Find a seat in which the terminal has focus */ + struct seat *seat = NULL; + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) { + seat = &it->item; + break; + } + } + + if (seat == NULL) { + LOG_WARN("OSC52: client tried to write to clipboard data while window was unfocused"); + return; + } + + const bool copy_allowed = term->conf->security.osc52 == OSC52_ENABLED + || term->conf->security.osc52 == OSC52_COPY_ENABLED; + + if (!copy_allowed) { + LOG_DBG("ignoring copy request: disabled in configuration"); + return; + } + + char *decoded = base64_decode(base64_data, NULL); + if (decoded == NULL || decoded[0] == '\0') { + if (decoded == NULL) { + if (errno == EINVAL) + LOG_WARN("OSC: invalid clipboard data: %s", base64_data); + else + LOG_ERRNO("base64_decode() failed"); + } + + if (to_clipboard) + selection_clipboard_unset(seat); + if (to_primary) + selection_primary_unset(seat); + free(decoded); + return; + } + + LOG_DBG("decoded: %s", decoded); + + if (to_clipboard) { + char *copy = xstrdup(decoded); + if (!text_to_clipboard(seat, term, copy, seat->kbd.serial)) + free(copy); + } + + if (to_primary) { + char *copy = xstrdup(decoded); + if (!text_to_primary(seat, term, copy, seat->kbd.serial)) + free(copy); + } + + free(decoded); +} + +struct clip_context { + struct seat *seat; + struct terminal *term; + uint8_t buf[3]; + int idx; +}; + +static void +from_clipboard_cb(char *text, size_t size, void *user) +{ + struct clip_context *ctx = user; + struct terminal *term = ctx->term; + + xassert(ctx->idx >= 0 && ctx->idx <= 2); + + const char *t = text; + size_t left = size; + + if (ctx->idx > 0) { + for (size_t i = ctx->idx; i < 3 && left > 0; i++, t++, left--) + ctx->buf[ctx->idx++] = *t; + + xassert(ctx->idx <= 3); + if (ctx->idx == 3) { + char *chunk = base64_encode(ctx->buf, 3); + xassert(chunk != NULL); + xassert(strlen(chunk) == 4); + + term_paste_data_to_slave(term, chunk, 4); + free(chunk); + + ctx->idx = 0; + } + } + + if (left == 0) + return; + + xassert(ctx->idx == 0); + + int remaining = left % 3; + for (int i = remaining; i > 0; i--) + ctx->buf[ctx->idx++] = text[size - i]; + xassert(ctx->idx == remaining); + + char *chunk = base64_encode((const uint8_t *)t, left / 3 * 3); + xassert(chunk != NULL); + xassert(strlen(chunk) % 4 == 0); + term_paste_data_to_slave(term, chunk, strlen(chunk)); + free(chunk); +} + +static void +from_clipboard_done(void *user) +{ + struct clip_context *ctx = user; + struct terminal *term = ctx->term; + + if (ctx->idx > 0) { + char res[4]; + base64_encode_final(ctx->buf, ctx->idx, res); + term_paste_data_to_slave(term, res, 4); + } + + if (term->vt.osc.bel) + term_paste_data_to_slave(term, "\a", 1); + else + term_paste_data_to_slave(term, "\033\\", 2); + + term->is_sending_paste_data = false; + + /* Make sure we send any queued up non-paste data */ + if (tll_length(term->ptmx_buffers) > 0) + fdm_event_add(term->fdm, term->ptmx, EPOLLOUT); + + free(ctx); +} + +static void +osc_from_clipboard(struct terminal *term, const char *source) +{ + /* Find a seat in which the terminal has focus */ + struct seat *seat = NULL; + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) { + seat = &it->item; + break; + } + } + + if (seat == NULL) { + LOG_WARN("OSC52: client tried to read clipboard data while window was unfocused"); + return; + } + + const bool paste_allowed = term->conf->security.osc52 == OSC52_ENABLED + || term->conf->security.osc52 == OSC52_PASTE_ENABLED; + if (!paste_allowed) { + LOG_DBG("ignoring paste request: disabled in configuration"); + return; + } + + /* Use clipboard if no source has been specified */ + char src = source[0] == '\0' ? 'c' : 0; + bool from_clipboard = src == 'c'; + bool from_primary = false; + + for (const char *s = source; + *s != '\0' && !from_clipboard && !from_primary; + s++) + { + if (*s == 'c' || *s == 'p' || *s == 's') { + src = *s; + + switch (src) { + case 'c': + from_clipboard = selection_clipboard_has_data(seat); + break; + + case 's': + case 'p': + from_primary = selection_primary_has_data(seat); + break; + } + } else + LOG_WARN("unimplemented: clipboard source '%c'", *s); + } + + if (!from_clipboard && !from_primary) + return; + + if (term->is_sending_paste_data) { + /* FIXME: we should wait for the paste to end, then continue + with the OSC-52 reply */ + term_to_slave(term, "\033]52;", 5); + term_to_slave(term, &src, 1); + term_to_slave(term, ";", 1); + if (term->vt.osc.bel) + term_to_slave(term, "\a", 1); + else + term_to_slave(term, "\033\\", 2); + return; + } + + term->is_sending_paste_data = true; + + term_paste_data_to_slave(term, "\033]52;", 5); + term_paste_data_to_slave(term, &src, 1); + term_paste_data_to_slave(term, ";", 1); + + struct clip_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct clip_context) {.seat = seat, .term = term}; + + if (from_clipboard) { + text_from_clipboard( + seat, term, true, &from_clipboard_cb, &from_clipboard_done, ctx); + } + + if (from_primary) { + text_from_primary( + seat, term, true, &from_clipboard_cb, &from_clipboard_done, ctx); + } +} + +static void +osc_selection(struct terminal *term, char *string) +{ + char *p = string; + bool clipboard_done = false; + + /* The first parameter is a string of clipbard sources/targets */ + while (*p != '\0' && !clipboard_done) { + switch (*p) { + case ';': + clipboard_done = true; + *p = '\0'; + break; + } + + p++; + } + + LOG_DBG("clipboard: target = %s data = %s", string, p); + + if (p[0] == '?' && p[1] == '\0') + osc_from_clipboard(term, string); + else + osc_to_clipboard(term, string, p); +} + +static void +osc_flash(struct terminal *term) +{ + /* Our own private - flash */ + term_flash(term, 50); +} + +static bool +parse_legacy_color(const char *string, uint32_t *color, bool *_have_alpha, + uint16_t *_alpha) +{ + bool have_alpha = false; + uint16_t alpha = 0xffff; + + if (string[0] == '[') { + /* e.g. \E]11;[50]#00ff00 */ + const char *start = &string[1]; + + errno = 0; + char *end; + unsigned long percent = strtoul(start, &end, 10); + + if (errno != 0 || *end != ']') + return false; + + have_alpha = true; + alpha = (0xffff * min(percent, 100) + 50) / 100; + + string = end + 1; + } + + if (string[0] != '#') + return false; + + string++; + const size_t len = strlen(string); + + if (len % 3 != 0) + return false; + + const int digits = len / 3; + + int rgb[3]; + for (size_t i = 0; i < 3; i++) { + rgb[i] = 0; + for (size_t j = 0; j < digits; j++) { + size_t idx = i * digits + j; + char c = string[idx]; + rgb[i] <<= 4; + + if (!isxdigit(c)) + rgb[i] |= 0; + else + rgb[i] |= c >= '0' && c <= '9' ? c - '0' : + c >= 'a' && c <= 'f' ? c - 'a' + 10 : c - 'A' + 10; + } + + /* Values with less than 16 bits represent the *most + * significant bits*. I.e. the values are *not* scaled */ + rgb[i] <<= 16 - (4 * digits); + } + + /* Re-scale to 8-bit */ + uint8_t r = 256 * (rgb[0] / 65536.); + uint8_t g = 256 * (rgb[1] / 65536.); + uint8_t b = 256 * (rgb[2] / 65536.); + + LOG_DBG("legacy: %02x%02x%02x (alpha=%04x)", r, g, b, + have_alpha ? alpha : 0xffff); + + *color = r << 16 | g << 8 | b; + + if (_have_alpha != NULL) + *_have_alpha = have_alpha; + if (_alpha != NULL) + *_alpha = alpha; + return true; +} + +static bool +parse_rgb(const char *string, uint32_t *color, bool *_have_alpha, + uint16_t *_alpha) +{ + size_t len = strlen(string); + bool have_alpha = len >= 4 && strncmp(string, "rgba", 4) == 0; + + /* Verify we have the minimum required length (for "") */ + if (have_alpha) { + if (len < STRLEN("rgba:x/x/x/x")) + return false; + } else { + if (len < STRLEN("rgb:x/x/x")) + return false; + } + + /* Verify prefix is "rgb:" or "rgba:" */ + if (have_alpha) { + if (strncmp(string, "rgba:", 5) != 0) + return false; + string += 5; + len -= 5; + } else { + if (strncmp(string, "rgb:", 4) != 0) + return false; + string += 4; + len -= 4; + } + + int rgb[4]; + int digits[4]; + + for (size_t i = 0; i < (have_alpha ? 4 : 3); i++) { + for (rgb[i] = 0, digits[i] = 0; + len > 0 && *string != '/'; + len--, string++, digits[i]++) + { + char c = *string; + rgb[i] <<= 4; + + if (!isxdigit(c)) + rgb[i] |= 0; + else + rgb[i] |= c >= '0' && c <= '9' ? c - '0' : + c >= 'a' && c <= 'f' ? c - 'a' + 10 : c - 'A' + 10; + } + + if (i >= (have_alpha ? 3 : 2)) + break; + + if (len == 0 || *string != '/') + return false; + string++; len--; + } + + /* Re-scale to 8-bit */ + uint8_t r = 256 * (rgb[0] / (double)(1 << (4 * digits[0]))); + uint8_t g = 256 * (rgb[1] / (double)(1 << (4 * digits[1]))); + uint8_t b = 256 * (rgb[2] / (double)(1 << (4 * digits[2]))); + + uint16_t alpha = 0xffff; + if (have_alpha) + alpha = 65536 * (rgb[3] / (double)(1 << (4 * digits[3]))); + + if (have_alpha) + LOG_DBG("rgba: %02x%02x%02x (alpha=%04x)", r, g, b, alpha); + else + LOG_DBG("rgb: %02x%02x%02x", r, g, b); + + if (_have_alpha != NULL) + *_have_alpha = have_alpha; + if (_alpha != NULL) + *_alpha = alpha; + + *color = r << 16 | g << 8 | b; + return true; +} + +static void +osc_set_pwd(struct terminal *term, char *string) +{ + LOG_DBG("PWD: URI: %s", string); + + char *scheme, *host, *path; + if (!uri_parse(string, strlen(string), &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL)) { + LOG_ERR("OSC7: invalid URI: %s", string); + return; + } + + if (streq(scheme, "file") && hostname_is_localhost(host)) { + LOG_DBG("OSC7: pwd: %s", path); + free(term->cwd); + term->cwd = path; + render_refresh_tab_bar(term); + } else + free(path); + + free(scheme); + free(host); +} + +static void +osc_uri(struct terminal *term, char *string) +{ + /* + * \E]8;;URI\e\\ + * + * Params are key=value pairs, separated by ':'. + * + * The only defined key (as of 2020-05-31) is 'id', which is used + * to group split-up URIs: + * + * ╔═ file1 ════╗ + * ║ ╔═ file2 ═══╗ + * ║http://exa║Lorem ipsum║ + * ║le.com ║ dolor sit ║ + * ║ ║amet, conse║ + * ╚══════════║ctetur adip║ + * ╚═══════════╝ + * + * This lets a terminal emulator highlight both parts at the same + * time (e.g. when hovering over one of the parts with the mouse). + */ + + char *params = string; + char *params_end = strchr(params, ';'); + if (params_end == NULL) + return; + + *params_end = '\0'; + const char *uri = params_end + 1; + uint64_t id = (uint64_t)rand() << 32 | rand(); + + char *ctx = NULL; + for (const char *key_value = strtok_r(params, ":", &ctx); + key_value != NULL; + key_value = strtok_r(NULL, ":", &ctx)) + { + const char *key = key_value; + char *operator = (char *)strchr(key_value, '='); + + if (operator == NULL) + continue; + *operator = '\0'; + + const char *value = operator + 1; + + if (streq(key, "id")) + id = sdbm_hash(value); + } + + + if (uri[0] == '\0') { + LOG_DBG("OSC-8: close"); + term_osc8_close(term); + } else { + LOG_DBG("OSC-8: URL=%s, id=%" PRIu64, uri, id); + term_osc8_open(term, id, uri); + } +} + +static void +osc_notify(struct terminal *term, char *string) +{ + /* + * The 'notify' perl extension + * (https://pub.phyks.me/scripts/urxvt/notify) is very simple: + * + * #!/usr/bin/perl + * + * sub on_osc_seq_perl { + * my ($term, $osc, $resp) = @_; + * if ($osc =~ /^notify;(\S+);(.*)$/) { + * system("notify-send '$1' '$2'"); + * } + * } + * + * As can be seen, the notification text is not encoded in any + * way. The regex does a greedy match of the ';' separator. Thus, + * any extra ';' will end up being part of the title. There's no + * way to have a ';' in the message body. + * + * I've changed that behavior slightly in; we split the title from + * body on the *first* ';', allowing us to have semicolons in the + * message body, but *not* in the title. + */ + char *ctx = NULL; + const char *title = strtok_r(string, ";", &ctx); + const char *msg = strtok_r(NULL, "\x00", &ctx); + + if (title == NULL) + return; + + if (mbsntoc32(NULL, title, strlen(title), 0) == (size_t)-1) { + LOG_WARN("%s: notification title is not valid UTF-8, ignoring", title); + return; + } + + if (msg != NULL && mbsntoc32(NULL, msg, strlen(msg), 0) == (size_t)-1) { + LOG_WARN("%s: notification message is not valid UTF-8, ignoring", msg); + return; + } + + char *msgdup = NULL; + if (msg != NULL) + msgdup = xstrdup(msg); + + notify_notify(term, &(struct notification){ + .title = xstrdup(title), + .body = msgdup, + .expire_time = -1, + .focus = true, + }); +} + +IGNORE_WARNING("-Wpedantic") +static bool +verify_kitty_id_is_valid(const char *id) +{ + const size_t len = strlen(id); + + for (size_t i = 0; i < len; i++) { + switch (id[i]) { + case 'a' ... 'z': + case 'A' ... 'Z': + case '0' ... '9': + case '_': + case '-': + case '+': + case '.': + break; + + default: + return false; + } + } + + return true; +} +UNIGNORE_WARNINGS + +static void +kitty_notification(struct terminal *term, char *string) +{ + /* https://sw.kovidgoyal.net/kitty/desktop-notifications */ + + char *payload_raw = strchr(string, ';'); + if (payload_raw == NULL) + return; + + char *parameters = string; + *payload_raw = '\0'; + payload_raw++; + + char *id = NULL; /* The 'i' parameter */ + char *app_id = NULL; /* The 'f' parameter */ + char *icon_cache_id = NULL; /* The 'g' parameter */ + char *symbolic_icon = NULL; /* The 'n' parameter */ + char *category = NULL; /* The 't' parameter */ + char *sound_name = NULL; /* The 's' parameter */ + char *payload = NULL; + + bool focus = true; /* The 'a' parameter */ + bool report_activated = false; /* The 'a' parameter */ + bool report_closed = false; /* The 'c' parameter */ + bool done = true; /* The 'd' parameter */ + bool base64 = false; /* The 'e' parameter */ + + int32_t expire_time = -1; /* The 'w' parameter */ + + size_t payload_size; + enum { + PAYLOAD_TITLE, + PAYLOAD_BODY, + PAYLOAD_CLOSE, + PAYLOAD_ALIVE, + PAYLOAD_ICON, + PAYLOAD_BUTTON, + } payload_type = PAYLOAD_TITLE; /* The 'p' parameter */ + + enum notify_when when = NOTIFY_ALWAYS; + enum notify_urgency urgency = NOTIFY_URGENCY_NORMAL; + + bool have_a = false; + bool have_c = false; + bool have_o = false; + bool have_u = false; + bool have_w = false; + + char *ctx = NULL; + for (char *param = strtok_r(parameters, ":", &ctx); + param != NULL; + param = strtok_r(NULL, ":", &ctx)) + { + /* All parameters are on the form X=value, where X is always + exactly one character */ + if (param[0] == '\0' || param[1] != '=') + continue; + + char *value = ¶m[2]; + + switch (param[0]) { + case 'a': { + /* notification activation action: focus|report|-focus|-report */ + have_a = true; + char *a_ctx = NULL; + + for (const char *v = strtok_r(value, ",", &a_ctx); + v != NULL; + v = strtok_r(NULL, ",", &a_ctx)) + { + bool reverse = v[0] == '-'; + if (reverse) + v++; + + if (streq(v, "focus")) + focus = !reverse; + else if (streq(v, "report")) + report_activated = !reverse; + } + + break; + } + + case 'c': + if (value[0] == '1' && value[1] == '\0') + report_closed = true; + else if (value[0] == '0' && value[1] == '\0') + report_closed = false; + have_c = true; + break; + + case 'd': + /* done: 0|1 */ + if (value[0] == '0' && value[1] == '\0') + done = false; + else if (value[0] == '1' && value[1] == '\0') + done = true; + break; + + case 'e': + /* base64 (payload encoding): 0=utf8, 1=base64(utf8) */ + if (value[0] == '0' && value[1] == '\0') + base64 = false; + else if (value[0] == '1' && value[1] == '\0') + base64 = true; + break; + + case 'i': + /* id */ + if (verify_kitty_id_is_valid(value)) { + free(id); + id = xstrdup(value); + } else + LOG_WARN("OSC-99: ignoring invalid 'i' identifier"); + break; + + case 'p': + /* payload content: title|body */ + if (streq(value, "title")) + payload_type = PAYLOAD_TITLE; + else if (streq(value, "body")) + payload_type = PAYLOAD_BODY; + else if (streq(value, "close")) + payload_type = PAYLOAD_CLOSE; + else if (streq(value, "alive")) + payload_type = PAYLOAD_ALIVE; + else if (streq(value, "icon")) + payload_type = PAYLOAD_ICON; + else if (streq(value, "buttons")) + payload_type = PAYLOAD_BUTTON; + else if (streq(value, "?")) { + /* Query capabilities */ + + const char *reply_id = id != NULL ? id : "0"; + + const char *p_caps = "title,body,?,close,alive,icon,buttons"; + const char *a_caps = "focus,report"; + const char *u_caps = "0,1,2"; + + char when_caps[64]; + strcpy(when_caps, "unfocused"); + if (!term->conf->desktop_notifications.inhibit_when_focused) + strcat(when_caps, ",always"); + + const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; + + char reply[128]; + size_t n = xsnprintf( + reply, sizeof(reply), + "\033]99;i=%s:p=?;p=%s:a=%s:o=%s:u=%s:c=1:w=1:s=system,silent,error,warn,warning,info,question%s", + reply_id, p_caps, a_caps, when_caps, u_caps, terminator); + + xassert(n < sizeof(reply)); + term_to_slave(term, reply, n); + goto out; + } + break; + + case 'o': + /* honor when: always|unfocused|invisible */ + have_o = true; + if (streq(value, "always")) + when = NOTIFY_ALWAYS; + else if (streq(value, "unfocused")) + when = NOTIFY_UNFOCUSED; + else if (streq(value, "invisible")) + when = NOTIFY_INVISIBLE; + break; + + case 'u': + /* urgency: 0=low, 1=normal, 2=critical */ + have_u = true; + if (value[0] == '0' && value[1] == '\0') + urgency = NOTIFY_URGENCY_LOW; + else if (value[0] == '1' && value[1] == '\0') + urgency = NOTIFY_URGENCY_NORMAL; + else if (value[0] == '2' && value[1] == '\0') + urgency = NOTIFY_URGENCY_CRITICAL; + break; + + case 'w': { + /* Notification timeout */ + errno = 0; + char *end = NULL; + long timeout = strtol(value, &end, 10); + + if (errno == 0 && *end == '\0' && timeout <= INT32_MAX) { + expire_time = timeout; + have_w = true; + } + break; + } + + case 'f': { + /* App-name */ + char *decoded = base64_decode(value, NULL); + if (decoded != NULL) { + free(app_id); + app_id = decoded; + } + break; + } + + case 't': { + /* Type (category) */ + char *decoded = base64_decode(value, NULL); + if (decoded != NULL) { + if (category == NULL) + category = decoded; + else { + /* Append, comma separated */ + char *old_category = category; + category = xstrjoin3(old_category, ",", decoded); + free(decoded); + free(old_category); + } + } + break; + } + + case 's': { + /* Sound */ + char *decoded = base64_decode(value, NULL); + if (decoded != NULL) { + free(sound_name); + sound_name = decoded; + + const char *translated_name = NULL; + + if (streq(decoded, "error")) + translated_name = "dialog-error"; + else if (streq(decoded, "warn") || streq(decoded, "warning")) + translated_name = "dialog-warning"; + else if (streq(decoded, "info")) + translated_name = "dialog-information"; + else if (streq(decoded, "question")) + translated_name = "dialog-question"; + + if (translated_name != NULL) { + free(sound_name); + sound_name = xstrdup(translated_name); + } + } + break; + } + + case 'g': + /* graphical ID (see 'n' and 'p=icon') */ + free(icon_cache_id); + icon_cache_id = xstrdup(value); + break; + + case 'n': { + /* Symbolic icon name, may used with 'g' */ + + /* + * Sigh, protocol says 'n' can be used multiple times, and + * that the terminal picks the first one that it can + * resolve. + * + * We can't resolve any icons at all. So, enter + * heuristics... let's pick the *shortest* symbolic + * name. The idea is that icon *names* are typically + * shorter than .desktop names, and macOS bundle + * identifiers. + */ + char *maybe_new_symbolic_icon = base64_decode(value, NULL); + if (maybe_new_symbolic_icon == NULL) + break; + + if (symbolic_icon == NULL || + strlen(maybe_new_symbolic_icon) < strlen(symbolic_icon)) + { + free(symbolic_icon); + symbolic_icon = maybe_new_symbolic_icon; + + /* Translate OSC-99 "special" names */ + if (symbolic_icon != NULL) { + const char *translated_name = NULL; + + if (streq(symbolic_icon, "error")) + translated_name = "dialog-error"; + else if (streq(symbolic_icon, "warn") || + streq(symbolic_icon, "warning")) + translated_name = "dialog-warning"; + else if (streq(symbolic_icon, "info")) + translated_name = "dialog-information"; + else if (streq(symbolic_icon, "question")) + translated_name = "dialog-question"; + else if (streq(symbolic_icon, "help")) + translated_name = "system-help"; + else if (streq(symbolic_icon, "file-manager")) + translated_name = "system-file-manager"; + else if (streq(symbolic_icon, "system-monitor")) + translated_name = "utilities-system-monitor"; + else if (streq(symbolic_icon, "text-editor")) + translated_name = "text-editor"; + + if (translated_name != NULL) { + free(symbolic_icon); + symbolic_icon = xstrdup(translated_name); + } + } + } else { + free(maybe_new_symbolic_icon); + } + break; + } + } + } + + if (base64) { + payload = base64_decode(payload_raw, &payload_size); + if (payload == NULL) + goto out; + } else { + payload = xstrdup(payload_raw); + payload_size = strlen(payload); + } + + /* Append metadata to previous notification chunk */ + struct notification *notif = &term->kitty_notification; + + if (!((id == NULL && notif->id == NULL) || + (id != NULL && notif->id != NULL && streq(id, notif->id))) || + !notif->may_be_programatically_closed) /* Free:d notification has this as false... */ + { + /* ID mismatch, ignore previous notification state */ + notify_free(term, notif); + + notif->id = id; + notif->when = when; + notif->urgency = urgency; + notif->expire_time = expire_time; + notif->focus = focus; + notif->may_be_programatically_closed = true; + notif->report_activated = report_activated; + notif->report_closed = report_closed; + + id = NULL; /* Prevent double free */ + } + + if (have_a) { + notif->focus = focus; + notif->report_activated = report_activated; + } + + if (have_c) + notif->report_closed = report_closed; + + if (have_o) + notif->when = when; + if (have_u) + notif->urgency = urgency; + if (have_w) + notif->expire_time = expire_time; + + if (icon_cache_id != NULL) { + free(notif->icon_cache_id); + notif->icon_cache_id = icon_cache_id; + icon_cache_id = NULL; /* Prevent double free */ + } + + if (symbolic_icon != NULL) { + free(notif->icon_symbolic_name); + notif->icon_symbolic_name = symbolic_icon; + symbolic_icon = NULL; + } + + if (app_id != NULL) { + free(notif->app_id); + notif->app_id = app_id; + app_id = NULL; /* Prevent double free */ + } + + if (category != NULL) { + if (notif->category == NULL) { + notif->category = category; + category = NULL; /* Prevent double free */ + } else { + /* Append, comma separated */ + char *new_category = xstrjoin3(notif->category, ",", category); + free(notif->category); + notif->category = new_category; + } + } + + if (sound_name != NULL) { + notif->muted = streq(sound_name, "silent"); + + if (notif->muted || streq(sound_name, "system")) { + free(notif->sound_name); + notif->sound_name = NULL; + } else { + free(notif->sound_name); + notif->sound_name = sound_name; + sound_name = NULL; /* Prevent double free */ + } + } + + /* Handled chunked payload - append to existing metadata */ + switch (payload_type) { + case PAYLOAD_TITLE: + case PAYLOAD_BODY: { + char **ptr = payload_type == PAYLOAD_TITLE + ? ¬if->title + : ¬if->body; + + if (*ptr == NULL) { + *ptr = payload; + payload = NULL; + } else { + char *old = *ptr; + *ptr = xstrjoin(old, payload); + free(old); + } + break; + } + + case PAYLOAD_CLOSE: + case PAYLOAD_ALIVE: + /* Ignore payload */ + break; + + case PAYLOAD_ICON: + if (notif->icon_data == NULL) { + notif->icon_data = (uint8_t *)payload; + notif->icon_data_sz = payload_size; + payload = NULL; + } else { + notif->icon_data = xrealloc( + notif->icon_data, notif->icon_data_sz + payload_size); + memcpy(¬if->icon_data[notif->icon_data_sz], payload, payload_size); + notif->icon_data_sz += payload_size; + } + break; + + case PAYLOAD_BUTTON: { + char *ctx = NULL; + for (const char *button = strtok_r(payload, "\u2028", &ctx); + button != NULL; + button = strtok_r(NULL, "\u2028", &ctx)) + { + if (button[0] != '\0') { + tll_push_back(notif->actions, xstrdup(button)); + } + } + + break; + } + } + + if (done) { + /* Update icon cache, if necessary */ + if (notif->icon_cache_id != NULL && + (notif->icon_symbolic_name != NULL || notif->icon_data != NULL)) + { + notify_icon_del(term, notif->icon_cache_id); + notify_icon_add(term, notif->icon_cache_id, + notif->icon_symbolic_name, + notif->icon_data, notif->icon_data_sz); + + /* Don't need this anymore */ + free(notif->icon_symbolic_name); + free(notif->icon_data); + notif->icon_symbolic_name = NULL; + notif->icon_data = NULL; + notif->icon_data_sz = 0; + } + + if (payload_type == PAYLOAD_CLOSE) { + if (notif->id != NULL) + notify_close(term, notif->id); + } else if (payload_type == PAYLOAD_ALIVE) { + char *alive_ids = NULL; + + tll_foreach(term->active_notifications, it) { + /* TODO: check with kitty: use "0" for all + notifications with no ID? */ + + const char *item_id = it->item.id != NULL ? it->item.id : "0"; + + if (alive_ids == NULL) + alive_ids = xstrdup(item_id); + else { + char *old_alive_ids = alive_ids; + alive_ids = xstrjoin3(old_alive_ids, ",", item_id); + free(old_alive_ids); + } + } + + char *reply = xasprintf( + "\033]99;i=%s:p=alive;%s\033\\", + notif->id != NULL ? notif->id : "0", + alive_ids != NULL ? alive_ids : ""); + + term_to_slave(term, reply, strlen(reply)); + free(reply); + free(alive_ids); + } else { + /* + * Show notification. + * + * The checks for title|body is to handle notifications that + * only load icon data into the icon cache + */ + if (notif->title != NULL || notif->body != NULL) { + notify_notify(term, notif); + } + } + + notify_free(term, notif); + } + +out: + free(id); + free(app_id); + free(icon_cache_id); + free(symbolic_icon); + free(payload); + free(category); + free(sound_name); +} + +static void +kitty_text_size(struct terminal *term, char *string) +{ + char *text = strchr(string, ';'); + if (text == NULL) + return; + + char *parameters = string; + *text = '\0'; + text++; + + char32_t *wchars = ambstoc32(text); + if (wchars == NULL) + return; + + int forced_width = 0; + + char *ctx = NULL; + for (char *param = strtok_r(parameters, ":", &ctx); + param != NULL; + param = strtok_r(NULL, ":", &ctx)) + { + /* All parameters are on the form X=value, where X is always + exactly one character */ + if (param[0] == '\0' || param[1] != '=') + continue; + + char *value = ¶m[2]; + + switch (param[0]) { + case 'w': { + errno = 0; + char *end = NULL; + unsigned long w = strtoul(value, &end, 10); + + if (*end == '\0' && errno == 0 && w <= 7) { + forced_width = (int)w; + break; + } else + LOG_ERR("OSC-66: invalid 'w' value, ignoring"); + break; + } + + case 's': + case 'n': + case 'd': + case 'v': + LOG_WARN("OSC-66: unsupported: '%c' parameter, ignoring", param[0]); + break; + } + } + + const size_t len = c32len(wchars); + + if (forced_width == 0) { + /* + * w=0 means we split the text up as we'd normally do... Since + * we don't support any other parameters of the text-sizing + * protocol, that means we just process the string as if it + * has been printed without this OSC. + */ + for (size_t i = 0; i < len; i++) + term_process_and_print_non_ascii(term, wchars[i]); + free(wchars); + return; + } + + size_t max_cp_width = 0; + size_t all_cp_width = 0; + + for (size_t i = 0; i < len; i++) { + const size_t cp_width = c32width(wchars[i]); + all_cp_width += cp_width; + max_cp_width = max(max_cp_width, cp_width); + } + + size_t calculated_width = 0; + switch (term->conf->tweak.grapheme_width_method) { + case GRAPHEME_WIDTH_WCSWIDTH: calculated_width = all_cp_width; break; + case GRAPHEME_WIDTH_MAX: calculated_width = max_cp_width; break; + case GRAPHEME_WIDTH_DOUBLE: calculated_width = min(max_cp_width, 2); break; + } + + const size_t width = forced_width == 0 ? calculated_width : forced_width; + + LOG_DBG("len=%zu, forced=%d, calculated=%zu, using=%zu", + len, forced_width, calculated_width, width); + +#if 0 + if (len == 1 && calculated_width == forced_width) { + /* + * Optimization: if there's a single codepoint, and either + * w=0, or the 'w' matches the calculated width, print + * codepoint directly instead of creating a combining + * character. + */ + term_print(term, wchars[0], width); + free(wchars); + return; + } +#endif + + uint32_t key = composed_key_from_chars(wchars, len); + + const struct composed *composed = composed_lookup_without_collision( + term->composed, &key, wchars, len - 1, wchars[len - 1], forced_width); + + if (composed == NULL) { + struct composed *new_cc = xmalloc(sizeof(*new_cc)); + new_cc->chars = wchars; + new_cc->count = len; + new_cc->key = key; + new_cc->width = width; + new_cc->forced_width = forced_width; + + term->composed_count++; + composed_insert(&term->composed, new_cc); + composed = new_cc; + } else if (composed->width == width) { + free(wchars); + } + + term_print( + term, CELL_COMB_CHARS_LO + composed->key, + composed->forced_width > 0 ? composed->forced_width : composed->width, + false); +} + +void +osc_dispatch(struct terminal *term) +{ + unsigned param = 0; + int data_ofs = 0; + + for (size_t i = 0; i < term->vt.osc.idx; i++, data_ofs++) { + char c = term->vt.osc.data[i]; + + if (c == ';') { + data_ofs++; + break; + } + + if (!isdigit(c)) { + UNHANDLED(); + return; + } + + param *= 10; + param += c - '0'; + } + + LOG_DBG("OSC: %.*s (param = %d)", + (int)term->vt.osc.idx, term->vt.osc.data, param); + + char *string = (char *)&term->vt.osc.data[data_ofs]; + + switch (param) { + case 0: /* icon + title */ + term_set_window_title(term, string); + break; + + case 1: /* icon */ + break; + + case 2: /* title */ + term_set_window_title(term, string); + break; + + case 4: { + /* Set color */ + + string--; + if (*string != ';') + break; + + xassert(*string == ';'); + + for (const char *s_idx = strtok(string, ";"), *s_color = strtok(NULL, ";"); + s_idx != NULL && s_color != NULL; + s_idx = strtok(NULL, ";"), s_color = strtok(NULL, ";")) + { + /* Parse parameter */ + unsigned idx = 0; + for (; *s_idx != '\0'; s_idx++) { + char c = *s_idx; + idx *= 10; + idx += c - '0'; + } + + if (idx >= ALEN(term->colors.table)) { + LOG_WARN("invalid OSC 4 color index: %u", idx); + break; + } + + /* Client queried for current value */ + if (s_color[0] == '?' && s_color[1] == '\0') { + uint32_t color = term->colors.table[idx]; + uint8_t r = (color >> 16) & 0xff; + uint8_t g = (color >> 8) & 0xff; + uint8_t b = (color >> 0) & 0xff; + const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; + + char reply[32]; + size_t n = xsnprintf( + reply, sizeof(reply), + "\033]4;%u;rgb:%02hhx%02hhx/%02hhx%02hhx/%02hhx%02hhx%s", + idx, r, r, g, g, b, b, terminator); + term_to_slave(term, reply, n); + } + + else { + uint32_t color; + bool color_is_valid = s_color[0] == '#' || s_color[0] == '[' + ? parse_legacy_color(s_color, &color, NULL, NULL) + : parse_rgb(s_color, &color, NULL, NULL); + + if (!color_is_valid) + continue; + + LOG_DBG("change color definition for #%u from %06x to %06x", + idx, term->colors.table[idx], color); + + term->colors.table[idx] = color; + term_damage_color(term, COLOR_BASE256, idx); + } + } + + break; + } + + case 7: + /* Update terminal's understanding of PWD */ + osc_set_pwd(term, string); + break; + + case 8: + osc_uri(term, string); + break; + + case 9: { + /* iTerm2 Growl notifications */ + const char *sep = strchr(string, ';'); + if (sep != NULL) { + errno = 0; + char *end = NULL; + strtoul(string, &end, 10); + if (end == sep && errno == 0) { + /* Ignore ConEmu/Windows Terminal escape */ + break; + } + } + + osc_notify(term, string); + break; + } + + case 10: /* fg */ + case 11: /* bg */ + case 12: /* cursor */ + case 17: /* highlight (selection) fg */ + case 19: { /* highlight (selection) bg */ + /* Set default foreground/background/highlight-bg/highlight-fg color */ + + /* Client queried for current value */ + if (string[0] == '?' && string[1] == '\0') { + uint32_t color = param == 10 + ? term->colors.fg + : param == 11 + ? term->colors.bg + : param == 12 + ? term->colors.cursor_bg + : param == 17 + ? term->colors.selection_bg + : term->colors.selection_fg; + + uint8_t r = (color >> 16) & 0xff; + uint8_t g = (color >> 8) & 0xff; + uint8_t b = (color >> 0) & 0xff; + const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; + + /* + * Reply in XParseColor format + * E.g. for color 0xdcdccc we reply "\033]10;rgb:dc/dc/cc\033\\" + */ + char reply[32]; + size_t n = xsnprintf( + reply, sizeof(reply), + "\033]%u;rgb:%02hhx%02hhx/%02hhx%02hhx/%02hhx%02hhx%s", + param, r, r, g, g, b, b, terminator); + + term_to_slave(term, reply, n); + break; + } + + uint32_t color; + bool have_alpha = false; + uint16_t alpha = 0xffff; + + if (string[0] == '#' || string[0] == '[' + ? !parse_legacy_color(string, &color, &have_alpha, &alpha) + : !parse_rgb(string, &color, &have_alpha, &alpha)) + { + break; + } + + LOG_DBG("change color definition for %s to %06x", + param == 10 ? "foreground" : + param == 11 ? "background" : + param == 12 ? "cursor" : + param == 17 ? "selection background" : + "selection foreground", + color); + + switch (param) { + case 10: + term->colors.fg = color; + term_damage_color(term, COLOR_DEFAULT, 0); + break; + + case 11: + term->colors.bg = color; + if (!have_alpha) + alpha = term_theme_get(term)->alpha; + + const bool changed = term->colors.alpha != alpha; + term->colors.alpha = alpha; + + if (changed) { + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + } + + term_damage_color(term, COLOR_DEFAULT, 0); + term_damage_margins(term); + break; + + case 12: + term->colors.cursor_bg = 1u << 31 | color; + term_damage_cursor(term); + break; + + case 17: + term->colors.selection_bg = color; + break; + + case 19: + term->colors.selection_fg = color; + break; + } + + break; + } + + case 22: /* Set mouse cursor */ + term_set_user_mouse_cursor(term, string); + break; + + case 30: /* Set tab title */ + break; + + case 52: /* Copy to/from clipboard/primary */ + osc_selection(term, string); + break; + + case 66: /* text-size protocol (kitty) */ + kitty_text_size(term, string); + break; + + case 99: /* Kitty notifications */ + kitty_notification(term, string); + break; + + case 104: { + /* Reset Color Number 'c' (whole table if no parameter) */ + + const struct color_theme *theme = term_theme_get(term); + + if (string[0] == '\0') { + LOG_DBG("resetting all colors"); + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); + term_damage_view(term); + } + + else { + for (const char *s_idx = strtok(string, ";"); + s_idx != NULL; + s_idx = strtok(NULL, ";")) + { + unsigned idx = 0; + for (; *s_idx != '\0'; s_idx++) { + char c = *s_idx; + idx *= 10; + idx += c - '0'; + } + + if (idx >= ALEN(term->colors.table)) { + LOG_WARN("invalid OSC 104 color index: %u", idx); + continue; + } + + LOG_DBG("resetting color #%u", idx); + term->colors.table[idx] = theme->table[idx]; + term_damage_color(term, COLOR_BASE256, idx); + } + + } + break; + } + + case 105: /* Reset Special Color Number 'c' */ + break; + + case 110: /* Reset default text foreground color */ + LOG_DBG("resetting foreground color"); + + const struct color_theme *theme = term_theme_get(term); + term->colors.fg = theme->fg; + term_damage_color(term, COLOR_DEFAULT, 0); + break; + + case 111: { /* Reset default text background color */ + LOG_DBG("resetting background color"); + + const struct color_theme *theme = term_theme_get(term); + bool alpha_changed = term->colors.alpha != theme->alpha; + + term->colors.bg = theme->bg; + term->colors.alpha = theme->alpha; + + if (alpha_changed) { + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + } + + term_damage_color(term, COLOR_DEFAULT, 0); + term_damage_margins(term); + break; + } + + case 112: { + LOG_DBG("resetting cursor color"); + + const struct color_theme *theme = term_theme_get(term); + term->colors.cursor_fg = theme->cursor.text; + term->colors.cursor_bg = theme->cursor.cursor; + + if (term->conf->colors_dark.use_custom.cursor) { + term->colors.cursor_fg |= 1u << 31; + term->colors.cursor_bg |= 1u << 31; + } + + term_damage_cursor(term); + break; + } + + case 117: { + LOG_DBG("resetting selection background color"); + + const struct color_theme *theme = term_theme_get(term); + term->colors.selection_bg = theme->selection_bg; + break; + } + + case 119: { + LOG_DBG("resetting selection foreground color"); + + const struct color_theme *theme = term_theme_get(term); + term->colors.selection_fg = theme->selection_fg; + break; + } + + case 133: + /* + * Shell integration; see + * https://iterm2.com/documentation-escape-codes.html (Shell + * Integration/FinalTerm) + * + * [PROMPT]prompt% [COMMAND_START] ls -l + * [COMMAND_EXECUTED] + * -rw-r--r-- 1 user group 127 May 1 2016 filename + * [COMMAND_FINISHED] + */ + switch (string[0]) { + case 'A': + LOG_DBG("FTCS_PROMPT: %dx%d", + term->grid->cursor.point.row, + term->grid->cursor.point.col); + + term->grid->cur_row->shell_integration.prompt_marker = true; + break; + + case 'B': + LOG_DBG("FTCS_COMMAND_START"); + break; + + case 'C': + LOG_DBG("FTCS_COMMAND_EXECUTED: %dx%d", + term->grid->cursor.point.row, + term->grid->cursor.point.col); + term->grid->cur_row->shell_integration.cmd_start = term->grid->cursor.point.col; + break; + + case 'D': + LOG_DBG("FTCS_COMMAND_FINISHED: %dx%d", + term->grid->cursor.point.row, + term->grid->cursor.point.col); + term->grid->cur_row->shell_integration.cmd_end = term->grid->cursor.point.col; + break; + } + break; + + case 176: + if (string[0] == '?' && string[1] == '\0') { +#if 0 /* Disabled for now, see #1894 */ + const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; + char *reply = xasprintf( + "\033]176;%s%s", + term->app_id != NULL ? term->app_id : term->conf->app_id, + terminator); + + term_to_slave(term, reply, strlen(reply)); + free(reply); +#else + LOG_WARN("OSC-176 app-id query ignored"); +#endif + break; + } + + term_set_app_id(term, string); + break; + + case 555: + osc_flash(term); + break; + + case 777: { + /* + * OSC 777 is an URxvt generic escape used to send commands to + * perl extensions. The generic syntax is: \E]777;;ST + * + * We only recognize the 'notify' command, which is, if not + * well established, at least fairly well known. + */ + + char *param_brk = strchr(string, ';'); + if (param_brk == NULL) { + UNHANDLED(); + return; + } + + if (strncmp(string, "notify", param_brk - string) == 0) + osc_notify(term, param_brk + 1); + else + UNHANDLED(); + break; + } + + default: + UNHANDLED(); + break; + } +} + +bool +osc_ensure_size(struct terminal *term, size_t required_size) +{ + if (likely(required_size <= term->vt.osc.size)) + return true; + + const size_t pow2_max = ~(SIZE_MAX >> 1); + if (unlikely(required_size > pow2_max)) { + LOG_ERR("required OSC buffer size (%zu) exceeds limit (%zu)", + required_size, pow2_max); + return false; + } + + size_t new_size = max(term->vt.osc.size, 4096); + while (new_size < required_size) { + new_size <<= 1; + } + + uint8_t *new_data = realloc(term->vt.osc.data, new_size); + if (new_data == NULL) { + LOG_ERRNO("failed to increase size of OSC buffer"); + return false; + } + + LOG_DBG("resized OSC buffer: %zu", new_size); + term->vt.osc.data = new_data; + term->vt.osc.size = new_size; + return true; +} diff --git a/osc.h b/osc.h new file mode 100644 index 0000000..0820a8f --- /dev/null +++ b/osc.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include "terminal.h" + +bool osc_ensure_size(struct terminal *term, size_t required_size); +void osc_dispatch(struct terminal *term); diff --git a/pgo/full-current-session.sh b/pgo/full-current-session.sh new file mode 100755 index 0000000..363ee79 --- /dev/null +++ b/pgo/full-current-session.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +set -eux + +srcdir=$(realpath "${1}") +blddir=$(realpath "${2}") + +"${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}" diff --git a/pgo/full-headless-cage.sh b/pgo/full-headless-cage.sh new file mode 100755 index 0000000..50fc750 --- /dev/null +++ b/pgo/full-headless-cage.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -eux + +srcdir=$(realpath "${1}") +blddir=$(realpath "${2}") + +runtime_dir=$(mktemp -d) +trap "rm -rf '${runtime_dir}'" EXIT INT HUP TERM + +XDG_RUNTIME_DIR="${runtime_dir}" WLR_RENDERER=pixman WLR_BACKENDS=headless cage "${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}" + +# Cage's exit code doesn't reflect our script's exit code +[ -f "${blddir}"/pgo-ok ] || exit 1 diff --git a/pgo/full-headless-sway-inner.sh b/pgo/full-headless-sway-inner.sh new file mode 100755 index 0000000..dd612a2 --- /dev/null +++ b/pgo/full-headless-sway-inner.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +set -ux + +srcdir=$(realpath "${1}") +blddir=$(realpath "${2}") + +"${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}" +swaymsg exit diff --git a/pgo/full-headless-sway.sh b/pgo/full-headless-sway.sh new file mode 100755 index 0000000..8f6812b --- /dev/null +++ b/pgo/full-headless-sway.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +set -eux + +srcdir=$(realpath "${1}") +blddir=$(realpath "${2}") + +runtime_dir=$(mktemp -d) +sway_conf=$(mktemp) + +cleanup() { + rm -f "${sway_conf}" + rm -rf "${runtime_dir}" +} +trap cleanup EXIT INT HUP TERM + +# Generate a custom config that executes our generate-pgo-data script +> "${sway_conf}" echo "exec '${srcdir}'/pgo/full-headless-sway-inner.sh '${srcdir}' '${blddir}'" + +# Run Sway. full-headless-sway-inner.sh ends with a 'swaymsg exit' +XDG_RUNTIME_DIR="${runtime_dir}" WLR_RENDERER=pixman WLR_BACKENDS=headless sway -c "${sway_conf}" --unsupported-gpu + +# Sway's exit code doesn't reflect our script's exit code +[ -f "${blddir}"/pgo-ok ] || exit 1 diff --git a/pgo/full-inner.sh b/pgo/full-inner.sh new file mode 100755 index 0000000..c2205e5 --- /dev/null +++ b/pgo/full-inner.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +set -eux + +srcdir=$(realpath "${1}") +blddir=$(realpath "${2}") + +. "${srcdir}"/pgo/options + +pgo_data=$(mktemp) +trap "rm -f '${pgo_data}'" EXIT INT HUP TERM + +rm -f "${blddir}"/pgo-ok + +# To ensure profiling data is generated in the build directory +cd "${blddir}" + +"${blddir}"/utils/xtgettcap +"${blddir}"/footclient --version +"${blddir}"/foot \ + --config=/dev/null \ + --override tweak.grapheme-shaping=no \ + --term=xterm \ + sh -c " + set -eux + + '${srcdir}/scripts/generate-alt-random-writes.py' \ + ${script_options} \"${pgo_data}\" + + cat \"${pgo_data}\" + " +touch "${blddir}"/pgo-ok diff --git a/pgo/options b/pgo/options new file mode 100644 index 0000000..3fb821e --- /dev/null +++ b/pgo/options @@ -0,0 +1 @@ +script_options="--scroll --scroll-region --colors-regular --colors-bright --colors-256 --colors-rgb --attr-bold --attr-italic --attr-underline --sixel" diff --git a/pgo/partial.sh b/pgo/partial.sh new file mode 100755 index 0000000..6d6fdff --- /dev/null +++ b/pgo/partial.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +set -eux + +srcdir=$(realpath "${1}") +blddir=$(realpath "${2}") + +. "${srcdir}"/pgo/options + +pgo_data=$(mktemp) +trap "rm -f ${pgo_data}" EXIT INT HUP TERM + +rm -f "${blddir}"/pgo-ok + +"${srcdir}"/scripts/generate-alt-random-writes.py \ + --rows=67 \ + --cols=135 \ + ${script_options} \ + "${pgo_data}" + +# To ensure profiling data is generated in the build directory +cd "${blddir}" + +"${blddir}"/utils/xtgettcap +"${blddir}"/footclient --version +"${blddir}"/foot --version +"${blddir}"/pgo "${pgo_data}" + +touch "${blddir}"/pgo-ok diff --git a/pgo/pgo.c b/pgo/pgo.c new file mode 100644 index 0000000..96ddcce --- /dev/null +++ b/pgo/pgo.c @@ -0,0 +1,442 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "async.h" +#include "config.h" +#include "key-binding.h" +#include "reaper.h" +#include "sixel.h" +#include "user-notification.h" +#include "vt.h" + +extern bool fdm_ptmx(struct fdm *fdm, int fd, int events, void *data); + +static void +usage(const char *prog_name) +{ + printf( + "Usage: %s stimuli-file1 stimuli-file2 ... stimuli-fileN\n", + prog_name); +} + +enum async_write_status +async_write(int fd, const void *data, size_t len, size_t *idx) +{ + return ASYNC_WRITE_DONE; +} + +bool +fdm_add(struct fdm *fdm, int fd, int events, fdm_fd_handler_t handler, void *data) +{ + return true; +} + +bool +fdm_del(struct fdm *fdm, int fd) +{ + return true; +} + +bool +fdm_event_add(struct fdm *fdm, int fd, int events) +{ + return true; +} + +bool +fdm_event_del(struct fdm *fdm, int fd, int events) +{ + return true; +} + +bool +render_resize( + struct terminal *term, int width, int height, uint8_t resize_options) +{ + return true; +} + +void render_refresh(struct terminal *term) {} +void render_refresh_csd(struct terminal *term) {} +void render_refresh_title(struct terminal *term) {} +void render_refresh_app_id(struct terminal *term) {} +void render_refresh_icon(struct terminal *term) {} +void render_refresh_tab_bar(struct terminal *term) {} + +void render_overlay(struct terminal *term) {} + +void render_buffer_release_callback(struct buffer *buf, void *data) {} + +bool +render_xcursor_is_valid(const struct seat *seat, const char *cursor) +{ + return true; +} + +bool +render_xcursor_set(struct seat *seat, struct terminal *term, enum cursor_shape shape) +{ + return true; +} + +enum cursor_shape +xcursor_for_csd_border(struct terminal *term, int x, int y) +{ + return CURSOR_SHAPE_LEFT_PTR; +} + +struct wl_window * +wayl_win_init(struct terminal *term, const char *token) +{ + return NULL; +} + +void wayl_win_destroy(struct wl_window *win) {} +void wayl_win_alpha_changed(struct wl_window *win) {} +bool wayl_win_set_urgent(struct wl_window *win) { return true; } +bool wayl_win_ring_bell(const struct wl_window *win) { return true; } +bool wayl_fractional_scaling(const struct wayland *wayl) { return true; } + +pid_t +spawn(struct reaper *reaper, const char *cwd, char *const argv[], + int stdin_fd, int stdout_fd, int stderr_fd, + reaper_cb cb, void *cb_data, const char *xdg_activation_token) +{ + return 2; +} + +pid_t +slave_spawn( + int ptmx, int argc, const char *cwd, char *const *argv, char *const *envp, + const env_var_list_t *extra_env_vars, const char *term_env, + const char *conf_shell, bool login_shell, + const user_notifications_t *notifications) +{ + return 0; +} + +int +render_worker_thread(void *_ctx) +{ + return 0; +} + +bool +wayl_do_linear_blending(const struct wayland *wayl, const struct config *conf) +{ + return false; +} + +struct extraction_context * +extract_begin(enum selection_kind kind, bool strip_trailing_empty) +{ + return NULL; +} + +bool +extract_one( + const struct terminal *term, const struct row *row, const struct cell *cell, + int col, void *context) +{ + return true; +} + +bool +extract_finish(struct extraction_context *context, char **text, size_t *len) +{ + return true; +} + +void cmd_scrollback_up(struct terminal *term, int rows) {} +void cmd_scrollback_down(struct terminal *term, int rows) {} + +void ime_enable(struct seat *seat) {} +void ime_disable(struct seat *seat) {} +void ime_reset_preedit(struct seat *seat) {} + +bool +notify_notify(struct terminal *term, struct notification *notif) +{ + return true; +} + +void +notify_close(struct terminal *term, const char *id) +{ +} + +void +notify_free(struct terminal *term, struct notification *notif) +{ +} + +void +notify_icon_add(struct terminal *term, const char *id, + const char *symbolic_name, const uint8_t *data, + size_t data_sz) +{ +} + +void +notify_icon_del(struct terminal *term, const char *id) +{ +} + +void +notify_icon_free(struct notification_icon *icon) +{ +} + +void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data) {} +void reaper_del(struct reaper *reaper, pid_t pid) {} + +void urls_reset(struct terminal *term) {} + +void shm_unref(struct buffer *buf) {} +void shm_chain_free(struct buffer_chain *chain) {} +enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain) { return SHM_BITS_8; } + +struct buffer_chain * +shm_chain_new( + struct wayland *wayl, bool scrollable, size_t pix_instances, + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data) +{ + return NULL; +} + + +void search_selection_cancelled(struct terminal *term) {} + +void get_current_modifiers(const struct seat *seat, + xkb_mod_mask_t *effective, + xkb_mod_mask_t *consumed, uint32_t key, + bool filter_locked) {} + +static struct key_binding_set kbd; +static bool kbd_initialized = false; + +struct key_binding_set * +key_binding_for( + struct key_binding_manager *mgr, const struct config *conf, + const struct seat *seat) +{ + return &kbd; +} + +void +key_binding_new_for_conf( + struct key_binding_manager *mgr, const struct wayland *wayl, + const struct config *conf) +{ + if (!kbd_initialized) { + kbd_initialized = true; + kbd = (struct key_binding_set){ + .key = tll_init(), + .search = tll_init(), + .url = tll_init(), + .mouse = tll_init(), + .selection_overrides = 0, + }; + } +} + +void +key_binding_unref(struct key_binding_manager *mgr, const struct config *conf) +{ +} + +int +main(int argc, const char *const *argv) +{ + if (argc < 2) { + usage(argv[0]); + return EXIT_FAILURE; + } + + const int row_count = 67; + const int col_count = 135; + const int grid_row_count = 16384; + + int lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (lower_fd < 0) + return EXIT_FAILURE; + + int upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (upper_fd < 0) { + close(lower_fd); + return EXIT_FAILURE; + } + + struct row **normal_rows = calloc(grid_row_count, sizeof(normal_rows[0])); + struct row **alt_rows = calloc(grid_row_count, sizeof(alt_rows[0])); + + for (int i = 0; i < grid_row_count; i++) { + normal_rows[i] = calloc(1, sizeof(*normal_rows[i])); + normal_rows[i]->cells = calloc(col_count, sizeof(normal_rows[i]->cells[0])); + alt_rows[i] = calloc(1, sizeof(*alt_rows[i])); + alt_rows[i]->cells = calloc(col_count, sizeof(alt_rows[i]->cells[0])); + } + + struct config conf = { + .tweak = { + .delayed_render_lower_ns = 500000, /* 0.5ms */ + .delayed_render_upper_ns = 16666666 / 2, /* half a frame period (60Hz) */ + }, + }; + + struct wayland wayl = { + .seats = tll_init(), + .monitors = tll_init(), + .terms = tll_init(), + }; + + struct terminal term = { + .conf = &conf, + .wl = &wayl, + .grid = &term.normal, + .normal = { + .num_rows = grid_row_count, + .num_cols = col_count, + .rows = normal_rows, + .cur_row = normal_rows[0], + }, + .alt = { + .num_rows = grid_row_count, + .num_cols = col_count, + .rows = alt_rows, + .cur_row = alt_rows[0], + }, + .scale = 1, + .width = col_count * 8, + .height = row_count * 15, + .cols = col_count, + .rows = row_count, + .cell_width = 8, + .cell_height = 15, + .scroll_region = { + .start = 0, + .end = row_count, + }, + .selection = { + .coords = { + .start = {-1, -1}, + .end = {-1, -1}, + }, + }, + .delayed_render_timer = { + .lower_fd = lower_fd, + .upper_fd = upper_fd + }, + .sixel = { + .palette_size = SIXEL_MAX_COLORS, + .max_width = SIXEL_MAX_WIDTH, + .max_height = SIXEL_MAX_HEIGHT, + }, + }; + + tll_push_back(wayl.terms, &term); + + int ret = EXIT_FAILURE; + + for (int i = 1; i < argc; i++) { + struct stat st; + if (stat(argv[i], &st) < 0) { + fprintf(stderr, "error: %s: failed to stat: %s\n", + argv[i], strerror(errno)); + goto out; + } + + uint8_t *data = malloc(st.st_size); + if (data == NULL) { + fprintf(stderr, "error: %s: failed to allocate buffer: %s\n", + argv[i], strerror(errno)); + goto out; + } + + int fd = open(argv[1], O_RDONLY); + if (fd < 0) { + fprintf(stderr, "error: %s: failed to open: %s\n", + argv[i], strerror(errno)); + goto out; + } + + ssize_t amount = read(fd, data, st.st_size); + if (amount != st.st_size) { + fprintf(stderr, "error: %s: failed to read: %s\n", + argv[i], strerror(errno)); + goto out; + } + + close(fd); + +#if defined(MEMFD_CREATE) + int mem_fd = memfd_create("foot-pgo-ptmx", MFD_CLOEXEC); +#elif defined(__FreeBSD__) + // memfd_create on FreeBSD 13 is SHM_ANON without sealing support + int mem_fd = shm_open(SHM_ANON, O_RDWR | O_CLOEXEC, 0600); +#else + char name[] = "/tmp/foot-pgo-ptmx-XXXXXX"; + int mem_fd = mkostemp(name, O_CLOEXEC); + unlink(name); +#endif + if (mem_fd < 0) { + fprintf(stderr, "error: failed to create memory FD\n"); + goto out; + } + + if (write(mem_fd, data, st.st_size) < 0) { + fprintf(stderr, "error: failed to write memory FD\n"); + close(mem_fd); + goto out; + } + + free(data); + + term.ptmx = mem_fd; + lseek(mem_fd, 0, SEEK_SET); + + printf("Feeding VT parser with %s (%lld bytes)\n", + argv[i], (long long)st.st_size); + + while (lseek(mem_fd, 0, SEEK_CUR) < st.st_size) { + if (!fdm_ptmx(NULL, -1, EPOLLIN, &term)) { + fprintf(stderr, "error: fdm_ptmx() failed\n"); + close(mem_fd); + goto out; + } + } + close(mem_fd); + } + + ret = EXIT_SUCCESS; + +out: + tll_free(wayl.terms); + + for (int i = 0; i < grid_row_count; i++) { + if (normal_rows[i] != NULL) + free(normal_rows[i]->cells); + free(normal_rows[i]); + + if (alt_rows[i] != NULL) + free(alt_rows[i]->cells); + free(alt_rows[i]); + } + + free(normal_rows); + free(alt_rows); + close(lower_fd); + close(upper_fd); + return ret; +} diff --git a/pgo/pgo.sh b/pgo/pgo.sh new file mode 100755 index 0000000..24597c8 --- /dev/null +++ b/pgo/pgo.sh @@ -0,0 +1,116 @@ +#!/bin/sh + +set -eu + +usage_and_die() { + echo "Usage: ${0} none|partial|full-current-session|full-headless-sway|full-headless-cage|[auto] [meson options]" + exit 1 +} + +[ ${#} -ge 3 ] || usage_and_die + +mode=${1} +srcdir=$(realpath "${2}") +blddir=$(realpath "${3}") +shift 3 + +# if [ -e "${blddir}" ]; then +# echo "error: ${blddir}: build directory already exists" +# exit 1 +# fi + +if [ ! -f "${srcdir}"/generate-version.sh ]; then + echo "error: ${srcdir}: does not appear to be a foot source directory" + exit 1 +fi + +compiler=other +do_pgo=no + +CFLAGS="${CFLAGS-} -O3" + +case $(${CC-cc} --version) in + *Free\ Software\ Foundation*) + compiler=gcc + do_pgo=yes + ;; + + *clang*) + compiler=clang + + if command -v llvm-profdata > /dev/null; then + do_pgo=yes + CFLAGS="${CFLAGS} -Wno-ignored-optimization-argument" + fi + ;; +esac + +case ${mode} in + partial|full-current-session|full-headless-sway|full-headless-cage) + ;; + + none) + do_pgo=no + ;; + + auto) + if [ -n "${WAYLAND_DISPLAY+x}" ]; then + mode=full-current-session + elif command -v sway > /dev/null; then + mode=full-headless-sway + elif command -v cage > /dev/null; then + mode=full-headless-cage + else + mode=partial + fi + ;; + + *) + usage_and_die + ;; +esac + +set -x + +# echo "source: ${srcdir}" +# echo "build: ${blddir}" +# echo "compiler: ${compiler}" +# echo "mode: ${mode}" +# echo "CFLAGS: ${CFLAGS}" + +export CFLAGS +export CCACHE_DISABLE=1 +meson setup --buildtype=release -Db_lto=true "${@}" "${blddir}" "${srcdir}" + +if [ ${do_pgo} = yes ]; then + find "${blddir}" \ + '(' \ + -name "*.gcda" -o \ + -name "*.profraw" -o \ + -name default.profdata \ + ')' \ + -delete + + meson configure "${blddir}" -Db_pgo=generate + ninja -C "${blddir}" + + # If fcft/tllist are subprojects, we need to ensure their tests + # have been executed, or we'll get "profile count data file not + # found" errors. + ninja -C "${blddir}" test + + # Run mode-dependent script to generate profiling data + export LLVM_PROFILE_FILE="${blddir}/default_%m.profraw" + "${srcdir}"/pgo/${mode}.sh "${srcdir}" "${blddir}" + + if [ ${compiler} = clang ]; then + llvm-profdata \ + merge \ + "${blddir}"/default_*.profraw \ + --output="${blddir}"/default.profdata + fi + + meson configure "${blddir}" -Db_pgo=use +fi + +ninja -C "${blddir}" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f5fc08a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[tool.pyright] +strict = ['scripts'] + +[tool.mypy] +files = '$MYPY_CONFIG_FILE_DIR/scripts' +strict = true + +[tool.codespell] +skip = 'pyproject.toml,./subprojects,./pkg,./src,./bld,foot.info,./unicode,./venv' +ignore-regex = 'terminfo capability `rin`|\* Simon Ser|\* \[zar\]\(https://codeberg.org/zar\)|iterm theme|iterm.toml|iterm/OneHalfDark.itermcolors' \ No newline at end of file diff --git a/quirks.c b/quirks.c new file mode 100644 index 0000000..67cb587 --- /dev/null +++ b/quirks.c @@ -0,0 +1,86 @@ +#include "quirks.h" + +#include +#include +#include + +#define LOG_MODULE "quirks" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "util.h" + +static bool +is_weston(void) +{ + static bool is_weston = false; + static bool initialized = false; + + if (!initialized) { + initialized = true; + is_weston = getenv("WESTON_CONFIG_FILE") != NULL; + if (is_weston) + LOG_WARN("applying wl_subsurface_set_desync() workaround for weston"); + } + + return is_weston; +} + +void +quirk_weston_subsurface_desync_on(struct wl_subsurface *sub) +{ + if (!is_weston()) + return; + + wl_subsurface_set_desync(sub); +} + +void +quirk_weston_subsurface_desync_off(struct wl_subsurface *sub) +{ + if (!is_weston()) + return; + + wl_subsurface_set_sync(sub); +} + +void +quirk_weston_csd_on(struct terminal *term) +{ + if (term->window->csd_mode != CSD_YES) + return; + if (term->window->is_fullscreen) + return; + + for (int i = 0; i < ALEN(term->window->csd.surface); i++) + quirk_weston_subsurface_desync_on(term->window->csd.surface[i].sub); +} + +void +quirk_weston_csd_off(struct terminal *term) +{ + if (term->window->csd_mode != CSD_YES) + return; + if (term->window->is_fullscreen) + return; + + for (int i = 0; i < ALEN(term->window->csd.surface); i++) + quirk_weston_subsurface_desync_off(term->window->csd.surface[i].sub); +} + +#if 0 +static bool +is_sway(void) +{ + static bool is_sway = false; + static bool initialized = false; + + if (!initialized) { + initialized = true; + is_sway = getenv("SWAYSOCK") != NULL; + if (is_sway) + LOG_WARN("applying wl_surface_damage_buffer() workaround for Sway"); + } + + return is_sway; +} +#endif diff --git a/quirks.h b/quirks.h new file mode 100644 index 0000000..e762bb3 --- /dev/null +++ b/quirks.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "terminal.h" + +/* + * On weston (8.0), synchronized subsurfaces aren't updated correctly. + + * They appear to render once, but after that, updates are + * sporadic. Sometimes they update, most of the time they don't. + * + * Adding explicit parent surface commits right after the subsurface + * commit doesn't help (and would be useless anyway, since it would + * defeat the purpose of having the subsurface synchronized in the + * first place). + */ +void quirk_weston_subsurface_desync_on(struct wl_subsurface *sub); +void quirk_weston_subsurface_desync_off(struct wl_subsurface *sub); + +/* Shortcuts to call desync_{on,off} on all CSD subsurfaces */ +void quirk_weston_csd_on(struct terminal *term); +void quirk_weston_csd_off(struct terminal *term); diff --git a/reaper.c b/reaper.c new file mode 100644 index 0000000..4abc785 --- /dev/null +++ b/reaper.c @@ -0,0 +1,120 @@ +#include "reaper.h" + +#include +#include +#include +#include +#include +#include + +#define LOG_MODULE "reaper" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +struct child { + pid_t pid; + reaper_cb cb; + void *cb_data; +}; + +struct reaper { + struct fdm *fdm; + tll(struct child) children; +}; + +static bool fdm_reap(struct fdm *fdm, int signo, void *data); + +struct reaper * +reaper_init(struct fdm *fdm) +{ + struct reaper *reaper = malloc(sizeof(*reaper)); + if (unlikely(reaper == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } + + *reaper = (struct reaper){ + .fdm = fdm, + .children = tll_init(), + }; + + if (!fdm_signal_add(fdm, SIGCHLD, &fdm_reap, reaper)) + goto err; + + return reaper; + +err: + tll_free(reaper->children); + free(reaper); + return NULL; +} + +void +reaper_destroy(struct reaper *reaper) +{ + if (reaper == NULL) + return; + + fdm_signal_del(reaper->fdm, SIGCHLD); + tll_free(reaper->children); + free(reaper); +} + +void +reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data) +{ + LOG_DBG("adding pid=%d", pid); + tll_push_back( + reaper->children, + ((struct child){.pid = pid, .cb = cb, .cb_data = cb_data})); +} + +void +reaper_del(struct reaper *reaper, pid_t pid) +{ + tll_foreach(reaper->children, it) { + if (it->item.pid == pid) { + tll_remove(reaper->children, it); + break; + } + } +} + +static bool +fdm_reap(struct fdm *fdm, int signo, void *data) +{ + struct reaper *reaper = data; + + while (true) { + int status; + pid_t pid = waitpid(-1, &status, WNOHANG); + if (pid <= 0) + break; + + if (WIFEXITED(status)) + LOG_DBG("pid=%d: exited with status=%d", pid, WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + LOG_DBG("pid=%d: killed by signal=%d", pid, WTERMSIG(status)); + else + LOG_DBG("pid=%d: died of unknown resason", pid); + + tll_foreach(reaper->children, it) { + struct child *_child = &it->item; + + if (_child->pid != pid) + continue; + + /* Make sure we remove it *before* the callback, since it too + * may remove it */ + struct child child = it->item; + tll_remove(reaper->children, it); + + if (child.cb != NULL) + child.cb(reaper, child.pid, status, child.cb_data); + + break; + } + } + + return true; +} diff --git a/reaper.h b/reaper.h new file mode 100644 index 0000000..4416af0 --- /dev/null +++ b/reaper.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +#include "fdm.h" + +struct reaper; + +struct reaper *reaper_init(struct fdm *fdm); +void reaper_destroy(struct reaper *reaper); + +typedef void (*reaper_cb)( + struct reaper *reaper, pid_t pid, int status, void *data); + +void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data); +void reaper_del(struct reaper *reaper, pid_t pid); diff --git a/render.c b/render.c new file mode 100644 index 0000000..c8f1a4e --- /dev/null +++ b/render.c @@ -0,0 +1,6421 @@ +#include "render.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "macros.h" +#if HAS_INCLUDE() + #include + #define pthread_setname_np(thread, name) (pthread_set_name_np(thread, name), 0) +#elif defined(__NetBSD__) + #define pthread_setname_np(thread, name) pthread_setname_np(thread, "%s", (void *)name) +#endif + +#include +#include +#include +#include + +#include + +#define LOG_MODULE "render" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "box-drawing.h" +#include "char32.h" +#include "config.h" +#include "cursor-shape.h" +#include "grid.h" +#include "ime.h" +#include "quirks.h" +#include "search.h" +#include "selection.h" +#include "shm.h" +#include "sixel.h" +#include "srgb.h" +#include "url-mode.h" +#include "util.h" +#include "xmalloc.h" + +#define TIME_SCROLL_DAMAGE 0 + +struct renderer { + struct fdm *fdm; + struct wayland *wayl; +}; + +static struct { + size_t total; + size_t zero; /* commits presented in less than one frame interval */ + size_t one; /* commits presented in one frame interval */ + size_t two; /* commits presented in two or more frame intervals */ +} presentation_statistics = {0}; + +static void fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data); +static void render_tab_bar(struct terminal *term); +static void render_tab_overview(struct terminal *term); + +struct renderer * +render_init(struct fdm *fdm, struct wayland *wayl) +{ + struct renderer *renderer = malloc(sizeof(*renderer)); + if (unlikely(renderer == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } + + *renderer = (struct renderer) { + .fdm = fdm, + .wayl = wayl, + }; + + if (!fdm_hook_add(fdm, &fdm_hook_refresh_pending_terminals, renderer, + FDM_HOOK_PRIORITY_NORMAL)) + { + LOG_ERR("failed to register FDM hook"); + free(renderer); + return NULL; + } + + return renderer; +} + +void +render_destroy(struct renderer *renderer) +{ + if (renderer == NULL) + return; + + fdm_hook_del(renderer->fdm, &fdm_hook_refresh_pending_terminals, + FDM_HOOK_PRIORITY_NORMAL); + + free(renderer); +} + +static void DESTRUCTOR +log_presentation_statistics(void) +{ + if (presentation_statistics.total == 0) + return; + + const size_t total = presentation_statistics.total; + LOG_INFO("presentation statistics: zero=%f%%, one=%f%%, two=%f%%", + 100. * presentation_statistics.zero / total, + 100. * presentation_statistics.one / total, + 100. * presentation_statistics.two / total); +} + +static void +sync_output(void *data, + struct wp_presentation_feedback *wp_presentation_feedback, + struct wl_output *output) +{ +} + +struct presentation_context { + struct terminal *term; + struct timeval input; + struct timeval commit; +}; + +static void +presented(void *data, + struct wp_presentation_feedback *wp_presentation_feedback, + uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec, + uint32_t refresh, uint32_t seq_hi, uint32_t seq_lo, uint32_t flags) +{ + struct presentation_context *ctx = data; + struct terminal *term = ctx->term; + const struct timeval *input = &ctx->input; + const struct timeval *commit = &ctx->commit; + + const struct timeval presented = { + .tv_sec = (uint64_t)tv_sec_hi << 32 | tv_sec_lo, + .tv_usec = tv_nsec / 1000, + }; + + bool use_input = (input->tv_sec > 0 || input->tv_usec > 0) && + timercmp(&presented, input, >); + char msg[1024]; + int chars = 0; + + if (use_input && timercmp(&presented, input, <)) + return; + else if (timercmp(&presented, commit, <)) + return; + + LOG_DBG("commit: %lu s %lu µs, presented: %lu s %lu µs", + commit->tv_sec, commit->tv_usec, presented.tv_sec, presented.tv_usec); + + if (use_input) { + struct timeval diff; + timersub(commit, input, &diff); + chars += snprintf( + &msg[chars], sizeof(msg) - chars, + "input - %llu µs -> ", (unsigned long long)diff.tv_usec); + } + + struct timeval diff; + timersub(&presented, commit, &diff); + chars += snprintf( + &msg[chars], sizeof(msg) - chars, + "commit - %llu µs -> ", (unsigned long long)diff.tv_usec); + + if (use_input) { + xassert(timercmp(&presented, input, >)); + timersub(&presented, input, &diff); + } else { + xassert(timercmp(&presented, commit, >)); + timersub(&presented, commit, &diff); + } + + chars += snprintf( + &msg[chars], sizeof(msg) - chars, + "presented (total: %llu µs)", (unsigned long long)diff.tv_usec); + + unsigned frame_count = 0; + if (tll_length(term->window->on_outputs) > 0) { + const struct monitor *mon = tll_front(term->window->on_outputs); + frame_count = (diff.tv_sec * 1000000. + diff.tv_usec) / (1000000. / mon->refresh); + } + + presentation_statistics.total++; + if (frame_count >= 2) + presentation_statistics.two++; + else if (frame_count >= 1) + presentation_statistics.one++; + else + presentation_statistics.zero++; + +#define _log_fmt "%s (more than %u frames)" + + if (frame_count >= 2) + LOG_ERR(_log_fmt, msg, frame_count); + else if (frame_count >= 1) + LOG_WARN(_log_fmt, msg, frame_count); + else + LOG_INFO(_log_fmt, msg, frame_count); + +#undef _log_fmt + + wp_presentation_feedback_destroy(wp_presentation_feedback); + free(ctx); +} + +static void +discarded(void *data, struct wp_presentation_feedback *wp_presentation_feedback) +{ + struct presentation_context *ctx = data; + wp_presentation_feedback_destroy(wp_presentation_feedback); + free(ctx); +} + +static const struct wp_presentation_feedback_listener presentation_feedback_listener = { + .sync_output = &sync_output, + .presented = &presented, + .discarded = &discarded, +}; + +static struct fcft_font * +attrs_to_font(const struct terminal *term, const struct attributes *attrs) +{ + int idx = attrs->italic << 1 | attrs->bold; + return term->fonts[idx]; +} + +static pixman_color_t +color_hex_to_pixman_srgb(uint32_t color, uint16_t alpha) +{ + return (pixman_color_t){ + .alpha = alpha, /* Consider alpha linear already? */ + .red = srgb_decode_8_to_16((color >> 16) & 0xff), + .green = srgb_decode_8_to_16((color >> 8) & 0xff), + .blue = srgb_decode_8_to_16((color >> 0) & 0xff), + }; +} + +static inline pixman_color_t +color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha, bool srgb) +{ + pixman_color_t ret; + + if (srgb) + ret = color_hex_to_pixman_srgb(color, alpha); + else { + ret = (pixman_color_t){ + .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)), + .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)), + .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)), + .alpha = alpha, + }; + } + + ret.red = (uint32_t)ret.red * alpha / 0xffff; + ret.green = (uint32_t)ret.green * alpha / 0xffff; + ret.blue = (uint32_t)ret.blue * alpha / 0xffff; + + return ret; +} + +static inline pixman_color_t +color_hex_to_pixman(uint32_t color, bool srgb) +{ + /* Count on the compiler optimizing this */ + return color_hex_to_pixman_with_alpha(color, 0xffff, srgb); +} + +static inline int i_lerp(int from, int to, float t) { + return from + (to - from) * t; +} + +static inline uint32_t +color_blend_towards(uint32_t from, uint32_t to, float amount) +{ + if (unlikely(amount == 0)) + return from; + float t = 1 - 1/amount; + + uint32_t alpha = from & 0xff000000; + uint8_t r = i_lerp((from>>16)&0xff, (to>>16)&0xff, t); + uint8_t g = i_lerp((from>>8)&0xff, (to>>8)&0xff, t); + uint8_t b = i_lerp((from>>0)&0xff, (to>>0)&0xff, t); + + return alpha | (r<<16) | (g<<8) | (b<<0); +} + +static inline uint32_t +color_dim(const struct terminal *term, uint32_t color) +{ + const struct config *conf = term->conf; + const uint8_t custom_dim = conf->colors_dark.use_custom.dim; + + if (unlikely(custom_dim != 0)) { + for (size_t i = 0; i < 8; i++) { + if (((custom_dim >> i) & 1) == 0) + continue; + + if (term->colors.table[0 + i] == color) { + /* "Regular" color, return the corresponding "dim" */ + return conf->colors_dark.dim[i]; + } + + else if (term->colors.table[8 + i] == color) { + /* "Bright" color, return the corresponding "regular" */ + return term->colors.table[i]; + } + } + } + + const struct color_theme *theme = term_theme_get(term); + + return color_blend_towards( + color, + theme->dim_blend_towards == DIM_BLEND_TOWARDS_BLACK ? 0x00000000 : 0x00ffffff, + conf->dim.amount); +} + +static inline uint32_t +color_brighten(const struct terminal *term, uint32_t color) +{ + /* + * First try to match the color against the base 8 colors. If we + * find a match, return the corresponding bright color. + */ + if (term->conf->bold_in_bright.palette_based) { + for (size_t i = 0; i < 8; i++) { + if (term->colors.table[i] == color) + return term->colors.table[i + 8]; + } + return color; + } + + return color_blend_towards(color, 0x00ffffff, term->conf->bold_in_bright.amount); +} + +static void +draw_hollow_block(const struct terminal *term, pixman_image_t *pix, + const pixman_color_t *color, int x, int y, int cell_cols) +{ + const int scale = (int)roundf(term->scale); + const int width = min(min(scale, term->cell_width), term->cell_height); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, 4, + (pixman_rectangle16_t []){ + {x, y, cell_cols * term->cell_width, width}, /* top */ + {x, y, width, term->cell_height}, /* left */ + {x + cell_cols * term->cell_width - width, y, width, term->cell_height}, /* right */ + {x, y + term->cell_height - width, cell_cols * term->cell_width, width}, /* bottom */ + }); +} + +static void +draw_beam_cursor(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, int x, int y) +{ + int baseline = y + term->font_baseline - term->fonts[0]->ascent; + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, + 1, &(pixman_rectangle16_t){ + x, baseline, + term_pt_or_px_as_pixels(term, &term->conf->cursor.beam_thickness), + term->fonts[0]->ascent + term->fonts[0]->descent}); +} + +static int +underline_offset(const struct terminal *term, const struct fcft_font *font) +{ + return term->font_baseline - + (term->conf->use_custom_underline_offset + ? -term_pt_or_px_as_pixels(term, &term->conf->underline_offset) + : font->underline.position); +} + +static void +draw_underline_cursor(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, int x, int y, int cols) +{ + int thickness = term->conf->cursor.underline_thickness.px >= 0 + ? term_pt_or_px_as_pixels( + term, &term->conf->cursor.underline_thickness) + : font->underline.thickness; + + /* Make sure the line isn't positioned below the cell */ + const int y_ofs = min(underline_offset(term, font) + thickness, + term->cell_height - thickness); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, + 1, &(pixman_rectangle16_t){ + x, y + y_ofs, cols * term->cell_width, thickness}); +} + +static void +draw_underline(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, int x, int y, int cols) +{ + const int thickness = term->conf->underline_thickness.px >= 0 + ? term_pt_or_px_as_pixels( + term, &term->conf->underline_thickness) + : font->underline.thickness; + + /* Make sure the line isn't positioned below the cell */ + const int y_ofs = min(underline_offset(term, font), + term->cell_height - thickness); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, + 1, &(pixman_rectangle16_t){ + x, y + y_ofs, cols * term->cell_width, thickness}); +} + +static void +draw_styled_underline(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, + enum underline_style style, int x, int y, int cols) +{ + xassert(style != UNDERLINE_NONE); + + if (style == UNDERLINE_SINGLE) { + draw_underline(term, pix, font, color, x, y, cols); + return; + } + + const int thickness = term->conf->underline_thickness.px >= 0 + ? term_pt_or_px_as_pixels( + term, &term->conf->underline_thickness) + : font->underline.thickness; + + int y_ofs; + + /* Make sure the line isn't positioned below the cell */ + switch (style) { + case UNDERLINE_DOUBLE: + case UNDERLINE_CURLY: + y_ofs = min(underline_offset(term, font), + term->cell_height - thickness * 3); + break; + + case UNDERLINE_DASHED: + case UNDERLINE_DOTTED: + y_ofs = min(underline_offset(term, font), + term->cell_height - thickness); + break; + + case UNDERLINE_NONE: + case UNDERLINE_SINGLE: + default: + BUG("unexpected underline style: %d", (int)style); + return; + } + + const int ceil_w = cols * term->cell_width; + + switch (style) { + case UNDERLINE_DOUBLE: { + const pixman_rectangle16_t rects[] = { + {x, y + y_ofs, ceil_w, thickness}, + {x, y + y_ofs + thickness * 2, ceil_w, thickness}}; + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 2, rects); + break; + } + + case UNDERLINE_DASHED: { + const int ceil_w = cols * term->cell_width; + const int dash_w = ceil_w / 3 + (ceil_w % 3 > 0); + const pixman_rectangle16_t rects[] = { + {x, y + y_ofs, dash_w, thickness}, + {x + dash_w * 2, y + y_ofs, dash_w, thickness}, + }; + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, 2, rects); + break; + } + + case UNDERLINE_DOTTED: { + /* Number of dots per cell */ + int per_cell = (term->cell_width / thickness) / 2; + if (per_cell == 0) + per_cell = 1; + + xassert(per_cell >= 1); + + /* Spacing between dots; start with the same width as the dots + themselves, then widen them if necessary, to consume unused + pixels */ + int spacing[per_cell]; + for (int i = 0; i < per_cell; i++) + spacing[i] = thickness; + + /* Pixels remaining at the end of the cell */ + int remaining = term->cell_width - (per_cell * 2) * thickness; + + /* Spread out the left-over pixels across the spacing between + the dots */ + for (int i = 0; remaining > 0; i = (i + 1) % per_cell, remaining--) + spacing[i]++; + + xassert(remaining <= 0); + + pixman_rectangle16_t rects[per_cell]; + int dot_x = x; + for (int i = 0; i < per_cell; i++) { + rects[i] = (pixman_rectangle16_t){ + dot_x, y + y_ofs, thickness, thickness + }; + + dot_x += thickness + spacing[i]; + } + + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, per_cell, rects); + break; + } + + case UNDERLINE_CURLY: { + const int top = y + y_ofs; + const int bot = top + thickness * 3; + const int half_x = x + ceil_w / 2.0, full_x = x + ceil_w; + + const double bt_2 = (bot - top) * (bot - top); + const double th_2 = thickness * thickness; + const double hx_2 = ceil_w * ceil_w / 4.0; + const int th = round(sqrt(th_2 + (th_2 * bt_2 / hx_2)) / 2.); + + #define I(x) pixman_int_to_fixed(x) + const pixman_trapezoid_t traps[] = { +#if 0 /* characters sit within the "dips" of the curlies */ + { + I(top), I(bot), + {{I(x), I(top + th)}, {I(half_x), I(bot + th)}}, + {{I(x), I(top - th)}, {I(half_x), I(bot - th)}}, + }, + { + I(top), I(bot), + {{I(half_x), I(bot - th)}, {I(full_x), I(top - th)}}, + {{I(half_x), I(bot + th)}, {I(full_x), I(top + th)}}, + } +#else /* characters sit on top of the curlies */ + { + I(top), I(bot), + {{I(x), I(bot - th)}, {I(half_x), I(top - th)}}, + {{I(x), I(bot + th)}, {I(half_x), I(top + th)}}, + }, + { + I(top), I(bot), + {{I(half_x), I(top + th)}, {I(full_x), I(bot + th)}}, + {{I(half_x), I(top - th)}, {I(full_x), I(bot - th)}}, + } +#endif + }; + + pixman_image_t *fill = pixman_image_create_solid_fill(color); + pixman_composite_trapezoids( + PIXMAN_OP_OVER, fill, pix, PIXMAN_a8, 0, 0, 0, 0, + sizeof(traps) / sizeof(traps[0]), traps); + + pixman_image_unref(fill); + break; + } + + case UNDERLINE_NONE: + case UNDERLINE_SINGLE: + BUG("underline styles not supposed to be handled here"); + break; + } +} + +static void +draw_strikeout(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, int x, int y, int cols) +{ + const int thickness = term->conf->strikeout_thickness.px >= 0 + ? term_pt_or_px_as_pixels( + term, &term->conf->strikeout_thickness) + : font->strikeout.thickness; + + /* Try to center custom strikeout */ + const int position = term->conf->strikeout_thickness.px >= 0 + ? font->strikeout.position - round(font->strikeout.thickness / 2.) + round(thickness / 2.) + : font->strikeout.position; + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, + 1, &(pixman_rectangle16_t){ + x, y + term->font_baseline - position, + cols * term->cell_width, thickness}); +} + +static void +cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, + const pixman_color_t *fg, const pixman_color_t *bg, + pixman_color_t *cursor_color, pixman_color_t *text_color, + bool gamma_correct) +{ + if (term->colors.cursor_bg >> 31) + *cursor_color = color_hex_to_pixman(term->colors.cursor_bg, gamma_correct); + else + *cursor_color = *fg; + + if (term->colors.cursor_fg >> 31) + *text_color = color_hex_to_pixman(term->colors.cursor_fg, gamma_correct); + else { + xassert(bg->alpha == 0xffff); + *text_color = *bg; + } + + if (text_color->red == cursor_color->red && + text_color->green == cursor_color->green && + text_color->blue == cursor_color->blue) + { + *text_color = color_hex_to_pixman(term->colors.bg, gamma_correct); + *cursor_color = color_hex_to_pixman(term->colors.fg, gamma_correct); + } +} + +static void +draw_cursor(const struct terminal *term, const struct cell *cell, + const struct fcft_font *font, pixman_image_t *pix, pixman_color_t *fg, + const pixman_color_t *bg, int x, int y, int cols) +{ + pixman_color_t cursor_color; + pixman_color_t text_color; + cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color, + wayl_do_linear_blending(term->wl, term->conf)); + + if (unlikely(!term->kbd_focus)) { + switch (term->conf->cursor.unfocused_style) { + case CURSOR_UNFOCUSED_UNCHANGED: + break; + + case CURSOR_UNFOCUSED_HOLLOW: + draw_hollow_block(term, pix, &cursor_color, x, y, cols); + return; + + case CURSOR_UNFOCUSED_NONE: + return; + } + } + + switch (term->cursor_style) { + case CURSOR_BLOCK: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON) || + !term->kbd_focus) + { + *fg = text_color; + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, &cursor_color, 1, + &(pixman_rectangle16_t){x, y, cols * term->cell_width, term->cell_height}); + } + break; + + case CURSOR_BEAM: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || + !term->kbd_focus)) + { + draw_beam_cursor(term, pix, font, &cursor_color, x, y); + } + break; + + case CURSOR_UNDERLINE: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || + !term->kbd_focus)) + { + draw_underline_cursor(term, pix, font, &cursor_color, x, y, cols); + } + break; + + case CURSOR_HOLLOW: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) + draw_hollow_block(term, pix, &cursor_color, x, y, cols); + break; + } +} + +static int +render_cell(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, struct row *row, int row_no, int col, + bool has_cursor) +{ + struct cell *cell = &row->cells[col]; + if (cell->attrs.clean) + return 0; + + cell->attrs.clean = 1; + cell->attrs.confined = true; + + int width = term->cell_width; + int height = term->cell_height; + const int x = term->margins.left + col * width; + const int y = term->margins.top + row_no * height; + + uint32_t _fg = 0; + uint32_t _bg = 0; + + uint16_t alpha = 0xffff; + const bool is_selected = cell->attrs.selected; + + /* Use cell specific color, if set, otherwise the default colors (possible reversed) */ + switch (cell->attrs.fg_src) { + case COLOR_RGB: + _fg = cell->attrs.fg; + break; + + case COLOR_BASE16: + case COLOR_BASE256: + xassert(cell->attrs.fg < ALEN(term->colors.table)); + _fg = term->colors.table[cell->attrs.fg]; + break; + + case COLOR_DEFAULT: + _fg = term->reverse ? term->colors.bg : term->colors.fg; + break; + } + + switch (cell->attrs.bg_src) { + case COLOR_RGB: + _bg = cell->attrs.bg; + break; + + case COLOR_BASE16: + case COLOR_BASE256: + xassert(cell->attrs.bg < ALEN(term->colors.table)); + _bg = term->colors.table[cell->attrs.bg]; + break; + + case COLOR_DEFAULT: + _bg = term->reverse ? term->colors.fg : term->colors.bg; + break; + } + + if (unlikely(is_selected)) { + const uint32_t cell_fg = _fg; + const uint32_t cell_bg = _bg; + + const bool custom_fg = term->colors.selection_fg >> 24 == 0; + const bool custom_bg = term->colors.selection_bg >> 24 == 0; + const bool custom_both = custom_fg && custom_bg; + + if (custom_both) { + _fg = term->colors.selection_fg; + _bg = term->colors.selection_bg; + } else if (custom_bg) { + _bg = term->colors.selection_bg; + _fg = cell->attrs.reverse ? cell_bg : cell_fg; + } else if (custom_fg) { + _fg = term->colors.selection_fg; + _bg = cell->attrs.reverse ? cell_fg : cell_bg; + } else { + _bg = cell_fg; + _fg = cell_bg; + } + + if (unlikely(_fg == _bg)) { + /* Invert bg when selected/highlighted text has same fg/bg */ + _bg = ~_bg; + alpha = 0xffff; + } + + } else { + if (unlikely(cell->attrs.reverse)) { + uint32_t swap = _fg; + _fg = _bg; + _bg = swap; + } + + else if (!term->window->is_fullscreen && term->colors.alpha != 0xffff) { + switch (term->conf->colors_dark.alpha_mode) { + case ALPHA_MODE_DEFAULT: { + if (cell->attrs.bg_src == COLOR_DEFAULT) { + alpha = term->colors.alpha; + } + break; + } + + case ALPHA_MODE_MATCHING: { + if (cell->attrs.bg_src == COLOR_DEFAULT || + ((cell->attrs.bg_src == COLOR_BASE16 || + cell->attrs.bg_src == COLOR_BASE256) && + term->colors.table[cell->attrs.bg] == term->colors.bg) || + (cell->attrs.bg_src == COLOR_RGB && + cell->attrs.bg == term->colors.bg)) + { + alpha = term->colors.alpha; + } + break; + } + + case ALPHA_MODE_ALL: { + alpha = term->colors.alpha; + break; + } + } + } else { + /* + * Note: disable transparency when fullscreened. + * + * This is because the wayland protocol mandates no screen + * content is shown behind the fullscreened window. + * + * The _intent_ of the specification is that a black (or + * other static color) should be used as background. + * + * There's a bit of gray area however, and some + * compositors have chosen to interpret the specification + * in a way that allows wallpapers to be seen through a + * fullscreen window. + * + * Given that a) the intent of the specification, and b) + * we don't know what the compositor will do, we simply + * disable transparency while in fullscreen. + * + * To see why, consider what happens if we keep our + * transparency. For example, if the background color is + * white, and alpha is 0.5, then the window will be drawn + * in a shade of gray while fullscreened. + * + * See + * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/116 + * for a discussion on whether transparent, fullscreen + * windows should be allowed in some way or not. + * + * NOTE: if changing this, also update render_margin() + */ + xassert(alpha == 0xffff); + } + } + + if (cell->attrs.dim) + _fg = color_dim(term, _fg); + if (term->conf->bold_in_bright.enabled && cell->attrs.bold) + _fg = color_brighten(term, _fg); + + if (cell->attrs.blink && term->blink.state == BLINK_OFF) + _fg = color_blend_towards(_fg, 0x00000000, term->conf->dim.amount); + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); + + struct fcft_font *font = attrs_to_font(term, &cell->attrs); + const struct composed *composed = NULL; + const struct fcft_grapheme *grapheme = NULL; + const struct fcft_glyph *single = NULL; + const struct fcft_glyph **glyphs = NULL; + unsigned glyph_count = 0; + + char32_t base = cell->wc; + int cell_cols = 1; + + if (base != 0) { + if (unlikely( + /* Classic box drawings */ + (base >= GLYPH_BOX_DRAWING_FIRST && + base <= GLYPH_BOX_DRAWING_LAST) || + + /* Braille */ + (base >= GLYPH_BRAILLE_FIRST && + base <= GLYPH_BRAILLE_LAST) || + + /* + * Unicode 13 "Symbols for Legacy Computing" + * sub-ranges below. + * + * Note, the full range is U+1FB00 - U+1FBF9 + */ + (base >= GLYPH_LEGACY_FIRST && + base <= GLYPH_LEGACY_LAST) || + + /* + * Unicode 16 "Symbols for Legacy Computing Supplement" + * + * Note, the full range is U+1CC00 - U+1CEAF + */ + (base >= GLYPH_OCTANTS_FIRST && + base <= GLYPH_OCTANTS_LAST)) && + + likely(!term->conf->box_drawings_uses_font_glyphs)) + { + struct fcft_glyph ***arr; + size_t count; + size_t idx; + + if (base >= GLYPH_LEGACY_FIRST) { + arr = &term->custom_glyphs.legacy; + count = GLYPH_LEGACY_COUNT; + idx = base - GLYPH_LEGACY_FIRST; + } else if (base >= GLYPH_OCTANTS_FIRST) { + arr = &term->custom_glyphs.octants; + count = GLYPH_OCTANTS_COUNT; + idx = base - GLYPH_OCTANTS_FIRST; + } else if (base >= GLYPH_BRAILLE_FIRST) { + arr = &term->custom_glyphs.braille; + count = GLYPH_BRAILLE_COUNT; + idx = base - GLYPH_BRAILLE_FIRST; + } else { + arr = &term->custom_glyphs.box_drawing; + count = GLYPH_BOX_DRAWING_COUNT; + idx = base - GLYPH_BOX_DRAWING_FIRST; + } + + if (unlikely(*arr == NULL)) + *arr = xcalloc(count, sizeof((*arr)[0])); + + if (likely((*arr)[idx] != NULL)) + single = (*arr)[idx]; + else { + mtx_lock(&term->render.workers.lock); + + /* Other thread may have instantiated it while we + * acquired the lock */ + single = (*arr)[idx]; + if (likely(single == NULL)) + single = (*arr)[idx] = box_drawing(term, base); + mtx_unlock(&term->render.workers.lock); + } + + if (single != NULL) { + glyph_count = 1; + glyphs = &single; + cell_cols = single->cols; + } + } + + else if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) + { + composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); + base = composed->chars[0]; + + if (term->conf->can_shape_grapheme && term->conf->tweak.grapheme_shaping) { + grapheme = fcft_rasterize_grapheme_utf32( + font, composed->count, composed->chars, term->font_subpixel); + } + + if (grapheme != NULL) { + const int forced_width = composed->forced_width; + + cell_cols = forced_width > 0 ? forced_width : composed->width; + + composed = NULL; + glyphs = grapheme->glyphs; + glyph_count = grapheme->count; + + if (forced_width > 0) + glyph_count = min(glyph_count, forced_width); + } + } + + if (single == NULL && grapheme == NULL) { + if (unlikely(base >= CELL_SPACER)) { + glyph_count = 0; + cell_cols = 1; + } else { + xassert(base != 0); + single = fcft_rasterize_char_utf32(font, base, term->font_subpixel); + if (single == NULL) { + glyph_count = 0; + cell_cols = 1; + } else { + glyph_count = 1; + glyphs = &single; + + const size_t forced_width = composed != NULL ? composed->forced_width : 0; + cell_cols = forced_width > 0 ? forced_width : single->cols; + } + } + } + } + + assert(glyph_count == 0 || glyphs != NULL); + + const int cols_left = term->cols - col; + cell_cols = max(1, min(cell_cols, cols_left)); + + /* + * Determine cells that will bleed into their right neighbor and remember + * them for cleanup in the next frame. + */ + int render_width = cell_cols * width; + if (term->conf->tweak.overflowing_glyphs && + glyph_count > 0 && + cols_left > cell_cols) + { + int glyph_width = 0, advance = 0; + for (size_t i = 0; i < glyph_count; i++) { + glyph_width = max(glyph_width, + advance + glyphs[i]->x + glyphs[i]->width); + advance += glyphs[i]->advance.x; + } + + if (glyph_width > render_width) { + render_width = min(glyph_width, render_width + width); + + for (int i = 0; i < cell_cols; i++) + row->cells[col + i].attrs.confined = false; + } + } + + pixman_region32_t clip; + pixman_region32_init_rect( + &clip, x, y, + render_width, term->cell_height); + pixman_image_set_clip_region32(pix, &clip); + + if (damage != NULL) { + pixman_region32_union_rect( + damage, damage, x, y, render_width, term->cell_height); + } + + pixman_region32_fini(&clip); + + /* Background */ + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, &bg, 1, + &(pixman_rectangle16_t){x, y, cell_cols * width, height}); + + if (cell->attrs.blink && term->blink.fd < 0) { + /* TODO: use a custom lock for this? */ + mtx_lock(&term->render.workers.lock); + term_arm_blink_timer(term); + mtx_unlock(&term->render.workers.lock); + } + + if (unlikely(has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus)) { + const pixman_color_t bg_without_alpha = color_hex_to_pixman(_bg, gamma_correct); + draw_cursor(term, cell, font, pix, &fg, &bg_without_alpha, x, y, cell_cols); + } + + if (cell->wc == 0 || cell->wc >= CELL_SPACER || cell->wc == U'\t' || + (unlikely(cell->attrs.conceal) && !is_selected)) + { + goto draw_cursor; + } + + pixman_image_t *clr_pix = pixman_image_create_solid_fill(&fg); + + int pen_x = x; + for (unsigned i = 0; i < glyph_count; i++) { + const int letter_x_ofs = i == 0 ? term->font_x_ofs : 0; + + const struct fcft_glyph *glyph = glyphs[i]; + if (glyph == NULL) + continue; + + int g_x = glyph->x; + int g_y = glyph->y; + + if (i > 0 && glyph->x >= 0 && cell_cols == 1) + g_x -= term->cell_width; + + if (unlikely(glyph->is_color_glyph)) { + /* Glyph surface is a pre-rendered image (typically a color emoji...) */ + if (!(cell->attrs.blink && term->blink.state == BLINK_OFF)) { + pixman_image_composite32( + PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, 0, + pen_x + letter_x_ofs + g_x, y + term->font_baseline - g_y, + glyph->width, glyph->height); + } + } else { + pixman_image_composite32( + PIXMAN_OP_OVER, clr_pix, glyph->pix, pix, 0, 0, 0, 0, + pen_x + letter_x_ofs + g_x, y + term->font_baseline - g_y, + glyph->width, glyph->height); + + /* Combining characters */ + if (composed != NULL) { + assert(glyph_count == 1); + + for (size_t j = 1; j < composed->count; j++) { + const struct fcft_glyph *g = fcft_rasterize_char_utf32( + font, composed->chars[j], term->font_subpixel); + + if (g == NULL) + continue; + + /* + * Fonts _should_ assume the pen position is now + * *after* the base glyph, and thus use negative + * offsets for combining glyphs. + * + * Not all fonts behave like this however, and we + * try to accommodate both variants. + * + * Since we haven't moved our pen position yet, we + * add a full cell width to the offset (or two, in + * case of double-width characters). + * + * If the font does *not* use negative offsets, + * we'd normally use an offset of 0. However, to + * somewhat deal with double-width glyphs we use + * an offset of *one* cell. + */ + int x_ofs = cell_cols == 1 + ? g->x < 0 + ? cell_cols * term->cell_width + : (cell_cols - 1) * term->cell_width + : 0; + + if (cell_cols > 1) + pen_x += term->cell_width; + + pixman_image_composite32( + PIXMAN_OP_OVER, clr_pix, g->pix, pix, 0, 0, 0, 0, + /* Some fonts use a negative offset, while others use a + * "normal" offset */ + pen_x + letter_x_ofs + x_ofs + g->x, + y + term->font_baseline - g->y, g->width, g->height); + } + } + } + + pen_x += cell_cols > 1 ? term->cell_width : glyph->advance.x; + } + + pixman_image_unref(clr_pix); + + /* Underline */ + if (cell->attrs.underline) { + pixman_color_t underline_color = fg; + enum underline_style underline_style = UNDERLINE_SINGLE; + + /* Check if cell has a styled underline. This lookup is fairly + expensive... */ + if (row->extra != NULL) { + for (int i = 0; i < row->extra->underline_ranges.count; i++) { + const struct row_range *range = &row->extra->underline_ranges.v[i]; + + if (range->start > col) + break; + + if (range->start <= col && col <= range->end) { + switch (range->underline.color_src) { + case COLOR_BASE256: + underline_color = color_hex_to_pixman( + term->colors.table[range->underline.color], gamma_correct); + break; + + case COLOR_RGB: + underline_color = + color_hex_to_pixman(range->underline.color, gamma_correct); + break; + + case COLOR_DEFAULT: + break; + + case COLOR_BASE16: + BUG("underline color can't be base-16"); + break; + } + + underline_style = range->underline.style; + break; + } + } + } + + draw_styled_underline( + term, pix, font, &underline_color, underline_style, x, y, cell_cols); + + } + + if (cell->attrs.strikethrough) + draw_strikeout(term, pix, font, &fg, x, y, cell_cols); + + if (unlikely(cell->attrs.url && term->conf->url.style != UNDERLINE_NONE)) { + pixman_color_t url_color = color_hex_to_pixman( + term->conf->colors_dark.use_custom.url + ? term->conf->colors_dark.url + : term->colors.table[3], + gamma_correct); + + draw_styled_underline( + term, pix, font, &url_color, term->conf->url.style, + x, y, cell_cols); + } + +draw_cursor: + if (has_cursor && (term->cursor_style != CURSOR_BLOCK || !term->kbd_focus)) { + const pixman_color_t bg_without_alpha = color_hex_to_pixman(_bg, gamma_correct); + draw_cursor(term, cell, font, pix, &fg, &bg_without_alpha, x, y, cell_cols); + } + + pixman_image_set_clip_region32(pix, NULL); + return cell_cols; +} + +static void +render_row(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, struct row *row, + int row_no, int cursor_col) +{ + for (int col = term->cols - 1; col >= 0; col--) + render_cell(term, pix, damage, row, row_no, col, cursor_col == col); +} + +static void +render_urgency(struct terminal *term, struct buffer *buf) +{ + uint32_t red = term->colors.table[1]; + pixman_color_t bg = color_hex_to_pixman( + red, wayl_do_linear_blending(term->wl, term->conf)); + + int width = min(min(term->margins.left, term->margins.right), + min(term->margins.top, term->margins.bottom)); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &bg, 4, + (pixman_rectangle16_t[]){ + /* Top */ + {0, 0, term->width, width}, + + /* Bottom */ + {0, term->height - width, term->width, width}, + + /* Left */ + {0, width, width, term->height - 2 * width}, + + /* Right */ + {term->width - width, width, width, term->height - 2 * width}, + }); +} + +static void +render_margin(struct terminal *term, struct buffer *buf, + int start_line, int end_line, bool apply_damage) +{ + /* Fill area outside the cell grid with the default background color */ + const int rmargin = term->width - term->margins.right; + const int bmargin = term->height - term->margins.bottom; + const int line_count = end_line - start_line; + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + const uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; + uint16_t alpha = term->colors.alpha; + + if (term->window->is_fullscreen) { + /* Disable alpha in fullscreen - see render_cell() for details */ + alpha = 0xffff; + } + + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &bg, 4, + (pixman_rectangle16_t[]){ + /* Top */ + {0, 0, term->width, term->margins.top}, + + /* Bottom */ + {0, bmargin, term->width, term->margins.bottom}, + + /* Left */ + {0, term->margins.top + start_line * term->cell_height, + term->margins.left, line_count * term->cell_height}, + + /* Right */ + {rmargin, term->margins.top + start_line * term->cell_height, + term->margins.right, line_count * term->cell_height}, + }); + + if (term->render.urgency) + render_urgency(term, buf); + + /* Ensure the updated regions are copied to the next frame's + * buffer when we're double buffering */ + pixman_region32_union_rect( + &buf->dirty[0], &buf->dirty[0], 0, 0, term->width, term->margins.top); + pixman_region32_union_rect( + &buf->dirty[0], &buf->dirty[0], 0, bmargin, term->width, term->margins.bottom); + pixman_region32_union_rect( + &buf->dirty[0], &buf->dirty[0], 0, 0, term->margins.left, term->height); + pixman_region32_union_rect( + &buf->dirty[0], &buf->dirty[0], + rmargin, 0, term->margins.right, term->height); + + if (apply_damage) { + /* Top */ + wl_surface_damage_buffer( + term->window->surface.surf, 0, 0, term->width, term->margins.top); + + /* Bottom */ + wl_surface_damage_buffer( + term->window->surface.surf, 0, bmargin, term->width, term->margins.bottom); + + /* Left */ + wl_surface_damage_buffer( + term->window->surface.surf, + 0, term->margins.top + start_line * term->cell_height, + term->margins.left, line_count * term->cell_height); + + /* Right */ + wl_surface_damage_buffer( + term->window->surface.surf, + rmargin, term->margins.top + start_line * term->cell_height, + term->margins.right, line_count * term->cell_height); + } +} + +static void +grid_render_scroll(struct terminal *term, struct buffer *buf, + const struct damage *dmg) +{ + LOG_DBG( + "damage: SCROLL: %d-%d by %d lines", + dmg->region.start, dmg->region.end, dmg->lines); + + const int region_size = dmg->region.end - dmg->region.start; + + if (dmg->lines >= region_size) { + /* The entire scroll region will be scrolled out (i.e. replaced) */ + return; + } + + const int height = (region_size - dmg->lines) * term->cell_height; + xassert(height > 0); + +#if TIME_SCROLL_DAMAGE + struct timespec start_time; + clock_gettime(CLOCK_MONOTONIC, &start_time); +#endif + + int dst_y = term->margins.top + (dmg->region.start + 0) * term->cell_height; + int src_y = term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height; + + /* + * SHM scrolling can be *much* faster, but it depends on how many + * lines we're scrolling, and how much repairing we need to do. + * + * In short, scrolling a *large* number of rows is faster with a + * memmove, while scrolling a *small* number of lines is faster + * with SHM scrolling. + * + * However, since we need to restore the scrolling regions when + * SHM scrolling, we also need to take this into account. + * + * Finally, we also have to restore the window margins, and this + * is a *huge* performance hit when scrolling a large number of + * lines (in addition to the sloweness of SHM scrolling as + * method). + * + * So, we need to figure out when to SHM scroll, and when to + * memmove. + * + * For now, assume that the both methods perform roughly the same, + * given an equal number of bytes to move/allocate, and use the + * method that results in the least amount of bytes to touch. + * + * Since number of lines directly translates to bytes, we can + * simply count lines. + * + * SHM scrolling needs to first "move" (punch hole + allocate) + * dmg->lines number of lines, and then we need to restore + * the bottom scroll region. + * + * If the total number of lines is less than half the screen - use + * SHM. Otherwise use memmove. + */ + bool try_shm_scroll = + shm_can_scroll(buf) && ( + dmg->lines + + dmg->region.start + + (term->rows - dmg->region.end)) < term->rows / 2; + + bool did_shm_scroll = false; + + //try_shm_scroll = false; + //try_shm_scroll = true; + + if (try_shm_scroll) { + did_shm_scroll = shm_scroll( + buf, dmg->lines * term->cell_height, + term->margins.top, dmg->region.start * term->cell_height, + term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height); + } + + if (did_shm_scroll) { + /* Restore margins */ + render_margin( + term, buf, dmg->region.end - dmg->lines, term->rows, false); + } else { + /* Fallback for when we either cannot do SHM scrolling, or it failed */ + uint8_t *raw = buf->data; + memmove(raw + dst_y * buf->stride, + raw + src_y * buf->stride, + height * buf->stride); + } + +#if TIME_SCROLL_DAMAGE + struct timespec end_time; + clock_gettime(CLOCK_MONOTONIC, &end_time); + + struct timespec memmove_time; + timespec_sub(&end_time, &start_time, &memmove_time); + LOG_INFO("scrolled %dKB (%d lines) using %s in %lds %ldns", + height * buf->stride / 1024, dmg->lines, + did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", + (long)memmove_time.tv_sec, memmove_time.tv_nsec); +#endif + + wl_surface_damage_buffer( + term->window->surface.surf, term->margins.left, dst_y, + term->width - term->margins.left - term->margins.right, height); + + /* + * TODO: remove this if re-enabling scroll damage when re-applying + * last frame's damage (see reapply_old_damage() + */ + pixman_region32_union_rect( + &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); +} + +static void +grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, + const struct damage *dmg) +{ + LOG_DBG( + "damage: SCROLL REVERSE: %d-%d by %d lines", + dmg->region.start, dmg->region.end, dmg->lines); + + const int region_size = dmg->region.end - dmg->region.start; + + if (dmg->lines >= region_size) { + /* The entire scroll region will be scrolled out (i.e. replaced) */ + return; + } + + const int height = (region_size - dmg->lines) * term->cell_height; + xassert(height > 0); + +#if TIME_SCROLL_DAMAGE + struct timespec start_time; + clock_gettime(CLOCK_MONOTONIC, &start_time); +#endif + + int src_y = term->margins.top + (dmg->region.start + 0) * term->cell_height; + int dst_y = term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height; + + bool try_shm_scroll = + shm_can_scroll(buf) && ( + dmg->lines + + dmg->region.start + + (term->rows - dmg->region.end)) < term->rows / 2; + + bool did_shm_scroll = false; + + if (try_shm_scroll) { + did_shm_scroll = shm_scroll( + buf, -dmg->lines * term->cell_height, + term->margins.top, dmg->region.start * term->cell_height, + term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height); + } + + if (did_shm_scroll) { + /* Restore margins */ + render_margin( + term, buf, dmg->region.start, dmg->region.start + dmg->lines, false); + } else { + /* Fallback for when we either cannot do SHM scrolling, or it failed */ + uint8_t *raw = buf->data; + memmove(raw + dst_y * buf->stride, + raw + src_y * buf->stride, + height * buf->stride); + } + +#if TIME_SCROLL_DAMAGE + struct timespec end_time; + clock_gettime(CLOCK_MONOTONIC, &end_time); + + struct timespec memmove_time; + timespec_sub(&end_time, &start_time, &memmove_time); + LOG_INFO("scrolled REVERSE %dKB (%d lines) using %s in %lds %ldns", + height * buf->stride / 1024, dmg->lines, + did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", + (long)memmove_time.tv_sec, memmove_time.tv_nsec); +#endif + + wl_surface_damage_buffer( + term->window->surface.surf, term->margins.left, dst_y, + term->width - term->margins.left - term->margins.right, height); + + /* + * TODO: remove this if re-enabling scroll damage when re-applying + * last frame's damage (see reapply_old_damage() + */ + pixman_region32_union_rect( + &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); +} + +static void +render_sixel_chunk(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, const struct sixel *sixel, + int term_start_row, int img_start_row, int count) +{ + /* Translate row/column to x/y pixel values */ + const int x = term->margins.left + sixel->pos.col * term->cell_width; + const int y = term->margins.top + term_start_row * term->cell_height; + + /* Width/height, in pixels - and don't touch the window margins */ + const int width = max( + 0, + min(sixel->width, + term->width - x - term->margins.right)); + const int height = max( + 0, + min( + min(count * term->cell_height, /* 'count' number of rows */ + sixel->height - img_start_row * term->cell_height), /* What remains of the sixel */ + term->height - y - term->margins.bottom)); + + /* Verify we're not stepping outside the grid */ + xassert(x >= term->margins.left); + xassert(y >= term->margins.top); + xassert(width == 0 || x + width <= term->width - term->margins.right); + xassert(height == 0 || y + height <= term->height - term->margins.bottom); + + //LOG_DBG("sixel chunk: %dx%d %dx%d", x, y, width, height); + + pixman_image_composite32( + sixel->opaque ? PIXMAN_OP_SRC : PIXMAN_OP_OVER, + sixel->pix, + NULL, + pix, + 0, img_start_row * term->cell_height, + 0, 0, + x, y, + width, height); + + if (damage != NULL) + pixman_region32_union_rect(damage, damage, x, y, width, height); +} + +static void +render_sixel(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, const struct coord *cursor, + const struct sixel *sixel) +{ + xassert(sixel->pix != NULL); + xassert(sixel->width >= 0); + xassert(sixel->height >= 0); + + const int view_end = (term->grid->view + term->rows - 1) & (term->grid->num_rows - 1); + const bool last_row_needs_erase = sixel->height % term->cell_height != 0; + const bool last_col_needs_erase = sixel->width % term->cell_width != 0; + + int chunk_img_start = -1; /* Image-relative start row of chunk */ + int chunk_term_start = -1; /* Viewport relative start row of chunk */ + int chunk_row_count = 0; /* Number of rows to emit */ + +#define maybe_emit_sixel_chunk_then_reset() \ + if (chunk_row_count != 0) { \ + render_sixel_chunk( \ + term, pix, damage, sixel, \ + chunk_term_start, chunk_img_start, chunk_row_count); \ + chunk_term_start = chunk_img_start = -1; \ + chunk_row_count = 0; \ + } + + /* + * Iterate all sixel rows: + * + * - ignore rows that aren't visible on-screen + * - ignore rows that aren't dirty (they have already been rendered) + * - chunk consecutive dirty rows into a 'chunk' + * - emit (render) chunk as soon as a row isn't visible, or is clean + * - emit final chunk after we've iterated all rows + * + * The purpose of this is to reduce the amount of pixels that + * needs to be composited and marked as damaged for the + * compositor. + * + * Since we do CPU based composition, rendering is a slow and + * heavy task for foot, and thus it is important to not re-render + * things unnecessarily. + */ + + for (int _abs_row_no = sixel->pos.row; + _abs_row_no < sixel->pos.row + sixel->rows; + _abs_row_no++) + { + const int abs_row_no = _abs_row_no & (term->grid->num_rows - 1); + const int term_row_no = + (abs_row_no - term->grid->view + term->grid->num_rows) & + (term->grid->num_rows - 1); + + /* Check if row is in the visible viewport */ + if (view_end >= term->grid->view) { + /* Not wrapped */ + if (!(abs_row_no >= term->grid->view && abs_row_no <= view_end)) { + /* Not visible */ + maybe_emit_sixel_chunk_then_reset(); + continue; + } + } else { + /* Wrapped */ + if (!(abs_row_no >= term->grid->view || abs_row_no <= view_end)) { + /* Not visible */ + maybe_emit_sixel_chunk_then_reset(); + continue; + } + } + + /* Is the row dirty? */ + struct row *row = term->grid->rows[abs_row_no]; + xassert(row != NULL); /* Should be visible */ + + if (!row->dirty) { + maybe_emit_sixel_chunk_then_reset(); + continue; + } + + int cursor_col = cursor->row == term_row_no ? cursor->col : -1; + + /* + * If image contains transparent parts, render all (dirty) + * cells beneath it. + * + * If image is opaque, loop cells and set their 'clean' bit, + * to prevent the grid rendered from overwriting the sixel + * + * If the last sixel row only partially covers the cell row, + * 'erase' the cell by rendering them. + * + * In all cases, do *not* clear the 'dirty' bit on the row, to + * ensure the regular renderer includes them in the damage + * rect. + */ + if (!sixel->opaque) { + /* TODO: multithreading */ + render_row(term, pix, damage, row, term_row_no, cursor_col); + } else { + for (int col = sixel->pos.col; + col < min(sixel->pos.col + sixel->cols, term->cols); + col++) + { + struct cell *cell = &row->cells[col]; + + if (!cell->attrs.clean) { + bool last_row = abs_row_no == sixel->pos.row + sixel->rows - 1; + bool last_col = col == sixel->pos.col + sixel->cols - 1; + + if ((last_row_needs_erase && last_row) || + (last_col_needs_erase && last_col)) + { + render_cell(term, pix, damage, row, term_row_no, col, cursor_col == col); + } else { + cell->attrs.clean = 1; + cell->attrs.confined = 1; + } + } + } + } + + if (chunk_term_start == -1) { + xassert(chunk_img_start == -1); + chunk_term_start = term_row_no; + chunk_img_start = _abs_row_no - sixel->pos.row; + chunk_row_count = 1; + } else + chunk_row_count++; + } + + maybe_emit_sixel_chunk_then_reset(); +#undef maybe_emit_sixel_chunk_then_reset +} + +static void +render_sixel_images(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, const struct coord *cursor) +{ + if (likely(tll_length(term->grid->sixel_images)) == 0) + return; + + const int scrollback_end + = (term->grid->offset + term->rows) & (term->grid->num_rows - 1); + + const int view_start + = (term->grid->view + - scrollback_end + + term->grid->num_rows) & (term->grid->num_rows - 1); + + const int view_end = view_start + term->rows - 1; + + //LOG_DBG("SIXELS: %zu images, view=%d-%d", + // tll_length(term->grid->sixel_images), view_start, view_end); + + tll_foreach(term->grid->sixel_images, it) { + const struct sixel *six = &it->item; + const int start + = (six->pos.row + - scrollback_end + + term->grid->num_rows) & (term->grid->num_rows - 1); + const int end = start + six->rows - 1; + + //LOG_DBG(" sixel: %d-%d", start, end); + if (start > view_end) { + /* Sixel starts after view ends, no need to try to render it */ + continue; + } else if (end < view_start) { + /* Image ends before view starts. Since the image list is + * sorted, we can safely stop here */ + break; + } + + sixel_sync_cache(term, &it->item); + render_sixel(term, pix, damage, cursor, &it->item); + } +} + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED +static void +render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, + struct buffer *buf) +{ + if (likely(seat->ime.preedit.cells == NULL)) + return; + + if (unlikely(term->is_searching)) + return; + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + + /* Adjust cursor position to viewport */ + struct coord cursor; + cursor = term->grid->cursor.point; + cursor.row += term->grid->offset; + cursor.row -= term->grid->view; + cursor.row &= term->grid->num_rows - 1; + + if (cursor.row < 0 || cursor.row >= term->rows) + return; + + int cells_needed = seat->ime.preedit.count; + + if (seat->ime.preedit.cursor.start == cells_needed && + seat->ime.preedit.cursor.end == cells_needed) + { + /* Cursor will be drawn *after* the pre-edit string, i.e. in + * the cell *after*. This means we need to copy, and dirty, + * one extra cell from the original grid, or we'll leave + * trailing "cursors" after us if the user deletes text while + * pre-editing */ + cells_needed++; + } + + int row_idx = cursor.row; + int col_idx = cursor.col; + int ime_ofs = 0; /* Offset into pre-edit string to start rendering at */ + + int cells_left = term->cols - cursor.col; + int cells_used = min(cells_needed, term->cols); + + /* Adjust start of pre-edit text to the left if string doesn't fit on row */ + if (cells_left < cells_used) + col_idx -= cells_used - cells_left; + + if (cells_needed > cells_used) { + int start = seat->ime.preedit.cursor.start; + int end = seat->ime.preedit.cursor.end; + + if (start == end) { + /* Ensure *end* of pre-edit string is visible */ + ime_ofs = cells_needed - cells_used; + } else { + /* Ensure the *beginning* of the cursor-area is visible */ + ime_ofs = start; + + /* Display as much as possible of the pre-edit string */ + if (cells_needed - ime_ofs < cells_used) + ime_ofs = cells_needed - cells_used; + } + + /* Make sure we don't start in the middle of a character */ + while (ime_ofs < cells_needed && + seat->ime.preedit.cells[ime_ofs].wc >= CELL_SPACER) + { + ime_ofs++; + } + } + + xassert(col_idx >= 0); + xassert(col_idx < term->cols); + + struct row *row = grid_row_in_view(term->grid, row_idx); + + /* Don't start pre-edit text in the middle of a double-width character */ + while (col_idx > 0 && row->cells[col_idx].wc >= CELL_SPACER) { + cells_used++; + col_idx--; + } + + /* + * Copy original content (render_cell() reads cell data directly + * from grid), and mark all cells as dirty. This ensures they are + * re-rendered when the pre-edit text is modified or removed. + */ + struct cell *real_cells = xmalloc(cells_used * sizeof(real_cells[0])); + for (int i = 0; i < cells_used; i++) { + xassert(col_idx + i < term->cols); + real_cells[i] = row->cells[col_idx + i]; + real_cells[i].attrs.clean = 0; + } + row->dirty = true; + + /* Render pre-edit text */ + xassert(seat->ime.preedit.cells[ime_ofs].wc < CELL_SPACER); + for (int i = 0, idx = ime_ofs; idx < seat->ime.preedit.count; i++, idx++) { + const struct cell *cell = &seat->ime.preedit.cells[idx]; + + if (cell->wc >= CELL_SPACER) + continue; + + int width = max(1, c32width(cell->wc)); + if (col_idx + i + width > term->cols) + break; + + row->cells[col_idx + i] = *cell; + render_cell(term, buf->pix[0], NULL, row, row_idx, col_idx + i, false); + } + + int start = seat->ime.preedit.cursor.start - ime_ofs; + int end = seat->ime.preedit.cursor.end - ime_ofs; + + if (!seat->ime.preedit.cursor.hidden) { + const struct cell *start_cell = &seat->ime.preedit.cells[0]; + + pixman_color_t fg = color_hex_to_pixman(term->colors.fg, gamma_correct); + pixman_color_t bg = color_hex_to_pixman(term->colors.bg, gamma_correct); + + pixman_color_t cursor_color, text_color; + cursor_colors_for_cell( + term, start_cell, &fg, &bg, &cursor_color, &text_color, gamma_correct); + + int x = term->margins.left + (col_idx + start) * term->cell_width; + int y = term->margins.top + row_idx * term->cell_height; + + if (end == start) { + /* Bar */ + if (start >= 0) { + struct fcft_font *font = attrs_to_font(term, &start_cell->attrs); + draw_beam_cursor(term, buf->pix[0], font, &cursor_color, x, y); + } + term_ime_set_cursor_rect(term, x, y, 1, term->cell_height); + } + + else if (end > start) { + /* Hollow cursor */ + if (start >= 0 && end <= term->cols) { + int cols = end - start; + draw_hollow_block(term, buf->pix[0], &cursor_color, x, y, cols); + } + + term_ime_set_cursor_rect( + term, x, y, (end - start) * term->cell_width, term->cell_height); + } + } + + /* Restore original content (but do not render) */ + for (int i = 0; i < cells_used; i++) + row->cells[col_idx + i] = real_cells[i]; + free(real_cells); + + const int damage_x = term->margins.left + col_idx * term->cell_width; + const int damage_y = term->margins.top + row_idx * term->cell_height; + const int damage_w = cells_used * term->cell_width; + const int damage_h = term->cell_height; + + wl_surface_damage_buffer( + term->window->surface.surf, + damage_x, damage_y, damage_w, damage_h); +} +#endif + +static void +render_ime_preedit(struct terminal *term, struct buffer *buf) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) + render_ime_preedit_for_seat(term, &it->item, buf); + } +#endif +} + +static void +render_overlay_single_pixel(struct terminal *term, enum overlay_style style, + pixman_color_t color) +{ + struct wayland *wayl = term->wl; + struct wayl_sub_surface *overlay = &term->window->overlay; + struct wl_buffer *buf = NULL; + + /* + * In an ideal world, we'd only update the surface (i.e. commit + * any changes) if anything has actually changed. + * + * For technical reasons, we can't do that, since we can't + * determine whether the last committed buffer is still valid + * (i.e. does it correspond to the current overlay style, *and* + * does last frame's size match the current size?) + * + * What we _can_ do is use the fact that single-pixel buffers + * don't have a size; you have to use a viewport to "size" them. + * + * This means we can check if the last frame's overlay style is + * the same as the current size. If so, then we *know* that the + * currently attached buffer is valid, and we *don't* have to + * create a new single-pixel buffer. + * + * What we do *not* know if the *size* is still valid. This means + * we do have to do the viewport calls, and a surface commit. + * + * This is still better than *always* creating a new buffer. + */ + + assert(style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH); + assert(wayl->single_pixel_manager != NULL); + assert(overlay->surface.viewport != NULL); + + quirk_weston_subsurface_desync_on(overlay->sub); + + if (style != term->render.last_overlay_style) { + buf = wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer( + wayl->single_pixel_manager, + (double)color.red / 0xffff * 0xffffffff, + (double)color.green / 0xffff * 0xffffffff, + (double)color.blue / 0xffff * 0xffffffff, + (double)color.alpha / 0xffff * 0xffffffff); + + wl_surface_set_buffer_scale(overlay->surface.surf, 1); + wl_surface_attach(overlay->surface.surf, buf, 0, 0); + } + + wp_viewport_set_destination( + overlay->surface.viewport, + roundf(term->width / term->scale), + roundf(term->height / term->scale)); + + wl_subsurface_set_position(overlay->sub, 0, 0); + + wl_surface_damage_buffer( + overlay->surface.surf, 0, 0, term->width, term->height); + + wl_surface_commit(overlay->surface.surf); + quirk_weston_subsurface_desync_off(overlay->sub); + + term->render.last_overlay_style = style; + + if (buf != NULL) { + wl_buffer_destroy(buf); + } +} + +void +render_overlay(struct terminal *term) +{ + struct wayl_sub_surface *overlay = &term->window->overlay; + const bool unicode_mode_active = term->unicode_mode.active; + + const enum overlay_style style = + term->flash.active ? OVERLAY_FLASH : + unicode_mode_active ? OVERLAY_UNICODE_MODE : + OVERLAY_NONE; + + if (likely(style == OVERLAY_NONE)) { + if (term->render.last_overlay_style != OVERLAY_NONE) { + /* Unmap overlay sub-surface */ + wl_surface_attach(overlay->surface.surf, NULL, 0, 0); + wl_surface_commit(overlay->surface.surf); + term->render.last_overlay_style = OVERLAY_NONE; + term->render.last_overlay_buf = NULL; + } + return; + } + + pixman_color_t color; + + switch (style) { + case OVERLAY_SEARCH: + case OVERLAY_UNICODE_MODE: + color = (pixman_color_t){0, 0, 0, 0x7fff}; + break; + + case OVERLAY_FLASH: + color = color_hex_to_pixman_with_alpha( + term->conf->colors_dark.flash, + term->conf->colors_dark.flash_alpha, + wayl_do_linear_blending(term->wl, term->conf)); + break; + + case OVERLAY_NONE: + xassert(false); + break; + } + + const bool single_pixel = + (style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH) && + term->wl->single_pixel_manager != NULL && + overlay->surface.viewport != NULL; + + if (single_pixel) { + render_overlay_single_pixel(term, style, color); + return; + } + + struct buffer *buf = shm_get_buffer( + term->render.chains.overlay, term->width, term->height); + pixman_image_set_clip_region32(buf->pix[0], NULL); + + /* Bounding rectangle of damaged areas - for wl_surface_damage_buffer() */ + pixman_box32_t damage_bounds; + + if (style == OVERLAY_SEARCH) { + /* + * When possible, we only update the areas that have *changed* + * since the last frame. That means: + * + * - clearing/erasing cells that are now selected, but weren't + * in the last frame + * - dimming cells that were selected, but aren't anymore + * + * To do this, we save the last frame's selected cells as a + * pixman region. + * + * Then, we calculate the corresponding region for this + * frame's selected cells. + * + * Last frame's region minus this frame's region gives us the + * region that needs to be *dimmed* in this frame + * + * This frame's region minus last frame's region gives us the + * region that needs to be *cleared* in this frame. + * + * Finally, the union of the two "diff" regions above, gives + * us the total region affected by a change, in either way. We + * use this as the bounding box for the + * wl_surface_damage_buffer() call. + */ + pixman_region32_t *see_through = &term->render.last_overlay_clip; + pixman_region32_t old_see_through; + const bool buffer_reuse = + buf == term->render.last_overlay_buf && + style == term->render.last_overlay_style && + buf->age == 0; + + if (!buffer_reuse) { + /* Can't reuse last frame's damage - set to full window, + * to ensure *everything* is updated */ + pixman_region32_init_rect( + &old_see_through, 0, 0, buf->width, buf->height); + } else { + /* Use last frame's saved region */ + pixman_region32_init(&old_see_through); + pixman_region32_copy(&old_see_through, see_through); + } + + pixman_region32_clear(see_through); + + /* Build region consisting of all current search matches */ + struct search_match_iterator iter = search_matches_new_iter(term); + for (struct range match = search_matches_next(&iter); + match.start.row >= 0; + match = search_matches_next(&iter)) + { + int r = match.start.row; + int start_col = match.start.col; + const int end_row = match.end.row; + + while (true) { + const int end_col = + r == end_row ? match.end.col : term->cols - 1; + + int x = term->margins.left + start_col * term->cell_width; + int y = term->margins.top + r * term->cell_height; + int width = (end_col + 1 - start_col) * term->cell_width; + int height = 1 * term->cell_height; + + pixman_region32_union_rect( + see_through, see_through, x, y, width, height); + + if (++r > end_row) + break; + + start_col = 0; + } + } + + /* Areas that need to be cleared: cells that were dimmed in + * the last frame but is now see-through */ + pixman_region32_t new_see_through; + pixman_region32_init(&new_see_through); + + if (buffer_reuse) + pixman_region32_subtract(&new_see_through, see_through, &old_see_through); + else { + /* Buffer content is unknown - explicitly clear *all* + * current see-through areas */ + pixman_region32_copy(&new_see_through, see_through); + } + pixman_image_set_clip_region32(buf->pix[0], &new_see_through); + + /* Areas that need to be dimmed: cells that were cleared in + * the last frame but is not anymore */ + pixman_region32_t new_dimmed; + pixman_region32_init(&new_dimmed); + pixman_region32_subtract(&new_dimmed, &old_see_through, see_through); + pixman_region32_fini(&old_see_through); + + /* Total affected area */ + pixman_region32_t damage; + pixman_region32_init(&damage); + pixman_region32_union(&damage, &new_see_through, &new_dimmed); + damage_bounds = damage.extents; + + /* Clear cells that became selected in this frame. */ + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &(pixman_color_t){0}, 1, + &(pixman_rectangle16_t){0, 0, term->width, term->height}); + + /* Set clip region for the newly dimmed cells. The actual + * paint call is done below */ + pixman_image_set_clip_region32(buf->pix[0], &new_dimmed); + + pixman_region32_fini(&new_see_through); + pixman_region32_fini(&new_dimmed); + pixman_region32_fini(&damage); + } + + else if (buf == term->render.last_overlay_buf && + style == term->render.last_overlay_style) + { + xassert(style == OVERLAY_FLASH || style == OVERLAY_UNICODE_MODE); + shm_did_not_use_buf(buf); + return; + } else { + pixman_image_set_clip_region32(buf->pix[0], NULL); + damage_bounds = (pixman_box32_t){0, 0, buf->width, buf->height}; + } + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 1, + &(pixman_rectangle16_t){0, 0, term->width, term->height}); + + quirk_weston_subsurface_desync_on(overlay->sub); + wayl_surface_scale( + term->window, &overlay->surface, buf, term->scale); + wl_subsurface_set_position(overlay->sub, 0, 0); + wl_surface_attach(overlay->surface.surf, buf->wl_buf, 0, 0); + + wl_surface_damage_buffer( + overlay->surface.surf, + damage_bounds.x1, damage_bounds.y1, + damage_bounds.x2 - damage_bounds.x1, + damage_bounds.y2 - damage_bounds.y1); + + wl_surface_commit(overlay->surface.surf); + quirk_weston_subsurface_desync_off(overlay->sub); + + buf->age = 0; + term->render.last_overlay_buf = buf; + term->render.last_overlay_style = style; +} + +int +render_worker_thread(void *_ctx) +{ + struct render_worker_context *ctx = _ctx; + struct terminal *term = ctx->term; + const int my_id = ctx->my_id; + free(ctx); + + sigset_t mask; + sigfillset(&mask); + pthread_sigmask(SIG_SETMASK, &mask, NULL); + + char proc_title[16]; + snprintf(proc_title, sizeof(proc_title), "foot:render:%d", my_id); + + if (pthread_setname_np(pthread_self(), proc_title) < 0) + LOG_ERRNO("render worker %d: failed to set process title", my_id); + + sem_t *start = &term->render.workers.start; + sem_t *done = &term->render.workers.done; + mtx_t *lock = &term->render.workers.lock; + + while (true) { + sem_wait(start); + + struct buffer *buf = term->render.workers.buf; + + bool frame_done = false; + + /* Translate offset-relative cursor row to view-relative */ + struct coord cursor = {-1, -1}; + if (!term->hide_cursor) { + cursor = term->grid->cursor.point; + cursor.row += term->grid->offset; + cursor.row -= term->grid->view; + cursor.row &= term->grid->num_rows - 1; + } + + while (!frame_done) { + mtx_lock(lock); + xassert(tll_length(term->render.workers.queue) > 0); + + int row_no = tll_pop_front(term->render.workers.queue); + mtx_unlock(lock); + + switch (row_no) { + default: { + struct row *row = grid_row_in_view(term->grid, row_no); + int cursor_col = cursor.row == row_no ? cursor.col : -1; + + render_row(term, buf->pix[my_id], &buf->dirty[my_id], + row, row_no, cursor_col); + break; + } + + case -1: + frame_done = true; + sem_post(done); + break; + + case -2: + return 0; + + case -3: { + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) + clock_gettime(CLOCK_MONOTONIC, &term->render.workers.preapplied_damage.start); + + mtx_lock(&term->render.workers.preapplied_damage.lock); + buf = term->render.workers.preapplied_damage.buf; + xassert(buf != NULL); + + if (likely(term->render.last_buf != NULL)) { + mtx_unlock(&term->render.workers.preapplied_damage.lock); + + pixman_region32_t dmg; + pixman_region32_init(&dmg); + + if (buf->age == 0) + ; /* No need to do anything */ + else if (buf->age == 1) + pixman_region32_copy(&dmg, + &term->render.last_buf->dirty[0]); + else + pixman_region32_init_rect(&dmg, 0, 0, buf->width, + buf->height); + + pixman_image_set_clip_region32(buf->pix[my_id], &dmg); + pixman_image_composite32(PIXMAN_OP_SRC, + term->render.last_buf->pix[my_id], + NULL, buf->pix[my_id], 0, 0, 0, 0, 0, + 0, buf->width, buf->height); + + pixman_region32_fini(&dmg); + + buf->age = 0; + shm_unref(term->render.last_buf); + shm_addref(buf); + term->render.last_buf = buf; + + mtx_lock(&term->render.workers.preapplied_damage.lock); + } + + term->render.workers.preapplied_damage.buf = NULL; + cnd_signal(&term->render.workers.preapplied_damage.cond); + mtx_unlock(&term->render.workers.preapplied_damage.lock); + + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) + clock_gettime(CLOCK_MONOTONIC, &term->render.workers.preapplied_damage.stop); + + frame_done = true; + break; + } + } + } + }; + + return -1; +} + +void +render_wait_for_preapply_damage(struct terminal *term) +{ + if (!term->render.preapply_last_frame_damage) + return; + if (term->render.workers.preapplied_damage.buf == NULL) + return; + + mtx_lock(&term->render.workers.preapplied_damage.lock); + while (term->render.workers.preapplied_damage.buf != NULL) { + cnd_wait(&term->render.workers.preapplied_damage.cond, + &term->render.workers.preapplied_damage.lock); + } + mtx_unlock(&term->render.workers.preapplied_damage.lock); +} + +struct csd_data +get_csd_data(const struct terminal *term, enum csd_surface surf_idx) +{ + xassert(term->window->csd_mode == CSD_YES); + + const bool borders_visible = wayl_win_csd_borders_visible(term->window); + const bool title_visible = wayl_win_csd_titlebar_visible(term->window); + + const float scale = term->scale; + + const int border_width = borders_visible + ? roundf(term->conf->csd.border_width * scale) : 0; + + const int title_height = title_visible + ? roundf(term->conf->csd.title_height * scale) : 0; + + const int button_width = title_visible + ? roundf(term->conf->csd.button_width * scale) : 0; + + int remaining_width = term->width; + + const int button_close_width = remaining_width >= button_width ? button_width : 0; + remaining_width -= button_close_width; + const int button_close_start = remaining_width; + + const int button_maximize_width = remaining_width >= button_width && + term->window->wm_capabilities.maximize ? button_width : 0; + remaining_width -= button_maximize_width; + const int button_maximize_start = remaining_width; + + const int button_minimize_width = remaining_width >= button_width && + term->window->wm_capabilities.minimize ? button_width : 0; + remaining_width -= button_minimize_width; + const int button_minimize_start = remaining_width; + + /* + * With fractional scaling, we must ensure the offset, when + * divided by the scale (in set_position()), and the scaled back + * (by the compositor), matches the actual pixel count made up by + * the titlebar and the border. + */ + const int top_offset = roundf( + scale * (roundf(-title_height / scale) - roundf(border_width / scale))); + + const int top_bottom_width = roundf( + scale * (roundf(term->width / scale) + 2 * roundf(border_width / scale))); + + const int left_right_height = roundf( + scale * (roundf(title_height / scale) + roundf(term->height / scale))); + + switch (surf_idx) { + case CSD_SURF_TITLE: return (struct csd_data){ 0, -title_height, term->width, title_height}; + case CSD_SURF_LEFT: return (struct csd_data){-border_width, -title_height, border_width, left_right_height}; + case CSD_SURF_RIGHT: return (struct csd_data){ term->width, -title_height, border_width, left_right_height}; + case CSD_SURF_TOP: return (struct csd_data){-border_width, top_offset, top_bottom_width, border_width}; + case CSD_SURF_BOTTOM: return (struct csd_data){-border_width, term->height, top_bottom_width, border_width}; + + /* Positioned relative to CSD_SURF_TITLE */ + case CSD_SURF_MINIMIZE: return (struct csd_data){button_minimize_start, 0, button_minimize_width, title_height}; + case CSD_SURF_MAXIMIZE: return (struct csd_data){button_maximize_start, 0, button_maximize_width, title_height}; + case CSD_SURF_CLOSE: return (struct csd_data){ button_close_start, 0, button_close_width, title_height}; + + case CSD_SURF_COUNT: + break; + } + + BUG("Invalid csd_surface type"); + return (struct csd_data){0}; +} + +static void +csd_commit(struct terminal *term, struct wayl_surface *surf, struct buffer *buf) +{ + wayl_surface_scale(term->window, surf, buf, term->scale); + wl_surface_attach(surf->surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(surf->surf, 0, 0, buf->width, buf->height); + wl_surface_commit(surf->surf); +} + +static void +render_csd_part(struct terminal *term, + struct wl_surface *surf, struct buffer *buf, + int width, int height, pixman_color_t *color) +{ + xassert(term->window->csd_mode == CSD_YES); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], color, 1, + &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); +} + +static void +render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, + struct fcft_font *font, struct buffer *buf, + const char32_t *text, uint32_t _fg, uint32_t _bg, + unsigned x) +{ + pixman_region32_t clip; + pixman_region32_init_rect(&clip, 0, 0, buf->width, buf->height); + pixman_image_set_clip_region32(buf->pix[0], &clip); + pixman_region32_fini(&clip); + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + uint16_t alpha = _bg >> 24 | (_bg >> 24 << 8); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &bg, 1, + &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); + + pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); + const int x_ofs = term->font_x_ofs; + + const size_t len = c32len(text); + struct fcft_text_run *text_run = NULL; + const struct fcft_glyph **glyphs = NULL; + const struct fcft_glyph *_glyphs[len]; + size_t glyph_count = 0; + + if (fcft_capabilities() & FCFT_CAPABILITY_TEXT_RUN_SHAPING) { + text_run = fcft_rasterize_text_run_utf32( + font, len, (const char32_t *)text, term->font_subpixel); + + if (text_run != NULL) { + glyphs = text_run->glyphs; + glyph_count = text_run->count; + } + } + + if (glyphs == NULL) { + for (size_t i = 0; i < len; i++) { + const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( + font, text[i], term->font_subpixel); + + if (glyph == NULL) + continue; + + _glyphs[glyph_count++] = glyph; + } + + glyphs = _glyphs; + } + + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + + /* Calculate baseline */ + unsigned y; + { + const int line_height = buf->height; + const int font_height = max(font->height, font->ascent + font->descent); + const int glyph_top_y = round((line_height - font_height) / 2.); + y = term->font_y_ofs + glyph_top_y + font->ascent; + } + + for (size_t i = 0; i < glyph_count; i++) { + const struct fcft_glyph *glyph = glyphs[i]; + + if (unlikely(glyph->is_color_glyph)) { + pixman_image_composite32( + PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, + x + x_ofs + glyph->x, y - glyph->y, + glyph->width, glyph->height); + } else { + pixman_image_composite32( + PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, + x + x_ofs + glyph->x, y - glyph->y, + glyph->width, glyph->height); + } + + x += glyph->advance.x; + } + + fcft_text_run_destroy(text_run); + pixman_image_unref(src); + pixman_image_set_clip_region32(buf->pix[0], NULL); + + quirk_weston_subsurface_desync_on(sub_surf->sub); + wayl_surface_scale(term->window, &sub_surf->surface, buf, term->scale); + wl_surface_attach(sub_surf->surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(sub_surf->surface.surf, 0, 0, buf->width, buf->height); + + if (alpha == 0xffff) { + struct wl_region *region = wl_compositor_create_region(term->wl->compositor); + if (region != NULL) { + wl_region_add(region, 0, 0, buf->width, buf->height); + wl_surface_set_opaque_region(sub_surf->surface.surf, region); + wl_region_destroy(region); + } + } else + wl_surface_set_opaque_region(sub_surf->surface.surf, NULL); + + wl_surface_commit(sub_surf->surface.surf); + quirk_weston_subsurface_desync_off(sub_surf->sub); +} + +static void +render_csd_title(struct terminal *term, const struct csd_data *info, + struct buffer *buf) +{ + xassert(term->window->csd_mode == CSD_YES); + + struct wayl_sub_surface *surf = &term->window->csd.surface[CSD_SURF_TITLE]; + if (info->width == 0 || info->height == 0) + return; + + uint32_t bg = term->conf->csd.color.title_set + ? term->conf->csd.color.title + : 0xffu << 24 | term->conf->colors_dark.fg; + uint32_t fg = term->conf->csd.color.buttons_set + ? term->conf->csd.color.buttons + : term->conf->colors_dark.bg; + + if (!term->visual_focus) { + bg = color_dim(term, bg); + fg = color_dim(term, fg); + } + + char32_t *_title_text = ambstoc32(term->window_title); + const char32_t *title_text = _title_text != NULL ? _title_text : U""; + + struct wl_window *win = term->window; + + const struct fcft_glyph *M = fcft_rasterize_char_utf32( + win->csd.font, U'M', term->font_subpixel); + + const int margin = M != NULL ? M->advance.x : win->csd.font->max_advance.x; + + render_osd(term, surf, win->csd.font, buf, title_text, fg, bg, margin); + csd_commit(term, &surf->surface, buf); + free(_title_text); +} + +static void +render_csd_border(struct terminal *term, enum csd_surface surf_idx, + const struct csd_data *info, struct buffer *buf) +{ + xassert(term->window->csd_mode == CSD_YES); + xassert(surf_idx >= CSD_SURF_LEFT && surf_idx <= CSD_SURF_BOTTOM); + + struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; + + if (info->width == 0 || info->height == 0) + return; + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + + { + /* Fully transparent - no need to do a color space transform */ + pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0, gamma_correct); + render_csd_part(term, surf->surf, buf, info->width, info->height, &color); + } + + /* + * The "visible" border. + */ + + float scale = term->scale; + int bwidth = (int)roundf(term->conf->csd.border_width * scale); + int vwidth = (int)roundf(term->conf->csd.border_width_visible * scale); /* Visible size */ + + xassert(bwidth >= vwidth); + + if (vwidth > 0) { + + const struct config *conf = term->conf; + int x = 0, y = 0, w = 0, h = 0; + + + switch (surf_idx) { + case CSD_SURF_TOP: + case CSD_SURF_BOTTOM: + x = bwidth - vwidth; + y = surf_idx == CSD_SURF_TOP ? info->height - vwidth : 0; + w = info->width - 2 * x; + h = vwidth; + break; + + case CSD_SURF_LEFT: + case CSD_SURF_RIGHT: + x = surf_idx == CSD_SURF_LEFT ? bwidth - vwidth : 0; + y = 0; + w = vwidth; + h = info->height; + break; + + case CSD_SURF_TITLE: + case CSD_SURF_MINIMIZE: + case CSD_SURF_MAXIMIZE: + case CSD_SURF_CLOSE: + case CSD_SURF_COUNT: + BUG("unexpected CSD surface type"); + } + + xassert(x >= 0); + xassert(y >= 0); + xassert(w >= 0); + xassert(h >= 0); + + xassert(x + w <= info->width); + xassert(y + h <= info->height); + + uint32_t _color = + conf->csd.color.border_set ? conf->csd.color.border : + conf->csd.color.title_set ? conf->csd.color.title : + 0xffu << 24 | term->conf->colors_dark.fg; + if (!term->visual_focus) + _color = color_dim(term, _color); + + uint16_t alpha = _color >> 24 | (_color >> 24 << 8); + pixman_color_t color = + color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 1, + &(pixman_rectangle16_t){x, y, w, h}); + } + + csd_commit(term, surf, buf); +} + +static pixman_color_t +get_csd_button_fg_color(const struct terminal *term) +{ + const struct config *conf = term->conf; + uint32_t _color = conf->colors_dark.bg; + uint16_t alpha = 0xffff; + + if (conf->csd.color.buttons_set) { + _color = conf->csd.color.buttons; + alpha = _color >> 24 | (_color >> 24 << 8); + } + + return color_hex_to_pixman_with_alpha( + _color, alpha, wayl_do_linear_blending(term->wl, term->conf)); +} + +static void +render_csd_button_minimize(struct terminal *term, struct buffer *buf) +{ + pixman_color_t color = get_csd_button_fg_color(term); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; + + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); + + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 1, + (pixman_rectangle16_t[]) { + {x_margin, y_margin + width - thick, width, thick} + }); + + pixman_image_unref(src); +} + +static void +render_csd_button_maximize_maximized( + struct terminal *term, struct buffer *buf) +{ + pixman_color_t color = get_csd_button_fg_color(term); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; + + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); + + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + const int shrink = 1; + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 4, + (pixman_rectangle16_t[]){ + {x_margin + shrink, y_margin + shrink, width - 2 * shrink, thick}, + { x_margin + shrink, y_margin + thick, thick, width - 2 * thick - shrink }, + { x_margin + width - thick - shrink, y_margin + thick, thick, width - 2 * thick - shrink }, + { x_margin + shrink, y_margin + width - thick - shrink, width - 2 * shrink, thick }}); + + pixman_image_unref(src); + +} + +static void +render_csd_button_maximize_window( + struct terminal *term, struct buffer *buf) +{ + pixman_color_t color = get_csd_button_fg_color(term); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; + + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); + + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 4, + (pixman_rectangle16_t[]) { + {x_margin, y_margin, width, thick}, + { x_margin, y_margin + thick, thick, width - 2 * thick }, + { x_margin + width - thick, y_margin + thick, thick, width - 2 * thick }, + { x_margin, y_margin + width - thick, width, thick } + }); + + pixman_image_unref(src); +} + +static void +render_csd_button_maximize(struct terminal *term, struct buffer *buf) +{ + if (term->window->is_maximized) + render_csd_button_maximize_maximized(term, buf); + else + render_csd_button_maximize_window(term, buf); +} + +static void +render_csd_button_close(struct terminal *term, struct buffer *buf) +{ + pixman_color_t color = get_csd_button_fg_color(term); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; + + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + + pixman_triangle_t tri[4] = { + { + .p1 = { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin + thick), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin + width - thick), + .y = pixman_int_to_fixed(y_margin + width), + }, + .p3 = { + .x = pixman_int_to_fixed(x_margin + thick), + .y = pixman_int_to_fixed(y_margin), + }, + }, + + { + .p1 = { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin + width - thick), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin + thick), + .y = pixman_int_to_fixed(y_margin), + }, + .p3 = { + .x = pixman_int_to_fixed(x_margin + width - thick), + .y = pixman_int_to_fixed(y_margin + width), + }, + }, + + { + .p1 = { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin + width - thick), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin + thick), + }, + .p3 = { + .x = pixman_int_to_fixed(x_margin + thick), + .y = pixman_int_to_fixed(y_margin + width), + }, + }, + + { + .p1 = { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin + thick), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin + width - thick), + }, + .p3 = { + .x = pixman_int_to_fixed(x_margin + width - thick), + .y = pixman_int_to_fixed(y_margin), + }, + }, + }; + + pixman_composite_triangles( + PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1, + 0, 0, 0, 0, 4, tri); + + pixman_image_unref(src); +} + +static bool +any_pointer_is_on_button(const struct terminal *term, enum csd_surface csd_surface) +{ + if (unlikely(tll_length(term->wl->seats) == 0)) + return false; + + tll_foreach(term->wl->seats, it) { + const struct seat *seat = &it->item; + + if (seat->mouse.x < 0) + continue; + if (seat->mouse.y < 0) + continue; + + struct csd_data info = get_csd_data(term, csd_surface); + if (seat->mouse.x > info.width) + continue; + + if (seat->mouse.y > info.height) + continue; + return true; + } + + return false; +} + +static void +render_csd_button(struct terminal *term, enum csd_surface surf_idx, + const struct csd_data *info, struct buffer *buf) +{ + xassert(term->window->csd_mode == CSD_YES); + xassert(surf_idx >= CSD_SURF_MINIMIZE && surf_idx <= CSD_SURF_CLOSE); + + struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; + + if (info->width == 0 || info->height == 0) + return; + + uint32_t _color; + uint16_t alpha = 0xffff; + bool is_active = false; + bool is_set = false; + const uint32_t *conf_color = NULL; + + switch (surf_idx) { + case CSD_SURF_MINIMIZE: + _color = term->conf->colors_dark.table[4]; /* blue */ + is_set = term->conf->csd.color.minimize_set; + conf_color = &term->conf->csd.color.minimize; + is_active = term->active_surface == TERM_SURF_BUTTON_MINIMIZE && + any_pointer_is_on_button(term, CSD_SURF_MINIMIZE); + break; + + case CSD_SURF_MAXIMIZE: + _color = term->conf->colors_dark.table[2]; /* green */ + is_set = term->conf->csd.color.maximize_set; + conf_color = &term->conf->csd.color.maximize; + is_active = term->active_surface == TERM_SURF_BUTTON_MAXIMIZE && + any_pointer_is_on_button(term, CSD_SURF_MAXIMIZE); + break; + + case CSD_SURF_CLOSE: + _color = term->conf->colors_dark.table[1]; /* red */ + is_set = term->conf->csd.color.close_set; + conf_color = &term->conf->csd.color.quit; + is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE && + any_pointer_is_on_button(term, CSD_SURF_CLOSE); + break; + + default: + BUG("unhandled surface type: %u", (unsigned)surf_idx); + break; + } + + if (is_active) { + if (is_set) { + _color = *conf_color; + alpha = _color >> 24 | (_color >> 24 << 8); + } + } else { + _color = 0; + alpha = 0; + } + + if (!term->visual_focus) + _color = color_dim(term, _color); + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); + render_csd_part(term, surf->surf, buf, info->width, info->height, &color); + + switch (surf_idx) { + case CSD_SURF_MINIMIZE: render_csd_button_minimize(term, buf); break; + case CSD_SURF_MAXIMIZE: render_csd_button_maximize(term, buf); break; + case CSD_SURF_CLOSE: render_csd_button_close(term, buf); break; + + default: + BUG("unhandled surface type: %u", (unsigned)surf_idx); + break; + } + + csd_commit(term, surf, buf); +} + +static void +render_csd(struct terminal *term) +{ + xassert(term->window->csd_mode == CSD_YES); + + if (term->window->is_fullscreen) + return; + + const float scale = term->scale; + struct csd_data infos[CSD_SURF_COUNT]; + int widths[CSD_SURF_COUNT]; + int heights[CSD_SURF_COUNT]; + + for (size_t i = 0; i < CSD_SURF_COUNT; i++) { + infos[i] = get_csd_data(term, i); + const int x = infos[i].x; + const int y = infos[i].y; + const int width = infos[i].width; + const int height = infos[i].height; + + struct wl_surface *surf = term->window->csd.surface[i].surface.surf; + struct wl_subsurface *sub = term->window->csd.surface[i].sub; + + xassert(surf != NULL); + xassert(sub != NULL); + + if (width == 0 || height == 0) { + widths[i] = heights[i] = 0; + wl_subsurface_set_position(sub, 0, 0); + wl_surface_attach(surf, NULL, 0, 0); + wl_surface_commit(surf); + continue; + } + + widths[i] = width; + heights[i] = height; + wl_subsurface_set_position(sub, roundf(x / scale), roundf(y / scale)); + } + + struct buffer *bufs[CSD_SURF_COUNT]; + shm_get_many(term->render.chains.csd, CSD_SURF_COUNT, widths, heights, bufs); + + for (size_t i = CSD_SURF_LEFT; i <= CSD_SURF_BOTTOM; i++) + render_csd_border(term, i, &infos[i], bufs[i]); + for (size_t i = CSD_SURF_MINIMIZE; i <= CSD_SURF_CLOSE; i++) + render_csd_button(term, i, &infos[i], bufs[i]); + render_csd_title(term, &infos[CSD_SURF_TITLE], bufs[CSD_SURF_TITLE]); +} + +static void +render_scrollback_position(struct terminal *term) +{ + if (term->conf->scrollback.indicator.position == SCROLLBACK_INDICATOR_POSITION_NONE) + return; + + struct wl_window *win = term->window; + + if (term->grid->view == term->grid->offset) { + if (win->scrollback_indicator.surface.surf != NULL) + wayl_win_subsurface_destroy(&win->scrollback_indicator); + return; + } + + if (win->scrollback_indicator.surface.surf == NULL) { + if (!wayl_win_subsurface_new( + win, &win->scrollback_indicator, false)) + { + LOG_ERR("failed to create scrollback indicator surface"); + return; + } + } + + xassert(win->scrollback_indicator.surface.surf != NULL); + xassert(win->scrollback_indicator.sub != NULL); + + /* Find absolute row number of the scrollback start */ + int scrollback_start = term->grid->offset + term->rows; + int empty_rows = 0; + while (term->grid->rows[scrollback_start & (term->grid->num_rows - 1)] == NULL) { + scrollback_start++; + empty_rows++; + } + + /* Rebase viewport against scrollback start (so that 0 is at + * the beginning of the scrollback) */ + int rebased_view = term->grid->view - scrollback_start + term->grid->num_rows; + rebased_view &= term->grid->num_rows - 1; + + /* How much of the scrollback is actually used? */ + int populated_rows = term->grid->num_rows - empty_rows; + xassert(populated_rows > 0); + xassert(populated_rows <= term->grid->num_rows); + + /* + * How far down in the scrollback we are. + * + * 0% -> at the beginning of the scrollback + * 100% -> at the bottom, i.e. where new lines are inserted + */ + double percent = + rebased_view + term->rows == populated_rows + ? 1.0 + : (double)rebased_view / (populated_rows - term->rows); + + char32_t _text[64]; + const char32_t *text = _text; + int cell_count = 0; + + /* *What* to render */ + switch (term->conf->scrollback.indicator.format) { + case SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE: { + char percent_str[8]; + snprintf(percent_str, sizeof(percent_str), "%u%%", (int)(100 * percent)); + mbstoc32(_text, percent_str, ALEN(_text)); + cell_count = 3; + break; + } + + case SCROLLBACK_INDICATOR_FORMAT_LINENO: { + char lineno_str[64]; + snprintf(lineno_str, sizeof(lineno_str), "%d", rebased_view + 1); + mbstoc32(_text, lineno_str, ALEN(_text)); + cell_count = (int)ceilf(log10f(term->grid->num_rows)); + break; + } + + case SCROLLBACK_INDICATOR_FORMAT_TEXT: + text = term->conf->scrollback.indicator.text; + cell_count = c32len(text); + break; + } + + const float scale = term->scale; + const int margin = (int)roundf(3. * scale); + + int width = margin + cell_count * term->cell_width + margin; + int height = margin + term->cell_height + margin; + + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); + + /* *Where* to render - parent relative coordinates */ + int surf_top = 0; + switch (term->conf->scrollback.indicator.position) { + case SCROLLBACK_INDICATOR_POSITION_NONE: + BUG("Invalid scrollback indicator position type"); + return; + + case SCROLLBACK_INDICATOR_POSITION_FIXED: + surf_top = term->cell_height - margin; + break; + + case SCROLLBACK_INDICATOR_POSITION_RELATIVE: { + int lines = term->rows - 2; /* Avoid using first and last rows */ + if (term->is_searching) { + /* Make sure we don't collide with the scrollback search box */ + lines--; + } + + lines = max(lines, 0); + + int pixels = max(lines * term->cell_height - height + 2 * margin, 0); + surf_top = term->cell_height - margin + (int)(percent * pixels); + break; + } + } + + int x = term->width - margin - width; + int y = term->margins.top + surf_top; + + x = roundf(scale * ceilf(x / scale)); + y = roundf(scale * ceilf(y / scale)); + + if (y + height > term->height) { + wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); + wl_surface_commit(win->scrollback_indicator.surface.surf); + return; + } + + struct buffer_chain *chain = term->render.chains.scrollback_indicator; + struct buffer *buf = shm_get_buffer(chain, width, height); + + wl_subsurface_set_position( + win->scrollback_indicator.sub, roundf(x / scale), roundf(y / scale)); + + uint32_t fg = term->colors.table[0]; + uint32_t bg = term->colors.table[8 + 4]; + if (term->conf->colors_dark.use_custom.scrollback_indicator) { + fg = term->conf->colors_dark.scrollback_indicator.fg; + bg = term->conf->colors_dark.scrollback_indicator.bg; + } + + render_osd( + term, + &win->scrollback_indicator, + term->fonts[0], buf, text, + fg, 0xffu << 24 | bg, + width - margin - c32len(text) * term->cell_width); +} + +static void +render_render_timer(struct terminal *term, struct timespec render_time) +{ + struct wl_window *win = term->window; + + char usecs_str[256]; + double usecs = render_time.tv_sec * 1000000 + render_time.tv_nsec / 1000.0; + snprintf(usecs_str, sizeof(usecs_str), "%.2f µs", usecs); + + char32_t text[256]; + mbstoc32(text, usecs_str, ALEN(text)); + + const float scale = term->scale; + const int cell_count = c32len(text); + const int margin = (int)roundf(3. * scale); + + int width = margin + cell_count * term->cell_width + margin; + int height = margin + term->cell_height + margin; + + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); + + struct buffer_chain *chain = term->render.chains.render_timer; + struct buffer *buf = shm_get_buffer(chain, width, height); + + wl_subsurface_set_position( + win->render_timer.sub, + roundf(margin / scale), + roundf((term->margins.top + term->cell_height - margin) / scale)); + + render_osd( + term, + &win->render_timer, + term->fonts[0], buf, text, + term->colors.table[0], 0xffu << 24 | term->colors.table[8 + 1], + margin); +} + +static void frame_callback( + void *data, struct wl_callback *wl_callback, uint32_t callback_data); + +static const struct wl_callback_listener frame_listener = { + .done = &frame_callback, +}; + +static void +force_full_repaint(struct terminal *term, struct buffer *buf) +{ + tll_free(term->grid->scroll_damage); + render_margin(term, buf, 0, term->rows, true); + term_damage_view(term); +} + +static void +reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old) +{ + if (new->age > 1) { + memcpy(new->data, old->data, new->height * new->stride); + return; + } + + pixman_region32_t dirty; + pixman_region32_init(&dirty); + + /* + * Figure out current frame's damage region + * + * If current frame doesn't have any scroll damage, we can simply + * subtract this frame's damage from the last frame's damage. That + * way, we don't have to copy areas from the old frame that'll + * just get overwritten by current frame. + * + * Note that this is row based. A "half damaged" row is not + * excluded. I.e. the entire row will be copied from the old frame + * to the new, and then when actually rendering the new frame, the + * updated cells will overwrite parts of the copied row. + * + * Since we're scanning the entire viewport anyway, we also track + * whether *all* cells are to be updated. In this case, just force + * a full re-rendering, and don't copy anything from the old + * frame. + */ + bool full_repaint_needed = true; + + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + + if (!row->dirty) { + full_repaint_needed = false; + continue; + } + + bool row_all_dirty = true; + for (int c = 0; c < term->cols; c++) { + if (row->cells[c].attrs.clean) { + row_all_dirty = false; + full_repaint_needed = false; + break; + } + } + + if (row_all_dirty) { + pixman_region32_union_rect( + &dirty, &dirty, + term->margins.left, + term->margins.top + r * term->cell_height, + term->width - term->margins.left - term->margins.right, + term->cell_height); + } + } + + if (full_repaint_needed) { + force_full_repaint(term, new); + return; + } + + /* + * TODO: re-apply last frame's scroll damage + * + * We used to do this, but it turned out to be buggy. If we decide + * to re-add it, this is where to do it. Note that we'd also have + * to remove the updates to buf->dirty from grid_render_scroll() + * and grid_render_scroll_reverse(). + */ + + if (tll_length(term->grid->scroll_damage) == 0) { + /* + * We can only subtract current frame's damage from the old + * frame's if we don't have any scroll damage. + * + * If we do have scroll damage, the damage region we + * calculated above is not (yet) valid - we need to apply the + * current frame's scroll damage *first*. This is done later, + * when rendering the frame. + */ + pixman_region32_subtract(&dirty, &old->dirty[0], &dirty); + pixman_image_set_clip_region32(new->pix[0], &dirty); + } else { + /* Copy *all* of last frame's damaged areas */ + pixman_image_set_clip_region32(new->pix[0], &old->dirty[0]); + } + + pixman_image_composite32( + PIXMAN_OP_SRC, old->pix[0], NULL, new->pix[0], + 0, 0, 0, 0, 0, 0, term->width, term->height); + + pixman_image_set_clip_region32(new->pix[0], NULL); + pixman_region32_fini(&dirty); +} + +static void +dirty_old_cursor(struct terminal *term) +{ + if (term->render.last_cursor.row != NULL && !term->render.last_cursor.hidden) { + struct row *row = term->render.last_cursor.row; + struct cell *cell = &row->cells[term->render.last_cursor.col]; + cell->attrs.clean = 0; + row->dirty = true; + } + + /* Remember current cursor position, for the next frame */ + term->render.last_cursor.row = grid_row(term->grid, term->grid->cursor.point.row); + term->render.last_cursor.col = term->grid->cursor.point.col; + term->render.last_cursor.hidden = term->hide_cursor; +} + +static void +dirty_cursor(struct terminal *term) +{ + if (term->hide_cursor) + return; + + const struct coord *cursor = &term->grid->cursor.point; + + struct row *row = grid_row(term->grid, cursor->row); + struct cell *cell = &row->cells[cursor->col]; + cell->attrs.clean = 0; + row->dirty = true; +} + +static void +grid_render(struct terminal *term) +{ + if (term->shutdown.in_progress) + return; + + struct timespec start_time; + struct timespec start_wait_preapply = {0}, stop_wait_preapply = {0}; + struct timespec start_double_buffering = {0}, stop_double_buffering = {0}; + + /* Might be a thread doing pre-applied damage */ + if (unlikely(term->render.preapply_last_frame_damage && + term->render.workers.preapplied_damage.buf != NULL)) + { + clock_gettime(CLOCK_MONOTONIC, &start_wait_preapply); + render_wait_for_preapply_damage(term); + clock_gettime(CLOCK_MONOTONIC, &stop_wait_preapply); + } + + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) + clock_gettime(CLOCK_MONOTONIC, &start_time); + + xassert(term->width > 0); + xassert(term->height > 0); + + struct buffer_chain *chain = term->render.chains.grid; + struct buffer *buf = shm_get_buffer(chain, term->width, term->height); + + /* Dirty old and current cursor cell, to ensure they're repainted */ + dirty_old_cursor(term); + dirty_cursor(term); + + LOG_DBG("buffer age: %u (%p)", buf->age, (void *)buf); + + if (term->render.last_buf == NULL || + term->render.last_buf->width != buf->width || + term->render.last_buf->height != buf->height || + term->render.margins) + { + force_full_repaint(term, buf); + } + + else if (buf->age > 0) { + LOG_DBG("buffer age: %u (%p)", buf->age, (void *)buf); + + xassert(term->render.last_buf != NULL); + xassert(term->render.last_buf != buf); + xassert(term->render.last_buf->width == buf->width); + xassert(term->render.last_buf->height == buf->height); + + if (++term->render.frames_since_last_immediate_release > 10) { + static bool have_warned = false; + + if (!term->render.preapply_last_frame_damage && + term->conf->tweak.preapply_damage && + term->render.workers.count > 0) + { + LOG_INFO("enabling pre-applied frame damage"); + term->render.preapply_last_frame_damage = true; + } else if (!have_warned && !term->render.preapply_last_frame_damage) { + LOG_WARN("compositor is not releasing buffers immediately; " + "expect lower rendering performance"); + have_warned = true; + } + } + + clock_gettime(CLOCK_MONOTONIC, &start_double_buffering); + reapply_old_damage(term, buf, term->render.last_buf); + clock_gettime(CLOCK_MONOTONIC, &stop_double_buffering); + } else if (!term->render.preapply_last_frame_damage) { + term->render.frames_since_last_immediate_release = 0; + } + + if (term->render.last_buf != NULL) { + shm_unref(term->render.last_buf); + term->render.last_buf = NULL; + } + + term->render.last_buf = buf; + shm_addref(buf); + buf->age = 0; + + + tll_foreach(term->grid->scroll_damage, it) { + switch (it->item.type) { + case DAMAGE_SCROLL: + if (term->grid->view == term->grid->offset) + grid_render_scroll(term, buf, &it->item); + break; + + case DAMAGE_SCROLL_REVERSE: + if (term->grid->view == term->grid->offset) + grid_render_scroll_reverse(term, buf, &it->item); + break; + + case DAMAGE_SCROLL_IN_VIEW: + grid_render_scroll(term, buf, &it->item); + break; + + case DAMAGE_SCROLL_REVERSE_IN_VIEW: + grid_render_scroll_reverse(term, buf, &it->item); + break; + } + + tll_remove(term->grid->scroll_damage, it); + } + + /* + * Ensure selected cells have their 'selected' bit set. This is + * normally "automatically" true - the bit is set when the + * selection is made. + * + * However, if the cell is updated (printed to) while the + * selection is active, the 'selected' bit is cleared. Checking + * for this and re-setting the bit in term_print() is too + * expensive performance wise. + * + * Instead, we synchronize the selection bits here and now. This + * makes the performance impact linear to the number of selected + * cells rather than to the number of updated cells. + * + * (note that selection_dirty_cells() will not set the dirty flag + * on cells where the 'selected' bit is already set) + */ + selection_dirty_cells(term); + + /* Translate offset-relative row to view-relative, unless cursor + * is hidden, then we just set it to -1 */ + struct coord cursor = {-1, -1}; + if (!term->hide_cursor) { + cursor = term->grid->cursor.point; + cursor.row += term->grid->offset; + cursor.row -= term->grid->view; + cursor.row &= term->grid->num_rows - 1; + } + + if (term->conf->tweak.overflowing_glyphs) { + /* + * Pre-pass to dirty cells affected by overflowing glyphs. + * + * Given any two pair of cells where the first cell is + * overflowing into the second, *both* cells must be + * re-rendered if any one of them is dirty. + * + * Thus, given a string of overflowing glyphs, with a single + * dirty cell in the middle, we need to re-render the entire + * string. + */ + for (int r = 0; r < term->rows; r++) { + struct row *row = grid_row_in_view(term->grid, r); + + if (!row->dirty) + continue; + + /* Loop row from left to right, looking for dirty cells */ + for (struct cell *cell = &row->cells[0]; + cell < &row->cells[term->cols]; + cell++) + { + if (cell->attrs.clean) + continue; + + /* + * Cell is dirty, go back and dirty previous cells, if + * they are overflowing. + * + * As soon as we see a non-overflowing cell we can + * stop, since it isn't affecting the string of + * overflowing glyphs that follows it. + * + * As soon as we see a dirty cell, we can stop, since + * that means we've already handled it (remember the + * outer loop goes from left to right). + */ + for (struct cell *c = cell - 1; c >= &row->cells[0]; c--) { + if (c->attrs.confined) + break; + if (!c->attrs.clean) + break; + c->attrs.clean = false; + } + + /* + * Now move forward, dirtying all cells until we hit a + * non-overflowing cell. + * + * Note that the first non-overflowing cell must be + * re-rendered as well, but any cell *after* that is + * unaffected by the string of overflowing glyphs + * we're dealing with right now. + * + * For performance, this iterates the *outer* loop's + * cell pointer - no point in re-checking all these + * glyphs again, in the outer loop. + */ + for (; cell < &row->cells[term->cols]; cell++) { + cell->attrs.clean = false; + if (cell->attrs.confined) + break; + } + } + } + } + +#if defined(_DEBUG) + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + + if (row->dirty) { + bool all_clean = true; + for (int c = 0; c < term->cols; c++) { + if (!row->cells[c].attrs.clean) { + all_clean = false; + break; + } + } + if (all_clean) + BUG("row #%d is dirty, but all cells are marked as clean", r); + } else { + for (int c = 0; c < term->cols; c++) { + if (!row->cells[c].attrs.clean) + BUG("row #%d is clean, but cell #%d is dirty", r, c); + } + } + } +#endif + + pixman_region32_t damage; + pixman_region32_init(&damage); + + render_sixel_images(term, buf->pix[0], &damage, &cursor); + + + if (term->render.workers.count > 0) { + mtx_lock(&term->render.workers.lock); + term->render.workers.buf = buf; + for (size_t i = 0; i < term->render.workers.count; i++) + sem_post(&term->render.workers.start); + + xassert(tll_length(term->render.workers.queue) == 0); + } + + for (int r = 0; r < term->rows; r++) { + struct row *row = grid_row_in_view(term->grid, r); + + if (!row->dirty) + continue; + + row->dirty = false; + + if (term->render.workers.count > 0) + tll_push_back(term->render.workers.queue, r); + + else { + /* TODO: damage region */ + int cursor_col = cursor.row == r ? cursor.col : -1; + render_row(term, buf->pix[0], &damage, row, r, cursor_col); + } + } + + /* Signal workers the frame is done */ + if (term->render.workers.count > 0) { + for (size_t i = 0; i < term->render.workers.count; i++) + tll_push_back(term->render.workers.queue, -1); + mtx_unlock(&term->render.workers.lock); + + for (size_t i = 0; i < term->render.workers.count; i++) + sem_wait(&term->render.workers.done); + term->render.workers.buf = NULL; + } + + for (size_t i = 0; i < term->render.workers.count; i++) + pixman_region32_union(&damage, &damage, &buf->dirty[i + 1]); + + pixman_region32_union(&buf->dirty[0], &buf->dirty[0], &damage); + + { + int box_count = 0; + pixman_box32_t *boxes = pixman_region32_rectangles(&damage, &box_count); + + for (size_t i = 0; i < box_count; i++) { + wl_surface_damage_buffer( + term->window->surface.surf, + boxes[i].x1, boxes[i].y1, + boxes[i].x2 - boxes[i].x1, boxes[i].y2 - boxes[i].y1); + } + } + + pixman_region32_fini(&damage); + + render_overlay(term); + render_ime_preedit(term, buf); + render_scrollback_position(term); + + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) { + struct timespec end_time; + clock_gettime(CLOCK_MONOTONIC, &end_time); + + struct timespec wait_time; + timespec_sub(&stop_wait_preapply, &start_wait_preapply, &wait_time); + + struct timespec render_time; + timespec_sub(&end_time, &start_time, &render_time); + + struct timespec double_buffering_time; + timespec_sub(&stop_double_buffering, &start_double_buffering, &double_buffering_time); + + struct timespec preapply_damage; + timespec_sub(&term->render.workers.preapplied_damage.stop, + &term->render.workers.preapplied_damage.start, + &preapply_damage); + + struct timespec total_render_time; + timespec_add(&render_time, &double_buffering_time, &total_render_time); + timespec_add(&wait_time, &total_render_time, &total_render_time); + + switch (term->conf->tweak.render_timer) { + case RENDER_TIMER_LOG: + case RENDER_TIMER_BOTH: + LOG_INFO( + "frame rendered in %lds %9ldns " + "(%lds %9ldns wait, %lds %9ldns rendering, %lds %9ldns double buffering) not included: %lds %ldns pre-apply damage", + (long)total_render_time.tv_sec, + total_render_time.tv_nsec, + (long)wait_time.tv_sec, + wait_time.tv_nsec, + (long)render_time.tv_sec, + render_time.tv_nsec, + (long)double_buffering_time.tv_sec, + double_buffering_time.tv_nsec, + (long)preapply_damage.tv_sec, + preapply_damage.tv_nsec); + break; + + case RENDER_TIMER_OSD: + case RENDER_TIMER_NONE: + break; + } + + switch (term->conf->tweak.render_timer) { + case RENDER_TIMER_OSD: + case RENDER_TIMER_BOTH: + render_render_timer(term, total_render_time); + break; + + case RENDER_TIMER_LOG: + case RENDER_TIMER_NONE: + break; + } + } + + xassert(term->grid->offset >= 0 && term->grid->offset < term->grid->num_rows); + xassert(term->grid->view >= 0 && term->grid->view < term->grid->num_rows); + + xassert(term->window->frame_callback == NULL); + term->window->frame_callback = wl_surface_frame(term->window->surface.surf); + wl_callback_add_listener(term->window->frame_callback, &frame_listener, term); + + wayl_win_scale(term->window, buf); + + if (term->wl->presentation != NULL && term->conf->presentation_timings) { + struct timespec commit_time; + clock_gettime(term->wl->presentation_clock_id, &commit_time); + + struct wp_presentation_feedback *feedback = wp_presentation_feedback( + term->wl->presentation, term->window->surface.surf); + + if (feedback == NULL) { + LOG_WARN("failed to create presentation feedback"); + } else { + struct presentation_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct presentation_context){ + .term = term, + .input.tv_sec = term->render.input_time.tv_sec, + .input.tv_usec = term->render.input_time.tv_nsec / 1000, + .commit.tv_sec = commit_time.tv_sec, + .commit.tv_usec = commit_time.tv_nsec / 1000, + }; + + wp_presentation_feedback_add_listener( + feedback, &presentation_feedback_listener, ctx); + + term->render.input_time.tv_sec = 0; + term->render.input_time.tv_nsec = 0; + } + } + + if (term->conf->tweak.damage_whole_window) { + wl_surface_damage_buffer( + term->window->surface.surf, 0, 0, INT32_MAX, INT32_MAX); + } + + wl_surface_attach(term->window->surface.surf, buf->wl_buf, 0, 0); + wl_surface_commit(term->window->surface.surf); +} + +static void +render_search_box(struct terminal *term) +{ + xassert(term->window->search.sub != NULL); + + /* + * We treat the search box pretty much like a row of cells. That + * is, a glyph is either 1 or 2 (or more) "cells" wide. + * + * The search 'length', and 'cursor' (position) is in + * *characters*, not cells. This means we need to translate from + * character count to cell count when calculating the length of + * the search box, where in the search string we should start + * rendering etc. + */ + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + /* TODO: do we want to/need to handle multi-seat? */ + struct seat *ime_seat = NULL; + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) { + ime_seat = &it->item; + break; + } + } + + size_t text_len = term->search.len; + if (ime_seat != NULL && ime_seat->ime.preedit.text != NULL) + text_len += c32len(ime_seat->ime.preedit.text); + + char32_t *text = xmalloc((text_len + 1) * sizeof(char32_t)); + text[0] = U'\0'; + + /* Copy everything up to the cursor */ + c32ncpy(text, term->search.buf, term->search.cursor); + text[term->search.cursor] = U'\0'; + + /* Insert pre-edit text at cursor */ + if (ime_seat != NULL && ime_seat->ime.preedit.text != NULL) + c32cat(text, ime_seat->ime.preedit.text); + + /* And finally everything after the cursor */ + c32ncat(text, &term->search.buf[term->search.cursor], + term->search.len - term->search.cursor); +#else + const char32_t *text = term->search.buf; + const size_t text_len = term->search.len; +#endif + + /* Calculate the width of each character */ + int widths[text_len + 1]; + for (size_t i = 0; i < text_len; i++) + widths[i] = max(0, c32width(text[i])); + widths[text_len] = 0; + + const size_t total_cells = c32swidth(text, text_len); + const size_t wanted_visible_cells = max(20, total_cells); + + /* + * Build status string: " Aa Wb Re 3/17 ↻" + * Aa - case mode (blank=smart, A=sensitive, a=insensitive) + * Wb - whole-word toggle + * Re - regex toggle + * N/M - match counter + * ↻ - wrap indicator + */ + char32_t status[64]; + size_t st_len = 0; + + if (term->search.case_mode == SEARCH_CASE_SENSITIVE) { + status[st_len++] = U'A'; status[st_len++] = U'a'; + status[st_len++] = U' '; + } else if (term->search.case_mode == SEARCH_CASE_INSENSITIVE) { + status[st_len++] = U'a'; status[st_len++] = U'a'; + status[st_len++] = U' '; + } + if (term->search.whole_word) { + status[st_len++] = U'W'; status[st_len++] = U'b'; + status[st_len++] = U' '; + } + if (term->search.regex) { + status[st_len++] = U'.'; status[st_len++] = U'*'; + status[st_len++] = U' '; + } + if (term->search.wrapped) { + status[st_len++] = U'~'; status[st_len++] = U' '; + } + if (term->search.len > 0 && term->search.total_count > 0) { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%zu/%zu", + term->search.current_idx, term->search.total_count); + for (size_t i = 0; tmp[i] != '\0' && st_len < ALEN(status) - 1; i++) + status[st_len++] = (char32_t)tmp[i]; + status[st_len++] = U' '; + } + status[st_len] = U'\0'; + const size_t status_cells = c32swidth(status, st_len); + + const float scale = term->scale; + xassert(scale >= 1.); + const size_t margin = (size_t)roundf(3 * scale); + const size_t outer_margin = (size_t)roundf(6 * scale); + const size_t close_gap = (size_t)roundf(4 * scale); + const size_t close_w = term->cell_width; + const size_t status_w = status_cells * term->cell_width; + const size_t chrome_w = margin + status_w + close_gap + close_w + margin; + + const size_t max_box_w = term->width > (int)(2 * outer_margin) + ? (size_t)term->width - 2 * outer_margin : (size_t)term->width; + const size_t want_box_w = chrome_w + wanted_visible_cells * term->cell_width; + + size_t width = min(want_box_w, max_box_w); + size_t height = min( + term->height - 2 * outer_margin, + margin + term->cell_height + margin); + + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); + + const size_t text_area_w = width > chrome_w ? width - chrome_w : 0; + const size_t visible_cells = text_area_w / term->cell_width; + size_t glyph_offset = term->render.search_glyph_offset; + + struct buffer_chain *chain = term->render.chains.search; + struct buffer *buf = shm_get_buffer(chain, width, height); + + pixman_region32_t clip; + pixman_region32_init_rect(&clip, 0, 0, width, height); + pixman_image_set_clip_region32(buf->pix[0], &clip); + pixman_region32_fini(&clip); + +#define WINDOW_X(x) ((int)term->width - (int)width - (int)outer_margin + (x)) +#define WINDOW_Y(y) ((int)outer_margin + (y)) + + /* Use the terminal's own bg/fg for the popup, tinted to indicate + * the search state. */ + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + + uint32_t bg_hex = term->colors.bg; + uint32_t fg_hex = term->colors.fg; + + if (term->search.len > 0 && term->search.match_len == 0) { + /* No match — red */ + bg_hex = term->colors.table[1]; + fg_hex = term->colors.table[0]; + } else if (term->search.wrapped) { + /* Wrapped — yellow */ + bg_hex = term->colors.table[3]; + fg_hex = term->colors.table[0]; + } + + const pixman_color_t color = color_hex_to_pixman(bg_hex, gamma_correct); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, + 1, &(pixman_rectangle16_t){0, 0, width, height}); + + struct fcft_font *font = term->fonts[0]; + const int x_left = margin; + const int x_ofs = term->font_x_ofs; + int x = x_left; + int y = margin; + + pixman_color_t fg = color_hex_to_pixman(fg_hex, gamma_correct); + + /* Move offset we start rendering at, to ensure the cursor is visible */ + for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; cell_idx += widths[i], i++) { + if (i != term->search.cursor) + continue; + +#if (FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (ime_seat != NULL && ime_seat->ime.preedit.cells != NULL) { + if (ime_seat->ime.preedit.cursor.start == ime_seat->ime.preedit.cursor.end) { + /* All IME's I've seen so far keeps the cursor at + * index 0, so ensure the *end* of the pre-edit string + * is visible */ + cell_idx += ime_seat->ime.preedit.count; + } else { + /* Try to predict in which direction we'll shift the text */ + if (cell_idx + ime_seat->ime.preedit.cursor.start > glyph_offset) + cell_idx += ime_seat->ime.preedit.cursor.end; + else + cell_idx += ime_seat->ime.preedit.cursor.start; + } + } +#endif + + if (cell_idx < glyph_offset) { + /* Shift to the *left*, making *this* character the + * *first* visible one */ + term->render.search_glyph_offset = glyph_offset = cell_idx; + } + + else if (cell_idx > glyph_offset + visible_cells) { + /* Shift to the *right*, making *this* character the + * *last* visible one */ + term->render.search_glyph_offset = glyph_offset = + cell_idx - min(cell_idx, visible_cells); + } + + /* Adjust offset if there is free space available */ + if (total_cells - glyph_offset < visible_cells) { + term->render.search_glyph_offset = glyph_offset = + total_cells - min(total_cells, visible_cells); + } + + break; + } + + /* Ensure offset is at a character boundary */ + for (size_t i = 0, cell_idx = 0; i <= text_len; cell_idx += widths[i], i++) { + if (cell_idx >= glyph_offset) { + term->render.search_glyph_offset = glyph_offset = cell_idx; + break; + } + } + + /* + * Render the search string, starting at 'glyph_offset'. Note that + * glyph_offset is in cells, not characters + */ + for (size_t i = 0, + cell_idx = 0, + width = widths[i], + next_cell_idx = width; + i < text_len; + i++, + cell_idx = next_cell_idx, + width = widths[i], + next_cell_idx += width) + { + /* Convert subsurface coordinates to window coordinates*/ + /* Render cursor */ + if (i == term->search.cursor) { +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + bool have_preedit = + ime_seat != NULL && ime_seat->ime.preedit.cells != NULL; + bool hidden = + ime_seat != NULL && ime_seat->ime.preedit.cursor.hidden; + + if (have_preedit && !hidden) { + /* Cursor may be outside the visible area: + * cell_idx-glyph_offset can be negative */ + int cells_left = visible_cells - max( + (ssize_t)(cell_idx - glyph_offset), 0); + + /* If cursor is outside the visible area, we need to + * adjust our rectangle's position */ + int start = ime_seat->ime.preedit.cursor.start + + min((ssize_t)(cell_idx - glyph_offset), 0); + int end = ime_seat->ime.preedit.cursor.end + + min((ssize_t)(cell_idx - glyph_offset), 0); + + if (start == end) { + int count = min(ime_seat->ime.preedit.count, cells_left); + + /* Underline the entire (visible part of) pre-edit text */ + draw_underline(term, buf->pix[0], font, &fg, x, y, count); + + /* Bar-styled cursor, if in the visible area */ + if (start >= 0 && start <= visible_cells) { + draw_beam_cursor( + term, buf->pix[0], font, &fg, + x + start * term->cell_width, y); + } + + term_ime_set_cursor_rect(term, + WINDOW_X(x + start * term->cell_width), WINDOW_Y(y), + 1, term->cell_height); + } else { + /* Underline everything before and after the cursor */ + int count1 = min(start, cells_left); + int count2 = max( + min(ime_seat->ime.preedit.count - ime_seat->ime.preedit.cursor.end, + cells_left - end), + 0); + draw_underline(term, buf->pix[0], font, &fg, x, y, count1); + draw_underline(term, buf->pix[0], font, &fg, x + end * term->cell_width, y, count2); + + /* TODO: how do we handle a partially hidden rectangle? */ + if (start >= 0 && end <= visible_cells) { + draw_hollow_block( + term, buf->pix[0], &fg, x + start * term->cell_width, y, end - start); + } + term_ime_set_cursor_rect(term, + WINDOW_X(x + start * term->cell_width), WINDOW_Y(y), + term->cell_width * (end - start), term->cell_height); + } + } else if (!have_preedit) +#endif + { + /* Cursor *should* be in the visible area */ + xassert(cell_idx >= glyph_offset); + xassert(cell_idx <= glyph_offset + visible_cells); + draw_beam_cursor(term, buf->pix[0], font, &fg, x, y); + term_ime_set_cursor_rect( + term, WINDOW_X(x), WINDOW_Y(y), 1, term->cell_height); + } + } + + if (next_cell_idx >= glyph_offset && next_cell_idx - glyph_offset > visible_cells) { + /* We're now beyond the visible area - nothing more to render */ + break; + } + + if (cell_idx < glyph_offset) { + /* We haven't yet reached the visible part of the string */ + cell_idx = next_cell_idx; + continue; + } + + const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( + font, text[i], term->font_subpixel); + + if (glyph == NULL) { + cell_idx = next_cell_idx; + continue; + } + + if (unlikely(glyph->is_color_glyph)) { + pixman_image_composite32( + PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, + x + x_ofs + glyph->x, y + term->font_baseline - glyph->y, + glyph->width, glyph->height); + } else { + int combining_ofs = width == 0 + ? (glyph->x < 0 + ? width * term->cell_width + : (width - 1) * term->cell_width) + : 0; /* Not a zero-width character - no additional offset */ + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32( + PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, + x + x_ofs + combining_ofs + glyph->x, + y + term->font_baseline - glyph->y, + glyph->width, glyph->height); + pixman_image_unref(src); + } + + x += width * term->cell_width; + cell_idx = next_cell_idx; + } + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (ime_seat != NULL && ime_seat->ime.preedit.cells != NULL) + /* Already rendered */; + else +#endif + if (term->search.cursor >= term->search.len) { + draw_beam_cursor(term, buf->pix[0], font, &fg, x, y); + term_ime_set_cursor_rect( + term, WINDOW_X(x), WINDOW_Y(y), 1, term->cell_height); + } + + /* Status string (toggles + counter) immediately to the left of + * the close glyph */ + { + int sx = (int)width - (int)margin - (int)close_w + - (int)close_gap - (int)status_w; + const int sy = (int)margin; + for (size_t i = 0; i < st_len; i++) { + const struct fcft_glyph *g = fcft_rasterize_char_utf32( + font, status[i], term->font_subpixel); + int w = max(1, c32width(status[i])); + if (g != NULL && !g->is_color_glyph) { + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32( + PIXMAN_OP_OVER, src, g->pix, buf->pix[0], 0, 0, 0, 0, + sx + x_ofs + g->x, + sy + term->font_baseline - g->y, + g->width, g->height); + pixman_image_unref(src); + } + sx += w * (int)term->cell_width; + } + } + + /* Close glyph (×) at the top-right of the popup */ + { + const struct fcft_glyph *cg = fcft_rasterize_char_utf32( + font, U'×', term->font_subpixel); + if (cg != NULL) { + const int cx = (int)width - (int)margin - (int)close_w; + const int cy = (int)margin; + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32( + PIXMAN_OP_OVER, src, cg->pix, buf->pix[0], 0, 0, 0, 0, + cx + x_ofs + cg->x, + cy + term->font_baseline - cg->y, + cg->width, cg->height); + pixman_image_unref(src); + } + } + + quirk_weston_subsurface_desync_on(term->window->search.sub); + + /* TODO: this is only necessary on a window resize */ + wl_subsurface_set_position( + term->window->search.sub, + (int32_t)roundf(max(0, (int32_t)term->width - (int32_t)width - (int32_t)outer_margin) / scale), + (int32_t)roundf(outer_margin / scale)); + + wayl_surface_scale(term->window, &term->window->search.surface, buf, scale); + wl_surface_attach(term->window->search.surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(term->window->search.surface.surf, 0, 0, width, height); + + struct wl_region *region = wl_compositor_create_region(term->wl->compositor); + if (region != NULL) { + wl_region_add(region, 0, 0, width, height); + wl_surface_set_opaque_region(term->window->search.surface.surf, region); + wl_region_destroy(region); + } + + wl_surface_commit(term->window->search.surface.surf); + quirk_weston_subsurface_desync_off(term->window->search.sub); + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + free(text); +#endif +#undef WINDOW_X +#undef WINDOW_Y +} + +static void +render_urls(struct terminal *term) +{ + struct wl_window *win = term->window; + xassert(tll_length(win->urls) > 0); + + const float scale = term->scale; + const int x_margin = (int)roundf(2 * scale); + const int y_margin = (int)roundf(1 * scale); + + /* Calculate view start, counted from the *current* scrollback start */ + const int scrollback_end + = (term->grid->offset + term->rows) & (term->grid->num_rows - 1); + const int view_start + = (term->grid->view + - scrollback_end + + term->grid->num_rows) & (term->grid->num_rows - 1); + const int view_end = view_start + term->rows - 1; + + const bool show_url = term->urls_show_uri_on_jump_label; + + /* + * There can potentially be a lot of URLs. + * + * Since each URL is a separate sub-surface, and requires its own + * SHM buffer, we may be allocating a lot of buffers. + * + * SHM buffers normally have their own, private SHM buffer + * pool. Each pool is mmapped, and thus allocates *at least* + * 4K. Since URL labels are typically small, we end up using an + * excessive amount of both virtual and physical memory. + * + * For this reason, we instead use shm_get_many(), which uses a + * single, shared pool for all buffers. + * + * To be able to use it, we need to have all the *all* the buffer + * dimensions up front. + * + * Thus, the first iteration through the URLs do the heavy + * lifting: builds the label contents and calculates both its + * position and size. But instead of rendering the label + * immediately, we store the calculated data, and then do a second + * pass, where we first get all our buffers, and then render to + * them. + */ + + /* Positioning data + label contents */ + struct { + const struct wl_url *url; + char32_t *text; + int x; + int y; + } info[tll_length(win->urls)]; + + /* For shm_get_many() */ + int widths[tll_length(win->urls)]; + int heights[tll_length(win->urls)]; + + size_t render_count = 0; + + tll_foreach(win->urls, it) { + const struct url *url = it->item.url; + const char32_t *key = url->key; + const size_t entered_key_len = c32len(term->url_keys); + + if (key == NULL) { + /* TODO: if we decide to use the .text field, we cannot + * just skip the entire jump label like this */ + continue; + } + + struct wl_surface *surf = it->item.surf.surface.surf; + struct wl_subsurface *sub_surf = it->item.surf.sub; + + if (surf == NULL || sub_surf == NULL) + continue; + + bool hide = false; + const struct coord *pos = &url->range.start; + const int _row + = (pos->row + - scrollback_end + + term->grid->num_rows) & (term->grid->num_rows - 1); + + if (_row < view_start || _row > view_end) + hide = true; + if (c32len(key) <= entered_key_len) + hide = true; + if (c32ncasecmp(term->url_keys, key, entered_key_len) != 0) + hide = true; + + if (hide) { + wl_surface_attach(surf, NULL, 0, 0); + wl_surface_commit(surf); + continue; + } + + int col = pos->col; + int row = pos->row - term->grid->view; + while (row < 0) + row += term->grid->num_rows; + row &= (term->grid->num_rows - 1); + + /* Position label slightly above and to the left */ + int x = col * term->cell_width - 15 * term->cell_width / 10; + int y = row * term->cell_height - 5 * term->cell_height / 10; + + /* Don't position it outside our window */ + if (x < -term->margins.left) + x = -term->margins.left; + if (y < -term->margins.top) + y = -term->margins.top; + + /* Maximum width of label, in pixels */ + const int max_width = + term->width - term->margins.left - term->margins.right - x; + const int max_cols = max_width / term->cell_width; + + const size_t key_len = c32len(key); + + size_t url_len = mbstoc32(NULL, url->url, 0); + if (url_len == (size_t)-1) + url_len = 0; + + char32_t url_wchars[url_len + 1]; + mbstoc32(url_wchars, url->url, url_len + 1); + + /* Format label, not yet subject to any size limitations */ + size_t chars = key_len + (show_url ? (2 + url_len) : 0); + char32_t label[chars + 1]; + label[chars] = U'\0'; + + if (show_url) { + c32cpy(label, key); + c32cat(label, U": "); + c32cat(label, url_wchars); + } else + c32ncpy(label, key, chars); + + /* Upper case the key characters */ + for (size_t i = 0; i < c32len(key); i++) + label[i] = toc32upper(label[i]); + + /* Blank already entered key characters */ + for (size_t i = 0; i < entered_key_len; i++) + label[i] = U' '; + + /* + * Don't extend outside our window + * + * Truncate label so that it doesn't extend outside our + * window. + * + * Do it in a way such that we don't cut the label in the + * middle of a double-width character. + */ + + int cols = 0; + + for (size_t i = 0; i <= c32len(label); i++) { + int _cols = c32swidth(label, i); + + if (_cols == (size_t)-1) + continue; + + if (_cols >= max_cols) { + if (i > 0) + label[i - 1] = U'…'; + label[i] = U'\0'; + cols = max_cols; + break; + } + cols = _cols; + } + + if (cols == 0) + continue; + + int width = x_margin + cols * term->cell_width + x_margin; + int height = y_margin + term->cell_height + y_margin; + + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); + + info[render_count].url = &it->item; + info[render_count].text = xc32dup(label); + info[render_count].x = x; + info[render_count].y = y; + + widths[render_count] = width; + heights[render_count] = height; + + render_count++; + } + + struct buffer_chain *chain = term->render.chains.url; + struct buffer *bufs[render_count]; + shm_get_many(chain, render_count, widths, heights, bufs); + + uint32_t fg = term->conf->colors_dark.use_custom.jump_label + ? term->conf->colors_dark.jump_label.fg + : term->colors.table[0]; + uint32_t bg = term->conf->colors_dark.use_custom.jump_label + ? term->conf->colors_dark.jump_label.bg + : term->colors.table[3]; + + for (size_t i = 0; i < render_count; i++) { + const struct wayl_sub_surface *sub_surf = &info[i].url->surf; + + const char32_t *label = info[i].text; + const int x = info[i].x; + const int y = info[i].y; + + xassert(sub_surf->surface.surf != NULL); + xassert(sub_surf->sub != NULL); + + wl_subsurface_set_position( + sub_surf->sub, + roundf((term->margins.left + x) / scale), + roundf((term->margins.top + y) / scale)); + + render_osd( + term, sub_surf, term->fonts[0], bufs[i], label, + fg, 0xffu << 24 | bg, x_margin); + + free(info[i].text); + } +} + +static void +render_update_title(struct terminal *term) +{ + static const size_t max_len = 2048; + + const char *title = term->window_title != NULL ? term->window_title : "foot"; + char *copy = NULL; + + if (strlen(title) > max_len) { + copy = xstrndup(title, max_len); + title = copy; + } + + xdg_toplevel_set_title(term->window->xdg_toplevel, title); + free(copy); +} + +static void +frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) +{ + struct terminal *term = data; + + xassert(term->window->frame_callback == wl_callback); + wl_callback_destroy(wl_callback); + term->window->frame_callback = NULL; + + bool grid = term->render.pending.grid; + bool csd = term->render.pending.csd; + bool search = term->is_searching && term->render.pending.search; + bool urls = urls_mode_is_active(term) > 0 && term->render.pending.urls; + + term->render.pending.grid = false; + term->render.pending.csd = false; + term->render.pending.search = false; + term->render.pending.urls = false; + + struct grid *original_grid = term->grid; + if (urls_mode_is_active(term)) { + xassert(term->url_grid_snapshot != NULL); + term->grid = term->url_grid_snapshot; + } + + if (csd && term->window->csd_mode == CSD_YES) { + quirk_weston_csd_on(term); + render_csd(term); + quirk_weston_csd_off(term); + } + + if (search) + render_search_box(term); + + if (urls) + render_urls(term); + + if (term->conf->tabs.enabled && tab_overview_is_active(term->window)) + render_tab_overview(term); + + if ((grid && !term->delayed_render_timer.is_armed) || (csd | search | urls)) + grid_render(term); + + tll_foreach(term->wl->seats, it) { + if (it->item.ime_focus == term) + ime_update_cursor_rect(&it->item); + } + + term->grid = original_grid; +} + +static void +tiocswinsz(struct terminal *term) +{ + if (term->ptmx >= 0) { + if (ioctl(term->ptmx, (unsigned int)TIOCSWINSZ, + &(struct winsize){ + .ws_row = term->rows, + .ws_col = term->cols, + .ws_xpixel = term->cols * term->cell_width, + .ws_ypixel = term->rows * term->cell_height}) < 0) + { + LOG_ERRNO("TIOCSWINSZ"); + } + + term_send_size_notification(term); + } +} + +static void +delayed_reflow_of_normal_grid(struct terminal *term) +{ + if (term->interactive_resizing.grid == NULL) + return; + + xassert(term->interactive_resizing.new_rows > 0); + + struct coord *const tracking_points[] = { + &term->selection.coords.start, + &term->selection.coords.end, + }; + + /* Reflow the original (since before the resize was started) grid, + * to the *current* dimensions */ + grid_resize_and_reflow( + term->interactive_resizing.grid, term, + term->interactive_resizing.new_rows, term->normal.num_cols, + term->interactive_resizing.old_screen_rows, term->rows, + term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, + tracking_points); + + /* Replace the current, truncated, "normal" grid with the + * correctly reflowed one */ + grid_free(&term->normal); + term->normal = *term->interactive_resizing.grid; + free(term->interactive_resizing.grid); + + term->hide_cursor = term->interactive_resizing.old_hide_cursor; + + /* Reset */ + term->interactive_resizing.grid = NULL; + term->interactive_resizing.old_screen_rows = 0; + term->interactive_resizing.new_rows = 0; + term->interactive_resizing.old_hide_cursor = false; + + /* Invalidate render pointers */ + render_wait_for_preapply_damage(term); + shm_unref(term->render.last_buf); + term->render.last_buf = NULL; + term->render.last_cursor.row = NULL; + + tll_free(term->normal.scroll_damage); + sixel_reflow_grid(term, &term->normal); + + if (term->grid == &term->normal) { + term_damage_view(term); + render_refresh(term); + } + + term_ptmx_resume(term); +} + +static bool +fdm_tiocswinsz(struct fdm *fdm, int fd, int events, void *data) +{ + struct terminal *term = data; + + if (events & EPOLLIN) { + tiocswinsz(term); + delayed_reflow_of_normal_grid(term); + } + + if (term->window->resize_timeout_fd >= 0) { + fdm_del(fdm, term->window->resize_timeout_fd); + term->window->resize_timeout_fd = -1; + } + return true; +} + +static void +send_dimensions_to_client(struct terminal *term) +{ + struct wl_window *win = term->window; + + if (!win->is_resizing || term->conf->resize_delay_ms == 0) { + /* Send new dimensions to client immediately */ + tiocswinsz(term); + delayed_reflow_of_normal_grid(term); + + /* And make sure to reset and deallocate a lingering timer */ + if (win->resize_timeout_fd >= 0) { + fdm_del(term->fdm, win->resize_timeout_fd); + win->resize_timeout_fd = -1; + } + } else { + /* Send new dimensions to client "in a while" */ + assert(win->is_resizing && term->conf->resize_delay_ms > 0); + + int fd = win->resize_timeout_fd; + uint16_t delay_ms = term->conf->resize_delay_ms; + bool successfully_scheduled = false; + + if (fd < 0) { + /* Lazy create timer fd */ + fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd < 0) + LOG_ERRNO("failed to create TIOCSWINSZ timer"); + else if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_tiocswinsz, term)) { + close(fd); + fd = -1; + } + + win->resize_timeout_fd = fd; + } + + if (fd >= 0) { + /* Reset timeout */ + const struct itimerspec timeout = { + .it_value = { + .tv_sec = delay_ms / 1000, + .tv_nsec = (delay_ms % 1000) * 1000000, + }, + }; + + if (timerfd_settime(fd, 0, &timeout, NULL) < 0) { + LOG_ERRNO("failed to arm TIOCSWINSZ timer"); + fdm_del(term->fdm, fd); + win->resize_timeout_fd = -1; + } else + successfully_scheduled = true; + } + + if (!successfully_scheduled) { + tiocswinsz(term); + delayed_reflow_of_normal_grid(term); + } + } +} + +static void +set_size_from_grid(struct terminal *term, int *width, int *height, int cols, int rows) +{ + int new_width, new_height; + + /* Nominal grid dimensions */ + new_width = cols * term->cell_width; + new_height = rows * term->cell_height; + + /* Include any configured padding */ + new_width += (term->conf->pad_left + term->conf->pad_right) * term->scale; + new_height += (term->conf->pad_top + term->conf->pad_bottom) * term->scale; + + /* Round to multiples of scale */ + new_width = round(term->scale * round(new_width / term->scale)); + new_height = round(term->scale * round(new_height / term->scale)); + + if (width != NULL) + *width = new_width; + if (height != NULL) + *height = new_height; +} + +/* Move to terminal.c? */ +bool +render_resize(struct terminal *term, int width, int height, uint8_t opts) +{ + if (term->shutdown.in_progress) + return false; + + if (!term->window->is_configured) + return false; + + if (term->cell_width == 0 && term->cell_height == 0) + return false; + + const bool is_floating = + !term->window->is_maximized && + !term->window->is_fullscreen && + !term->window->is_tiled; + + /* Convert logical size to physical size */ + const float scale = term->scale; + width = round(width * scale); + height = round(height * scale); + + /* If the grid should be kept, the size should be overridden */ + if (is_floating && (opts & RESIZE_KEEP_GRID)) { + set_size_from_grid(term, &width, &height, term->cols, term->rows); + } + + if (width == 0) { + /* The compositor is letting us choose the width */ + if (term->stashed_width != 0) { + /* If a default size is requested, prefer the "last used" size */ + width = term->stashed_width; + } else { + /* Otherwise, use a user-configured size */ + switch (term->conf->size.type) { + case CONF_SIZE_PX: + width = term->conf->size.width; + + if (wayl_win_csd_borders_visible(term->window)) + width -= 2 * term->conf->csd.border_width_visible; + + width *= scale; + break; + + case CONF_SIZE_CELLS: + set_size_from_grid(term, &width, NULL, + term->conf->size.width, term->conf->size.height); + break; + } + } + } + + if (height == 0) { + /* The compositor is letting us choose the height */ + if (term->stashed_height != 0) { + /* If a default size is requested, prefer the "last used" size */ + height = term->stashed_height; + } else { + /* Otherwise, use a user-configured size */ + switch (term->conf->size.type) { + case CONF_SIZE_PX: + height = term->conf->size.height; + + /* Take CSDs into account */ + if (wayl_win_csd_titlebar_visible(term->window)) + height -= term->conf->csd.title_height; + + if (wayl_win_csd_borders_visible(term->window)) + height -= 2 * term->conf->csd.border_width_visible; + + height *= scale; + break; + + case CONF_SIZE_CELLS: + set_size_from_grid(term, NULL, &height, + term->conf->size.width, term->conf->size.height); + break; + } + } + } + + /* Don't shrink grid too much */ + const int min_cols = 1; + const int min_rows = 1; + + /* Minimum window size (must be divisible by the scaling factor)*/ + const int min_width = roundf(scale * ceilf((min_cols * term->cell_width) / scale)); + const int min_height = roundf(scale * ceilf((min_rows * term->cell_height) / scale)); + + width = max(width, min_width); + height = max(height, min_height); + + /* Padding */ + const int max_pad_x = (width - min_width) / 2; + const int max_pad_y = (height - min_height) / 2; + /* Total bar height = pill height + margin (margin only used in floating layout) */ + const uint16_t tabs_bar_logical = + term->conf->tabs.enabled + ? (term->conf->tabs.height + + (term->conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING + ? term->conf->tabs.margin : 0)) + : 0; + const int conf_pad_top = term->conf->pad_top + + (term->conf->tabs.enabled && + term->conf->tabs.position == CONF_TABS_POSITION_TOP + ? tabs_bar_logical : 0); + const int conf_pad_bottom = term->conf->pad_bottom + + (term->conf->tabs.enabled && + term->conf->tabs.position == CONF_TABS_POSITION_BOTTOM + ? tabs_bar_logical : 0); + const int pad_left = min(max_pad_x, scale * term->conf->pad_left); + const int pad_right = min(max_pad_x, scale * term->conf->pad_right); + const int pad_top = min(max_pad_y, scale * conf_pad_top); + const int pad_bottom= min(max_pad_y, scale * conf_pad_bottom); + + if (is_floating && + (opts & RESIZE_BY_CELLS) && + term->conf->resize_by_cells) + { + /* If resizing in cell increments, restrict the width and height */ + width = ((width - (pad_left + pad_right)) / term->cell_width) + * term->cell_width + (pad_left + pad_right); + width = max(min_width, roundf(scale * roundf(width / scale))); + + height = ((height - (pad_top + pad_bottom)) / term->cell_height) + * term->cell_height + (pad_top + pad_bottom); + height = max(min_height, roundf(scale * roundf(height / scale))); + } + + if (!(opts & RESIZE_FORCE) && + width == term->width && + height == term->height && + scale == term->scale) + { + return false; + } + + /* Cancel an application initiated "Synchronized Update" */ + term_disable_app_sync_updates(term); + + /* Drop out of URL mode */ + urls_reset(term); + + LOG_DBG("resized: size=%dx%d (scale=%.2f)", width, height, term->scale); + term->width = width; + term->height = height; + + /* Screen rows/cols before resize */ + int old_cols = term->cols; + int old_rows = term->rows; + + /* Screen rows/cols after resize */ + const int new_cols = + (term->width - (pad_left + pad_right)) / term->cell_width; + const int new_rows = + (term->height - (pad_top + pad_bottom)) / term->cell_height; + + /* + * Requirements for scrollback: + * + * a) total number of rows (visible + scrollback history) must be + * a power of two + * b) must be representable in a plain int (signed) + * + * This means that on a "normal" system, where ints are 32-bit, + * the largest possible scrollback size is 1073741824 (0x40000000, + * 1 << 30). + * + * The largest *signed* int is 2147483647 (0x7fffffff), which is + * *not* a power of two. + * + * Note that these are theoretical limits. Most of the time, + * you'll get a memory allocation failure when trying to allocate + * the grid array. + */ + const unsigned max_scrollback = (INT_MAX >> 1) + 1; + const unsigned scrollback_lines_not_yet_power_of_two = + min((uint64_t)term->render.scrollback_lines + new_rows - 1, max_scrollback); + + /* Grid rows/cols after resize */ + const int new_normal_grid_rows = + min(1u << (32 - __builtin_clz(scrollback_lines_not_yet_power_of_two)), + max_scrollback); + const int new_alt_grid_rows = + min(1u << (32 - __builtin_clz(new_rows)), max_scrollback); + + LOG_DBG("grid rows: %d", new_normal_grid_rows); + + xassert(new_cols >= 1); + xassert(new_rows >= 1); + + /* Margins */ + const int grid_width = new_cols * term->cell_width; + const int grid_height = new_rows * term->cell_height; + const int total_x_pad = term->width - grid_width; + const int total_y_pad = term->height - grid_height; + + const enum center_when center = term->conf->center_when; + const bool centered_padding = + center == CENTER_ALWAYS || + (center == CENTER_MAXIMIZED_AND_FULLSCREEN && + (term->window->is_fullscreen || term->window->is_maximized)) || + (center == CENTER_FULLSCREEN && term->window->is_fullscreen); + + if (centered_padding && !term->window->is_resizing) { + term->margins.left = max(pad_left, min(total_x_pad - pad_right, total_x_pad / 2)); + term->margins.top = max(pad_top, min(total_y_pad - pad_bottom, total_y_pad / 2)); + } else { + term->margins.left = pad_left; + term->margins.top = pad_top; + } + term->margins.right = total_x_pad - term->margins.left; + term->margins.bottom = total_y_pad - term->margins.top; + + xassert(term->margins.left >= pad_left); + xassert(term->margins.right >= pad_right); + xassert(term->margins.top >= pad_top); + xassert(term->margins.bottom >= pad_bottom); + + if (new_cols == old_cols && new_rows == old_rows) { + LOG_DBG("grid layout unaffected; skipping reflow"); + term->interactive_resizing.new_rows = new_normal_grid_rows; + goto damage_view; + } + + + /* + * Since text reflow is slow, don't do it *while* resizing. Only + * do it when done, or after "pausing" the resize for sufficiently + * long. We reuse the TIOCSWINSZ timer to handle this. See + * send_dimensions_to_client() and fdm_tiocswinsz(). + * + * To be able to do the final reflow correctly, we need a copy of + * the original grid, before the resize started. + */ + if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { + if (term->interactive_resizing.grid == NULL) { + term_ptmx_pause(term); + + /* Stash the current 'normal' grid, as-is, to be used when + * doing the final reflow */ + term->interactive_resizing.old_screen_rows = term->rows; + term->interactive_resizing.old_cols = term->cols; + term->interactive_resizing.old_hide_cursor = term->hide_cursor; + term->interactive_resizing.grid = xmalloc(sizeof(*term->interactive_resizing.grid)); + *term->interactive_resizing.grid = term->normal; + + if (term->grid == &term->normal) + term->interactive_resizing.selection_coords = term->selection.coords; + } else { + /* We'll replace the current temporary grid, with a new + * one (again based on the original grid) */ + grid_free(&term->normal); + } + + struct grid *orig = term->interactive_resizing.grid; + + /* + * Copy the current viewport (of the original grid) to a new + * grid that will be used during the resize. For now, throw + * away sixels and OSC-8 URLs. They'll be "restored" when we + * do the final reflow. + * + * Note that OSC-8 URLs are perfectly ok to throw away; they + * cannot be interacted with during the resize. And, even if + * url.osc8-underline=always, the "underline" attribute is + * part of the cell, not the URI struct (and thus our faked + * grid will still render OSC-8 links underlined). + * + * TODO: + * - sixels? + */ + struct grid g = { + .num_rows = 1 << (32 - __builtin_clz(term->interactive_resizing.old_screen_rows)), + .num_cols = term->interactive_resizing.old_cols, + .offset = 0, + .view = 0, + .cursor = orig->cursor, + .saved_cursor = orig->saved_cursor, + .rows = xcalloc(g.num_rows, sizeof(g.rows[0])), + .cur_row = NULL, + .scroll_damage = tll_init(), + .sixel_images = tll_init(), + .kitty_kbd = orig->kitty_kbd, + }; + + term->selection.coords.start.row -= orig->view; + term->selection.coords.end.row -= orig->view; + + for (size_t i = 0, j = orig->view; + i < term->interactive_resizing.old_screen_rows; + i++, j = (j + 1) & (orig->num_rows - 1)) + { + g.rows[i] = grid_row_alloc(g.num_cols, false); + memcpy(g.rows[i]->cells, + orig->rows[j]->cells, + g.num_cols * sizeof(g.rows[i]->cells[0])); + + if (orig->rows[j]->extra == NULL || + orig->rows[j]->extra->underline_ranges.count == 0) + { + continue; + } + + /* + * Copy underline ranges + */ + + const struct row_ranges *underline_src = &orig->rows[j]->extra->underline_ranges; + + const int count = underline_src->count; + g.rows[i]->extra = xcalloc(1, sizeof(*g.rows[i]->extra)); + g.rows[i]->extra->underline_ranges.v = xmalloc( + count * sizeof(g.rows[i]->extra->underline_ranges.v[0])); + + struct row_ranges *underline_dst = &g.rows[i]->extra->underline_ranges; + underline_dst->count = underline_dst->size = count; + + for (int k = 0; k < count; k++) + underline_dst->v[k] = underline_src->v[k]; + } + + term->normal = g; + term->hide_cursor = true; + } + + if (term->grid == &term->alt) + selection_cancel(term); + else { + /* + * Don't cancel, but make sure there aren't any ongoing + * selections after the resize. + */ + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) + selection_finalize(&it->item, term, it->item.pointer.serial); + } + } + + /* + * TODO: if we remove the selection_finalize() call above (i.e. if + * we start allowing selections to be ongoing across resizes), the + * selection's pivot point coordinates *must* be added to the + * tracking points list. + */ + /* Resize grids */ + if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { + /* Simple truncating resize, *while* an interactive resize is + * ongoing. */ + xassert(term->interactive_resizing.grid != NULL); + xassert(new_normal_grid_rows > 0); + term->interactive_resizing.new_rows = new_normal_grid_rows; + + grid_resize_without_reflow( + &term->normal, new_alt_grid_rows, new_cols, + term->interactive_resizing.old_screen_rows, new_rows); + } else { + /* Full text reflow */ + + int old_normal_rows = old_rows; + + if (term->interactive_resizing.grid != NULL) { + /* Throw away the current, truncated, "normal" grid, and + * use the original grid instead (from before the resize + * started) */ + grid_free(&term->normal); + term->normal = *term->interactive_resizing.grid; + free(term->interactive_resizing.grid); + + term->hide_cursor = term->interactive_resizing.old_hide_cursor; + term->selection.coords = term->interactive_resizing.selection_coords; + + old_normal_rows = term->interactive_resizing.old_screen_rows; + + term->interactive_resizing.grid = NULL; + term->interactive_resizing.old_screen_rows = 0; + term->interactive_resizing.new_rows = 0; + term->interactive_resizing.old_hide_cursor = false; + term->interactive_resizing.selection_coords = (struct range){{-1, -1}, {-1, -1}}; + term_ptmx_resume(term); + } + + struct coord *const tracking_points[] = { + &term->selection.coords.start, + &term->selection.coords.end, + }; + + grid_resize_and_reflow( + &term->normal, term, new_normal_grid_rows, new_cols, old_normal_rows, new_rows, + term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, + tracking_points); + } + + grid_resize_without_reflow( + &term->alt, new_alt_grid_rows, new_cols, old_rows, new_rows); + + /* Reset tab stops */ + tll_free(term->tab_stops); + for (int c = 0; c < new_cols; c += 8) + tll_push_back(term->tab_stops, c); + + term->cols = new_cols; + term->rows = new_rows; + + sixel_reflow(term); + + LOG_DBG("resized: grid: cols=%d, rows=%d " + "(left-margin=%d, right-margin=%d, top-margin=%d, bottom-margin=%d)", + term->cols, term->rows, + term->margins.left, term->margins.right, + term->margins.top, term->margins.bottom); + + if (term->scroll_region.start >= term->rows) + term->scroll_region.start = 0; + if (term->scroll_region.end > term->rows || + term->scroll_region.end >= old_rows) + { + term->scroll_region.end = term->rows; + } + + term->render.last_cursor.row = NULL; + +damage_view: + /* Signal TIOCSWINSZ */ + send_dimensions_to_client(term); + + if (is_floating) { + /* Stash current size, to enable us to restore it when we're + * being un-maximized/fullscreened/tiled */ + term->stashed_width = term->width; + term->stashed_height = term->height; + } + + { + const bool title_shown = wayl_win_csd_titlebar_visible(term->window); + const bool border_shown = wayl_win_csd_borders_visible(term->window); + + const int title = title_shown + ? roundf(term->conf->csd.title_height * scale) + : 0; + const int border = border_shown + ? roundf(term->conf->csd.border_width_visible * scale) + : 0; + + /* Must use surface logical coordinates (same calculations as + in get_csd_data(), but with different inputs) */ + const int toplevel_min_width = roundf(border / scale) + + roundf(min_width / scale) + + roundf(border / scale); + + const int toplevel_min_height = roundf(border / scale) + + roundf(title / scale) + + roundf(min_height / scale) + + roundf(border / scale); + + const int toplevel_width = roundf(border / scale) + + roundf(term->width / scale) + + roundf(border / scale); + + const int toplevel_height = roundf(border / scale) + + roundf(title / scale) + + roundf(term->height / scale) + + roundf(border / scale); + + const int x = roundf(-border / scale); + const int y = roundf(-title / scale) - roundf(border / scale); + + xdg_toplevel_set_min_size( + term->window->xdg_toplevel, + toplevel_min_width, toplevel_min_height); + + xdg_surface_set_window_geometry( + term->window->xdg_surface, + x, y, toplevel_width, toplevel_height); + } + + tll_free(term->normal.scroll_damage); + tll_free(term->alt.scroll_damage); + + render_wait_for_preapply_damage(term); + shm_unref(term->render.last_buf); + term->render.last_buf = NULL; + term_damage_view(term); + render_refresh_csd(term); + render_refresh_search(term); + render_refresh(term); + + return true; +} + +static void xcursor_callback( + void *data, struct wl_callback *wl_callback, uint32_t callback_data); +static const struct wl_callback_listener xcursor_listener = { + .done = &xcursor_callback, +}; + +bool +render_xcursor_is_valid(const struct seat *seat, const char *cursor) +{ + if (cursor == NULL) + return false; + if (seat->pointer.theme == NULL) + return false; + return wl_cursor_theme_get_cursor(seat->pointer.theme, cursor) != NULL; +} + +static void +render_xcursor_update(struct seat *seat) +{ + /* If called from a frame callback, we may no longer have mouse focus */ + if (!seat->mouse_focus) + return; + + xassert(seat->pointer.shape != CURSOR_SHAPE_NONE); + + if (seat->pointer.shape == CURSOR_SHAPE_HIDDEN) { + /* Hide cursor */ + LOG_DBG("hiding cursor using client-side NULL-surface"); + wl_surface_attach(seat->pointer.surface.surf, NULL, 0, 0); + wl_pointer_set_cursor( + seat->wl_pointer, seat->pointer.serial, seat->pointer.surface.surf, + 0, 0); + wl_surface_commit(seat->pointer.surface.surf); + return; + } + + const enum cursor_shape shape = seat->pointer.shape; + const char *const xcursor = seat->pointer.last_custom_xcursor; + + if (seat->pointer.shape_device != NULL) { + xassert(shape != CURSOR_SHAPE_CUSTOM || xcursor != NULL); + + const enum wp_cursor_shape_device_v1_shape custom_shape = + (shape == CURSOR_SHAPE_CUSTOM && xcursor != NULL + ? cursor_string_to_server_shape( + xcursor, seat->wayl->shape_manager_version) + : 0); + + if (shape != CURSOR_SHAPE_CUSTOM || custom_shape != 0) { + xassert(custom_shape == 0 || shape == CURSOR_SHAPE_CUSTOM); + + const enum wp_cursor_shape_device_v1_shape wp_shape = custom_shape != 0 + ? custom_shape + : cursor_shape_to_server_shape(shape); + + LOG_DBG("setting %scursor shape using cursor-shape-v1", + custom_shape != 0 ? "custom " : ""); + + wp_cursor_shape_device_v1_set_shape( + seat->pointer.shape_device, + seat->pointer.serial, + wp_shape); + + return; + } + } + + LOG_DBG("setting %scursor shape using a client-side cursor surface", + seat->pointer.shape == CURSOR_SHAPE_CUSTOM ? "custom " : ""); + + if (seat->pointer.cursor == NULL) { + /* + * Normally, we never get here with a NULL-cursor, because we + * only schedule a cursor update when we succeed to load the + * cursor image. + * + * However, it is possible that we did succeed to load an + * image, and scheduled an update. But, *before* the scheduled + * update triggers, the user mvoes the pointer, and we try to + * load a new cursor image. This time failing. + * + * In this case, we have a NULL cursor, but the scheduled + * update is still scheduled. + */ + return; + } + + const float scale = seat->pointer.scale; + struct wl_cursor_image *image = seat->pointer.cursor->images[0]; + struct wl_buffer *buf = wl_cursor_image_get_buffer(image); + + wayl_surface_scale_explicit_width_height( + seat->mouse_focus->window, + &seat->pointer.surface, image->width, image->height, scale); + + wl_surface_attach(seat->pointer.surface.surf, buf, 0, 0); + + wl_pointer_set_cursor( + seat->wl_pointer, seat->pointer.serial, + seat->pointer.surface.surf, + image->hotspot_x / scale, image->hotspot_y / scale); + + wl_surface_damage_buffer( + seat->pointer.surface.surf, 0, 0, INT32_MAX, INT32_MAX); + + xassert(seat->pointer.xcursor_callback == NULL); + seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface.surf); + wl_callback_add_listener(seat->pointer.xcursor_callback, &xcursor_listener, seat); + + wl_surface_commit(seat->pointer.surface.surf); +} + +static void +xcursor_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) +{ + struct seat *seat = data; + + xassert(seat->pointer.xcursor_callback == wl_callback); + wl_callback_destroy(wl_callback); + seat->pointer.xcursor_callback = NULL; + + if (seat->pointer.xcursor_pending) { + render_xcursor_update(seat); + seat->pointer.xcursor_pending = false; + } +} + +static void +fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) +{ + struct renderer *renderer = data; + struct wayland *wayl = renderer->wayl; + + tll_foreach(renderer->wayl->terms, it) { + struct terminal *term = it->item; + + if (unlikely(term->shutdown.in_progress || !term->window->is_configured)) + continue; + + /* Skip inactive tabs - they process PTY data but don't render */ + if (term->window->term != term) + continue; + + bool grid = term->render.refresh.grid; + bool csd = term->render.refresh.csd; + bool search = term->is_searching && term->render.refresh.search; + bool urls = urls_mode_is_active(term) && term->render.refresh.urls; + + if (!(grid | csd | search | urls)) + continue; + + if (term->render.app_sync_updates.enabled && !(csd | search | urls)) + continue; + + term->render.refresh.grid = false; + term->render.refresh.csd = false; + term->render.refresh.search = false; + term->render.refresh.urls = false; + + if (term->window->frame_callback == NULL) { + struct grid *original_grid = term->grid; + if (urls_mode_is_active(term)) { + xassert(term->url_grid_snapshot != NULL); + term->grid = term->url_grid_snapshot; + } + + if (csd && term->window->csd_mode == CSD_YES) { + quirk_weston_csd_on(term); + render_csd(term); + quirk_weston_csd_off(term); + } + if (search) + render_search_box(term); + if (urls) + render_urls(term); + if (term->conf->tabs.enabled) + render_tab_bar(term); + if (term->conf->tabs.enabled && tab_overview_is_active(term->window)) + render_tab_overview(term); + if (grid | csd | search | urls) + grid_render(term); + + tll_foreach(term->wl->seats, it) { + if (it->item.ime_focus == term) + ime_update_cursor_rect(&it->item); + } + + term->grid = original_grid; + } else { + /* Tells the frame callback to render again */ + term->render.pending.grid |= grid; + term->render.pending.csd |= csd; + term->render.pending.search |= search; + term->render.pending.urls |= urls; + + /* The tab bar is a desync subsurface — its commits apply + * independently of the parent surface, so we can repaint it + * now instead of deferring to the frame callback (which never + * calls render_tab_bar anyway). Without this, title changes + * while a frame is in flight leave the tab label stale until + * something else dirties the grid. */ + if (grid && term->conf->tabs.enabled) + render_tab_bar(term); + if (grid && term->conf->tabs.enabled && + tab_overview_is_active(term->window)) + render_tab_overview(term); + } + } + + tll_foreach(wayl->seats, it) { + if (it->item.pointer.xcursor_pending) { + if (it->item.pointer.xcursor_callback == NULL) { + render_xcursor_update(&it->item); + it->item.pointer.xcursor_pending = false; + } else { + /* Frame callback will call render_xcursor_update() */ + } + } + } +} + +void +render_refresh_title(struct terminal *term) +{ + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return; + + struct timespec diff; + timespec_sub(&now, &term->render.title.last_update, &diff); + + if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, + }; + + timerfd_settime(term->render.title.timer_fd, 0, &timeout, NULL); + } else { + term->render.title.last_update = now; + render_update_title(term); + } + + render_refresh_csd(term); +} + +void +render_refresh_app_id(struct terminal *term) +{ + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return; + + struct timespec diff; + timespec_sub(&now, &term->render.app_id.last_update, &diff); + + if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, + }; + + timerfd_settime(term->render.app_id.timer_fd, 0, &timeout, NULL); + return; + } + + const char *app_id = + term->app_id != NULL ? term->app_id : term->conf->app_id; + + xdg_toplevel_set_app_id(term->window->xdg_toplevel, app_id); + term->render.app_id.last_update = now; +} + +void +render_refresh_icon(struct terminal *term) +{ + if (term->wl->toplevel_icon_manager == NULL) { + LOG_DBG("compositor does not implement xdg-toplevel-icon: " + "ignoring request to refresh window icon"); + return; + } + + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return; + + struct timespec diff; + timespec_sub(&now, &term->render.icon.last_update, &diff); + + if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, + }; + + timerfd_settime(term->render.icon.timer_fd, 0, &timeout, NULL); + return; + } + + const char *icon_name = term_icon(term); + LOG_DBG("setting toplevel icon: %s", icon_name); + + struct xdg_toplevel_icon_v1 *icon = + xdg_toplevel_icon_manager_v1_create_icon(term->wl->toplevel_icon_manager); + xdg_toplevel_icon_v1_set_name(icon, icon_name); + xdg_toplevel_icon_manager_v1_set_icon( + term->wl->toplevel_icon_manager, term->window->xdg_toplevel, icon); + xdg_toplevel_icon_v1_destroy(icon); + + term->render.icon.last_update = now; +} + +void +render_refresh(struct terminal *term) +{ + term->render.refresh.grid = true; +} + +void +render_refresh_csd(struct terminal *term) +{ + if (term->window->csd_mode == CSD_YES) + term->render.refresh.csd = true; +} + +void +render_refresh_search(struct terminal *term) +{ + if (term->is_searching) + term->render.refresh.search = true; +} + +void +render_refresh_urls(struct terminal *term) +{ + if (urls_mode_is_active(term)) + term->render.refresh.urls = true; +} + +void +render_refresh_tab_bar(struct terminal *term) +{ + if (term->conf->tabs.enabled) + term->render.refresh.grid = true; /* triggers full re-render which includes tab bar */ +} + +static void +draw_rounded_corner_aa(pixman_image_t *pix, const pixman_color_t *color, + int dx, int dy, int r, + int cx_inside, int cy_inside) +{ + if (r <= 0) + return; + + const int stride = (r + 3) & ~3; /* A8 stride aligned to 4 bytes */ + uint8_t *data = xcalloc(stride * r, sizeof(uint8_t)); + + /* Circle center in local (0..r, 0..r) coords of the corner tile. */ + const float cx = cx_inside > 0 ? (float)r : 0.f; + const float cy = cy_inside > 0 ? (float)r : 0.f; + const float r_f = (float)r; + + for (int py = 0; py < r; py++) { + for (int px = 0; px < r; px++) { + const float ex = (float)px + 0.5f - cx; + const float ey = (float)py + 0.5f - cy; + const float dist = sqrtf(ex * ex + ey * ey); + float cov = r_f - dist + 0.5f; + if (cov <= 0.f) + cov = 0.f; + else if (cov >= 1.f) + cov = 1.f; + data[py * stride + px] = (uint8_t)(cov * 255.f + 0.5f); + } + } + + pixman_image_t *mask = pixman_image_create_bits( + PIXMAN_a8, r, r, (uint32_t *)data, stride); + pixman_image_t *src = pixman_image_create_solid_fill(color); + pixman_image_composite32(PIXMAN_OP_OVER, + src, mask, pix, 0, 0, 0, 0, dx, dy, r, r); + pixman_image_unref(mask); + pixman_image_unref(src); + free(data); +} + +static void +draw_rounded_rect(pixman_image_t *pix, const pixman_color_t *color, + int x, int y, int w, int h, int r, unsigned corners) +{ + if (r <= 0 || w <= 0 || h <= 0) { + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y, w, h}); + return; + } + r = min(r, min(w / 2, h / 2)); + + const bool tl = corners & 1; + const bool tr = corners & 2; + const bool bl = corners & 4; + const bool br = corners & 8; + + /* Fill body regions. Corners handled separately if rounded. */ + /* Top band */ + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x + (tl ? r : 0), y, + w - (tl ? r : 0) - (tr ? r : 0), r}); + /* Middle band (full width) */ + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y + r, w, h - 2 * r}); + /* Bottom band */ + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x + (bl ? r : 0), y + h - r, + w - (bl ? r : 0) - (br ? r : 0), r}); + + /* Square corners: fill the r×r square. */ + if (!tl) + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y, r, r}); + if (!tr) + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x + w - r, y, r, r}); + if (!bl) + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y + h - r, r, r}); + if (!br) + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x + w - r, y + h - r, r, r}); + + /* Rounded corners: anti-aliased quarter-circles. */ + if (tl) draw_rounded_corner_aa(pix, color, x, y, r, 1, 1); + if (tr) draw_rounded_corner_aa(pix, color, x + w - r, y, r, -1, 1); + if (bl) draw_rounded_corner_aa(pix, color, x, y + h - r, r, 1, -1); + if (br) draw_rounded_corner_aa(pix, color, x + w - r, y + h - r, r, -1, -1); +} + +static int +tab_label_build(char *buf, size_t bufsz, + const struct terminal *tab, size_t idx) +{ + const char *title = NULL; + if (tab->window_title_has_been_set && tab->window_title != NULL) + title = tab->window_title; + else if (tab->cwd != NULL) { + const char *slash = strrchr(tab->cwd, '/'); + title = (slash != NULL && slash[1] != '\0') ? slash + 1 : tab->cwd; + } + int len = snprintf(buf, bufsz, "%zu: %s", idx + 1, + title != NULL ? title : ""); + if (len < 0) + return 0; + if ((size_t)len >= bufsz) + len = (int)bufsz - 1; + return len; +} + +static int +tab_label_width(struct fcft_font *font, const char *buf, int len) +{ + if (font == NULL || len <= 0) + return 0; + + int total = 0; + const unsigned char *p = (const unsigned char *)buf; + const unsigned char *end = p + len; + while (p < end) { + utf8proc_int32_t cp; + utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); + if (consumed <= 0 || cp < 0) { + p++; + continue; + } + p += consumed; + + const struct fcft_glyph *g = fcft_rasterize_char_utf32( + font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); + if (g != NULL) + total += g->advance.x; + } + return total; +} + +/* Width (px) of the unread indicator + trailing gap, or 0 if disabled */ +static int +tab_unread_indicator_width(struct fcft_font *font, const struct config *conf, + bool has_unread, float scale) +{ + if (!has_unread || font == NULL || + conf->tabs.unread_indicator == NULL || + conf->tabs.unread_indicator[0] == '\0') + return 0; + + int w = tab_label_width(font, conf->tabs.unread_indicator, + (int)strlen(conf->tabs.unread_indicator)); + if (w <= 0) + return 0; + return w + (int)roundf(4 * scale); +} + +/* Draw the unread indicator at (x, baseline_y); returns the x offset to + * advance past the indicator (including trailing gap). */ +static int +render_tab_unread_indicator(pixman_image_t *pix, struct fcft_font *font, + const struct config *conf, float scale, + int x, int baseline_y, bool gamma_correct) +{ + if (font == NULL || conf->tabs.unread_indicator == NULL || + conf->tabs.unread_indicator[0] == '\0') + return 0; + + const pixman_color_t fg = color_hex_to_pixman( + conf->tabs.colors.unread_fg, gamma_correct); + + int gx = x; + const unsigned char *p = (const unsigned char *)conf->tabs.unread_indicator; + const unsigned char *end = p + strlen(conf->tabs.unread_indicator); + + while (p < end) { + utf8proc_int32_t cp; + utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); + if (consumed <= 0 || cp < 0) { + p++; + continue; + } + p += consumed; + + const struct fcft_glyph *g = fcft_rasterize_char_utf32( + font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); + if (g == NULL) + continue; + + if (pixman_image_get_format(g->pix) == PIXMAN_a8r8g8b8) { + pixman_image_composite32(PIXMAN_OP_OVER, + g->pix, NULL, pix, 0, 0, 0, 0, + gx + g->x, baseline_y - g->y, g->width, g->height); + } else { + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32(PIXMAN_OP_OVER, + src, g->pix, pix, 0, 0, 0, 0, + gx + g->x, baseline_y - g->y, g->width, g->height); + pixman_image_unref(src); + } + gx += g->advance.x; + } + return (gx - x) + (int)roundf(4 * scale); +} + +static void +render_tab_label(pixman_image_t *pix, struct fcft_font *font, + const pixman_color_t *fg, const struct terminal *tab, + size_t idx, int x, int y, int max_x, float scale) +{ + char label_buf[256]; + int label_len = tab_label_build(label_buf, sizeof(label_buf), tab, idx); + if (label_len <= 0 || font == NULL) + return; + + const int pad = (int)roundf(tab->conf->tabs.label_padding * scale); + int gx = x + pad; + const int clip_x = max_x - pad; + + const unsigned char *p = (const unsigned char *)label_buf; + const unsigned char *end = p + label_len; + while (p < end) { + utf8proc_int32_t cp; + utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); + if (consumed <= 0 || cp < 0) { + p++; + continue; + } + p += consumed; + + const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( + font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); + if (glyph == NULL) + continue; + + if (gx + glyph->advance.x > clip_x) + break; + + if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { + pixman_image_composite32(PIXMAN_OP_OVER, + glyph->pix, NULL, pix, 0, 0, 0, 0, + gx + glyph->x, y - glyph->y, + glyph->width, glyph->height); + } else { + pixman_image_t *src = pixman_image_create_solid_fill(fg); + pixman_image_composite32(PIXMAN_OP_OVER, + src, glyph->pix, pix, 0, 0, 0, 0, + gx + glyph->x, y - glyph->y, + glyph->width, glyph->height); + pixman_image_unref(src); + } + gx += glyph->advance.x; + } +} + +static void +render_tab_bar(struct terminal *term) +{ + struct wl_window *win = term->window; + if (win->tab_bar.sub == NULL) + return; + + const struct config *conf = term->conf; + const float scale = term->scale; + const bool floating = (conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING); + const bool pos_top = (conf->tabs.position == CONF_TABS_POSITION_TOP); + + /* pill height is always conf->tabs.height; total bar = pill + margin in floating */ + const int margin_px = floating ? (int)roundf(scale * conf->tabs.margin) : 0; + const int tab_h_px = (int)roundf(scale * conf->tabs.height); + const int total_h = tab_h_px + margin_px; + const int width = term->width; + + if (width <= 0 || total_h <= 0) + return; + + struct buffer_chain *chain = term->render.chains.tab_bar; + struct buffer *buf = shm_get_buffer(chain, width, total_h); + if (buf == NULL) + return; + + const bool gamma_correct = wayl_do_linear_blending(win->term->wl, conf); + const bool rounded = (conf->tabs.style == CONF_TABS_STYLE_ROUNDED); + const int r = rounded ? (int)roundf(scale * conf->tabs.corner_radius) : 0; + + /* Clear buffer: transparent for floating (shows terminal behind gaps), bg for span */ + const pixman_color_t transparent = {0, 0, 0, 0}; + const pixman_color_t bg_color = color_hex_to_pixman(conf->tabs.colors.bg, gamma_correct); + pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], + floating ? &transparent : &bg_color, + 1, &(pixman_rectangle16_t){0, 0, width, total_h}); + + const size_t tab_count = win->tab_count; + if (tab_count == 0) + goto commit; + + struct fcft_font *font = term->fonts[0]; + + /* Per-tab layout arrays */ + int tab_xs[TAB_MAX]; + int tab_ws[TAB_MAX]; + int tab_y, tab_h; + + const size_t n = min(tab_count, (size_t)TAB_MAX); + + if (floating) { + const int pad_px = (int)roundf(scale * conf->tabs.tab_padding); + const int max_tw = (int)roundf(scale * conf->tabs.tab_width); + const int label_pad = (int)roundf(scale * conf->tabs.label_padding); + const int min_tw = max(2 * label_pad, 1); + + /* Measure each tab's natural width (label + padding), capped at max_tw */ + int natural_w[TAB_MAX]; + int total_w = 0; + for (size_t i = 0; i < n; i++) { + char buf_s[256]; + int len = tab_label_build(buf_s, sizeof(buf_s), win->tabs[i], i); + int lw = tab_label_width(font, buf_s, len); + int iw = tab_unread_indicator_width( + font, conf, win->tabs[i]->has_unread, scale); + int w = lw + iw + 2 * label_pad; + if (w > max_tw) w = max_tw; + if (w < min_tw) w = min_tw; + natural_w[i] = w; + total_w += w; + } + const int total_pads = pad_px * ((int)n - 1); + int bar_w = total_w + total_pads; + + /* Shrink uniformly if we overflow the bar width */ + if (bar_w > width && total_w > 0) { + const int avail = max(width - total_pads, (int)n); + int used = 0; + for (size_t i = 0; i < n; i++) { + int w = (int)((int64_t)natural_w[i] * avail / total_w); + if (w < 1) w = 1; + tab_ws[i] = w; + used += w; + } + /* Distribute rounding residue to the first few tabs */ + int residue = avail - used; + for (size_t i = 0; i < n && residue > 0; i++, residue--) + tab_ws[i]++; + bar_w = avail + total_pads; + } else { + for (size_t i = 0; i < n; i++) + tab_ws[i] = natural_w[i]; + } + + int x_cur = (width - bar_w) / 2; + for (size_t i = 0; i < n; i++) { + tab_xs[i] = x_cur; + x_cur += tab_ws[i] + pad_px; + } + + tab_y = pos_top ? margin_px : 0; + tab_h = tab_h_px; + } else { + const int base = width / (int)n; + int x_cur = 0; + for (size_t i = 0; i < n; i++) { + tab_xs[i] = x_cur; + tab_ws[i] = (i + 1 == n) ? (width - x_cur) : base; + x_cur += base; + } + tab_y = 0; + tab_h = tab_h_px; + } + + /* Text baseline centered within the pill */ + const int text_baseline = tab_y + (font + ? (tab_h - (int)(font->ascent + font->descent)) / 2 + (int)font->ascent + : tab_h / 2); + + for (size_t i = 0; i < n; i++) { + struct terminal *tab = win->tabs[i]; + const bool is_active = (i == win->active_tab); + const int x = tab_xs[i]; + const int w = tab_ws[i]; + + const pixman_color_t tab_bg = color_hex_to_pixman( + is_active ? conf->tabs.colors.active_bg : conf->tabs.colors.bg, + gamma_correct); + const pixman_color_t fg_color = color_hex_to_pixman( + is_active ? conf->tabs.colors.active_fg : conf->tabs.colors.fg, + gamma_correct); + + if (rounded) { + /* Floating: all 4 corners rounded. Span: only the open edge rounded. */ + unsigned corners; + if (floating) { + corners = 0xf; /* all corners */ + } else if (pos_top) { + corners = 0x3; /* top-left + top-right */ + } else { + corners = 0xc; /* bottom-left + bottom-right */ + } + draw_rounded_rect(buf->pix[0], &tab_bg, x, tab_y, w, tab_h, r, corners); + } else { + pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &tab_bg, + 1, &(pixman_rectangle16_t){x, tab_y, w, tab_h}); + } + + /* Span: separator between inactive tabs */ + if (!floating && !is_active && i + 1 < n) { + const pixman_color_t sep = color_hex_to_pixman( + conf->tabs.colors.bg, gamma_correct); + pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &sep, + 1, &(pixman_rectangle16_t){x + w - 1, tab_y + 2, 1, tab_h - 4}); + } + + if (font != NULL) { + int label_x = x; + if (tab->has_unread) { + const int pad = (int)roundf(conf->tabs.label_padding * scale); + int adv = render_tab_unread_indicator( + buf->pix[0], font, conf, scale, + x + pad, text_baseline, gamma_correct); + label_x = x + adv; + } + render_tab_label(buf->pix[0], font, &fg_color, tab, i, + label_x, text_baseline, x + w, scale); + } + } + + /* Publish layout for hit-testing */ + for (size_t i = 0; i < n; i++) { + win->tab_layout.xs[i] = tab_xs[i]; + win->tab_layout.ws[i] = tab_ws[i]; + } + win->tab_layout.y = tab_y; + win->tab_layout.h = tab_h; + win->tab_layout.count = n; + +commit: ; + if (tab_count == 0) + win->tab_layout.count = 0; + const int y_pos = pos_top ? 0 : term->height - total_h; + wl_subsurface_set_position(win->tab_bar.sub, 0, (int)roundf(y_pos / scale)); + + wayl_surface_scale(win, &win->tab_bar.surface, buf, scale); + wl_surface_attach(win->tab_bar.surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(win->tab_bar.surface.surf, 0, 0, width, total_h); + + if (!floating) { + struct wl_region *region = wl_compositor_create_region(term->wl->compositor); + if (region != NULL) { + wl_region_add(region, 0, 0, width, total_h); + wl_surface_set_opaque_region(win->tab_bar.surface.surf, region); + wl_region_destroy(region); + } + } else { + wl_surface_set_opaque_region(win->tab_bar.surface.surf, NULL); + } + + wl_surface_commit(win->tab_bar.surface.surf); +} + +/* === Tab overview === + * + * A full-window sub-surface that grids out all tabs as live downscaled + * thumbnails, animating in/out with a 400ms ease-out-cubic on scale + + * opacity. The animation duration matches libadwaita's Adw.TabOverview. + */ + +#define TAB_OVERVIEW_DURATION_NS 400000000ull + +static uint64_t +now_monotonic_ns(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; +} + +static float +ease_out_cubic(float t) +{ + if (t <= 0.f) return 0.f; + if (t >= 1.f) return 1.f; + float u = 1.f - t; + return 1.f - u * u * u; +} + +bool +tab_overview_is_active(const struct wl_window *win) +{ + if (win == NULL) + return false; + const __typeof__(win->tab_overview_state) *s = &win->tab_overview_state; + return s->target_open || s->progress > 0.f || s->visible; +} + +void +tab_overview_toggle(struct wl_window *win) +{ + if (win == NULL || win->tab_overview.sub == NULL) + return; + + /* Snapshot current progress so the animation continues smoothly even + * if the user toggles mid-fade. */ + struct terminal *active = win->tabs[win->active_tab]; + if (active == NULL) + return; + + win->tab_overview_state.target_open = !win->tab_overview_state.target_open; + win->tab_overview_state.anim_from = win->tab_overview_state.progress; + win->tab_overview_state.anim_start_ns = now_monotonic_ns(); + win->tab_overview_state.sel_idx = win->active_tab; + win->tab_overview_state.hover_idx = -1; + + render_refresh(active); +} + +void +tab_overview_close_instant(struct wl_window *win) +{ + if (win == NULL || win->tab_overview.sub == NULL) + return; + __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; + st->target_open = false; + st->progress = 0.f; + st->anim_from = 0.f; + st->anim_start_ns = 0; + st->hover_idx = -1; + + /* Hide the sub-surface immediately so the new active tab is visible + * without going through the fade-out frames. */ + if (st->visible) { + wl_surface_attach(win->tab_overview.surface.surf, NULL, 0, 0); + wl_surface_commit(win->tab_overview.surface.surf); + st->visible = false; + } + st->card_count = 0; + + struct terminal *active = win->tabs[win->active_tab]; + if (active != NULL) + render_refresh(active); +} + +void +render_refresh_tab_overview(struct terminal *term) +{ + if (term->conf->tabs.enabled) + term->render.refresh.grid = true; +} + +/* Compute the {cols, rows, card_w, card_h, gap, top_left} layout for n + * tabs in a window of (win_w, win_h) buffer pixels. The card aspect ratio + * mirrors the window's. */ +static void +overview_layout(int win_w, int win_h, size_t n, + int *out_cols, int *out_rows, + int *out_card_w, int *out_card_h, + int *out_gap, int *out_origin_x, int *out_origin_y, + int *out_title_h) +{ + if (n == 0 || win_w <= 0 || win_h <= 0) { + *out_cols = *out_rows = 0; + *out_card_w = *out_card_h = 0; + *out_gap = *out_origin_x = *out_origin_y = *out_title_h = 0; + return; + } + + const int margin = max(24, win_w / 32); + const int gap = max(12, win_w / 64); + const int title_h = max(18, win_h / 36); + + /* Pick column count that yields cards closest to window aspect ratio. + * cols = round(sqrt(n * win_w/win_h)) is the standard heuristic. */ + const float aspect = (float)win_w / (float)win_h; + int cols = (int)roundf(sqrtf((float)n * aspect)); + if (cols < 1) cols = 1; + if ((size_t)cols > n) cols = (int)n; + int rows = (int)((n + cols - 1) / cols); + + /* Compute card size that fits both axes */ + int avail_w = win_w - 2 * margin - (cols - 1) * gap; + int avail_h = win_h - 2 * margin - (rows - 1) * gap; + if (avail_w < 1) avail_w = 1; + if (avail_h < 1) avail_h = 1; + + int card_w = avail_w / cols; + int card_h_from_w = (int)((int64_t)card_w * win_h / win_w); + int card_h_avail = (avail_h - rows * title_h) / rows; + if (card_h_avail < 1) card_h_avail = 1; + + int card_h; + if (card_h_from_w <= card_h_avail) { + card_h = card_h_from_w; + } else { + card_h = card_h_avail; + card_w = (int)((int64_t)card_h * win_w / win_h); + } + if (card_w < 1) card_w = 1; + if (card_h < 1) card_h = 1; + + const int total_w = cols * card_w + (cols - 1) * gap; + const int total_h = rows * (card_h + title_h) + (rows - 1) * gap; + const int origin_x = (win_w - total_w) / 2; + const int origin_y = (win_h - total_h) / 2; + + *out_cols = cols; + *out_rows = rows; + *out_card_w = card_w; + *out_card_h = card_h; + *out_gap = gap; + *out_origin_x = origin_x; + *out_origin_y = origin_y; + *out_title_h = title_h; +} + +static void +draw_card_thumbnail(pixman_image_t *dst, struct buffer *src_buf, + int dst_x, int dst_y, int dst_w, int dst_h) +{ + if (src_buf == NULL || src_buf->pix == NULL || src_buf->pix[0] == NULL || + src_buf->width <= 0 || src_buf->height <= 0 || + dst_w <= 0 || dst_h <= 0) + return; + + pixman_image_t *src = src_buf->pix[0]; + pixman_image_set_filter(src, PIXMAN_FILTER_BILINEAR, NULL, 0); + + pixman_transform_t xform; + pixman_transform_init_scale(&xform, + pixman_double_to_fixed((double)src_buf->width / dst_w), + pixman_double_to_fixed((double)src_buf->height / dst_h)); + pixman_image_set_transform(src, &xform); + + pixman_image_composite32(PIXMAN_OP_SRC, + src, NULL, dst, + 0, 0, 0, 0, + dst_x, dst_y, dst_w, dst_h); + + /* Reset for subsequent normal use */ + pixman_image_set_transform(src, NULL); + pixman_image_set_filter(src, PIXMAN_FILTER_NEAREST, NULL, 0); +} + +static void +render_tab_overview(struct terminal *term) +{ + struct wl_window *win = term->window; + if (win->tab_overview.sub == NULL) + return; + + __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; + + /* Advance animation */ + if (st->progress != (st->target_open ? 1.f : 0.f)) { + const uint64_t now = now_monotonic_ns(); + const uint64_t elapsed = now - st->anim_start_ns; + float t = (float)elapsed / (float)TAB_OVERVIEW_DURATION_NS; + if (t < 0.f) t = 0.f; + if (t > 1.f) t = 1.f; + const float eased = ease_out_cubic(t); + const float target = st->target_open ? 1.f : 0.f; + st->progress = st->anim_from + (target - st->anim_from) * eased; + if (t >= 1.f) + st->progress = target; + } + + /* Hide sub-surface entirely when fully closed */ + if (st->progress <= 0.f && !st->target_open) { + if (st->visible) { + wl_surface_attach(win->tab_overview.surface.surf, NULL, 0, 0); + wl_surface_commit(win->tab_overview.surface.surf); + st->visible = false; + } + st->card_count = 0; + return; + } + + const float scale = term->scale; + const int width = term->width; + const int height = term->height; + if (width <= 0 || height <= 0) + return; + + struct buffer_chain *chain = term->render.chains.tab_overview; + struct buffer *buf = shm_get_buffer(chain, width, height); + if (buf == NULL) + return; + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + + /* Backdrop dim: black at progress*0.55 alpha */ + { + const uint16_t dim = (uint16_t)(0xffff * (0.55f * st->progress)); + const pixman_color_t dim_color = {0, 0, 0, dim}; + pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], + &dim_color, 1, + &(pixman_rectangle16_t){0, 0, width, height}); + } + + const size_t n = min(win->tab_count, (size_t)TAB_MAX); + if (n == 0) + goto commit; + + int cols, rows, card_w, card_h, gap, origin_x, origin_y, title_h; + overview_layout(width, height, n, + &cols, &rows, &card_w, &card_h, &gap, + &origin_x, &origin_y, &title_h); + + /* Scale factor: 0.92 -> 1.0 over the animation */ + const float card_scale = 0.92f + 0.08f * st->progress; + const int sc_w = (int)roundf(card_w * card_scale); + const int sc_h = (int)roundf(card_h * card_scale); + const int sc_title_h = (int)roundf(title_h * card_scale); + + const int corner_r = max(6, (int)roundf(8.f * scale)); + + struct fcft_font *font = term->fonts[0]; + + /* Card opacity: ramps in faster than the backdrop */ + const float card_alpha = st->progress; + const uint16_t alpha16 = (uint16_t)(0xffff * card_alpha); + + /* Use the window/terminal palette to make cards feel native */ + pixman_color_t card_bg = color_hex_to_pixman(term->colors.bg, gamma_correct); + pixman_color_t card_fg = color_hex_to_pixman(term->colors.fg, gamma_correct); + /* Accent for active card: borrow active_bg from tab config if rounded + * tabs are configured, otherwise tint by mixing fg/bg */ + pixman_color_t accent = color_hex_to_pixman( + term->conf->tabs.colors.active_bg ? term->conf->tabs.colors.active_bg + : term->colors.fg, + gamma_correct); + card_bg.alpha = alpha16; + accent.alpha = alpha16; + + pixman_color_t title_bg = {0, 0, 0, (uint16_t)(0x8000 * card_alpha)}; + + st->card_count = n; + + for (size_t i = 0; i < n; i++) { + const int col = (int)(i % (size_t)cols); + const int row = (int)(i / (size_t)cols); + + /* Per-card layout (full size) */ + const int cell_x = origin_x + col * (card_w + gap); + const int cell_y = origin_y + row * (card_h + title_h + gap); + + /* Cache full-size geometry for hit-testing */ + st->cards[i].x = cell_x; + st->cards[i].y = cell_y; + st->cards[i].w = card_w; + st->cards[i].h = card_h + title_h; + + /* Scaled-around-center geometry for drawing */ + const int draw_x = cell_x + (card_w - sc_w) / 2; + const int draw_y = cell_y + (card_h + title_h - sc_h - sc_title_h) / 2; + + const bool is_active = (i == win->active_tab); + const bool is_sel = (i == st->sel_idx); + const bool is_hover = ((int)i == st->hover_idx); + + /* Card background */ + draw_rounded_rect(buf->pix[0], &card_bg, + draw_x, draw_y, sc_w, sc_h + sc_title_h, + corner_r, 0xf); + + /* Thumbnail of the tab's last rendered grid */ + struct terminal *tab = win->tabs[i]; + if (tab != NULL && sc_w > 4 && sc_h > 4) { + draw_card_thumbnail(buf->pix[0], tab->render.last_buf, + draw_x + 2, draw_y + 2, + sc_w - 4, sc_h - 4); + } + + /* Title bar strip across the bottom of the card */ + pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &title_bg, + 1, &(pixman_rectangle16_t){ + draw_x, draw_y + sc_h, sc_w, sc_title_h}); + + if (font != NULL && tab != NULL) { + const int baseline = draw_y + sc_h + + (sc_title_h - (int)(font->ascent + font->descent)) / 2 + + (int)font->ascent; + const int label_pad = max(4, (int)roundf(4.f * scale)); + render_tab_label(buf->pix[0], font, &card_fg, tab, i, + draw_x + label_pad, baseline, + draw_x + sc_w - label_pad, scale); + } + + /* Highlights (drawn on top): a 2-3 px ring */ + if (is_active || is_sel || is_hover) { + const int ring = max(2, (int)roundf(2.f * scale)) + + (is_active ? 1 : 0); + pixman_color_t ring_color = accent; + if (!is_active && is_hover) { + ring_color.alpha = (uint16_t)(0xc000 * card_alpha); + } else if (!is_active && is_sel) { + ring_color.alpha = (uint16_t)(0xa000 * card_alpha); + } + /* Top */ + pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, + 1, &(pixman_rectangle16_t){draw_x, draw_y, sc_w, ring}); + /* Bottom */ + pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, + 1, &(pixman_rectangle16_t){ + draw_x, draw_y + sc_h + sc_title_h - ring, sc_w, ring}); + /* Left */ + pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, + 1, &(pixman_rectangle16_t){ + draw_x, draw_y, ring, sc_h + sc_title_h}); + /* Right */ + pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, + 1, &(pixman_rectangle16_t){ + draw_x + sc_w - ring, draw_y, ring, sc_h + sc_title_h}); + } + } + +commit: + wl_subsurface_set_position(win->tab_overview.sub, 0, 0); + wayl_surface_scale(win, &win->tab_overview.surface, buf, scale); + wl_surface_attach(win->tab_overview.surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(win->tab_overview.surface.surf, 0, 0, width, height); + wl_surface_set_opaque_region(win->tab_overview.surface.surf, NULL); + wl_surface_commit(win->tab_overview.surface.surf); + st->visible = true; + + /* Keep animating until we reach the target */ + if (st->progress != (st->target_open ? 1.f : 0.f)) + term->render.refresh.grid = true; +} + +int +tab_overview_hit_test(struct wl_window *win, int x_buf, int y_buf) +{ + if (win == NULL || !tab_overview_is_active(win)) + return -1; + const __typeof__(win->tab_overview_state) *s = &win->tab_overview_state; + for (size_t i = 0; i < s->card_count; i++) { + const int x = s->cards[i].x; + const int y = s->cards[i].y; + const int w = s->cards[i].w; + const int h = s->cards[i].h; + if (x_buf >= x && x_buf < x + w && y_buf >= y && y_buf < y + h) + return (int)i; + } + return -1; +} + +bool +render_xcursor_set(struct seat *seat, struct terminal *term, + enum cursor_shape shape) +{ + if (seat->pointer.theme == NULL && seat->pointer.shape_device == NULL) + return false; + + if (seat->mouse_focus == NULL) { + seat->pointer.shape = CURSOR_SHAPE_NONE; + return true; + } + + if (seat->mouse_focus != term) { + /* This terminal doesn't have mouse focus */ + return true; + } + + if (seat->pointer.shape == shape && + !(shape == CURSOR_SHAPE_CUSTOM && + !streq(seat->pointer.last_custom_xcursor, + term->mouse_user_cursor))) + { + return true; + } + + if (shape == CURSOR_SHAPE_HIDDEN) { + seat->pointer.cursor = NULL; + free(seat->pointer.last_custom_xcursor); + seat->pointer.last_custom_xcursor = NULL; + } + + else if (seat->pointer.shape_device == NULL) { + const char *const custom_xcursors[] = {term->mouse_user_cursor, NULL}; + const char *const *xcursors = shape == CURSOR_SHAPE_CUSTOM + ? custom_xcursors + : cursor_shape_to_string(shape); + + xassert(xcursors[0] != NULL); + + seat->pointer.cursor = NULL; + + for (size_t i = 0; xcursors[i] != NULL; i++) { + seat->pointer.cursor = + wl_cursor_theme_get_cursor(seat->pointer.theme, xcursors[i]); + + if (seat->pointer.cursor != NULL) { + LOG_DBG("loaded xcursor %s", xcursors[i]); + break; + } + } + + if (seat->pointer.cursor == NULL) { + LOG_ERR( + "failed to load xcursor pointer '%s', and all of its fallbacks", + xcursors[0]); + return false; + } + } else { + /* Server-side cursors - no need to load anything */ + } + + if (shape == CURSOR_SHAPE_CUSTOM) { + free(seat->pointer.last_custom_xcursor); + seat->pointer.last_custom_xcursor = + xstrdup(term->mouse_user_cursor); + } + + /* FDM hook takes care of actual rendering */ + seat->pointer.shape = shape; + seat->pointer.xcursor_pending = true; + return true; +} + +void +render_buffer_release_callback(struct buffer *buf, void *data) +{ + /* + * Called from shm.c when a buffer is released + * + * We use it to pre-apply last-frame's damage to it, when we're + * forced to double buffer (compositor doesn't release buffers + * immediately). + * + * The timeline is thus: + * 1. We render and push a new frame + * 2. Some (hopefully short) time after that, the compositor releases the previous buffer + * 3. We're called, and kick off the thread that copies the changes from (1) to the just freed buffer + * 4. Time passes.... + * 5. The compositor calls our frame callback, signalling to us that it's time to start rendering the next frame + * 6. Hopefully, our thread is already done with copying the changes, otherwise we stall, waiting for it + * 7. We render the frame as if the compositor does immediate releases. + * + * What's the gain? Reduced latency, by applying the previous + * frame's damage as soon as possible, we shorten the time it + * takes to render the frame after the frame callback. + * + * This means the compositor can, in theory, push the frame + * callback closer to the vblank deadline, and thus reduce input + * latency. Not all compositors (most, in fact?) don't adapt like + * this, unfortunately. But some allows the user to manually + * configure the deadline. + */ + struct terminal *term = data; + + if (likely(buf->age != 1)) + return; + + if (likely(!term->render.preapply_last_frame_damage)) + return; + + if (term->render.last_buf == NULL) + return; + + if (term->render.last_buf->age != 0) + return; + + if (buf->width != term->render.last_buf->width) + return; + + if (buf->height != term->render.last_buf->height) + return; + + xassert(term->render.workers.count > 0); + xassert(term->render.last_buf != NULL); + + xassert(term->render.last_buf->age == 0); + xassert(term->render.last_buf != buf); + + mtx_lock(&term->render.workers.preapplied_damage.lock); + if (term->render.workers.preapplied_damage.buf != NULL) { + mtx_unlock(&term->render.workers.preapplied_damage.lock); + return; + } + + xassert(term->render.workers.preapplied_damage.buf == NULL); + term->render.workers.preapplied_damage.buf = buf; + term->render.workers.preapplied_damage.start = (struct timespec){0}; + term->render.workers.preapplied_damage.stop = (struct timespec){0}; + mtx_unlock(&term->render.workers.preapplied_damage.lock); + + mtx_lock(&term->render.workers.lock); + sem_post(&term->render.workers.start); + xassert(tll_length(term->render.workers.queue) == 0); + tll_push_back(term->render.workers.queue, -3); + mtx_unlock(&term->render.workers.lock); +} diff --git a/render.h b/render.h new file mode 100644 index 0000000..04d4328 --- /dev/null +++ b/render.h @@ -0,0 +1,67 @@ +#pragma once +#include + +#include "terminal.h" +#include "fdm.h" +#include "wayland.h" +#include "misc.h" + +struct renderer; +struct renderer *render_init(struct fdm *fdm, struct wayland *wayl); +void render_destroy(struct renderer *renderer); + +enum resize_options { + RESIZE_NORMAL = 0, + RESIZE_FORCE = 1 << 0, + RESIZE_BY_CELLS = 1 << 1, + RESIZE_KEEP_GRID = 1 << 2, +}; + +bool render_resize( + struct terminal *term, int width, int height, uint8_t resize_options); + +void render_refresh(struct terminal *term); +void render_refresh_app_id(struct terminal *term); +void render_refresh_icon(struct terminal *term); +void render_refresh_csd(struct terminal *term); +void render_refresh_search(struct terminal *term); +void render_refresh_title(struct terminal *term); +void render_refresh_urls(struct terminal *term); +void render_refresh_tab_bar(struct terminal *term); +void render_refresh_tab_overview(struct terminal *term); + +/* Toggle the tab-overview animation on the given window. */ +void tab_overview_toggle(struct wl_window *win); + +/* Snap the overview fully closed with no closing animation. Used when + * the user activates a card (click / Enter / digit). */ +void tab_overview_close_instant(struct wl_window *win); + +/* Hit test in window-buffer pixel coords. Returns card index or -1. */ +int tab_overview_hit_test(struct wl_window *win, int x_buf, int y_buf); + +/* Whether overview is currently visible (open or animating). */ +bool tab_overview_is_active(const struct wl_window *win); +bool render_xcursor_set( + struct seat *seat, struct terminal *term, enum cursor_shape shape); +bool render_xcursor_is_valid(const struct seat *seat, const char *cursor); + +void render_overlay(struct terminal *term); + +struct render_worker_context { + int my_id; + struct terminal *term; +}; +int render_worker_thread(void *_ctx); + +struct csd_data { + int x; + int y; + int width; + int height; +}; + +struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx); + +void render_buffer_release_callback(struct buffer *buf, void *data); +void render_wait_for_preapply_damage(struct terminal *term); diff --git a/scripts/benchmark.py b/scripts/benchmark.py new file mode 100755 index 0000000..fe820d9 --- /dev/null +++ b/scripts/benchmark.py @@ -0,0 +1,51 @@ +#!/usr/bin/env -S python3 -u + +import argparse +import fcntl +import os +import statistics +import struct +import sys +import termios + +from datetime import datetime + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('files', type=argparse.FileType('rb'), nargs='+') + parser.add_argument('--iterations', type=int, default=20) + + args = parser.parse_args() + + lines, cols, height, width = struct.unpack( + 'HHHH', + fcntl.ioctl(sys.stdout.fileno(), + termios.TIOCGWINSZ, + struct.pack('HHHH', 0, 0, 0, 0))) + + times: dict[str, list[float]] = {name: [] for name in [f.name for f in args.files]} + + for f in args.files: + bench_bytes = f.read() + + for _ in range(args.iterations): + start = datetime.now() + sys.stdout.buffer.write(bench_bytes) + stop = datetime.now() + + times[f.name].append((stop - start).total_seconds()) + + del bench_bytes + + print('\033[J') + print(times) + print(f'cols={cols}, lines={lines}, width={width}px, height={height}px') + for f in args.files: + print(f'{os.path.basename(f.name)}: ' + f'{statistics.mean(times[f.name]):.3f}s ' + f'±{statistics.stdev(times[f.name]):.3f}') + + +if __name__ == '__main__': + main() diff --git a/scripts/generate-alt-random-writes.py b/scripts/generate-alt-random-writes.py new file mode 100755 index 0000000..7ad1460 --- /dev/null +++ b/scripts/generate-alt-random-writes.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +import argparse +import enum +import fcntl +import random +import signal +import struct +import sys +import termios + +from typing import Any + + +class ColorVariant(enum.IntEnum): + NONE = enum.auto() + REGULAR = enum.auto() + BRIGHT = enum.auto() + CUBE = enum.auto() + RGB = enum.auto() + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + 'out', type=argparse.FileType(mode='w'), nargs='?', help='name of output file') + parser.add_argument('--cols', type=int) + parser.add_argument('--rows', type=int) + parser.add_argument('--colors-regular', action='store_true') + parser.add_argument('--colors-bright', action='store_true') + parser.add_argument('--colors-256', action='store_true') + parser.add_argument('--colors-rgb', action='store_true') + parser.add_argument('--scroll', action='store_true') + parser.add_argument('--scroll-region', action='store_true') + parser.add_argument('--attr-bold', action='store_true') + parser.add_argument('--attr-italic', action='store_true') + parser.add_argument('--attr-underline', action='store_true') + parser.add_argument('--sixel', action='store_true') + parser.add_argument('--seed', type=int) + + opts = parser.parse_args() + out = opts.out if opts.out is not None else sys.stdout + + lines: int | None = None + cols: int | None = None + width: int | None = None + height: int | None = None + + if opts.rows is None or opts.cols is None: + try: + def dummy(*args: Any) -> None: + """Need a handler installed for sigwait() to trigger.""" + _ = args + pass + signal.signal(signal.SIGWINCH, dummy) + + while True: + with open('/dev/tty', 'rb') as pty: + lines, cols, height, width = struct.unpack( + 'HHHH', + fcntl.ioctl(pty, + termios.TIOCGWINSZ, + struct.pack('HHHH', 0, 0, 0, 0))) + + assert width is not None + assert height is not None + + if width > 0 and height > 0: + break + + # We’re early; the foot window hasn’t been mapped yet. Or, + # to be more precise, fonts haven’t yet been loaded, + # meaning it doesn’t have any cell geometry yet. + signal.sigwait([signal.SIGWINCH]) + + signal.signal(signal.SIGWINCH, signal.SIG_DFL) + + except OSError: + lines = None + cols = None + height = None + width = None + + if opts.rows is not None: + lines = opts.rows + assert lines is not None + height = 15 * lines # PGO helper binary hardcodes cell height to 15px + if opts.cols is not None: + cols = opts.cols + assert cols is not None + width = 8 * cols # PGO help binary hardcodes cell width to 8px + + if lines is None or cols is None or height is None or width is None: + raise Exception('could not get terminal width/height; use --rows and --cols') + + assert lines > 0, f'{lines}' + assert cols > 0, f'{cols}' + assert width > 0, f'{width}' + assert height > 0, f'{height}' + + # Number of characters to write to screen + count = 256 * 1024**1 + + # Characters to choose from + alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRTSTUVWXYZ0123456789 öäå 👨👩🧒👩🏽‍🔬🇸🇪' + + color_variants = ([ColorVariant.NONE] + + ([ColorVariant.REGULAR] if opts.colors_regular else []) + + ([ColorVariant.BRIGHT] if opts.colors_bright else []) + + ([ColorVariant.CUBE] if opts.colors_256 else []) + + ([ColorVariant.RGB] if opts.colors_rgb else [])) + + # Enter alt screen + out.write('\033[?1049h') + + # uses system time or /dev/urandom if available if opt.seed == None + # pin seeding method to make seeding stable across future versions + random.seed(a=opts.seed, version=2) + + for _ in range(count): + if opts.scroll and random.randrange(256) == 0: + out.write('\033[m') + + if opts.scroll_region and random.randrange(256) == 0: + top = random.randrange(3) + bottom = random.randrange(3) + out.write(f'\033[{top};{lines - bottom}r') + + lines_to_scroll = random.randrange(lines - 1) + rev = random.randrange(2) + if not rev and random.randrange(2): + out.write(f'\033[{lines};{cols}H') + out.write('\n' * lines_to_scroll) + else: + out.write(f'\033[{lines_to_scroll + 1}{"T" if rev == 1 else "S"}') + continue + + # Generate a random location and a random character + row = random.randrange(lines) + col = random.randrange(cols) + c = random.choice(alphabet) + + repeat = random.randrange((cols - col) + 1) + assert col + repeat <= cols + + color_variant = random.choice(color_variants) + + # Position cursor + out.write(f'\033[{row + 1};{col + 1}H') + + if color_variant in [ColorVariant.REGULAR, ColorVariant.BRIGHT]: + do_bg = random.randrange(2) + base = 40 if do_bg else 30 + base += 60 if color_variant == ColorVariant.BRIGHT else 0 + + idx = random.randrange(8) + out.write(f'\033[{base + idx}m') + + elif color_variant == ColorVariant.CUBE: + do_bg = random.randrange(2) + base = 48 if do_bg else 38 + + idx = random.randrange(256) + if random.randrange(2): + # Old-style + out.write(f'\033[{base};5;{idx}m') + else: + # New-style (sub-parameter based) + out.write(f'\033[{base}:5:{idx}m') + + elif color_variant == ColorVariant.RGB: + do_bg = random.randrange(2) + base = 48 if do_bg else 38 + + # use list comprehension in favor of randbytes(n) + # which is only available for Python >= 3.9 + rgb = [random.randrange(256) for _ in range(3)] + + if random.randrange(2): + # Old-style + out.write(f'\033[{base};2;{rgb[0]};{rgb[1]};{rgb[2]}m') + else: + # New-style (sub-parameter based) + out.write(f'\033[{base}:2::{rgb[0]}:{rgb[1]}:{rgb[2]}m') + + if opts.attr_bold and random.randrange(5) == 0: + out.write('\033[1m') + if opts.attr_italic and random.randrange(5) == 0: + out.write('\033[3m') + if opts.attr_underline and random.randrange(5) == 0: + out.write('\033[4m') + + out.write(c * repeat) + + do_sgr_reset = random.randrange(2) + if do_sgr_reset: + reset_actions = ['\033[m', '\033[39m', '\033[49m'] + out.write(random.choice(reset_actions)) + + # Reset colors + out.write('\033[m\033[r') + + if opts.sixel: + # The sixel 'alphabet' + sixels = '?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~' + + last_pos: tuple[int, int] | None = None + last_size: tuple[int, int] = 0, 0 + + for _ in range(20): + if last_pos is not None and random.randrange(2): + # Overwrite last sixel. I.e. use same position and + # size as last sixel + pass + else: + # Random origin in upper left quadrant + last_pos = random.randrange(lines // 2) + 1, random.randrange(cols // 2) + 1 + last_size = random.randrange((height + 1) // 2), random.randrange((width + 1) // 2) + + out.write(f'\033[{last_pos[0]};{last_pos[1]}H') + six_height, six_width = last_size + six_rows = (six_height + 5) // 6 # Round up; each sixel is 6 pixels + + # Begin sixel (with P2 set to either 0 or 1 - opaque or transparent) + sixel_p2 = random.randrange(2) + out.write(f'\033P;{sixel_p2}q') + + # Sixel size. Without this, sixels will be + # auto-resized on cell-boundaries. + out.write(f'"1;1;{six_width};{six_height}') + + # Set up 256 random colors + for idx in range(256): + # param 2: 1=HLS, 2=RGB. + # param 3/4/5: HLS/RGB values in range 0-100 + # (except 'hue' which is 0..360) + out.write(f'#{idx};2;{random.randrange(101)};{random.randrange(101)};{random.randrange(101)}') + + for row in range(six_rows): + band_count = random.randrange(4, 33) + for band in range(band_count): + # Choose a random color + out.write(f'#{random.randrange(256)}') + + if random.randrange(2): + for col in range(six_width): + out.write(f'{random.choice(sixels)}') + else: + pix_left = six_width + while pix_left > 0: + repeat_count = random.randrange(1, pix_left + 1) + out.write(f'!{repeat_count}{random.choice(sixels)}') + pix_left -= repeat_count + + # Next line + if band + 1 < band_count: + # Move cursor to beginning of current row + out.write('$') + elif row + 1 < six_rows: + # Newline + out.write('-') + + # End sixel + out.write('\033\\') + + # Leave alt screen + out.write('\033[?1049l') + + +if __name__ == '__main__': + main() diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py new file mode 100755 index 0000000..c10373d --- /dev/null +++ b/scripts/generate-builtin-terminfo.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 + +import argparse +import os +import re + + +class Capability: + def __init__(self, name: str, value: bool | int | str): + self._name = name + self._value = value + + @property + def name(self) -> str: + return self._name + + @property + def value(self) -> bool | int | str: + return self._value + + def __lt__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return self._name < other._name + + def __le__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return self._name <= other._name + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return self._name == other._name + + def __ne__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return bool(self._name != other._name) + + def __gt__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return bool(self._name > other._name) + + def __ge__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return self._name >= other._name + + +class BoolCapability(Capability): + def __init__(self, name: str): + super().__init__(name, True) + + +class IntCapability(Capability): + pass + + +class StringCapability(Capability): + def __init__(self, name: str, value: str): + # see terminfo(5) for valid escape sequences + + # Control characters + def translate_ctrl_chr(m: re.Match[str]) -> str: + ctrl = m.group(1) + if ctrl == '?': + return '\\x7f' + return f'\\x{ord(ctrl) - ord("@"):02x}' + value = re.sub(r'\^([@A-Z[\\\\\]^_?])', translate_ctrl_chr, value) + + # Ensure e.g. \E7 (or \e7) doesn’t get translated to “\0337”, + # which would be interpreted as octal 337 by the C compiler + value = re.sub(r'(\\E|\\e)([0-7])', r'\\033" "\2', value) + + # Replace \E and \e with ESC + value = re.sub(r'\\E|\\e', r'\\033', value) + + # Unescape ,:^ + value = re.sub(r'\\(,|:|\^)', r'\1', value) + + # Replace \s with space + value = value.replace('\\s', ' ') + + # Let \\, \n, \r, \t, \b and \f "fall through", to the C string literal + + if re.search(r'\\l', value): + raise NotImplementedError('\\l escape sequence') + + super().__init__(name, value) + + +class Fragment: + def __init__(self, name: str, description: str): + self._name = name + self._description = description + self._caps = dict[str, Capability]() + + @property + def name(self) -> str: + return self._name + + @property + def description(self) -> str: + return self._description + + @property + def caps(self) -> dict[str, Capability]: + return self._caps + + def add_capability(self, cap: Capability) -> None: + assert cap.name not in self._caps + self._caps[cap.name] = cap + + def del_capability(self, name: str) -> None: + del self._caps[name] + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('source_entry_name') + parser.add_argument('source', type=argparse.FileType('r')) + parser.add_argument('target_entry_name') + parser.add_argument('target', type=argparse.FileType('w')) + + opts = parser.parse_args() + source_entry_name = opts.source_entry_name + target_entry_name = opts.target_entry_name + source = opts.source + target = opts.target + + lines = list[str]() + for line in source.readlines(): + line = line.strip() + if line.startswith('#'): + continue + lines.append(line) + + fragments = dict[str, Fragment]() + cur_fragment: Fragment | None = None + + for m in re.finditer( + r'(?P(?P[-+\w@]+)\|(?P.+?),)|' + r'(?P(?P\w+),)|' + r'(?P(?P\w+)#(?P(0x)?[0-9a-fA-F]+),)|' + r'(?P(?P\w+)=(?P(.+?)),)', + ''.join(lines)): + + if m.group('name') is not None: + name = m.group('entry_name') + description = m.group('entry_desc') + + assert name not in fragments + fragments[name] = Fragment(name, description) + cur_fragment = fragments[name] + + elif m.group('bool_cap') is not None: + name = m.group('bool_name') + assert cur_fragment is not None + cur_fragment.add_capability(BoolCapability(name)) + + elif m.group('int_cap') is not None: + name = m.group('int_name') + int_value = int(m.group('int_val'), 0) + assert cur_fragment is not None + cur_fragment.add_capability(IntCapability(name, int_value)) + + elif m.group('str_cap') is not None: + name = m.group('str_name') + str_value = m.group('str_val') + assert cur_fragment is not None + cur_fragment.add_capability(StringCapability(name, str_value)) + + else: + assert False + + # Expand ‘use’ capabilities + for frag in fragments.values(): + for cap in frag.caps.values(): + if cap.name == 'use': + assert isinstance(cap, StringCapability) + assert isinstance(cap.value, str) + + use_frag = fragments[cap.value] + for use_cap in use_frag.caps.values(): + frag.add_capability(use_cap) + + + frag.del_capability(cap.name) + break + + entry = fragments[source_entry_name] + + try: + entry.del_capability('RGB') + except KeyError: + pass + + entry.add_capability(IntCapability('Co', 256)) + entry.add_capability(StringCapability('TN', target_entry_name)) + entry.add_capability(StringCapability('name', target_entry_name)) + entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel + entry.add_capability(StringCapability('query-os-name', os.uname().sysname)) + + terminfo_parts = list[str]() + for cap in sorted(entry.caps.values()): + name = cap.name + value = str(cap.value) + + # Escape ‘“‘ + name = name.replace('"', '\"') + value = value.replace('"', '\"') + + terminfo_parts.append(name) + if isinstance(cap, BoolCapability): + terminfo_parts.append('') + else: + terminfo_parts.append(value) + + terminfo = '\\0" "'.join(terminfo_parts) + + target.write('#pragma once\n') + target.write('\n') + target.write(f'static const char terminfo_capabilities[] = "{terminfo}";') + target.write('\n') + + +if __name__ == '__main__': + main() diff --git a/scripts/generate-emoji-variation-sequences.py b/scripts/generate-emoji-variation-sequences.py new file mode 100644 index 0000000..57e881c --- /dev/null +++ b/scripts/generate-emoji-variation-sequences.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +import argparse + + +class Codepoint: + def __init__(self, start: int, end: None | int = None): + self.start = start + self.end = start if end is None else end + self.vs15 = False + self.vs16 = False + + def __repr__(self) -> str: + return f'{self.start:x}-{self.end:x}, vs15={self.vs15}, vs16={self.vs16}' + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('input', type=argparse.FileType('r')) + parser.add_argument('output', type=argparse.FileType('w')) + opts = parser.parse_args() + + codepoints: dict[int, Codepoint] = {} + + for line in opts.input: + line = line.rstrip() + if not line: + continue + if line[0] == '#': + continue + + # Example: "0023 FE0E ; text style; # (1.1) NUMBER SIGN" + cps, _ = line.split(';', maxsplit=1) # cps = "0023 FE0F " + cps = cps.strip().split(' ') # cps = ["0023", "FE0F"] + + if len(cps) != 2: + raise NotImplementedError(f'emoji variation sequences with more than one base codepoint: {cps}') + + cp, vs = cps # cp = "0023", vs = "FE0F" + cp = int(cp, 16) # cp = 0x23 + vs = int(vs, 16) # vs = 0xfe0f + + assert vs in [0xfe0e, 0xfe0f] + + if cp not in codepoints: + codepoints[cp] = Codepoint(cp) + + assert codepoints[cp].start == cp + + if vs == 0xfe0e: + codepoints[cp].vs15 = True + else: + codepoints[cp].vs16 = True + + sorted_list = sorted(codepoints.values(), key=lambda cp: cp.start) + + compacted: list[Codepoint] = [] + for i, cp in enumerate(sorted_list): + assert cp.end == cp.start + + if i == 0: + compacted.append(cp) + continue + + last_cp = compacted[-1] + if last_cp.end == cp.start - 1 and last_cp.vs15 == cp.vs15 and last_cp.vs16 == cp.vs16: + compacted[-1].end = cp.start + else: + compacted.append(cp) + + opts.output.write('#pragma once\n') + opts.output.write('#include \n') + opts.output.write('#include \n') + opts.output.write('\n') + opts.output.write('struct emoji_vs {\n') + opts.output.write(' uint32_t start:21;\n') + opts.output.write(' uint32_t end:21;\n') + opts.output.write(' bool vs15:1;\n') + opts.output.write(' bool vs16:1;\n') + opts.output.write('} __attribute__((packed));\n') + opts.output.write('_Static_assert(sizeof(struct emoji_vs) == 6, "unexpected struct size");\n') + opts.output.write('\n') + opts.output.write('#if defined(FOOT_GRAPHEME_CLUSTERING)\n') + opts.output.write('\n') + + opts.output.write(f'static const struct emoji_vs emoji_vs[{len(compacted)}] = {{\n') + + for cp in compacted: + opts.output.write(' {\n') + opts.output.write(f' .start = 0x{cp.start:X},\n') + opts.output.write(f' .end = 0x{cp.end:x},\n') + opts.output.write(f' .vs15 = {"true" if cp.vs15 else "false"},\n') + opts.output.write(f' .vs16 = {"true" if cp.vs16 else "false"},\n') + opts.output.write(' },\n') + + opts.output.write('};\n') + opts.output.write('\n') + opts.output.write('#endif /* FOOT_GRAPHEME_CLUSTERING */\n') + + +if __name__ == '__main__': + main() diff --git a/scripts/srgb.py b/scripts/srgb.py new file mode 100755 index 0000000..a6aa0f4 --- /dev/null +++ b/scripts/srgb.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +import argparse +import math + + +# Note: we use a pure gamma 2.2 function, rather than the piece-wise +# sRGB transfer function, since that is what all compositors do. + +def srgb_to_linear(f: float) -> float: + assert(f >= 0 and f <= 1.0) + return math.pow(f, 2.2) + + +def linear_to_srgb(f: float) -> float: + return math.pow(f, 1 / 2.2) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('c_output', type=argparse.FileType('w')) + parser.add_argument('h_output', type=argparse.FileType('w')) + opts = parser.parse_args() + + linear_table: list[int] = [] + + for i in range(256): + linear_table.append(int(srgb_to_linear(float(i) / 255) * 65535 + 0.5)) + + + opts.h_output.write("#pragma once\n") + opts.h_output.write("#include \n") + opts.h_output.write("\n") + opts.h_output.write('/* 8-bit input, 16-bit output */\n') + opts.h_output.write("extern const uint16_t srgb_decode_8_to_16_table[256];") + + opts.h_output.write('\n') + opts.h_output.write('static inline uint16_t\n') + opts.h_output.write('srgb_decode_8_to_16(uint8_t v)\n') + opts.h_output.write('{\n') + opts.h_output.write(' return srgb_decode_8_to_16_table[v];\n') + opts.h_output.write('}\n') + + opts.h_output.write('\n') + opts.h_output.write('/* 8-bit input, 8-bit output */\n') + opts.h_output.write("extern const uint8_t srgb_decode_8_to_8_table[256];\n") + + opts.h_output.write('\n') + opts.h_output.write('static inline uint8_t\n') + opts.h_output.write('srgb_decode_8_to_8(uint8_t v)\n') + opts.h_output.write('{\n') + opts.h_output.write(' return srgb_decode_8_to_8_table[v];\n') + opts.h_output.write('}\n') + + opts.c_output.write('#include "srgb.h"\n') + opts.c_output.write('\n') + + opts.c_output.write("const uint16_t srgb_decode_8_to_16_table[256] = {\n") + for i in range(256): + opts.c_output.write(f' {linear_table[i]},\n') + opts.c_output.write('};\n') + + opts.c_output.write("const uint8_t srgb_decode_8_to_8_table[256] = {\n") + for i in range(256): + opts.c_output.write(f' {linear_table[i] >> 8},\n') + opts.c_output.write('};\n') + + +if __name__ == '__main__': + main() diff --git a/search.c b/search.c new file mode 100644 index 0000000..0fa2756 --- /dev/null +++ b/search.c @@ -0,0 +1,2077 @@ +#include "search.h" + +#include +#include + +#include +#include + +#define LOG_MODULE "search" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" +#include "commands.h" +#include "config.h" +#include "extract.h" +#include "grid.h" +#include "input.h" +#include "key-binding.h" +#include "misc.h" +#include "quirks.h" +#include "render.h" +#include "selection.h" +#include "shm.h" +#include "unicode-mode.h" +#include "util.h" +#include "xmalloc.h" + +/* Hard cap on counting matches, to keep the counter cheap on huge + * scrollbacks with very generic queries. */ +#define SEARCH_COUNT_CAP 9999 + +/* + * Ensures a "new" viewport doesn't contain any unallocated rows. + * + * This is done by first checking if the *first* row is NULL. If so, + * we move the viewport *forward*, until the first row is non-NULL. At + * this point, the entire viewport should be allocated rows only. + * + * If the first row already was non-NULL, we instead check the *last* + * row, and if it is NULL, we move the viewport *backward* until the + * last row is non-NULL. + */ +static int +ensure_view_is_allocated(struct terminal *term, int new_view) +{ + struct grid *grid = term->grid; + int view_end = (new_view + term->rows - 1) & (grid->num_rows - 1); + + if (grid->rows[new_view] == NULL) { + while (grid->rows[new_view] == NULL) + new_view = (new_view + 1) & (grid->num_rows - 1); + } + + else if (grid->rows[view_end] == NULL) { + while (grid->rows[view_end] == NULL) { + new_view--; + if (new_view < 0) + new_view += grid->num_rows; + view_end = (new_view + term->rows - 1) & (grid->num_rows - 1); + } + } + +#if defined(_DEBUG) + for (size_t r = 0; r < term->rows; r++) + xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); +#endif + + return new_view; +} + +static bool +search_ensure_size(struct terminal *term, size_t wanted_size) +{ + while (wanted_size >= term->search.sz) { + size_t new_sz = term->search.sz == 0 ? 64 : term->search.sz * 2; + char32_t *new_buf = realloc(term->search.buf, new_sz * sizeof(term->search.buf[0])); + + if (new_buf == NULL) { + LOG_ERRNO("failed to resize search buffer"); + return false; + } + + term->search.buf = new_buf; + term->search.sz = new_sz; + } + + return true; +} + +static bool +has_wrapped_around_left(const struct terminal *term, int abs_row_no) +{ + int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); + return rebased_row == term->grid->num_rows - 1 || term->grid->rows[abs_row_no] == NULL; +} + +static bool +has_wrapped_around_right(const struct terminal *term, int abs_row_no) +{ + int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); + return rebased_row == 0; +} + +static void +search_history_push(struct terminal *term, const char32_t *buf, size_t len) +{ + if (len == 0) + return; + + /* Avoid pushing a duplicate of the most recent entry */ + if (term->search.history_tail != NULL && + term->search.history_tail->len == len && + c32ncmp(term->search.history_tail->buf, buf, len) == 0) + return; + + struct search_history_entry *e = xcalloc(1, sizeof(*e)); + e->buf = xmalloc((len + 1) * sizeof(char32_t)); + memcpy(e->buf, buf, len * sizeof(char32_t)); + e->buf[len] = U'\0'; + e->len = len; + + e->prev = term->search.history_tail; + if (term->search.history_tail != NULL) + term->search.history_tail->next = e; + else + term->search.history_head = e; + term->search.history_tail = e; +} + +static void +search_cancel_keep_selection(struct terminal *term) +{ + struct wl_window *win = term->window; + wayl_win_subsurface_destroy(&win->search); + + if (term->search.len > 0) { + /* Save into history */ + search_history_push(term, term->search.buf, term->search.len); + + free(term->search.last.buf); + term->search.last.buf = term->search.buf; + term->search.last.len = term->search.len; + } else + free(term->search.buf); + + /* Free compiled regex */ + if (term->search.regex_compiled != NULL) { + regfree(term->search.regex_compiled); + free(term->search.regex_compiled); + term->search.regex_compiled = NULL; + } + term->search.regex_valid = false; + + term->search.buf = NULL; + term->search.len = term->search.sz = 0; + + term->search.cursor = 0; + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + term->search.total_count = 0; + term->search.current_idx = 0; + term->search.wrapped = false; + term->search.history_pos = NULL; + term->is_searching = false; + term->render.search_glyph_offset = 0; + + /* Reset IME state */ + if (term_ime_is_enabled(term)) { + term_ime_disable(term); + term_ime_enable(term); + } + + term_xcursor_update(term); + render_refresh(term); +} + +void +search_begin(struct terminal *term) +{ + LOG_DBG("search: begin"); + + search_cancel_keep_selection(term); + selection_cancel(term); + + /* Reset IME state */ + if (term_ime_is_enabled(term)) { + term_ime_disable(term); + term_ime_enable(term); + } + + /* On-demand instantiate wayland surface */ + bool ret = wayl_win_subsurface_new( + term->window, &term->window->search, false); + xassert(ret); + + const struct grid *grid = term->grid; + term->search.original_view = grid->view; + term->search.view_followed_offset = grid->view == grid->offset; + term->is_searching = true; + + term->search.len = 0; + term->search.sz = 64; + term->search.buf = xmalloc(term->search.sz * sizeof(term->search.buf[0])); + term->search.buf[0] = U'\0'; + + term_xcursor_update(term); + render_refresh_search(term); +} + +void +search_cancel(struct terminal *term) +{ + if (!term->is_searching) + return; + + search_cancel_keep_selection(term); + selection_cancel(term); +} + +void +search_selection_cancelled(struct terminal *term) +{ + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + render_refresh_search(term); +} + +static void +search_update_selection(struct terminal *term, const struct range *match) +{ + struct grid *grid = term->grid; + int start_row = match->start.row; + int start_col = match->start.col; + int end_row = match->end.row; + int end_col = match->end.col; + + xassert(start_row >= 0); + xassert(start_row < grid->num_rows); + + bool move_viewport = true; + + int view_end = (grid->view + term->rows - 1) & (grid->num_rows - 1); + if (view_end >= grid->view) { + /* Viewport does *not* wrap around */ + if (start_row >= grid->view && end_row <= view_end) + move_viewport = false; + } else { + /* Viewport wraps */ + if (start_row >= grid->view || end_row <= view_end) + move_viewport = false; + } + + if (move_viewport) { + int rebased_new_view = grid_row_abs_to_sb(grid, term->rows, start_row); + + rebased_new_view -= term->rows / 2; + rebased_new_view = + min(max(rebased_new_view, 0), grid->num_rows - term->rows); + + const int old_view = grid->view; + int new_view = grid_row_sb_to_abs(grid, term->rows, rebased_new_view); + + /* Scrollback may not be completely filled yet */ + { + const int mask = grid->num_rows - 1; + while (grid->rows[new_view] == NULL) + new_view = (new_view + 1) & mask; + } + +#if defined(_DEBUG) + /* Verify all to-be-visible rows have been allocated */ + for (int r = 0; r < term->rows; r++) + xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); +#endif + +#if defined(_DEBUG) + { + int rel_start_row = grid_row_abs_to_sb(grid, term->rows, start_row); + int rel_view = grid_row_abs_to_sb(grid, term->rows, new_view); + xassert(rel_view <= rel_start_row); + xassert(rel_start_row < rel_view + term->rows); + } +#endif + + /* Update view */ + grid->view = new_view; + if (new_view != old_view) + term_damage_view(term); + } + + if (start_row != term->search.match.row || + start_col != term->search.match.col || + + /* Pointer leave events trigger selection_finalize() :/ */ + !term->selection.ongoing) + { + int selection_row = start_row - grid->view + grid->num_rows; + selection_row &= grid->num_rows - 1; + + selection_start( + term, start_col, selection_row, SELECTION_CHAR_WISE, false); + + term->search.match.row = start_row; + term->search.match.col = start_col; + } + + /* Update selection endpoint */ + { + int selection_row = end_row - grid->view + grid->num_rows; + selection_row &= grid->num_rows - 1; + selection_update(term, end_col, selection_row); + } +} + +static bool +search_is_case_sensitive(const struct terminal *term) +{ + switch (term->search.case_mode) { + case SEARCH_CASE_SENSITIVE: return true; + case SEARCH_CASE_INSENSITIVE: return false; + case SEARCH_CASE_SMART: return hasc32upper(term->search.buf); + } + return true; +} + +static bool +search_cell_is_word(const struct terminal *term, const struct cell *cell) +{ + char32_t base = cell->wc; + if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) { + const struct composed *c = + composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); + base = c->chars[0]; + } + if (base == 0 || base >= CELL_SPACER) + return false; + return isword(base, false, term->conf->word_delimiters); +} + +/* True if the cell *immediately before* (col-1, row), wrapping back a + * row if needed, is a word character. Returns false at scrollback + * boundaries (treat boundary as non-word). */ +static bool +search_neighbor_is_word(const struct terminal *term, int row, int col, + bool look_right) +{ + const struct grid *grid = term->grid; + int r = row, c = col; + + if (look_right) { + c++; + if (c >= term->cols) { + r = (r + 1) & (grid->num_rows - 1); + c = 0; + if (has_wrapped_around_right(term, r)) + return false; + } + } else { + c--; + if (c < 0) { + r = (r - 1 + grid->num_rows) & (grid->num_rows - 1); + c = term->cols - 1; + if (has_wrapped_around_left(term, r)) + return false; + } + } + + const struct row *gr = grid->rows[r]; + if (gr == NULL) + return false; + return search_cell_is_word(term, &gr->cells[c]); +} + +/* Extract a row's printable cells into a UTF-8 buffer plus a + * byte-offset → cell-column map. The map has one entry per UTF-8 + * code unit (so byte_to_col[i] is the column the byte at offset i + * came from). The trailing NUL has the column == term->cols. */ +struct row_text { + char *utf8; + size_t len; + int *byte_to_col; +}; + +static bool +extract_row_text(const struct terminal *term, const struct row *row, + struct row_text *out) +{ + out->utf8 = NULL; + out->len = 0; + out->byte_to_col = NULL; + + if (row == NULL) + return false; + + /* Worst case: every cell becomes 4 UTF-8 bytes for the base char, + * plus combining chars (cap to a few). */ + size_t cap = (size_t)term->cols * 8 + 1; + char *buf = xmalloc(cap); + int *map = xmalloc(cap * sizeof(int)); + size_t pos = 0; + + mbstate_t ps = {0}; + + for (int col = 0; col < term->cols; col++) { + const struct cell *cell = &row->cells[col]; + char32_t base = cell->wc; + + if (base >= CELL_SPACER) + continue; /* right-half of wide char */ + + if (base == 0) + base = U' '; + + const struct composed *composed = NULL; + if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) { + composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); + base = composed->chars[0]; + } + + size_t encoded = c32rtomb(&buf[pos], base, &ps); + if (encoded == (size_t)-1) { + buf[pos] = '?'; + encoded = 1; + memset(&ps, 0, sizeof(ps)); + } + for (size_t i = 0; i < encoded; i++) + map[pos + i] = col; + pos += encoded; + + if (composed != NULL) { + for (size_t j = 1; j < composed->count; j++) { + size_t e = c32rtomb(&buf[pos], composed->chars[j], &ps); + if (e == (size_t)-1) { + buf[pos] = '?'; + e = 1; + memset(&ps, 0, sizeof(ps)); + } + for (size_t i = 0; i < e; i++) + map[pos + i] = col; + pos += e; + } + } + } + + buf[pos] = '\0'; + map[pos] = term->cols; + + out->utf8 = buf; + out->len = pos; + out->byte_to_col = map; + return true; +} + +static void +free_row_text(struct row_text *rt) +{ + free(rt->utf8); + free(rt->byte_to_col); + rt->utf8 = NULL; + rt->byte_to_col = NULL; + rt->len = 0; +} + +static bool +search_compile_regex(struct terminal *term) +{ + /* Free any previously compiled regex */ + if (term->search.regex_compiled != NULL) { + regfree(term->search.regex_compiled); + free(term->search.regex_compiled); + term->search.regex_compiled = NULL; + } + term->search.regex_valid = false; + + if (!term->search.regex || term->search.len == 0) + return false; + + /* Convert search buffer to UTF-8 */ + char pattern[term->search.len * 4 + 1]; + mbstate_t ps = {0}; + size_t out = 0; + for (size_t i = 0; i < term->search.len; i++) { + size_t e = c32rtomb(&pattern[out], term->search.buf[i], &ps); + if (e == (size_t)-1) { + pattern[out++] = '?'; + memset(&ps, 0, sizeof(ps)); + } else { + out += e; + } + } + pattern[out] = '\0'; + + regex_t *re = xmalloc(sizeof(*re)); + int flags = REG_EXTENDED; + if (!search_is_case_sensitive(term)) + flags |= REG_ICASE; + + if (regcomp(re, pattern, flags) != 0) { + free(re); + return false; + } + + term->search.regex_compiled = re; + term->search.regex_valid = true; + return true; +} + +/* Find one regex match on a single row. Returns true if found and + * fills [start_col, end_col]. */ +static bool +regex_find_in_row(const struct terminal *term, const struct row *row, + int min_col, int max_col, int *out_start, int *out_end) +{ + if (!term->search.regex_valid || row == NULL) + return false; + + struct row_text rt; + if (!extract_row_text(term, row, &rt)) + return false; + + bool found = false; + regmatch_t m; + size_t scan_from = 0; + + /* Find matches; honor min_col by skipping ones that end before it, + * and max_col by stopping after them. Pick the *first* match whose + * starting column is in [min_col, max_col]. */ + while (scan_from <= rt.len) { + if (regexec(term->search.regex_compiled, rt.utf8 + scan_from, 1, &m, + scan_from > 0 ? REG_NOTBOL : 0) != 0) + break; + if (m.rm_so == m.rm_eo) { + /* Zero-width — advance one byte to avoid infinite loop */ + scan_from++; + continue; + } + + int s_col = rt.byte_to_col[scan_from + m.rm_so]; + int e_col = rt.byte_to_col[scan_from + m.rm_eo - 1]; + + if (s_col >= min_col && s_col <= max_col) { + if (term->search.whole_word) { + if ((s_col > 0 && search_cell_is_word(term, &row->cells[s_col - 1])) || + (e_col + 1 < term->cols && search_cell_is_word(term, &row->cells[e_col + 1]))) + { + scan_from += m.rm_eo; + continue; + } + } + *out_start = s_col; + *out_end = e_col; + found = true; + break; + } + + scan_from += m.rm_eo; + } + + free_row_text(&rt); + return found; +} + +/* Walk rows in the requested direction, returning the first regex + * match found within the [start, end] range. */ +static bool +regex_find_next(const struct terminal *term, enum search_direction direction, + struct coord abs_start, struct coord abs_end, + struct range *match) +{ + const struct grid *grid = term->grid; + const bool backward = direction != SEARCH_FORWARD; + + int row_no = abs_start.row; + int from_col = abs_start.col; + int to_col = (row_no == abs_end.row) ? abs_end.col : + (backward ? 0 : term->cols - 1); + + while (true) { + const struct row *row = grid->rows[row_no]; + if (row != NULL) { + int min_col = backward ? min(from_col, to_col) : from_col; + int max_col = backward ? max(from_col, to_col) : to_col; + + int s, e; + if (regex_find_in_row(term, row, min_col, max_col, &s, &e)) { + match->start = (struct coord){s, row_no}; + match->end = (struct coord){e, row_no}; + return true; + } + } + + if (row_no == abs_end.row) + break; + + if (backward) { + row_no = (row_no - 1 + grid->num_rows) & (grid->num_rows - 1); + from_col = term->cols - 1; + to_col = (row_no == abs_end.row) ? abs_end.col : 0; + } else { + row_no = (row_no + 1) & (grid->num_rows - 1); + from_col = 0; + to_col = (row_no == abs_end.row) ? abs_end.col : term->cols - 1; + } + } + + return false; +} + +static ssize_t +matches_cell(const struct terminal *term, const struct cell *cell, size_t search_ofs) +{ + assert(search_ofs < term->search.len); + + char32_t base = cell->wc; + const struct composed *composed = NULL; + + if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) + { + composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); + base = composed->chars[0]; + } + + if (composed == NULL && base == 0 && term->search.buf[search_ofs] == U' ') + return 1; + + if (search_is_case_sensitive(term)) { + if (c32ncmp(&base, &term->search.buf[search_ofs], 1) != 0) + return -1; + } else { + if (c32ncasecmp(&base, &term->search.buf[search_ofs], 1) != 0) + return -1; + } + + if (composed != NULL) { + if (search_ofs + composed->count > term->search.len) + return -1; + + for (size_t j = 1; j < composed->count; j++) { + if (composed->chars[j] != term->search.buf[search_ofs + j]) + return -1; + } + } + + return composed != NULL ? composed->count : 1; +} + +static bool +find_next(struct terminal *term, enum search_direction direction, + struct coord abs_start, struct coord abs_end, struct range *match) +{ +#define ROW_DEC(_r) ((_r) = ((_r) - 1 + grid->num_rows) & (grid->num_rows - 1)) +#define ROW_INC(_r) ((_r) = ((_r) + 1) & (grid->num_rows - 1)) + + if (term->search.regex && term->search.regex_valid) + return regex_find_next(term, direction, abs_start, abs_end, match); + + struct grid *grid = term->grid; + const bool backward = direction != SEARCH_FORWARD; + + LOG_DBG("%s: start: %dx%d, end: %dx%d", backward ? "backward" : "forward", + abs_start.row, abs_start.col, abs_end.row, abs_end.col); + + xassert(abs_start.row >= 0); + xassert(abs_start.row < grid->num_rows); + xassert(abs_start.col >= 0); + xassert(abs_start.col < term->cols); + + xassert(abs_end.row >= 0); + xassert(abs_end.row < grid->num_rows); + xassert(abs_end.col >= 0); + xassert(abs_end.col < term->cols); + + for (int match_start_row = abs_start.row, match_start_col = abs_start.col; + ; + backward ? ROW_DEC(match_start_row) : ROW_INC(match_start_row)) { + + const struct row *row = grid->rows[match_start_row]; + if (row == NULL) { + if (match_start_row == abs_end.row) + break; + continue; + } + + for (; + backward ? match_start_col >= 0 : match_start_col < term->cols; + backward ? match_start_col-- : match_start_col++) + { + if (matches_cell(term, &row->cells[match_start_col], 0) < 0) { + if (match_start_row == abs_end.row && + match_start_col == abs_end.col) + { + break; + } + continue; + } + + /* + * Got a match on the first letter. Now we'll see if the + * rest of the search buffer matches. + */ + + LOG_DBG("search: initial match at row=%d, col=%d", + match_start_row, match_start_col); + + int match_end_row = match_start_row; + int match_end_col = match_start_col; + const struct row *match_row = row; + size_t match_len = 0; + + for (size_t i = 0; i < term->search.len;) { + if (match_end_col >= term->cols) { + ROW_INC(match_end_row); + match_end_col = 0; + + match_row = grid->rows[match_end_row]; + if (match_row == NULL) + break; + } + + if (match_row->cells[match_end_col].wc >= CELL_SPACER) { + match_end_col++; + continue; + } + + ssize_t additional_chars = matches_cell( + term, &match_row->cells[match_end_col], i); + if (additional_chars < 0) + break; + + i += additional_chars; + match_len += additional_chars; + match_end_col++; + + while (match_end_col < term->cols && + match_row->cells[match_end_col].wc > CELL_SPACER) + { + match_end_col++; + } + } + + if (match_len != term->search.len) { + /* Didn't match (completely) */ + + if (match_start_row == abs_end.row && + match_start_col == abs_end.col) + { + break; + } + + continue; + } + + if (term->search.whole_word) { + /* Reject if neighbour cells are word-chars */ + if (search_neighbor_is_word( + term, match_start_row, match_start_col, false) || + search_neighbor_is_word( + term, match_end_row, match_end_col - 1, true)) + { + if (match_start_row == abs_end.row && + match_start_col == abs_end.col) + { + break; + } + continue; + } + } + + *match = (struct range){ + .start = {match_start_col, match_start_row}, + .end = {match_end_col - 1, match_end_row}, + }; + + return true; + } + + if (match_start_row == abs_end.row && match_start_col == abs_end.col) + break; + + match_start_col = backward ? term->cols - 1 : 0; + } + + return false; +} + +/* Count total matches across the whole grid, capped at + * SEARCH_COUNT_CAP. Returns the cap if we hit it. */ +static size_t +search_count_all(struct terminal *term) +{ + if (term->search.len == 0) + return 0; + if (term->search.regex && !term->search.regex_valid) + return 0; + + struct grid *grid = term->grid; + + /* Start at the very top of the scrollback (oldest row) */ + int oldest = (grid->offset + term->rows) & (grid->num_rows - 1); + + struct coord pos = { 0, oldest }; + struct coord end = { term->cols - 1, oldest }; + /* end.row should be the row *before* oldest, i.e. the newest */ + end.row = (oldest - 1 + grid->num_rows) & (grid->num_rows - 1); + + size_t count = 0; + while (count < SEARCH_COUNT_CAP) { + struct range m; + if (!find_next(term, SEARCH_FORWARD, pos, end, &m)) + break; + count++; + + /* Advance one cell past the match start */ + pos.col = m.start.col + 1; + pos.row = m.start.row; + if (pos.col >= term->cols) { + pos.col = 0; + pos.row = (pos.row + 1) & (grid->num_rows - 1); + if (pos.row == oldest) + break; /* wrapped */ + } + + if (pos.row == end.row && pos.col > end.col) + break; + } + + return count; +} + +/* Recompute current_idx by counting matches from the top of the + * scrollback up to (and including) the current match position. */ +static size_t +search_compute_current_idx(struct terminal *term) +{ + if (term->search.match_len == 0 || term->search.len == 0) + return 0; + if (term->search.regex && !term->search.regex_valid) + return 0; + + struct grid *grid = term->grid; + int oldest = (grid->offset + term->rows) & (grid->num_rows - 1); + + /* Sentinel "newest cell" - we'll abort when we pass our own match */ + struct coord newest = { term->cols - 1, + (oldest - 1 + grid->num_rows) & (grid->num_rows - 1) }; + + struct coord pos = { 0, oldest }; + + size_t idx = 0; + while (idx < SEARCH_COUNT_CAP) { + struct range m; + if (!find_next(term, SEARCH_FORWARD, pos, newest, &m)) + break; + idx++; + + if (m.start.row == term->search.match.row && + m.start.col == term->search.match.col) + return idx; + + pos.col = m.start.col + 1; + pos.row = m.start.row; + if (pos.col >= term->cols) { + pos.col = 0; + pos.row = (pos.row + 1) & (grid->num_rows - 1); + if (pos.row == oldest) + break; + } + } + + return 0; +} + +static void +search_find_next(struct terminal *term, enum search_direction direction) +{ + struct grid *grid = term->grid; + + /* Recompile regex if active (cheap when len==0; no-op otherwise) */ + if (term->search.regex) + search_compile_regex(term); + + if (term->search.len == 0) { + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + term->search.total_count = 0; + term->search.current_idx = 0; + term->search.wrapped = false; + selection_cancel(term); + return; + } + + struct coord start = term->search.match; + size_t len = term->search.match_len; + + xassert((len == 0 && start.row == -1 && start.col == -1) || + (len > 0 && start.row >= 0 && start.col >= 0)); + + if (len == 0) { + /* No previous match, start from the top, or bottom, of the scrollback */ + switch (direction) { + case SEARCH_FORWARD: + start.row = grid_row_absolute_in_view(grid, 0); + start.col = 0; + break; + + case SEARCH_BACKWARD: + case SEARCH_BACKWARD_SAME_POSITION: + start.row = grid_row_absolute_in_view(grid, term->rows - 1); + start.col = term->cols - 1; + break; + } + } else { + /* Continue from last match */ + xassert(start.row >= 0); + xassert(start.col >= 0); + + switch (direction) { + case SEARCH_BACKWARD_SAME_POSITION: + break; + + case SEARCH_BACKWARD: + if (--start.col < 0) { + start.col = term->cols - 1; + start.row += grid->num_rows - 1; + start.row &= grid->num_rows - 1; + } + break; + + case SEARCH_FORWARD: + if (++start.col >= term->cols) { + start.col = 0; + start.row++; + start.row &= grid->num_rows - 1; + } + break; + } + + xassert(start.row >= 0); + xassert(start.row < grid->num_rows); + xassert(start.col >= 0); + xassert(start.col < term->cols); + } + + LOG_DBG( + "update: %s: starting at row=%d col=%d " + "(offset = %d, view = %d)", + direction != SEARCH_FORWARD ? "backward" : "forward", + start.row, start.col, + grid->offset, grid->view); + + struct coord end = start; + switch (direction) { + case SEARCH_FORWARD: + /* Search forward, until we reach the cell *before* current start */ + if (--end.col < 0) { + end.col = term->cols - 1; + end.row += grid->num_rows - 1; + end.row &= grid->num_rows - 1; + } + break; + + case SEARCH_BACKWARD: + case SEARCH_BACKWARD_SAME_POSITION: + /* Search backwards, until we reach the cell *after* current start */ + if (++end.col >= term->cols) { + end.col = 0; + end.row++; + end.row &= grid->num_rows - 1; + } + break; + } + + /* Remember previous match position for wrap detection */ + const struct coord prev_match = term->search.match; + const size_t prev_match_len = term->search.match_len; + + struct range match; + bool found = find_next(term, direction, start, end, &match); + + term->search.wrapped = false; + + if (found) { + LOG_DBG("primary match found at %dx%d", + match.start.row, match.start.col); + + /* Detect wrap: if we had a prior match and the new match is in + * the "wrong" direction relative to it, we wrapped. */ + if (prev_match_len > 0 && + direction != SEARCH_BACKWARD_SAME_POSITION) + { + int oldest = (grid->offset + term->rows) & (grid->num_rows - 1); + int prev_rebased = (prev_match.row - oldest + grid->num_rows) + & (grid->num_rows - 1); + int new_rebased = (match.start.row - oldest + grid->num_rows) + & (grid->num_rows - 1); + + if (direction == SEARCH_FORWARD) { + if (new_rebased < prev_rebased || + (new_rebased == prev_rebased && + match.start.col <= prev_match.col)) + term->search.wrapped = true; + } else { + if (new_rebased > prev_rebased || + (new_rebased == prev_rebased && + match.start.col >= prev_match.col)) + term->search.wrapped = true; + } + } + + search_update_selection(term, &match); + term->search.match = match.start; + term->search.match_len = term->search.len; + } else { + LOG_DBG("no match"); + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + selection_cancel(term); + } + + /* Refresh counter */ + term->search.total_count = search_count_all(term); + term->search.current_idx = search_compute_current_idx(term); +#undef ROW_DEC +} + +struct search_match_iterator +search_matches_new_iter(struct terminal *term) +{ + return (struct search_match_iterator){ + .term = term, + .start = {0, 0}, + }; +} + +struct range +search_matches_next(struct search_match_iterator *iter) +{ + struct terminal *term = iter->term; + struct grid *grid = term->grid; + + if (term->search.match_len == 0) + goto no_match; + + if (iter->start.row >= term->rows) + goto no_match; + + xassert(iter->start.row >= 0); + xassert(iter->start.row < term->rows); + xassert(iter->start.col >= 0); + xassert(iter->start.col < term->cols); + + struct coord abs_start = iter->start; + abs_start.row = grid_row_absolute_in_view(grid, abs_start.row); + + struct coord abs_end = { + term->cols - 1, + grid_row_absolute_in_view(grid, term->rows - 1)}; + + /* BUG: matches *starting* outside the view, but ending *inside*, aren't matched */ + struct range match; + bool found = find_next(term, SEARCH_FORWARD, abs_start, abs_end, &match); + if (!found) + goto no_match; + + LOG_DBG("match at (absolute coordinates) %dx%d-%dx%d", + match.start.row, match.start.col, + match.end.row, match.end.col); + + /* Convert absolute row numbers back to view relative */ + match.start.row = match.start.row - grid->view + grid->num_rows; + match.start.row &= grid->num_rows - 1; + match.end.row = match.end.row - grid->view + grid->num_rows; + match.end.row &= grid->num_rows - 1; + + LOG_DBG("match at (view-local coordinates) %dx%d-%dx%d, view=%d", + match.start.row, match.start.col, + match.end.row, match.end.col, grid->view); + + /* Assert match end comes *after* the match start */ + xassert(match.end.row > match.start.row || + (match.end.row == match.start.row && + match.end.col >= match.start.col)); + + /* Assert the match starts at, or after, the iterator position */ + xassert(match.start.row > iter->start.row || + (match.start.row == iter->start.row && + match.start.col >= iter->start.col)); + + /* Continue at next column, next time */ + iter->start.row = match.start.row; + iter->start.col = match.start.col + 1; + + if (iter->start.col >= term->cols) { + iter->start.col = 0; + iter->start.row++; /* Overflow is caught in next iteration */ + } + + xassert(iter->start.row >= 0); + xassert(iter->start.row <= term->rows); + xassert(iter->start.col >= 0); + xassert(iter->start.col < term->cols); + return match; + +no_match: + iter->start.row = -1; + iter->start.col = -1; + return (struct range){{-1, -1}, {-1, -1}}; +} + +static void +add_wchars(struct terminal *term, char32_t *src, size_t count) +{ + /* Strip non-printable characters */ + for (size_t i = 0, j = 0, orig_count = count; i < orig_count; i++) { + if (isc32print(src[i])) + src[j++] = src[i]; + else + count--; + } + + if (!search_ensure_size(term, term->search.len + count)) + return; + + xassert(term->search.len + count < term->search.sz); + + memmove(&term->search.buf[term->search.cursor + count], + &term->search.buf[term->search.cursor], + (term->search.len - term->search.cursor) * sizeof(char32_t)); + + memcpy(&term->search.buf[term->search.cursor], src, count * sizeof(char32_t)); + + term->search.len += count; + term->search.cursor += count; + term->search.buf[term->search.len] = U'\0'; +} + +void +search_add_chars(struct terminal *term, const char *src, size_t count) +{ + size_t chars = mbsntoc32(NULL, src, count, 0); + if (chars == (size_t)-1) { + LOG_ERRNO("failed to convert %.*s to Unicode", (int)count, src); + return; + } + + char32_t c32s[chars + 1]; + mbsntoc32(c32s, src, count, chars); + add_wchars(term, c32s, chars); +} + +enum extend_direction {SEARCH_EXTEND_LEFT, SEARCH_EXTEND_RIGHT}; + +static bool +coord_advance_left(const struct terminal *term, struct coord *pos, + const struct row **row) +{ + const struct grid *grid = term->grid; + struct coord new_pos = *pos; + + if (--new_pos.col < 0) { + new_pos.row = (new_pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); + new_pos.col = term->cols - 1; + + if (has_wrapped_around_left(term, new_pos.row)) + return false; + + if (row != NULL) + *row = grid->rows[new_pos.row]; + } + + *pos = new_pos; + return true; +} + +static bool +coord_advance_right(const struct terminal *term, struct coord *pos, + const struct row **row) +{ + const struct grid *grid = term->grid; + struct coord new_pos = *pos; + + if (++new_pos.col >= term->cols) { + new_pos.row = (new_pos.row + 1) & (grid->num_rows - 1); + new_pos.col = 0; + + if (has_wrapped_around_right(term, new_pos.row)) + return false; + + if (row != NULL) + *row = grid->rows[new_pos.row]; + } + + *pos = new_pos; + return true; +} + +static bool +search_extend_find_char(const struct terminal *term, struct coord *target, + enum extend_direction direction) +{ + if (term->search.match_len == 0) + return false; + + struct coord pos = direction == SEARCH_EXTEND_LEFT + ? selection_get_start(term) : selection_get_end(term); + xassert(pos.row >= 0); + xassert(pos.row < term->grid->num_rows); + + *target = pos; + + const struct row *row = term->grid->rows[pos.row]; + + while (true) { + switch (direction) { + case SEARCH_EXTEND_LEFT: + if (!coord_advance_left(term, &pos, &row)) + return false; + break; + + case SEARCH_EXTEND_RIGHT: + if (!coord_advance_right(term, &pos, &row)) + return false; + break; + } + + const char32_t wc = row->cells[pos.col].wc; + + if (wc >= CELL_SPACER || wc == U'\0') + continue; + + *target = pos; + return true; + } +} + +static bool +search_extend_find_char_left(const struct terminal *term, struct coord *target) +{ + return search_extend_find_char(term, target, SEARCH_EXTEND_LEFT); +} + +static bool +search_extend_find_char_right(const struct terminal *term, struct coord *target) +{ + return search_extend_find_char(term, target, SEARCH_EXTEND_RIGHT); +} + +static bool +search_extend_find_word(const struct terminal *term, bool spaces_only, + struct coord *target, enum extend_direction direction) +{ + if (term->search.match_len == 0) + return false; + + struct grid *grid = term->grid; + struct coord pos = direction == SEARCH_EXTEND_LEFT + ? selection_get_start(term) + : selection_get_end(term); + + xassert(pos.row >= 0); + xassert(pos.row < grid->num_rows); + + *target = pos; + + /* First character to consider is the *next* character */ + switch (direction) { + case SEARCH_EXTEND_LEFT: + if (!coord_advance_left(term, &pos, NULL)) + return false; + break; + + case SEARCH_EXTEND_RIGHT: + if (!coord_advance_right(term, &pos, NULL)) + return false; + break; + } + + xassert(pos.row >= 0); + xassert(pos.row < grid->num_rows); + xassert(grid->rows[pos.row] != NULL); + + /* Find next word boundary */ + switch (direction) { + case SEARCH_EXTEND_LEFT: + selection_find_word_boundary_left(term, &pos, spaces_only); + break; + + case SEARCH_EXTEND_RIGHT: + selection_find_word_boundary_right(term, &pos, spaces_only, false); + break; + } + + *target = pos; + return true; +} + +static bool +search_extend_find_word_left(const struct terminal *term, bool spaces_only, + struct coord *target) +{ + return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_LEFT); +} + +static bool +search_extend_find_word_right(const struct terminal *term, bool spaces_only, + struct coord *target) +{ + return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_RIGHT); +} + +static bool +search_extend_find_line(const struct terminal *term, struct coord *target, + enum extend_direction direction) +{ + if (term->search.match_len == 0) + return false; + + struct coord pos = direction == SEARCH_EXTEND_LEFT + ? selection_get_start(term) : selection_get_end(term); + + xassert(pos.row >= 0); + xassert(pos.row < term->grid->num_rows); + + *target = pos; + + const struct grid *grid = term->grid; + + switch (direction) { + case SEARCH_EXTEND_LEFT: + pos.row = (pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); + if (has_wrapped_around_left(term, pos.row)) + return false; + break; + + case SEARCH_EXTEND_RIGHT: + pos.row = (pos.row + 1) & (grid->num_rows - 1); + if (has_wrapped_around_right(term, pos.row)) + return false; + break; + } + + *target = pos; + return true; +} + +static bool +search_extend_find_line_up(const struct terminal *term, struct coord *target) +{ + return search_extend_find_line(term, target, SEARCH_EXTEND_LEFT); +} + +static bool +search_extend_find_line_down(const struct terminal *term, struct coord *target) +{ + return search_extend_find_line(term, target, SEARCH_EXTEND_RIGHT); +} + +static void +search_extend_left(struct terminal *term, const struct coord *target) +{ + if (term->search.match_len == 0) + return; + + const struct coord last_coord = selection_get_start(term); + struct coord pos = *target; + const struct row *row = term->grid->rows[pos.row]; + + const bool move_cursor = term->search.cursor != 0; + + struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); + if (ctx == NULL) + return; + + while (pos.col != last_coord.col || pos.row != last_coord.row) { + if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) + break; + if (!coord_advance_right(term, &pos, &row)) + break; + } + + char32_t *new_text; + size_t new_len; + + if (!extract_finish_wide(ctx, &new_text, &new_len)) + return; + + if (!search_ensure_size(term, term->search.len + new_len)) + return; + + memmove(&term->search.buf[new_len], &term->search.buf[0], + term->search.len * sizeof(term->search.buf[0])); + + size_t actually_copied = 0; + for (size_t i = 0; i < new_len; i++) { + if (new_text[i] == U'\n') { + /* extract() adds newlines, which we never match against */ + continue; + } + + term->search.buf[actually_copied++] = new_text[i]; + term->search.len++; + } + + xassert(actually_copied <= new_len); + if (actually_copied < new_len) { + memmove( + &term->search.buf[actually_copied], &term->search.buf[new_len], + (term->search.len - actually_copied) * sizeof(term->search.buf[0])); + } + + term->search.buf[term->search.len] = U'\0'; + free(new_text); + + if (move_cursor) + term->search.cursor += actually_copied; + + struct range match = {.start = *target, .end = selection_get_end(term)}; + search_update_selection(term, &match); + + term->search.match_len = term->search.len; +} + +static void +search_extend_right(struct terminal *term, const struct coord *target) +{ + if (term->search.match_len == 0) + return; + + struct coord pos = selection_get_end(term); + const struct row *row = term->grid->rows[pos.row]; + + const bool move_cursor = term->search.cursor == term->search.len; + + struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); + if (ctx == NULL) + return; + + do { + if (!coord_advance_right(term, &pos, &row)) + break; + if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) + break; + } while (pos.col != target->col || pos.row != target->row); + + char32_t *new_text; + size_t new_len; + + if (!extract_finish_wide(ctx, &new_text, &new_len)) + return; + + if (!search_ensure_size(term, term->search.len + new_len)) + return; + + for (size_t i = 0; i < new_len; i++) { + if (new_text[i] == U'\n') { + /* extract() adds newlines, which we never match against */ + continue; + } + + term->search.buf[term->search.len++] = new_text[i]; + } + + term->search.buf[term->search.len] = U'\0'; + free(new_text); + + if (move_cursor) + term->search.cursor = term->search.len; + + struct range match = {.start = term->search.match, .end = *target}; + search_update_selection(term, &match); + term->search.match_len = term->search.len; +} + +static size_t +distance_next_word(const struct terminal *term) +{ + size_t cursor = term->search.cursor; + + /* First eat non-whitespace. This is the word we're skipping past */ + while (cursor < term->search.len) { + if (isc32space(term->search.buf[cursor++])) + break; + } + + xassert(cursor == term->search.len || isc32space(term->search.buf[cursor - 1])); + + /* Now skip past whitespace, so that we end up at the beginning of + * the next word */ + while (cursor < term->search.len) { + if (!isc32space(term->search.buf[cursor++])) + break; + } + + xassert(cursor == term->search.len || !isc32space(term->search.buf[cursor - 1])); + + if (cursor < term->search.len && !isc32space(term->search.buf[cursor])) + cursor--; + + return cursor - term->search.cursor; +} + +static size_t +distance_prev_word(const struct terminal *term) +{ + int cursor = term->search.cursor; + + /* First, eat whitespace prefix */ + while (cursor > 0) { + if (!isc32space(term->search.buf[--cursor])) + break; + } + + xassert(cursor == 0 || !isc32space(term->search.buf[cursor])); + + /* Now eat non-whitespace. This is the word we're skipping past */ + while (cursor > 0) { + if (isc32space(term->search.buf[--cursor])) + break; + } + + xassert(cursor == 0 || isc32space(term->search.buf[cursor])); + if (cursor > 0 && isc32space(term->search.buf[cursor])) + cursor++; + + return term->search.cursor - cursor; +} + +static void +from_clipboard_cb(char *text, size_t size, void *user) +{ + struct terminal *term = user; + search_add_chars(term, text, size); +} + +static void +from_clipboard_done(void *user) +{ + struct terminal *term = user; + + LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf); + search_find_next(term, SEARCH_BACKWARD_SAME_POSITION); + render_refresh_search(term); +} + +static bool +execute_binding(struct seat *seat, struct terminal *term, + const struct key_binding *binding, uint32_t serial, + bool *update_search_result, enum search_direction *direction, + bool *redraw) +{ + *update_search_result = *redraw = false; + const enum bind_action_search action = binding->action; + + struct grid *grid = term->grid; + + switch (action) { + case BIND_ACTION_SEARCH_NONE: + return false; + + case BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->rows); + return true; + } + return false; + + case BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, 1); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->rows); + return true; + } + return false; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, 1); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_HOME: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_END: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_SEARCH_CANCEL: + if (term->search.view_followed_offset) + grid->view = grid->offset; + else { + grid->view = ensure_view_is_allocated( + term, term->search.original_view); + } + term_damage_view(term); + search_cancel(term); + return true; + + case BIND_ACTION_SEARCH_COMMIT: + selection_finalize(seat, term, serial); + search_cancel_keep_selection(term); + return true; + + case BIND_ACTION_SEARCH_FIND_PREV: + if (term->search.last.buf != NULL && term->search.len == 0) { + add_wchars(term, term->search.last.buf, term->search.last.len); + + free(term->search.last.buf); + term->search.last.buf = NULL; + term->search.last.len = 0; + } + + *direction = SEARCH_BACKWARD; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_FIND_NEXT: + if (term->search.last.buf != NULL && term->search.len == 0) { + add_wchars(term, term->search.last.buf, term->search.last.len); + + free(term->search.last.buf); + term->search.last.buf = NULL; + term->search.last.len = 0; + } + + *direction = SEARCH_FORWARD; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_EDIT_LEFT: + if (term->search.cursor > 0) { + term->search.cursor--; + *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_EDIT_LEFT_WORD: { + size_t diff = distance_prev_word(term); + term->search.cursor -= diff; + xassert(term->search.cursor <= term->search.len); + + if (diff > 0) + *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_EDIT_RIGHT: + if (term->search.cursor < term->search.len) { + term->search.cursor++; + *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_EDIT_RIGHT_WORD: { + size_t diff = distance_next_word(term); + term->search.cursor += diff; + xassert(term->search.cursor <= term->search.len); + + if (diff > 0) + *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_EDIT_HOME: + if (term->search.cursor != 0) { + term->search.cursor = 0; + *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_EDIT_END: + if (term->search.cursor != term->search.len) { + term->search.cursor = term->search.len; + *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_DELETE_PREV: + if (term->search.cursor > 0) { + memmove( + &term->search.buf[term->search.cursor - 1], + &term->search.buf[term->search.cursor], + (term->search.len - term->search.cursor) * sizeof(char32_t)); + term->search.cursor--; + term->search.buf[--term->search.len] = U'\0'; + *update_search_result = *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_DELETE_PREV_WORD: { + size_t diff = distance_prev_word(term); + size_t old_cursor = term->search.cursor; + size_t new_cursor = old_cursor - diff; + + if (diff > 0) { + memmove(&term->search.buf[new_cursor], + &term->search.buf[old_cursor], + (term->search.len - old_cursor) * sizeof(char32_t)); + + term->search.len -= diff; + term->search.cursor = new_cursor; + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_DELETE_NEXT: + if (term->search.cursor < term->search.len) { + memmove( + &term->search.buf[term->search.cursor], + &term->search.buf[term->search.cursor + 1], + (term->search.len - term->search.cursor - 1) * sizeof(char32_t)); + term->search.buf[--term->search.len] = U'\0'; + *update_search_result = *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_DELETE_NEXT_WORD: { + size_t diff = distance_next_word(term); + size_t cursor = term->search.cursor; + + if (diff > 0) { + memmove(&term->search.buf[cursor], + &term->search.buf[cursor + diff], + (term->search.len - (cursor + diff)) * sizeof(char32_t)); + + term->search.len -= diff; + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_DELETE_TO_START: { + if (term->search.cursor > 0) { + memmove(&term->search.buf[0], + &term->search.buf[term->search.cursor], + (term->search.len - term->search.cursor) + * sizeof(char32_t)); + + term->search.len -= term->search.cursor; + term->search.cursor = 0; + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_DELETE_TO_END: { + if (term->search.cursor < term->search.len) { + term->search.buf[term->search.cursor] = '\0'; + term->search.len = term->search.cursor; + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_CHAR: { + struct coord target; + if (search_extend_find_char_right(term, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_WORD: { + struct coord target; + if (search_extend_find_word_right(term, false, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_WORD_WS: { + struct coord target; + if (search_extend_find_word_right(term, true, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_LINE_DOWN: { + struct coord target; + if (search_extend_find_line_down(term, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR: { + struct coord target; + if (search_extend_find_char_left(term, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD: { + struct coord target; + if (search_extend_find_word_left(term, false, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS: { + struct coord target; + if (search_extend_find_word_left(term, true, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_LINE_UP: { + struct coord target; + if (search_extend_find_line_up(term, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_CLIPBOARD_PASTE: + text_from_clipboard( + seat, term, false, &from_clipboard_cb, &from_clipboard_done, term); + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_PRIMARY_PASTE: + text_from_primary( + seat, term, false, &from_clipboard_cb, &from_clipboard_done, term); + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_UNICODE_INPUT: + unicode_mode_activate(term); + return true; + + case BIND_ACTION_SEARCH_TOGGLE_CASE: + term->search.case_mode = (term->search.case_mode + 1) % 3; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD: + term->search.whole_word = !term->search.whole_word; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_TOGGLE_REGEX: + term->search.regex = !term->search.regex; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_HISTORY_PREV: { + struct search_history_entry *target; + if (term->search.history_pos == NULL) + target = term->search.history_tail; + else + target = term->search.history_pos->prev; + + if (target != NULL) { + term->search.history_pos = target; + term->search.len = 0; + term->search.cursor = 0; + if (term->search.buf != NULL) + term->search.buf[0] = U'\0'; + add_wchars(term, target->buf, target->len); + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_HISTORY_NEXT: { + if (term->search.history_pos == NULL) + return true; + + struct search_history_entry *target = term->search.history_pos->next; + term->search.history_pos = target; + + term->search.len = 0; + term->search.cursor = 0; + if (term->search.buf != NULL) + term->search.buf[0] = U'\0'; + + if (target != NULL) + add_wchars(term, target->buf, target->len); + + *update_search_result = *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_COMMIT_LINE: { + if (term->search.match_len == 0) { + /* No match — fall back to plain commit */ + selection_finalize(seat, term, serial); + search_cancel_keep_selection(term); + return true; + } + + /* Extend selection to span entire line(s) of the match */ + const struct coord match_end = selection_get_end(term); + const int start_row = term->search.match.row; + const int end_row = match_end.row; + + int sel_start_row = start_row - grid->view + grid->num_rows; + sel_start_row &= grid->num_rows - 1; + + int sel_end_row = end_row - grid->view + grid->num_rows; + sel_end_row &= grid->num_rows - 1; + + selection_cancel(term); + selection_start(term, 0, sel_start_row, SELECTION_CHAR_WISE, false); + selection_update(term, term->cols - 1, sel_end_row); + selection_finalize(seat, term, serial); + search_cancel_keep_selection(term); + return true; + } + + case BIND_ACTION_SEARCH_COUNT: + BUG("Invalid action type"); + return true; + } + + BUG("Unhandled action type"); + return false; +} + +void +search_input(struct seat *seat, struct terminal *term, + const struct key_binding_set *bindings, uint32_t key, + xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, + const xkb_keysym_t *raw_syms, size_t raw_count, + uint32_t serial) +{ + LOG_DBG("search: input: sym=%d/0x%x, mods=0x%08x, consumed=0x%08x", + sym, sym, mods, consumed); + + enum xkb_compose_status compose_status = seat->kbd.xkb_compose_state != NULL + ? xkb_compose_state_get_status(seat->kbd.xkb_compose_state) + : XKB_COMPOSE_NOTHING; + + enum search_direction search_direction = SEARCH_BACKWARD_SAME_POSITION; + bool update_search_result = false; + bool redraw = false; + + /* + * Key bindings + */ + + /* Match untranslated symbols */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods || bind->mods == 0) + continue; + + for (size_t i = 0; i < raw_count; i++) { + if (bind->k.sym == raw_syms[i]) { + if (execute_binding(seat, term, bind, serial, + &update_search_result, &search_direction, + &redraw)) + { + goto update_search; + } + return; + } + } + } + + /* Match translated symbol */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; + + if (bind->k.sym == sym && + bind->mods == (mods & ~consumed)) { + + if (execute_binding(seat, term, bind, serial, + &update_search_result, &search_direction, + &redraw)) + { + goto update_search; + } + return; + } + } + + /* Match raw key code */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods || bind->mods == 0) + continue; + + tll_foreach(bind->k.key_codes, code) { + if (code->item == key) { + if (execute_binding(seat, term, bind, serial, + &update_search_result, &search_direction, + &redraw)) + { + goto update_search; + } + return; + } + } + } + + uint8_t buf[64] = {0}; + int count = 0; + + if (compose_status == XKB_COMPOSE_COMPOSED) { + count = xkb_compose_state_get_utf8( + seat->kbd.xkb_compose_state, (char *)buf, sizeof(buf)); + xkb_compose_state_reset(seat->kbd.xkb_compose_state); + } else if (compose_status == XKB_COMPOSE_CANCELLED || + compose_status == XKB_COMPOSE_COMPOSING) { + count = 0; + } else { + count = xkb_state_key_get_utf8( + seat->kbd.xkb_state, key, (char *)buf, sizeof(buf)); + } + + update_search_result = redraw = count > 0; + search_direction = SEARCH_BACKWARD_SAME_POSITION; + + if (count == 0) + return; + + search_add_chars(term, (const char *)buf, count); + +update_search: + LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf); + if (update_search_result) + search_find_next(term, search_direction); + if (redraw) + render_refresh_search(term); +} diff --git a/search.h b/search.h new file mode 100644 index 0000000..ee8ecd7 --- /dev/null +++ b/search.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "key-binding.h" +#include "terminal.h" + +void search_begin(struct terminal *term); +void search_cancel(struct terminal *term); +void search_input( + struct seat *seat, struct terminal *term, + const struct key_binding_set *bindings, uint32_t key, + xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, + const xkb_keysym_t *raw_syms, size_t raw_count, + uint32_t serial); +void search_add_chars(struct terminal *term, const char *text, size_t len); + +void search_selection_cancelled(struct terminal *term); + +struct search_match_iterator { + struct terminal *term; + struct coord start; +}; + +struct search_match_iterator search_matches_new_iter(struct terminal *term); +struct range search_matches_next(struct search_match_iterator *iter); diff --git a/selection.c b/selection.c new file mode 100644 index 0000000..0a479ee --- /dev/null +++ b/selection.c @@ -0,0 +1,2919 @@ +#include "selection.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#define LOG_MODULE "selection" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "async.h" +#include "char32.h" +#include "commands.h" +#include "config.h" +#include "debug.h" +#include "extract.h" +#include "grid.h" +#include "misc.h" +#include "render.h" +#include "search.h" +#include "uri.h" +#include "util.h" +#include "vt.h" +#include "xmalloc.h" + +static const char *const mime_type_map[] = { + [DATA_OFFER_MIME_UNSET] = NULL, + [DATA_OFFER_MIME_TEXT_PLAIN] = "text/plain", + [DATA_OFFER_MIME_TEXT_UTF8] = "text/plain;charset=utf-8", + [DATA_OFFER_MIME_URI_LIST] = "text/uri-list", + + [DATA_OFFER_MIME_TEXT_TEXT] = "TEXT", + [DATA_OFFER_MIME_TEXT_STRING] = "STRING", + [DATA_OFFER_MIME_TEXT_UTF8_STRING] = "UTF8_STRING", +}; + +static inline struct coord +bounded(const struct grid *grid, struct coord coord) +{ + coord.row &= grid->num_rows - 1; + return coord; +} + +struct coord +selection_get_start(const struct terminal *term) +{ + if (term->selection.coords.start.row < 0) + return term->selection.coords.start; + return bounded(term->grid, term->selection.coords.start); +} + +struct coord +selection_get_end(const struct terminal *term) +{ + if (term->selection.coords.end.row < 0) + return term->selection.coords.end; + return bounded(term->grid, term->selection.coords.end); +} + +bool +selection_on_rows(const struct terminal *term, int row_start, int row_end) +{ + xassert(term->selection.coords.end.row >= 0); + + LOG_DBG("on rows: %d-%d, range: %d-%d (offset=%d)", + term->selection.coords.start.row, term->selection.coords.end.row, + row_start, row_end, term->grid->offset); + + row_start += term->grid->offset; + row_end += term->grid->offset; + xassert(row_end >= row_start); + + const struct coord *start = &term->selection.coords.start; + const struct coord *end = &term->selection.coords.end; + + const struct grid *grid = term->grid; + const int sb_start = grid->offset + term->rows; + + /* Use scrollback relative coords when checking for overlap */ + const int rel_row_start = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_start); + const int rel_row_end = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_end); + int rel_sel_start = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, start->row); + int rel_sel_end = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, end->row); + + if (rel_sel_start > rel_sel_end) { + int tmp = rel_sel_start; + rel_sel_start = rel_sel_end; + rel_sel_end = tmp; + } + + if ((rel_row_start <= rel_sel_start && rel_row_end >= rel_sel_start) || + (rel_row_start <= rel_sel_end && rel_row_end >= rel_sel_end)) + { + /* The range crosses one of the selection boundaries */ + return true; + } + + if (rel_row_start >= rel_sel_start && rel_row_end <= rel_sel_end) + return true; + + return false; +} + +void +selection_scroll_up(struct terminal *term, int rows) +{ + xassert(term->selection.coords.end.row >= 0); + + const int rel_row_start = + grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.start.row); + const int rel_row_end = + grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.end.row); + const int actual_start = min(rel_row_start, rel_row_end); + + if (actual_start - rows < 0) { + /* Part of the selection will be scrolled out, cancel it */ + selection_cancel(term); + } +} + +void +selection_scroll_down(struct terminal *term, int rows) +{ + xassert(term->selection.coords.end.row >= 0); + + const struct grid *grid = term->grid; + const struct range *sel = &term->selection.coords; + + const int screen_end = + grid_row_abs_to_sb(grid, term->rows, grid->offset + term->rows - 1); + const int rel_row_start = + grid_row_abs_to_sb(term->grid, term->rows, sel->start.row); + const int rel_row_end = + grid_row_abs_to_sb(term->grid, term->rows, sel->end.row); + const int actual_end = max(rel_row_start, rel_row_end); + + if (actual_end > screen_end - rows) { + /* Part of the selection will be scrolled out, cancel it */ + selection_cancel(term); + } +} + +void +selection_view_up(struct terminal *term, int new_view) +{ + if (likely(term->selection.coords.start.row < 0)) + return; + + if (likely(new_view < term->grid->view)) + return; + + term->selection.coords.start.row += term->grid->num_rows; + if (term->selection.coords.end.row >= 0) + term->selection.coords.end.row += term->grid->num_rows; +} + +void +selection_view_down(struct terminal *term, int new_view) +{ + if (likely(term->selection.coords.start.row < 0)) + return; + + if (likely(new_view > term->grid->view)) + return; + + term->selection.coords.start.row &= term->grid->num_rows - 1; + if (term->selection.coords.end.row >= 0) + term->selection.coords.end.row &= term->grid->num_rows - 1; +} + +static void +foreach_selected_normal( + struct terminal *term, struct coord _start, struct coord _end, + bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, + int row_no, int col, void *data), + void *data) +{ + const struct coord *start = &_start; + const struct coord *end = &_end; + + const int grid_rows = term->grid->num_rows; + + /* Start/end rows, relative to the scrollback start */ + /* Start/end rows, relative to the scrollback start */ + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + + int start_row, end_row; + int start_col, end_col; + + if (rel_start_row < rel_end_row) { + start_row = start->row; + start_col = start->col; + end_row = end->row; + end_col = end->col; + } else if (rel_start_row > rel_end_row) { + start_row = end->row; + start_col = end->col; + end_row = start->row; + end_col = start->col; + } else { + start_row = end_row = start->row; + start_col = min(start->col, end->col); + end_col = max(start->col, end->col); + } + + start_row &= (grid_rows - 1); + end_row &= (grid_rows - 1); + + for (int r = start_row; r != end_row; r = (r + 1) & (grid_rows - 1)) { + struct row *row = term->grid->rows[r]; + xassert(row != NULL); + + for (int c = start_col; c <= term->cols - 1; c++) { + if (!cb(term, row, &row->cells[c], r, c, data)) + return; + } + + start_col = 0; + } + + /* Last, partial row */ + struct row *row = term->grid->rows[end_row]; + xassert(row != NULL); + + for (int c = start_col; c <= end_col; c++) { + if (!cb(term, row, &row->cells[c], end_row, c, data)) + return; + } +} + +static void +foreach_selected_block( + struct terminal *term, struct coord _start, struct coord _end, + bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, + int row_no, int col, void *data), + void *data) +{ + const struct coord *start = &_start; + const struct coord *end = &_end; + + const int grid_rows = term->grid->num_rows; + + /* Start/end rows, relative to the scrollback start */ + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + + struct coord top_left = { + .row = (rel_start_row < rel_end_row + ? start->row : end->row) & (grid_rows - 1), + .col = min(start->col, end->col), + }; + + struct coord bottom_right = { + .row = (rel_start_row > rel_end_row + ? start->row : end->row) & (grid_rows - 1), + .col = max(start->col, end->col), + }; + + int r = top_left.row; + while (true) { + struct row *row = term->grid->rows[r]; + xassert(row != NULL); + + for (int c = top_left.col; c <= bottom_right.col; c++) { + if (!cb(term, row, &row->cells[c], r, c, data)) + return; + } + + if (r == bottom_right.row) + break; + + r++; + r &= grid_rows - 1; + } +} + +static void +foreach_selected( + struct terminal *term, struct coord start, struct coord end, + bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, int row_no, int col, void *data), + void *data) +{ + switch (term->selection.kind) { + case SELECTION_CHAR_WISE: + case SELECTION_WORD_WISE: + case SELECTION_QUOTE_WISE: + case SELECTION_LINE_WISE: + foreach_selected_normal(term, start, end, cb, data); + return; + + case SELECTION_BLOCK: + foreach_selected_block(term, start, end, cb, data); + return; + + case SELECTION_NONE: + break; + } + + BUG("Invalid selection kind"); +} + +static bool +extract_one_const_wrapper(struct terminal *term, + struct row *row, struct cell *cell, + int row_no, int col, void *data) +{ + return extract_one(term, row, cell, col, data); +} + +char * +selection_to_text(const struct terminal *term) +{ + if (term->selection.coords.end.row == -1) + return NULL; + + struct extraction_context *ctx = extract_begin(term->selection.kind, true); + if (ctx == NULL) + return NULL; + + foreach_selected( + (struct terminal *)term, term->selection.coords.start, term->selection.coords.end, + &extract_one_const_wrapper, ctx); + + char *text; + return extract_finish(ctx, &text, NULL) ? text : NULL; +} + +/* Coordinates are in *absolute* row numbers (NOT view local) */ +void +selection_find_word_boundary_left(const struct terminal *term, struct coord *pos, + bool spaces_only) +{ + const struct grid *grid = term->grid; + + xassert(pos->col >= 0); + xassert(pos->col < term->cols); + xassert(pos->row >= 0); + pos->row &= grid->num_rows - 1; + + const struct row *r = grid->rows[pos->row]; + char32_t c = r->cells[pos->col].wc; + + while (c >= CELL_SPACER) { + xassert(pos->col > 0); + if (pos->col == 0) + return; + pos->col--; + c = r->cells[pos->col].wc; + } + + if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI) + c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0]; + + bool initial_is_space = c == 0 || isc32space(c); + bool initial_is_delim = + !initial_is_space && !isword(c, spaces_only, term->conf->word_delimiters); + bool initial_is_word = + c != 0 && isword(c, spaces_only, term->conf->word_delimiters); + + while (true) { + int next_col = pos->col - 1; + int next_row = pos->row; + + const struct row *row = grid->rows[next_row]; + + /* Linewrap */ + if (next_col < 0) { + next_col = term->cols - 1; + + next_row = (next_row - 1 + grid->num_rows) & (grid->num_rows - 1); + + if (grid_row_abs_to_sb(grid, term->rows, next_row) == term->grid->num_rows - 1 || + grid->rows[next_row] == NULL) + { + /* Scrollback wrap-around */ + break; + } + + row = grid->rows[next_row]; + + if (row->linebreak) { + /* Hard linebreak, treat as space. I.e. break selection */ + break; + } + } + + c = row->cells[next_col].wc; + while (c >= CELL_SPACER) { + xassert(next_col > 0); + if (--next_col < 0) + return; + c = row->cells[next_col].wc; + } + + if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI) + c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0]; + + bool is_space = c == 0 || isc32space(c); + bool is_delim = + !is_space && !isword(c, spaces_only, term->conf->word_delimiters); + bool is_word = + c != 0 && isword(c, spaces_only, term->conf->word_delimiters); + + if (initial_is_space && !is_space) + break; + if (initial_is_delim && !is_delim) + break; + if (initial_is_word && !is_word) + break; + + pos->col = next_col; + pos->row = next_row; + } +} + +/* Coordinates are in *absolute* row numbers (NOT view local) */ +void +selection_find_word_boundary_right(const struct terminal *term, struct coord *pos, + bool spaces_only, + bool stop_on_space_to_word_boundary) +{ + const struct grid *grid = term->grid; + + xassert(pos->col >= 0); + xassert(pos->col < term->cols); + xassert(pos->row >= 0); + pos->row &= grid->num_rows - 1; + + const struct row *r = grid->rows[pos->row]; + char32_t c = r->cells[pos->col].wc; + + while (c >= CELL_SPACER) { + xassert(pos->col > 0); + if (pos->col == 0) + return; + pos->col--; + c = r->cells[pos->col].wc; + } + + if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI) + c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0]; + + bool initial_is_space = c == 0 || isc32space(c); + bool initial_is_delim = + !initial_is_space && !isword(c, spaces_only, term->conf->word_delimiters); + bool initial_is_word = + c != 0 && isword(c, spaces_only, term->conf->word_delimiters); + bool have_seen_word = initial_is_word; + + while (true) { + int next_col = pos->col + 1; + int next_row = pos->row; + + const struct row *row = term->grid->rows[next_row]; + + /* Linewrap */ + if (next_col >= term->cols) { + if (row->linebreak) { + /* Hard linebreak, treat as space. I.e. break selection */ + break; + } + + next_col = 0; + next_row = (next_row + 1) & (grid->num_rows - 1); + + if (grid_row_abs_to_sb(grid, term->rows, next_row) == 0) { + /* Scrollback wrap-around */ + break; + } + + row = grid->rows[next_row]; + } + + c = row->cells[next_col].wc; + while (c >= CELL_SPACER) { + if (++next_col >= term->cols) { + next_col = 0; + if (++next_row >= term->rows) + return; + } + c = row->cells[next_col].wc; + } + + if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI) + c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0]; + + bool is_space = c == 0 || isc32space(c); + bool is_delim = + !is_space && !isword(c, spaces_only, term->conf->word_delimiters); + bool is_word = + c != 0 && isword(c, spaces_only, term->conf->word_delimiters); + + if (stop_on_space_to_word_boundary) { + if (initial_is_space && !is_space) + break; + if (initial_is_delim && !is_delim) + break; + } else { + if (initial_is_space && ((have_seen_word && is_space) || is_delim)) + break; + if (initial_is_delim && ((have_seen_word && is_delim) || is_space)) + break; + } + if (initial_is_word && !is_word) + break; + + have_seen_word = is_word; + + pos->col = next_col; + pos->row = next_row; + } +} + +static bool +selection_find_quote_left(struct terminal *term, struct coord *pos, + char32_t *quote_char) +{ + const struct row *row = grid_row_in_view(term->grid, pos->row); + char32_t wc = row->cells[pos->col].wc; + + if (*quote_char == '\0' ? (wc == '"' || wc == '\'') + : wc == *quote_char) + { + return false; + } + + int next_row = pos->row; + int next_col = pos->col; + + while (true) { + if (--next_col < 0) { + next_col = term->cols - 1; + if (--next_row < 0) + return false; + + row = grid_row_in_view(term->grid, next_row); + if (row->linebreak) + return false; + } + + wc = row->cells[next_col].wc; + + if (*quote_char == '\0' ? (wc == '"' || wc == '\'') + : wc == *quote_char) + { + xassert(next_col + 1 <= term->cols); + if (next_col + 1 == term->cols) { + xassert(next_row < pos->row); + pos->row = next_row + 1; + pos->col = 0; + } else { + pos->row = next_row; + pos->col = next_col + 1; + } + + *quote_char = wc; + return true; + } + } +} + +static bool +selection_find_quote_right(struct terminal *term, struct coord *pos, char32_t quote_char) +{ + if (quote_char == '\0') + return false; + + const struct row *row = grid_row_in_view(term->grid, pos->row); + char32_t wc = row->cells[pos->col].wc; + if (wc == quote_char) + return false; + + int next_row = pos->row; + int next_col = pos->col; + + while (true) { + if (++next_col >= term->cols) { + next_col = 0; + if (++next_row >= term->rows) + return false; + + if (row->linebreak) + return false; + + row = grid_row_in_view(term->grid, next_row); + } + + wc = row->cells[next_col].wc; + if (wc == quote_char) { + pos->row = next_row; + pos->col = next_col - 1; + xassert(pos->col >= 0); + return true; + } + } +} + +static void +selection_find_line_boundary_left(struct terminal *term, struct coord *pos) +{ + int next_row = pos->row; + pos->col = 0; + + while (true) { + if (--next_row < 0) + return; + + const struct row *row = grid_row_in_view(term->grid, next_row); + assert(row != NULL); + + if (row->linebreak) + return; + + pos->col = 0; + pos->row = next_row; + } +} + +static void +selection_find_line_boundary_right(struct terminal *term, struct coord *pos) +{ + int next_row = pos->row; + pos->col = term->cols - 1; + + while (true) { + const struct row *row = grid_row_in_view(term->grid, next_row); + assert(row != NULL); + + if (row->linebreak) + return; + + if (++next_row >= term->rows) + return; + + pos->col = term->cols - 1; + pos->row = next_row; + } +} + +void +selection_start(struct terminal *term, int col, int row, + enum selection_kind kind, + bool spaces_only) +{ + selection_cancel(term); + + LOG_DBG("%s selection started at %d,%d", + kind == SELECTION_CHAR_WISE ? "character-wise" : + kind == SELECTION_WORD_WISE ? "word-wise" : + kind == SELECTION_QUOTE_WISE ? "quote-wise" : + kind == SELECTION_LINE_WISE ? "line-wise" : + kind == SELECTION_BLOCK ? "block" : "", + row, col); + + term->selection.kind = kind; + term->selection.ongoing = true; + term->selection.spaces_only = spaces_only; + + switch (kind) { + case SELECTION_CHAR_WISE: + case SELECTION_BLOCK: + term->selection.coords.start = (struct coord){col, term->grid->view + row}; + term->selection.coords.end = (struct coord){-1, -1}; + + term->selection.pivot.start = term->selection.coords.start; + term->selection.pivot.end = term->selection.coords.end; + break; + + case SELECTION_WORD_WISE: { + struct coord start = {col, term->grid->view + row}; + struct coord end = {col, term->grid->view + row}; + selection_find_word_boundary_left(term, &start, spaces_only); + selection_find_word_boundary_right(term, &end, spaces_only, true); + + term->selection.coords.start = start; + + term->selection.pivot.start = term->selection.coords.start; + term->selection.pivot.end = end; + + /* + * FIXME: go through selection.c and make sure all public + * functions use the *same* coordinate system... + * + * selection_find_word_boundary*() uses absolute row numbers, + * while selection_update(), and pretty much all others, use + * view-local. + */ + + selection_update(term, end.col, end.row - term->grid->view); + break; + } + + case SELECTION_QUOTE_WISE: { + struct coord start = {col, row}, end = {col, row}; + + char32_t quote_char = '\0'; + bool found_left = selection_find_quote_left(term, &start, "e_char); + bool found_right = selection_find_quote_right(term, &end, quote_char); + + if (found_left && !found_right) { + xassert(quote_char != '\0'); + + /* + * Try to flip the quote character we're looking for. + * + * This lets us handle things like: + * + * "nested 'quotes are fun', right" + * + * In the example above, starting the selection at + * "right", will otherwise not match. find-left will find + * the single quote, causing find-right to fail. + * + * By flipping the quote-character, and re-trying, we + * find-left will find the starting double quote, letting + * find-right succeed as well. + */ + + if (quote_char == '\'') + quote_char = '"'; + else if (quote_char == '"') + quote_char = '\''; + + found_left = selection_find_quote_left(term, &start, "e_char); + found_right = selection_find_quote_right(term, &end, quote_char); + } + + if (found_left && found_right) { + term->selection.coords.start = (struct coord){ + start.col, term->grid->view + start.row}; + + term->selection.pivot.start = term->selection.coords.start; + term->selection.pivot.end = (struct coord){end.col, term->grid->view + end.row}; + + term->selection.kind = SELECTION_WORD_WISE; + selection_update(term, end.col, end.row); + break; + } else { + term->selection.kind = SELECTION_LINE_WISE; + /* FALLTHROUGH */ + } + } + + case SELECTION_LINE_WISE: { + struct coord start = {0, row}, end = {term->cols - 1, row}; + selection_find_line_boundary_left(term, &start); + selection_find_line_boundary_right(term, &end); + + term->selection.coords.start = (struct coord){ + start.col, term->grid->view + start.row}; + term->selection.pivot.start = term->selection.coords.start; + term->selection.pivot.end = (struct coord){end.col, term->grid->view + end.row}; + + selection_update(term, end.col, end.row); + break; + } + + case SELECTION_NONE: + BUG("Invalid selection kind"); + break; + } + +} + +static pixman_region32_t +pixman_region_for_coords_normal(const struct terminal *term, + const struct coord *start, + const struct coord *end) +{ + pixman_region32_t region; + pixman_region32_init(®ion); + + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + + if (rel_start_row < rel_end_row) { + /* First partial row (start ->)*/ + pixman_region32_union_rect( + ®ion, ®ion, + start->col, rel_start_row, + term->cols - start->col, 1); + + /* Full rows between start and end */ + if (rel_start_row + 1 < rel_end_row) { + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_start_row + 1, + term->cols, rel_end_row - rel_start_row - 1); + } + + /* Last partial row (-> end) */ + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_end_row, + end->col + 1, 1); + + } else if (rel_start_row > rel_end_row) { + /* First partial row (end ->) */ + pixman_region32_union_rect( + ®ion, ®ion, + end->col, rel_end_row, + term->cols - end->col, 1); + + /* Full rows between end and start */ + if (rel_end_row + 1 < rel_start_row) { + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_end_row + 1, + term->cols, rel_start_row - rel_end_row - 1); + } + + /* Last partial row (-> start) */ + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_start_row, + start->col + 1, 1); + } else { + const int start_col = min(start->col, end->col); + const int end_col = max(start->col, end->col); + + pixman_region32_union_rect( + ®ion, ®ion, + start_col, rel_start_row, + end_col + 1 - start_col, 1); + } + + return region; +} + +static pixman_region32_t +pixman_region_for_coords_block(const struct terminal *term, + const struct coord *start, const struct coord *end) +{ + pixman_region32_t region; + pixman_region32_init(®ion); + + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + + pixman_region32_union_rect( + ®ion, ®ion, + min(start->col, end->col), min(rel_start_row, rel_end_row), + abs(start->col - end->col) + 1, abs(rel_start_row - rel_end_row) + 1); + + return region; +} + +/* Returns a pixman region representing the selection between 'start' + * and 'end' (given the current selection kind), in *scrollback + * relative coordinates* */ +static pixman_region32_t +pixman_region_for_coords(const struct terminal *term, + const struct coord *start, const struct coord *end) +{ + switch (term->selection.kind) { + default: return pixman_region_for_coords_normal(term, start, end); + case SELECTION_BLOCK: return pixman_region_for_coords_block(term, start, end); + } +} + +enum mark_selection_variant { + MARK_SELECTION_MARK_AND_DIRTY, + MARK_SELECTION_UNMARK_AND_DIRTY, + MARK_SELECTION_MARK_FOR_RENDER, +}; + +static void +mark_selected_region(struct terminal *term, pixman_box32_t *boxes, + size_t count, enum mark_selection_variant mark_variant) +{ + const bool selected = + mark_variant == MARK_SELECTION_MARK_AND_DIRTY || + mark_variant == MARK_SELECTION_MARK_FOR_RENDER; + const bool dirty_cells = + mark_variant == MARK_SELECTION_MARK_AND_DIRTY || + mark_variant == MARK_SELECTION_UNMARK_AND_DIRTY; + const bool highlight_empty = + mark_variant != MARK_SELECTION_MARK_FOR_RENDER || + term->selection.kind == SELECTION_BLOCK; + + for (size_t i = 0; i < count; i++) { + const pixman_box32_t *box = &boxes[i]; + + LOG_DBG("%s selection in region: %dx%d - %dx%d", + selected ? "marking" : "unmarking", + box->x1, box->y1, + box->x2, box->y2); + + int abs_row_start = grid_row_sb_to_abs( + term->grid, term->rows, box->y1); + + for (int r = abs_row_start, rel_r = box->y1; + rel_r < box->y2; + r = (r + 1) & (term->grid->num_rows - 1), rel_r++) + { + struct row *row = term->grid->rows[r]; + xassert(row != NULL); + + if (dirty_cells) + row->dirty = true; + + for (int c = box->x1, empty_count = 0; c < box->x2; c++) { + struct cell *cell = &row->cells[c]; + + if (cell->wc == 0 && !highlight_empty) { + /* + * We used to highlight empty cells *if* they were + * followed by non-empty cell(s), since this + * corresponds to what gets extracted when the + * selection is copied (that is, empty cells + * "between" non-empty cells are converted to + * spaces). + * + * However, they way we handle selection updates + * (diffing the "old" selection area against the + * "new" one, using pixman regions), means we + * can't correctly update the state of empty + * cells. The result is "random" empty cells being + * rendered as selected when they shouldn't. + * + * "Fix" by *never* highlighting selected empty + * cells (they still get converted to spaces when + * copied, if followed by non-empty cells). + */ + empty_count++; + + /* + * When the selection is *modified*, empty cells + * are treated just like non-empty cells; they are + * marked as selected, and dirtied. + * + * This is due to how the algorithm for updating + * the selection works; it uses regions to + * calculate the difference between the "old" and + * the "new" selection. This makes it impossible + * to tell if an empty cell is a *trailing* empty + * cell (that should not be highlighted), or an + * empty cells between non-empty cells (that + * *should* be highlighted). + * + * Then, when a frame is rendered, we loop the + * *visibible* cells that belong to the + * selection. At this point, we *can* tell if an + * empty cell is trailing or not. + * + * So, what we need to do is check if a + * 'selected', and empty cell has been marked as + * selected, temporarily unmark (forcing it dirty, + * to ensure it gets re-rendered). If it is *not* + * a trailing empty cell, it will get re-tagged as + * selected in the for-loop below. + */ + cell->attrs.clean = false; + cell->attrs.selected = false; + row->dirty = true; + continue; + } + + for (int j = 0; j < empty_count + 1; j++) { + xassert(c - j >= 0); + struct cell *cell = &row->cells[c - j]; + + if (dirty_cells) { + cell->attrs.clean = false; + row->dirty = true; + } + cell->attrs.selected = selected; + } + + empty_count = 0; + } + } + } +} + +static void +selection_modify(struct terminal *term, struct coord start, struct coord end) +{ + xassert(term->selection.coords.start.row != -1); + xassert(start.row != -1 && start.col != -1); + xassert(end.row != -1 && end.col != -1); + + pixman_region32_t previous_selection; + if (term->selection.coords.end.row >= 0) { + previous_selection = pixman_region_for_coords( + term, + &term->selection.coords.start, + &term->selection.coords.end); + } else + pixman_region32_init(&previous_selection); + + pixman_region32_t current_selection = pixman_region_for_coords( + term, &start, &end); + + pixman_region32_t no_longer_selected; + pixman_region32_init(&no_longer_selected); + pixman_region32_subtract( + &no_longer_selected, &previous_selection, ¤t_selection); + + pixman_region32_t newly_selected; + pixman_region32_init(&newly_selected); + pixman_region32_subtract( + &newly_selected, ¤t_selection, &previous_selection); + + /* Clear selection in cells no longer selected */ + int n_rects = -1; + pixman_box32_t *boxes = NULL; + + boxes = pixman_region32_rectangles(&no_longer_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_UNMARK_AND_DIRTY); + + boxes = pixman_region32_rectangles(&newly_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_AND_DIRTY); + + pixman_region32_fini(&newly_selected); + pixman_region32_fini(&no_longer_selected); + pixman_region32_fini(¤t_selection); + pixman_region32_fini(&previous_selection); + + term->selection.coords.start = start; + term->selection.coords.end = end; + render_refresh(term); +} + +static void +set_pivot_point_for_block_and_char_wise(struct terminal *term, + struct coord start, + enum selection_direction new_direction) +{ + struct coord *pivot_start = &term->selection.pivot.start; + struct coord *pivot_end = &term->selection.pivot.end; + + *pivot_start = start; + + /* First, make sure 'start' isn't in the middle of a + * multi-column character */ + while (true) { + const struct row *row = term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]; + const struct cell *cell = &row->cells[pivot_start->col]; + + if (cell->wc < CELL_SPACER) + break; + + /* Multi-column chars don't cross rows */ + xassert(pivot_start->col > 0); + if (pivot_start->col == 0) + break; + + pivot_start->col--; + } + + /* + * Setup pivot end to be one character *before* start + * Which one we move, the end or start point, depends + * on the initial selection direction. + */ + + *pivot_end = *pivot_start; + + if (new_direction == SELECTION_RIGHT) { + bool keep_going = true; + while (keep_going) { + const struct row *row = term->grid->rows[pivot_end->row & (term->grid->num_rows - 1)]; + const char32_t wc = row->cells[pivot_end->col].wc; + + keep_going = wc >= CELL_SPACER; + + if (pivot_end->col == 0) { + if (pivot_end->row - term->grid->view <= 0) + break; + pivot_end->col = term->cols - 1; + pivot_end->row--; + } else + pivot_end->col--; + } + } else { + bool keep_going = true; + while (keep_going) { + const struct row *row = term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]; + const char32_t wc = pivot_start->col < term->cols - 1 + ? row->cells[pivot_start->col + 1].wc : 0; + + keep_going = wc >= CELL_SPACER; + + if (pivot_start->col >= term->cols - 1) { + if (pivot_start->row - term->grid->view >= term->rows - 1) + break; + pivot_start->col = 0; + pivot_start->row++; + } else + pivot_start->col++; + } + } + + xassert(term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]-> + cells[pivot_start->col].wc <= CELL_SPACER); + xassert(term->grid->rows[pivot_end->row & (term->grid->num_rows - 1)]-> + cells[pivot_end->col].wc <= CELL_SPACER + 1); +} + +void +selection_update(struct terminal *term, int col, int row) +{ + if (term->selection.coords.start.row < 0) + return; + + if (!term->selection.ongoing) + return; + + xassert(term->grid->view + row != -1); + + struct coord new_start = term->selection.coords.start; + struct coord new_end = {col, term->grid->view + row}; + + LOG_DBG("selection updated: start = %d,%d, end = %d,%d -> %d, %d", + term->selection.coords.start.row, term->selection.coords.start.col, + term->selection.coords.end.row, term->selection.coords.end.col, + new_end.row, new_end.col); + + /* Adjust start point if the selection has changed 'direction' */ + if (!(new_end.row == new_start.row && new_end.col == new_start.col)) { + enum selection_direction new_direction = term->selection.direction; + + struct coord *pivot_start = &term->selection.pivot.start; + struct coord *pivot_end = &term->selection.pivot.end; + + if (term->selection.kind == SELECTION_BLOCK) { + if (new_end.col > pivot_start->col) + new_direction = SELECTION_RIGHT; + else + new_direction = SELECTION_LEFT; + + if (term->selection.direction == SELECTION_UNDIR) + set_pivot_point_for_block_and_char_wise(term, *pivot_start, new_direction); + + if (new_direction == SELECTION_LEFT) + new_start = *pivot_end; + else + new_start = *pivot_start; + term->selection.direction = new_direction; + } else { + if (new_end.row < pivot_start->row || + (new_end.row == pivot_start->row && + new_end.col < pivot_start->col)) + { + /* New end point is before the start point */ + new_direction = SELECTION_LEFT; + } else { + /* The new end point is after the start point */ + new_direction = SELECTION_RIGHT; + } + + if (term->selection.direction != new_direction) { + if (term->selection.direction == SELECTION_UNDIR && + pivot_end->row < 0) + { + set_pivot_point_for_block_and_char_wise( + term, *pivot_start, new_direction); + } + + if (new_direction == SELECTION_LEFT) { + xassert(pivot_end->row >= 0); + new_start = *pivot_end; + } else + new_start = *pivot_start; + + term->selection.direction = new_direction; + } + } + } + + switch (term->selection.kind) { + case SELECTION_CHAR_WISE: + case SELECTION_BLOCK: + break; + + case SELECTION_WORD_WISE: + switch (term->selection.direction) { + case SELECTION_LEFT: + new_end = (struct coord){col, term->grid->view + row}; + selection_find_word_boundary_left( + term, &new_end, term->selection.spaces_only); + break; + + case SELECTION_RIGHT: + new_end = (struct coord){col, term->grid->view + row}; + selection_find_word_boundary_right( + term, &new_end, term->selection.spaces_only, true); + break; + + case SELECTION_UNDIR: + break; + } + break; + + case SELECTION_QUOTE_WISE: + BUG("quote-wise selection should always be transformed to either word-wise or line-wise"); + break; + + case SELECTION_LINE_WISE: + switch (term->selection.direction) { + case SELECTION_LEFT: { + struct coord end = {0, row}; + selection_find_line_boundary_left(term, &end); + new_end = (struct coord){end.col, term->grid->view + end.row}; + break; + } + + case SELECTION_RIGHT: { + struct coord end = {col, row}; + selection_find_line_boundary_right(term, &end); + new_end = (struct coord){end.col, term->grid->view + end.row}; + break; + } + + case SELECTION_UNDIR: + break; + } + break; + + case SELECTION_NONE: + BUG("Invalid selection kind"); + break; + } + + size_t start_row_idx = new_start.row & (term->grid->num_rows - 1); + size_t end_row_idx = new_end.row & (term->grid->num_rows - 1); + + const struct row *row_start = term->grid->rows[start_row_idx]; + const struct row *row_end = term->grid->rows[end_row_idx]; + + /* If an end point is in the middle of a multi-column character, + * expand the selection to cover the entire character */ + if (new_start.row < new_end.row || + (new_start.row == new_end.row && new_start.col <= new_end.col)) + { + while (new_start.col >= 1 && + row_start->cells[new_start.col].wc >= CELL_SPACER) + new_start.col--; + while (new_end.col < term->cols - 1 && + row_end->cells[new_end.col + 1].wc >= CELL_SPACER) + new_end.col++; + } else { + while (new_end.col >= 1 && + row_end->cells[new_end.col].wc >= CELL_SPACER) + new_end.col--; + while (new_start.col < term->cols - 1 && + row_start->cells[new_start.col + 1].wc >= CELL_SPACER) + new_start.col++; + } + + selection_modify(term, new_start, new_end); +} + +void +selection_dirty_cells(struct terminal *term) +{ + if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) + return; + + pixman_region32_t selection = pixman_region_for_coords( + term, &term->selection.coords.start, &term->selection.coords.end); + + pixman_region32_t view = pixman_region_for_coords( + term, + &(struct coord){0, term->grid->view}, + &(struct coord){term->cols - 1, term->grid->view + term->rows - 1}); + + pixman_region32_t visible_and_selected; + pixman_region32_init(&visible_and_selected); + pixman_region32_intersect(&visible_and_selected, &selection, &view); + + int n_rects = -1; + pixman_box32_t *boxes = + pixman_region32_rectangles(&visible_and_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_FOR_RENDER); + + pixman_region32_fini(&visible_and_selected); + pixman_region32_fini(&view); + pixman_region32_fini(&selection); +} + +static void +selection_extend_normal(struct terminal *term, int col, int row, + enum selection_kind new_kind) +{ + const struct coord *start = &term->selection.coords.start; + const struct coord *end = &term->selection.coords.end; + + const int rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); + int rel_start_row = grid_row_abs_to_sb(term->grid, term->rows, start->row); + int rel_end_row = grid_row_abs_to_sb(term->grid, term->rows, end->row); + + if (rel_start_row > rel_end_row || + (rel_start_row == rel_end_row && start->col > end->col)) + { + const struct coord *tmp = start; + start = end; + end = tmp; + + int tmp_row = rel_start_row; + rel_start_row = rel_end_row; + rel_end_row = tmp_row; + } + + struct coord new_start, new_end; + enum selection_direction direction; + + if (rel_row < rel_start_row || + (rel_row == rel_start_row && col < start->col)) + { + /* Extend selection to start *before* current start */ + new_start = *end; + new_end = (struct coord){col, row}; + direction = SELECTION_LEFT; + } + + else if (rel_row > rel_end_row || + (rel_row == rel_end_row && col > end->col)) + { + /* Extend selection to end *after* current end */ + new_start = *start; + new_end = (struct coord){col, row}; + direction = SELECTION_RIGHT; + } + + else { + /* Shrink selection from start or end, depending on which one is closest */ + + const int linear = rel_row * term->cols + col; + + if (abs(linear - (rel_start_row * term->cols + start->col)) < + abs(linear - (rel_end_row * term->cols + end->col))) + { + /* Move start point */ + new_start = *end; + new_end = (struct coord){col, row}; + direction = SELECTION_LEFT; + } + + else { + /* Move end point */ + new_start = *start; + new_end = (struct coord){col, row}; + direction = SELECTION_RIGHT; + } + } + + const bool spaces_only = term->selection.spaces_only; + + switch (term->selection.kind) { + case SELECTION_CHAR_WISE: + xassert(new_kind == SELECTION_CHAR_WISE); + set_pivot_point_for_block_and_char_wise(term, new_start, direction); + break; + + case SELECTION_WORD_WISE: { + xassert(new_kind == SELECTION_CHAR_WISE || + new_kind == SELECTION_WORD_WISE); + + struct coord pivot_start = {new_start.col, new_start.row}; + struct coord pivot_end = pivot_start; + + selection_find_word_boundary_left(term, &pivot_start, spaces_only); + selection_find_word_boundary_right(term, &pivot_end, spaces_only, true); + + term->selection.pivot.start = pivot_start; + term->selection.pivot.end = pivot_end; + break; + } + + case SELECTION_QUOTE_WISE: { + BUG("quote-wise selection should always be transformed to either word-wise or line-wise"); + break; + } + + case SELECTION_LINE_WISE: { + xassert(new_kind == SELECTION_CHAR_WISE || + new_kind == SELECTION_LINE_WISE); + + struct coord pivot_start = {new_start.col, new_start.row - term->grid->view}; + struct coord pivot_end = pivot_start; + + selection_find_line_boundary_left(term, &pivot_start); + selection_find_line_boundary_right(term, &pivot_end); + + term->selection.pivot.start = + (struct coord){pivot_start.col, term->grid->view + pivot_start.row}; + term->selection.pivot.end = + (struct coord){pivot_end.col, term->grid->view + pivot_end.row}; + break; + } + + case SELECTION_BLOCK: + case SELECTION_NONE: + BUG("Invalid selection kind in this context"); + break; + } + + term->selection.kind = new_kind; + term->selection.direction = direction; + selection_modify(term, new_start, new_end); +} + +static void +selection_extend_block(struct terminal *term, int col, int row) +{ + const struct coord *start = &term->selection.coords.start; + const struct coord *end = &term->selection.coords.end; + + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + + struct coord top_left = { + .row = rel_start_row < rel_end_row ? start->row : end->row, + .col = min(start->col, end->col), + }; + + struct coord top_right = { + .row = top_left.row, + .col = max(start->col, end->col), + }; + + struct coord bottom_left = { + .row = rel_start_row > rel_end_row ? start->row : end->row, + .col = min(start->col, end->col), + }; + + struct coord bottom_right = { + .row = bottom_left.row, + .col = max(start->col, end->col), + }; + + const int rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); + const int rel_top_row = grid_row_abs_to_sb(term->grid, term->rows, top_left.row); + const int rel_bottom_row = grid_row_abs_to_sb(term->grid, term->rows, bottom_left.row); + struct coord new_start; + struct coord new_end; + + enum selection_direction direction = SELECTION_UNDIR; + + if (rel_row <= rel_top_row || + abs(rel_row - rel_top_row) < abs(rel_row - rel_bottom_row)) + { + /* Move one of the top corners */ + + if (abs(col - top_left.col) < abs(col - top_right.col)) { + new_start = bottom_right; + new_end = (struct coord){col, row}; + } + + else { + new_start = bottom_left; + new_end = (struct coord){col, row}; + } + } + + else { + /* Move one of the bottom corners */ + + if (abs(col - bottom_left.col) < abs(col - bottom_right.col)) { + new_start = top_right; + new_end = (struct coord){col, row}; + } + + else { + new_start = top_left; + new_end = (struct coord){col, row}; + } + } + + direction = col > new_start.col ? SELECTION_RIGHT : SELECTION_LEFT; + set_pivot_point_for_block_and_char_wise(term, new_start, direction); + + term->selection.direction = direction; + selection_modify(term, new_start, new_end); +} + +void +selection_extend(struct seat *seat, struct terminal *term, + int col, int row, enum selection_kind new_kind) +{ + if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) { + /* No existing selection */ + return; + } + + if (term->selection.kind == SELECTION_BLOCK && new_kind != SELECTION_BLOCK) + return; + + term->selection.ongoing = true; + + row += term->grid->view; + + if ((row == term->selection.coords.start.row && col == term->selection.coords.start.col) || + (row == term->selection.coords.end.row && col == term->selection.coords.end.col)) + { + /* Extension point *is* one of the current end points */ + return; + } + + switch (term->selection.kind) { + case SELECTION_NONE: + BUG("Invalid selection kind"); + return; + + case SELECTION_CHAR_WISE: + case SELECTION_WORD_WISE: + case SELECTION_QUOTE_WISE: + case SELECTION_LINE_WISE: + selection_extend_normal(term, col, row, new_kind); + break; + + case SELECTION_BLOCK: + selection_extend_block(term, col, row); + break; + } +} + +//static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener; + +void +selection_finalize(struct seat *seat, struct terminal *term, uint32_t serial) +{ + if (!term->selection.ongoing) + return; + + LOG_DBG("selection finalize"); + + selection_stop_scroll_timer(term); + term->selection.ongoing = false; + + if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) + return; + + xassert(term->selection.coords.start.row != -1); + xassert(term->selection.coords.end.row != -1); + + term->selection.coords.start.row &= (term->grid->num_rows - 1); + term->selection.coords.end.row &= (term->grid->num_rows - 1); + + switch (term->conf->selection_target) { + case SELECTION_TARGET_NONE: + break; + + case SELECTION_TARGET_PRIMARY: + selection_to_primary(seat, term, serial); + break; + case SELECTION_TARGET_CLIPBOARD: + selection_to_clipboard(seat, term, serial); + break; + + case SELECTION_TARGET_BOTH: + selection_to_primary(seat, term, serial); + selection_to_clipboard(seat, term, serial); + break; + } +} + +static bool +unmark_selected(struct terminal *term, struct row *row, struct cell *cell, + int row_no, int col, void *data) +{ + if (!cell->attrs.selected) + return true; + + row->dirty = true; + cell->attrs.selected = false; + cell->attrs.clean = false; + return true; +} + +void +selection_cancel(struct terminal *term) +{ + LOG_DBG("selection cancelled: start = %d,%d end = %d,%d", + term->selection.coords.start.row, term->selection.coords.start.col, + term->selection.coords.end.row, term->selection.coords.end.col); + + selection_stop_scroll_timer(term); + + if (term->selection.coords.start.row >= 0 && term->selection.coords.end.row >= 0) { + foreach_selected( + term, term->selection.coords.start, term->selection.coords.end, + &unmark_selected, NULL); + render_refresh(term); + } + + term->selection.kind = SELECTION_NONE; + term->selection.coords.start = (struct coord){-1, -1}; + term->selection.coords.end = (struct coord){-1, -1}; + term->selection.pivot.start = (struct coord){-1, -1}; + term->selection.pivot.end = (struct coord){-1, -1}; + term->selection.direction = SELECTION_UNDIR; + term->selection.ongoing = false; + + search_selection_cancelled(term); +} + +bool +selection_clipboard_has_data(const struct seat *seat) +{ + return seat->clipboard.data_offer != NULL; +} + +bool +selection_primary_has_data(const struct seat *seat) +{ + return seat->primary.data_offer != NULL; +} + +void +selection_clipboard_unset(struct seat *seat) +{ + struct wl_clipboard *clipboard = &seat->clipboard; + + if (clipboard->data_source == NULL) + return; + + /* Kill previous data source */ + xassert(clipboard->serial != 0); + wl_data_device_set_selection(seat->data_device, NULL, clipboard->serial); + wl_data_source_destroy(clipboard->data_source); + + clipboard->data_source = NULL; + clipboard->serial = 0; + + free(clipboard->text); + clipboard->text = NULL; +} + +void +selection_primary_unset(struct seat *seat) +{ + struct wl_primary *primary = &seat->primary; + + if (primary->data_source == NULL) + return; + + xassert(primary->serial != 0); + zwp_primary_selection_device_v1_set_selection( + seat->primary_selection_device, NULL, primary->serial); + zwp_primary_selection_source_v1_destroy(primary->data_source); + + primary->data_source = NULL; + primary->serial = 0; + + free(primary->text); + primary->text = NULL; +} + +static bool +fdm_scroll_timer(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + + uint64_t expiration_count; + ssize_t ret = read( + term->selection.auto_scroll.fd, + &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read selection scroll timer"); + return false; + } + + switch (term->selection.auto_scroll.direction) { + case SELECTION_SCROLL_NOT: + return true; + + case SELECTION_SCROLL_UP: + cmd_scrollback_up(term, expiration_count); + selection_update(term, term->selection.auto_scroll.col, 0); + break; + + case SELECTION_SCROLL_DOWN: + cmd_scrollback_down(term, expiration_count); + selection_update(term, term->selection.auto_scroll.col, term->rows - 1); + break; + } + + + return true; +} + +void +selection_start_scroll_timer(struct terminal *term, int interval_ns, + enum selection_scroll_direction direction, int col) +{ + xassert(direction != SELECTION_SCROLL_NOT); + + if (!term->selection.ongoing) + return; + + if (term->selection.auto_scroll.fd < 0) { + int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd < 0) { + LOG_ERRNO("failed to create selection scroll timer"); + goto err; + } + + if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_scroll_timer, term)) { + close(fd); + return; + } + + term->selection.auto_scroll.fd = fd; + } + + struct itimerspec timer; + if (timerfd_gettime(term->selection.auto_scroll.fd, &timer) < 0) { + LOG_ERRNO("failed to get current selection scroll timer value"); + goto err; + } + + if (timer.it_value.tv_sec == 0 && timer.it_value.tv_nsec == 0) + timer.it_value.tv_nsec = 1; + + timer.it_interval.tv_sec = interval_ns / 1000000000; + timer.it_interval.tv_nsec = interval_ns % 1000000000; + + if (timerfd_settime(term->selection.auto_scroll.fd, 0, &timer, NULL) < 0) { + LOG_ERRNO("failed to set new selection scroll timer value"); + goto err; + } + + term->selection.auto_scroll.direction = direction; + term->selection.auto_scroll.col = col; + return; + +err: + selection_stop_scroll_timer(term); + return; +} + +void +selection_stop_scroll_timer(struct terminal *term) +{ + if (term->selection.auto_scroll.fd < 0) { + xassert(term->selection.auto_scroll.direction == SELECTION_SCROLL_NOT); + return; + } + + fdm_del(term->fdm, term->selection.auto_scroll.fd); + term->selection.auto_scroll.fd = -1; + term->selection.auto_scroll.direction = SELECTION_SCROLL_NOT; +} + +static void +target(void *data, struct wl_data_source *wl_data_source, const char *mime_type) +{ + LOG_DBG("TARGET: mime-type=%s", mime_type); +} + +struct clipboard_send { + char *data; + size_t len; + size_t idx; +}; + +static bool +fdm_send(struct fdm *fdm, int fd, int events, void *data) +{ + struct clipboard_send *ctx = data; + + if (events & EPOLLHUP) + goto done; + + switch (async_write(fd, ctx->data, ctx->len, &ctx->idx)) { + case ASYNC_WRITE_REMAIN: + return true; + + case ASYNC_WRITE_DONE: + break; + + case ASYNC_WRITE_ERR: + LOG_ERRNO( + "failed to asynchronously write %zu of selection data to FD=%d", + ctx->len - ctx->idx, fd); + break; + } + +done: + fdm_del(fdm, fd); + free(ctx->data); + free(ctx); + return true; +} + +static void +send_clipboard_or_primary(struct seat *seat, int fd, const char *selection, + const char *source_name) +{ + /* Make it NONBLOCK:ing right away - we don't want to block if the + * initial attempt to send the data synchronously fails */ + int flags; + if ((flags = fcntl(fd, F_GETFL)) < 0 || + fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) + { + LOG_ERRNO("failed to set O_NONBLOCK"); + return; + } + + size_t len = selection != NULL ? strlen(selection) : 0; + size_t async_idx = 0; + + switch (async_write(fd, selection, len, &async_idx)) { + case ASYNC_WRITE_REMAIN: { + struct clipboard_send *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct clipboard_send) { + .data = xstrdup(&selection[async_idx]), + .len = len - async_idx, + .idx = 0, + }; + + if (fdm_add(seat->wayl->fdm, fd, EPOLLOUT, &fdm_send, ctx)) + return; + + free(ctx->data); + free(ctx); + break; + } + + case ASYNC_WRITE_DONE: + break; + + case ASYNC_WRITE_ERR: + LOG_ERRNO("failed write %zu bytes of %s selection data to FD=%d", + len, source_name, fd); + break; + } + + close(fd); +} + +static void +send(void *data, struct wl_data_source *wl_data_source, const char *mime_type, + int32_t fd) +{ + struct seat *seat = data; + const struct wl_clipboard *clipboard = &seat->clipboard; + + send_clipboard_or_primary(seat, fd, clipboard->text, "clipboard"); +} + +static void +cancelled(void *data, struct wl_data_source *wl_data_source) +{ + struct seat *seat = data; + struct wl_clipboard *clipboard = &seat->clipboard; + xassert(clipboard->data_source == wl_data_source); + + wl_data_source_destroy(clipboard->data_source); + clipboard->data_source = NULL; + clipboard->serial = 0; + + free(clipboard->text); + clipboard->text = NULL; +} + +/* We don't support dragging *from* */ +static void +dnd_drop_performed(void *data, struct wl_data_source *wl_data_source) +{ + //LOG_DBG("DnD drop performed"); +} + +static void +dnd_finished(void *data, struct wl_data_source *wl_data_source) +{ + //LOG_DBG("DnD finished"); +} + +static void +action(void *data, struct wl_data_source *wl_data_source, uint32_t dnd_action) +{ + //LOG_DBG("DnD action: %u", dnd_action); +} + +static const struct wl_data_source_listener data_source_listener = { + .target = &target, + .send = &send, + .cancelled = &cancelled, + .dnd_drop_performed = &dnd_drop_performed, + .dnd_finished = &dnd_finished, + .action = &action, +}; + +static void +primary_send(void *data, + struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1, + const char *mime_type, int32_t fd) +{ + struct seat *seat = data; + const struct wl_primary *primary = &seat->primary; + + send_clipboard_or_primary(seat, fd, primary->text, "primary"); +} + +static void +primary_cancelled(void *data, + struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1) +{ + struct seat *seat = data; + struct wl_primary *primary = &seat->primary; + + zwp_primary_selection_source_v1_destroy(primary->data_source); + primary->data_source = NULL; + primary->serial = 0; + + free(primary->text); + primary->text = NULL; +} + +static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener = { + .send = &primary_send, + .cancelled = &primary_cancelled, +}; + +bool +text_to_clipboard(struct seat *seat, struct terminal *term, char *text, uint32_t serial) +{ + xassert(serial != 0); + + struct wl_clipboard *clipboard = &seat->clipboard; + + if (clipboard->data_source != NULL) { + /* Kill previous data source */ + xassert(clipboard->serial != 0); + wl_data_device_set_selection(seat->data_device, NULL, clipboard->serial); + wl_data_source_destroy(clipboard->data_source); + free(clipboard->text); + + clipboard->data_source = NULL; + clipboard->serial = 0; + clipboard->text = NULL; + } + + clipboard->data_source + = wl_data_device_manager_create_data_source(term->wl->data_device_manager); + + if (clipboard->data_source == NULL) { + LOG_ERR("failed to create clipboard data source"); + return false; + } + + clipboard->text = text; + + /* Configure source */ + wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8]); + wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_PLAIN]); + wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_TEXT]);; + wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_STRING]); + wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8_STRING]); + + wl_data_source_add_listener(clipboard->data_source, &data_source_listener, seat); + wl_data_device_set_selection(seat->data_device, clipboard->data_source, serial); + + /* Needed when sending the selection to other client */ + clipboard->serial = serial; + return true; +} + +void +selection_to_clipboard(struct seat *seat, struct terminal *term, uint32_t serial) +{ + if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) + return; + + /* Get selection as a string */ + char *text = selection_to_text(term); + if (!text_to_clipboard(seat, term, text, serial)) + free(text); +} + +struct clipboard_receive { + int read_fd; + int timeout_fd; + struct itimerspec timeout; + bool bracketed; + bool no_strip; + bool quote_paths; + + void (*decoder)(struct clipboard_receive *ctx, char *data, size_t size); + void (*finish)(struct clipboard_receive *ctx); + + /* URI state */ + bool add_space; + struct { + char *data; + size_t sz; + size_t idx; + } buf; + + /* Callback data */ + void (*cb)(char *data, size_t size, void *user); + void (*done)(void *user); + void *user; +}; + +static void +clipboard_receive_done(struct fdm *fdm, struct clipboard_receive *ctx) +{ + fdm_del(fdm, ctx->timeout_fd); + fdm_del(fdm, ctx->read_fd); + ctx->done(ctx->user); + free(ctx->buf.data); + free(ctx); +} + +static bool +fdm_receive_timeout(struct fdm *fdm, int fd, int events, void *data) +{ + struct clipboard_receive *ctx = data; + if (events & EPOLLHUP) + return false; + + xassert(events & EPOLLIN); + + uint64_t expire_count; + ssize_t ret = read(fd, &expire_count, sizeof(expire_count)); + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read clipboard timeout timer"); + return false; + } + + LOG_WARN("no data received from clipboard in %llu seconds, aborting", + (unsigned long long)ctx->timeout.it_value.tv_sec); + + clipboard_receive_done(fdm, ctx); + return true; +} + +static void +fdm_receive_decoder_plain(struct clipboard_receive *ctx, char *data, size_t size) +{ + ctx->cb(data, size, ctx->user); +} + +static void +fdm_receive_finish_plain(struct clipboard_receive *ctx) +{ +} + +static bool +decode_one_uri(struct clipboard_receive *ctx, char *uri, size_t len) +{ + LOG_DBG("URI: \"%.*s\"", (int)len, uri); + + if (len == 0) + return false; + + char *scheme, *host, *path; + if (!uri_parse(uri, len, &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL)) { + LOG_ERR("drag-and-drop: invalid URI: %.*s", (int)len, uri); + return false; + } + + if (ctx->add_space) + ctx->cb(" ", 1, ctx->user); + ctx->add_space = true; + + if (streq(scheme, "file") && hostname_is_localhost(host)) { + if (ctx->quote_paths) + ctx->cb("'", 1, ctx->user); + + ctx->cb(path, strlen(path), ctx->user); + + if (ctx->quote_paths) + ctx->cb("'", 1, ctx->user); + } else + ctx->cb(uri, len, ctx->user); + + free(scheme); + free(host); + free(path); + return true; +} + +static void +fdm_receive_decoder_uri(struct clipboard_receive *ctx, char *data, size_t size) +{ + while (ctx->buf.idx + size > ctx->buf.sz) { + size_t new_sz = ctx->buf.sz == 0 ? size : 2 * ctx->buf.sz; + ctx->buf.data = xrealloc(ctx->buf.data, new_sz); + ctx->buf.sz = new_sz; + } + + memcpy(&ctx->buf.data[ctx->buf.idx], data, size); + ctx->buf.idx += size; + + char *start = ctx->buf.data; + char *end = NULL; + + while (true) { + for (end = start; end < &ctx->buf.data[ctx->buf.idx]; end++) { + if (*end == '\r' || *end == '\n') + break; + } + + if (end >= &ctx->buf.data[ctx->buf.idx]) + break; + + decode_one_uri(ctx, start, end - start); + start = end + 1; + } + + const size_t ofs = start - ctx->buf.data; + const size_t left = ctx->buf.idx - ofs; + + memmove(&ctx->buf.data[0], &ctx->buf.data[ofs], left); + ctx->buf.idx = left; +} + +static void +fdm_receive_finish_uri(struct clipboard_receive *ctx) +{ + LOG_DBG("finish: %.*s", (int)ctx->buf.idx, ctx->buf.data); + decode_one_uri(ctx, ctx->buf.data, ctx->buf.idx); +} + +static bool +fdm_receive(struct fdm *fdm, int fd, int events, void *data) +{ + struct clipboard_receive *ctx = data; + const bool no_strip = ctx->no_strip; + const bool bracketed = ctx->bracketed; + + if ((events & EPOLLHUP) && !(events & EPOLLIN)) + goto done; + + /* Reset timeout timer */ + if (timerfd_settime(ctx->timeout_fd, 0, &ctx->timeout, NULL) < 0) { + LOG_ERRNO("failed to re-arm clipboard timeout timer"); + return false; + } + + /* Read until EOF */ + while (true) { + char text[256]; + ssize_t count = read(fd, text, sizeof(text)); + + if (count == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return true; + + LOG_ERRNO("failed to read clipboard data"); + break; + } + + if (count == 0) + break; + + /* + * Call cb while at same time replace: + * - \r\n -> \r (non-bracketed paste) + * - \n -> \r (non-bracketed paste) + * - C0 -> (strip non-formatting C0 characters) + * - \e -> (i.e. strip ESC) + */ + char *p = text; + size_t left = count; + +#define skip_one() \ + do { \ + ctx->decoder(ctx, p, i); \ + xassert(i + 1 <= left); \ + p += i + 1; \ + left -= i + 1; \ + } while (0) + + again: + for (size_t i = 0; i < left; i++) { + switch (p[i]) { + default: + break; + + case '\n': + if (!no_strip && !bracketed) { + p[i] = '\r'; + } + break; + + case '\r': + /* Convert \r\n -> \r */ + if (!no_strip && !bracketed && i + 1 < left && p[i + 1] == '\n') { + i++; + skip_one(); + goto again; + } + break; + + /* C0 non-formatting control characters (\b \t \n \r excluded) */ + case '\x01': case '\x02': case '\x03': case '\x04': case '\x05': + case '\x06': case '\x07': case '\x0e': case '\x0f': case '\x10': + case '\x11': case '\x12': case '\x13': case '\x14': case '\x15': + case '\x16': case '\x17': case '\x18': case '\x19': case '\x1a': + case '\x1b': case '\x1c': case '\x1d': case '\x1e': case '\x1f': + if (!no_strip) { + skip_one(); + goto again; + } + break; + + /* + * In addition to stripping non-formatting C0 controls, + * XTerm has an option, "disallowedPasteControls", that + * defines C0 controls that will be replaced with spaces + * when pasted. + * + * It's default value is BS,DEL,ENQ,EOT,NUL + * + * Instead of replacing them with spaces, we allow them in + * bracketed paste mode, and strip them completely in + * non-bracketed mode. + * + * Note some of the (default) XTerm controls are already + * handled above. + */ + case '\b': case '\x7f': case '\x00': + if (!no_strip && !bracketed) { + skip_one(); + goto again; + } + break; + } + } + + ctx->decoder(ctx, p, left); + left = 0; + } + +#undef skip_one + +done: + ctx->finish(ctx); + clipboard_receive_done(fdm, ctx); + return true; +} + +static void +begin_receive_clipboard(struct terminal *term, bool no_strip, + int read_fd, enum data_offer_mime_type mime_type, + void (*cb)(char *data, size_t size, void *user), + void (*done)(void *user), void *user) +{ + int timeout_fd = -1; + struct clipboard_receive *ctx = NULL; + + int flags; + if ((flags = fcntl(read_fd, F_GETFL)) < 0 || + fcntl(read_fd, F_SETFL, flags | O_NONBLOCK) < 0) + { + LOG_ERRNO("failed to set O_NONBLOCK"); + goto err; + } + + timeout_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); + if (timeout_fd < 0) { + LOG_ERRNO("failed to create clipboard timeout timer FD"); + goto err; + } + + const struct itimerspec timeout = {.it_value = {.tv_sec = 2}}; + if (timerfd_settime(timeout_fd, 0, &timeout, NULL) < 0) { + LOG_ERRNO("failed to arm clipboard timeout timer"); + goto err; + } + + ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct clipboard_receive) { + .read_fd = read_fd, + .timeout_fd = timeout_fd, + .timeout = timeout, + .bracketed = term->bracketed_paste, + .no_strip = no_strip, + .quote_paths = term->grid == &term->normal, + .decoder = (mime_type == DATA_OFFER_MIME_URI_LIST + ? &fdm_receive_decoder_uri + : &fdm_receive_decoder_plain), + .finish = (mime_type == DATA_OFFER_MIME_URI_LIST + ? &fdm_receive_finish_uri + : &fdm_receive_finish_plain), + .cb = cb, + .done = done, + .user = user, + }; + + if (!fdm_add(term->fdm, read_fd, EPOLLIN, &fdm_receive, ctx) || + !fdm_add(term->fdm, timeout_fd, EPOLLIN, &fdm_receive_timeout, ctx)) + { + goto err; + } + + return; + +err: + free(ctx); + fdm_del(term->fdm, timeout_fd); + fdm_del(term->fdm, read_fd); + done(user); +} + +void +text_from_clipboard(struct seat *seat, struct terminal *term, + bool no_strip, + void (*cb)(char *data, size_t size, void *user), + void (*done)(void *user), void *user) +{ + struct wl_clipboard *clipboard = &seat->clipboard; + if (clipboard->data_offer == NULL || + clipboard->mime_type == DATA_OFFER_MIME_UNSET) + { + done(user); + return; + } + + /* Prepare a pipe the other client can write its selection to us */ + int fds[2]; + if (pipe2(fds, O_CLOEXEC) == -1) { + LOG_ERRNO("failed to create pipe"); + done(user); + return; + } + + LOG_DBG("receive from clipboard: mime-type=%s", + mime_type_map[clipboard->mime_type]); + + int read_fd = fds[0]; + int write_fd = fds[1]; + + /* Give write-end of pipe to other client */ + wl_data_offer_receive( + clipboard->data_offer, mime_type_map[clipboard->mime_type], write_fd); + + /* Don't keep our copy of the write-end open (or we'll never get EOF) */ + close(write_fd); + + begin_receive_clipboard( + term, no_strip, read_fd, clipboard->mime_type, cb, done, user); +} + +static void +receive_offer(char *data, size_t size, void *user) +{ + struct terminal *term = user; + xassert(term->is_sending_paste_data); + term_paste_data_to_slave(term, data, size); +} + +static void +receive_offer_done(void *user) +{ + struct terminal *term = user; + + if (term->bracketed_paste) + term_paste_data_to_slave(term, "\033[201~", 6); + + term->is_sending_paste_data = false; + + /* Make sure we send any queued up non-paste data */ + if (tll_length(term->ptmx_buffers) > 0) + fdm_event_add(term->fdm, term->ptmx, EPOLLOUT); +} + +void +selection_from_clipboard(struct seat *seat, struct terminal *term, uint32_t serial) +{ + if (term->is_sending_paste_data) { + /* We're already pasting... */ + return; + } + + struct wl_clipboard *clipboard = &seat->clipboard; + if (clipboard->data_offer == NULL) + return; + + term->is_sending_paste_data = true; + + if (term->bracketed_paste) + term_paste_data_to_slave(term, "\033[200~", 6); + + text_from_clipboard( + seat, term, false, &receive_offer, &receive_offer_done, term); +} + +bool +text_to_primary(struct seat *seat, struct terminal *term, char *text, uint32_t serial) +{ + if (term->wl->primary_selection_device_manager == NULL) + return false; + + xassert(serial != 0); + + struct wl_primary *primary = &seat->primary; + + /* TODO: somehow share code with the clipboard equivalent */ + if (seat->primary.data_source != NULL) { + /* Kill previous data source */ + + xassert(primary->serial != 0); + zwp_primary_selection_device_v1_set_selection( + seat->primary_selection_device, NULL, primary->serial); + zwp_primary_selection_source_v1_destroy(primary->data_source); + free(primary->text); + + primary->data_source = NULL; + primary->serial = 0; + primary->text = NULL; + } + + primary->data_source + = zwp_primary_selection_device_manager_v1_create_source( + term->wl->primary_selection_device_manager); + + if (primary->data_source == NULL) { + LOG_ERR("failed to create clipboard data source"); + return false; + } + + /* Get selection as a string */ + primary->text = text; + + /* Configure source */ + zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8]); + zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_PLAIN]); + zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_TEXT]); + zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_STRING]); + zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8_STRING]); + + zwp_primary_selection_source_v1_add_listener(primary->data_source, &primary_selection_source_listener, seat); + zwp_primary_selection_device_v1_set_selection(seat->primary_selection_device, primary->data_source, serial); + + /* Needed when sending the selection to other client */ + primary->serial = serial; + return true; +} + +void +selection_to_primary(struct seat *seat, struct terminal *term, uint32_t serial) +{ + if (term->wl->primary_selection_device_manager == NULL) + return; + + /* Get selection as a string */ + char *text = selection_to_text(term); + if (!text_to_primary(seat, term, text, serial)) + free(text); +} + +void +text_from_primary( + struct seat *seat, struct terminal *term, bool no_strip, + void (*cb)(char *data, size_t size, void *user), + void (*done)(void *user), void *user) +{ + if (term->wl->primary_selection_device_manager == NULL) { + done(user); + return; + } + + struct wl_primary *primary = &seat->primary; + if (primary->data_offer == NULL || + primary->mime_type == DATA_OFFER_MIME_UNSET) + { + done(user); + return; + } + + /* Prepare a pipe the other client can write its selection to us */ + int fds[2]; + if (pipe2(fds, O_CLOEXEC) == -1) { + LOG_ERRNO("failed to create pipe"); + done(user); + return; + } + + LOG_DBG("receive from primary: mime-type=%s", + mime_type_map[primary->mime_type]); + + int read_fd = fds[0]; + int write_fd = fds[1]; + + /* Give write-end of pipe to other client */ + zwp_primary_selection_offer_v1_receive( + primary->data_offer, mime_type_map[primary->mime_type], write_fd); + + /* Don't keep our copy of the write-end open (or we'll never get EOF) */ + close(write_fd); + + begin_receive_clipboard( + term, no_strip, read_fd, primary->mime_type, cb, done, user); +} + +void +selection_from_primary(struct seat *seat, struct terminal *term) +{ + if (term->wl->primary_selection_device_manager == NULL) + return; + + if (term->is_sending_paste_data) { + /* We're already pasting... */ + return; + } + + struct wl_primary *primary = &seat->primary; + if (primary->data_offer == NULL) + return; + + term->is_sending_paste_data = true; + if (term->bracketed_paste) + term_paste_data_to_slave(term, "\033[200~", 6); + + text_from_primary( + seat, term, false, &receive_offer, &receive_offer_done, term); +} + +static void +select_mime_type_for_offer(const char *_mime_type, + enum data_offer_mime_type *type) +{ + enum data_offer_mime_type mime_type = DATA_OFFER_MIME_UNSET; + + /* Translate offered mime type to our mime type enum */ + for (size_t i = 0; i < ALEN(mime_type_map); i++) { + if (mime_type_map[i] == NULL) + continue; + + if (streq(_mime_type, mime_type_map[i])) { + mime_type = i; + break; + } + } + + LOG_DBG("mime-type: %s -> %s (offered type was %s)", + mime_type_map[*type], mime_type_map[mime_type], _mime_type); + + /* Mime-type transition; if the new mime-type is "better" than + * previously offered types, use the new type */ + + switch (mime_type) { + case DATA_OFFER_MIME_TEXT_PLAIN: + case DATA_OFFER_MIME_TEXT_TEXT: + case DATA_OFFER_MIME_TEXT_STRING: + /* text/plain is our least preferred type. Only use if current + * type is unset */ + switch (*type) { + case DATA_OFFER_MIME_UNSET: + *type = mime_type; + break; + + default: + break; + } + break; + + case DATA_OFFER_MIME_TEXT_UTF8: + case DATA_OFFER_MIME_TEXT_UTF8_STRING: + /* text/plain;charset=utf-8 is preferred over text/plain */ + switch (*type) { + case DATA_OFFER_MIME_UNSET: + case DATA_OFFER_MIME_TEXT_PLAIN: + case DATA_OFFER_MIME_TEXT_TEXT: + case DATA_OFFER_MIME_TEXT_STRING: + *type = mime_type; + break; + + default: + break; + } + break; + + case DATA_OFFER_MIME_URI_LIST: + /* text/uri-list is always used when offered */ + *type = mime_type; + break; + + case DATA_OFFER_MIME_UNSET: + break; + } +} + +static void +data_offer_reset(struct wl_clipboard *clipboard) +{ + if (clipboard->data_offer != NULL) { + wl_data_offer_destroy(clipboard->data_offer); + clipboard->data_offer = NULL; + } + + clipboard->window = NULL; + clipboard->mime_type = DATA_OFFER_MIME_UNSET; +} + +static void +offer(void *data, struct wl_data_offer *wl_data_offer, const char *mime_type) +{ + struct seat *seat = data; + select_mime_type_for_offer(mime_type, &seat->clipboard.mime_type); +} + +static void +source_actions(void *data, struct wl_data_offer *wl_data_offer, + uint32_t source_actions) +{ +#if defined(_DEBUG) && LOG_ENABLE_DBG + char actions_as_string[1024]; + size_t idx = 0; + + actions_as_string[0] = '\0'; + actions_as_string[sizeof(actions_as_string) - 1] = '\0'; + + for (size_t i = 0; i < 31; i++) { + if (((source_actions >> i) & 1) == 0) + continue; + + enum wl_data_device_manager_dnd_action action = 1 << i; + + const char *s = NULL; + + switch (action) { + case WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE: s = NULL; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY: s = "copy"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE: s = "move"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK: s = "ask"; break; + } + + if (s == NULL) + continue; + + strncat(actions_as_string, s, sizeof(actions_as_string) - idx - 1); + idx += strlen(s); + strncat(actions_as_string, ", ", sizeof(actions_as_string) - idx - 1); + idx += 2; + } + + /* Strip trailing ", " */ + if (strlen(actions_as_string) > 2) + actions_as_string[strlen(actions_as_string) - 2] = '\0'; + + LOG_DBG("DnD actions: %s (0x%08x)", actions_as_string, source_actions); +#endif +} + +static void +offer_action(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action) +{ +#if defined(_DEBUG) && LOG_ENABLE_DBG + const char *s = NULL; + + switch (dnd_action) { + case WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE: s = ""; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY: s = "copy"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE: s = "move"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK: s = "ask"; break; + } + + LOG_DBG("DnD offer action: %s (0x%08x)", s, dnd_action); +#endif +} + +static const struct wl_data_offer_listener data_offer_listener = { + .offer = &offer, + .source_actions = &source_actions, + .action = &offer_action, +}; + +static void +data_offer(void *data, struct wl_data_device *wl_data_device, + struct wl_data_offer *offer) +{ + struct seat *seat = data; + data_offer_reset(&seat->clipboard); + seat->clipboard.data_offer = offer; + wl_data_offer_add_listener(offer, &data_offer_listener, seat); +} + +static void +enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial, + struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y, + struct wl_data_offer *offer) +{ + struct seat *seat = data; + struct wayland *wayl = seat->wayl; + + xassert(offer == seat->clipboard.data_offer); + + if (seat->clipboard.mime_type == DATA_OFFER_MIME_UNSET) + goto reject_offer; + + /* Remember _which_ terminal the current DnD offer is targeting */ + xassert(seat->clipboard.window == NULL); + tll_foreach(wayl->terms, it) { + if (term_surface_kind(it->item, surface) == TERM_SURF_GRID && + !it->item->is_sending_paste_data) + { + wl_data_offer_accept( + offer, serial, mime_type_map[seat->clipboard.mime_type]); + wl_data_offer_set_actions( + offer, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY); + + seat->clipboard.window = it->item->window; + return; + } + } + +reject_offer: + /* Either terminal is already busy sending paste data, or mouse + * pointer isn't over the grid */ + seat->clipboard.window = NULL; + wl_data_offer_accept(offer, serial, NULL); + wl_data_offer_set_actions( + offer, + WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE, + WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE); +} + +static void +leave(void *data, struct wl_data_device *wl_data_device) +{ + struct seat *seat = data; + seat->clipboard.window = NULL; +} + +static void +motion(void *data, struct wl_data_device *wl_data_device, uint32_t time, + wl_fixed_t x, wl_fixed_t y) +{ +} + +struct dnd_context { + struct terminal *term; + struct wl_data_offer *data_offer; +}; + +static void +receive_dnd(char *data, size_t size, void *user) +{ + struct dnd_context *ctx = user; + receive_offer(data, size, ctx->term); +} + +static void +receive_dnd_done(void *user) +{ + struct dnd_context *ctx = user; + + wl_data_offer_finish(ctx->data_offer); + wl_data_offer_destroy(ctx->data_offer); + receive_offer_done(ctx->term); + free(ctx); +} + +static void +drop(void *data, struct wl_data_device *wl_data_device) +{ + struct seat *seat = data; + + xassert(seat->clipboard.window != NULL); + struct terminal *term = seat->clipboard.window->term; + + struct wl_clipboard *clipboard = &seat->clipboard; + + if (clipboard->mime_type == DATA_OFFER_MIME_UNSET) { + LOG_WARN("compositor called data_device::drop() " + "even though we rejected the drag-and-drop"); + return; + } + + struct dnd_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct dnd_context){ + .term = term, + .data_offer = clipboard->data_offer, + }; + + /* Prepare a pipe the other client can write its selection to us */ + int fds[2]; + if (pipe2(fds, O_CLOEXEC) == -1) { + LOG_ERRNO("failed to create pipe"); + free(ctx); + return; + } + + int read_fd = fds[0]; + int write_fd = fds[1]; + + LOG_DBG("DnD drop: mime-type=%s", mime_type_map[clipboard->mime_type]); + + /* Give write-end of pipe to other client */ + wl_data_offer_receive( + clipboard->data_offer, mime_type_map[clipboard->mime_type], write_fd); + + /* Don't keep our copy of the write-end open (or we'll never get EOF) */ + close(write_fd); + + term->is_sending_paste_data = true; + + if (term->bracketed_paste) + term_paste_data_to_slave(term, "\033[200~", 6); + + begin_receive_clipboard( + term, false, read_fd, clipboard->mime_type, + &receive_dnd, &receive_dnd_done, ctx); + + /* data offer is now "owned" by the receive context */ + clipboard->data_offer = NULL; + clipboard->mime_type = DATA_OFFER_MIME_UNSET; +} + +static void +selection(void *data, struct wl_data_device *wl_data_device, + struct wl_data_offer *offer) +{ + /* Selection offer from other client */ + struct seat *seat = data; + if (offer == NULL) + data_offer_reset(&seat->clipboard); + else + xassert(offer == seat->clipboard.data_offer); +} + +const struct wl_data_device_listener data_device_listener = { + .data_offer = &data_offer, + .enter = &enter, + .leave = &leave, + .motion = &motion, + .drop = &drop, + .selection = &selection, +}; + +static void +primary_offer(void *data, + struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer, + const char *mime_type) +{ + LOG_DBG("primary offer: %s", mime_type); + struct seat *seat = data; + select_mime_type_for_offer(mime_type, &seat->primary.mime_type); +} + +static const struct zwp_primary_selection_offer_v1_listener primary_selection_offer_listener = { + .offer = &primary_offer, +}; + +static void +primary_offer_reset(struct wl_primary *primary) +{ + if (primary->data_offer != NULL) { + zwp_primary_selection_offer_v1_destroy(primary->data_offer); + primary->data_offer = NULL; + } + + primary->mime_type = DATA_OFFER_MIME_UNSET; +} + +static void +primary_data_offer(void *data, + struct zwp_primary_selection_device_v1 *zwp_primary_selection_device, + struct zwp_primary_selection_offer_v1 *offer) +{ + struct seat *seat = data; + primary_offer_reset(&seat->primary); + seat->primary.data_offer = offer; + zwp_primary_selection_offer_v1_add_listener( + offer, &primary_selection_offer_listener, seat); +} + +static void +primary_selection(void *data, + struct zwp_primary_selection_device_v1 *zwp_primary_selection_device, + struct zwp_primary_selection_offer_v1 *offer) +{ + /* Selection offer from other client, for primary */ + + struct seat *seat = data; + if (offer == NULL) + primary_offer_reset(&seat->primary); + else + xassert(seat->primary.data_offer == offer); +} + +const struct zwp_primary_selection_device_v1_listener primary_selection_device_listener = { + .data_offer = &primary_data_offer, + .selection = &primary_selection, +}; + diff --git a/selection.h b/selection.h new file mode 100644 index 0000000..b6ad099 --- /dev/null +++ b/selection.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include + +#include "terminal.h" + +extern const struct wl_data_device_listener data_device_listener; +extern const struct zwp_primary_selection_device_v1_listener primary_selection_device_listener; + +void selection_start( + struct terminal *term, int col, int row, + enum selection_kind new_kind, bool spaces_only); +void selection_update(struct terminal *term, int col, int row); +void selection_finalize( + struct seat *seat, struct terminal *term, uint32_t serial); +void selection_dirty_cells(struct terminal *term); +void selection_cancel(struct terminal *term); +void selection_extend( + struct seat *seat, struct terminal *term, + int col, int row, enum selection_kind kind); + +bool selection_on_rows(const struct terminal *term, int start, int end); + +void selection_scroll_up(struct terminal *term, int rows); +void selection_scroll_down(struct terminal *term, int rows); +void selection_view_up(struct terminal *term, int new_view); +void selection_view_down(struct terminal *term, int new_view); + +void selection_clipboard_unset(struct seat *seat); +void selection_primary_unset(struct seat *seat); + +bool selection_clipboard_has_data(const struct seat *seat); +bool selection_primary_has_data(const struct seat *seat); + +char *selection_to_text(const struct terminal *term); +void selection_to_clipboard( + struct seat *seat, struct terminal *term, uint32_t serial); +void selection_from_clipboard( + struct seat *seat, struct terminal *term, uint32_t serial); +void selection_to_primary( + struct seat *seat, struct terminal *term, uint32_t serial); +void selection_from_primary(struct seat *seat, struct terminal *term); + +/* Copy text *to* primary/clipboard */ +bool text_to_clipboard( + struct seat *seat, struct terminal *term, char *text, uint32_t serial); +bool text_to_primary( + struct seat *seat, struct terminal *term, char *text, uint32_t serial); + +/* + * Copy text *from* primary/clipboard + * + * Note that these are asynchronous; they *will* return + * immediately. The 'cb' callback will be called 0..n times with + * clipboard data. When done (or on error), the 'done' callback is + * called. + * + * As such, keep this in mind: + * - The 'user' context must not be stack allocated + * - Don't expect clipboard data to have been received when these + * functions return (it will *never* have been received at this + * point). + */ +void text_from_clipboard( + struct seat *seat, struct terminal *term, bool no_strip, + void (*cb)(char *data, size_t size, void *user), + void (*done)(void *user), void *user); + +void text_from_primary( + struct seat *seat, struct terminal *term, bool no_strip, + void (*cb)(char *data, size_t size, void *user), + void (*dont)(void *user), void *user); + +void selection_start_scroll_timer( + struct terminal *term, int interval_ns, + enum selection_scroll_direction direction, int col); +void selection_stop_scroll_timer(struct terminal *term); + +void selection_find_word_boundary_left( + const struct terminal *term, struct coord *pos, bool spaces_only); +void selection_find_word_boundary_right( + const struct terminal *term, struct coord *pos, bool spaces_only, + bool stop_on_space_to_word_boundary); + +struct coord selection_get_start(const struct terminal *term); +struct coord selection_get_end(const struct terminal *term); diff --git a/server.c b/server.c new file mode 100644 index 0000000..2596332 --- /dev/null +++ b/server.c @@ -0,0 +1,690 @@ +#include "server.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#define LOG_MODULE "server" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "client-protocol.h" +#include "terminal.h" +#include "util.h" +#include "wayland.h" +#include "xmalloc.h" + +#define NON_ZERO_OPT (INT_MIN / 7) + +struct client; +struct terminal_instance; + +struct server { + struct config *conf; + struct fdm *fdm; + struct reaper *reaper; + struct wayland *wayl; + + int fd; + const char *sock_path; + + tll(struct client *) clients; + tll(struct terminal_instance *) terminals; +}; + +struct client { + struct server *server; + int fd; + + struct { + uint8_t *data; + size_t left; + size_t idx; + } buffer; + + struct terminal_instance *instance; +}; +static void client_destroy(struct client *client); + +struct terminal_instance { + struct terminal *terminal; + struct server *server; + struct client *client; + struct config *conf; +}; +static void instance_destroy(struct terminal_instance *instance, int exit_code); + +static void +client_destroy(struct client *client) +{ + if (client == NULL) + return; + + if (client->instance != NULL) { + LOG_WARN("client FD=%d: terminal still alive", client->fd); + client->instance->client = NULL; + instance_destroy(client->instance, 1); + } + + if (client->fd != -1) { + LOG_DBG("client FD=%d: disconnected", client->fd); + fdm_del(client->server->fdm, client->fd); + } + + tll_foreach(client->server->clients, it) { + if (it->item == client) { + tll_remove(client->server->clients, it); + break; + } + } + + free(client->buffer.data); + free(client); +} + +static void +client_send_exit_code(struct client *client, int exit_code) +{ + if (client->fd == -1) + return; + + if (write(client->fd, &exit_code, sizeof(exit_code)) != sizeof(exit_code)) + LOG_ERRNO("failed to write slave exit code to client"); +} + +static void +instance_destroy(struct terminal_instance *instance, int exit_code) +{ + if (instance->terminal != NULL) + term_destroy(instance->terminal); + + tll_foreach(instance->server->terminals, it) { + if (it->item == instance) { + tll_remove(instance->server->terminals, it); + break; + } + } + + if (instance->client != NULL) { + instance->client->instance = NULL; + client_send_exit_code(instance->client, exit_code); + client_destroy(instance->client); + } + + /* TODO: clone server conf completely, so that we can just call + * conf_destroy() here */ + if (instance->conf != NULL) { + config_free(instance->conf); + free(instance->conf); + } + free(instance); + +} + +static void +term_shutdown_handler(void *data, int exit_code) +{ + struct terminal_instance *instance = data; + + instance->terminal = NULL; + instance_destroy(instance, exit_code); +} + +static bool +fdm_client(struct fdm *fdm, int fd, int events, void *data) +{ + struct client *client = data; + struct server *server = client->server; + + char **argv = NULL; + config_override_t overrides = tll_init(); + char **envp = NULL; + + if (events & EPOLLHUP) + goto shutdown; + + xassert(events & EPOLLIN); + + if (client->instance != NULL) { + struct client_ipc_hdr ipc_hdr; + ssize_t count = read(fd, &ipc_hdr, sizeof(ipc_hdr)); + + if (count != sizeof(ipc_hdr)) { + LOG_WARN("client unexpectedly sent %zd bytes", count); + return true; /* TODO: shutdown instead? */ + } + + switch (ipc_hdr.ipc_code) { + case FOOT_IPC_SIGUSR: { + xassert(ipc_hdr.size == sizeof(struct client_ipc_sigusr)); + + struct client_ipc_sigusr sigusr; + count = read(fd, &sigusr, sizeof(sigusr)); + if (count < 0) { + LOG_ERRNO("failed to read SIGUSR IPC data from client"); + return true; /* TODO: shutdown instead? */ + } + + if ((size_t)count != sizeof(sigusr)) { + LOG_ERR("failed to read SIGUSR IPC data from client"); + return true; /* TODO: shutdown instead? */ + } + + switch (sigusr.signo) { + case SIGUSR1: + term_theme_switch_to_dark(client->instance->terminal); + break; + + case SIGUSR2: + term_theme_switch_to_light(client->instance->terminal); + break; + + default: + LOG_ERR( + "client sent bad SIGUSR number: %d " + "(expected SIGUSR1=%d or SIGUSR2=%d)", + sigusr.signo, SIGUSR1, SIGUSR2); + break; + } + + return true; + } + + default: + LOG_WARN( + "client sent unrecognized IPC (0x%04x), ignoring %hhu bytes", + ipc_hdr.ipc_code, ipc_hdr.size); + + /* TODO: slightly broken, since not all data is guaranteed + to be readable yet */ + uint8_t dummy[ipc_hdr.size]; + (void)!!read(fd, dummy, ipc_hdr.size); + return true; + } + } + + if (client->buffer.data == NULL) { + /* + * We haven't received any data yet - the first thing the + * client sends is the total size of the initialization + * data. + */ + uint32_t total_len; + ssize_t count = recv(fd, &total_len, sizeof(total_len), 0); + if (count < 0) { + LOG_ERRNO("failed to read total length"); + goto shutdown; + } + + if (count != sizeof(total_len)) { + LOG_ERR("client did not send setup packet size"); + goto shutdown; + } + + const uint32_t max_size = 128 * 1024; + if (total_len > max_size) { + LOG_ERR("client wants to send too large setup packet (%u > %u)", + total_len, max_size); + goto shutdown; + } + + LOG_DBG("total len: %u", total_len); + client->buffer.data = xmalloc(total_len + 1); + client->buffer.left = total_len; + client->buffer.idx = 0; + + /* Prevent our strlen() calls to run outside */ + client->buffer.data[total_len] = '\0'; + return true; /* Let FDM trigger again when we have more data */ + } + + /* Keep filling our buffer of initialization data */ + ssize_t count = recv( + fd, &client->buffer.data[client->buffer.idx], client->buffer.left, 0); + + if (count < 0) { + LOG_ERRNO("failed to read"); + goto shutdown; + } + + client->buffer.idx += count; + client->buffer.left -= count; + + if (client->buffer.left > 0) { + /* Not done yet */ + return true; + } + + if (tll_length(server->wayl->monitors) == 0) { + LOG_ERR("no monitors available for new terminal"); + client_send_exit_code(client, -26); + goto shutdown; + } + + /* All initialization data received - time to instantiate a terminal! */ + + xassert(client->instance == NULL); + xassert(client->buffer.data != NULL); + xassert(client->buffer.left == 0); + + /* + * Parse the received buffer, verifying lengths etc + */ + +#define CHECK_BUF(sz) do { \ + if (p + (sz) > end) \ + goto shutdown; \ + } while (0) + +#define CHECK_BUF_AND_NULL(sz) do { \ + CHECK_BUF(sz); \ + if (sz == 0) \ + goto shutdown; \ + if (p[sz - 1] != '\0') \ + goto shutdown; \ + } while (0) + + uint8_t *p = client->buffer.data; + const uint8_t *end = &client->buffer.data[client->buffer.idx]; + + struct client_data cdata; + CHECK_BUF(sizeof(cdata)); + memcpy(&cdata, p, sizeof(cdata)); + p += sizeof(cdata); + + CHECK_BUF_AND_NULL(cdata.cwd_len); + const char *cwd = (const char *)p; p += cdata.cwd_len; + LOG_DBG("CWD = %.*s", cdata.cwd_len, cwd); + + /* XDGA token */ + const char *token = NULL; + if (cdata.xdga_token) { + + CHECK_BUF_AND_NULL(cdata.token_len); + token = (const char *)p; p += cdata.token_len; + LOG_DBG("XDGA = %.*s", cdata.token_len, token); + } else { + LOG_DBG("No XDGA token"); + } + + /* Overrides */ + for (uint16_t i = 0; i < cdata.override_count; i++) { + struct client_string arg; + CHECK_BUF(sizeof(arg)); + memcpy(&arg, p, sizeof(arg)); p += sizeof(arg); + + CHECK_BUF_AND_NULL(arg.len); + const char *str = (const char *)p; + p += arg.len; + + tll_push_back(overrides, xstrdup(str)); + } + + /* argv */ + argv = xcalloc(cdata.argc + 1, sizeof(argv[0])); + for (uint16_t i = 0; i < cdata.argc; i++) { + struct client_string arg; + CHECK_BUF(sizeof(arg)); + memcpy(&arg, p, sizeof(arg)); p += sizeof(arg); + + CHECK_BUF_AND_NULL(arg.len); + argv[i] = (char *)p; p += arg.len; + LOG_DBG("argv[%hu] = %.*s", i, arg.len, argv[i]); + } + + /* envp */ + envp = cdata.env_count != 0 + ? xcalloc(cdata.env_count + 1, sizeof(envp[0])) + : NULL; + + for (uint16_t i = 0; i < cdata.env_count; i++) { + struct client_string e; + CHECK_BUF(sizeof(e)); + memcpy(&e, p, sizeof(e)); p += sizeof(e); + + CHECK_BUF_AND_NULL(e.len); + envp[i] = (char *)p; p += e.len; + LOG_DBG("env[%hu] = %.*s", i, e.len, envp[i]); + } + +#undef CHECK_BUF_AND_NULL +#undef CHECK_BUF + + struct terminal_instance *instance = xmalloc(sizeof(struct terminal_instance)); + + const bool need_to_clone_conf = + tll_length(overrides)> 0 || + cdata.hold != server->conf->hold_at_exit; + + struct config *conf = NULL; + if (need_to_clone_conf) { + conf = config_clone(server->conf); + + if (cdata.hold != server->conf->hold_at_exit) + conf->hold_at_exit = cdata.hold; + + config_override_apply(conf, &overrides, false); + + if (conf->tweak.font_monospace_warn && conf->fonts[0].count > 0) { + check_if_font_is_monospaced( + conf->fonts[0].arr[0].pattern, + &conf->notifications); + } + } + + *instance = (struct terminal_instance) { + .client = NULL, + .server = server, + .conf = conf, + }; + + instance->terminal = term_init( + conf != NULL ? conf : server->conf, + server->fdm, server->reaper, server->wayl, "footclient", cwd, token, + NULL, cdata.argc, argv, (const char *const *)envp, + &term_shutdown_handler, instance); + + if (instance->terminal == NULL) { + LOG_ERR("failed to instantiate new terminal"); + client_send_exit_code(client, -26); + instance_destroy(instance, -1); + goto shutdown; + } + + if (cdata.no_wait) { + // the server owns the instance + tll_push_back(server->terminals, instance); + client_send_exit_code(client, 0); + goto shutdown; + } else { + // the instance is attached to the client + instance->client = client; + client->instance = instance; + free(argv); + free(envp); + tll_free_and_free(overrides, free); + } + + return true; + +shutdown: + LOG_DBG("client FD=%d: disconnected", client->fd); + + free(argv); + free(envp); + tll_free_and_free(overrides, free); + fdm_del(fdm, fd); + client->fd = -1; + + if (client->instance != NULL && + !client->instance->terminal->shutdown.in_progress) + { + term_shutdown(client->instance->terminal); + } else + client_destroy(client); + + return true; +} + +static bool +fdm_server(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct server *server = data; + + struct sockaddr_un addr; + socklen_t addr_size = sizeof(addr); + int client_fd = accept4( + server->fd, (struct sockaddr *)&addr, &addr_size, SOCK_CLOEXEC | SOCK_NONBLOCK); + + if (client_fd == -1) { + LOG_ERRNO("failed to accept client connection"); + return false; + } + + struct client *client = xmalloc(sizeof(*client)); + *client = (struct client) { + .server = server, + .fd = client_fd, + }; + + if (!fdm_add(server->fdm, client_fd, EPOLLIN, &fdm_client, client)) { + close(client_fd); + free(client); + return false; + } + + LOG_DBG("client FD=%d: connected", client_fd); + tll_push_back(server->clients, client); + return true; +} + +enum connect_status {CONNECT_ERR, CONNECT_FAIL, CONNECT_SUCCESS}; + +static enum connect_status +try_connect(const char *sock_path) +{ + enum connect_status ret = CONNECT_ERR; + + int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (fd == -1) { + LOG_ERRNO("failed to create UNIX socket"); + goto err; + } + + struct sockaddr_un addr = {.sun_family = AF_UNIX}; + strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1); + + switch (connect(fd, (struct sockaddr *)&addr, sizeof(addr))) { + case 0: + ret = CONNECT_SUCCESS; + break; + + case -1: + LOG_DBG("connect() failed: %s", strerror(errno)); + ret = CONNECT_FAIL; + break; + } + +err: + if (fd != -1) + close(fd); + return ret; +} + +static bool +prepare_socket(int fd) +{ + int flags = fcntl(fd, F_GETFD); + if (flags < 0) { + LOG_ERRNO("failed to get file descriptors flag for passed socket"); + return false; + } + if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) { + LOG_ERRNO("failed to set FD_CLOEXEC for passed socket"); + return false; + } + + flags = fcntl(fd, F_GETFL); + if (flags < 0) { + LOG_ERRNO("failed to get file status flags for passed socket"); + return false; + } + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { + LOG_ERRNO("failed to set non-blocking mode on passed socket"); + return false; + } + + int const socket_options[] = { SO_DOMAIN, SO_ACCEPTCONN, SO_TYPE }; + int const socket_options_values[] = { AF_UNIX, NON_ZERO_OPT, SOCK_STREAM}; + char const * const socket_options_names[] = { "SO_DOMAIN", "SO_ACCEPTCONN", "SO_TYPE" }; + + xassert(ALEN(socket_options) == ALEN(socket_options_values)); + xassert(ALEN(socket_options) == ALEN(socket_options_names)); + + int socket_option = 0; + socklen_t len; + for (size_t i = 0; i < ALEN(socket_options) ; i++) { + len = sizeof(socket_option); + if (getsockopt(fd, SOL_SOCKET, socket_options[i], &socket_option, &len) == -1 || + len != sizeof(socket_option)) { + LOG_ERRNO("failed to read socket option from passed file descriptor"); + return false; + } + if (socket_options_values[i] == NON_ZERO_OPT && socket_option) + socket_option = NON_ZERO_OPT; + if (socket_option != socket_options_values[i]) { + LOG_ERR("wrong socket value for socket option '%s' on passed file descriptor", + socket_options_names[i]); + return false; + } + } + + return true; +} + +struct server * +server_init(struct config *conf, struct fdm *fdm, struct reaper *reaper, + struct wayland *wayl) +{ + int fd; + struct server *server = NULL; + const char *sock_path = conf->server_socket_path; + char *end; + + errno = 0; + fd = strtol(sock_path, &end, 10); + if (*end == '\0' && *sock_path != '\0') + { + if (!prepare_socket(fd)) + goto err; + LOG_DBG("we've been started by socket activation, using passed socket"); + sock_path = NULL; + } + else { + LOG_DBG("no suitable pre-existing socket found, creating our own"); + fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (fd == -1) { + LOG_ERRNO("failed to create UNIX socket"); + return NULL; + } + + switch (try_connect(sock_path)) { + case CONNECT_FAIL: + break; + + case CONNECT_SUCCESS: + LOG_ERR("%s is already accepting connections; is 'foot --server' already running", sock_path); + /* FALLTHROUGH */ + + case CONNECT_ERR: + goto err; + } + + unlink(sock_path); + + struct sockaddr_un addr = {.sun_family = AF_UNIX}; + strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1); + + if (bind(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { + LOG_ERRNO("%s: failed to bind", addr.sun_path); + goto err; + } + + if (listen(fd, 0) < 0) { + LOG_ERRNO("%s: failed to listen", addr.sun_path); + goto err; + } + } + + server = malloc(sizeof(*server)); + if (unlikely(server == NULL)) { + LOG_ERRNO("malloc() failed"); + goto err; + } + + *server = (struct server) { + .conf = conf, + .fdm = fdm, + .reaper = reaper, + .wayl = wayl, + + .fd = fd, + .sock_path = sock_path, + + .clients = tll_init(), + .terminals = tll_init(), + }; + + if (!fdm_add(fdm, fd, EPOLLIN, &fdm_server, server)) + goto err; + + LOG_INFO("accepting connections on %s", sock_path != NULL ? sock_path : "socket provided through socket activation"); + + return server; + +err: + free(server); + if (fd != -1) + close(fd); + return NULL; +} + +void +server_destroy(struct server *server) +{ + if (server == NULL) + return; + + LOG_DBG("server destroy, %zu clients still alive", + tll_length(server->clients)); + + tll_foreach(server->clients, it) { + client_send_exit_code(it->item, -26); + client_destroy(it->item); + } + + tll_free(server->clients); + + tll_foreach(server->terminals, it) + instance_destroy(it->item, 1); + + tll_free(server->terminals); + + fdm_del(server->fdm, server->fd); + if (server->sock_path != NULL) + unlink(server->sock_path); + free(server); +} + +void +server_global_theme_switch_to_dark(struct server *server) +{ + server->conf->initial_color_theme = COLOR_THEME_DARK; + tll_foreach(server->clients, it) + term_theme_switch_to_dark(it->item->instance->terminal); + tll_foreach(server->terminals, it) + term_theme_switch_to_dark(it->item->terminal); +} + +void +server_global_theme_switch_to_light(struct server *server) +{ + server->conf->initial_color_theme = COLOR_THEME_LIGHT; + tll_foreach(server->clients, it) + term_theme_switch_to_light(it->item->instance->terminal); + tll_foreach(server->terminals, it) + term_theme_switch_to_light(it->item->terminal); +} diff --git a/server.h b/server.h new file mode 100644 index 0000000..683ad74 --- /dev/null +++ b/server.h @@ -0,0 +1,14 @@ +#pragma once + +#include "fdm.h" +#include "config.h" +#include "reaper.h" +#include "wayland.h" + +struct server; +struct server *server_init(struct config *conf, struct fdm *fdm, + struct reaper *reaper, struct wayland *wayl); +void server_destroy(struct server *server); + +void server_global_theme_switch_to_dark(struct server *server); +void server_global_theme_switch_to_light(struct server *server); diff --git a/shm-formats.h b/shm-formats.h new file mode 100644 index 0000000..a73ba1f --- /dev/null +++ b/shm-formats.h @@ -0,0 +1,138 @@ +#pragma once + +#include + +#if defined(_DEBUG) +static const struct shm_formats { + uint32_t format; + const char *description; +} shm_formats[] = { + {WL_SHM_FORMAT_ARGB8888, "ARGB8888"}, + {WL_SHM_FORMAT_XRGB8888, "XRGB8888"}, + {WL_SHM_FORMAT_C8, "C8"}, + {WL_SHM_FORMAT_RGB332, "RGB332"}, + {WL_SHM_FORMAT_BGR233, "BGR233"}, + {WL_SHM_FORMAT_XRGB4444, "XRGB4444"}, + {WL_SHM_FORMAT_XBGR4444, "XBGR4444"}, + {WL_SHM_FORMAT_RGBX4444, "RGBX4444"}, + {WL_SHM_FORMAT_BGRX4444, "BGRX4444"}, + {WL_SHM_FORMAT_ARGB4444, "ARGB4444"}, + {WL_SHM_FORMAT_ABGR4444, "ABGR4444"}, + {WL_SHM_FORMAT_RGBA4444, "RGBA4444"}, + {WL_SHM_FORMAT_BGRA4444, "BGRA4444"}, + {WL_SHM_FORMAT_XRGB1555, "XRGB1555"}, + {WL_SHM_FORMAT_XBGR1555, "XBGR1555"}, + {WL_SHM_FORMAT_RGBX5551, "RGBX5551"}, + {WL_SHM_FORMAT_BGRX5551, "BGRX5551"}, + {WL_SHM_FORMAT_ARGB1555, "ARGB1555"}, + {WL_SHM_FORMAT_ABGR1555, "ABGR1555"}, + {WL_SHM_FORMAT_RGBA5551, "RGBA5551"}, + {WL_SHM_FORMAT_BGRA5551, "BGRA5551"}, + {WL_SHM_FORMAT_RGB565, "RGB565"}, + {WL_SHM_FORMAT_BGR565, "BGR565"}, + {WL_SHM_FORMAT_RGB888, "RGB888"}, + {WL_SHM_FORMAT_BGR888, "BGR888"}, + {WL_SHM_FORMAT_XBGR8888, "XBGR8888"}, + {WL_SHM_FORMAT_RGBX8888, "RGBX8888"}, + {WL_SHM_FORMAT_BGRX8888, "BGRX8888"}, + {WL_SHM_FORMAT_ABGR8888, "ABGR8888"}, + {WL_SHM_FORMAT_RGBA8888, "RGBA8888"}, + {WL_SHM_FORMAT_BGRA8888, "BGRA8888"}, + {WL_SHM_FORMAT_XRGB2101010, "XRGB2101010"}, + {WL_SHM_FORMAT_XBGR2101010, "XBGR2101010"}, + {WL_SHM_FORMAT_RGBX1010102, "RGBX1010102"}, + {WL_SHM_FORMAT_BGRX1010102, "BGRX1010102"}, + {WL_SHM_FORMAT_ARGB2101010, "ARGB2101010"}, + {WL_SHM_FORMAT_ABGR2101010, "ABGR2101010"}, + {WL_SHM_FORMAT_RGBA1010102, "RGBA1010102"}, + {WL_SHM_FORMAT_BGRA1010102, "BGRA1010102"}, + {WL_SHM_FORMAT_YUYV, "YUYV"}, + {WL_SHM_FORMAT_YVYU, "YVYU"}, + {WL_SHM_FORMAT_UYVY, "UYVY"}, + {WL_SHM_FORMAT_VYUY, "VYUY"}, + {WL_SHM_FORMAT_AYUV, "AYUV"}, + {WL_SHM_FORMAT_NV12, "NV12"}, + {WL_SHM_FORMAT_NV21, "NV21"}, + {WL_SHM_FORMAT_NV16, "NV16"}, + {WL_SHM_FORMAT_NV61, "NV61"}, + {WL_SHM_FORMAT_YUV410, "YUV410"}, + {WL_SHM_FORMAT_YVU410, "YVU410"}, + {WL_SHM_FORMAT_YUV411, "YUV411"}, + {WL_SHM_FORMAT_YVU411, "YVU411"}, + {WL_SHM_FORMAT_YUV420, "YUV420"}, + {WL_SHM_FORMAT_YVU420, "YVU420"}, + {WL_SHM_FORMAT_YUV422, "YUV422"}, + {WL_SHM_FORMAT_YVU422, "YVU422"}, + {WL_SHM_FORMAT_YUV444, "YUV444"}, + {WL_SHM_FORMAT_YVU444, "YVU444"}, + {WL_SHM_FORMAT_R8, "R8"}, + {WL_SHM_FORMAT_R16, "R16"}, + {WL_SHM_FORMAT_RG88, "RG88"}, + {WL_SHM_FORMAT_GR88, "GR88"}, + {WL_SHM_FORMAT_RG1616, "RG1616"}, + {WL_SHM_FORMAT_GR1616, "GR1616"}, + {WL_SHM_FORMAT_XRGB16161616F, "XRGB16161616F"}, + {WL_SHM_FORMAT_XBGR16161616F, "XBGR16161616F"}, + {WL_SHM_FORMAT_ARGB16161616F, "ARGB16161616F"}, + {WL_SHM_FORMAT_ABGR16161616F, "ABGR16161616F"}, + {WL_SHM_FORMAT_XYUV8888, "XYUV8888"}, + {WL_SHM_FORMAT_VUY888, "VUY888"}, + {WL_SHM_FORMAT_VUY101010, "VUY101010"}, + {WL_SHM_FORMAT_Y210, "Y210"}, + {WL_SHM_FORMAT_Y212, "Y212"}, + {WL_SHM_FORMAT_Y216, "Y216"}, + {WL_SHM_FORMAT_Y410, "Y410"}, + {WL_SHM_FORMAT_Y412, "Y412"}, + {WL_SHM_FORMAT_Y416, "Y416"}, + {WL_SHM_FORMAT_XVYU2101010, "XVYU2101010"}, + {WL_SHM_FORMAT_XVYU12_16161616, "XVYU12_16161616"}, + {WL_SHM_FORMAT_XVYU16161616, "XVYU16161616"}, + {WL_SHM_FORMAT_Y0L0, "Y0L0"}, + {WL_SHM_FORMAT_X0L0, "X0L0"}, + {WL_SHM_FORMAT_Y0L2, "Y0L2"}, + {WL_SHM_FORMAT_X0L2, "X0L2"}, + {WL_SHM_FORMAT_YUV420_8BIT, "YUV420_8BIT"}, + {WL_SHM_FORMAT_YUV420_10BIT, "YUV420_10BIT"}, + {WL_SHM_FORMAT_XRGB8888_A8, "XRGB8888_A8"}, + {WL_SHM_FORMAT_XBGR8888_A8, "XBGR8888_A8"}, + {WL_SHM_FORMAT_RGBX8888_A8, "RGBX8888_A8"}, + {WL_SHM_FORMAT_BGRX8888_A8, "BGRX8888_A8"}, + {WL_SHM_FORMAT_RGB888_A8, "RGB888_A8"}, + {WL_SHM_FORMAT_BGR888_A8, "BGR888_A8"}, + {WL_SHM_FORMAT_RGB565_A8, "RGB565_A8"}, + {WL_SHM_FORMAT_BGR565_A8, "BGR565_A8"}, + {WL_SHM_FORMAT_NV24, "NV24"}, + {WL_SHM_FORMAT_NV42, "NV42"}, + {WL_SHM_FORMAT_P210, "P210"}, + {WL_SHM_FORMAT_P010, "P010"}, + {WL_SHM_FORMAT_P012, "P012"}, + {WL_SHM_FORMAT_P016, "P016"}, + {WL_SHM_FORMAT_AXBXGXRX106106106106, "AXBXGXRX106106106106"}, + {WL_SHM_FORMAT_NV15, "NV15"}, + {WL_SHM_FORMAT_Q410, "Q410"}, + {WL_SHM_FORMAT_Q401, "Q401"}, +#if WAYLAND_VERSION_MAJOR > 1 || WAYLAND_VERSION_MINOR >= 20 + {WL_SHM_FORMAT_XRGB16161616, "XRGB16161616"}, + {WL_SHM_FORMAT_XBGR16161616, "XBGR16161616"}, + {WL_SHM_FORMAT_ARGB16161616, "ARGB16161616"}, + {WL_SHM_FORMAT_ABGR16161616, "ABGR16161616"}, +#endif +#if WAYLAND_VERSION_MAJOR > 1 || WAYLAND_VERSION_MINOR >= 23 + {WL_SHM_FORMAT_C1, "C1"}, + {WL_SHM_FORMAT_C2, "C2"}, + {WL_SHM_FORMAT_C4, "C4"}, + {WL_SHM_FORMAT_D1, "D1"}, + {WL_SHM_FORMAT_D2, "D2"}, + {WL_SHM_FORMAT_D4, "D4"}, + {WL_SHM_FORMAT_D8, "D8"}, + {WL_SHM_FORMAT_R1, "R1"}, + {WL_SHM_FORMAT_R2, "R2"}, + {WL_SHM_FORMAT_R4, "R4"}, + {WL_SHM_FORMAT_R10, "R10"}, + {WL_SHM_FORMAT_R12, "R12"}, + {WL_SHM_FORMAT_AVUY8888, "AVUY8888"}, + {WL_SHM_FORMAT_XVUY8888, "XVUY8888"}, + {WL_SHM_FORMAT_P030, "P030"}, +#endif +}; +#endif diff --git a/shm.c b/shm.c new file mode 100644 index 0000000..5c1573a --- /dev/null +++ b/shm.c @@ -0,0 +1,1109 @@ +#include "shm.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#define LOG_MODULE "shm" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "macros.h" +#include "stride.h" +#include "xmalloc.h" + +#if !defined(MAP_UNINITIALIZED) + #define MAP_UNINITIALIZED 0 +#endif + +#if !defined(MFD_NOEXEC_SEAL) + #define MFD_NOEXEC_SEAL 0 +#endif + +#define TIME_SCROLL 0 + +#define FORCED_DOUBLE_BUFFERING 0 + +/* + * Maximum memfd size allowed. + * + * On 64-bit, we could in theory use up to 2GB (wk_shm_create_pool() + * is limited to int32_t), since we never mmap() the entire region. + * + * The compositor is different matter - it needs to mmap() the entire + * range, and *keep* the mapping for as long as is has buffers + * referencing it (thus - always). And if we open multiple terminals, + * then the required address space multiples... + * + * That said, 128TB (the total amount of available user address space + * on 64-bit) is *a lot*; we can fit 67108864 2GB memfds into + * that. But, let's be conservative for now. + * + * On 32-bit the available address space is too small and SHM + * scrolling is disabled. + * + * Note: this is the _default_ size. It can be overridden by calling + * shm_set_max_pool_size(); + */ +static off_t max_pool_size = 512 * 1024 * 1024; + +static bool can_punch_hole = false; +static bool can_punch_hole_initialized = false; + +static size_t min_stride_alignment = 0; + +struct buffer_pool { + int fd; /* memfd */ + struct wl_shm_pool *wl_pool; + + void *real_mmapped; /* Address returned from mmap */ + size_t mmap_size; /* Size of mmap (>= size) */ + + size_t ref_count; +}; + +struct buffer_chain; +struct buffer_private { + struct buffer public; + struct buffer_chain *chain; + + size_t ref_count; + bool busy; /* Owned by compositor */ + + struct buffer_pool *pool; + off_t offset; /* Offset into memfd where data begins */ + size_t size; + + bool scrollable; + + void (*release_cb)(struct buffer *buf, void *data); + void *cb_data; +}; + +struct buffer_chain { + tll(struct buffer_private *) bufs; + struct wl_shm *shm; + size_t pix_instances; + bool scrollable; + + pixman_format_code_t pixman_fmt; + enum wl_shm_format shm_format; + + void (*release_cb)(struct buffer *buf, void *data); + void *cb_data; +}; + +static tll(struct buffer_private *) deferred; + +#undef MEASURE_SHM_ALLOCS +#if defined(MEASURE_SHM_ALLOCS) +static size_t max_alloced = 0; +#endif + +void +shm_set_max_pool_size(off_t _max_pool_size) +{ + max_pool_size = _max_pool_size; +} + +void +shm_set_min_stride_alignment(size_t _min_stride_alignment) +{ + min_stride_alignment = _min_stride_alignment; +} + +static void +buffer_destroy_dont_close(struct buffer *buf) +{ + if (buf->pix != NULL) { + for (size_t i = 0; i < buf->pix_instances; i++) + if (buf->pix[i] != NULL) + pixman_image_unref(buf->pix[i]); + } + + if (buf->wl_buf != NULL) + wl_buffer_destroy(buf->wl_buf); + + free(buf->pix); + buf->pix = NULL; + buf->wl_buf = NULL; + buf->data = NULL; +} + +static void +pool_unref(struct buffer_pool *pool) +{ + if (pool == NULL) + return; + + xassert(pool->ref_count > 0); + pool->ref_count--; + + if (pool->ref_count > 0) + return; + + if (pool->real_mmapped != MAP_FAILED) + munmap(pool->real_mmapped, pool->mmap_size); + if (pool->wl_pool != NULL) + wl_shm_pool_destroy(pool->wl_pool); + if (pool->fd >= 0) + close(pool->fd); + + pool->real_mmapped = MAP_FAILED; + pool->wl_pool = NULL; + pool->fd = -1; + free(pool); +} + +static void +buffer_destroy(struct buffer_private *buf) +{ + buffer_destroy_dont_close(&buf->public); + pool_unref(buf->pool); + buf->pool = NULL; + + for (size_t i = 0; i < buf->public.pix_instances; i++) + pixman_region32_fini(&buf->public.dirty[i]); + free(buf->public.dirty); + free(buf); +} + +static bool +buffer_unref_no_remove_from_chain(struct buffer_private *buf) +{ + xassert(buf->ref_count > 0); + buf->ref_count--; + + if (buf->ref_count > 0) + return false; + + if (buf->busy) + tll_push_back(deferred, buf); + else + buffer_destroy(buf); + return true; +} + +void +shm_fini(void) +{ + LOG_DBG("deferred buffers: %zu", tll_length(deferred)); + + tll_foreach(deferred, it) { + buffer_destroy(it->item); + tll_remove(deferred, it); + } + +#if defined(MEASURE_SHM_ALLOCS) && MEASURE_SHM_ALLOCS + LOG_INFO("max total allocations was: %zu MB", max_alloced / 1024 / 1024); +#endif +} + +static void +buffer_release(void *data, struct wl_buffer *wl_buffer) +{ + struct buffer_private *buffer = data; + + xassert(buffer->public.wl_buf == wl_buffer); + xassert(buffer->busy); + buffer->busy = false; + + if (buffer->ref_count == 0) { + bool found = false; + tll_foreach(deferred, it) { + if (it->item == buffer) { + found = true; + tll_remove(deferred, it); + break; + } + } + + buffer_destroy(buffer); + + xassert(found); + if (!found) + LOG_WARN("deferred delete: buffer not on the 'deferred' list"); + } else { + if (buffer->release_cb != NULL) { + buffer->release_cb(&buffer->public, buffer->cb_data); + } + } +} + +static const struct wl_buffer_listener buffer_listener = { + .release = &buffer_release, +}; + +static size_t +page_size(void) +{ + static size_t size = 0; + if (size == 0) { + long n = sysconf(_SC_PAGE_SIZE); + if (n <= 0) { + LOG_ERRNO("failed to get page size"); + size = 4096; + } else { + size = (size_t)n; + } + } + xassert(size > 0); + return size; +} + +static bool +instantiate_offset(struct buffer_private *buf, off_t new_offset) +{ + xassert(buf->public.data == NULL); + xassert(buf->public.pix == NULL); + xassert(buf->public.wl_buf == NULL); + xassert(buf->pool != NULL); + + const struct buffer_pool *pool = buf->pool; + + void *mmapped = MAP_FAILED; + struct wl_buffer *wl_buf = NULL; + pixman_image_t **pix = xcalloc(buf->public.pix_instances, sizeof(pix[0])); + + mmapped = (uint8_t *)pool->real_mmapped + new_offset; + + wl_buf = wl_shm_pool_create_buffer( + pool->wl_pool, new_offset, + buf->public.width, buf->public.height, buf->public.stride, + buf->chain->shm_format); + + if (wl_buf == NULL) { + LOG_ERR("failed to create SHM buffer"); + goto err; + } + + /* One pixman image for each worker thread (do we really need multiple?) */ + for (size_t i = 0; i < buf->public.pix_instances; i++) { + pix[i] = pixman_image_create_bits_no_clear( + buf->chain->pixman_fmt, + buf->public.width, buf->public.height, + (uint32_t *)mmapped, buf->public.stride); + + if (pix[i] == NULL) { + LOG_ERR("failed to create pixman image"); + goto err; + } + } + + buf->public.data = mmapped; + buf->public.wl_buf = wl_buf; + buf->public.pix = pix; + buf->offset = new_offset; + + wl_buffer_add_listener(wl_buf, &buffer_listener, buf); + return true; + +err: + if (pix != NULL) { + for (size_t i = 0; i < buf->public.pix_instances; i++) + if (pix[i] != NULL) + pixman_image_unref(pix[i]); + } + free(pix); + if (wl_buf != NULL) + wl_buffer_destroy(wl_buf); + + abort(); + return false; +} + +static void NOINLINE +get_new_buffers(struct buffer_chain *chain, size_t count, + int widths[static count], int heights[static count], + struct buffer *bufs[static count], bool immediate_purge) +{ + xassert(count == 1 || !chain->scrollable); + /* + * No existing buffer available. Create a new one by: + * + * 1. open a memory backed "file" with memfd_create() + * 2. mmap() the memory file, to be used by the pixman image + * 3. create a wayland shm buffer for the same memory file + * + * The pixman image and the wayland buffer are now sharing memory. + */ + + int stride[count]; + int sizes[count]; + + size_t total_size = 0; + for (size_t i = 0; i < count; i++) { + stride[i] = stride_for_format_and_width( + chain->pixman_fmt, widths[i]); + + if (min_stride_alignment > 0) { + const size_t m = min_stride_alignment; + stride[i] = (stride[i] + m - 1) / m * m; + } + + xassert(min_stride_alignment == 0 || stride[i] % min_stride_alignment == 0); + sizes[i] = stride[i] * heights[i]; + total_size += sizes[i]; + } + if (total_size == 0) + return; + + int pool_fd = -1; + + void *real_mmapped = MAP_FAILED; + struct wl_shm_pool *wl_pool = NULL; + struct buffer_pool *pool = NULL; + + /* Backing memory for SHM */ +#if defined(MEMFD_CREATE) + /* + * Older kernels reject MFD_NOEXEC_SEAL with EINVAL. Try first + * *with* it, and if that fails, try again *without* it. + */ + errno = 0; + pool_fd = memfd_create( + "foot-wayland-shm-buffer-pool", + MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL); + + if (pool_fd < 0 && errno == EINVAL && MFD_NOEXEC_SEAL != 0) { + pool_fd = memfd_create( + "foot-wayland-shm-buffer-pool", MFD_CLOEXEC | MFD_ALLOW_SEALING); + } + +#elif defined(__FreeBSD__) + // memfd_create on FreeBSD 13 is SHM_ANON without sealing support + pool_fd = shm_open(SHM_ANON, O_RDWR | O_CLOEXEC, 0600); +#else + char name[] = "/tmp/foot-wayland-shm-buffer-pool-XXXXXX"; + pool_fd = mkostemp(name, O_CLOEXEC); + unlink(name); +#endif + if (pool_fd == -1) { + LOG_ERRNO("failed to create SHM backing memory file"); + goto err; + } + + const size_t page_sz = page_size(); + +#if __SIZEOF_POINTER__ == 8 + off_t offset = chain->scrollable && max_pool_size > 0 + ? (max_pool_size / 4) & ~(page_sz - 1) + : 0; + off_t memfd_size = chain->scrollable && max_pool_size > 0 + ? max_pool_size + : total_size; +#else + off_t offset = 0; + off_t memfd_size = total_size; +#endif + + /* Page align */ + memfd_size = (memfd_size + page_sz - 1) & ~(page_sz - 1); + + LOG_DBG("memfd-size: %lu, initial offset: %lu", memfd_size, offset); + + if (ftruncate(pool_fd, memfd_size) == -1) { + LOG_ERRNO("failed to set size of SHM backing memory file"); + goto err; + } + + if (!can_punch_hole_initialized) { + can_punch_hole_initialized = true; +#if __SIZEOF_POINTER__ == 8 && defined(FALLOC_FL_PUNCH_HOLE) + can_punch_hole = fallocate( + pool_fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0, 1) == 0; + + if (!can_punch_hole) { + LOG_WARN( + "fallocate(FALLOC_FL_PUNCH_HOLE) not " + "supported (%s): expect lower performance", strerror(errno)); + } +#else + /* This is mostly to make sure we skip the warning issued + * above */ + can_punch_hole = false; +#endif + } + + if (chain->scrollable && !can_punch_hole) { + offset = 0; + memfd_size = total_size; + chain->scrollable = false; + + /* Page align */ + memfd_size = (memfd_size + page_sz - 1) & ~(page_sz - 1); + + if (ftruncate(pool_fd, memfd_size) < 0) { + LOG_ERRNO("failed to set size of SHM backing memory file"); + goto err; + } + } + + real_mmapped = mmap( + NULL, memfd_size, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_UNINITIALIZED, pool_fd, 0); + + if (real_mmapped == MAP_FAILED) { + LOG_ERRNO("failed to mmap SHM backing memory file"); + goto err; + } + +#if defined(MEMFD_CREATE) + /* Seal file - we no longer allow any kind of resizing */ + /* TODO: wayland mmaps(PROT_WRITE), for some unknown reason, hence we cannot use F_SEAL_FUTURE_WRITE */ + if (fcntl(pool_fd, F_ADD_SEALS, + F_SEAL_GROW | F_SEAL_SHRINK | /*F_SEAL_FUTURE_WRITE |*/ F_SEAL_SEAL) < 0) + { + LOG_ERRNO("failed to seal SHM backing memory file"); + /* This is not a fatal error */ + } +#endif + + wl_pool = wl_shm_create_pool(chain->shm, pool_fd, memfd_size); + if (wl_pool == NULL) { + LOG_ERR("failed to create SHM pool"); + goto err; + } + + pool = xmalloc(sizeof(*pool)); + if (pool == NULL) { + LOG_ERRNO("failed to allocate buffer pool"); + goto err; + } + + *pool = (struct buffer_pool){ + .fd = pool_fd, + .wl_pool = wl_pool, + .real_mmapped = real_mmapped, + .mmap_size = memfd_size, + .ref_count = 0, + }; + + for (size_t i = 0; i < count; i++) { + if (sizes[i] == 0) { + bufs[i] = NULL; + continue; + } + + /* Push to list of available buffers, but marked as 'busy' */ + struct buffer_private *buf = xmalloc(sizeof(*buf)); + *buf = (struct buffer_private){ + .public = { + .width = widths[i], + .height = heights[i], + .stride = stride[i], + .pix_instances = chain->pix_instances, + .age = 1234, /* Force a full repaint */ + }, + .chain = chain, + .ref_count = immediate_purge ? 0 : 1, + .busy = true, + .pool = pool, + .offset = 0, + .size = sizes[i], + .scrollable = chain->scrollable, + .release_cb = chain->release_cb, + .cb_data = chain->cb_data, + }; + + if (!instantiate_offset(buf, offset)) { + free(buf); + goto err; + } + + if (immediate_purge) + tll_push_front(deferred, buf); + else + tll_push_front(chain->bufs, buf); + + buf->public.dirty = xmalloc( + chain->pix_instances * sizeof(buf->public.dirty[0])); + + for (size_t j = 0; j < chain->pix_instances; j++) + pixman_region32_init(&buf->public.dirty[j]); + + pool->ref_count++; + offset += buf->size; + bufs[i] = &buf->public; + } + +#if defined(MEASURE_SHM_ALLOCS) && MEASURE_SHM_ALLOCS + { + size_t currently_alloced = 0; + tll_foreach(buffers, it) + currently_alloced += it->item.size; + if (currently_alloced > max_alloced) + max_alloced = currently_alloced; + } +#endif + + if (!(bufs[0] && shm_can_scroll(bufs[0]))) { + /* We only need to keep the pool FD open if we're going to SHM + * scroll it */ + close(pool_fd); + pool->fd = -1; + } + + return; + +err: + pool_unref(pool); + if (wl_pool != NULL) + wl_shm_pool_destroy(wl_pool); + if (real_mmapped != MAP_FAILED) + munmap(real_mmapped, memfd_size); + if (pool_fd != -1) + close(pool_fd); + + /* We don't handle this */ + abort(); +} + +void +shm_did_not_use_buf(struct buffer *_buf) +{ + struct buffer_private *buf = (struct buffer_private *)_buf; + buf->busy = false; +} + +void +shm_get_many(struct buffer_chain *chain, size_t count, + int widths[static count], int heights[static count], + struct buffer *bufs[static count]) +{ + get_new_buffers(chain, count, widths, heights, bufs, true); +} + +struct buffer * +shm_get_buffer(struct buffer_chain *chain, int width, int height) +{ + LOG_DBG( + "chain=%p: looking for a reusable %dx%d buffer " + "among %zu potential buffers", + (void *)chain, width, height, tll_length(chain->bufs)); + + struct buffer_private *cached = NULL; + tll_foreach(chain->bufs, it) { + struct buffer_private *buf = it->item; + + if (buf->public.width != width || buf->public.height != height) { + LOG_DBG("purging mismatching buffer %p", (void *)buf); + if (buffer_unref_no_remove_from_chain(buf)) + tll_remove(chain->bufs, it); + continue; + } + + if (buf->busy) + buf->public.age++; + else +#if FORCED_DOUBLE_BUFFERING + if (buf->public.age == 0) + buf->public.age++; + else +#endif + { + if (cached == NULL) { + cached = buf; + } else { + /* We have multiple buffers eligible for + * reuse. Pick the "youngest" one, and mark the + * other one for purging */ + if (buf->public.age < cached->public.age) { + shm_unref(&cached->public); + cached = buf; + } else { + /* + * TODO: I think we _can_ use shm_unref() + * here... + * + * shm_unref() may remove 'it', but that + * should be safe; "our" tll_foreach() already + * holds the next pointer. + */ + if (buffer_unref_no_remove_from_chain(buf)) + tll_remove(chain->bufs, it); + } + } + } + } + + if (cached != NULL) { + LOG_DBG("reusing buffer %p from cache", (void *)cached); + cached->busy = true; + for (size_t i = 0; i < cached->public.pix_instances; i++) + pixman_region32_clear(&cached->public.dirty[i]); + xassert(cached->public.pix_instances == chain->pix_instances); + return &cached->public; + } + + struct buffer *ret; + get_new_buffers(chain, 1, &width, &height, &ret, false); + return ret; +} + +bool +shm_can_scroll(const struct buffer *_buf) +{ +#if __SIZEOF_POINTER__ == 8 + const struct buffer_private *buf = (const struct buffer_private *)_buf; + return can_punch_hole && max_pool_size > 0 && buf->scrollable; +#else + /* Not enough virtual address space in 32-bit */ + return false; +#endif +} + +#if __SIZEOF_POINTER__ == 8 && defined(FALLOC_FL_PUNCH_HOLE) +static bool +wrap_buffer(struct buffer_private *buf, off_t new_offset) +{ + struct buffer_pool *pool = buf->pool; + xassert(pool->ref_count == 1); + + /* We don't allow overlapping offsets */ + off_t UNUSED diff = new_offset < buf->offset + ? buf->offset - new_offset + : new_offset - buf->offset; + xassert(diff > buf->size); + + memcpy((uint8_t *)pool->real_mmapped + new_offset, + buf->public.data, + buf->size); + + off_t trim_ofs, trim_len; + if (new_offset > buf->offset) { + /* Trim everything *before* the new offset */ + trim_ofs = 0; + trim_len = new_offset; + } else { + /* Trim everything *after* the new buffer location */ + trim_ofs = new_offset + buf->size; + trim_len = pool->mmap_size - trim_ofs; + } + + if (fallocate( + pool->fd, + FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + trim_ofs, trim_len) < 0) + { + LOG_ERRNO("failed to trim SHM backing memory file"); + return false; + } + + /* Re-instantiate pixman+wl_buffer+raw pointersw */ + buffer_destroy_dont_close(&buf->public); + return instantiate_offset(buf, new_offset); +} + +static bool +shm_scroll_forward(struct buffer_private *buf, int rows, + int top_margin, int top_keep_rows, + int bottom_margin, int bottom_keep_rows) +{ + struct buffer_pool *pool = buf->pool; + + xassert(can_punch_hole); + xassert(buf->busy); + xassert(buf->public.pix != NULL); + xassert(buf->public.wl_buf != NULL); + xassert(pool != NULL); + xassert(pool->ref_count == 1); + xassert(pool->fd >= 0); + + LOG_DBG("scrolling %d rows (%d bytes)", rows, rows * buf->public.stride); + + const off_t diff = rows * buf->public.stride; + xassert(rows > 0); + xassert(diff < buf->size); + + if (buf->offset + diff + buf->size > max_pool_size) { + LOG_DBG("memfd offset wrap around"); + if (!wrap_buffer(buf, 0)) + goto err; + } + + off_t new_offset = buf->offset + diff; + xassert(new_offset > buf->offset); + xassert(new_offset + buf->size <= max_pool_size); + +#if TIME_SCROLL + struct timespec tot; + struct timespec time1; + clock_gettime(CLOCK_MONOTONIC, &time1); + + struct timespec time2 = time1; +#endif + + if (top_keep_rows > 0) { + /* Copy current 'top' region to its new location */ + const int stride = buf->public.stride; + uint8_t *base = buf->public.data; + + memmove( + base + (top_margin + rows) * stride, + base + (top_margin + 0) * stride, + top_keep_rows * stride); + +#if TIME_SCROLL + clock_gettime(CLOCK_MONOTONIC, &time2); + timespec_sub(&time2, &time1, &tot); + LOG_INFO("memmove (top region): %lds %ldns", + (long)tot.tv_sec, tot.tv_nsec); +#endif + } + + /* Destroy old objects (they point to the old offset) */ + buffer_destroy_dont_close(&buf->public); + + /* Free unused memory - everything up until the new offset */ + const off_t trim_ofs = 0; + const off_t trim_len = new_offset; + + if (fallocate( + pool->fd, + FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + trim_ofs, trim_len) < 0) + { + LOG_ERRNO("failed to trim SHM backing memory file"); + goto err; + } + +#if TIME_SCROLL + struct timespec time3; + clock_gettime(CLOCK_MONOTONIC, &time3); + timespec_sub(&time3, &time2, &tot); + LOG_INFO("PUNCH HOLE: %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); +#endif + + /* Re-instantiate pixman+wl_buffer+raw pointersw */ + bool ret = instantiate_offset(buf, new_offset); + +#if TIME_SCROLL + struct timespec time4; + clock_gettime(CLOCK_MONOTONIC, &time4); + timespec_sub(&time4, &time3, &tot); + LOG_INFO("instantiate offset: %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); +#endif + + if (ret && bottom_keep_rows > 0) { + /* Copy 'bottom' region to its new location */ + const size_t size = buf->size; + const int stride = buf->public.stride; + uint8_t *base = buf->public.data; + + memmove( + base + size - (bottom_margin + bottom_keep_rows) * stride, + base + size - (bottom_margin + rows + bottom_keep_rows) * stride, + bottom_keep_rows * stride); + +#if TIME_SCROLL + struct timespec time5; + clock_gettime(CLOCK_MONOTONIC, &time5); + + timespec_sub(&time5, &time4, &tot); + LOG_INFO("memmove (bottom region): %lds %ldns", + (long)tot.tv_sec, tot.tv_nsec); +#endif + } + + return ret; + +err: + abort(); + return false; +} + +static bool +shm_scroll_reverse(struct buffer_private *buf, int rows, + int top_margin, int top_keep_rows, + int bottom_margin, int bottom_keep_rows) +{ + xassert(rows > 0); + + struct buffer_pool *pool = buf->pool; + xassert(pool->ref_count == 1); + + const off_t diff = rows * buf->public.stride; + if (diff > buf->offset) { + LOG_DBG("memfd offset reverse wrap-around"); + if (!wrap_buffer(buf, (max_pool_size - buf->size) & ~(page_size() - 1))) + goto err; + } + + off_t new_offset = buf->offset - diff; + xassert(new_offset < buf->offset); + xassert(new_offset <= max_pool_size); + +#if TIME_SCROLL + struct timespec time0; + clock_gettime(CLOCK_MONOTONIC, &time0); + + struct timespec tot; + struct timespec time1 = time0; +#endif + + if (bottom_keep_rows > 0) { + /* Copy 'bottom' region to its new location */ + const size_t size = buf->size; + const int stride = buf->public.stride; + uint8_t *base = buf->public.data; + + memmove( + base + size - (bottom_margin + rows + bottom_keep_rows) * stride, + base + size - (bottom_margin + bottom_keep_rows) * stride, + bottom_keep_rows * stride); + +#if TIME_SCROLL + clock_gettime(CLOCK_MONOTONIC, &time1); + timespec_sub(&time1, &time0, &tot); + LOG_INFO("memmove (bottom region): %lds %ldns", + (long)tot.tv_sec, tot.tv_nsec); +#endif + } + + /* Destroy old objects (they point to the old offset) */ + buffer_destroy_dont_close(&buf->public); + + /* Free unused memory - everything after the relocated buffer */ + const off_t trim_ofs = new_offset + buf->size; + const off_t trim_len = pool->mmap_size - trim_ofs; + + if (fallocate( + pool->fd, + FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + trim_ofs, trim_len) < 0) + { + LOG_ERRNO("failed to trim SHM backing memory"); + goto err; + } +#if TIME_SCROLL + struct timespec time2; + clock_gettime(CLOCK_MONOTONIC, &time2); + timespec_sub(&time2, &time1, &tot); + LOG_INFO("fallocate: %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); +#endif + + /* Re-instantiate pixman+wl_buffer+raw pointers */ + bool ret = instantiate_offset(buf, new_offset); + +#if TIME_SCROLL + struct timespec time3; + clock_gettime(CLOCK_MONOTONIC, &time3); + timespec_sub(&time3, &time2, &tot); + LOG_INFO("instantiate offset: %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); +#endif + + if (ret && top_keep_rows > 0) { + /* Copy current 'top' region to its new location */ + const int stride = buf->public.stride; + uint8_t *base = buf->public.data; + + memmove( + base + (top_margin + 0) * stride, + base + (top_margin + rows) * stride, + top_keep_rows * stride); + +#if TIME_SCROLL + struct timespec time4; + clock_gettime(CLOCK_MONOTONIC, &time4); + timespec_sub(&time4, &time3, &tot); + LOG_INFO("memmove (top region): %lds %ldns", + (long)tot.tv_sec, tot.tv_nsec); +#endif + } + + return ret; + +err: + abort(); + return false; +} +#endif /* FALLOC_FL_PUNCH_HOLE */ + +bool +shm_scroll(struct buffer *_buf, int rows, + int top_margin, int top_keep_rows, + int bottom_margin, int bottom_keep_rows) +{ +#if __SIZEOF_POINTER__ == 8 && defined(FALLOC_FL_PUNCH_HOLE) + if (!shm_can_scroll(_buf)) + return false; + + struct buffer_private *buf = (struct buffer_private *)_buf; + + xassert(rows != 0); + return rows > 0 + ? shm_scroll_forward(buf, rows, top_margin, top_keep_rows, bottom_margin, bottom_keep_rows) + : shm_scroll_reverse(buf, -rows, top_margin, top_keep_rows, bottom_margin, bottom_keep_rows); +#else + return false; +#endif +} + +void +shm_purge(struct buffer_chain *chain) +{ + LOG_DBG("chain: %p: purging all buffers", (void *)chain); + + /* Purge old buffers associated with this cookie */ + tll_foreach(chain->bufs, it) { + if (buffer_unref_no_remove_from_chain(it->item)) + tll_remove(chain->bufs, it); + } +} + +void +shm_addref(struct buffer *_buf) +{ + struct buffer_private *buf = (struct buffer_private *)_buf; + buf->ref_count++; +} + +void +shm_unref(struct buffer *_buf) +{ + if (_buf == NULL) + return; + + struct buffer_private *buf = (struct buffer_private *)_buf; + struct buffer_chain *chain = buf->chain; + + tll_foreach(chain->bufs, it) { + if (it->item != buf) + continue; + + if (buffer_unref_no_remove_from_chain(buf)) + tll_remove(chain->bufs, it); + break; + } +} + +struct buffer_chain * +shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data) +{ + pixman_format_code_t pixman_fmt = PIXMAN_a8r8g8b8; + enum wl_shm_format shm_fmt = WL_SHM_FORMAT_ARGB8888; + + static bool have_logged = false; + static bool have_logged_10_fallback = false; + +#if defined(HAVE_PIXMAN_RGBA_16) + static bool have_logged_16_fallback = false; + + if (desired_bit_depth == SHM_BITS_16) { + if (wayl->shm_have_abgr161616) { + pixman_fmt = PIXMAN_a16b16g16r16; + shm_fmt = WL_SHM_FORMAT_ABGR16161616; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 16-bit BGR surfaces"); + } + } else { + if (!have_logged_16_fallback) { + have_logged_16_fallback = true; + + LOG_WARN( + "16-bit surfaces requested, but compositor does not " + "implement ABGR161616+XBGR161616"); + } + } + } +#endif + + if (desired_bit_depth >= SHM_BITS_10 && pixman_fmt == PIXMAN_a8r8g8b8) { + if (wayl->shm_have_argb2101010) { + pixman_fmt = PIXMAN_a2r10g10b10; + shm_fmt = WL_SHM_FORMAT_ARGB2101010; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 10-bit RGB surfaces"); + } + } + + else if (wayl->shm_have_abgr2101010) { + pixman_fmt = PIXMAN_a2b10g10r10; + shm_fmt = WL_SHM_FORMAT_ABGR2101010; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 10-bit BGR surfaces"); + } + } + + else { + if (!have_logged_10_fallback) { + have_logged_10_fallback = true; + + LOG_WARN( + "10-bit surfaces requested, but compositor does not " + "implement ARGB2101010+XRGB2101010, or " + "ABGR2101010+XBGR2101010"); + } + } + } else { + if (!have_logged) { + have_logged = true; + LOG_INFO("using 8-bit RGB surfaces"); + } + } + + struct buffer_chain *chain = xmalloc(sizeof(*chain)); + *chain = (struct buffer_chain){ + .bufs = tll_init(), + .shm = wayl->shm, + .pix_instances = pix_instances, + .scrollable = scrollable, + + .pixman_fmt = pixman_fmt, + .shm_format = shm_fmt, + + .release_cb = release_cb, + .cb_data = cb_data, + }; + return chain; +} + +void +shm_chain_free(struct buffer_chain *chain) +{ + if (chain == NULL) + return; + + shm_purge(chain); + + if (tll_length(chain->bufs) > 0) { + BUG("chain=%p: there are buffers remaining; " + "is there a missing call to shm_unref()?", (void *)chain); + } + + free(chain); +} + +enum shm_bit_depth +shm_chain_bit_depth(const struct buffer_chain *chain) +{ + const pixman_format_code_t fmt = chain->pixman_fmt; + + return fmt == PIXMAN_a8r8g8b8 + ? SHM_BITS_8 +#if defined(HAVE_PIXMAN_RGBA_16) + : fmt == PIXMAN_a16b16g16r16 + ? SHM_BITS_16 +#endif + : SHM_BITS_10; +} diff --git a/shm.h b/shm.h new file mode 100644 index 0000000..c58a853 --- /dev/null +++ b/shm.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include + +#include "config.h" +#include "wayland.h" + +struct damage; + +struct buffer { + int width; + int height; + int stride; + + void *data; + + struct wl_buffer *wl_buf; + pixman_image_t **pix; + size_t pix_instances; + + unsigned age; + + /* + * First item in the array is used to track frame-to-frame + * damage. This is used when re-applying damage from the last + * frame, when the compositor doesn't release buffers immediately + * (forcing us to double buffer) + * + * The remaining items are used to track surface damage. Each + * worker thread adds its own cell damage to "its" region. When + * the frame is done, all damage is converted to a single region, + * which is then used in calls to wl_surface_damage_buffer(). + */ + pixman_region32_t *dirty; +}; + +void shm_fini(void); + +/* TODO: combine into shm_init() */ +void shm_set_max_pool_size(off_t max_pool_size); +void shm_set_min_stride_alignment(size_t min_stride_alignment); + +struct buffer_chain; +struct buffer_chain *shm_chain_new( + struct wayland *wayl, bool scrollable, size_t pix_instances, + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data); +void shm_chain_free(struct buffer_chain *chain); + +enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain); + +/* + * Returns a single buffer. + * + * May returned a cached buffer. If so, the buffer's age indicates how + * many shm_get_buffer() calls have been made for the same + * width/height while the buffer was still busy. + * + * A newly allocated buffer has an age of 1234. + */ +struct buffer *shm_get_buffer(struct buffer_chain *chain, int width, int height); +/* + * Returns many buffers, described by 'info', all sharing the same SHM + * buffer pool. + * + * Never returns cached buffers. However, the newly created buffers + * are all inserted into the regular buffer cache, and are treated + * just like buffers created by shm_get_buffer(). + * + * This function is useful when allocating many small buffers, with + * (roughly) the same life time. + * + * Buffers are tagged for immediate purging, and will be destroyed as + * soon as the compositor releases them. + */ +void shm_get_many( + struct buffer_chain *chain, size_t count, + int widths[static count], int heights[static count], + struct buffer *bufs[static count]); + +void shm_did_not_use_buf(struct buffer *buf); + +bool shm_can_scroll(const struct buffer *buf); +bool shm_scroll(struct buffer *buf, int rows, + int top_margin, int top_keep_rows, + int bottom_margin, int bottom_keep_rows); + +void shm_addref(struct buffer *buf); +void shm_unref(struct buffer *buf); + +void shm_purge(struct buffer_chain *chain); diff --git a/sixel.c b/sixel.c new file mode 100644 index 0000000..187f134 --- /dev/null +++ b/sixel.c @@ -0,0 +1,2228 @@ +#include "sixel.h" + +#include +#include + +#define LOG_MODULE "sixel" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "grid.h" +#include "hsl.h" +#include "render.h" +#include "srgb.h" +#include "util.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +static size_t count; + +static void sixel_put_generic(struct terminal *term, uint8_t c); +static void sixel_put_ar_11(struct terminal *term, uint8_t c); + +static uint32_t +color_decode_srgb(const struct terminal *term, uint16_t r, uint16_t g, uint16_t b) +{ + if (term->sixel.linear_blending) { + if (term->sixel.use_10bit) { + r = srgb_decode_8_to_16(r) >> 6; + g = srgb_decode_8_to_16(g) >> 6; + b = srgb_decode_8_to_16(b) >> 6; + } else { + r = srgb_decode_8_to_8(r); + g = srgb_decode_8_to_8(g); + b = srgb_decode_8_to_8(b); + } + } else { + if (term->sixel.use_10bit) { + r <<= 2; + g <<= 2; + b <<= 2; + } + } + + uint32_t color; + + if (term->sixel.use_10bit) { + if (PIXMAN_FORMAT_TYPE(term->sixel.pixman_fmt) == PIXMAN_TYPE_ARGB) + color = 0x3u << 30 | r << 20 | g << 10 | b; + else + color = 0x3u << 30 | b << 20 | g << 10 | r; + } else + color = 0xffu << 24 | r << 16 | g << 8 | b; + + return color; +} + +void +sixel_fini(struct terminal *term) +{ + free(term->sixel.image.data); + free(term->sixel.private_palette); + free(term->sixel.shared_palette); +} + +sixel_put +sixel_init(struct terminal *term, int p1, int p2, int p3) +{ + /* + * P1: pixel aspect ratio + * - 0,1 - 2:1 + * - 2 - 5:1 + * - 3,4 - 3:1 + * - 5,6 - 2:1 + * - 7,8,9 - 1:1 + * + * P2: background color mode + * - 0|2: empty pixels use current background color + * - 1: empty pixels remain at their current color (i.e. transparent) + * P3: horizontal grid size - ignored + */ + + xassert(term->sixel.image.data == NULL); + xassert(term->sixel.palette_size <= SIXEL_MAX_COLORS); + + /* Default aspect ratio is 2:1 */ + const int pad = 1; + const int pan = + (p1 == 2) ? 5 : + (p1 == 3 || p1 == 4) ? 3 : + (p1 == 7 || p1 == 8 || p1 == 9) ? 1 : 2; + + LOG_DBG("initializing sixel with " + "p1=%d (pan=%d, pad=%d, aspect-ratio=%d:%d), " + "p2=%d (transparent=%s), " + "p3=%d (ignored)", + p1, pan, pad, pan, pad, p2, p2 == 1 ? "yes" : "no", p3); + + term->sixel.state = SIXEL_DECSIXEL; + term->sixel.pos = (struct coord){0, 0}; + term->sixel.color_idx = 0; + term->sixel.pan = pan; + term->sixel.pad = pad; + term->sixel.param = 0; + term->sixel.param_idx = 0; + memset(term->sixel.params, 0, sizeof(term->sixel.params)); + term->sixel.transparent_bg = p2 == 1; + term->sixel.image.data = NULL; + term->sixel.image.p = NULL; + term->sixel.image.width = 0; + term->sixel.image.height = 0; + term->sixel.image.alloc_height = 0; + term->sixel.image.bottom_pixel = 0; + term->sixel.linear_blending = wayl_do_linear_blending(term->wl, term->conf); + term->sixel.pixman_fmt = PIXMAN_a8r8g8b8; + + /* + * Use higher-precision sixel surfaces if we're using + * higher-precision window surfaces. + * + * This is to a) get more accurate colors when doing gamma-correct + * blending, and b) use the same pixman format as the main + * surfaces, for (hopefully) better performance. + * + * For now, don't support 16-bit surfaces (too much sixel logic + * that assumes 32-bit pixels). + */ + if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { + if (term->wl->shm_have_argb2101010) { + term->sixel.use_10bit = true; + term->sixel.pixman_fmt = PIXMAN_a2r10g10b10; + } + + else if (term->wl->shm_have_abgr2101010) { + term->sixel.use_10bit = true; + term->sixel.pixman_fmt = PIXMAN_a2b10g10r10; + } + } + + const size_t active_palette_entries = min( + ALEN(term->conf->colors_dark.sixel), term->sixel.palette_size); + + if (term->sixel.use_private_palette) { + xassert(term->sixel.private_palette == NULL); + term->sixel.private_palette = xcalloc( + term->sixel.palette_size, sizeof(term->sixel.private_palette[0])); + + memcpy( + term->sixel.private_palette, term->conf->colors_dark.sixel, + active_palette_entries * sizeof(term->sixel.private_palette[0])); + + if (term->sixel.linear_blending || term->sixel.use_10bit) { + for (size_t i = 0; i < active_palette_entries; i++) { + uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; + uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; + uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; + term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); + } + } + + term->sixel.palette = term->sixel.private_palette; + } else { + if (term->sixel.shared_palette == NULL) { + term->sixel.shared_palette = xcalloc( + term->sixel.palette_size, sizeof(term->sixel.shared_palette[0])); + + memcpy( + term->sixel.shared_palette, term->conf->colors_dark.sixel, + active_palette_entries * sizeof(term->sixel.shared_palette[0])); + + if (term->sixel.linear_blending || term->sixel.use_10bit) { + for (size_t i = 0; i < active_palette_entries; i++) { + uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; + uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; + uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; + term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); + } + } + } else { + /* Shared palette - do *not* reset palette for new sixels */ + } + + term->sixel.palette = term->sixel.shared_palette; + } + + count = 0; + return pan == 1 && pad == 1 ? &sixel_put_ar_11 : &sixel_put_generic; +} + +static void +sixel_invalidate_cache(struct sixel *sixel) +{ + if (sixel->scaled.pix != NULL) + pixman_image_unref(sixel->scaled.pix); + + free(sixel->scaled.data); + sixel->scaled.pix = NULL; + sixel->scaled.data = NULL; + sixel->scaled.width = -1; + sixel->scaled.height = -1; + + sixel->pix = NULL; + sixel->width = -1; + sixel->height = -1; +} + +void +sixel_destroy(struct sixel *sixel) +{ + sixel_invalidate_cache(sixel); + + if (sixel->original.pix != NULL) + pixman_image_unref(sixel->original.pix); + + free(sixel->original.data); + sixel->original.pix = NULL; + sixel->original.data = NULL; +} + +void +sixel_destroy_all(struct terminal *term) +{ + tll_foreach(term->normal.sixel_images, it) + sixel_destroy(&it->item); + tll_foreach(term->alt.sixel_images, it) + sixel_destroy(&it->item); + tll_free(term->normal.sixel_images); + tll_free(term->alt.sixel_images); +} + +static void +sixel_erase(struct terminal *term, struct sixel *sixel) +{ + for (int i = 0; i < sixel->rows; i++) { + int r = (sixel->pos.row + i) & (term->grid->num_rows - 1); + + struct row *row = term->grid->rows[r]; + if (row == NULL) { + /* A resize/reflow may cause row to now be unallocated */ + continue; + } + + row->dirty = true; + + for (int c = sixel->pos.col; c < min(sixel->pos.col + sixel->cols, term->cols); c++) + row->cells[c].attrs.clean = 0; + } + + sixel_destroy(sixel); +} + +/* + * Verify the sixels are sorted correctly. + * + * The sixels are sorted on their *end* row, in descending order. This + * invariant means the most recent sixels appear first in the list. + */ +static void +verify_list_order(const struct terminal *term) +{ +#if defined(_DEBUG) + int prev_row = INT_MAX; + int prev_col = -1; + int prev_col_count = 0; + + /* To aid debugging */ + size_t UNUSED idx = 0; + + tll_foreach(term->grid->sixel_images, it) { + int row = grid_row_abs_to_sb( + term->grid, term->rows, it->item.pos.row + it->item.rows - 1); + int col = it->item.pos.col; + int col_count = it->item.cols; + + xassert(row <= prev_row); + + if (row == prev_row) { + /* Allowed to be on the same row only if their columns + * don't overlap */ + + xassert(col + col_count <= prev_col || + prev_col + prev_col_count <= col); + } + + prev_row = row; + prev_col = col; + prev_col_count = col_count; + idx++; + } +#endif +} + +/* + * Verifies there aren't any sixels that cross the scrollback + * wrap-around. This invariant means a sixel's absolute row numbers + * are strictly increasing. + */ +static void +verify_no_wraparound_crossover(const struct terminal *term) +{ +#if defined(_DEBUG) + tll_foreach(term->grid->sixel_images, it) { + const struct sixel *six = &it->item; + + xassert(six->pos.row >= 0); + xassert(six->pos.row < term->grid->num_rows); + + int end = (six->pos.row + six->rows - 1) & (term->grid->num_rows - 1); + xassert(end >= six->pos.row); + } +#endif +} + +/* + * Verify there aren't any sixels that cross the scrollback end. This + * invariant means a sixel's rebased row numbers are strictly + * increasing. + */ +static void +verify_scrollback_consistency(const struct terminal *term) +{ +#if defined(_DEBUG) + tll_foreach(term->grid->sixel_images, it) { + const struct sixel *six = &it->item; + + int last_row = -1; + for (int i = 0; i < six->rows; i++) { + int row_no = grid_row_abs_to_sb( + term->grid, term->rows, six->pos.row + i); + + if (last_row != -1) + xassert(last_row < row_no); + + last_row = row_no; + } + } +#endif +} + +/* + * Verifies no sixel overlap with any other sixels. + */ +static void +verify_no_overlap(const struct terminal *term) +{ +#if defined(_DEBUG) + tll_foreach(term->grid->sixel_images, it) { + const struct sixel *six1 = &it->item; + + pixman_region32_t rect1; + pixman_region32_init_rect( + &rect1, six1->pos.col, six1->pos.row, six1->cols, six1->rows); + + tll_foreach(term->grid->sixel_images, it2) { + const struct sixel *six2 = &it2->item; + + if (six1 == six2) + continue; + + pixman_region32_t rect2; + pixman_region32_init_rect( + &rect2, six2->pos.col, + six2->pos.row, six2->cols, six2->rows); + + pixman_region32_t intersection; + pixman_region32_init(&intersection); + pixman_region32_intersect(&intersection, &rect1, &rect2); + + xassert(!pixman_region32_not_empty(&intersection)); + + pixman_region32_fini(&intersection); + pixman_region32_fini(&rect2); + } + + pixman_region32_fini(&rect1); + } +#endif +} + +static void +verify_sixels(const struct terminal *term) +{ + verify_no_wraparound_crossover(term); + verify_scrollback_consistency(term); + verify_no_overlap(term); + verify_list_order(term); +} + +static void +sixel_insert(struct terminal *term, struct sixel sixel) +{ + int end_row = grid_row_abs_to_sb( + term->grid, term->rows, sixel.pos.row + sixel.rows - 1); + + tll_foreach(term->grid->sixel_images, it) { + int rebased = grid_row_abs_to_sb( + term->grid, term->rows, it->item.pos.row + it->item.rows - 1); + + if (rebased < end_row) { + tll_insert_before(term->grid->sixel_images, it, sixel); + goto out; + } + } + + tll_push_back(term->grid->sixel_images, sixel); + +out: +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + LOG_DBG("sixel list after insertion:"); + tll_foreach(term->grid->sixel_images, it) { + LOG_DBG(" rows=%d+%d", it->item.pos.row, it->item.rows); + } +#endif + verify_sixels(term); +} + +void +sixel_scroll_up(struct terminal *term, int rows) +{ + if (likely(tll_length(term->grid->sixel_images) == 0)) + return; + + tll_rforeach(term->grid->sixel_images, it) { + struct sixel *six = &it->item; + + int six_start = grid_row_abs_to_sb(term->grid, term->rows, six->pos.row); + + if (six_start < rows) { + sixel_erase(term, six); + tll_remove(term->grid->sixel_images, it); + } else { + /* + * Unfortunately, we cannot break here. + * + * The sixels are sorted on their *end* row. This means + * there may be a sixel with a top row that will be + * scrolled out *anywhere* in the list (think of a huuuuge + * sixel that covers the entire scrollback) + */ + //break; + } + } + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; + term_update_ascii_printer(term); + verify_sixels(term); +} + +void +sixel_scroll_down(struct terminal *term, int rows) +{ + if (likely(tll_length(term->grid->sixel_images) == 0)) + return; + + xassert(term->grid->num_rows >= rows); + + tll_foreach(term->grid->sixel_images, it) { + struct sixel *six = &it->item; + + int six_end = grid_row_abs_to_sb( + term->grid, term->rows, six->pos.row + six->rows - 1); + if (six_end >= term->grid->num_rows - rows) { + sixel_erase(term, six); + tll_remove(term->grid->sixel_images, it); + } else + break; + } + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; + term_update_ascii_printer(term); + verify_sixels(term); +} + +static void +blend_new_image_over_old(const struct terminal *term, + const struct sixel *six, pixman_region32_t *six_rect, + int row, int col, pixman_image_t **pix, bool *opaque) +{ + xassert(pix != NULL); + xassert(opaque != NULL); + + /* + * TODO: handle images being emitted with different cell dimensions + */ + + const int six_ofs_x = six->pos.col * six->cell_width; + const int six_ofs_y = six->pos.row * six->cell_height; + const int img_ofs_x = col * six->cell_width; + const int img_ofs_y = row * six->cell_height; + const int img_width = pixman_image_get_width(*pix); + const int img_height = pixman_image_get_height(*pix); + + pixman_region32_t pix_rect; + pixman_region32_init_rect( + &pix_rect, img_ofs_x, img_ofs_y, img_width, img_height); + + /* Blend the intersection between the old and new images */ + pixman_region32_t intersection; + pixman_region32_init(&intersection); + pixman_region32_intersect(&intersection, six_rect, &pix_rect); + + int n_rects = -1; + pixman_box32_t *boxes = pixman_region32_rectangles( + &intersection, &n_rects); + + if (n_rects == 0) + goto out; + + xassert(n_rects == 1); + pixman_box32_t *box = &boxes[0]; + + if (!*opaque) { + /* + * New image is transparent - blend on top of the old + * sixel image. + */ + pixman_image_composite32( + PIXMAN_OP_OVER_REVERSE, + six->original.pix, NULL, *pix, + box->x1 - six_ofs_x, box->y1 - six_ofs_y, + 0, 0, + box->x1 - img_ofs_x, box->y1 - img_ofs_y, + box->x2 - box->x1, box->y2 - box->y1); + } + + /* + * Since the old image is split into sub-tiles on a + * per-row basis, we need to enlarge the new image and + * copy the old image if the old image extends beyond the + * new image. + * + * The "bounding" coordinates are either the edges of the + * old image, or the next cell boundary, whichever comes + * first. + */ + int bounding_x = six_ofs_x + six->original.width > img_ofs_x + img_width + ? min( + six_ofs_x + six->original.width, + (box->x2 + six->cell_width - 1) / six->cell_width * six->cell_width) + : box->x2; + int bounding_y = six_ofs_y + six->original.height > img_ofs_y + img_height + ? min( + six_ofs_y + six->original.height, + (box->y2 + six->cell_height - 1) / six->cell_height * six->cell_height) + : box->y2; + + /* The required size of the new image */ + const int required_width = bounding_x - img_ofs_x; + const int required_height = bounding_y - img_ofs_y; + + const int new_width = max(img_width, required_width); + const int new_height = max(img_height, required_height); + + if (new_width <= img_width && new_height <= img_height) + goto out; + + //LOG_INFO("enlarging: %dx%d -> %dx%d", img_width, img_height, new_width, new_height); + + if (!six->opaque) { + /* Transparency is viral */ + *opaque = false; + } + + /* Create a new pixmap */ + int stride = new_width * sizeof(uint32_t); + uint32_t *new_data = xmalloc(stride * new_height); + pixman_image_t *pix2 = pixman_image_create_bits_no_clear( + term->sixel.pixman_fmt, new_width, new_height, new_data, stride); + +#if defined(_DEBUG) + /* Fill new image with an easy-to-recognize color (green) */ + for (size_t i = 0; i < new_width * new_height; i++) + new_data[i] = 0xff00ff00; +#endif + + /* Copy the new image, from its old pixmap, to the new pixmap */ + pixman_image_composite32( + PIXMAN_OP_SRC, + *pix, NULL, pix2, 0, 0, 0, 0, 0, 0, img_width, img_height); + + /* Copy the bottom tile of the old sixel image into the new pixmap */ + pixman_image_composite32( + PIXMAN_OP_SRC, + six->original.pix, NULL, pix2, + box->x1 - six_ofs_x, box->y2 - six_ofs_y, + 0, 0, + box->x1 - img_ofs_x, box->y2 - img_ofs_y, + bounding_x - box->x1, bounding_y - box->y2); + + /* Copy the right tile of the old sixel image into the new pixmap */ + pixman_image_composite32( + PIXMAN_OP_SRC, + six->original.pix, NULL, pix2, + box->x2 - six_ofs_x, box->y1 - six_ofs_y, + 0, 0, + box->x2 - img_ofs_x, box->y1 - img_ofs_y, + bounding_x - box->x2, bounding_y - box->y1); + + /* + * Ensure the newly allocated area is initialized. + * + * Some of it, or all, will have been initialized above, by the + * bottom and right tiles from the old sixel image. However, there + * may be areas in the new image that isn't covered by the old + * image. These areas need to be made transparent. + */ + pixman_region32_t uninitialized; + pixman_region32_init_rects( + &uninitialized, + (const pixman_box32_t []){ + /* Extended image area on the right side */ + {img_ofs_x + img_width, img_ofs_y, img_ofs_x + new_width, img_ofs_y + new_height}, + + /* Bottom */ + {img_ofs_x, img_ofs_y + img_height, img_ofs_x + new_width, img_ofs_y + new_height}}, + 2); + + /* Subtract the old sixel image, since the area(s) covered by the + * old image has already been copied, and *must* not be + * overwritten */ + pixman_region32_t diff; + pixman_region32_init(&diff); + pixman_region32_subtract(&diff, &uninitialized, six_rect); + + if (pixman_region32_not_empty(&diff)) { + pixman_image_t *src = + pixman_image_create_solid_fill(&(pixman_color_t){0}); + + int count = -1; + pixman_box32_t *rects = pixman_region32_rectangles(&diff, &count); + + for (int i = 0; i < count; i++) { + pixman_image_composite32( + PIXMAN_OP_SRC, + src, NULL, pix2, + 0, 0, 0, 0, + rects[i].x1 - img_ofs_x, rects[i].y1 - img_ofs_y, + rects[i].x2 - rects[i].x1, + rects[i].y2 - rects[i].y1); + } + + pixman_image_unref(src); + *opaque = false; + } + + pixman_region32_fini(&diff); + pixman_region32_fini(&uninitialized); + + /* Use the new pixmap in place of the old one */ + free(pixman_image_get_data(*pix)); + pixman_image_unref(*pix); + *pix = pix2; + +out: + pixman_region32_fini(&intersection); + pixman_region32_fini(&pix_rect); +} + +static void +sixel_overwrite(struct terminal *term, struct sixel *six, + int row, int col, int height, int width, + pixman_image_t **pix, bool *opaque) +{ + pixman_region32_t six_rect; + pixman_region32_init_rect( + &six_rect, + six->pos.col * six->cell_width, six->pos.row * six->cell_height, + six->original.width, six->original.height); + + pixman_region32_t overwrite_rect; + pixman_region32_init_rect( + &overwrite_rect, + col * six->cell_width, row * six->cell_height, + width * six->cell_width, height * six->cell_height); + +#if defined(_DEBUG) + pixman_region32_t cell_intersection; + pixman_region32_init(&cell_intersection); + pixman_region32_intersect(&cell_intersection, &six_rect, &overwrite_rect); + xassert(!pixman_region32_not_empty(&six_rect) || + pixman_region32_not_empty(&cell_intersection)); + pixman_region32_fini(&cell_intersection); +#endif + + if (pix != NULL) + blend_new_image_over_old(term, six, &six_rect, row, col, pix, opaque); + + pixman_region32_t diff; + pixman_region32_init(&diff); + pixman_region32_subtract(&diff, &six_rect, &overwrite_rect); + + pixman_region32_fini(&six_rect); + pixman_region32_fini(&overwrite_rect); + + int n_rects = -1; + pixman_box32_t *boxes = pixman_region32_rectangles(&diff, &n_rects); + + for (int i = 0; i < n_rects; i++) { + LOG_DBG("box #%d: x1=%d, y1=%d, x2=%d, y2=%d", i, + boxes[i].x1, boxes[i].y1, boxes[i].x2, boxes[i].y2); + + xassert(boxes[i].x1 % six->cell_width == 0); + xassert(boxes[i].y1 % six->cell_height == 0); + + /* New image's position, in cells */ + const int new_col = boxes[i].x1 / six->cell_width; + const int new_row = boxes[i].y1 / six->cell_height; + + xassert(new_row < term->grid->num_rows); + + /* New image's width and height, in pixels */ + const int new_width = boxes[i].x2 - boxes[i].x1; + const int new_height = boxes[i].y2 - boxes[i].y1; + + uint32_t *new_data = xmalloc(new_width * new_height * sizeof(uint32_t)); + const uint32_t *old_data = six->original.data; + + /* Pixel offsets into old image backing memory */ + const int x_ofs = boxes[i].x1 - six->pos.col * six->cell_width; + const int y_ofs = boxes[i].y1 - six->pos.row * six->cell_height; + + /* Copy image data, one row at a time */ + for (size_t j = 0; j < new_height; j++) { + memcpy( + &new_data[(0 + j) * new_width], + &old_data[(y_ofs + j) * six->original.width + x_ofs], + new_width * sizeof(uint32_t)); + } + + pixman_image_t *new_pix = pixman_image_create_bits_no_clear( + term->sixel.pixman_fmt, new_width, new_height, new_data, new_width * sizeof(uint32_t)); + + struct sixel new_six = { + .pix = NULL, + .width = -1, + .height = -1, + .pos = {.col = new_col, .row = new_row}, + .cols = (new_width + six->cell_width - 1) / six->cell_width, + .rows = (new_height + six->cell_height - 1) / six->cell_height, + .opaque = six->opaque, + .cell_width = six->cell_width, + .cell_height = six->cell_height, + .original = { + .data = new_data, + .pix = new_pix, + .width = new_width, + .height = new_height, + }, + .scaled = { + .data = NULL, + .pix = NULL, + .width = -1, + .height = -1, + }, + }; + +#if defined(_DEBUG) + /* Assert we don't cross the scrollback wrap-around */ + const int new_end = new_six.pos.row + new_six.rows - 1; + xassert(new_end < term->grid->num_rows); +#endif + + sixel_insert(term, new_six); + } + + pixman_region32_fini(&diff); +} + +/* Row numbers are absolute */ +static void +_sixel_overwrite_by_rectangle( + struct terminal *term, int row, int col, int height, int width, + pixman_image_t **pix, bool *opaque) +{ + verify_sixels(term); + +#if defined(_DEBUG) + pixman_region32_t overwrite_rect; + pixman_region32_init_rect(&overwrite_rect, col, row, width, height); +#endif + + const int start = row; + const int end = row + height - 1; + + /* We should never generate scrollback wrapping sixels */ + xassert(end < term->grid->num_rows); + + const int scrollback_rel_start = grid_row_abs_to_sb( + term->grid, term->rows, start); + + bool UNUSED would_have_breaked = false; + + tll_foreach(term->grid->sixel_images, it) { + struct sixel *six = &it->item; + + const int six_start = six->pos.row; + const int six_end = (six_start + six->rows - 1); + const int six_scrollback_rel_end = + grid_row_abs_to_sb(term->grid, term->rows, six_end); + + /* We should never generate scrollback wrapping sixels */ + xassert(six_end < term->grid->num_rows); + + if (six_scrollback_rel_end < scrollback_rel_start) { + /* All remaining sixels are *before* our rectangle */ + would_have_breaked = true; + break; + } + +#if defined(_DEBUG) + pixman_region32_t six_rect; + pixman_region32_init_rect(&six_rect, six->pos.col, six->pos.row, six->cols, six->rows); + + pixman_region32_t intersection; + pixman_region32_init(&intersection); + pixman_region32_intersect(&intersection, &six_rect, &overwrite_rect); + + const bool collides = pixman_region32_not_empty(&intersection); +#else + const bool UNUSED collides = false; +#endif + + if ((start <= six_start && end >= six_start) || /* Crosses sixel start boundary */ + (start <= six_end && end >= six_end) || /* Crosses sixel end boundary */ + (start >= six_start && end <= six_end)) /* Fully within sixel range */ + { + const int col_start = six->pos.col; + const int col_end = six->pos.col + six->cols - 1; + + if ((col <= col_start && col + width - 1 >= col_start) || + (col <= col_end && col + width - 1 >= col_end) || + (col >= col_start && col + width - 1 <= col_end)) + { + xassert(!would_have_breaked); + + struct sixel to_be_erased = *six; + tll_remove(term->grid->sixel_images, it); + + sixel_overwrite(term, &to_be_erased, start, col, height, width, + pix, opaque); + sixel_erase(term, &to_be_erased); + } else + xassert(!collides); + } else + xassert(!collides); + +#if defined(_DEBUG) + pixman_region32_fini(&intersection); + pixman_region32_fini(&six_rect); +#endif + } + +#if defined(_DEBUG) + pixman_region32_fini(&overwrite_rect); +#endif +} + +void +sixel_overwrite_by_rectangle( + struct terminal *term, int row, int col, int height, int width) +{ + if (likely(tll_length(term->grid->sixel_images) == 0)) + return; + + const int start = (term->grid->offset + row) & (term->grid->num_rows - 1); + const int end = (start + height - 1) & (term->grid->num_rows - 1); + const bool wraps = end < start; + + if (wraps) { + int rows_to_wrap_around = term->grid->num_rows - start; + xassert(height - rows_to_wrap_around > 0); + _sixel_overwrite_by_rectangle(term, start, col, rows_to_wrap_around, width, NULL, NULL); + _sixel_overwrite_by_rectangle(term, 0, col, height - rows_to_wrap_around, width, NULL, NULL); + } else + _sixel_overwrite_by_rectangle(term, start, col, height, width, NULL, NULL); + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; + term_update_ascii_printer(term); +} + +/* Row numbers are relative to grid offset */ +void +sixel_overwrite_by_row(struct terminal *term, int _row, int col, int width) +{ + xassert(col >= 0); + + xassert(_row >= 0); + xassert(_row < term->rows); + xassert(col >= 0); + xassert(col < term->grid->num_cols); + + if (likely(tll_length(term->grid->sixel_images) == 0)) + return; + + if (col + width > term->grid->num_cols) + width = term->grid->num_cols - col; + + const int row = (term->grid->offset + _row) & (term->grid->num_rows - 1); + const int scrollback_rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); + + tll_foreach(term->grid->sixel_images, it) { + struct sixel *six = &it->item; + const int six_start = six->pos.row; + const int six_end = (six_start + six->rows - 1) & (term->grid->num_rows - 1); + + /* We should never generate scrollback wrapping sixels */ + xassert(six_end >= six_start); + + const int six_scrollback_rel_end = + grid_row_abs_to_sb(term->grid, term->rows, six_end); + + if (six_scrollback_rel_end < scrollback_rel_row) { + /* All remaining sixels are *before* "our" row */ + break; + } + + if (row >= six_start && row <= six_end) { + const int col_start = six->pos.col; + const int col_end = six->pos.col + six->cols - 1; + + if ((col <= col_start && col + width - 1 >= col_start) || + (col <= col_end && col + width - 1 >= col_end) || + (col >= col_start && col + width - 1 <= col_end)) + { + struct sixel to_be_erased = *six; + tll_remove(term->grid->sixel_images, it); + + sixel_overwrite(term, &to_be_erased, row, col, 1, width, NULL, NULL); + sixel_erase(term, &to_be_erased); + } + } + } + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; + term_update_ascii_printer(term); +} + +void +sixel_overwrite_at_cursor(struct terminal *term, int width) +{ + if (likely(tll_length(term->grid->sixel_images) == 0)) + return; + + sixel_overwrite_by_row( + term, term->grid->cursor.point.row, term->grid->cursor.point.col, width); +} + +void +sixel_cell_size_changed(struct terminal *term) +{ + tll_foreach(term->normal.sixel_images, it) + sixel_invalidate_cache(&it->item); + + tll_foreach(term->alt.sixel_images, it) + sixel_invalidate_cache(&it->item); +} + +void +sixel_sync_cache(const struct terminal *term, struct sixel *six) +{ + if (six->pix != NULL) { +#if defined(_DEBUG) + if (six->cell_width == term->cell_width && + six->cell_height == term->cell_height) + { + xassert(six->pix == six->original.pix); + xassert(six->width == six->original.width); + xassert(six->height == six->original.height); + + xassert(six->scaled.data == NULL); + xassert(six->scaled.pix == NULL); + xassert(six->scaled.width < 0); + xassert(six->scaled.height < 0); + } else { + xassert(six->pix == six->scaled.pix); + xassert(six->width == six->scaled.width); + xassert(six->height == six->scaled.height); + + xassert(six->scaled.data != NULL); + xassert(six->scaled.pix != NULL); + + /* TODO: check ratio */ + xassert(six->scaled.width >= 0); + xassert(six->scaled.height >= 0); + } +#endif + return; + } + + /* Cache should be invalid */ + xassert(six->scaled.data == NULL); + xassert(six->scaled.pix == NULL); + xassert(six->scaled.width < 0); + xassert(six->scaled.height < 0); + + if (six->cell_width == term->cell_width && + six->cell_height == term->cell_height) + { + six->pix = six->original.pix; + six->width = six->original.width; + six->height = six->original.height; + } else { + const double width_ratio = (double)term->cell_width / six->cell_width; + const double height_ratio = (double)term->cell_height / six->cell_height; + + struct pixman_f_transform scale; + pixman_f_transform_init_scale( + &scale, 1. / width_ratio, 1. / height_ratio); + + struct pixman_transform _scale; + pixman_transform_from_pixman_f_transform(&_scale, &scale); + pixman_image_set_transform(six->original.pix, &_scale); + pixman_image_set_filter(six->original.pix, PIXMAN_FILTER_BILINEAR, NULL, 0); + + int scaled_width = (double)six->original.width * width_ratio; + int scaled_height = (double)six->original.height * height_ratio; + int scaled_stride = scaled_width * sizeof(uint32_t); + + LOG_DBG("scaling sixel: %dx%d -> %dx%d", + six->original.width, six->original.height, + scaled_width, scaled_height); + + uint8_t *scaled_data = xmalloc(scaled_height * scaled_stride); + pixman_image_t *scaled_pix = pixman_image_create_bits_no_clear( + term->sixel.pixman_fmt, scaled_width, scaled_height, + (uint32_t *)scaled_data, scaled_stride); + + pixman_image_composite32( + PIXMAN_OP_SRC, six->original.pix, NULL, scaled_pix, 0, 0, 0, 0, + 0, 0, scaled_width, scaled_height); + + pixman_image_set_transform(six->original.pix, NULL); + + six->scaled.data = scaled_data; + six->scaled.pix = six->pix = scaled_pix; + six->scaled.width = six->width = scaled_width; + six->scaled.height = six->height = scaled_height; + } +} + +void +sixel_reflow_grid(struct terminal *term, struct grid *grid) +{ + /* Meh - the sixel functions we call use term->grid... */ + struct grid *active_grid = term->grid; + term->grid = grid; + + /* Need the "real" list to be empty from the beginning */ + tll(struct sixel) copy = tll_init(); + tll_foreach(grid->sixel_images, it) + tll_push_back(copy, it->item); + tll_free(grid->sixel_images); + + tll_rforeach(copy, it) { + struct sixel *six = &it->item; + int start = six->pos.row; + int end = (start + six->rows - 1) & (grid->num_rows - 1); + + if (end < start) { + /* Crosses scrollback wrap-around */ + /* TODO: split image */ + sixel_destroy(six); + continue; + } + + if (six->rows > grid->num_rows) { + /* Image too large */ + /* TODO: keep bottom part? */ + sixel_destroy(six); + continue; + } + + /* Drop sixels that now cross the current scrollback end + * border. This is similar to a sixel that have been + * scrolled out */ + /* TODO: should be possible to optimize this */ + bool sixel_destroyed = false; + int last_row = -1; + + for (int j = 0; j < six->rows; j++) { + int row_no = grid_row_abs_to_sb( + term->grid, term->rows, six->pos.row + j); + if (last_row != -1 && last_row >= row_no) { + sixel_destroy(six); + sixel_destroyed = true; + break; + } + + last_row = row_no; + } + + if (sixel_destroyed) { + LOG_WARN("destroyed sixel that now crossed history"); + continue; + } + + /* Sixels that didn't overlap may now do so, which isn't + * allowed of course */ + _sixel_overwrite_by_rectangle( + term, six->pos.row, six->pos.col, six->rows, six->cols, + &it->item.original.pix, &it->item.opaque); + + if (it->item.original.data != pixman_image_get_data(it->item.original.pix)) { + it->item.original.data = pixman_image_get_data(it->item.original.pix); + it->item.original.width = pixman_image_get_width(it->item.original.pix); + it->item.original.height = pixman_image_get_height(it->item.original.pix); + it->item.cols = (it->item.original.width + it->item.cell_width - 1) / it->item.cell_width; + it->item.rows = (it->item.original.height + it->item.cell_height - 1) / it->item.cell_height; + sixel_invalidate_cache(&it->item); + } + + sixel_insert(term, it->item); + } + + tll_free(copy); + term->grid = active_grid; +} + +void +sixel_reflow(struct terminal *term) +{ + for (size_t i = 0; i < 2; i++) { + struct grid *grid = i == 0 ? &term->normal : &term->alt; + sixel_reflow_grid(term, grid); + } +} + +void +sixel_unhook(struct terminal *term) +{ + if (term->sixel.pos.row < term->sixel.image.height && + term->sixel.pos.row + 6 * term->sixel.pan >= term->sixel.image.height) + { + /* + * Handle case where image has had its size set by raster + * attributes, and then one or more sixels were printed on the + * last row of the RA area. + * + * In this case, the image height may not be a multiple of + * 6*pan. But the printed sixels may still be outside the RA + * area. In this case, using the size from the RA would + * truncate the image. + * + * So, extend the image to a multiple of 6*pan. + * + * If this is a transparent image, the image may get trimmed + * below (most likely back the size set by RA). + */ + term->sixel.image.height = term->sixel.image.alloc_height; + } + + /* Strip trailing fully transparent rows, *unless* we *ended* with + * a trailing GNL, in which case we do *not* want to strip all 6 + * pixel rows */ + if (term->sixel.pos.col > 0) { + const int bits = sizeof(term->sixel.image.bottom_pixel) * 8; + const int leading_zeroes = term->sixel.image.bottom_pixel == 0 + ? bits + : __builtin_clz(term->sixel.image.bottom_pixel); + const int rows_to_trim = leading_zeroes + 6 - bits; + + LOG_DBG("bottom-pixel: 0x%02x, bits=%d, leading-zeroes=%d, " + "rows-to-trim=%d*%d", term->sixel.image.bottom_pixel, + bits, leading_zeroes, rows_to_trim, term->sixel.pan); + + /* + * If the current graphical cursor position is at the last row + * of the image, *and* the image is transparent (P2=1), trim + * the entire image. + * + * If the image is not transparent, then we can't trim the RA + * region (it is supposed to "erase", with the current + * background color.) + * + * We *do* "trim" transparent rows from the graphical cursor + * position, as this affects the positioning of the text + * cursor. + * + * See https://raw.githubusercontent.com/hackerb9/vt340test/main/sixeltests/p2effect.sh + */ + if (term->sixel.pos.row + 6 * term->sixel.pan >= term->sixel.image.alloc_height) { + LOG_DBG("trimming image"); + const int trimmed_height = + term->sixel.image.alloc_height - rows_to_trim * term->sixel.pan; + + if (term->sixel.transparent_bg) { + /* Image is transparent - trim as much as possible */ + term->sixel.image.height = trimmed_height; + } else { + /* Image is opaque. We can't trim anything "inside" + the RA region */ + if (trimmed_height > term->sixel.image.height) { + /* There are non-empty pixels *outside* the RA + region - trim up to that point */ + term->sixel.image.height = trimmed_height; + } + } + } else { + LOG_DBG("only adjusting cursor position"); + } + + term->sixel.pos.row += 6 * term->sixel.pan; + term->sixel.pos.row -= rows_to_trim * term->sixel.pan; + } + + int pixel_row_idx = 0; + int pixel_rows_left = term->sixel.image.height; + const int stride = term->sixel.image.width * sizeof(uint32_t); + + /* + * When sixel scrolling is enabled (the default), sixels behave + * pretty much like normal output; the sixel starts at the current + * cursor position and the cursor is moved to a point after the + * sixel. + * + * Furthermore, if the sixel reaches the bottom of the scrolling + * region, the terminal content is scrolled. + * + * When scrolling is disabled, sixels always start at (0,0), the + * cursor is not moved at all, and the terminal content never + * scrolls. + */ + + const bool do_scroll = term->sixel.scrolling; + + /* Number of rows we're allowed to use. + * + * When scrolling is enabled, we always allow the entire sixel to + * be emitted. + * + * When disabled, only the number of screen rows may be used. */ + int rows_avail = do_scroll + ? (term->sixel.image.height + term->cell_height - 1) / term->cell_height + : term->scroll_region.end; + + /* Initial sixel coordinates */ + int start_row = do_scroll ? term->grid->cursor.point.row : 0; + const int start_col = do_scroll ? term->grid->cursor.point.col : 0; + + /* Total number of rows needed by image */ + const int rows_needed = + (term->sixel.image.height + term->cell_height - 1) / term->cell_height; + + bool free_image_data = true; + + /* We do not allow sixels to cross the scrollback wrap-around, as + * this makes intersection calculations much more complicated */ + while (pixel_rows_left > 0 && + rows_avail > 0 && + rows_needed <= term->grid->num_rows) + { + const int cur_row = (term->grid->offset + start_row) & (term->grid->num_rows - 1); + const int rows_left_until_wrap_around = term->grid->num_rows - cur_row; + const int usable_rows = min(rows_avail, rows_left_until_wrap_around); + + const int pixel_rows_avail = usable_rows * term->cell_height; + + const int width = term->sixel.image.width; + const int height = min(pixel_rows_left, pixel_rows_avail); + + uint32_t *img_data; + if (pixel_row_idx == 0 && height == pixel_rows_left) { + /* Entire image will be emitted as a single chunk - reuse + * the source buffer */ + img_data = term->sixel.image.data; + free_image_data = false; + } else { + xassert(free_image_data); + img_data = xmalloc(height * stride); + memcpy( + img_data, + &((uint8_t *)term->sixel.image.data)[pixel_row_idx * stride], + height * stride); + } + + struct sixel image = { + .pix = NULL, + .width = -1, + .height = -1, + .rows = (height + term->cell_height - 1) / term->cell_height, + .cols = (width + term->cell_width - 1) / term->cell_width, + .pos = (struct coord){start_col, cur_row}, + .opaque = !term->sixel.transparent_bg, + .cell_width = term->cell_width, + .cell_height = term->cell_height, + .original = { + .data = img_data, + .pix = NULL, + .width = width, + .height = height, + }, + .scaled = { + .data = NULL, + .pix = NULL, + .width = -1, + .height = -1, + }, + }; + + xassert(image.rows <= term->grid->num_rows); + xassert(image.pos.row + image.rows - 1 < term->grid->num_rows); + + LOG_DBG("generating %s %dx%d pixman image at %d-%d", + image.opaque ? "opaque" : "transparent", + image.original.width, image.original.height, + image.pos.row, image.pos.row + image.rows); + + image.original.pix = pixman_image_create_bits_no_clear( + term->sixel.pixman_fmt, image.original.width, image.original.height, + img_data, stride); + + pixel_row_idx += height; + pixel_rows_left -= height; + rows_avail -= image.rows; + + if (do_scroll) { + /* + * Linefeeds - always one less than the number of rows + * occupied by the image. + * + * Unless this is *not* the last chunk. In that case, + * linefeed past the chunk, so that the next chunk + * "starts" at a "new" row. + */ + const int linefeed_count = rows_avail == 0 + ? max(0, image.rows - 1) + : image.rows; + + xassert(rows_avail == 0 || + image.original.height % term->cell_height == 0); + + for (size_t i = 0; i < linefeed_count; i++) + term_linefeed(term); + + /* Position text cursor if this is the last image chunk */ + if (rows_avail == 0) { + int row = term->grid->cursor.point.row; + + /* + * Position the text cursor based on the text row + * touched by the last sixel + */ + const int pixel_rows = pixel_rows_left > 0 + ? image.original.height + : term->sixel.pos.row; + const int term_rows = + (pixel_rows + term->cell_height - 1) / term->cell_height; + + xassert(term_rows <= image.rows); + + row -= (image.rows - term_rows); + + term_cursor_to( + term, + max(0, row), + (term->sixel.cursor_right_of_graphics + ? min(image.pos.col + image.cols, term->cols - 1) + : image.pos.col)); + } + + term->sixel.pos.row -= image.original.height; + } + + /* Dirty touched cells, and scroll terminal content if necessary */ + for (size_t i = 0; i < image.rows; i++) { + struct row *row = term->grid->rows[cur_row + i]; + row->dirty = true; + + for (int col = image.pos.col; + col < min(image.pos.col + image.cols, term->cols); + col++) + { + row->cells[col].attrs.clean = 0; + } + + } + + _sixel_overwrite_by_rectangle( + term, image.pos.row, image.pos.col, image.rows, image.cols, + &image.original.pix, &image.opaque); + + if (image.original.data != pixman_image_get_data(image.original.pix)) { + image.original.data = pixman_image_get_data(image.original.pix); + image.original.width = pixman_image_get_width(image.original.pix); + image.original.height = pixman_image_get_height(image.original.pix); + image.cols = (image.original.width + image.cell_width - 1) / image.cell_width; + image.rows = (image.original.height + image.cell_height - 1) / image.cell_height; + sixel_invalidate_cache(&image); + } + + sixel_insert(term, image); + + if (do_scroll) + start_row = term->grid->cursor.point.row; + else + start_row -= image.rows; + } + + if (free_image_data) + free(term->sixel.image.data); + + term->sixel.image.data = NULL; + term->sixel.image.p = NULL; + term->sixel.image.width = 0; + term->sixel.image.height = 0; + term->sixel.pos = (struct coord){0, 0}; + + free(term->sixel.private_palette); + term->sixel.private_palette = NULL; + + LOG_DBG("you now have %zu sixels in current grid", + tll_length(term->grid->sixel_images)); + + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; + term_update_ascii_printer(term); + render_refresh(term); +} + +static void ALWAYS_INLINE inline +memset_u32(uint32_t *data, uint32_t value, size_t count) +{ + static_assert(sizeof(wchar_t) == 4, "wchar_t is not 4 bytes"); + wmemset((wchar_t *)data, (wchar_t)value, count); +} + +static void +resize_horizontally(struct terminal *term, int new_width_mutable) +{ + if (unlikely(new_width_mutable > term->sixel.max_width)) { + LOG_WARN("maximum image dimensions exceeded, truncating"); + new_width_mutable = term->sixel.max_width; + } + + if (unlikely(term->sixel.image.width >= new_width_mutable)) + return; + + const int sixel_row_height = 6 * term->sixel.pan; + + uint32_t *old_data = term->sixel.image.data; + const int old_width = term->sixel.image.width; + const int new_width = new_width_mutable; + + int height; + if (unlikely(term->sixel.image.height == 0)) { + /* Lazy initialize height on first printed sixel */ + xassert(old_width == 0); + term->sixel.image.height = height = sixel_row_height; + term->sixel.image.alloc_height = sixel_row_height; + } else + height = term->sixel.image.height; + + LOG_DBG("resizing image horizontally: %dx(%d) -> %dx(%d)", + term->sixel.image.width, term->sixel.image.height, + new_width, height); + + int alloc_height = (height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; + + xassert(new_width >= old_width); + xassert(new_width > 0); + xassert(alloc_height > 0); + + /* Width (and thus stride) change - need to allocate a new buffer */ + uint32_t *new_data = xmalloc(new_width * alloc_height * sizeof(uint32_t)); + + uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; + + /* Copy old rows, and initialize new columns to background color */ + const uint32_t *end = &new_data[alloc_height * new_width]; + for (uint32_t *n = new_data, *o = old_data; + n < end; + n += new_width, o += old_width) + { + memcpy(n, o, old_width * sizeof(uint32_t)); + memset_u32(&n[old_width], bg, new_width - old_width); + } + + free(old_data); + + term->sixel.image.data = new_data; + term->sixel.image.width = new_width; + + const int ofs = term->sixel.pos.row * new_width + term->sixel.pos.col; + term->sixel.image.p = &term->sixel.image.data[ofs]; +} + +static bool +resize_vertically(struct terminal *term, const int new_height) +{ + LOG_DBG("resizing image vertically: (%d)x%d -> (%d)x%d", + term->sixel.image.width, term->sixel.image.height, + term->sixel.image.width, new_height); + + if (unlikely(new_height > term->sixel.max_height)) { + LOG_WARN("maximum image dimensions reached"); + return false; + } + + uint32_t *old_data = term->sixel.image.data; + const int width = term->sixel.image.width; + const int old_height = term->sixel.image.height; + const int sixel_row_height = 6 * term->sixel.pan; + + int alloc_height = (new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; + + xassert(new_height > 0); + + if (unlikely(width == 0)) { + xassert(term->sixel.image.data == NULL); + term->sixel.image.height = new_height; + term->sixel.image.alloc_height = alloc_height; + return true; + } + + uint32_t *new_data = realloc( + old_data, width * alloc_height * sizeof(uint32_t)); + + if (new_data == NULL) { + LOG_ERRNO("failed to reallocate sixel image buffer"); + return false; + } + + const uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; + + memset_u32(&new_data[old_height * width], + bg, + (alloc_height - old_height) * width); + + term->sixel.image.height = new_height; + term->sixel.image.alloc_height = alloc_height; + + const int ofs = + term->sixel.pos.row * term->sixel.image.width + term->sixel.pos.col; + + term->sixel.image.data = new_data; + term->sixel.image.p = &term->sixel.image.data[ofs]; + + return true; +} + +static bool +resize(struct terminal *term, int new_width_mutable, int new_height_mutable) +{ + LOG_DBG("resizing image: %dx%d -> %dx%d", + term->sixel.image.width, term->sixel.image.height, + new_width_mutable, new_height_mutable); + + if (unlikely(new_width_mutable > term->sixel.max_width)) { + LOG_WARN("maximum image width exceeded, truncating"); + new_width_mutable = term->sixel.max_width; + } + + if (unlikely(new_height_mutable > term->sixel.max_height)) { + LOG_WARN("maximum image height exceeded, truncating"); + new_height_mutable = term->sixel.max_height; + } + + if (unlikely(new_height_mutable == 0)) { + new_height_mutable = 6 * term->sixel.pan; + } + + uint32_t *old_data = term->sixel.image.data; + const int old_width = term->sixel.image.width; + const int old_height = term->sixel.image.height; + const int new_width = new_width_mutable; + const int new_height = new_height_mutable; + + if (unlikely(old_width == new_width && old_height == new_height)) + return true; + + const int sixel_row_height = 6 * term->sixel.pan; + const int alloc_new_height = + (new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; + + xassert(alloc_new_height >= new_height); + xassert(alloc_new_height - new_height < sixel_row_height); + + uint32_t *new_data = NULL; + const uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; + + /* + * If the image is resized horizontally, or if it's opaque, we + * need to explicitly initialize the "new" pixels. + * + * When the image is *not* resized horizontally, we simply do a + * realloc(). In this case, there's no need to manually copy the + * old pixels. We do however need to initialize the new pixels + * since realloc() returns uninitialized memory. + * + * When the image *is* resized horizontally, we need to allocate + * new memory (when the width changes, the stride changes, and + * thus we cannot simply realloc()) + * + * If the default background is transparent, the new pixels need + * to be initialized to 0x0. We do this by using calloc(). + * + * If the default background is opaque, then we need to manually + * initialize the new pixels. + */ + const bool initialize_bg = + !term->sixel.transparent_bg || new_width == old_width; + + if (new_width == old_width) { + /* Width (and thus stride) is the same, so we can simply + * re-alloc the existing buffer */ + + new_data = realloc(old_data, new_width * alloc_new_height * sizeof(uint32_t)); + if (new_data == NULL) { + LOG_ERRNO("failed to reallocate sixel image buffer"); + return false; + } + + xassert(new_height > old_height); + + } else { + /* Width (and thus stride) change - need to allocate a new buffer */ + xassert(new_width > old_width); + const size_t pixels = new_width * alloc_new_height; + + new_data = !initialize_bg + ? xcalloc(pixels, sizeof(uint32_t)) + : xmalloc(pixels * sizeof(uint32_t)); + + /* Copy old rows, and initialize new columns to background color */ + const int row_copy_count = min(old_height, alloc_new_height); + const uint32_t *end = &new_data[row_copy_count * new_width]; + + for (uint32_t *n = new_data, *o = old_data; + n < end; + n += new_width, o += old_width) + { + memcpy(n, o, old_width * sizeof(uint32_t)); + memset_u32(&n[old_width], bg, new_width - old_width); + } + free(old_data); + } + + if (initialize_bg) { + memset_u32(&new_data[old_height * new_width], + bg, + (alloc_new_height - old_height) * new_width); + } + + xassert(new_data != NULL); + term->sixel.image.data = new_data; + term->sixel.image.width = new_width; + term->sixel.image.height = new_height; + term->sixel.image.alloc_height = alloc_new_height; + term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * new_width + term->sixel.pos.col]; + + return true; +} + +static void +sixel_add_generic(struct terminal *term, uint32_t *data, int stride, uint32_t color, + uint8_t sixel) +{ + const int pan = term->sixel.pan; + + for (int i = 0; i < 6; i++, sixel >>= 1) { + if (sixel & 1) { + for (int r = 0; r < pan; r++, data += stride) + *data = color; + } else + data += stride * pan; + } + + xassert(sixel == 0); +} + +static void ALWAYS_INLINE inline +sixel_add_ar_11(struct terminal *term, uint32_t *data, int stride, uint32_t color, + uint8_t sixel) +{ + xassert(term->sixel.pan == 1); + + if (sixel & 0x01) + *data = color; + data += stride; + if (sixel & 0x02) + *data = color; + data += stride; + if (sixel & 0x04) + *data = color; + data += stride; + if (sixel & 0x08) + *data = color; + data += stride; + if (sixel & 0x10) + *data = color; + data += stride; + if (sixel & 0x20) + *data = color; +} + +static void +sixel_add_many_generic(struct terminal *term, uint8_t c, unsigned count) +{ + int col = term->sixel.pos.col; + int width = term->sixel.image.width; + + count *= term->sixel.pad; + + if (unlikely(col + count - 1 >= width)) { + resize_horizontally(term, col + count); + width = term->sixel.image.width; + count = min(count, max(width - col, 0)); + + if (unlikely(count == 0)) + return; + } + + uint32_t color = term->sixel.color; + uint32_t *data = term->sixel.image.p; + uint32_t *end = data + count; + + term->sixel.pos.col = col + count; + term->sixel.image.p = end; + term->sixel.image.bottom_pixel |= c; + + for (; data < end; data++) + sixel_add_generic(term, data, width, color, c); + +} + +static void ALWAYS_INLINE inline +sixel_add_one_ar_11(struct terminal *term, uint8_t c) +{ + xassert(term->sixel.pan == 1); + xassert(term->sixel.pad == 1); + + int col = term->sixel.pos.col; + int width = term->sixel.image.width; + + if (unlikely(col >= width)) { + resize_horizontally(term, col + count); + width = term->sixel.image.width; + count = min(count, max(width - col, 0)); + + if (unlikely(count == 0)) + return; + } + + uint32_t *data = term->sixel.image.p; + + term->sixel.pos.col += 1; + term->sixel.image.p += 1; + term->sixel.image.bottom_pixel |= c; + + sixel_add_ar_11(term, data, width, term->sixel.color, c); +} + +static void +sixel_add_many_ar_11(struct terminal *term, uint8_t c, unsigned count) +{ + xassert(term->sixel.pan == 1); + xassert(term->sixel.pad == 1); + + int col = term->sixel.pos.col; + int width = term->sixel.image.width; + + if (unlikely(col + count - 1 >= width)) { + resize_horizontally(term, col + count); + width = term->sixel.image.width; + count = min(count, max(width - col, 0)); + + if (unlikely(count == 0)) + return; + } + + uint32_t color = term->sixel.color; + uint32_t *data = term->sixel.image.p; + uint32_t *end = data + count; + + term->sixel.pos.col += count; + term->sixel.image.p = end; + term->sixel.image.bottom_pixel |= c; + + for (; data < end; data++) + sixel_add_ar_11(term, data, width, color, c); + +} + +IGNORE_WARNING("-Wpedantic") + +static void +decsixel_generic(struct terminal *term, uint8_t c) +{ + switch (c) { + case '"': + term->sixel.state = SIXEL_DECGRA; + term->sixel.param = 0; + term->sixel.param_idx = 0; + break; + + case '!': + term->sixel.state = SIXEL_DECGRI; + term->sixel.param = 0; + term->sixel.param_idx = 0; + term->sixel.repeat_count = 1; + break; + + case '#': + term->sixel.state = SIXEL_DECGCI; + term->sixel.color_idx = 0; + term->sixel.param = 0; + term->sixel.param_idx = 0; + break; + + case '$': + if (likely(term->sixel.pos.col <= term->sixel.max_width)) { + /* + * We set, and keep, 'col' outside the image boundary when + * we've reached the maximum image height, to avoid also + * having to check the row vs image height in the common + * path in sixel_add(). + */ + term->sixel.pos.col = 0; + term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width]; + } + break; + + case '-': /* GNL - Graphical New Line */ + term->sixel.pos.row += 6 * term->sixel.pan; + term->sixel.pos.col = 0; + term->sixel.image.bottom_pixel = 0; + term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width]; + + if (term->sixel.pos.row >= term->sixel.image.alloc_height) { + if (!resize_vertically(term, term->sixel.pos.row + 6 * term->sixel.pan)) + term->sixel.pos.col = term->sixel.max_width + 1 * term->sixel.pad; + } + break; + + case '?' ... '~': + sixel_add_many_generic(term, c - 63, 1); + break; + + case ' ': + case '\n': + case '\r': + break; + + default: + LOG_WARN("invalid sixel character: '%c' at idx=%zu", c, count); + break; + } +} + +UNIGNORE_WARNINGS + +static void +decsixel_ar_11(struct terminal *term, uint8_t c) +{ + if (likely(c >= '?' && c <= '~')) + sixel_add_one_ar_11(term, c - 63); + else + decsixel_generic(term, c); +} + +static void +decgra(struct terminal *term, uint8_t c) +{ + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + term->sixel.param *= 10; + term->sixel.param += c - '0'; + break; + + case ';': + if (term->sixel.param_idx < ALEN(term->sixel.params)) + term->sixel.params[term->sixel.param_idx++] = term->sixel.param; + term->sixel.param = 0; + break; + + default: { + if (term->sixel.param_idx < ALEN(term->sixel.params)) + term->sixel.params[term->sixel.param_idx++] = term->sixel.param; + + int nparams = term->sixel.param_idx; + unsigned pan = nparams > 0 ? term->sixel.params[0] : 0; + unsigned pad = nparams > 1 ? term->sixel.params[1] : 0; + unsigned ph = nparams > 2 ? term->sixel.params[2] : 0; + unsigned pv = nparams > 3 ? term->sixel.params[3] : 0; + + pan = pan > 0 ? pan : 1; + pad = pad > 0 ? pad : 1; + + if (likely(term->sixel.image.width == 0 && + term->sixel.image.height == 0)) + { + term->sixel.pan = pan; + term->sixel.pad = pad; + } else { + /* + * Unsure what the VT340 does... + * + * We currently do *not* handle changing pan/pad in the + * middle of a sixel, since that means resizing/stretching + * the existing image. + * + * I'm *guessing* the VT340 simply changes the aspect + * ratio of all subsequent sixels. But, given the design + * of our implementation (the entire sixel is written to a + * single pixman image), we can't easily do that. + */ + LOG_WARN("sixel: unsupported: pan/pad changed after printing sixels"); + pan = term->sixel.pan; + pad = term->sixel.pad; + } + + pv *= pan; + ph *= pad; + + LOG_DBG("pan=%u, pad=%u (aspect ratio = %d:%d), size=%ux%u", + pan, pad, pan, pad, ph, pv); + + /* + * RA really only acts as a rectangular erase - it fills the + * specified area with the sixel background color[^1]. Nothing + * else. It does *not* affect cursor positioning. + * + * This means that if the emitted sixel is *smaller* than the + * RA, the text cursor will be placed "inside" the RA area. + * + * This means it would be more correct to view the RA area as + * a *separate* sixel image, that is then overlaid with the + * actual sixel. + * + * Still, RA _is_ a hint - the final image is _likely_ going + * to be this large. And, treating RA as a separate image + * prevents us from pre-allocating the final sixel image. + * + * So we don't. We use the RA as a hint, and pre-allocates the + * backing image buffer. + * + * [^1]: i.e. it's a NOP if the sixel is transparent + */ + if (ph >= term->sixel.image.height && pv >= term->sixel.image.width && + ph <= term->sixel.max_height && pv <= term->sixel.max_width) + { + /* + * TODO: always resize to a multiple of 6*pan? + * + * We're effectively doing that already, except + * sixel.image.height is set to ph, instead of the + * allocated height (which is always a multiple of 6*pan). + * + * If the user wants to emit a sixel that isn't a multiple + * of 6 pixels, the bottom sixel rows should all be empty, + * and (assuming a transparent sixel), trimmed when the + * final image is generated. + */ + resize(term, ph, pv); + } + + term->sixel.state = SIXEL_DECSIXEL; + + /* Update DCS put handler, since pan/pad may have changed */ + term->vt.dcs.put_handler = pan == 1 && pad == 1 + ? &sixel_put_ar_11 + : &sixel_put_generic; + + if (likely(pan == 1 && pad == 1)) + decsixel_ar_11(term, c); + else + decsixel_generic(term, c); + + break; + } + } +} + +IGNORE_WARNING("-Wpedantic") + +static void +decgri_generic(struct terminal *term, uint8_t c) +{ + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': { + unsigned param = term->sixel.param; + param *= 10; + param += c - '0'; + term->sixel.repeat_count = term->sixel.param = param; + break; + } + + case '?' ... '~': { + unsigned count = term->sixel.repeat_count; + if (unlikely(count == 0)) { + count = 1; + } + + sixel_add_many_generic(term, c - 63, count); + term->sixel.state = SIXEL_DECSIXEL; + break; + } + + default: + term->sixel.state = SIXEL_DECSIXEL; + term->vt.dcs.put_handler(term, c); + break; + } +} + +UNIGNORE_WARNINGS + +static void +decgri_ar_11(struct terminal *term, uint8_t c) +{ + if (likely(c >= '?' && c <= '~')) { + unsigned count = term->sixel.repeat_count; + if (unlikely(count == 0)) { + count = 1; + } + + sixel_add_many_ar_11(term, c - 63, count); + term->sixel.state = SIXEL_DECSIXEL; + } else + decgri_generic(term, c); +} + +static void +decgci(struct terminal *term, uint8_t c) +{ + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + term->sixel.param *= 10; + term->sixel.param += c - '0'; + break; + + case ';': + if (term->sixel.param_idx < ALEN(term->sixel.params)) + term->sixel.params[term->sixel.param_idx++] = term->sixel.param; + term->sixel.param = 0; + break; + + default: { + if (term->sixel.param_idx < ALEN(term->sixel.params)) + term->sixel.params[term->sixel.param_idx++] = term->sixel.param; + + int nparams = term->sixel.param_idx; + + if (nparams > 0) + term->sixel.color_idx = min(term->sixel.params[0], term->sixel.palette_size - 1); + + if (nparams > 4) { + unsigned format = term->sixel.params[1]; + int c1 = term->sixel.params[2]; + int c2 = term->sixel.params[3]; + int c3 = term->sixel.params[4]; + + switch (format) { + case 1: { /* HLS */ + int hue = min(c1, 360); + int lum = min(c2, 100); + int sat = min(c3, 100); + + /* + * Sixel's HLS use the following primary color hues: + * blue: 0° + * red: 120° + * green: 240° + * + * While "standard" HSL uses: + * red: 0° + * green: 120° + * blue: 240° + */ + hue = (hue + 240) % 360; + + uint32_t rgb = hsl_to_rgb(hue, sat, lum); + + LOG_DBG("setting palette #%d = HLS %hhu/%hhu/%hhu (0x%06x)", + term->sixel.color_idx, hue, lum, sat, rgb); + + term->sixel.palette[term->sixel.color_idx] = 0xffu << 24 | rgb; + break; + } + + case 2: { /* RGB */ + uint16_t r = 255 * min(c1, 100) / 100; + uint16_t g = 255 * min(c2, 100) / 100; + uint16_t b = 255 * min(c3, 100) / 100; + + LOG_DBG("setting palette #%d = RGB %hu/%hu/%hu", + term->sixel.color_idx, r, g, b); + + term->sixel.palette[term->sixel.color_idx] = color_decode_srgb(term, r, g, b); + break; + } + } + } else + term->sixel.color = term->sixel.palette[term->sixel.color_idx]; + + term->sixel.state = SIXEL_DECSIXEL; + + if (likely(term->sixel.pan == 1 && term->sixel.pad == 1)) + decsixel_ar_11(term, c); + else + decsixel_generic(term, c); + break; + } + } +} + +static void +sixel_put_generic(struct terminal *term, uint8_t c) +{ + switch (term->sixel.state) { + case SIXEL_DECSIXEL: decsixel_generic(term, c); break; + case SIXEL_DECGRA: decgra(term, c); break; + case SIXEL_DECGRI: decgri_generic(term, c); break; + case SIXEL_DECGCI: decgci(term, c); break; + } + + count++; +} + +static void +sixel_put_ar_11(struct terminal *term, uint8_t c) +{ + switch (term->sixel.state) { + case SIXEL_DECSIXEL: decsixel_ar_11(term, c); break; + case SIXEL_DECGRA: decgra(term, c); break; + case SIXEL_DECGRI: decgri_ar_11(term, c); break; + case SIXEL_DECGCI: decgci(term, c); break; + } + + count++; +} + +void +sixel_colors_report_current(struct terminal *term) +{ + char reply[24]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[?1;0;%uS", term->sixel.palette_size); + term_to_slave(term, reply, n); + LOG_DBG("query response for current color count: %u", term->sixel.palette_size); +} + +void +sixel_colors_reset(struct terminal *term) +{ + LOG_DBG("sixel palette size reset to %u", SIXEL_MAX_COLORS); + + free(term->sixel.palette); + term->sixel.palette = NULL; + + term->sixel.palette_size = SIXEL_MAX_COLORS; + sixel_colors_report_current(term); +} + +void +sixel_colors_set(struct terminal *term, unsigned count) +{ + unsigned new_palette_size = min(max(2, count), SIXEL_MAX_COLORS); + LOG_DBG("sixel palette size set to %u", new_palette_size); + + free(term->sixel.private_palette); + free(term->sixel.shared_palette); + term->sixel.private_palette = NULL; + term->sixel.shared_palette = NULL; + + term->sixel.palette_size = new_palette_size; + sixel_colors_report_current(term); +} + +void +sixel_colors_report_max(struct terminal *term) +{ + char reply[24]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[?1;0;%uS", SIXEL_MAX_COLORS); + term_to_slave(term, reply, n); + LOG_DBG("query response for max color count: %u", SIXEL_MAX_COLORS); +} + +void +sixel_geometry_report_current(struct terminal *term) +{ + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[?2;0;%u;%uS", + min(term->cols * term->cell_width, term->sixel.max_width), + min(term->rows * term->cell_height, term->sixel.max_height)); + term_to_slave(term, reply, n); + + LOG_DBG("query response for current sixel geometry: %ux%u", + term->sixel.max_width, term->sixel.max_height); +} + +void +sixel_geometry_reset(struct terminal *term) +{ + LOG_DBG("sixel geometry reset to %ux%u", SIXEL_MAX_WIDTH, SIXEL_MAX_HEIGHT); + term->sixel.max_width = SIXEL_MAX_WIDTH; + term->sixel.max_height = SIXEL_MAX_HEIGHT; + sixel_geometry_report_current(term); +} + +void +sixel_geometry_set(struct terminal *term, unsigned width, unsigned height) +{ + LOG_DBG("sixel geometry set to %ux%u", width, height); + term->sixel.max_width = width; + term->sixel.max_height = height; + sixel_geometry_report_current(term); +} + +void +sixel_geometry_report_max(struct terminal *term) +{ + unsigned max_width = term->sixel.max_width; + unsigned max_height = term->sixel.max_height; + + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[?2;0;%u;%uS", max_width, max_height); + term_to_slave(term, reply, n); + + LOG_DBG("query response for max sixel geometry: %ux%u", + max_width, max_height); +} diff --git a/sixel.h b/sixel.h new file mode 100644 index 0000000..ab8a505 --- /dev/null +++ b/sixel.h @@ -0,0 +1,50 @@ +#pragma once + +#include "terminal.h" + +#define SIXEL_MAX_COLORS 1024u +#define SIXEL_MAX_WIDTH 10000u +#define SIXEL_MAX_HEIGHT 10000u + +typedef void (*sixel_put)(struct terminal *term, uint8_t c); + +void sixel_fini(struct terminal *term); + +sixel_put sixel_init(struct terminal *term, int p1, int p2, int p3); +void sixel_unhook(struct terminal *term); + +void sixel_destroy(struct sixel *sixel); +void sixel_destroy_all(struct terminal *term); + +void sixel_scroll_up(struct terminal *term, int rows); +void sixel_scroll_down(struct terminal *term, int rows); + +void sixel_cell_size_changed(struct terminal *term); +void sixel_sync_cache(const struct terminal *term, struct sixel *sixel); + +void sixel_reflow_grid(struct terminal *term, struct grid *grid); + +/* Shortcut for sixel_reflow_grid(normal) + sixel_reflow_grid(alt) */ +void sixel_reflow(struct terminal *term); + +/* + * Remove sixel data from the specified location. Used when printing + * or erasing characters, and when emitting new sixel images, to + * remove sixel data that would otherwise be rendered on-top. + * + * Row numbers are relative to the current grid offset + */ +void sixel_overwrite_by_rectangle( + struct terminal *term, int row, int col, int height, int width); +void sixel_overwrite_by_row(struct terminal *term, int row, int col, int width); +void sixel_overwrite_at_cursor(struct terminal *term, int width); + +void sixel_colors_report_current(struct terminal *term); +void sixel_colors_reset(struct terminal *term); +void sixel_colors_set(struct terminal *term, unsigned count); +void sixel_colors_report_max(struct terminal *term); + +void sixel_geometry_report_current(struct terminal *term); +void sixel_geometry_reset(struct terminal *term); +void sixel_geometry_set(struct terminal *term, unsigned width, unsigned height); +void sixel_geometry_report_max(struct terminal *term); diff --git a/slave.c b/slave.c new file mode 100644 index 0000000..6289937 --- /dev/null +++ b/slave.c @@ -0,0 +1,573 @@ +#include "slave.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define LOG_MODULE "slave" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "debug.h" +#include "macros.h" +#include "tokenize.h" +#include "util.h" +#include "xmalloc.h" + +extern char **environ; + +struct environ { + size_t count; + char **envp; +}; + +#if !defined(EXECVPE) +static char * +find_file_in_path(const char *file) +{ + if (strchr(file, '/') != NULL) + return xstrdup(file); + + const char *env_path = getenv("PATH"); + char *path_list = NULL; + + if (env_path != NULL && env_path[0] != '\0') + path_list = xstrdup(env_path); + else { + size_t sc_path_len = confstr(_CS_PATH, NULL, 0); + if (sc_path_len > 0) { + path_list = xmalloc(sc_path_len); + confstr(_CS_PATH, path_list, sc_path_len); + } else + return xstrdup(file); + } + + for (const char *path = strtok(path_list, ":"); + path != NULL; + path = strtok(NULL, ":")) + { + char *full = xstrjoin3(path, "/", file); + if (access(full, F_OK) == 0) { + free(path_list); + return full; + } + + free(full); + } + + free(path_list); + return xstrdup(file); +} + +static int +foot_execvpe(const char *file, char *const argv[], char *const envp[]) +{ + char *path = find_file_in_path(file); + int ret = execve(path, argv, envp); + + /* + * Getting here is an error + */ + free(path); + return ret; +} + +#else /* EXECVPE */ + +#define foot_execvpe(file, argv, envp) execvpe(file, argv, envp) + +#endif /* EXECVPE */ + +static bool +is_valid_shell(const char *shell) +{ + FILE *f = fopen("/etc/shells", "r"); + if (f == NULL) + goto err; + + char *_line = NULL; + size_t count = 0; + + while (true) { + errno = 0; + ssize_t ret = getline(&_line, &count, f); + + if (ret < 0) { + free(_line); + break; + } + + char *line = _line; + { + while (isspace(*line)) + line++; + if (line[0] != '\0') { + char *end = line + strlen(line) - 1; + while (isspace(*end)) + end--; + *(end + 1) = '\0'; + } + } + + if (line[0] == '#') + continue; + + if (streq(line, shell)) { + fclose(f); + return true; + } + } + +err: + if (f != NULL) + fclose(f); + return false; +} + +enum user_notification_ret_t {UN_OK, UN_NO_MORE, UN_FAIL}; + +static enum user_notification_ret_t +emit_one_notification(int fd, const struct user_notification *notif) +{ + const char *prefix = NULL; + const char *postfix = "\033[m\n"; + + switch (notif->kind) { + case USER_NOTIFICATION_DEPRECATED: + prefix = "\033[33;1mdeprecated\033[39;22m: "; + break; + + case USER_NOTIFICATION_WARNING: + prefix = "\033[33;1mwarning\033[39;22m: "; + break; + + case USER_NOTIFICATION_ERROR: + prefix = "\033[31;1merror\033[39;22m: "; + break; + } + + xassert(prefix != NULL); + + if (write(fd, prefix, strlen(prefix)) < 0 || + write(fd, "foot: ", 6) < 0 || + write(fd, notif->text, strlen(notif->text)) < 0 || + write(fd, postfix, strlen(postfix)) < 0) + { + /* + * The main process is blocking and waiting for us to close + * the error pipe. Thus, pts data will *not* be processed + * until we've exec:d. This means we cannot write anymore once + * the kernel buffer is full. Don't treat this as a fatal + * error. + */ + if (errno == EWOULDBLOCK || errno == EAGAIN) + return UN_NO_MORE; + else { + LOG_ERRNO("failed to write user-notification"); + return UN_FAIL; + } + } + + return UN_OK; +} + +static bool +emit_notifications_of_kind(int fd, + const user_notifications_t *notifications, + enum user_notification_kind kind) +{ + tll_foreach(*notifications, it) { + if (it->item.kind == kind) { + switch (emit_one_notification(fd, &it->item)) { + case UN_OK: + break; + case UN_NO_MORE: + return true; + case UN_FAIL: + return false; + } + } + } + + return true; +} + +static bool +emit_notifications(int fd, const user_notifications_t *notifications) +{ + return + emit_notifications_of_kind(fd, notifications, USER_NOTIFICATION_ERROR) && + emit_notifications_of_kind(fd, notifications, USER_NOTIFICATION_WARNING) && + emit_notifications_of_kind(fd, notifications, USER_NOTIFICATION_DEPRECATED); +} + +static noreturn void +slave_exec(int ptmx, char *argv[], char *const envp[], int err_fd, + bool login_shell, const user_notifications_t *notifications) +{ + int pts = -1; + const char *pts_name = ptsname(ptmx); + + if (grantpt(ptmx) == -1) { + LOG_ERRNO("failed to grantpt()"); + goto err; + } + if (unlockpt(ptmx) == -1) { + LOG_ERRNO("failed to unlockpt()"); + goto err; + } + + close(ptmx); + ptmx = -1; + + if (setsid() == -1) { + LOG_ERRNO("failed to setsid()"); + goto err; + } + + pts = open(pts_name, O_RDWR); + if (pts == -1) { + LOG_ERRNO("failed to open pseudo terminal slave device"); + goto err; + } + + if (ioctl(pts, TIOCSCTTY, 0) < 0) { + LOG_ERRNO("failed to configure controlling terminal"); + goto err; + } + +#ifdef IUTF8 + { + struct termios flags; + if (tcgetattr(pts, &flags) < 0) { + LOG_ERRNO("failed to get terminal attributes"); + goto err; + } + + flags.c_iflag |= IUTF8; + if (tcsetattr(pts, TCSANOW, &flags) < 0) { + LOG_ERRNO("failed to set IUTF8 terminal attribute"); + goto err; + } + } +#endif + + if (tll_length(*notifications) > 0) { + int flags = fcntl(pts, F_GETFL); + if (flags < 0) + goto err; + if (fcntl(pts, F_SETFL, flags | O_NONBLOCK) < 0) + goto err; + + if (!emit_notifications(pts, notifications)) + goto err; + + fcntl(pts, F_SETFL, flags); + } + + if (dup2(pts, STDIN_FILENO) == -1 || + dup2(pts, STDOUT_FILENO) == -1 || + dup2(pts, STDERR_FILENO) == -1) + { + LOG_ERRNO("failed to dup stdin/stdout/stderr"); + goto err; + } + + close(pts); + pts = -1; + + const char *file; + if (login_shell) { + file = xstrdup(argv[0]); + + char *arg0 = xmalloc(strlen(argv[0]) + 1 + 1); + arg0[0] = '-'; + arg0[1] = '\0'; + strcat(arg0, argv[0]); + + argv[0] = arg0; + } else + file = argv[0]; + + foot_execvpe(file, argv, envp); + +err: + (void)!write(err_fd, &errno, sizeof(errno)); + if (pts != -1) + close(pts); + if (ptmx != -1) + close(ptmx); + close(err_fd); + _exit(errno); +} + +static bool +env_matches_var_name(const char *e, const char *name) +{ + const size_t e_len = strlen(e); + const size_t name_len = strlen(name); + + if (e_len <= name_len) + return false; + if (memcmp(e, name, name_len) != 0) + return false; + if (e[name_len] != '=') + return false; + return true; +} + +static void +add_to_env(struct environ *env, const char *name, const char *value) +{ + if (env->envp == NULL) + setenv(name, value, 1); + else { + char *e = xstrjoin3(name, "=", value); + + /* Search for existing variable. If found, replace it with the + new value */ + for (size_t i = 0; i < env->count; i++) { + if (env_matches_var_name(env->envp[i], name)) { + free(env->envp[i]); + env->envp[i] = e; + return; + } + } + + /* If the variable does not already exist, add it */ + env->envp = xrealloc(env->envp, (env->count + 2) * sizeof(env->envp[0])); + env->envp[env->count++] = e; + env->envp[env->count] = NULL; + } +} + +static void +del_from_env(struct environ *env, const char *name) +{ + if (env->envp == NULL) + unsetenv(name); + else { + for (size_t i = 0; i < env->count; i++) { + if (env_matches_var_name(env->envp[i], name)) { + free(env->envp[i]); + memmove(&env->envp[i], + &env->envp[i + 1], + (env->count - i) * sizeof(env->envp[0])); + env->count--; + xassert(env->envp[env->count] == NULL); + break; + } + } + } +} + +pid_t +slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, + const char *const *envp, const env_var_list_t *extra_env_vars, + const char *term_env, const char *conf_shell, bool login_shell, + const user_notifications_t *notifications) +{ + int fork_pipe[2]; + if (pipe2(fork_pipe, O_CLOEXEC) < 0) { + LOG_ERRNO("failed to create pipe"); + return -1; + } + + pid_t pid = fork(); + switch (pid) { + case -1: + LOG_ERRNO("failed to fork"); + close(fork_pipe[0]); + close(fork_pipe[1]); + return -1; + + case 0: + /* Child */ + close(fork_pipe[0]); /* Close read end */ + + if (chdir(cwd) < 0) { + const int errno_copy = errno; + LOG_ERRNO("failed to change working directory to %s", cwd); + (void)!write(fork_pipe[1], &errno_copy, sizeof(errno_copy)); + _exit(errno_copy); + } + + /* Restore signal mask, and SIG_IGN'd signals */ + struct sigaction dfl = {.sa_handler = SIG_DFL}; + sigemptyset(&dfl.sa_mask); + sigset_t mask; + sigemptyset(&mask); + + if (sigprocmask(SIG_SETMASK, &mask, NULL) < 0 || + sigaction(SIGHUP, &dfl, NULL) < 0 || + sigaction(SIGPIPE, &dfl, NULL) < 0) + { + const int errno_copy = errno; + LOG_ERRNO_P(errno, "failed to restore signals"); + (void)!write(fork_pipe[1], &errno_copy, sizeof(errno_copy)); + _exit(errno_copy); + } + + /* Create a mutable copy of the environment */ + struct environ custom_env = {0}; + if (envp != NULL) { + for (const char *const *e = envp; *e != NULL; e++) + custom_env.count++; + + custom_env.envp = xcalloc( + custom_env.count + 1, sizeof(custom_env.envp[0])); + + size_t i = 0; + for (const char *const *e = envp; *e != NULL; e++, i++) + custom_env.envp[i] = xstrdup(*e); + xassert(custom_env.envp[custom_env.count] == NULL); + } + + add_to_env(&custom_env, "TERM", term_env); + add_to_env(&custom_env, "COLORTERM", "truecolor"); + add_to_env(&custom_env, "PWD", cwd); + + del_from_env(&custom_env, "TERM_PROGRAM"); /* Wezterm, Ghostty */ + del_from_env(&custom_env, "TERM_PROGRAM_VERSION"); /* Wezterm, Ghostty */ + del_from_env(&custom_env, "TERMINAL_NAME"); /* Contour */ + del_from_env(&custom_env, "TERMINAL_VERSION_STRING"); /* Contour */ + del_from_env(&custom_env, "TERMINAL_VERSION_TRIPLE"); /* Contour */ + + /* XTerm specific */ + del_from_env(&custom_env, "XTERM_SHELL"); + del_from_env(&custom_env, "XTERM_VERSION"); + del_from_env(&custom_env, "XTERM_LOCALE"); + + /* Mlterm specific */ + del_from_env(&custom_env, "MLTERM"); + + /* Zutty specific */ + del_from_env(&custom_env, "ZUTTY_VERSION"); + + /* Ghostty specific */ + del_from_env(&custom_env, "GHOSTTY_BIN_DIR"); + del_from_env(&custom_env, "GHOSTTY_SHELL_INTEGRATION_NO_SUDO"); + del_from_env(&custom_env, "GHOSTTY_RESOURCES_DIR"); + + /* Kitty specific */ + del_from_env(&custom_env, "KITTY_WINDOW_ID"); + del_from_env(&custom_env, "KITTY_PID"); + del_from_env(&custom_env, "KITTY_PUBLIC_KEY"); + del_from_env(&custom_env, "KITTY_INSTALLATION_DIR"); + + /* Contour specific */ + del_from_env(&custom_env, "CONTOUR_PROFILE"); + + /* Wezterm specific */ + del_from_env(&custom_env, "WEZTERM_PANE"); + del_from_env(&custom_env, "WEZTERM_EXECUTABLE"); + del_from_env(&custom_env, "WEZTERM_CONFIG_FILE"); + del_from_env(&custom_env, "WEZTERM_EXECUTABLE_DIR"); + del_from_env(&custom_env, "WEZTERM_UNIX_SOCKET"); + del_from_env(&custom_env, "WEZTERM_CONFIG_DIR"); + + /* Alacritty specific */ + del_from_env(&custom_env, "ALACRITTY_LOG"); + del_from_env(&custom_env, "ALACRITTY_WINDOW_ID"); + del_from_env(&custom_env, "ALACRITTY_SOCKET"); + + /* VTE, gnome-terminal, kgx etc */ + del_from_env(&custom_env, "VTE_VERSION"); + del_from_env(&custom_env, "GNOME_TERMINAL_SERVICE"); + del_from_env(&custom_env, "GNOME_TERMINAL_SCREEN"); + +#if defined(FOOT_TERMINFO_PATH) + add_to_env(&custom_env, "TERMINFO", FOOT_TERMINFO_PATH); +#endif + + if (extra_env_vars != NULL) { + tll_foreach(*extra_env_vars, it) { + const char *name = it->item.name; + const char *value = it->item.value; + + if (strlen(value) == 0) + del_from_env(&custom_env, name); + else + add_to_env(&custom_env, name, value); + } + } + + char **_shell_argv = NULL; + char **shell_argv = NULL; + + if (argc == 0) { + if (!tokenize_cmdline(conf_shell, &_shell_argv)) { + (void)!write(fork_pipe[1], &errno, sizeof(errno)); + _exit(0); + } + shell_argv = _shell_argv; + } else { + size_t count = 0; + for (; argv[count] != NULL; count++) + ; + shell_argv = xmalloc((count + 1) * sizeof(shell_argv[0])); + for (size_t i = 0; i < count; i++) + shell_argv[i] = argv[i]; + shell_argv[count] = NULL; + } + + if (is_valid_shell(shell_argv[0])) + add_to_env(&custom_env, "SHELL", shell_argv[0]); + + slave_exec(ptmx, shell_argv, + custom_env.envp != NULL ? custom_env.envp : environ, + fork_pipe[1], login_shell, notifications); + BUG("Unexpected return from slave_exec()"); + break; + + default: { + + /* + * Don't stay in CWD, since it may be an ephemeral path. For + * example, it may be a mount point of, say, a thumb drive. Us + * keeping it open will prevent the user from unmounting it. + */ + (void)!!chdir("/"); + + close(fork_pipe[1]); /* Close write end */ + LOG_DBG("slave has PID %d", pid); + + int errno_copy; + static_assert(sizeof(errno) == sizeof(errno_copy), "errno size mismatch"); + + ssize_t ret = read(fork_pipe[0], &errno_copy, sizeof(errno_copy)); + close(fork_pipe[0]); + + if (ret < 0) { + LOG_ERRNO("failed to read from pipe"); + return -1; + } else if (ret == sizeof(errno_copy)) { + LOG_ERRNO_P( + errno_copy, "%s: failed to execute", + argc == 0 ? conf_shell : argv[0]); + return -1; + } else + LOG_DBG("%s: successfully started", conf_shell); + + int fd_flags; + if ((fd_flags = fcntl(ptmx, F_GETFD)) < 0 || + fcntl(ptmx, F_SETFD, fd_flags | FD_CLOEXEC) < 0) + { + LOG_ERRNO("failed to set FD_CLOEXEC on ptmx"); + return -1; + } + break; + } + } + + return pid; +} diff --git a/slave.h b/slave.h new file mode 100644 index 0000000..26d93ab --- /dev/null +++ b/slave.h @@ -0,0 +1,13 @@ +#pragma once +#include + +#include + +#include "config.h" +#include "user-notification.h" + +pid_t slave_spawn( + int ptmx, int argc, const char *cwd, char *const *argv, const char *const *envp, + const env_var_list_t *extra_env_vars, const char *term_env, + const char *conf_shell, bool login_shell, + const user_notifications_t *notifications); diff --git a/spawn.c b/spawn.c new file mode 100644 index 0000000..17c821b --- /dev/null +++ b/spawn.c @@ -0,0 +1,205 @@ +#include "spawn.h" + +#include +#include +#include +#include + +#include +#include +#include + +#define LOG_MODULE "spawn" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "xmalloc.h" + +pid_t +spawn(struct reaper *reaper, const char *cwd, char *const argv[], + int stdin_fd, int stdout_fd, int stderr_fd, reaper_cb cb, void *cb_data, + const char *xdg_activation_token) +{ + int pipe_fds[2] = {-1, -1}; + if (pipe2(pipe_fds, O_CLOEXEC) < 0) { + LOG_ERRNO("failed to create pipe"); + goto err; + } + + pid_t pid = fork(); + if (pid < 0) { + LOG_ERRNO("failed to fork"); + goto err; + } + + if (pid == 0) { + /* Child */ + close(pipe_fds[0]); + + if (setsid() < 0) + goto child_err; + + /* Clear signal mask */ + sigset_t mask; + sigemptyset(&mask); + if (sigprocmask(SIG_SETMASK, &mask, NULL) < 0) + goto child_err; + + /* Restore ignored (SIG_IGN) signals */ + struct sigaction dfl = {.sa_handler = SIG_DFL}; + sigemptyset(&dfl.sa_mask); + if (sigaction(SIGHUP, &dfl, NULL) < 0 || + sigaction(SIGPIPE, &dfl, NULL) < 0) + { + goto child_err; + } + + if (cwd != NULL) { + setenv("PWD", cwd, 1); + if (chdir(cwd) < 0) { + LOG_WARN("failed to change working directory to %s: %s", + cwd, strerror(errno)); + } + } + + if (xdg_activation_token != NULL) { + setenv("XDG_ACTIVATION_TOKEN", xdg_activation_token, 1); + + if (getenv("DISPLAY") != NULL) + setenv("DESKTOP_STARTUP_ID", xdg_activation_token, 1); + } + + bool close_stderr = stderr_fd >= 0; + bool close_stdout = stdout_fd >= 0 && stdout_fd != stderr_fd; + bool close_stdin = stdin_fd >= 0 && stdin_fd != stdout_fd && stdin_fd != stderr_fd; + + if ((stdin_fd >= 0 && (dup2(stdin_fd, STDIN_FILENO) < 0 + || (close_stdin && close(stdin_fd) < 0))) || + (stdout_fd >= 0 && (dup2(stdout_fd, STDOUT_FILENO) < 0 + || (close_stdout && close(stdout_fd) < 0))) || + (stderr_fd >= 0 && (dup2(stderr_fd, STDERR_FILENO) < 0 + || (close_stderr && close(stderr_fd) < 0))) || + execvp(argv[0], argv) < 0) + { + goto child_err; + } + + xassert(false); + _exit(errno); + + child_err: + ; + const int errno_copy = errno; + (void)!write(pipe_fds[1], &errno_copy, sizeof(errno_copy)); + _exit(errno_copy); + } + + /* Parent */ + close(pipe_fds[1]); + + int errno_copy; + static_assert(sizeof(errno_copy) == sizeof(errno), "errno size mismatch"); + + ssize_t ret = read(pipe_fds[0], &errno_copy, sizeof(errno_copy)); + close(pipe_fds[0]); + + if (ret == 0) { + reaper_add(reaper, pid, cb, cb_data); + return pid; + } else if (ret < 0) { + LOG_ERRNO("failed to read from pipe"); + return -1; + } else { + LOG_ERRNO_P(errno_copy, "%s: failed to spawn", argv[0]); + errno = errno_copy; + waitpid(pid, NULL, 0); + return -1; + } + +err: + if (pipe_fds[0] != -1) + close(pipe_fds[0]); + if (pipe_fds[1] != -1) + close(pipe_fds[1]); + return -1; +} + +bool +spawn_expand_template(const struct config_spawn_template *template, + size_t key_count, + const char *key_names[static key_count], + const char *key_values[static key_count], + size_t *argc, char ***argv) +{ + *argc = 0; + *argv = NULL; + + for (; template->argv.args[*argc] != NULL; (*argc)++) + ; + +#define append(s, n) \ + do { \ + expanded = xrealloc(expanded, len + (n) + 1); \ + memcpy(&expanded[len], s, n); \ + len += n; \ + expanded[len] = '\0'; \ + } while (0) + + *argv = xmalloc((*argc + 1) * sizeof((*argv)[0])); + + /* Expand the provided keys */ + for (size_t i = 0; i < *argc; i++) { + size_t len = 0; + char *expanded = NULL; + + char *start = NULL; + char *last_end = template->argv.args[i]; + + while ((start = strstr(last_end, "${")) != NULL) { + /* Append everything from the last template's end to this + * one's beginning */ + append(last_end, start - last_end); + + /* Find end of template */ + start += 2; + char *end = strstr(start, "}"); + + if (end == NULL) { + /* Ensure final append() copies the unclosed '${' */ + last_end = start - 2; + LOG_WARN("notify: unclosed template: %s", last_end); + break; + } + + /* Expand template */ + bool valid_key = false; + for (size_t j = 0; j < key_count; j++) { + if (strncmp(start, key_names[j], end - start) != 0) + continue; + + append(key_values[j], strlen(key_values[j])); + valid_key = true; + break; + } + + if (!valid_key) { + /* Unrecognized template - append it as-is */ + start -= 2; + append(start, end + 1 - start); + LOG_WARN("notify: unrecognized template: %.*s", + (int)(end + 1 - start), start); + } + + last_end = end + 1; + } + + append( + last_end, + template->argv.args[i] + strlen(template->argv.args[i]) - last_end); + (*argv)[i] = expanded; + } + (*argv)[*argc] = NULL; + +#undef append + return true; +} diff --git a/spawn.h b/spawn.h new file mode 100644 index 0000000..1693f1a --- /dev/null +++ b/spawn.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +#include "config.h" +#include "reaper.h" + +pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[], + int stdin_fd, int stdout_fd, int stderr_fd, + reaper_cb cb, void *cb_data, const char *xdg_activation_token); + +bool spawn_expand_template( + const struct config_spawn_template *template, + size_t key_count, const char *key_names[static key_count], + const char *key_values[static key_count], size_t *argc, char ***argv); diff --git a/stride.h b/stride.h new file mode 100644 index 0000000..b2c71a7 --- /dev/null +++ b/stride.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +static inline int +stride_for_format_and_width(pixman_format_code_t format, int width) +{ + return (((PIXMAN_FORMAT_BPP(format) * width + 7) / 8 + 4 - 1) & -4); +} diff --git a/subprojects/fcft.wrap b/subprojects/fcft.wrap new file mode 100644 index 0000000..d2709d4 --- /dev/null +++ b/subprojects/fcft.wrap @@ -0,0 +1,3 @@ +[wrap-git] +url = https://codeberg.org/dnkl/fcft.git +revision = master diff --git a/subprojects/tllist.wrap b/subprojects/tllist.wrap new file mode 100644 index 0000000..75f395a --- /dev/null +++ b/subprojects/tllist.wrap @@ -0,0 +1,3 @@ +[wrap-git] +url = https://codeberg.org/dnkl/tllist.git +revision = master diff --git a/subprojects/wayland-protocols.wrap b/subprojects/wayland-protocols.wrap new file mode 100644 index 0000000..74e5e91 --- /dev/null +++ b/subprojects/wayland-protocols.wrap @@ -0,0 +1,3 @@ +[wrap-git] +url = https://gitlab.freedesktop.org/wayland/wayland-protocols.git +revision = main diff --git a/terminal.c b/terminal.c new file mode 100644 index 0000000..0b9f56d --- /dev/null +++ b/terminal.c @@ -0,0 +1,5363 @@ +#include "terminal.h" + +#if defined(__GLIBC__) +#include +#endif +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_MODULE "terminal" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "async.h" +#include "commands.h" +#include "config.h" +#include "debug.h" +#include "emoji-variation-sequences.h" +#include "extract.h" +#include "grid.h" +#include "ime.h" +#include "input.h" +#include "notify.h" +#include "quirks.h" +#include "reaper.h" +#include "render.h" +#include "selection.h" +#include "shm.h" +#include "sixel.h" +#include "slave.h" +#include "spawn.h" +#include "url-mode.h" +#include "util.h" +#include "vt.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +#define PTMX_TIMING 0 + +static void +enqueue_data_for_slave(const void *data, size_t len, size_t offset, + ptmx_buffer_list_t *buffer_list) +{ + struct ptmx_buffer queued = { + .data = xmemdup(data, len), + .len = len, + .idx = offset, + }; + tll_push_back(*buffer_list, queued); +} + +static bool +data_to_slave(struct terminal *term, const void *data, size_t len, + ptmx_buffer_list_t *buffer_list) +{ + /* + * Try a synchronous write first. If we fail to write everything, + * switch to asynchronous. + */ + + size_t async_idx = 0; + switch (async_write(term->ptmx, data, len, &async_idx)) { + case ASYNC_WRITE_REMAIN: + /* Switch to asynchronous mode; let FDM write the remaining data */ + if (!fdm_event_add(term->fdm, term->ptmx, EPOLLOUT)) + return false; + enqueue_data_for_slave(data, len, async_idx, buffer_list); + return true; + + case ASYNC_WRITE_DONE: + return true; + + case ASYNC_WRITE_ERR: + LOG_ERRNO("failed to synchronously write %zu bytes to slave", len); + return false; + } + + BUG("Unexpected async_write() return value"); + return false; +} + +bool +term_paste_data_to_slave(struct terminal *term, const void *data, size_t len) +{ + xassert(term->is_sending_paste_data); + + if (term->ptmx < 0) { + /* We're probably in "hold" */ + return false; + } + + if (tll_length(term->ptmx_paste_buffers) > 0) { + /* Don't even try to send data *now* if there's queued up + * data, since that would result in events arriving out of + * order. */ + enqueue_data_for_slave(data, len, 0, &term->ptmx_paste_buffers); + return true; + } + + return data_to_slave(term, data, len, &term->ptmx_paste_buffers); +} + +bool +term_to_slave(struct terminal *term, const void *data, size_t len) +{ + if (term->ptmx < 0) { + /* We're probably in "hold" */ + return false; + } + + if (unlikely(tll_length(term->ptmx_buffers) > 0 || + term->is_sending_paste_data || + tll_length(term->ptmx_paste_buffers) > 0)) + { + /* + * Don't even try to send data *now* if there's queued up + * data, since that would result in events arriving out of + * order. + * + * Furthermore, if we're currently sending paste data to the + * client, do *not* mix that stream with other events + * (https://codeberg.org/dnkl/foot/issues/101). + */ + enqueue_data_for_slave(data, len, 0, &term->ptmx_buffers); + return true; + } + + return data_to_slave(term, data, len, &term->ptmx_buffers); +} + +static bool +fdm_ptmx_out(struct fdm *fdm, int fd, int events, void *data) +{ + struct terminal *term = data; + + /* If there is no queued data, then we shouldn't be in asynchronous mode */ + xassert(tll_length(term->ptmx_buffers) > 0 || + tll_length(term->ptmx_paste_buffers) > 0); + + /* Writes a single buffer, returns if not all of it could be written */ +#define write_one_buffer(buffer_list) \ + { \ + switch (async_write(term->ptmx, it->item.data, it->item.len, &it->item.idx)) { \ + case ASYNC_WRITE_DONE: \ + free(it->item.data); \ + tll_remove(buffer_list, it); \ + break; \ + case ASYNC_WRITE_REMAIN: \ + /* to_slave() updated it->item.idx */ \ + return true; \ + case ASYNC_WRITE_ERR: \ + LOG_ERRNO("failed to asynchronously write %zu bytes to slave", \ + it->item.len - it->item.idx); \ + return false; \ + } \ + } + + tll_foreach(term->ptmx_paste_buffers, it) + write_one_buffer(term->ptmx_paste_buffers); + + /* If we get here, *all* paste data buffers were successfully + * flushed */ + + if (!term->is_sending_paste_data) { + tll_foreach(term->ptmx_buffers, it) + write_one_buffer(term->ptmx_buffers); + } + + /* + * If we get here, *all* buffers were successfully flushed. + * + * Or, we're still sending paste data, in which case we do *not* + * want to send the "normal" queued up data + * + * In both cases, we want to *disable* the FDM callback since + * otherwise we'd just be called right away again, with nothing to + * write. + */ + fdm_event_del(term->fdm, term->ptmx, EPOLLOUT); + return true; +} + +static bool +add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) +{ +#if defined(UTMP_ADD) + if (ptmx < 0) + return true; + if (conf->utmp_helper_path == NULL) + return true; + + char *const argv[] = {conf->utmp_helper_path, UTMP_ADD, getenv("WAYLAND_DISPLAY"), NULL}; + return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0; +#else + return true; +#endif +} + +static bool +del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) +{ +#if defined(UTMP_DEL) + if (ptmx < 0) + return true; + if (conf->utmp_helper_path == NULL) + return true; + + char *del_argument = +#if defined(UTMP_DEL_HAVE_ARGUMENT) + getenv("WAYLAND_DISPLAY") +#else + NULL +#endif + ; + + char *const argv[] = {conf->utmp_helper_path, UTMP_DEL, del_argument, NULL}; + return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0; +#else + return true; +#endif +} + +#if PTMX_TIMING +static struct timespec last = {0}; +#endif + +static bool cursor_blink_rearm_timer(struct terminal *term); + +/* Externally visible, but not declared in terminal.h, to enable pgo + * to call this function directly */ +bool +fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) +{ + struct terminal *term = data; + + const bool pollin = events & EPOLLIN; + const bool pollout = events & EPOLLOUT; + const bool hup = events & EPOLLHUP; + + if (pollout) { + if (!fdm_ptmx_out(fdm, fd, events, data)) + return false; + } + + /* Prevent blinking while typing */ + if (term->cursor_blink.fd >= 0) { + term->cursor_blink.state = CURSOR_BLINK_ON; + cursor_blink_rearm_timer(term); + } + + if (unlikely(term->interactive_resizing.grid != NULL)) { + /* + * Don't consume PTMX while we're doing an interactive resize, + * since the 'normal' grid we're currently using is a + * temporary one - all changes done to it will be lost when + * the interactive resize ends. + */ + return true; + } + + uint8_t buf[24 * 1024]; + const size_t max_iterations = !hup ? 10 : SIZE_MAX; + + for (size_t i = 0; i < max_iterations && pollin; i++) { + xassert(pollin); + ssize_t count = read(term->ptmx, buf, sizeof(buf)); + + if (count < 0) { + if (errno == EAGAIN || errno == EIO) { + /* + * EAGAIN: no more to read - FDM will trigger us again + * EIO: assume PTY was closed - we already have, or will get, a EPOLLHUP + */ + break; + } + + LOG_ERRNO("failed to read from pseudo terminal"); + return false; + } else if (count == 0) { + /* Reached end-of-file */ + break; + } + + xassert(term->interactive_resizing.grid == NULL); + vt_from_slave(term, buf, count); + + /* Mark inactive tabs as having unread output, so the tab bar + * can show an indicator. Force a tab-bar refresh on the active + * terminal, since the active terminal owns the rendered bar. */ + if (term->window != NULL && term->window->tab_count > 1 && + term != term->window->term) + { + if (!term->has_unread) { + term->has_unread = true; + if (term->window->term != NULL && term->conf->tabs.enabled) + render_refresh_tab_bar(term->window->term); + } + } + } + + if (!term->render.app_sync_updates.enabled) { + /* + * We likely need to re-render. But, we don't want to do it + * immediately. Often, a single client update is done through + * multiple writes. This could lead to us rendering one frame with + * "intermediate" state. + * + * For example, we might end up rendering a frame + * where the client just erased a line, while in the + * next frame, the client wrote to the same line. This + * causes screen "flickering". + * + * Mitigate by always incuring a small delay before + * rendering the next frame. This gives the client + * some time to finish the operation (and thus gives + * us time to receive the last writes before doing any + * actual rendering). + * + * We incur this delay *every* time we receive + * input. To ensure we don't delay rendering + * indefinitely, we start a second timer that is only + * reset when we render. + * + * Note that when the client is producing data at a + * very high pace, we're rate limited by the wayland + * compositor anyway. The delay we introduce here only + * has any effect when the renderer is idle. + */ + uint64_t lower_ns = term->conf->tweak.delayed_render_lower_ns; + uint64_t upper_ns = term->conf->tweak.delayed_render_upper_ns; + + if (lower_ns > 0 && upper_ns > 0) { +#if PTMX_TIMING + struct timespec now; + + clock_gettime(CLOCK_MONOTONIC, &now); + if (last.tv_sec > 0 || last.tv_nsec > 0) { + struct timespec diff; + + timespec_sub(&now, &last, &diff); + LOG_INFO("waited %lds %ldns for more input", + (long)diff.tv_sec, diff.tv_nsec); + } + last = now; +#endif + + xassert(lower_ns < 1000000000); + xassert(upper_ns < 1000000000); + xassert(upper_ns > lower_ns); + + timerfd_settime( + term->delayed_render_timer.lower_fd, 0, + &(struct itimerspec){.it_value = {.tv_nsec = lower_ns}}, + NULL); + + /* Second timeout - only reset when we render. Set to one + * frame (assuming 60Hz) */ + if (!term->delayed_render_timer.is_armed) { + timerfd_settime( + term->delayed_render_timer.upper_fd, 0, + &(struct itimerspec){.it_value = {.tv_nsec = upper_ns}}, + NULL); + term->delayed_render_timer.is_armed = true; + } + } else + render_refresh(term); + } + + if (hup) { + del_utmp_record(term->conf, term->reaper, term->ptmx); + fdm_del(fdm, fd); + term->ptmx = -1; + + /* + * Normally, we do *not* want to shutdown when the PTY is + * closed. Instead, we want to wait for the client application + * to exit. + * + * However, when we're using a pre-existing PTY (the --pty + * option), there _is_ no client application. That is, foot + * does *not* fork+exec anything, and thus the only way to + * shutdown is to wait for the PTY to be closed. + */ + if (term->slave < 0 && !term->conf->hold_at_exit) { + term_shutdown(term); + } + } + + return true; +} + +bool +term_ptmx_pause(struct terminal *term) +{ + if (term->ptmx < 0) + return false; + return fdm_event_del(term->fdm, term->ptmx, EPOLLIN); +} + +bool +term_ptmx_resume(struct terminal *term) +{ + if (term->ptmx < 0) + return false; + return fdm_event_add(term->fdm, term->ptmx, EPOLLIN); +} + +static bool +fdm_flash(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t expiration_count; + ssize_t ret = read( + term->flash.fd, &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read flash timer"); + return false; + } + + LOG_DBG("flash timer expired %llu times", + (unsigned long long)expiration_count); + + term->flash.active = false; + render_overlay(term); + + // since the overlay surface is synced with the main window surface, we have + // to commit the main surface for the compositor to acknowledge the new + // overlay state. + wl_surface_commit(term->window->surface.surf); + return true; +} + +static bool +fdm_blink(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t expiration_count; + ssize_t ret = read( + term->blink.fd, &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read blink timer"); + return false; + } + + LOG_DBG("blink timer expired %llu times", + (unsigned long long)expiration_count); + + /* Invert blink state */ + term->blink.state = term->blink.state == BLINK_ON + ? BLINK_OFF : BLINK_ON; + + /* Scan all visible cells and mark rows with blinking cells dirty */ + bool no_blinking_cells = true; + for (int r = 0; r < term->rows; r++) { + struct row *row = grid_row_in_view(term->grid, r); + for (int col = 0; col < term->cols; col++) { + struct cell *cell = &row->cells[col]; + + if (cell->attrs.blink) { + cell->attrs.clean = 0; + row->dirty = true; + no_blinking_cells = false; + } + } + } + + if (no_blinking_cells) { + LOG_DBG("disarming blink timer"); + + term->blink.state = BLINK_ON; + fdm_del(term->fdm, term->blink.fd); + term->blink.fd = -1; + } else + render_refresh(term); + return true; +} + +void +term_arm_blink_timer(struct terminal *term) +{ + if (term->blink.fd >= 0) + return; + + LOG_DBG("arming blink timer"); + + int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd < 0) { + LOG_ERRNO("failed to create blink timer FD"); + return; + } + + if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_blink, term)) { + close(fd); + return; + } + + struct itimerspec alarm = { + .it_value = {.tv_sec = 0, .tv_nsec = 500 * 1000000}, + .it_interval = {.tv_sec = 0, .tv_nsec = 500 * 1000000}, + }; + + if (timerfd_settime(fd, 0, &alarm, NULL) < 0) { + LOG_ERRNO("failed to arm blink timer"); + fdm_del(term->fdm, fd); + } + + term->blink.fd = fd; +} + +static void +cursor_refresh(struct terminal *term) +{ + if (!term->window->is_configured) + return; + + term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0; + term->grid->cur_row->dirty = true; + render_refresh(term); +} + +static bool +fdm_cursor_blink(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t expiration_count; + ssize_t ret = read( + term->cursor_blink.fd, &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read cursor blink timer"); + return false; + } + + LOG_DBG("cursor blink timer expired %llu times", + (unsigned long long)expiration_count); + + /* Invert blink state */ + term->cursor_blink.state = term->cursor_blink.state == CURSOR_BLINK_ON + ? CURSOR_BLINK_OFF : CURSOR_BLINK_ON; + + cursor_refresh(term); + return true; +} + +static bool +fdm_delayed_render(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + + uint64_t unused; + ssize_t ret1 = 0; + ssize_t ret2 = 0; + + if (fd == term->delayed_render_timer.lower_fd) + ret1 = read(term->delayed_render_timer.lower_fd, &unused, sizeof(unused)); + if (fd == term->delayed_render_timer.upper_fd) + ret2 = read(term->delayed_render_timer.upper_fd, &unused, sizeof(unused)); + + if ((ret1 < 0 || ret2 < 0)) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read timeout timer"); + return false; + } + + if (ret1 > 0) + LOG_DBG("lower delay timer expired"); + else if (ret2 > 0) + LOG_DBG("upper delay timer expired"); + + if (ret1 == 0 && ret2 == 0) + return true; + +#if PTMX_TIMING + last = (struct timespec){0}; +#endif + + /* Reset timers */ + struct itimerspec reset = {{0}}; + timerfd_settime(term->delayed_render_timer.lower_fd, 0, &reset, NULL); + timerfd_settime(term->delayed_render_timer.upper_fd, 0, &reset, NULL); + term->delayed_render_timer.is_armed = false; + + render_refresh(term); + return true; +} + +static bool +fdm_app_sync_updates_timeout( + struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = read(term->render.app_sync_updates.timer_fd, + &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read application synchronized updates timeout timer"); + return false; + } + + term_disable_app_sync_updates(term); + return true; +} + +static bool +fdm_title_update_timeout(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = read(term->render.title.timer_fd, &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read title update throttle timer"); + return false; + } + + struct itimerspec reset = {{0}}; + timerfd_settime(term->render.title.timer_fd, 0, &reset, NULL); + + render_refresh_title(term); + return true; +} + +static bool +fdm_icon_update_timeout(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = read(term->render.icon.timer_fd, &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read icon update throttle timer"); + return false; + } + + struct itimerspec reset = {{0}}; + timerfd_settime(term->render.icon.timer_fd, 0, &reset, NULL); + + render_refresh_icon(term); + return true; +} + +static bool +fdm_app_id_update_timeout(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = read(term->render.app_id.timer_fd, &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read app ID update throttle timer"); + return false; + } + + struct itimerspec reset = {{0}}; + timerfd_settime(term->render.app_id.timer_fd, 0, &reset, NULL); + + render_refresh_app_id(term); + return true; +} + +static bool +initialize_render_workers(struct terminal *term) +{ + LOG_INFO("using %hu rendering threads", term->render.workers.count); + + if (sem_init(&term->render.workers.start, 0, 0) < 0 || + sem_init(&term->render.workers.done, 0, 0) < 0) + { + LOG_ERRNO("failed to instantiate render worker semaphores"); + return false; + } + + int err; + if ((err = mtx_init(&term->render.workers.lock, mtx_plain)) != thrd_success) { + LOG_ERR("failed to instantiate render worker mutex: %s (%d)", + thrd_err_as_string(err), err); + goto err_sem_destroy; + } + + mtx_init(&term->render.workers.preapplied_damage.lock, mtx_plain); + cnd_init(&term->render.workers.preapplied_damage.cond); + + term->render.workers.threads = xcalloc( + term->render.workers.count, sizeof(term->render.workers.threads[0])); + + for (size_t i = 0; i < term->render.workers.count; i++) { + struct render_worker_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct render_worker_context) { + .term = term, + .my_id = 1 + i, + }; + + int ret = thrd_create( + &term->render.workers.threads[i], &render_worker_thread, ctx); + if (ret != thrd_success) { + + LOG_ERR("failed to create render worker thread: %s (%d)", + thrd_err_as_string(ret), ret); + term->render.workers.threads[i] = 0; + return false; + } + } + + return true; + +err_sem_destroy: + sem_destroy(&term->render.workers.start); + sem_destroy(&term->render.workers.done); + return false; +} + +static void +free_custom_glyph(struct fcft_glyph **glyph) +{ + if (*glyph == NULL) + return; + + free(pixman_image_get_data((*glyph)->pix)); + pixman_image_unref((*glyph)->pix); + free(*glyph); + *glyph = NULL; +} + +static void +free_custom_glyphs(struct fcft_glyph ***glyphs, size_t count) +{ + if (*glyphs == NULL) + return; + + for (size_t i = 0; i < count; i++) + free_custom_glyph(&(*glyphs)[i]); + + free(*glyphs); + *glyphs = NULL; +} + +static void +term_line_height_update(struct terminal *term) +{ + const struct config *conf = term->conf; + + if (term->conf->line_height.px < 0) { + term->font_line_height.pt = 0; + term->font_line_height.px = -1; + return; + } + + const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + + const float font_original_pt_size = + conf->fonts[0].arr[0].px_size > 0 + ? conf->fonts[0].arr[0].px_size * 72. / dpi + : conf->fonts[0].arr[0].pt_size; + const float font_current_pt_size = + term->font_sizes[0][0].px_size > 0 + ? term->font_sizes[0][0].px_size * 72. / dpi + : term->font_sizes[0][0].pt_size; + + const float change = font_current_pt_size / font_original_pt_size; + const float line_original_pt_size = conf->line_height.px > 0 + ? conf->line_height.px * 72. / dpi + : conf->line_height.pt; + + term->font_line_height.px = 0; + term->font_line_height.pt = fmaxf(line_original_pt_size * change, 0.); +} + +static bool +term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4], + bool resize_grid) +{ + for (size_t i = 0; i < 4; i++) { + xassert(fonts[i] != NULL); + + fcft_destroy(term->fonts[i]); + term->fonts[i] = fonts[i]; + } + + free_custom_glyphs( + &term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT); + free_custom_glyphs( + &term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); + free_custom_glyphs( + &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); + free_custom_glyphs( + &term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT); + + const struct config *conf = term->conf; + + const struct fcft_glyph *M = fcft_rasterize_char_utf32( + fonts[0], U'M', term->font_subpixel); + int advance = M != NULL ? M->advance.x : term->fonts[0]->max_advance.x; + + term_line_height_update(term); + + term->cell_width = advance + + term_pt_or_px_as_pixels(term, &conf->letter_spacing); + + term->cell_height = term->font_line_height.px >= 0 + ? term_pt_or_px_as_pixels(term, &term->font_line_height) + : max(term->fonts[0]->height, + term->fonts[0]->ascent + term->fonts[0]->descent); + + if (term->cell_width <= 0) + term->cell_width = 1; + if (term->cell_height <= 0) + term->cell_height = 1; + + term->font_x_ofs = term_pt_or_px_as_pixels(term, &conf->horizontal_letter_offset); + term->font_y_ofs = term_pt_or_px_as_pixels(term, &conf->vertical_letter_offset); + + term->font_baseline = term_font_baseline(term); + + LOG_INFO("cell width=%d, height=%d", term->cell_width, term->cell_height); + + sixel_cell_size_changed(term); + + /* Optimization - some code paths (are forced to) call + * render_resize() after this function */ + if (resize_grid) { + /* Use force, since cell-width/height may have changed */ + enum resize_options resize_opts = RESIZE_FORCE; + if (conf->resize_keep_grid) + resize_opts |= RESIZE_KEEP_GRID; + + render_resize( + term, + (int)roundf(term->width / term->scale), + (int)roundf(term->height / term->scale), + resize_opts); + } + return true; +} + +static float +get_font_dpi(const struct terminal *term) +{ + /* + * Use output's DPI to scale font. This is to ensure the font has + * the same physical height (if measured by a ruler) regardless of + * monitor. + * + * Conceptually, we use the physical monitor specs to calculate + * the DPI, and we ignore the output's scaling factor. + * + * However, to deal with legacy fractional scaling, where we're + * told to render at e.g. 2x, but are then downscaled by the + * compositor to e.g. 1.25, we use the scaled DPI value multiplied + * by the scale factor instead. + * + * For integral scaling factors the resulting DPI is the same as + * if we had used the physical DPI. + * + * For legacy fractional scaling factors we'll get a DPI *larger* + * than the physical DPI, that ends up being right when later + * downscaled by the compositor. + * + * With the newer fractional-scale-v1 protocol, we use the + * monitor's real DPI, since we scale everything to the correct + * scaling factor (no downscaling done by the compositor). + */ + + const struct wl_window *win = term->window; + const struct monitor *mon = NULL; + + if (tll_length(win->on_outputs) > 0) + mon = tll_back(win->on_outputs); + else { + if (term->font_dpi_before_unmap > 0.) { + /* + * Use last known "good" DPI + * + * This avoids flickering when window is unmapped/mapped + * (some compositors do this when a window is minimized), + * on a multi-monitor setup with different monitor DPIs. + */ + return term->font_dpi_before_unmap; + } + + if (tll_length(term->wl->monitors) > 0) + mon = &tll_front(term->wl->monitors); + } + + const float monitor_dpi = mon != NULL + ? term_fractional_scaling(term) + ? mon->dpi.physical + : mon->dpi.scaled + : 96.; + + return monitor_dpi > 0. ? monitor_dpi : 96.; +} + +static enum fcft_subpixel +get_font_subpixel(const struct terminal *term) +{ + if (term->colors.alpha != 0xffff) { + /* Can't do subpixel rendering on transparent background */ + return FCFT_SUBPIXEL_NONE; + } + + enum wl_output_subpixel wl_subpixel; + + /* + * Wayland doesn't tell us *which* part of the surface that goes + * on a specific output, only whether the surface is mapped to an + * output or not. + * + * Thus, when determining which subpixel mode to use, we can't do + * much but select *an* output. So, we pick the one we were most + * recently mapped on. + * + * If we're not mapped at all, we pick the first available + * monitor, and hope that's where we'll eventually get mapped. + * + * If there aren't any monitors we use the "default" subpixel + * mode. + */ + + if (tll_length(term->window->on_outputs) > 0) + wl_subpixel = tll_back(term->window->on_outputs)->subpixel; + else if (tll_length(term->wl->monitors) > 0) + wl_subpixel = tll_front(term->wl->monitors).subpixel; + else + wl_subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN; + + switch (wl_subpixel) { + case WL_OUTPUT_SUBPIXEL_UNKNOWN: return FCFT_SUBPIXEL_DEFAULT; + case WL_OUTPUT_SUBPIXEL_NONE: return FCFT_SUBPIXEL_NONE; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB: return FCFT_SUBPIXEL_HORIZONTAL_RGB; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR: return FCFT_SUBPIXEL_HORIZONTAL_BGR; + case WL_OUTPUT_SUBPIXEL_VERTICAL_RGB: return FCFT_SUBPIXEL_VERTICAL_RGB; + case WL_OUTPUT_SUBPIXEL_VERTICAL_BGR: return FCFT_SUBPIXEL_VERTICAL_BGR; + } + + return FCFT_SUBPIXEL_DEFAULT; +} + +int +term_pt_or_px_as_pixels(const struct terminal *term, + const struct pt_or_px *pt_or_px) +{ + float scale = !term->font_is_sized_by_dpi ? term->scale : 1.; + float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + + return pt_or_px->px == 0 + ? (int)roundf(pt_or_px->pt * scale * dpi / 72) + : (int)roundf(pt_or_px->px * scale); +} + +struct font_load_data { + size_t count; + const char **names; + const char *attrs; + + const struct fcft_font_options *options; + struct fcft_font **font; +}; + +static int +font_loader_thread(void *_data) +{ + struct font_load_data *data = _data; + *data->font = fcft_from_name2( + data->count, data->names, data->attrs, data->options); + return *data->font != NULL; +} + +static bool +reload_fonts(struct terminal *term, bool resize_grid) +{ + const struct config *conf = term->conf; + + const size_t counts[4] = { + conf->fonts[0].count, + conf->fonts[1].count, + conf->fonts[2].count, + conf->fonts[3].count, + }; + + /* Configure size (which may have been changed run-time) */ + char **names[4]; + for (size_t i = 0; i < 4; i++) { + names[i] = xmalloc(counts[i] * sizeof(names[i][0])); + + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + const struct config_font *font = &font_list->arr[j]; + bool use_px_size = term->font_sizes[i][j].px_size > 0; + char size[64]; + + const float scale = term->font_is_sized_by_dpi ? 1. : term->scale; + + if (use_px_size) + snprintf(size, sizeof(size), ":pixelsize=%d", + (int)roundf(term->font_sizes[i][j].px_size * scale)); + else + snprintf(size, sizeof(size), ":size=%.2f", + term->font_sizes[i][j].pt_size * scale); + + names[i][j] = xstrjoin(font->pattern, size); + } + } + + /* Did user configure custom bold/italic fonts? + * Or should we use the regular font, with weight/slant attributes? */ + const bool custom_bold = counts[1] > 0; + const bool custom_italic = counts[2] > 0; + const bool custom_bold_italic = counts[3] > 0; + + const size_t count_regular = counts[0]; + const char **names_regular = (const char **)names[0]; + + const size_t count_bold = custom_bold ? counts[1] : counts[0]; + const char **names_bold = (const char **)(custom_bold ? names[1] : names[0]); + + const size_t count_italic = custom_italic ? counts[2] : counts[0]; + const char **names_italic = (const char **)(custom_italic ? names[2] : names[0]); + + const size_t count_bold_italic = custom_bold_italic ? counts[3] : counts[0]; + const char **names_bold_italic = (const char **)(custom_bold_italic ? names[3] : names[0]); + + const bool use_dpi = term->font_is_sized_by_dpi; + char *dpi = xasprintf("dpi=%.2f", use_dpi ? term->font_dpi : 96.); + + char *attrs[4] = { + [0] = dpi, /* Takes ownership */ + [1] = xstrjoin(dpi, !custom_bold ? ":weight=bold" : ""), + [2] = xstrjoin(dpi, !custom_italic ? ":slant=italic" : ""), + [3] = xstrjoin(dpi, !custom_bold_italic ? ":weight=bold:slant=italic" : ""), + }; + + struct fcft_font_options *options = fcft_font_options_create(); + + options->scaling_filter = conf->tweak.fcft_filter; + options->color_glyphs.format = PIXMAN_a8r8g8b8; + options->color_glyphs.srgb_decode = + wayl_do_linear_blending(term->wl, term->conf); + + if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { + /* + * Use a high-res buffer type for emojis. We don't want to use + * an a2r10g0b10 type of surface, since we need more than 2 + * bits for alpha. + */ +#if defined(HAVE_PIXMAN_RGBA_16) + options->color_glyphs.format = PIXMAN_a16b16g16r16; +#else + options->color_glyphs.format = PIXMAN_rgba_float; +#endif + } + + struct fcft_font *fonts[4]; + struct font_load_data data[4] = { + {count_regular, names_regular, attrs[0], options, &fonts[0]}, + {count_bold, names_bold, attrs[1], options, &fonts[1]}, + {count_italic, names_italic, attrs[2], options, &fonts[2]}, + {count_bold_italic, names_bold_italic, attrs[3], options, &fonts[3]}, + }; + + thrd_t tids[4] = {0}; + for (size_t i = 0; i < 4; i++) { + int ret = thrd_create(&tids[i], &font_loader_thread, &data[i]); + if (ret != thrd_success) { + LOG_ERR("failed to create font loader thread: %s (%d)", + thrd_err_as_string(ret), ret); + break; + } + } + + bool success = true; + for (size_t i = 0; i < 4; i++) { + if (tids[i] != 0) { + int ret; + if (thrd_join(tids[i], &ret) != thrd_success) + success = false; + else + success = success && ret; + } else + success = false; + } + + fcft_font_options_destroy(options); + + for (size_t i = 0; i < 4; i++) { + for (size_t j = 0; j < counts[i]; j++) + free(names[i][j]); + free(names[i]); + free(attrs[i]); + } + + if (!success) { + LOG_ERR("failed to load primary fonts"); + for (size_t i = 0; i < 4; i++) { + fcft_destroy(fonts[i]); + fonts[i] = NULL; + } + } + + return success ? term_set_fonts(term, fonts, resize_grid) : success; +} + +static bool +load_fonts_from_conf(struct terminal *term) +{ + const struct config *conf = term->conf; + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + const struct config_font *font = &font_list->arr[j]; + term->font_sizes[i][j] = (struct config_font){ + .pt_size = font->pt_size, .px_size = font->px_size}; + } + } + + return reload_fonts(term, true); +} + +static void fdm_client_terminated( + struct reaper *reaper, pid_t pid, int status, void *data); + +static const int PTY_OPEN_FLAGS = O_RDWR | O_NOCTTY; + +struct terminal * +term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, + struct wayland *wayl, const char *foot_exe, const char *cwd, + const char *token, const char *pty_path, + int argc, char *const *argv, const char *const *envp, + void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data) +{ + int ptmx = -1; + int flash_fd = -1; + int delay_lower_fd = -1; + int delay_upper_fd = -1; + int app_sync_updates_fd = -1; + int title_update_fd = -1; + int icon_update_fd = -1; + int app_id_update_fd = -1; + + struct terminal *term = malloc(sizeof(*term)); + if (unlikely(term == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } + + ptmx = pty_path ? open(pty_path, PTY_OPEN_FLAGS) : posix_openpt(PTY_OPEN_FLAGS); + if (ptmx < 0) { + LOG_ERRNO("failed to open PTY"); + goto close_fds; + } + if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { + LOG_ERRNO("failed to create flash timer FD"); + goto close_fds; + } + if ((delay_lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (delay_upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create delayed rendering timer FDs"); + goto close_fds; + } + + if ((app_sync_updates_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create application synchronized updates timer FD"); + goto close_fds; + } + + if ((title_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create title update throttle timer FD"); + goto close_fds; + } + + if ((icon_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create icon update throttle timer FD"); + goto close_fds; + } + + if ((app_id_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create app ID update throttle timer FD"); + goto close_fds; + } + + if (ioctl(ptmx, (unsigned int)TIOCSWINSZ, + &(struct winsize){.ws_row = 24, .ws_col = 80}) < 0) + { + LOG_ERRNO("failed to set initial TIOCSWINSZ"); + goto close_fds; + } + + /* Need to register *very* early (before the first "goto err"), to + * ensure term_destroy() doesn't unref a key-binding we haven't + * yet ref:d */ + key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); + + int ptmx_flags; + if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 || + fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) + { + LOG_ERRNO("failed to configure ptmx as non-blocking"); + goto err; + } + + /* + * Enable all FDM callbackes *except* ptmx - we can't do that + * until the window has been 'configured' since we don't have a + * size (and thus no grid) before then. + */ + + if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) || + !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) || + !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || + !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, term) || + !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) || + !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) || + !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term)) + { + goto err; + } + + const enum shm_bit_depth desired_bit_depth = + conf->tweak.surface_bit_depth == SHM_BITS_AUTO + ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8 + : conf->tweak.surface_bit_depth; + + const struct color_theme *theme = NULL; + switch (conf->initial_color_theme) { + case COLOR_THEME_DARK: theme = &conf->colors_dark; break; + case COLOR_THEME_LIGHT: theme = &conf->colors_light; break; + case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; + case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; + } + + /* Initialize configure-based terminal attributes */ + *term = (struct terminal) { + .fdm = fdm, + .reaper = reaper, + .conf = conf, + .slave = -1, + .ptmx = ptmx, + .ptmx_buffers = tll_init(), + .ptmx_paste_buffers = tll_init(), + .font_sizes = { + xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count), + xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count), + xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count), + xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), + }, + .font_dpi = 0., + .font_dpi_before_unmap = -1., + .font_subpixel = (theme->alpha == 0xffff /* Can't do subpixel rendering on transparent background */ + ? FCFT_SUBPIXEL_DEFAULT + : FCFT_SUBPIXEL_NONE), + .cursor_keys_mode = CURSOR_KEYS_NORMAL, + .keypad_keys_mode = KEYPAD_NUMERICAL, + .reverse_wrap = true, + .auto_margin = true, + .window_title_stack = tll_init(), + .scale = 1., + .scale_before_unmap = -1, + .flash = {.fd = flash_fd}, + .blink = {.fd = -1}, + .vt = { + .state = 0, /* STATE_GROUND */ + }, + .colors = { + .fg = theme->fg, + .bg = theme->bg, + .alpha = theme->alpha, + .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text, + .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor, + .selection_fg = theme->selection_fg, + .selection_bg = theme->selection_bg, + .active_theme = conf->initial_color_theme, + }, + .color_stack = { + .stack = NULL, + .size = 0, + .idx = 0, + }, + .origin = ORIGIN_ABSOLUTE, + .cursor_style = conf->cursor.style, + .cursor_blink = { + .decset = false, + .deccsusr = conf->cursor.blink.enabled, + .state = CURSOR_BLINK_ON, + .fd = -1, + }, + .selection = { + .coords = { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .pivot = { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .auto_scroll = { + .fd = -1, + }, + }, + .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, + .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, + .grid = &term->normal, + .composed = NULL, + .alt_scrolling = conf->mouse.alternate_scroll_mode, + .meta = { + .esc_prefix = true, + .eight_bit = true, + }, + .num_lock_modifier = true, + .bell_action_enabled = true, + .tab_stops = tll_init(), + .wl = wayl, + .render = { + .chains = { + .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, + desired_bit_depth, &render_buffer_release_callback, term), + .search = shm_chain_new(wayl, false, 1 ,desired_bit_depth, NULL, NULL), + .scrollback_indicator = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .render_timer = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .url = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .csd = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .tab_bar = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .tab_overview = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + }, + .scrollback_lines = conf->scrollback.lines, + .app_sync_updates.timer_fd = app_sync_updates_fd, + .title = { + .timer_fd = title_update_fd, + }, + .icon = { + .timer_fd = icon_update_fd, + }, + .app_id = { + .timer_fd = app_id_update_fd, + }, + .workers = { + .count = conf->render_worker_count, + .queue = tll_init(), + }, + }, + .delayed_render_timer = { + .is_armed = false, + .lower_fd = delay_lower_fd, + .upper_fd = delay_upper_fd, + }, + .sixel = { + .scrolling = true, + .use_private_palette = true, + .palette_size = SIXEL_MAX_COLORS, + .max_width = SIXEL_MAX_WIDTH, + .max_height = SIXEL_MAX_HEIGHT, + }, + .shutdown = { + .terminate_timeout_fd = -1, + .cb = shutdown_cb, + .cb_data = shutdown_data, + }, + .foot_exe = xstrdup(foot_exe), + .cwd = xstrdup(cwd), + .grapheme_shaping = conf->tweak.grapheme_shaping, +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + .ime_enabled = true, +#endif + .active_notifications = tll_init(), + }; + + pixman_region32_init(&term->render.last_overlay_clip); + + term_update_ascii_printer(term); + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + for (size_t j = 0; j < font_list->count; j++) { + const struct config_font *font = &font_list->arr[j]; + term->font_sizes[i][j] = (struct config_font){ + .pt_size = font->pt_size, .px_size = font->px_size}; + } + } + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + term->notification_icons[i].tmp_file_fd = -1; + } + + add_utmp_record(conf, reaper, ptmx); + + if (!pty_path) { + /* Start the slave/client */ + if ((term->slave = slave_spawn( + term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, + conf->term, conf->shell, conf->login_shell, + &conf->notifications)) == -1) + { + goto err; + } + + reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); + } + + /* Guess scale; we're not mapped yet, so we don't know on which + * output we'll be. Use scaling factor from first monitor */ + xassert(tll_length(term->wl->monitors) > 0); + term->scale = tll_front(term->wl->monitors).scale; + + /* Initialize the Wayland window backend */ + if ((term->window = wayl_win_init(term, token)) == NULL) + goto err; + + /* Load fonts */ + if (!term_font_dpi_changed(term, 0.)) + goto err; + + term->font_subpixel = get_font_subpixel(term); + + term_set_window_title(term, conf->title); + + /* Let the Wayland backend know we exist */ + tll_push_back(wayl->terms, term); + + switch (conf->startup_mode) { + case STARTUP_WINDOWED: + break; + + case STARTUP_MAXIMIZED: + xdg_toplevel_set_maximized(term->window->xdg_toplevel); + break; + + case STARTUP_FULLSCREEN: + xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL); + break; + } + + if (!initialize_render_workers(term)) + goto err; + + return term; + +err: + term->shutdown.in_progress = true; + term_destroy(term); + return NULL; + +close_fds: + close(ptmx); + fdm_del(fdm, flash_fd); + fdm_del(fdm, delay_lower_fd); + fdm_del(fdm, delay_upper_fd); + fdm_del(fdm, app_sync_updates_fd); + fdm_del(fdm, title_update_fd); + fdm_del(fdm, icon_update_fd); + fdm_del(fdm, app_id_update_fd); + + free(term); + return NULL; +} + +struct terminal * +term_tab_new(struct terminal *primary, + int argc, char *const *argv, const char *const *envp, + void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data) +{ + struct wl_window *win = primary->window; + + if (win->tab_count >= TAB_MAX) { + LOG_ERR("maximum number of tabs (%d) reached", TAB_MAX); + return NULL; + } + + const struct config *conf = primary->conf; + struct fdm *fdm = primary->fdm; + struct reaper *reaper = primary->reaper; + struct wayland *wayl = primary->wl; + + int ptmx = -1; + int flash_fd = -1; + int delay_lower_fd = -1; + int delay_upper_fd = -1; + int app_sync_updates_fd = -1; + int title_update_fd = -1; + int icon_update_fd = -1; + int app_id_update_fd = -1; + + struct terminal *term = malloc(sizeof(*term)); + if (unlikely(term == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } + + ptmx = posix_openpt(PTY_OPEN_FLAGS); + if (ptmx < 0) { + LOG_ERRNO("failed to open PTY for new tab"); + goto close_fds; + } + if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (delay_lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (delay_upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (app_sync_updates_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (title_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (icon_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (app_id_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create timer FDs for new tab"); + goto close_fds; + } + + if (ioctl(ptmx, (unsigned int)TIOCSWINSZ, + &(struct winsize){ + .ws_row = (unsigned short)primary->rows, + .ws_col = (unsigned short)primary->cols}) < 0) + { + LOG_ERRNO("failed to set TIOCSWINSZ for new tab"); + goto close_fds; + } + + key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); + + int ptmx_flags; + if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 || + fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) + { + LOG_ERRNO("failed to configure ptmx as non-blocking"); + goto err; + } + + const enum shm_bit_depth desired_bit_depth = + conf->tweak.surface_bit_depth == SHM_BITS_AUTO + ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8 + : conf->tweak.surface_bit_depth; + + const struct color_theme *theme = NULL; + switch (conf->initial_color_theme) { + case COLOR_THEME_DARK: theme = &conf->colors_dark; break; + case COLOR_THEME_LIGHT: theme = &conf->colors_light; break; + case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; + case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; + } + + *term = (struct terminal){ + .fdm = fdm, + .reaper = reaper, + .conf = conf, + .is_tab = true, + .slave = -1, + .ptmx = ptmx, + .ptmx_buffers = tll_init(), + .ptmx_paste_buffers = tll_init(), + .font_sizes = { + xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count), + xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count), + xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count), + xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), + }, + .font_dpi = 0., + .font_dpi_before_unmap = -1., + .font_subpixel = (theme->alpha == 0xffff + ? FCFT_SUBPIXEL_DEFAULT + : FCFT_SUBPIXEL_NONE), + .cursor_keys_mode = CURSOR_KEYS_NORMAL, + .keypad_keys_mode = KEYPAD_NUMERICAL, + .reverse_wrap = true, + .auto_margin = true, + .window_title_stack = tll_init(), + .scale = primary->scale, + .scale_before_unmap = -1, + .flash = {.fd = flash_fd}, + .blink = {.fd = -1}, + .vt = {.state = 0}, + .colors = { + .fg = theme->fg, + .bg = theme->bg, + .alpha = theme->alpha, + .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text, + .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor, + .selection_fg = theme->selection_fg, + .selection_bg = theme->selection_bg, + .active_theme = conf->initial_color_theme, + }, + .color_stack = {.stack = NULL, .size = 0, .idx = 0}, + .origin = ORIGIN_ABSOLUTE, + .cursor_style = conf->cursor.style, + .cursor_blink = { + .decset = false, + .deccsusr = conf->cursor.blink.enabled, + .state = CURSOR_BLINK_ON, + .fd = -1, + }, + .selection = { + .coords = {.start = {-1, -1}, .end = {-1, -1}}, + .pivot = {.start = {-1, -1}, .end = {-1, -1}}, + .auto_scroll = {.fd = -1}, + }, + .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, + .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, + .grid = &term->normal, + .composed = NULL, + .alt_scrolling = conf->mouse.alternate_scroll_mode, + .meta = {.esc_prefix = true, .eight_bit = true}, + .num_lock_modifier = true, + .bell_action_enabled = true, + .tab_stops = tll_init(), + .wl = wayl, + .window = win, + .render = { + .chains = { + .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, + desired_bit_depth, &render_buffer_release_callback, term), + .search = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .scrollback_indicator = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .render_timer = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .url = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .csd = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .tab_bar = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .tab_overview = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + }, + .scrollback_lines = conf->scrollback.lines, + .app_sync_updates.timer_fd = app_sync_updates_fd, + .title = {.timer_fd = title_update_fd}, + .icon = {.timer_fd = icon_update_fd}, + .app_id = {.timer_fd = app_id_update_fd}, + .workers = { + .count = conf->render_worker_count, + .queue = tll_init(), + }, + }, + .delayed_render_timer = { + .is_armed = false, + .lower_fd = delay_lower_fd, + .upper_fd = delay_upper_fd, + }, + .sixel = { + .scrolling = true, + .use_private_palette = true, + .palette_size = SIXEL_MAX_COLORS, + .max_width = SIXEL_MAX_WIDTH, + .max_height = SIXEL_MAX_HEIGHT, + }, + .shutdown = { + .terminate_timeout_fd = -1, + .cb = shutdown_cb, + .cb_data = shutdown_data, + }, + .foot_exe = xstrdup(primary->foot_exe), + .cwd = xstrdup( + conf->tabs.inherit_cwd + ? win->term->cwd + : (getenv("HOME") != NULL ? getenv("HOME") : "/")), + .grapheme_shaping = conf->tweak.grapheme_shaping, +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + .ime_enabled = true, +#endif + .active_notifications = tll_init(), + }; + + pixman_region32_init(&term->render.last_overlay_clip); + term_update_ascii_printer(term); + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); + + /* Inherit font sizes from primary so the new tab matches its zoom level */ + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + for (size_t j = 0; j < font_list->count; j++) + term->font_sizes[i][j] = primary->font_sizes[i][j]; + } + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) + term->notification_icons[i].tmp_file_fd = -1; + + if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) || + !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) || + !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || + !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, term) || + !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) || + !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) || + !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term)) + { + goto err; + } + + add_utmp_record(conf, reaper, ptmx); + + if ((term->slave = slave_spawn( + term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, + conf->term, conf->shell, conf->login_shell, + &conf->notifications)) == -1) + { + goto err; + } + + reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); + + /* Load fonts (uses primary's scale) */ + if (!term_font_dpi_changed(term, 0.)) + goto err; + + term->font_subpixel = get_font_subpixel(term); + + /* Resize grid to match current window dimensions. cell_width and + * cell_height were already computed by term_set_fonts above, using the + * inherited font_sizes — inheriting primary's cell geometry would mask + * that, since primary may be mid-zoom or mid-reload. */ + term->width = primary->width; + term->height = primary->height; + + tll_push_back(wayl->terms, term); + + /* Register in window's tab list */ + win->tabs[win->tab_count++] = term; + + /* Enable ptmx I/O now (window is already configured) */ + fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); + + if (!initialize_render_workers(term)) + goto err; + + /* Switch to the new tab */ + term_tab_switch(win, win->tab_count - 1); + + return term; + +err: + term->shutdown.in_progress = true; + term_destroy(term); + return NULL; + +close_fds: + if (ptmx >= 0) close(ptmx); + fdm_del(fdm, flash_fd); + fdm_del(fdm, delay_lower_fd); + fdm_del(fdm, delay_upper_fd); + fdm_del(fdm, app_sync_updates_fd); + fdm_del(fdm, title_update_fd); + fdm_del(fdm, icon_update_fd); + fdm_del(fdm, app_id_update_fd); + free(term); + return NULL; +} + +void +term_tab_switch(struct wl_window *win, size_t idx) +{ + if (idx >= win->tab_count) + return; + + struct terminal *prev = win->term; + /* render_resize expects logical (unscaled) sizes; prev->width/height + * are physical pixels. Convert back to logical so render_resize's + * internal scale multiplication doesn't double-apply it. */ + int cur_width = (int)roundf(prev->width / prev->scale); + int cur_height = (int)roundf(prev->height / prev->scale); + + win->active_tab = idx; + win->term = win->tabs[idx]; + + struct terminal *term = win->term; + + /* The newly active tab is being looked at — clear its unread flag */ + term->has_unread = false; + + /* Inherit the surface kind so cursor updates work without a pointer re-enter */ + term->active_surface = prev->active_surface; + + /* Update keyboard and mouse focus for all seats looking at this window. + * Do this before term_kbd_focus_out(prev) so its guard (checking whether + * any seat still points to it) sees that no seat does. */ + tll_foreach(term->wl->seats, it) { + struct seat *seat = &it->item; + if (seat->kbd_focus != NULL && seat->kbd_focus->window == win) + seat->kbd_focus = term; + if (seat->mouse_focus != NULL && seat->mouse_focus->window == win) + seat->mouse_focus = term; + } + + /* prev loses keyboard focus now that no seat points to it */ + term_kbd_focus_out(prev); + + bool has_kbd_focus = false; + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) { + has_kbd_focus = true; + break; + } + } + if (has_kbd_focus) + term_visual_focus_in(term); + else + term_visual_focus_out(term); + + /* Scale changes (fractional-scale-v1, preferred_buffer_scale, output + * enter/leave) only notify win->term — inactive tabs miss them. Catch + * the incoming tab up on scale, DPI, and font size before we resize. */ + const float old_scale = term->scale; + term_update_scale(term); + term_font_dpi_changed(term, old_scale); + term_font_subpixel_changed(term); + + /* Cancel the outgoing tab's in-flight frame_callback. Its listener data + * points at prev, so when it fires it services prev's empty pending bits + * and nothing gets drawn. Dropping it lets fdm_hook_refresh_pending_terminals + * take the immediate-render path for the new active tab. */ + if (win->frame_callback != NULL) { + wl_callback_destroy(win->frame_callback); + win->frame_callback = NULL; + } + + /* Use current window dimensions, not the inactive tab's stale ones. + * render_resize allocates the grid rows — term_kbd_focus_in must come + * after this because cursor_refresh dereferences cur_row. */ + render_resize(term, cur_width, cur_height, RESIZE_FORCE); + + if (has_kbd_focus) + term_kbd_focus_in(term); + + render_refresh_csd(term); + render_refresh_tab_bar(term); + term_xcursor_update(term); +} + +void +term_tab_close(struct terminal *term) +{ + term_shutdown(term); +} + +size_t +term_tab_bar_hit_test(const struct terminal *term, int x, int y) +{ + const struct wl_window *win = term->window; + if (win->tab_bar.sub == NULL) + return SIZE_MAX; + + const size_t n = win->tab_layout.count; + if (n == 0) + return SIZE_MAX; + + if (y < win->tab_layout.y || y >= win->tab_layout.y + win->tab_layout.h) + return SIZE_MAX; + + for (size_t i = 0; i < n; i++) { + const int tx = win->tab_layout.xs[i]; + const int tw = win->tab_layout.ws[i]; + if (x >= tx && x < tx + tw) + return i; + } + return SIZE_MAX; +} + +void +term_window_configured(struct terminal *term) +{ + /* Enable ptmx FDM callback */ + if (!term->shutdown.in_progress) { + xassert(term->window->is_configured); + fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + LOG_INFO("gamma-correct blending: %s", gamma_correct ? "enabled" : "disabled"); + } +} + +/* + * Shutdown logic + * + * A foot instance can be terminated in two ways: + * + * - the client application terminates (user types 'exit', or pressed C-d in the + * shell, etc) + * - the foot window is closed + * + * Both variants need to trigger to "other" action. I.e. if the client + * application is terminated, then we need to close the window. If the window is + * closed, we need to terminate the client application. + * + * Only when *both* tasks have completed do we consider ourselves fully + * shutdown. This is when we can call term_destroy(), and the user provided + * shutdown callback. + * + * The functions involved with this are: + * + * - shutdown_maybe_done(): called after any of the two tasks above have + * completed. When it determines that *both* tasks are done, it calls + * term_destroy() and the user provided shutdown callback. + * + * - fdm_client_terminated(): reaper callback, called when the client + * application has terminated. + * + * + Kills the "terminate" timeout timer + * + Calls shutdown_maybe_done() if the shutdown procedure has already + * started (i.e. the window being closed initiated the shutdown) + * -OR- + * Initiates the shutdown itself, by calling term_shutdown() (client + * application termination initiated the shutdown). + * + * - term_shutdown(): unregisters all FDM callbacks, sends SIGTERM to the client + * application and installs a "terminate" timeout timer (if it hasn't already + * terminated). Finally registers an event FD with the FDM, which is + * immediately triggered. This is done to ensure any pending FDM events are + * handled before shutting down. + * + * - fdm_shutdown(): FDM callback, triggered by the event FD in + * term_shutdown(). Unmaps and destroys the window resources, and ensures the + * seats' focused pointers don't reference us. Finally calls + * shutdown_maybe_done(). + * + * - fdm_terminate_timeout(): FDM callback for the "terminate" timeout + * timer. This function is called when the client application hasn't + * terminated after 60 seconds (after the SIGTERM). Sends SIGKILL to the + * client application. + * + * - term_destroy(): normally called from shutdown_maybe_done(), when both the + * window has been unmapped, and the client application has terminated. In + * this case, it simply destroys all resources. + * + * It may however also be called without term_shutdown() having been called + * (typically in error code paths - for example, when the Wayland connection + * is closed by the compositor). In this case, the client application is + * typically still running, and we can't assume the FDM is running. To handle + * this, we install configure a 60 second SIGALRM, send SIGTERM to the client + * application, and then enter a blocking waitpid(). + * + * If the alarm triggers, we send SIGKILL and once again enter a blocking + * waitpid(). + */ + +static void +shutdown_maybe_done(struct terminal *term) +{ + bool shutdown_done = + term->shutdown.fdm_done && term->shutdown.client_has_terminated; + + LOG_DBG("fdm_done=%d, slave-has-been-reaped=%d --> %s", + term->shutdown.fdm_done, term->shutdown.client_has_terminated, + (shutdown_done + ? "shutdown done, calling term_destroy()" + : "no action")); + + if (!shutdown_done) + return; + + void (*cb)(void *, int) = term->shutdown.cb; + void *cb_data = term->shutdown.cb_data; + + int exit_code = term_destroy(term); + if (cb != NULL) + cb(cb_data, exit_code); +} + +static void +fdm_client_terminated(struct reaper *reaper, pid_t pid, int status, void *data) +{ + struct terminal *term = data; + LOG_DBG("slave (PID=%u) died", pid); + + term->shutdown.client_has_terminated = true; + term->shutdown.exit_status = status; + + if (term->shutdown.terminate_timeout_fd >= 0) { + fdm_del(term->fdm, term->shutdown.terminate_timeout_fd); + term->shutdown.terminate_timeout_fd = -1; + } + + if (term->shutdown.in_progress) + shutdown_maybe_done(term); + else if (!term->conf->hold_at_exit) + term_shutdown(term); +} + +static bool +fdm_shutdown(struct fdm *fdm, int fd, int events, void *data) +{ + struct terminal *term = data; + + /* Kill the event FD */ + fdm_del(term->fdm, fd); + + struct wl_window *win = term->window; + + if (win != NULL && win->tab_count <= 1) { + /* + * Last (or only) tab — destroy the whole window. + * + * Normally we'd get unmapped when we destroy the Wayland + * surface above. However, it appears that under certain + * conditions, those events are deferred (for example, when + * a screen locker is active), and thus we can get here + * without having been unmapped. + */ + wayl_win_destroy(win); + term->window = NULL; + + struct wayland *wayl = term->wl; + tll_foreach(wayl->seats, it) { + if (it->item.kbd_focus == term) + it->item.kbd_focus = NULL; + if (it->item.mouse_focus == term) + it->item.mouse_focus = NULL; + } + } + /* + * Multi-tab case: leave term->window intact so term_destroy() can + * use its existing tab-removal and tab-switch logic. + */ + + term->shutdown.fdm_done = true; + shutdown_maybe_done(term); + return true; +} + +static bool +fdm_terminate_timeout(struct fdm *fdm, int fd, int events, void *data) +{ + uint64_t unused; + ssize_t bytes = read(fd, &unused, sizeof(unused)); + if (bytes < 0) { + LOG_ERRNO("failed to read from slave terminate timeout FD"); + return false; + } + + struct terminal *term = data; + xassert(!term->shutdown.client_has_terminated); + + LOG_DBG("slave (PID=%u) has not terminated, sending %s (%d)", + term->slave, + term->shutdown.next_signal == SIGTERM ? "SIGTERM" + : term->shutdown.next_signal == SIGKILL ? "SIGKILL" + : "", + term->shutdown.next_signal); + + kill(-term->slave, term->shutdown.next_signal); + + switch (term->shutdown.next_signal) { + case SIGTERM: + term->shutdown.next_signal = SIGKILL; + break; + + case SIGKILL: + /* Disarm. Shouldn't be necessary, as we should be able to + shutdown completely after sending SIGKILL, before the next + timeout occurs). But lets play it safe... */ + if (term->shutdown.terminate_timeout_fd >= 0) { + timerfd_settime( + term->shutdown.terminate_timeout_fd, 0, + &(const struct itimerspec){0}, NULL); + } + break; + + default: + BUG("can only handle SIGTERM and SIGKILL"); + return false; + } + + return true; +} + +bool +term_shutdown(struct terminal *term) +{ + if (term->shutdown.in_progress) + return true; + + term->shutdown.in_progress = true; + + /* + * Close FDs then postpone self-destruction to the next poll + * iteration, by creating an event FD that we trigger immediately. + */ + + term_cursor_blink_update(term); + xassert(term->cursor_blink.fd < 0); + + fdm_del(term->fdm, term->selection.auto_scroll.fd); + fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); + fdm_del(term->fdm, term->render.app_id.timer_fd); + fdm_del(term->fdm, term->render.icon.timer_fd); + fdm_del(term->fdm, term->render.title.timer_fd); + fdm_del(term->fdm, term->delayed_render_timer.lower_fd); + fdm_del(term->fdm, term->delayed_render_timer.upper_fd); + fdm_del(term->fdm, term->blink.fd); + fdm_del(term->fdm, term->flash.fd); + + del_utmp_record(term->conf, term->reaper, term->ptmx); + + if (term->window != NULL && term->window->is_configured) + fdm_del(term->fdm, term->ptmx); + else + close(term->ptmx); + + if (!term->shutdown.client_has_terminated) { + if (term->slave <= 0) { + term->shutdown.client_has_terminated = true; + } else { + LOG_DBG("initiating asynchronous terminate of slave; " + "sending SIGHUP to PID=%u", term->slave); + + kill(-term->slave, SIGHUP); + + /* + * Set up a timer, with an interval - on the first timeout + * we'll send SIGTERM. If the the client application still + * isn't terminating, we'll wait an additional interval, + * and then send SIGKILL. + */ + const struct itimerspec timeout = {.it_value = {.tv_sec = 30}, + .it_interval = {.tv_sec = 30}}; + + int timeout_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (timeout_fd < 0 || + timerfd_settime(timeout_fd, 0, &timeout, NULL) < 0 || + !fdm_add(term->fdm, timeout_fd, EPOLLIN, &fdm_terminate_timeout, term)) + { + if (timeout_fd >= 0) + close(timeout_fd); + LOG_ERRNO("failed to create slave terminate timeout FD"); + return false; + } + + xassert(term->shutdown.terminate_timeout_fd < 0); + term->shutdown.terminate_timeout_fd = timeout_fd; + term->shutdown.next_signal = SIGTERM; + } + } + + term->selection.auto_scroll.fd = -1; + term->render.app_sync_updates.timer_fd = -1; + term->render.app_id.timer_fd = -1; + term->render.icon.timer_fd = -1; + term->render.title.timer_fd = -1; + term->delayed_render_timer.lower_fd = -1; + term->delayed_render_timer.upper_fd = -1; + term->blink.fd = -1; + term->flash.fd = -1; + term->ptmx = -1; + + int event_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (event_fd == -1) { + LOG_ERRNO("failed to create terminal shutdown event FD"); + return false; + } + + if (!fdm_add(term->fdm, event_fd, EPOLLIN, &fdm_shutdown, term)) { + close(event_fd); + return false; + } + + if (write(event_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) { + LOG_ERRNO("failed to send terminal shutdown event"); + fdm_del(term->fdm, event_fd); + return false; + } + + return true; +} + +static volatile sig_atomic_t alarm_raised; + +static void +sig_alarm(int signo) +{ + LOG_DBG("SIGALRM"); + alarm_raised = 1; +} + +int +term_destroy(struct terminal *term) +{ + if (term == NULL) + return 0; + + tll_foreach(term->wl->terms, it) { + if (it->item == term) { + tll_remove(term->wl->terms, it); + break; + } + } + + del_utmp_record(term->conf, term->reaper, term->ptmx); + + fdm_del(term->fdm, term->selection.auto_scroll.fd); + fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); + fdm_del(term->fdm, term->render.app_id.timer_fd); + fdm_del(term->fdm, term->render.icon.timer_fd); + fdm_del(term->fdm, term->render.title.timer_fd); + fdm_del(term->fdm, term->delayed_render_timer.lower_fd); + fdm_del(term->fdm, term->delayed_render_timer.upper_fd); + fdm_del(term->fdm, term->cursor_blink.fd); + fdm_del(term->fdm, term->blink.fd); + fdm_del(term->fdm, term->flash.fd); + fdm_del(term->fdm, term->ptmx); + if (term->shutdown.terminate_timeout_fd >= 0) + fdm_del(term->fdm, term->shutdown.terminate_timeout_fd); + + if (term->window != NULL) { + struct wl_window *win = term->window; + + /* Remove ourselves from the window's tab list */ + bool was_active = (win->term == term); + size_t our_idx = win->tab_count; /* invalid sentinel */ + for (size_t i = 0; i < win->tab_count; i++) { + if (win->tabs[i] == term) { + our_idx = i; + memmove(&win->tabs[i], &win->tabs[i + 1], + (win->tab_count - i - 1) * sizeof(win->tabs[0])); + win->tab_count--; + if (win->active_tab > 0 && win->active_tab >= win->tab_count) + win->active_tab = win->tab_count - 1; + else if (our_idx < win->active_tab) + win->active_tab--; + break; + } + } + + if (win->tab_count == 0) { + /* Last tab - destroy the window normally */ + wayl_win_destroy(win); + } else { + /* Other tabs remain - just purge our render chains */ + + /* Cancel any pending frame callback that still holds a pointer + * to this terminal, preventing a use-after-free when the + * compositor fires it after term is freed. */ + if (win->frame_callback != NULL) { + wl_callback_destroy(win->frame_callback); + win->frame_callback = NULL; + } + + render_wait_for_preapply_damage(term); + shm_purge(term->render.chains.search); + shm_purge(term->render.chains.scrollback_indicator); + shm_purge(term->render.chains.render_timer); + shm_purge(term->render.chains.grid); + shm_purge(term->render.chains.url); + shm_purge(term->render.chains.csd); + shm_purge(term->render.chains.tab_bar); + shm_purge(term->render.chains.tab_overview); + shm_purge(term->render.chains.overlay); + + /* Switch to the new active tab if needed */ + if (was_active) { + struct terminal *next = win->tabs[win->active_tab]; + win->term = next; + next->active_surface = term->active_surface; + + /* Update keyboard and mouse focus */ + bool has_kbd_focus = false; + tll_foreach(next->wl->seats, it) { + struct seat *seat = &it->item; + if (seat->kbd_focus == term) + seat->kbd_focus = next; + if (seat->mouse_focus == term) + seat->mouse_focus = next; + if (seat->kbd_focus == next) + has_kbd_focus = true; + } + + if (has_kbd_focus) { + term_kbd_focus_in(next); + term_visual_focus_in(next); + } else { + term_visual_focus_out(next); + } + + render_resize(next, + (int)roundf(term->width / term->scale), + (int)roundf(term->height / term->scale), + RESIZE_FORCE); + render_refresh_csd(next); + render_refresh_tab_bar(next); + term_xcursor_update(next); + } else { + /* Just update the tab bar to reflect removed tab */ + render_refresh_tab_bar(win->term); + } + } + + term->window = NULL; + } + + mtx_lock(&term->render.workers.lock); + xassert(tll_length(term->render.workers.queue) == 0); + + /* Count livinig threads - we may get here when only some of the + * threads have been successfully started */ + size_t worker_count = 0; + if (term->render.workers.threads != NULL) { + for (size_t i = 0; i < term->render.workers.count; i++, worker_count++) { + if (term->render.workers.threads[i] == 0) + break; + } + + for (size_t i = 0; i < worker_count; i++) { + sem_post(&term->render.workers.start); + tll_push_back(term->render.workers.queue, -2); + } + } + mtx_unlock(&term->render.workers.lock); + + key_binding_unref(term->wl->key_binding_manager, term->conf); + + urls_reset(term); + + free(term->vt.osc.data); + free(term->vt.osc8.uri); + + composed_free(term->composed); + + free(term->app_id); + free(term->window_title); + tll_free_and_free(term->window_title_stack, free); + + for (size_t i = 0; i < sizeof(term->fonts) / sizeof(term->fonts[0]); i++) + fcft_destroy(term->fonts[i]); + for (size_t i = 0; i < 4; i++) + free(term->font_sizes[i]); + + + free_custom_glyphs( + &term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT); + free_custom_glyphs( + &term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); + free_custom_glyphs( + &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); + free_custom_glyphs( + &term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT); + + free(term->search.buf); + free(term->search.last.buf); + + /* Free search history */ + { + struct search_history_entry *e = term->search.history_head; + while (e != NULL) { + struct search_history_entry *next = e->next; + free(e->buf); + free(e); + e = next; + } + } + + /* Free compiled regex if any */ + if (term->search.regex_compiled != NULL) { + regfree(term->search.regex_compiled); + free(term->search.regex_compiled); + } + + if (term->render.workers.threads != NULL) { + for (size_t i = 0; i < term->render.workers.count; i++) { + if (term->render.workers.threads[i] != 0) + thrd_join(term->render.workers.threads[i], NULL); + } + } + free(term->render.workers.threads); + mtx_destroy(&term->render.workers.preapplied_damage.lock); + cnd_destroy(&term->render.workers.preapplied_damage.cond); + mtx_destroy(&term->render.workers.lock); + sem_destroy(&term->render.workers.start); + sem_destroy(&term->render.workers.done); + xassert(tll_length(term->render.workers.queue) == 0); + tll_free(term->render.workers.queue); + + shm_unref(term->render.last_buf); + shm_chain_free(term->render.chains.grid); + shm_chain_free(term->render.chains.search); + shm_chain_free(term->render.chains.scrollback_indicator); + shm_chain_free(term->render.chains.render_timer); + shm_chain_free(term->render.chains.url); + shm_chain_free(term->render.chains.csd); + shm_chain_free(term->render.chains.overlay); + shm_chain_free(term->render.chains.tab_bar); + shm_chain_free(term->render.chains.tab_overview); + pixman_region32_fini(&term->render.last_overlay_clip); + + tll_free(term->tab_stops); + + tll_foreach(term->ptmx_buffers, it) { + free(it->item.data); + tll_remove(term->ptmx_buffers, it); + } + tll_foreach(term->ptmx_paste_buffers, it) { + free(it->item.data); + tll_remove(term->ptmx_paste_buffers, it); + } + + notify_free(term, &term->kitty_notification); + tll_foreach(term->active_notifications, it) { + notify_free(term, &it->item); + tll_remove(term->active_notifications, it); + } + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) + notify_icon_free(&term->notification_icons[i]); + + sixel_fini(term); + + term_ime_reset(term); + + grid_free(&term->normal); + grid_free(&term->alt); + grid_free(term->interactive_resizing.grid); + free(term->interactive_resizing.grid); + + free(term->foot_exe); + free(term->cwd); + free(term->mouse_user_cursor); + free(term->color_stack.stack); + + int ret = EXIT_SUCCESS; + + if (term->slave > 0) { + /* We'll deal with this explicitly */ + reaper_del(term->reaper, term->slave); + + int exit_status; + + if (term->shutdown.client_has_terminated) + exit_status = term->shutdown.exit_status; + else { + LOG_DBG("initiating blocking terminate of slave; " + "sending SIGHUP to PID=%u", term->slave); + + kill(-term->slave, SIGHUP); + + /* + * we've closed the ptxm, and sent SIGTERM to the client + * application. It *should* exit... + * + * But, since it is possible to write clients that ignore + * this, we need to handle it in *some* way. + * + * So, what we do is register a SIGALRM handler, and configure a 30 + * second alarm. If the slave hasn't died after this time, we send + * it a SIGKILL, + * + * Note that this solution is *not* asynchronous, and any + * other events etc will be ignored during this time. This of + * course only applies to a 'foot --server' instance, where + * there might be other terminals running. + */ + struct sigaction action = {.sa_handler = &sig_alarm}; + sigemptyset(&action.sa_mask); + sigaction(SIGALRM, &action, NULL); + + /* Wait, then send SIGTERM, wait again, then send SIGKILL */ + int next_signal = SIGTERM; + + alarm_raised = 0; + alarm(30); + + while (true) { + int r = waitpid(term->slave, &exit_status, 0); + + if (r == term->slave) + break; + + if (r == -1) { + xassert(errno == EINTR); + + if (alarm_raised) { + LOG_DBG("slave (PID=%u) has not terminated yet, " + "sending: %s (%d)", term->slave, + next_signal == SIGTERM ? "SIGTERM" : "SIGKILL", + next_signal); + + kill(-term->slave, next_signal); + next_signal = SIGKILL; + + alarm_raised = 0; + alarm(30); + } + } + } + + /* Cancel alarm */ + alarm(0); + action.sa_handler = SIG_DFL; + sigaction(SIGALRM, &action, NULL); + } + + ret = EXIT_FAILURE; + if (WIFEXITED(exit_status)) { + ret = WEXITSTATUS(exit_status); + LOG_DBG("slave exited with code %d", ret); + } else if (WIFSIGNALED(exit_status)) { + ret = WTERMSIG(exit_status); + LOG_WARN("slave exited with signal %d (%s)", ret, strsignal(ret)); + } else { + LOG_WARN("slave exited for unknown reason (status = 0x%08x)", + exit_status); + } + } + + free(term); + +#if defined(__GLIBC__) + if (!malloc_trim(0)) + LOG_WARN("failed to trim memory"); +#endif + + return ret; +} + +static inline void +erase_cell_range(struct terminal *term, struct row *row, int start, int end) +{ + xassert(start < term->cols); + xassert(end < term->cols); + + row->dirty = true; + + const enum color_source bg_src = term->vt.attrs.bg_src; + + if (unlikely(bg_src != COLOR_DEFAULT)) { + for (int col = start; col <= end; col++) { + struct cell *c = &row->cells[col]; + c->wc = 0; + c->attrs = (struct attributes){.bg_src = bg_src, .bg = term->vt.attrs.bg}; + } + } else + memset(&row->cells[start], 0, (end - start + 1) * sizeof(row->cells[0])); + + if (unlikely(row->extra != NULL)) { + grid_row_uri_range_erase(row, start, end); + grid_row_underline_range_erase(row, start, end); + } +} + +static inline void +erase_line(struct terminal *term, struct row *row) +{ + erase_cell_range(term, row, 0, term->cols - 1); + row->linebreak = true; + row->shell_integration.prompt_marker = false; + row->shell_integration.cmd_start = -1; + row->shell_integration.cmd_end = -1; +} + +static void +term_theme_apply(struct terminal *term, const struct color_theme *theme) +{ + term->colors.fg = theme->fg; + term->colors.bg = theme->bg; + term->colors.alpha = theme->alpha; + term->colors.cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text; + term->colors.cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor; + term->colors.selection_fg = theme->selection_fg; + term->colors.selection_bg = theme->selection_bg; + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); +} + +void +term_reset(struct terminal *term, bool hard) +{ + LOG_INFO("%s resetting the terminal", hard ? "hard" : "soft"); + + term->cursor_keys_mode = CURSOR_KEYS_NORMAL; + term->keypad_keys_mode = KEYPAD_NUMERICAL; + term->reverse = false; + term->hide_cursor = false; + term->reverse_wrap = true; + term->auto_margin = true; + term->insert_mode = false; + term->bracketed_paste = false; + term->focus_events = false; + term->num_lock_modifier = true; + term->bell_action_enabled = true; + term->mouse_tracking = MOUSE_NONE; + term->mouse_reporting = MOUSE_NORMAL; + term->charsets.selected = G0; + term->charsets.set[G0] = CHARSET_ASCII; + term->charsets.set[G1] = CHARSET_ASCII; + term->charsets.set[G2] = CHARSET_ASCII; + term->charsets.set[G3] = CHARSET_ASCII; + term->saved_charsets = term->charsets; + tll_free_and_free(term->window_title_stack, free); + term_set_window_title(term, term->conf->title); + term_set_app_id(term, NULL); + + term_set_user_mouse_cursor(term, NULL); + + term->modify_other_keys_2 = false; + memset(term->normal.kitty_kbd.flags, 0, sizeof(term->normal.kitty_kbd.flags)); + memset(term->alt.kitty_kbd.flags, 0, sizeof(term->alt.kitty_kbd.flags)); + term->normal.kitty_kbd.idx = term->alt.kitty_kbd.idx = 0; + + term->scroll_region.start = 0; + term->scroll_region.end = term->rows; + + free(term->vt.osc8.uri); + free(term->vt.osc.data); + + term->vt = (struct vt){ + .state = 0, /* STATE_GROUND */ + }; + + if (term->grid == &term->alt) { + term->grid = &term->normal; + selection_cancel(term); + } + + term->meta.esc_prefix = true; + term->meta.eight_bit = true; + + tll_foreach(term->normal.sixel_images, it) { + sixel_destroy(&it->item); + tll_remove(term->normal.sixel_images, it); + } + tll_foreach(term->alt.sixel_images, it) { + sixel_destroy(&it->item); + tll_remove(term->alt.sixel_images, it); + } + + notify_free(term, &term->kitty_notification); + tll_foreach(term->active_notifications, it) { + notify_free(term, &it->item); + tll_remove(term->active_notifications, it); + } + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) + notify_icon_free(&term->notification_icons[i]); + + term->grapheme_shaping = term->conf->tweak.grapheme_shaping; + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + term_ime_enable(term); +#endif + + term->bits_affecting_ascii_printer.value = 0; + term_update_ascii_printer(term); + + if (!hard) + return; + + const struct color_theme *theme = NULL; + + switch (term->conf->initial_color_theme) { + case COLOR_THEME_DARK: theme = &term->conf->colors_dark; break; + case COLOR_THEME_LIGHT: theme = &term->conf->colors_light; break; + case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; + case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; + } + + term->flash.active = false; + term->blink.state = BLINK_ON; + fdm_del(term->fdm, term->blink.fd); term->blink.fd = -1; + term_theme_apply(term, theme); + term->colors.active_theme = term->conf->initial_color_theme; + free(term->color_stack.stack); + term->color_stack.stack = NULL; + term->color_stack.size = 0; + term->color_stack.idx = 0; + term->origin = ORIGIN_ABSOLUTE; + term->normal.cursor.lcf = false; + term->alt.cursor.lcf = false; + term->normal.cursor = (struct cursor){.point = {0, 0}}; + term->normal.saved_cursor = (struct cursor){.point = {0, 0}}; + term->alt.cursor = (struct cursor){.point = {0, 0}}; + term->alt.saved_cursor = (struct cursor){.point = {0, 0}}; + term->cursor_style = term->conf->cursor.style; + term->cursor_blink.decset = false; + term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; + term_cursor_blink_update(term); + selection_cancel(term); + term->normal.offset = term->normal.view = 0; + term->alt.offset = term->alt.view = 0; + for (size_t i = 0; i < term->rows; i++) { + struct row *r = grid_row_and_alloc(&term->normal, i); + erase_line(term, r); + } + for (size_t i = 0; i < term->rows; i++) { + struct row *r = grid_row_and_alloc(&term->alt, i); + erase_line(term, r); + } + for (size_t i = term->rows; i < term->normal.num_rows; i++) { + grid_row_free(term->normal.rows[i]); + term->normal.rows[i] = NULL; + } + for (size_t i = term->rows; i < term->alt.num_rows; i++) { + grid_row_free(term->alt.rows[i]); + term->alt.rows[i] = NULL; + } + term->normal.cur_row = term->normal.rows[0]; + term->alt.cur_row = term->alt.rows[0]; + tll_free(term->normal.scroll_damage); + tll_free(term->alt.scroll_damage); + term->render.last_cursor.row = NULL; + term_damage_all(term); + + term->sixel.scrolling = true; + term->sixel.cursor_right_of_graphics = false; + term->sixel.use_private_palette = true; + term->sixel.max_width = SIXEL_MAX_WIDTH; + term->sixel.max_height = SIXEL_MAX_HEIGHT; + term->sixel.palette_size = SIXEL_MAX_COLORS; + free(term->sixel.private_palette); + free(term->sixel.shared_palette); + term->sixel.private_palette = term->sixel.shared_palette = NULL; +} + +static bool +term_font_size_adjust_by_points(struct terminal *term, float amount) +{ + const struct config *conf = term->conf; + const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + struct config_font *font = &term->font_sizes[i][j]; + float old_pt_size = font->pt_size; + + if (font->px_size > 0) + old_pt_size = font->px_size * 72. / dpi; + + font->pt_size = fmaxf(old_pt_size + amount, 0.); + font->px_size = -1; + } + } + + return reload_fonts(term, true); +} + +static bool +term_font_size_adjust_by_pixels(struct terminal *term, int amount) +{ + const struct config *conf = term->conf; + const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + struct config_font *font = &term->font_sizes[i][j]; + int old_px_size = font->px_size; + + if (font->px_size <= 0) + old_px_size = font->pt_size * dpi / 72.; + + font->px_size = max(old_px_size + amount, 1); + } + } + + return reload_fonts(term, true); +} + +static bool +term_font_size_adjust_by_percent(struct terminal *term, bool increment, float percent) +{ + const struct config *conf = term->conf; + const float multiplier = increment + ? 1. + percent + : 1. / (1. + percent); + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + struct config_font *font = &term->font_sizes[i][j]; + + if (font->px_size > 0) + font->px_size = max(font->px_size * multiplier, 1); + else + font->pt_size = fmax(font->pt_size * multiplier, 0); + } + } + + return reload_fonts(term, true); +} + +bool +term_font_size_increase(struct terminal *term) +{ + const struct config *conf = term->conf; + const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; + + if (inc_dec->percent > 0.) + return term_font_size_adjust_by_percent(term, true, inc_dec->percent); + else if (inc_dec->pt_or_px.px > 0) + return term_font_size_adjust_by_pixels(term, inc_dec->pt_or_px.px); + else + return term_font_size_adjust_by_points(term, inc_dec->pt_or_px.pt); +} + +bool +term_font_size_decrease(struct terminal *term) +{ + const struct config *conf = term->conf; + const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; + + if (inc_dec->percent > 0.) + return term_font_size_adjust_by_percent(term, false, inc_dec->percent); + else if (inc_dec->pt_or_px.px > 0) + return term_font_size_adjust_by_pixels(term, -inc_dec->pt_or_px.px); + else + return term_font_size_adjust_by_points(term, -inc_dec->pt_or_px.pt); +} + +bool +term_font_size_reset(struct terminal *term) +{ + return load_fonts_from_conf(term); +} + +bool +term_fractional_scaling(const struct terminal *term) +{ + return term->wl->fractional_scale_manager != NULL && + term->wl->viewporter != NULL && + term->window->scale > 0.; +} + +bool +term_preferred_buffer_scale(const struct terminal *term) +{ + return term->window->preferred_buffer_scale > 0; +} + +bool +term_update_scale(struct terminal *term) +{ + const struct wl_window *win = term->window; + + /* + * We have a number of "sources" we can use as scale. We choose + * the scale in the following order: + * + * - "preferred" scale, from the fractional-scale-v1 protocol + * - "preferred" scale, from wl_compositor version 6. + NOTE: if the compositor advertises version 6 we must use 1.0 + until wl_surface.preferred_buffer_scale is sent + * - scaling factor of output we most recently were mapped on + * - if we're not mapped, use the last known scaling factor + * - if we're not mapped, and we don't have a last known scaling + * factor, use the scaling factor from the first available + * output. + * - if there aren't any outputs available, use 1.0 + */ + const float new_scale = (term_fractional_scaling(term) + ? win->scale + : term_preferred_buffer_scale(term) + ? win->preferred_buffer_scale + : tll_length(win->on_outputs) > 0 + ? tll_back(win->on_outputs)->scale + : term->scale_before_unmap > 0. + ? term->scale_before_unmap + : tll_length(term->wl->monitors) > 0 + ? tll_front(term->wl->monitors).scale + : 1.); + + if (new_scale == term->scale) + return false; + + LOG_DBG("scaling factor changed: %.2f -> %.2f", term->scale, new_scale); + term->scale_before_unmap = new_scale; + term->scale = new_scale; + return true; +} + +bool +term_font_dpi_changed(struct terminal *term, float old_scale) +{ + float dpi = get_font_dpi(term); + xassert(term->scale > 0.); + + bool was_scaled_using_dpi = term->font_is_sized_by_dpi; + bool will_scale_using_dpi = term->conf->dpi_aware; + + bool need_font_reload = + was_scaled_using_dpi != will_scale_using_dpi || + (will_scale_using_dpi + ? term->font_dpi != dpi + : old_scale != term->scale); + + if (need_font_reload) { + LOG_DBG("DPI/scale change: DPI-aware=%s, " + "DPI: %.2f -> %.2f, scale: %.2f -> %.2f, " + "sizing font based on monitor's %s", + term->conf->dpi_aware ? "yes" : "no", + term->font_dpi, dpi, old_scale, term->scale, + will_scale_using_dpi ? "DPI" : "scaling factor"); + } + + term->font_dpi = dpi; + term->font_dpi_before_unmap = dpi; + term->font_is_sized_by_dpi = will_scale_using_dpi; + + if (!need_font_reload) + return false; + + return reload_fonts(term, false); +} + +void +term_font_subpixel_changed(struct terminal *term) +{ + enum fcft_subpixel subpixel = get_font_subpixel(term); + + if (term->font_subpixel == subpixel) + return; + +#if defined(_DEBUG) && LOG_ENABLE_DBG + static const char *const str[] = { + [FCFT_SUBPIXEL_DEFAULT] = "default", + [FCFT_SUBPIXEL_NONE] = "disabled", + [FCFT_SUBPIXEL_HORIZONTAL_RGB] = "RGB", + [FCFT_SUBPIXEL_HORIZONTAL_BGR] = "BGR", + [FCFT_SUBPIXEL_VERTICAL_RGB] = "V-RGB", + [FCFT_SUBPIXEL_VERTICAL_BGR] = "V-BGR", + }; + + LOG_DBG("subpixel mode changed: %s -> %s", str[term->font_subpixel], str[subpixel]); +#endif + + term->font_subpixel = subpixel; + term_damage_view(term); + render_refresh(term); +} + +int +term_font_baseline(const struct terminal *term) +{ + const struct fcft_font *font = term->fonts[0]; + const int line_height = term->cell_height; + const int font_height = font->ascent + font->descent; + + /* + * Center glyph on the line *if* using a custom line height, + * otherwise the baseline is simply 'descent' pixels above the + * bottom of the cell + */ + const int glyph_top_y = term->font_line_height.px >= 0 + ? round((line_height - font_height) / 2.) + : 0; + + return term->font_y_ofs + line_height - glyph_top_y - font->descent; +} + +void +term_damage_rows(struct terminal *term, int start, int end) +{ + xassert(start <= end); + for (int r = start; r <= end; r++) { + struct row *row = grid_row(term->grid, r); + row->dirty = true; + for (int c = 0; c < term->grid->num_cols; c++) + row->cells[c].attrs.clean = 0; + } +} + +void +term_damage_rows_in_view(struct terminal *term, int start, int end) +{ + xassert(start <= end); + for (int r = start; r <= end; r++) { + struct row *row = grid_row_in_view(term->grid, r); + row->dirty = true; + for (int c = 0; c < term->grid->num_cols; c++) + row->cells[c].attrs.clean = 0; + } +} + +void +term_damage_all(struct terminal *term) +{ + term_damage_rows(term, 0, term->rows - 1); +} + +void +term_damage_view(struct terminal *term) +{ + term_damage_rows_in_view(term, 0, term->rows - 1); +} + +void +term_damage_cursor(struct terminal *term) +{ + term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0; + term->grid->cur_row->dirty = true; +} + +void +term_damage_margins(struct terminal *term) +{ + term->render.margins = true; +} + +void +term_damage_color(struct terminal *term, enum color_source src, int idx) +{ + xassert(src == COLOR_DEFAULT || src == COLOR_BASE256); + + for (int r = 0; r < term->rows; r++) { + struct row *row = grid_row_in_view(term->grid, r); + struct cell *cell = &row->cells[0]; + const struct cell *end = &row->cells[term->cols]; + + for (; cell < end; cell++) { + bool dirty = false; + + switch (cell->attrs.fg_src) { + case COLOR_BASE16: + case COLOR_BASE256: + if (src == COLOR_BASE256 && cell->attrs.fg == idx) + dirty = true; + break; + + case COLOR_DEFAULT: + if (src == COLOR_DEFAULT) { + /* Doesn't matter whether we've updated the + default foreground, or background, we still + want to dirty this cell, to be sure we handle + all cases of color inversion/reversal */ + dirty = true; + } + break; + + case COLOR_RGB: + /* Not affected */ + break; + } + + switch (cell->attrs.bg_src) { + case COLOR_BASE16: + case COLOR_BASE256: + if (src == COLOR_BASE256 && cell->attrs.bg == idx) + dirty = true; + break; + + case COLOR_DEFAULT: + if (src == COLOR_DEFAULT) { + /* Doesn't matter whether we've updated the + default foreground, or background, we still + want to dirty this cell, to be sure we handle + all cases of color inversion/reversal */ + dirty = true; + } + break; + + case COLOR_RGB: + /* Not affected */ + break; + } + + if (dirty) { + cell->attrs.clean = 0; + row->dirty = true; + } + } + + /* Colored underlines */ + if (row->extra != NULL) { + const struct row_ranges *underlines = &row->extra->underline_ranges; + + for (int i = 0; i < underlines->count; i++) { + const struct row_range *range = &underlines->v[i]; + + /* Underline colors are either default, or + BASE256/RGB, but never BASE16 */ + xassert(range->underline.color_src == COLOR_DEFAULT || + range->underline.color_src == COLOR_BASE256 || + range->underline.color_src == COLOR_RGB); + + if (range->underline.color_src == src) { + struct cell *c = &row->cells[range->start]; + const struct cell *e = &row->cells[range->end + 1]; + + for (; c < e; c++) + c->attrs.clean = 0; + + row->dirty = true; + } + } + } + } +} + +void +term_damage_scroll(struct terminal *term, enum damage_type damage_type, + struct scroll_region region, int lines) +{ + if (likely(tll_length(term->grid->scroll_damage) > 0)) { + struct damage *dmg = &tll_back(term->grid->scroll_damage); + + if (likely( + dmg->type == damage_type && + dmg->region.start == region.start && + dmg->region.end == region.end)) + { + /* Make sure we don't overflow... */ + int new_line_count = (int)dmg->lines + lines; + if (likely(new_line_count <= UINT16_MAX)) { + dmg->lines = new_line_count; + return; + } + } + } + struct damage dmg = { + .type = damage_type, + .region = region, + .lines = lines, + }; + tll_push_back(term->grid->scroll_damage, dmg); +} + +void +term_erase(struct terminal *term, int start_row, int start_col, + int end_row, int end_col) +{ + xassert(start_row <= end_row); + xassert(start_col <= end_col || start_row < end_row); + + if (start_row == end_row) { + struct row *row = grid_row(term->grid, start_row); + erase_cell_range(term, row, start_col, end_col); + sixel_overwrite_by_row(term, start_row, start_col, end_col - start_col + 1); + return; + } + + xassert(end_row > start_row); + + erase_cell_range( + term, grid_row(term->grid, start_row), start_col, term->cols - 1); + sixel_overwrite_by_row(term, start_row, start_col, term->cols - start_col); + + for (int r = start_row + 1; r < end_row; r++) + erase_line(term, grid_row(term->grid, r)); + sixel_overwrite_by_rectangle( + term, start_row + 1, 0, end_row - start_row, term->cols); + + erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col); + sixel_overwrite_by_row(term, end_row, 0, end_col + 1); +} + +void +term_erase_scrollback(struct terminal *term) +{ + const struct grid *grid = term->grid; + const int num_rows = grid->num_rows; + const int mask = num_rows - 1; + + const int scrollback_history_size = num_rows - term->rows; + if (scrollback_history_size == 0) + return; + + const int start = (grid->offset + term->rows) & mask; + const int end = (grid->offset - 1) & mask; + + const int rel_start = grid_row_abs_to_sb(grid, term->rows, start); + const int rel_end = grid_row_abs_to_sb(grid, term->rows, end); + + const int sel_start = selection_get_start(term).row; + const int sel_end = selection_get_end(term).row; + + if (sel_end >= 0) { + /* + * Cancel selection if it touches any of the rows in the + * scrollback, since we can't have the selection reference + * soon-to-be deleted rows. + * + * This is done by range checking the selection range against + * the scrollback range. + * + * To make this comparison simpler, the start/end absolute row + * numbers are "rebased" against the scrollback start, where + * row 0 is the *first* row in the scrollback. A high number + * thus means the row is further *down* in the scrollback, + * closer to the screen bottom. + */ + + const int rel_sel_start = grid_row_abs_to_sb(grid, term->rows, sel_start); + const int rel_sel_end = grid_row_abs_to_sb(grid, term->rows, sel_end); + + if ((rel_sel_start <= rel_start && rel_sel_end >= rel_start) || + (rel_sel_start <= rel_end && rel_sel_end >= rel_end) || + (rel_sel_start >= rel_start && rel_sel_end <= rel_end)) + { + selection_cancel(term); + } + } + + tll_foreach(term->grid->sixel_images, it) { + struct sixel *six = &it->item; + const int six_start = grid_row_abs_to_sb(grid, term->rows, six->pos.row); + const int six_end = grid_row_abs_to_sb( + grid, term->rows, six->pos.row + six->rows - 1); + + if ((six_start <= rel_start && six_end >= rel_start) || + (six_start <= rel_end && six_end >= rel_end) || + (six_start >= rel_start && six_end <= rel_end)) + { + sixel_destroy(six); + tll_remove(term->grid->sixel_images, it); + } + } + + for (int i = start;; i = (i + 1) & mask) { + struct row *row = term->grid->rows[i]; + if (row != NULL) { + if (term->render.last_cursor.row == row) + term->render.last_cursor.row = NULL; + + grid_row_free(row); + term->grid->rows[i] = NULL; + } + + if (i == end) + break; + } + + term->grid->view = term->grid->offset; + +#if defined(_DEBUG) + for (int i = 0; i < term->rows; i++) { + xassert(grid_row_in_view(term->grid, i) != NULL); + } +#endif + + term_damage_view(term); +} + +UNITTEST +{ + const int scrollback_rows = 16; + const int term_rows = 5; + const int cols = 5; + + struct fdm *fdm = fdm_init(); + xassert(fdm != NULL); + + struct terminal term = { + .fdm = fdm, + .rows = term_rows, + .cols = cols, + .normal = { + .rows = xcalloc(scrollback_rows, sizeof(term.normal.rows[0])), + .num_rows = scrollback_rows, + .num_cols = cols, + }, + .grid = &term.normal, + .selection = { + .coords = { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .kind = SELECTION_NONE, + .auto_scroll = { + .fd = -1, + }, + }, + }; + +#define populate_scrollback() do { \ + for (int i = 0; i < scrollback_rows; i++) { \ + if (term.normal.rows[i] == NULL) { \ + struct row *r = xcalloc(1, sizeof(*term.normal.rows[i])); \ + r->cells = xcalloc(cols, sizeof(r->cells[0])); \ + term.normal.rows[i] = r; \ + } \ + } \ + } while (0) + + /* + * Test case 1 - no selection, just verify all rows except those + * on screen have been deleted. + */ + + populate_scrollback(); + term.normal.offset = 11; + term_erase_scrollback(&term); + for (int i = 0; i < scrollback_rows; i++) { + if (i >= term.normal.offset && i < term.normal.offset + term_rows) + xassert(term.normal.rows[i] != NULL); + else + xassert(term.normal.rows[i] == NULL); + } + + /* + * Test case 2 - selection that touches the scrollback. Verify the + * selection is cancelled. + */ + + term.normal.offset = 14; /* Screen covers rows 14,15,0,1,2 */ + + /* Selection covers rows 15,0,1,2,3 */ + term.selection.coords.start = (struct coord){.row = 15}; + term.selection.coords.end = (struct coord){.row = 19}; + term.selection.kind = SELECTION_CHAR_WISE; + + populate_scrollback(); + term_erase_scrollback(&term); + xassert(term.selection.coords.start.row < 0); + xassert(term.selection.coords.end.row < 0); + xassert(term.selection.kind == SELECTION_NONE); + + /* + * Test case 3 - selection that does *not* touch the + * scrollback. Verify the selection is *not* cancelled. + */ + + /* Selection covers rows 15,0 */ + term.selection.coords.start = (struct coord){.row = 15}; + term.selection.coords.end = (struct coord){.row = 16}; + term.selection.kind = SELECTION_CHAR_WISE; + + populate_scrollback(); + term_erase_scrollback(&term); + xassert(term.selection.coords.start.row == 15); + xassert(term.selection.coords.end.row == 16); + xassert(term.selection.kind == SELECTION_CHAR_WISE); + + term.selection.coords.start = (struct coord){-1, -1}; + term.selection.coords.end = (struct coord){-1, -1}; + term.selection.kind = SELECTION_NONE; + + /* + * Test case 4 - sixel that touch the scrollback + */ + + struct sixel six = { + .rows = 5, + .pos = { + .row = 15, + }, + }; + tll_push_back(term.normal.sixel_images, six); + populate_scrollback(); + term_erase_scrollback(&term); + xassert(tll_length(term.normal.sixel_images) == 0); + + /* + * Test case 5 - sixel that does *not* touch the scrollback + */ + six.rows = 3; + tll_push_back(term.normal.sixel_images, six); + populate_scrollback(); + term_erase_scrollback(&term); + xassert(tll_length(term.normal.sixel_images) == 1); + + /* Cleanup */ + tll_free(term.normal.sixel_images); + xassert(term.selection.auto_scroll.fd == -1); + for (int i = 0; i < scrollback_rows; i++) + grid_row_free(term.normal.rows[i]); + free(term.normal.rows); + fdm_destroy(fdm); +} + +int +term_row_rel_to_abs(const struct terminal *term, int row) +{ + switch (term->origin) { + case ORIGIN_ABSOLUTE: + return min(row, term->rows - 1); + + case ORIGIN_RELATIVE: + return min(row + term->scroll_region.start, term->scroll_region.end - 1); + } + + BUG("Invalid cursor_origin value"); + return -1; +} + +void +term_cursor_to(struct terminal *term, int row, int col) +{ + xassert(row < term->rows); + xassert(col < term->cols); + + term->grid->cursor.lcf = false; + + term->grid->cursor.point.col = col; + term->grid->cursor.point.row = row; + + term_reset_grapheme_state(term); + + term->grid->cur_row = grid_row(term->grid, row); +} + +void +term_cursor_home(struct terminal *term) +{ + term_cursor_to(term, term_row_rel_to_abs(term, 0), 0); +} + +void +term_cursor_col(struct terminal *term, int col) +{ + xassert(col < term->cols); + + term->grid->cursor.lcf = false; + term->grid->cursor.point.col = col; + term_reset_grapheme_state(term); +} + +void +term_cursor_left(struct terminal *term, int count) +{ + int move_amount = min(term->grid->cursor.point.col, count); + term->grid->cursor.point.col -= move_amount; + xassert(term->grid->cursor.point.col >= 0); + term->grid->cursor.lcf = false; + term_reset_grapheme_state(term); +} + +void +term_cursor_right(struct terminal *term, int count) +{ + int move_amount = min(term->cols - term->grid->cursor.point.col - 1, count); + term->grid->cursor.point.col += move_amount; + xassert(term->grid->cursor.point.col < term->cols); + term->grid->cursor.lcf = false; + term_reset_grapheme_state(term); +} + +void +term_cursor_up(struct terminal *term, int count) +{ + int top = term->origin == ORIGIN_ABSOLUTE ? 0 : term->scroll_region.start; + xassert(term->grid->cursor.point.row >= top); + + int move_amount = min(term->grid->cursor.point.row - top, count); + term_cursor_to(term, term->grid->cursor.point.row - move_amount, term->grid->cursor.point.col); +} + +void +term_cursor_down(struct terminal *term, int count) +{ + int bottom = term->origin == ORIGIN_ABSOLUTE ? term->rows : term->scroll_region.end; + xassert(bottom >= term->grid->cursor.point.row); + + int move_amount = min(bottom - term->grid->cursor.point.row - 1, count); + term_cursor_to(term, term->grid->cursor.point.row + move_amount, term->grid->cursor.point.col); +} + +static bool +cursor_blink_rearm_timer(struct terminal *term) +{ + if (term->cursor_blink.fd < 0) { + int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd < 0) { + LOG_ERRNO("failed to create cursor blink timer FD"); + return false; + } + + if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_cursor_blink, term)) { + close(fd); + return false; + } + + term->cursor_blink.fd = fd; + } + + const int rate_ms = term->conf->cursor.blink.rate_ms; + const long secs = rate_ms / 1000; + const long nsecs = (rate_ms % 1000) * 1000000; + + const struct itimerspec timer = { + .it_value = {.tv_sec = secs, .tv_nsec = nsecs}, + .it_interval = {.tv_sec = secs, .tv_nsec = nsecs}, + }; + + if (timerfd_settime(term->cursor_blink.fd, 0, &timer, NULL) < 0) { + LOG_ERRNO("failed to arm cursor blink timer"); + fdm_del(term->fdm, term->cursor_blink.fd); + term->cursor_blink.fd = -1; + return false; + } + + return true; +} + +static bool +cursor_blink_disarm_timer(struct terminal *term) +{ + fdm_del(term->fdm, term->cursor_blink.fd); + term->cursor_blink.fd = -1; + return true; +} + +void +term_cursor_blink_update(struct terminal *term) +{ + bool enable = term->cursor_blink.decset || term->cursor_blink.deccsusr; + bool activate = !term->shutdown.in_progress && enable && term->visual_focus; + + LOG_DBG("decset=%d, deccsrusr=%d, focus=%d, shutting-down=%d, enable=%d, activate=%d", + term->cursor_blink.decset, term->cursor_blink.deccsusr, + term->visual_focus, term->shutdown.in_progress, + enable, activate); + + if (activate && term->cursor_blink.fd < 0) { + term->cursor_blink.state = CURSOR_BLINK_ON; + cursor_blink_rearm_timer(term); + } else if (!activate && term->cursor_blink.fd >= 0) + cursor_blink_disarm_timer(term); +} + +static bool +selection_on_top_region(const struct terminal *term, + struct scroll_region region) +{ + return region.start > 0 && + selection_on_rows(term, 0, region.start - 1); +} + +static bool +selection_on_bottom_region(const struct terminal *term, + struct scroll_region region) +{ + return region.end < term->rows && + selection_on_rows(term, region.end, term->rows - 1); +} + +void +term_scroll_partial(struct terminal *term, struct scroll_region region, int rows) +{ + LOG_DBG("scroll: rows=%d, region.start=%d, region.end=%d", + rows, region.start, region.end); + + /* Verify scroll amount has been clamped */ + xassert(rows <= region.end - region.start); + + /* Cancel selections that cannot be scrolled */ + if (unlikely(term->selection.coords.end.row >= 0)) { + /* + * Selection is (partly) inside either the top or bottom + * scrolling regions, or on (at least one) of the lines + * scrolled in (i.e. reused lines). + */ + if (selection_on_top_region(term, region) || + selection_on_bottom_region(term, region)) + { + selection_cancel(term); + } else + selection_scroll_up(term, rows); + } + + sixel_scroll_up(term, rows); + + /* How many lines from the scrollback start is the current viewport? */ + int view_sb_start_distance = grid_row_abs_to_sb( + term->grid, term->rows, term->grid->view); + + bool view_follows = term->grid->view == term->grid->offset; + term->grid->offset += rows; + term->grid->offset &= term->grid->num_rows - 1; + + if (likely(view_follows)) { + term_damage_scroll(term, DAMAGE_SCROLL, region, rows); + selection_view_down(term, term->grid->offset); + term->grid->view = term->grid->offset; + } else if (unlikely(rows > view_sb_start_distance)) { + /* Part of current view is being scrolled out */ + int new_view = grid_row_sb_to_abs(term->grid, term->rows, 0); + selection_view_down(term, new_view); + cmd_scrollback_down(term, rows - view_sb_start_distance); + } + + /* Top non-scrolling region. */ + for (int i = region.start - 1; i >= 0; i--) + grid_swap_row(term->grid, i - rows, i); + + /* Bottom non-scrolling region */ + for (int i = term->rows - 1; i >= region.end; i--) + grid_swap_row(term->grid, i - rows, i); + + /* Erase scrolled in lines */ + for (int r = region.end - rows; r < region.end; r++) { + struct row *row = grid_row_and_alloc(term->grid, r); + erase_line(term, row); + } + + term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); + +#if defined(_DEBUG) + for (int r = 0; r < term->rows; r++) + xassert(grid_row(term->grid, r) != NULL); +#endif +} + +void +term_scroll(struct terminal *term, int rows) +{ + term_scroll_partial(term, term->scroll_region, rows); +} + +void +term_scroll_reverse_partial(struct terminal *term, + struct scroll_region region, int rows) +{ + LOG_DBG("scroll reverse: rows=%d, region.start=%d, region.end=%d", + rows, region.start, region.end); + + /* Verify scroll amount has been clamped */ + xassert(rows <= region.end - region.start); + + /* Cancel selections that cannot be scrolled */ + if (unlikely(term->selection.coords.end.row >= 0)) { + /* + * Selection is (partly) inside either the top or bottom + * scrolling regions, or on (at least one) of the lines + * scrolled in (i.e. reused lines). + */ + if (selection_on_top_region(term, region) || + selection_on_bottom_region(term, region)) + { + selection_cancel(term); + } else + selection_scroll_down(term, rows); + } + + /* Unallocate scrolled out lines */ + for (int r = region.end - rows; r < region.end; r++) { + const int abs_r = grid_row_absolute(term->grid, r); + struct row *row = term->grid->rows[abs_r]; + + grid_row_free(row); + term->grid->rows[abs_r] = NULL; + + if (term->render.last_cursor.row == row) + term->render.last_cursor.row = NULL; + } + + sixel_scroll_down(term, rows); + + const bool view_follows = term->grid->view == term->grid->offset; + term->grid->offset -= rows; + term->grid->offset += term->grid->num_rows; + term->grid->offset &= term->grid->num_rows - 1; + + /* How many lines from the scrollback start is the current viewport? */ + const int view_sb_start_distance = grid_row_abs_to_sb( + term->grid, term->rows, term->grid->view); + const int offset_sb_start_distance = grid_row_abs_to_sb( + term->grid, term->rows, term->grid->offset); + + xassert(term->grid->offset >= 0); + xassert(term->grid->offset < term->grid->num_rows); + + if (view_follows) { + term_damage_scroll(term, DAMAGE_SCROLL_REVERSE, region, rows); + selection_view_up(term, term->grid->offset); + term->grid->view = term->grid->offset; + } else if (unlikely(view_sb_start_distance > offset_sb_start_distance)) { + /* Part of current view is being scrolled out */ + int new_view = term->grid->offset; + selection_view_up(term, new_view); + term->grid->view = new_view; + } + + /* Bottom non-scrolling region */ + for (int i = region.end + rows; i < term->rows + rows; i++) + grid_swap_row(term->grid, i, i - rows); + + /* Top non-scrolling region */ + for (int i = 0 + rows; i < region.start + rows; i++) + grid_swap_row(term->grid, i, i - rows); + + /* Erase scrolled in lines */ + for (int r = region.start; r < region.start + rows; r++) { + struct row *row = grid_row_and_alloc(term->grid, r); + erase_line(term, row); + } + + if (unlikely(view_sb_start_distance > offset_sb_start_distance)) + term_damage_view(term); + + term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); + +#if defined(_DEBUG) + for (int r = 0; r < term->rows; r++) + xassert(grid_row(term->grid, r) != NULL); + for (int r = 0; r < term->rows; r++) + xassert(grid_row_in_view(term->grid, r) != NULL); +#endif +} + +void +term_scroll_reverse(struct terminal *term, int rows) +{ + term_scroll_reverse_partial(term, term->scroll_region, rows); +} + +void +term_carriage_return(struct terminal *term) +{ + term_cursor_left(term, term->grid->cursor.point.col); +} + +void +term_linefeed(struct terminal *term) +{ + term->grid->cursor.lcf = false; + + if (term->grid->cursor.point.row == term->scroll_region.end - 1) + term_scroll(term, 1); + else + term_cursor_down(term, 1); + + term_reset_grapheme_state(term); +} + +void +term_reverse_index(struct terminal *term) +{ + if (term->grid->cursor.point.row == term->scroll_region.start) + term_scroll_reverse(term, 1); + else + term_cursor_up(term, 1); +} + +void +term_reset_view(struct terminal *term) +{ + if (term->grid->view == term->grid->offset) + return; + + term->grid->view = term->grid->offset; + term_damage_view(term); +} + +void +term_save_cursor(struct terminal *term) +{ + term->grid->saved_cursor = term->grid->cursor; + term->vt.saved_attrs = term->vt.attrs; + term->saved_charsets = term->charsets; +} + +void +term_restore_cursor(struct terminal *term, const struct cursor *cursor) +{ + int row = min(cursor->point.row, term->rows - 1); + int col = min(cursor->point.col, term->cols - 1); + + term_cursor_to(term, row, col); + term->grid->cursor.lcf = cursor->lcf; + + term->vt.attrs = term->vt.saved_attrs; + term->charsets = term->saved_charsets; + + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); +} + +void +term_visual_focus_in(struct terminal *term) +{ + if (term->visual_focus) + return; + + term->visual_focus = true; + term_cursor_blink_update(term); + render_refresh_csd(term); +} + +void +term_visual_focus_out(struct terminal *term) +{ + if (!term->visual_focus) + return; + + term->visual_focus = false; + term_cursor_blink_update(term); + render_refresh_csd(term); +} + +void +term_kbd_focus_in(struct terminal *term) +{ + if (term->kbd_focus) + return; + + term->kbd_focus = true; + + if (term->render.urgency) { + term->render.urgency = false; + term_damage_margins(term); + } + + cursor_refresh(term); + + if (term->focus_events) + term_to_slave(term, "\033[I", 3); +} + +void +term_kbd_focus_out(struct terminal *term) +{ + if (!term->kbd_focus) + return; + + tll_foreach(term->wl->seats, it) + if (it->item.kbd_focus == term) + return; + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (term_ime_reset(term)) + render_refresh(term); +#endif + + term->kbd_focus = false; + cursor_refresh(term); + + if (term->focus_events) + term_to_slave(term, "\033[O", 3); +} + +static int +linux_mouse_button_to_x(int button) +{ + /* Note: on X11, scroll events where reported as buttons. Not so + * on Wayland. We manually map scroll events to custom "button" + * defines (BTN_WHEEL_*). + */ + switch (button) { + case BTN_LEFT: return 1; + case BTN_MIDDLE: return 2; + case BTN_RIGHT: return 3; + case BTN_WHEEL_BACK: return 4; /* Foot custom define */ + case BTN_WHEEL_FORWARD: return 5; /* Foot custom define */ + case BTN_WHEEL_LEFT: return 6; /* Foot custom define */ + case BTN_WHEEL_RIGHT: return 7; /* Foot custom define */ + case BTN_SIDE: return 8; + case BTN_EXTRA: return 9; + case BTN_FORWARD: return 10; + case BTN_BACK: return 11; + case BTN_TASK: return 12; /* Guessing... */ + + default: + LOG_WARN("unrecognized mouse button: %d (0x%x)", button, button); + return -1; + } +} + +static int +encode_xbutton(int xbutton) +{ + switch (xbutton) { + case 1: case 2: case 3: + return xbutton - 1; + + case 4: case 5: case 6: case 7: + /* Like button 1 and 2, but with 64 added */ + return xbutton - 4 + 64; + + case 8: case 9: case 10: case 11: + /* Similar to 4 and 5, but adding 128 instead of 64 */ + return xbutton - 8 + 128; + + default: + LOG_ERR("cannot encode X mouse button: %d", xbutton); + return -1; + } +} + +static void +report_mouse_click(struct terminal *term, int encoded_button, int row, int col, + int row_pixels, int col_pixels, bool release) +{ + char response[128]; + + switch (term->mouse_reporting) { + case MOUSE_NORMAL: { + int encoded_col = 32 + col + 1; + int encoded_row = 32 + row + 1; + if (encoded_col > 255 || encoded_row > 255) + return; + + snprintf(response, sizeof(response), "\033[M%c%c%c", + 32 + (release ? 3 : encoded_button), encoded_col, encoded_row); + break; + } + + case MOUSE_SGR: + snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", + encoded_button, col + 1, row + 1, release ? 'm' : 'M'); + break; + + case MOUSE_SGR_PIXELS: { + const int bounded_col = max(col_pixels, 0); + const int bounded_row = max(row_pixels, 0); + snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", + encoded_button, bounded_col + 1, bounded_row + 1, release ? 'm' : 'M'); + break; + } + + case MOUSE_URXVT: + snprintf(response, sizeof(response), "\033[%d;%d;%dM", + 32 + (release ? 3 : encoded_button), col + 1, row + 1); + break; + + case MOUSE_UTF8: + /* Unimplemented */ + return; + } + + term_to_slave(term, response, strlen(response)); +} + +static void +report_mouse_motion(struct terminal *term, int encoded_button, int row, int col, int row_pixels, int col_pixels) +{ + report_mouse_click(term, encoded_button, row, col, row_pixels, col_pixels, false); +} + +bool +term_mouse_grabbed(const struct terminal *term, const struct seat *seat) +{ + /* + * Mouse is grabbed by us, regardless of whether mouse tracking + * has been enabled or not. + */ + + xkb_mod_mask_t mods; + get_current_modifiers(seat, &mods, NULL, 0, true); + + const struct key_binding_set *bindings = + key_binding_for(term->wl->key_binding_manager, term->conf, seat); + const xkb_mod_mask_t override_modmask = bindings->selection_overrides; + bool override_mods_pressed = (mods & override_modmask) == override_modmask; + + return term->mouse_tracking == MOUSE_NONE || + (seat->kbd_focus == term && override_mods_pressed); +} + +void +term_mouse_down(struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, + bool _shift, bool _alt, bool _ctrl) +{ + /* Map libevent button event code to X button number */ + int xbutton = linux_mouse_button_to_x(button); + if (xbutton == -1) + return; + + int encoded = encode_xbutton(xbutton); + if (encoded == -1) + return; + + + bool has_focus = term->kbd_focus; + bool shift = has_focus ? _shift : false; + bool alt = has_focus ? _alt : false; + bool ctrl = has_focus ? _ctrl : false; + + encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); + + switch (term->mouse_tracking) { + case MOUSE_NONE: + break; + + case MOUSE_CLICK: + case MOUSE_DRAG: + case MOUSE_MOTION: + report_mouse_click(term, encoded, row, col, row_pixels, col_pixels, false); + break; + + case MOUSE_X10: + /* Never enabled */ + BUG("X10 mouse mode not implemented"); + break; + } +} + +void +term_mouse_up(struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, + bool _shift, bool _alt, bool _ctrl) +{ + /* Map libevent button event code to X button number */ + int xbutton = linux_mouse_button_to_x(button); + if (xbutton == -1) + return; + + if (xbutton == 4 || xbutton == 5) { + /* No release events for vertical scroll wheel buttons */ + return; + } + + int encoded = encode_xbutton(xbutton); + if (encoded == -1) + return; + + bool has_focus = term->kbd_focus; + bool shift = has_focus ? _shift : false; + bool alt = has_focus ? _alt : false; + bool ctrl = has_focus ? _ctrl : false; + + encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); + + switch (term->mouse_tracking) { + case MOUSE_NONE: + break; + + case MOUSE_CLICK: + case MOUSE_DRAG: + case MOUSE_MOTION: + report_mouse_click(term, encoded, row, col, row_pixels, col_pixels, true); + break; + + case MOUSE_X10: + /* Never enabled */ + BUG("X10 mouse mode not implemented"); + break; + } +} + +void +term_mouse_motion(struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, + bool _shift, bool _alt, bool _ctrl) +{ + int encoded = 0; + + if (button != 0) { + /* Map libevent button event code to X button number */ + int xbutton = linux_mouse_button_to_x(button); + if (xbutton == -1) + return; + + encoded = encode_xbutton(xbutton); + if (encoded == -1) + return; + } else + encoded = 3; /* "released" */ + + bool has_focus = term->kbd_focus; + bool shift = has_focus ? _shift : false; + bool alt = has_focus ? _alt : false; + bool ctrl = has_focus ? _ctrl : false; + + encoded += 32; /* Motion event */ + encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); + + switch (term->mouse_tracking) { + case MOUSE_NONE: + case MOUSE_CLICK: + return; + + case MOUSE_DRAG: + if (button == 0) + return; + /* FALLTHROUGH */ + + case MOUSE_MOTION: + report_mouse_motion(term, encoded, row, col, row_pixels, col_pixels); + break; + + case MOUSE_X10: + /* Never enabled */ + BUG("X10 mouse mode not implemented"); + break; + } +} + +void +term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) +{ + enum cursor_shape shape = CURSOR_SHAPE_NONE; + + switch (term->active_surface) { + case TERM_SURF_GRID: + if (seat->pointer.hidden) + shape = CURSOR_SHAPE_HIDDEN; + + else if (cursor_string_to_server_shape( + term->mouse_user_cursor, + term->wl->shape_manager_version) != 0 || + render_xcursor_is_valid(seat, term->mouse_user_cursor)) + { + shape = CURSOR_SHAPE_CUSTOM; + } + + else if (term_mouse_grabbed(term, seat)) { + shape = CURSOR_SHAPE_TEXT; + } + + else + shape = CURSOR_SHAPE_LEFT_PTR; + break; + + case TERM_SURF_TITLE: + case TERM_SURF_BUTTON_MINIMIZE: + case TERM_SURF_BUTTON_MAXIMIZE: + case TERM_SURF_BUTTON_CLOSE: + shape = CURSOR_SHAPE_LEFT_PTR; + break; + + case TERM_SURF_TAB_BAR: { + size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y); + shape = (idx != SIZE_MAX) ? CURSOR_SHAPE_POINTER : CURSOR_SHAPE_LEFT_PTR; + break; + } + + case TERM_SURF_TAB_OVERVIEW: + shape = CURSOR_SHAPE_POINTER; + break; + + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + shape = xcursor_for_csd_border(term, seat->mouse.x, seat->mouse.y); + break; + + case TERM_SURF_NONE: + return; + } + + if (shape == CURSOR_SHAPE_NONE) + BUG("xcursor not set"); + + render_xcursor_set(seat, term, shape); +} + +void +term_xcursor_update(struct terminal *term) +{ + tll_foreach(term->wl->seats, it) + term_xcursor_update_for_seat(term, &it->item); +} + +void +term_set_window_title(struct terminal *term, const char *title) +{ + if (term->conf->locked_title && term->window_title_has_been_set) + return; + + if (term->window_title != NULL && streq(term->window_title, title)) + return; + + if (!is_valid_utf8_and_printable(title)) { + /* It's an xdg_toplevel::set_title() protocol violation to set + a title with an invalid UTF-8 sequence */ + LOG_WARN("%s: title is not valid UTF-8, ignoring", title); + return; + } + + free(term->window_title); + term->window_title = xstrdup(title); + render_refresh_title(term); + render_refresh_tab_bar(term); + term->window_title_has_been_set = true; +} + +void +term_set_app_id(struct terminal *term, const char *app_id) +{ + if (app_id != NULL && *app_id == '\0') + app_id = NULL; + + if (term->app_id == NULL && app_id == NULL) + return; + + if (term->app_id != NULL && app_id != NULL && streq(term->app_id, app_id)) + return; + + if (app_id != NULL && !is_valid_utf8_and_printable(app_id)) { + LOG_WARN("%s: app-id is not valid UTF-8, ignoring", app_id); + return; + } + + free(term->app_id); + if (app_id != NULL) { + term->app_id = xstrdup(app_id); + } else { + term->app_id = NULL; + } + + const size_t length = app_id != NULL ? strlen(app_id) : 0; + if (length > 2048) { + /* + * Not sure if there's a limit in the protocol, or the + * libwayland implementation, or e.g. wlroots, but too long + * app-id's (not e.g. title) causes at least river and sway to + * peg the CPU at 100%, and stop sending e.g. frame callbacks. + * + */ + term->app_id[2048] = '\0'; + } + + render_refresh_app_id(term); + render_refresh_icon(term); +} + +const char * +term_icon(const struct terminal *term) +{ + const char *app_id = + term->app_id != NULL ? term->app_id : term->conf->app_id; + + return +#if 0 +term->window_icon != NULL + ? term->window_icon + : + #endif + streq(app_id, "footclient") + ? "foot" + : app_id; +} + +void +term_flash(struct terminal *term, unsigned duration_ms) +{ + LOG_DBG("FLASH for %ums", duration_ms); + + struct itimerspec alarm = { + .it_value = {.tv_sec = 0, .tv_nsec = duration_ms * 1000000}, + }; + + if (timerfd_settime(term->flash.fd, 0, &alarm, NULL) < 0) + LOG_ERRNO("failed to arm flash timer"); + else { + term->flash.active = true; + } +} + +void +term_bell(struct terminal *term) +{ + + if (!term->bell_action_enabled) + return; + + if (term->conf->bell.urgent && !term->kbd_focus) { + if (!wayl_win_set_urgent(term->window)) { + /* + * Urgency (xdg-activation) is relatively new in + * Wayland. Fallback to our old, "faked", urgency - + * rendering our window margins in red + */ + term->render.urgency = true; + term_damage_margins(term); + } + } + + if (term->conf->bell.system_bell) + wayl_win_ring_bell(term->window); + + if (term->conf->bell.notify) { + notify_notify(term, &(struct notification){ + .title = xstrdup("Bell"), + .body = xstrdup("Bell in terminal"), + .expire_time = -1, + .focus = true, + }); + } + + if (term->conf->bell.flash) + term_flash(term, 100); + + if ((term->conf->bell.command.argv.args != NULL) && + (!term->kbd_focus || term->conf->bell.command_focused)) + { + int devnull = open("/dev/null", O_RDONLY); + spawn(term->reaper, NULL, term->conf->bell.command.argv.args, + devnull, -1, -1, NULL, NULL, NULL); + + if (devnull >= 0) + close(devnull); + } +} + +bool +term_spawn_new(const struct terminal *term) +{ + char *argv[4]; + int argc = 0; + + argv[argc++] = term->foot_exe; + if (term->conf->conf_path != NULL) { + argv[argc++] = "--config"; + argv[argc++] = term->conf->conf_path; + } + argv[argc] = NULL; + + return spawn( + term->reaper, term->cwd, argv, + -1, -1, -1, NULL, NULL, NULL) >= 0; +} + +void +term_enable_app_sync_updates(struct terminal *term) +{ + term->render.app_sync_updates.enabled = true; + + if (timerfd_settime( + term->render.app_sync_updates.timer_fd, 0, + &(struct itimerspec){.it_value = {.tv_sec = 1}}, NULL) < 0) + { + LOG_ERR("failed to arm timer for application synchronized updates"); + } + + /* Disable pending refresh *iff* the grid is the *only* thing + * scheduled to be re-rendered */ + if (!term->render.refresh.csd && !term->render.refresh.search && + !term->render.pending.csd && !term->render.pending.search) + { + term->render.refresh.grid = false; + term->render.pending.grid = false; + } + + /* Disarm delayed rendering timers */ + timerfd_settime( + term->delayed_render_timer.lower_fd, 0, + &(struct itimerspec){{0}}, NULL); + timerfd_settime( + term->delayed_render_timer.upper_fd, 0, + &(struct itimerspec){{0}}, NULL); + term->delayed_render_timer.is_armed = false; +} + +void +term_disable_app_sync_updates(struct terminal *term) +{ + if (!term->render.app_sync_updates.enabled) + return; + + term->render.app_sync_updates.enabled = false; + render_refresh(term); + + /* Reset timers */ + timerfd_settime( + term->render.app_sync_updates.timer_fd, 0, + &(struct itimerspec){{0}}, NULL); +} + +static inline void +print_linewrap(struct terminal *term) +{ + if (likely(!term->grid->cursor.lcf)) { + /* Not and end of line */ + return; + } + + if (unlikely(!term->auto_margin)) { + /* Auto-wrap disabled */ + return; + } + + term->grid->cur_row->linebreak = false; + term->grid->cursor.lcf = false; + + const int row = term->grid->cursor.point.row; + + if (row == term->scroll_region.end - 1) + term_scroll(term, 1); + else { + const int new_row = min(row + 1, term->rows - 1); + term->grid->cursor.point.row = new_row; + term->grid->cur_row = grid_row(term->grid, new_row); + } + + term->grid->cursor.point.col = 0; +} + +static inline void +print_insert(struct terminal *term, int width) +{ + if (likely(!term->insert_mode)) + return; + + xassert(width > 0); + + struct row *row = term->grid->cur_row; + const size_t move_count = max(0, term->cols - term->grid->cursor.point.col - width); + + memmove( + &row->cells[term->grid->cursor.point.col + width], + &row->cells[term->grid->cursor.point.col], + move_count * sizeof(struct cell)); + + /* Mark moved cells as dirty */ + for (size_t i = term->grid->cursor.point.col + width; i < term->cols; i++) + row->cells[i].attrs.clean = 0; +} + +static void +print_spacer(struct terminal *term, int col, int remaining) +{ + struct grid *grid = term->grid; + struct row *row = grid->cur_row; + struct cell *cell = &row->cells[col]; + + cell->wc = CELL_SPACER + remaining; + cell->attrs = (struct attributes){0}; +} + +/* + * Puts a character on the grid. Coordinates are in screen coordinates + * (i.e. ‘cursor’ coordinates). + * + * Does NOT: + * - update the cursor + * - linewrap + * - erase sixels + * + * Limitations: + * - double width characters not supported + */ +void +term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, + bool use_sgr_attrs) +{ + struct row *row = grid_row(term->grid, r); + row->dirty = true; + + xassert(c + count <= term->cols); + + struct attributes attrs = use_sgr_attrs + ? term->vt.attrs + : (struct attributes){0}; + + const struct cell *last = &row->cells[c + count]; + for (struct cell *cell = &row->cells[c]; cell < last; cell++) { + cell->wc = data; + cell->attrs = attrs; + + /* TODO: why do we print the URI here, and then erase it below? */ + if (unlikely(use_sgr_attrs && term->vt.osc8.uri != NULL)) { + grid_row_uri_range_put(row, c, term->vt.osc8.uri, term->vt.osc8.id); + + switch (term->conf->url.osc8_underline) { + case OSC8_UNDERLINE_ALWAYS: + cell->attrs.url = true; + break; + + case OSC8_UNDERLINE_URL_MODE: + break; + } + } + + if (unlikely(use_sgr_attrs && + (term->vt.underline.style > UNDERLINE_SINGLE || + term->vt.underline.color_src != COLOR_DEFAULT))) + { + grid_row_underline_range_put(row, c, term->vt.underline); + } + } + + if (unlikely(row->extra != NULL)) { + if (likely(term->vt.osc8.uri != NULL)) + grid_row_uri_range_erase(row, c, c + count - 1); + + if (likely(term->vt.underline.style <= UNDERLINE_SINGLE && + term->vt.underline.color_src == COLOR_DEFAULT)) + { + /* No extended/styled underlines active, so erase any such + attributes at the target columns */ + grid_row_underline_range_erase(row, c, c + count - 1); + } + } +} + +void +term_print(struct terminal *term, char32_t wc, int width, bool insert_mode_disable) +{ + xassert(width > 0); + + struct grid *grid = term->grid; + + if (unlikely(term->charsets.set[term->charsets.selected] == CHARSET_GRAPHIC) && + wc >= 0x60 && wc <= 0x7e) + { + /* 0x60 - 0x7e */ + static const char32_t vt100_0[] = { + U'◆', U'▒', U'␉', U'␌', U'␍', U'␊', U'°', U'±', /* ` - g */ + U'␤', U'␋', U'┘', U'┐', U'┌', U'└', U'┼', U'⎺', /* h - o */ + U'⎻', U'─', U'⎼', U'⎽', U'├', U'┤', U'┴', U'┬', /* p - w */ + U'│', U'≤', U'≥', U'π', U'≠', U'£', U'·', /* x - ~ */ + }; + + xassert(width == 1); + wc = vt100_0[wc - 0x60]; + } + + print_linewrap(term); + if (!insert_mode_disable) + print_insert(term, width); + + int col = grid->cursor.point.col; + + if (unlikely(width > 1) && likely(term->auto_margin) && + col + width > term->cols) + { + /* Multi-column character that doesn't fit on current line - + * pad with spacers */ + for (size_t i = col; i < term->cols; i++) + print_spacer(term, i, 0); + + /* And force a line-wrap */ + grid->cursor.lcf = 1; + print_linewrap(term); + col = 0; + } + + sixel_overwrite_at_cursor(term, width); + + /* *Must* get current cell *after* linewrap+insert */ + struct row *row = grid->cur_row; + row->dirty = true; + row->linebreak = true; + + struct cell *cell = &row->cells[col]; + cell->wc = term->vt.last_printed = wc; + cell->attrs = term->vt.attrs; + + if (unlikely(term->vt.osc8.uri != NULL)) { + for (int i = 0; i < width && (col + i) < term->cols; i++) { + grid_row_uri_range_put( + row, col + i, term->vt.osc8.uri, term->vt.osc8.id); + } + + switch (term->conf->url.osc8_underline) { + case OSC8_UNDERLINE_ALWAYS: + cell->attrs.url = true; + break; + + case OSC8_UNDERLINE_URL_MODE: + break; + } + } else if (row->extra != NULL) + grid_row_uri_range_erase(row, col, col + width - 1); + + if (unlikely(term->vt.underline.style > UNDERLINE_SINGLE || + term->vt.underline.color_src != COLOR_DEFAULT)) + { + grid_row_underline_range_put(row, col, term->vt.underline); + } else if (row->extra != NULL) + grid_row_underline_range_erase(row, col, col + width - 1); + + /* Advance cursor the 'additional' columns while dirty:ing the cells */ + for (int i = 1; i < width && (col + 1) < term->cols; i++) { + col++; + print_spacer(term, col, width - i); + } + + xassert(col < term->cols); + + /* Advance cursor */ + if (unlikely(++col >= term->cols)) { + grid->cursor.lcf = true; + col--; + } else + xassert(!grid->cursor.lcf); + + grid->cursor.point.col = col; +} + +static void +ascii_printer_generic(struct terminal *term, char32_t wc) +{ + term_print(term, wc, 1, false); +} + +static void +ascii_printer_fast(struct terminal *term, char32_t wc) +{ + struct grid *grid = term->grid; + + xassert(term->charsets.set[term->charsets.selected] == CHARSET_ASCII); + xassert(!term->insert_mode); + xassert(tll_length(grid->sixel_images) == 0); + + print_linewrap(term); + + /* *Must* get current cell *after* linewrap+insert */ + int col = grid->cursor.point.col; + const int uri_start = col; + + struct row *row = grid->cur_row; + row->dirty = true; + row->linebreak = true; + + struct cell *cell = &row->cells[col]; + cell->wc = term->vt.last_printed = wc; + cell->attrs = term->vt.attrs; + + /* Advance cursor */ + if (unlikely(++col >= term->cols)) { + xassert(col == term->cols); + grid->cursor.lcf = true; + col--; + } else + xassert(!grid->cursor.lcf); + + grid->cursor.point.col = col; + + if (unlikely(row->extra != NULL)) { + grid_row_uri_range_erase(row, uri_start, uri_start); + grid_row_underline_range_erase(row, uri_start, uri_start); + } +} + +static void +ascii_printer_single_shift(struct terminal *term, char32_t wc) +{ + ascii_printer_generic(term, wc); + term->charsets.selected = term->charsets.saved; + + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); +} + +void +term_update_ascii_printer(struct terminal *term) +{ + _Static_assert(sizeof(term->bits_affecting_ascii_printer) == sizeof(uint8_t), "bad size"); + + void (*new_printer)(struct terminal *term, char32_t wc) = + unlikely(term->bits_affecting_ascii_printer.value != 0) + ? &ascii_printer_generic + : &ascii_printer_fast; + +#if defined(_DEBUG) && LOG_ENABLE_DBG + if (term->ascii_printer != new_printer) { + LOG_DBG("switching ASCII printer %s -> %s", + term->ascii_printer == &ascii_printer_fast ? "fast" : "generic", + new_printer == &ascii_printer_fast ? "fast" : "generic"); + } +#endif + + term->ascii_printer = new_printer; +} + +void +term_single_shift(struct terminal *term, enum charset_designator idx) +{ + term->charsets.saved = term->charsets.selected; + term->charsets.selected = idx; + term->ascii_printer = &ascii_printer_single_shift; +} + +#if defined(FOOT_GRAPHEME_CLUSTERING) +static int +emoji_vs_compare(const void *_key, const void *_entry) +{ + const struct emoji_vs *key = _key; + const struct emoji_vs *entry = _entry; + + uint32_t cp = key->start; + + if (cp < entry->start) + return -1; + else if (cp > entry->end) + return 1; + else + return 0; +} + +UNITTEST +{ + /* Verify the emoji_vs list is sorted */ + int64_t last_end = -1; + + for (size_t i = 0; i < sizeof(emoji_vs) / sizeof(emoji_vs[0]); i++) { + const struct emoji_vs *vs = &emoji_vs[i]; + xassert(vs->start <= vs->end); + xassert(vs->start > last_end); + xassert(vs->vs15 || vs->vs16); + last_end = vs->end; + } +} +#endif + +void +term_process_and_print_non_ascii(struct terminal *term, char32_t wc) +{ + int width = c32width(wc); + bool insert_mode_disable = false; + const bool grapheme_clustering = term->grapheme_shaping; + +#if !defined(FOOT_GRAPHEME_CLUSTERING) + xassert(!grapheme_clustering); +#endif + + if (term->grid->cursor.point.col > 0 && + (grapheme_clustering || + (!grapheme_clustering && width == 0 && wc >= 0x300))) + { + int col = term->grid->cursor.point.col; + if (!term->grid->cursor.lcf) + col--; + + /* Skip past spacers */ + struct row *row = term->grid->cur_row; + while (row->cells[col].wc >= CELL_SPACER && col > 0) + col--; + + xassert(col >= 0 && col < term->cols); + char32_t base = row->cells[col].wc; + char32_t UNUSED last = base; + + /* Is base cell already a cluster? */ + const struct composed *composed = + (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) + ? composed_lookup(term->composed, base - CELL_COMB_CHARS_LO) + : NULL; + + uint32_t key; + + if (composed != NULL) { + base = composed->chars[0]; + last = composed->chars[composed->count - 1]; + key = composed_key_from_key(composed->key, wc); + } else + key = composed_key_from_key(base, wc); + +#if defined(FOOT_GRAPHEME_CLUSTERING) + if (grapheme_clustering) { + /* Check if we're on a grapheme cluster break */ + if (utf8proc_grapheme_break_stateful( + last, wc, &term->vt.grapheme_state)) + { + term_reset_grapheme_state(term); + goto out; + } + } +#endif + + int base_width = c32width(base); + if (base_width > 0) { + term->grid->cursor.point.col = col; + term->grid->cursor.lcf = false; + insert_mode_disable = true; + + if (composed == NULL) { + bool base_from_primary; + bool comb_from_primary; + bool pre_from_primary; + + char32_t precomposed = term->fonts[0] != NULL + ? fcft_precompose( + term->fonts[0], base, wc, &base_from_primary, + &comb_from_primary, &pre_from_primary) + : (char32_t)-1; + + int precomposed_width = c32width(precomposed); + + /* + * Only use the pre-composed character if: + * + * 1. we *have* a pre-composed character + * 2. the width matches the base characters width + * 3. it's in the primary font, OR one of the base or + * combining characters are *not* from the primary + * font + */ + + if (precomposed != (char32_t)-1 && + precomposed_width == base_width && + (pre_from_primary || + !base_from_primary || + !comb_from_primary)) + { + wc = precomposed; + width = precomposed_width; + term_reset_grapheme_state(term); + goto out; + } + } + + size_t wanted_count = composed != NULL ? composed->count + 1 : 2; + if (wanted_count > 255) { + xassert(composed != NULL); + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + LOG_WARN("combining character overflow:"); + LOG_WARN(" base: 0x%04x", composed->chars[0]); + for (size_t i = 1; i < composed->count; i++) + LOG_WARN(" cc: 0x%04x", composed->chars[i]); + LOG_ERR(" new: 0x%04x", wc); +#endif + /* This is going to break anyway... */ + wanted_count--; + } + + xassert(wanted_count <= 255); + + /* Check if we already have a match for the entire compose chain */ + const struct composed *cc = + composed_lookup_without_collision( + term->composed, &key, + composed != NULL ? composed->chars : &(char32_t){base}, + composed != NULL ? composed->count : 1, + wc, 0); + + if (cc != NULL) { + /* We *do* have a match! */ + wc = CELL_COMB_CHARS_LO + cc->key; + width = cc->width; + goto out; + } else { + /* No match - allocate a new chain below */ + } + + if (unlikely(term->composed_count >= + (CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO))) + { + /* We reached our maximum number of allowed composed + * character chains. Fall through here and print the + * current zero-width character to the current cell */ + LOG_WARN("maximum number of composed characters reached"); + term_reset_grapheme_state(term); + goto out; + } + + /* Allocate new chain */ + struct composed *new_cc = xmalloc(sizeof(*new_cc)); + new_cc->chars = xmalloc(wanted_count * sizeof(new_cc->chars[0])); + new_cc->key = key; + new_cc->count = wanted_count; + new_cc->chars[0] = base; + new_cc->chars[wanted_count - 1] = wc; + new_cc->forced_width = composed != NULL ? composed->forced_width : 0; + + if (composed != NULL) { + memcpy(&new_cc->chars[1], &composed->chars[1], + (wanted_count - 2) * sizeof(new_cc->chars[0])); + } + + const int grapheme_width = + composed != NULL ? composed->width : base_width; + + switch (term->conf->tweak.grapheme_width_method) { + case GRAPHEME_WIDTH_MAX: + new_cc->width = max(grapheme_width, width); + break; + + case GRAPHEME_WIDTH_DOUBLE: + new_cc->width = min(grapheme_width + width, 2); + +#if defined(FOOT_GRAPHEME_CLUSTERING) + /* Handle VS-15 and VS-16 variation selectors */ + if (unlikely(grapheme_clustering && + (wc == 0xfe0e || wc == 0xfe0f) && + new_cc->count == 2)) + { + const struct emoji_vs *vs = + bsearch( + &(struct emoji_vs){.start = new_cc->chars[0]}, + emoji_vs, sizeof(emoji_vs) / sizeof(emoji_vs[0]), + sizeof(struct emoji_vs), + &emoji_vs_compare); + + if (vs != NULL) { + xassert(new_cc->chars[0] >= vs->start && + new_cc->chars[0] <= vs->end); + + /* Force a grapheme width of 1 for VS-15, and 2 for VS-16 */ + if (wc == 0xfe0e) { + if (vs->vs15) + new_cc->width = 1; + } else if (wc == 0xfe0f) { + if (vs->vs16) + new_cc->width = 2; + } + } + } +#endif + + break; + + case GRAPHEME_WIDTH_WCSWIDTH: + new_cc->width = grapheme_width + width; + break; + } + + term->composed_count++; + composed_insert(&term->composed, new_cc); + + wc = CELL_COMB_CHARS_LO + new_cc->key; + width = new_cc->forced_width > 0 ? new_cc->forced_width : new_cc->width; + + xassert(wc >= CELL_COMB_CHARS_LO); + xassert(wc <= CELL_COMB_CHARS_HI); + goto out; + } + } else + term_reset_grapheme_state(term); + + +out: + if (width > 0) + term_print(term, wc, width, insert_mode_disable); +} + +enum term_surface +term_surface_kind(const struct terminal *term, const struct wl_surface *surface) +{ + if (likely(surface == term->window->surface.surf)) + return TERM_SURF_GRID; + else if (surface == term->window->csd.surface[CSD_SURF_TITLE].surface.surf) + return TERM_SURF_TITLE; + else if (surface == term->window->csd.surface[CSD_SURF_LEFT].surface.surf) + return TERM_SURF_BORDER_LEFT; + else if (surface == term->window->csd.surface[CSD_SURF_RIGHT].surface.surf) + return TERM_SURF_BORDER_RIGHT; + else if (surface == term->window->csd.surface[CSD_SURF_TOP].surface.surf) + return TERM_SURF_BORDER_TOP; + else if (surface == term->window->csd.surface[CSD_SURF_BOTTOM].surface.surf) + return TERM_SURF_BORDER_BOTTOM; + else if (surface == term->window->csd.surface[CSD_SURF_MINIMIZE].surface.surf) + return TERM_SURF_BUTTON_MINIMIZE; + else if (surface == term->window->csd.surface[CSD_SURF_MAXIMIZE].surface.surf) + return TERM_SURF_BUTTON_MAXIMIZE; + else if (surface == term->window->csd.surface[CSD_SURF_CLOSE].surface.surf) + return TERM_SURF_BUTTON_CLOSE; + else if (surface == term->window->tab_bar.surface.surf) + return TERM_SURF_TAB_BAR; + else if (surface == term->window->tab_overview.surface.surf) + return TERM_SURF_TAB_OVERVIEW; + else + return TERM_SURF_NONE; +} + +static bool +rows_to_text(const struct terminal *term, int start, int end, + int col_start, int col_end, char **text, size_t *len) +{ + struct extraction_context *ctx = extract_begin(SELECTION_NONE, true); + if (ctx == NULL) + return false; + + const int grid_rows = term->grid->num_rows; + int r = start; + + while (true) { + const struct row *row = term->grid->rows[r]; + xassert(row != NULL); + + const int c_end = r == end ? col_end : term->cols; + + for (int c = col_start; c < c_end; c++) { + if (!extract_one(term, row, &row->cells[c], c, ctx)) + goto out; + } + + if (r == end) + break; + + r++; + r &= grid_rows - 1; + + col_start = 0; + } + +out: + return extract_finish(ctx, text, len); +} + +bool +term_scrollback_to_text(const struct terminal *term, char **text, size_t *len) +{ + const int grid_rows = term->grid->num_rows; + int start = (term->grid->offset + term->rows) & (grid_rows - 1); + int end = (term->grid->offset + term->rows - 1) & (grid_rows - 1); + + xassert(start >= 0); + xassert(start < grid_rows); + xassert(end >= 0); + xassert(end < grid_rows); + + /* If scrollback isn't full yet, this may be NULL, so scan forward + * until we find the first non-NULL row */ + while (term->grid->rows[start] == NULL) { + start++; + start &= grid_rows - 1; + } + + while (term->grid->rows[end] == NULL) { + end--; + if (end < 0) + end += term->grid->num_rows; + } + + return rows_to_text(term, start, end, 0, term->cols, text, len); +} + +bool +term_view_to_text(const struct terminal *term, char **text, size_t *len) +{ + int start = grid_row_absolute_in_view(term->grid, 0); + int end = grid_row_absolute_in_view(term->grid, term->rows - 1); + return rows_to_text(term, start, end, 0, term->cols, text, len); +} + +bool +term_command_output_to_text(const struct terminal *term, char **text, size_t *len) +{ + int start_row = -1; + int end_row = -1; + int start_col = -1; + int end_col = -1; + + const struct grid *grid = term->grid; + const int sb_end = grid_row_absolute(grid, term->rows - 1); + const int sb_start = (sb_end + 1) & (grid->num_rows - 1); + int r = sb_end; + + while (start_row < 0) { + const struct row *row = grid->rows[r]; + if (row == NULL) + break; + + if (row->shell_integration.cmd_end >= 0) { + end_row = r; + end_col = row->shell_integration.cmd_end; + } + + if (end_row >= 0 && row->shell_integration.cmd_start >= 0) { + start_row = r; + start_col = row->shell_integration.cmd_start; + } + + if (r == sb_start) + break; + + r = (r - 1 + grid->num_rows) & (grid->num_rows - 1); + } + + if (start_row < 0) + return false; + + bool ret = rows_to_text(term, start_row, end_row, start_col, end_col, text, len); + if (!ret) + return false; + + /* + * If the FTCS_COMMAND_FINISHED marker was emitted at the *first* + * column, then the *entire* previous line is part of the command + * output. *Including* the newline, if any. + * + * Since rows_to_text() doesn't extract the column + * FTCS_COMMAND_FINISHED was emitted at (that would be wrong - + * FTCS_COMMAND_FINISHED is emitted *after* the command output, + * not at its last character), the extraction logic will not see + * the last newline (this is true for all non-line-wise selection + * types), and the extracted text will *not* end with a newline. + * + * Here we try to compensate for that. Note that if 'end_col' is + * not 0, then the command output only covers a partial row, and + * thus we do *not* want to append a newline. + */ + + if (end_col > 0) { + /* Command output covers partial row - don't append newline */ + return true; + } + + int next_to_last_row = (end_row - 1 + grid->num_rows) & (grid->num_rows - 1); + const struct row *row = grid->rows[next_to_last_row]; + + /* Add newline if last row has a hard linebreak */ + if (row->linebreak) { + char *new_text = xrealloc(*text, *len + 1 + 1); + + if (new_text == NULL) { + /* Ignore failure - use text as is (without inserting newline) */ + return true; + } + + *text = new_text; + (*len)++; + (*text)[*len - 1] = '\n'; + (*text)[*len] = '\0'; + } + + return true; +} + +bool +term_ime_is_enabled(const struct terminal *term) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + return term->ime_enabled; +#else + return false; +#endif +} + +void +term_ime_enable(struct terminal *term) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (term->ime_enabled) + return; + + LOG_DBG("IME enabled"); + + term->ime_enabled = true; + + /* IME is per seat - enable on all seat currently focusing us */ + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) + ime_enable(&it->item); + } +#endif +} + +void +term_ime_disable(struct terminal *term) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (!term->ime_enabled) + return; + + LOG_DBG("IME disabled"); + + term->ime_enabled = false; + + /* IME is per seat - disable on all seat currently focusing us */ + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) + ime_disable(&it->item); + } +#endif +} + +bool +term_ime_reset(struct terminal *term) +{ + bool at_least_one_seat_was_reset = false; + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + tll_foreach(term->wl->seats, it) { + struct seat *seat = &it->item; + + if (seat->kbd_focus != term) + continue; + + ime_reset_preedit(seat); + at_least_one_seat_was_reset = true; + } +#endif + + return at_least_one_seat_was_reset; +} + +void +term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width, + int height) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) { + it->item.ime.cursor_rect.pending.x = x; + it->item.ime.cursor_rect.pending.y = y; + it->item.ime.cursor_rect.pending.width = width; + it->item.ime.cursor_rect.pending.height = height; + } + } +#endif +} + +void +term_osc8_open(struct terminal *term, uint64_t id, const char *uri) +{ + term_osc8_close(term); + xassert(term->vt.osc8.uri == NULL); + + term->vt.osc8.id = id; + term->vt.osc8.uri = xstrdup(uri); + + term->bits_affecting_ascii_printer.osc8 = true; + term_update_ascii_printer(term); +} + +void +term_osc8_close(struct terminal *term) +{ + free(term->vt.osc8.uri); + term->vt.osc8.uri = NULL; + term->vt.osc8.id = 0; + term->bits_affecting_ascii_printer.osc8 = false; + term_update_ascii_printer(term); +} + +void +term_set_user_mouse_cursor(struct terminal *term, const char *cursor) +{ + free(term->mouse_user_cursor); + term->mouse_user_cursor = cursor != NULL && strlen(cursor) > 0 + ? xstrdup(cursor) + : NULL; + term_xcursor_update(term); +} + +void +term_enable_size_notifications(struct terminal *term) +{ + /* Note: always send current size upon activation, regardless of + previous state */ + term->size_notifications = true; + term_send_size_notification(term); +} + +void +term_disable_size_notifications(struct terminal *term) +{ + if (!term->size_notifications) + return; + + term->size_notifications = false; +} + +void +term_send_size_notification(struct terminal *term) +{ + if (!term->size_notifications) + return; + + const int height = term->height - term->margins.top - term->margins.bottom; + const int width = term->width - term->margins.left - term->margins.right; + + char buf[128]; + const size_t n = xsnprintf( + buf, sizeof(buf), "\033[48;%d;%d;%d;%dt", + term->rows, term->cols, height, width); + term_to_slave(term, buf, n); +} + +void +term_theme_switch_to_dark(struct terminal *term) +{ + if (term->colors.active_theme == COLOR_THEME_DARK) + return; + + term_theme_apply(term, &term->conf->colors_dark); + term->colors.active_theme = COLOR_THEME_DARK; + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;1n", 9); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); +} + +void +term_theme_switch_to_light(struct terminal *term) +{ + if (term->colors.active_theme == COLOR_THEME_LIGHT) + return; + + term_theme_apply(term, &term->conf->colors_light); + term->colors.active_theme = COLOR_THEME_LIGHT; + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;2n", 9); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); +} + +void +term_theme_toggle(struct terminal *term) +{ + if (term->colors.active_theme == COLOR_THEME_DARK) { + term_theme_apply(term, &term->conf->colors_light); + term->colors.active_theme = COLOR_THEME_LIGHT; + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;2n", 9); + } else { + term_theme_apply(term, &term->conf->colors_dark); + term->colors.active_theme = COLOR_THEME_DARK; + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;1n", 9); + } + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); +} + +const struct color_theme * +term_theme_get(const struct terminal *term) +{ + return term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; +} diff --git a/terminal.h b/terminal.h new file mode 100644 index 0000000..985d2ae --- /dev/null +++ b/terminal.h @@ -0,0 +1,1042 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#if defined(FOOT_GRAPHEME_CLUSTERING) + #include +#endif + +#include +#include + +#include "composed.h" +#include "config.h" +#include "debug.h" +#include "fdm.h" +#include "key-binding.h" +#include "macros.h" +#include "notify.h" +#include "reaper.h" +#include "shm.h" +#include "wayland.h" + +enum color_source { + COLOR_DEFAULT, + COLOR_BASE16, + COLOR_BASE256, + COLOR_RGB, +}; + +/* + * Note: we want the cells to be as small as possible. Larger cells + * means fewer scrollback lines (or performance drops due to cache + * misses) + * + * Note that the members are laid out optimized for x86 + */ +struct attributes { + bool bold:1; + bool dim:1; + bool italic:1; + bool underline:1; + bool strikethrough:1; + bool blink:1; + bool conceal:1; + bool reverse:1; + uint32_t fg:24; + + bool clean:1; + enum color_source fg_src:2; + enum color_source bg_src:2; + bool confined:1; + bool selected:1; + bool url:1; + uint32_t bg:24; +}; +static_assert(sizeof(struct attributes) == 8, "VT attribute struct too large"); + +/* Last valid Unicode code point is 0x0010FFFFul */ +#define CELL_COMB_CHARS_LO 0x00200000ul +#define CELL_COMB_CHARS_HI (CELL_COMB_CHARS_LO + 0x3fffffff) +#define CELL_SPACER (CELL_COMB_CHARS_HI + 1) + +struct cell { + char32_t wc; + struct attributes attrs; +}; +static_assert(sizeof(struct cell) == 12, "bad size"); + +struct scroll_region { + int start; + int end; +}; + +struct coord { + int col; + int row; +}; + +struct range { + struct coord start; + struct coord end; +}; + +struct cursor { + struct coord point; + bool lcf; /* Last Column Flag; https://github.com/mattiase/wraptest#basic-vt-line-wrapping-rules */ +}; + +enum damage_type {DAMAGE_SCROLL, DAMAGE_SCROLL_REVERSE, + DAMAGE_SCROLL_IN_VIEW, DAMAGE_SCROLL_REVERSE_IN_VIEW}; + +struct damage { + enum damage_type type; + struct scroll_region region; + uint16_t lines; +}; + +struct uri_range_data { + uint64_t id; + char *uri; +}; + +struct underline_range_data { + enum underline_style style; + enum color_source color_src; + uint32_t color; +}; + +union row_range_data { + struct uri_range_data uri; + struct underline_range_data underline; +}; + +struct row_range { + int start; + int end; + + union { + /* This is just an expanded union row_range_data, but + * anonymous, so that we don't have to write range->u.uri.id, + * but can instead do range->uri.id */ + union { + struct uri_range_data uri; + struct underline_range_data underline; + }; + union row_range_data data; + }; +}; + +struct row_ranges { + struct row_range *v; + int size; + int count; +}; + +enum row_range_type {ROW_RANGE_URI, ROW_RANGE_UNDERLINE}; + +struct row_data { + struct row_ranges uri_ranges; + struct row_ranges underline_ranges; +}; + +struct row { + struct cell *cells; + struct row_data *extra; + + bool dirty; + bool linebreak; + + struct { + bool prompt_marker; + int cmd_start; /* Column, -1 if unset */ + int cmd_end; /* Column, -1 if unset */ + } shell_integration; +}; + +struct sixel { + /* + * These three members reflect the "current", maybe scaled version + * of the image. + * + * The values will either be NULL/-1/-1, or match either the + * values in "original", or "scaled". + * + * They are typically reset when we need to invalidate the cached + * version (e.g. when the cell dimensions change). + */ + pixman_image_t *pix; + int width; + int height; + + int rows; + int cols; + struct coord pos; + bool opaque; + + /* + * We store the cell dimensions of the time the sixel was emitted. + * + * If the font size is changed, we rescale the image accordingly, + * to ensure it stays within its cell boundaries. 'scaled' is a + * cached, rescaled version of 'data' + 'pix'. + */ + int cell_width; + int cell_height; + + struct { + void *data; + pixman_image_t *pix; + int width; + int height; + } original; + + struct { + void *data; + pixman_image_t *pix; + int width; + int height; + } scaled; +}; + +enum kitty_kbd_flags { + KITTY_KBD_DISAMBIGUATE = 0x01, + KITTY_KBD_REPORT_EVENT = 0x02, + KITTY_KBD_REPORT_ALTERNATE = 0x04, + KITTY_KBD_REPORT_ALL = 0x08, + KITTY_KBD_REPORT_ASSOCIATED = 0x10, + KITTY_KBD_SUPPORTED = (KITTY_KBD_DISAMBIGUATE | + KITTY_KBD_REPORT_EVENT | + KITTY_KBD_REPORT_ALTERNATE | + KITTY_KBD_REPORT_ALL | + KITTY_KBD_REPORT_ASSOCIATED), +}; + +struct grid { + int num_rows; + int num_cols; + int offset; + int view; + + /* + * Note: the cursor (not the *saved* cursor) could most likely be + * global state in the term struct. + * + * However, we have grid specific functions that does not have + * access to the owning term struct, but does need access to the + * cursor. + */ + struct cursor cursor; + struct cursor saved_cursor; + + struct row **rows; + struct row *cur_row; + + tll(struct damage) scroll_damage; + tll(struct sixel) sixel_images; + + struct { + enum kitty_kbd_flags flags[8]; + uint8_t idx; + } kitty_kbd; + +}; + +struct vt_subparams { + uint8_t idx; + unsigned *cur; + unsigned value[16]; + unsigned dummy; +}; + +struct vt_param { + unsigned value; + struct vt_subparams sub; +}; + +struct vt { + int state; /* enum state */ + char32_t last_printed; +#if defined(FOOT_GRAPHEME_CLUSTERING) + utf8proc_int32_t grapheme_state; +#endif + char32_t utf8; + struct { + uint8_t idx; + struct vt_param *cur; + struct vt_param v[16]; + struct vt_param dummy; + } params; + + uint32_t private; /* LSB=priv0, MSB=priv3 */ + + struct attributes attrs; + struct attributes saved_attrs; + + struct { + uint8_t *data; + size_t size; + size_t idx; + bool bel; /* true if OSC string was terminated by BEL */ + } osc; + + /* Start coordinate for current OSC-8 URI */ + struct { + uint64_t id; + char *uri; + } osc8; + + struct underline_range_data underline; + + struct { + uint8_t *data; + size_t size; + size_t idx; + void (*put_handler)(struct terminal *term, uint8_t c); + void (*unhook_handler)(struct terminal *term); + } dcs; +}; + +enum cursor_origin { ORIGIN_ABSOLUTE, ORIGIN_RELATIVE }; +enum cursor_keys { CURSOR_KEYS_DONTCARE, CURSOR_KEYS_NORMAL, CURSOR_KEYS_APPLICATION }; +enum keypad_keys { KEYPAD_DONTCARE, KEYPAD_NUMERICAL, KEYPAD_APPLICATION }; +enum charset { CHARSET_ASCII, CHARSET_GRAPHIC }; +enum charset_designator { G0, G1, G2, G3 }; + +struct charsets { + enum charset_designator selected; + enum charset_designator saved; + enum charset set[4]; /* G0-G3 */ +}; + +/* *What* to report */ +enum mouse_tracking { + MOUSE_NONE, + MOUSE_X10, /* ?9h */ + MOUSE_CLICK, /* ?1000h - report mouse clicks */ + MOUSE_DRAG, /* ?1002h - report clicks and drag motions */ + MOUSE_MOTION, /* ?1003h - report clicks and motion */ +}; + +/* *How* to report */ +enum mouse_reporting { + MOUSE_NORMAL, + MOUSE_UTF8, /* ?1005h */ + MOUSE_SGR, /* ?1006h */ + MOUSE_URXVT, /* ?1015h */ + MOUSE_SGR_PIXELS, /* ?1016h */ +}; + +enum selection_kind { + SELECTION_NONE, + SELECTION_CHAR_WISE, + SELECTION_WORD_WISE, + SELECTION_QUOTE_WISE, + SELECTION_LINE_WISE, + SELECTION_BLOCK +}; +enum selection_direction {SELECTION_UNDIR, SELECTION_LEFT, SELECTION_RIGHT}; +enum selection_scroll_direction {SELECTION_SCROLL_NOT, SELECTION_SCROLL_UP, SELECTION_SCROLL_DOWN}; +enum search_direction { SEARCH_BACKWARD_SAME_POSITION, SEARCH_BACKWARD, SEARCH_FORWARD }; +enum search_case_mode { SEARCH_CASE_SMART, SEARCH_CASE_SENSITIVE, SEARCH_CASE_INSENSITIVE }; + +struct ptmx_buffer { + void *data; + size_t len; + size_t idx; +}; + +enum term_surface { + TERM_SURF_NONE, + TERM_SURF_GRID, + TERM_SURF_TITLE, + TERM_SURF_BORDER_LEFT, + TERM_SURF_BORDER_RIGHT, + TERM_SURF_BORDER_TOP, + TERM_SURF_BORDER_BOTTOM, + TERM_SURF_BUTTON_MINIMIZE, + TERM_SURF_BUTTON_MAXIMIZE, + TERM_SURF_BUTTON_CLOSE, + TERM_SURF_TAB_BAR, + TERM_SURF_TAB_OVERVIEW, +}; + +enum overlay_style { + OVERLAY_NONE, + OVERLAY_SEARCH, + OVERLAY_FLASH, + OVERLAY_UNICODE_MODE, +}; + +typedef tll(struct ptmx_buffer) ptmx_buffer_list_t; + +enum url_action { URL_ACTION_COPY, URL_ACTION_LAUNCH, URL_ACTION_PERSISTENT }; +struct url { + uint64_t id; + char *url; + char32_t *key; + struct range range; + enum url_action action; + bool url_mode_dont_change_url_attr; /* Entering/exiting URL mode doesn't touch the cells' attr.url */ + bool osc8; + bool duplicate; +}; +typedef tll(struct url) url_list_t; + + +struct colors { + uint32_t fg; + uint32_t bg; + uint32_t table[256]; + uint16_t alpha; + uint32_t cursor_fg; /* Text color */ + uint32_t cursor_bg; /* cursor color */ + uint32_t selection_fg; + uint32_t selection_bg; + enum which_color_theme active_theme; +}; + +struct terminal { + struct fdm *fdm; + struct reaper *reaper; + const struct config *conf; + bool is_tab; /* Secondary tab terminal (shares window with primary) */ + + void (*ascii_printer)(struct terminal *term, char32_t c); + union { + struct { + bool sixels:1; + bool osc8:1; + bool underline_style:1; + bool underline_color:1; + bool insert_mode:1; + bool charset:1; + }; + uint8_t value; + } bits_affecting_ascii_printer; + + pid_t slave; + int ptmx; + + struct vt vt; + struct grid *grid; + struct grid normal; + struct grid alt; + + int cols; /* number of columns */ + int rows; /* number of rows */ + struct scroll_region scroll_region; + + struct charsets charsets; + struct charsets saved_charsets; /* For save/restore cursor + attributes */ + + bool auto_margin; + bool insert_mode; + bool reverse; + bool hide_cursor; + bool reverse_wrap; + bool bracketed_paste; + bool focus_events; + bool alt_scrolling; + bool modify_other_keys_2; /* True when modifyOtherKeys=2 (i.e. "CSI >4;2m") */ + enum cursor_origin origin; + enum cursor_keys cursor_keys_mode; + enum keypad_keys keypad_keys_mode; + enum mouse_tracking mouse_tracking; + enum mouse_reporting mouse_reporting; + char *mouse_user_cursor; /* For OSC-22 */ + + tll(int) tab_stops; + + size_t composed_count; + struct composed *composed; + + /* Temporary: for FDM */ + struct { + bool is_armed; + int lower_fd; + int upper_fd; + } delayed_render_timer; + + struct fcft_font *fonts[4]; + struct config_font *font_sizes[4]; + struct pt_or_px font_line_height; + float font_dpi; + float font_dpi_before_unmap; + bool font_is_sized_by_dpi; + int16_t font_x_ofs; + int16_t font_y_ofs; + int16_t font_baseline; + enum fcft_subpixel font_subpixel; + + struct { + struct fcft_glyph **box_drawing; + struct fcft_glyph **braille; + struct fcft_glyph **octants; + struct fcft_glyph **legacy; + + #define GLYPH_BOX_DRAWING_FIRST 0x2500 + #define GLYPH_BOX_DRAWING_LAST 0x259F + #define GLYPH_BOX_DRAWING_COUNT \ + (GLYPH_BOX_DRAWING_LAST - GLYPH_BOX_DRAWING_FIRST + 1) + + #define GLYPH_BRAILLE_FIRST 0x2800 + #define GLYPH_BRAILLE_LAST 0x28FF + #define GLYPH_BRAILLE_COUNT \ + (GLYPH_BRAILLE_LAST - GLYPH_BRAILLE_FIRST + 1) + + #define GLYPH_OCTANTS_FIRST 0x1CD00 + #define GLYPH_OCTANTS_LAST 0x1CDE5 + #define GLYPH_OCTANTS_COUNT \ + (GLYPH_OCTANTS_LAST - GLYPH_OCTANTS_FIRST + 1) + + #define GLYPH_LEGACY_FIRST 0x1FB00 + #define GLYPH_LEGACY_LAST 0x1FB9B + #define GLYPH_LEGACY_COUNT \ + (GLYPH_LEGACY_LAST - GLYPH_LEGACY_FIRST + 1) + } custom_glyphs; + + bool is_sending_paste_data; + ptmx_buffer_list_t ptmx_buffers; + ptmx_buffer_list_t ptmx_paste_buffers; + + struct { + bool esc_prefix; + bool eight_bit; + } meta; + + bool num_lock_modifier; + bool bell_action_enabled; + bool report_theme_changes; + + /* Saved DECSET modes - we save the SET state */ + struct { + bool origin:1; + bool application_cursor_keys:1; + bool application_keypad_keys:1; + bool reverse:1; + bool show_cursor:1; + bool reverse_wrap:1; + bool auto_margin:1; + bool cursor_blink:1; + bool bracketed_paste:1; + bool focus_events:1; + bool alt_scrolling:1; + //bool mouse_x10:1; + bool mouse_click:1; + bool mouse_drag:1; + bool mouse_motion:1; + //bool mouse_utf8:1; + bool mouse_sgr:1; + bool mouse_urxvt:1; + bool mouse_sgr_pixels:1; + bool meta_eight_bit:1; + bool meta_esc_prefix:1; + bool num_lock_modifier:1; + bool bell_action_enabled:1; + bool alt_screen:1; + bool ime:1; + bool app_sync_updates:1; + bool grapheme_shaping:1; + bool report_theme_changes:1; + + bool size_notifications:1; + + bool sixel_display_mode:1; + bool sixel_private_palette:1; + bool sixel_cursor_right_of_graphics:1; + } xtsave; + + bool window_title_has_been_set; + char *window_title; + tll(char *) window_title_stack; + + /* Tab activity: set when this terminal receives PTY output while + * not the active tab; cleared on tab switch. */ + bool has_unread; + //char *window_icon; /* No escape sequence available to set the icon */ + //tll(char *)window_icon_stack; + char *app_id; + + struct { + bool active; + int fd; + } flash; + + struct { + enum { BLINK_ON, BLINK_OFF } state; + int fd; + } blink; + + float scale; + float scale_before_unmap; /* Last scaling factor used */ + int width; /* pixels */ + int height; /* pixels */ + int stashed_width; + int stashed_height; + struct { + int left; + int right; + int top; + int bottom; + } margins; + int cell_width; /* pixels per cell, x-wise */ + int cell_height; /* pixels per cell, y-wise */ + + struct colors colors; + + struct { + struct colors *stack; + size_t idx; + size_t size; + } color_stack; + + enum cursor_style cursor_style; + struct { + bool decset; /* Blink enabled via '\E[?12h' */ + bool deccsusr; /* Blink enabled via '\E[X q' */ + int fd; + enum { CURSOR_BLINK_ON, CURSOR_BLINK_OFF } state; + } cursor_blink; + + struct { + enum selection_kind kind; + enum selection_direction direction; + struct range coords; + bool ongoing; + bool spaces_only; /* SELECTION_SEMANTIC_WORD */ + + struct range pivot; + + struct { + int fd; + int col; + enum selection_scroll_direction direction; + } auto_scroll; + } selection; + + bool is_searching; + struct { + char32_t *buf; + size_t len; + size_t sz; + size_t cursor; + + int original_view; + bool view_followed_offset; + struct coord match; + size_t match_len; + + /* Counter: position of current match within total matches */ + size_t total_count; + size_t current_idx; /* 1-based; 0 means no current */ + + /* Toggles */ + enum search_case_mode case_mode; + bool whole_word; + bool regex; + + /* Wrap indicator: set transiently when find wraps around */ + bool wrapped; + + /* Compiled regex (when regex == true and pattern is valid) */ + void *regex_compiled; /* regex_t* or NULL */ + bool regex_valid; + + /* History */ + struct search_history_entry { + char32_t *buf; + size_t len; + struct search_history_entry *prev, *next; + } *history_head, *history_tail; + struct search_history_entry *history_pos; /* NULL = at fresh entry */ + + struct { + char32_t *buf; + size_t len; + } last; + } search; + + struct wayland *wl; + struct wl_window *window; + bool visual_focus; + bool kbd_focus; + enum term_surface active_surface; + + struct { + struct { + struct buffer_chain *grid; + struct buffer_chain *search; + struct buffer_chain *scrollback_indicator; + struct buffer_chain *render_timer; + struct buffer_chain *url; + struct buffer_chain *csd; + struct buffer_chain *overlay; + struct buffer_chain *tab_bar; + struct buffer_chain *tab_overview; + } chains; + + /* Scheduled for rendering, as soon-as-possible */ + struct { + bool grid; + bool csd; + bool search; + bool urls; + } refresh; + + /* Scheduled for rendering, in the next frame callback */ + struct { + bool grid; + bool csd; + bool search; + bool urls; + } pending; + + bool margins; /* Someone explicitly requested a refresh of the margins */ + bool urgency; /* Signal 'urgency' (paint borders red) */ + + struct { + struct timespec last_update; + int timer_fd; + } title; + + struct { + struct timespec last_update; + int timer_fd; + } icon; + + struct { + struct timespec last_update; + int timer_fd; + } app_id; + + uint32_t scrollback_lines; /* Number of scrollback lines, from conf (TODO: move out from render struct?) */ + + struct { + bool enabled; + int timer_fd; + } app_sync_updates; + + /* Render threads + synchronization primitives */ + struct { + uint16_t count; + sem_t start; + sem_t done; + mtx_t lock; + tll(int) queue; + thrd_t *threads; + struct buffer *buf; + + struct { + mtx_t lock; + cnd_t cond; + struct buffer *buf; + struct timespec start; + struct timespec stop; + } preapplied_damage; + } workers; + + /* Last rendered cursor position */ + struct { + struct row *row; + int col; + bool hidden; + } last_cursor; + + struct buffer *last_buf; /* Buffer we rendered to last time */ + size_t frames_since_last_immediate_release; + bool preapply_last_frame_damage; + + enum overlay_style last_overlay_style; + struct buffer *last_overlay_buf; + pixman_region32_t last_overlay_clip; + + size_t search_glyph_offset; + + struct timespec input_time; + } render; + + struct { + struct grid *grid; /* Original 'normal' grid, before resize started */ + int old_screen_rows; /* term->rows before resize started */ + int old_cols; /* term->cols before resize started */ + int old_hide_cursor; /* term->hide_cursor before resize started */ + int new_rows; /* New number of scrollback rows */ + struct range selection_coords; + } interactive_resizing; + + struct { + enum { + SIXEL_DECSIXEL, /* DECSIXEL body part ", $, -, ? ... ~ */ + SIXEL_DECGRA, /* DECGRA Set Raster Attributes " Pan; Pad; Ph; Pv */ + SIXEL_DECGRI, /* DECGRI Graphics Repeat Introducer ! Pn Ch */ + SIXEL_DECGCI, /* DECGCI Graphics Color Introducer # Pc; Pu; Px; Py; Pz */ + } state; + + struct coord pos; /* Current sixel coordinate */ + int color_idx; /* Current palette index */ + uint32_t *private_palette; /* Private palette, used when private mode 1070 is enabled */ + uint32_t *shared_palette; /* Shared palette, used when private mode 1070 is disabled */ + uint32_t *palette; /* Points to either private_palette or shared_palette */ + uint32_t color; + + struct { + uint32_t *data; /* Raw image data, in ARGB */ + uint32_t *p; /* Pointer into data, for current position */ + int width; /* Image width, in pixels */ + int height; /* Image height, in pixels */ + int alloc_height; + unsigned int bottom_pixel; + } image; + + /* + * Pan is the vertical shape of a pixel + * Pad is the horizontal shape of a pixel + * + * pan/pad is the sixel's aspect ratio + */ + int pan; + int pad; + + bool scrolling:1; /* Private mode 80 */ + bool use_private_palette:1; /* Private mode 1070 */ + bool cursor_right_of_graphics:1; /* Private mode 8452 */ + + unsigned params[5]; /* Collected parameters, for RASTER, COLOR_SPEC */ + unsigned param; /* Currently collecting parameter, for RASTER, COLOR_SPEC and REPEAT */ + unsigned param_idx; /* Parameters seen */ + unsigned repeat_count; + + bool transparent_bg; + + bool linear_blending; + bool use_10bit; + pixman_format_code_t pixman_fmt; + + /* Application configurable */ + unsigned palette_size; /* Number of colors in palette */ + unsigned max_width; /* Maximum image width, in pixels */ + unsigned max_height; /* Maximum image height, in pixels */ + } sixel; + + /* TODO: wrap in a struct */ + url_list_t urls; + char32_t url_keys[5]; + bool urls_show_uri_on_jump_label; + struct grid *url_grid_snapshot; + bool ime_reenable_after_url_mode; + const struct config_spawn_template *url_launch; + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + bool ime_enabled; +#endif + + struct { + bool active; + int count; + char32_t character; + } unicode_mode; + + struct { + bool in_progress; + bool client_has_terminated; + bool fdm_done; + int terminate_timeout_fd; + int exit_status; + int next_signal; + + void (*cb)(void *data, int exit_code); + void *cb_data; + } shutdown; + + /* State, to handle chunked notifications */ + struct notification kitty_notification; + + /* Currently active notifications, from foot's perspective (their + notification helper processes are still running) */ + tll(struct notification) active_notifications; + struct notification_icon notification_icons[32]; + + char *foot_exe; + char *cwd; + + bool grapheme_shaping; + bool size_notifications; +}; + +struct config; +struct terminal *term_init( + const struct config *conf, struct fdm *fdm, struct reaper *reaper, + struct wayland *wayl, const char *foot_exe, const char *cwd, + const char *token, const char *pty_path, + int argc, char *const *argv, const char *const *envp, + void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data); + +struct terminal *term_tab_new( + struct terminal *primary, + int argc, char *const *argv, const char *const *envp, + void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data); + +void term_tab_switch(struct wl_window *win, size_t idx); +void term_tab_close(struct terminal *term); +size_t term_tab_bar_hit_test(const struct terminal *term, int x, int y); + +bool term_shutdown(struct terminal *term); +int term_destroy(struct terminal *term); + +void term_update_ascii_printer(struct terminal *term); +void term_single_shift(struct terminal *term, enum charset_designator idx); + +void term_reset(struct terminal *term, bool hard); +bool term_to_slave(struct terminal *term, const void *data, size_t len); +bool term_paste_data_to_slave( + struct terminal *term, const void *data, size_t len); + +bool term_fractional_scaling(const struct terminal *term); +bool term_preferred_buffer_scale(const struct terminal *term); +bool term_update_scale(struct terminal *term); +bool term_font_size_increase(struct terminal *term); +bool term_font_size_decrease(struct terminal *term); +bool term_font_size_reset(struct terminal *term); +bool term_font_dpi_changed(struct terminal *term, float old_scale); +void term_font_subpixel_changed(struct terminal *term); +int term_font_baseline(const struct terminal *term); + +int term_pt_or_px_as_pixels( + const struct terminal *term, const struct pt_or_px *pt_or_px); + + +void term_window_configured(struct terminal *term); + +void term_damage_rows(struct terminal *term, int start, int end); +void term_damage_rows_in_view(struct terminal *term, int start, int end); + +void term_damage_all(struct terminal *term); +void term_damage_view(struct terminal *term); + +void term_damage_cursor(struct terminal *term); +void term_damage_margins(struct terminal *term); +void term_damage_color(struct terminal *term, enum color_source src, int idx); + +void term_reset_view(struct terminal *term); + +void term_damage_scroll( + struct terminal *term, enum damage_type damage_type, + struct scroll_region region, int lines); + +void term_erase( + struct terminal *term, + int start_row, int start_col, + int end_row, int end_col); +void term_erase_scrollback(struct terminal *term); + +int term_row_rel_to_abs(const struct terminal *term, int row); +void term_cursor_home(struct terminal *term); +void term_cursor_to(struct terminal *term, int row, int col); +void term_cursor_col(struct terminal *term, int col); +void term_cursor_left(struct terminal *term, int count); +void term_cursor_right(struct terminal *term, int count); +void term_cursor_up(struct terminal *term, int count); +void term_cursor_down(struct terminal *term, int count); +void term_cursor_blink_update(struct terminal *term); + +void term_process_and_print_non_ascii(struct terminal *term, char32_t wc); +void term_print(struct terminal *term, char32_t wc, int width, + bool insert_mode_disable); +void term_fill(struct terminal *term, int row, int col, uint8_t c, size_t count, + bool use_sgr_attrs); + +void term_scroll(struct terminal *term, int rows); +void term_scroll_reverse(struct terminal *term, int rows); + +void term_scroll_partial( + struct terminal *term, struct scroll_region region, int rows); +void term_scroll_reverse_partial( + struct terminal *term, struct scroll_region region, int rows); + +void term_carriage_return(struct terminal *term); +void term_linefeed(struct terminal *term); +void term_reverse_index(struct terminal *term); + +void term_arm_blink_timer(struct terminal *term); + +void term_save_cursor(struct terminal *term); +void term_restore_cursor(struct terminal *term, const struct cursor *cursor); + +void term_visual_focus_in(struct terminal *term); +void term_visual_focus_out(struct terminal *term); +void term_kbd_focus_in(struct terminal *term); +void term_kbd_focus_out(struct terminal *term); +void term_mouse_down( + struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, + bool shift, bool alt, bool ctrl); +void term_mouse_up( + struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, + bool shift, bool alt, bool ctrl); +void term_mouse_motion( + struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, + bool shift, bool alt, bool ctrl); +bool term_mouse_grabbed(const struct terminal *term, const struct seat *seat); +void term_xcursor_update(struct terminal *term); +void term_xcursor_update_for_seat(struct terminal *term, struct seat *seat); +void term_set_user_mouse_cursor(struct terminal *term, const char *cursor); + +void term_set_window_title(struct terminal *term, const char *title); +void term_set_app_id(struct terminal *term, const char *app_id); +const char *term_icon(const struct terminal *term); +void term_flash(struct terminal *term, unsigned duration_ms); +void term_bell(struct terminal *term); +bool term_spawn_new(const struct terminal *term); + +void term_enable_app_sync_updates(struct terminal *term); +void term_disable_app_sync_updates(struct terminal *term); + +enum term_surface term_surface_kind( + const struct terminal *term, const struct wl_surface *surface); + +bool term_scrollback_to_text( + const struct terminal *term, char **text, size_t *len); +bool term_view_to_text( + const struct terminal *term, char **text, size_t *len); +bool term_command_output_to_text( + const struct terminal *term, char **text, size_t *len); + +bool term_ime_is_enabled(const struct terminal *term); +void term_ime_enable(struct terminal *term); +void term_ime_disable(struct terminal *term); +bool term_ime_reset(struct terminal *term); +void term_ime_set_cursor_rect( + struct terminal *term, int x, int y, int width, int height); + +void term_urls_reset(struct terminal *term); +void term_collect_urls(struct terminal *term); + +void term_osc8_open(struct terminal *term, uint64_t id, const char *uri); +void term_osc8_close(struct terminal *term); + +bool term_ptmx_pause(struct terminal *term); +bool term_ptmx_resume(struct terminal *term); + +void term_enable_size_notifications(struct terminal *term); +void term_disable_size_notifications(struct terminal *term); +void term_send_size_notification(struct terminal *term); + +void term_theme_switch_to_dark(struct terminal *term); +void term_theme_switch_to_light(struct terminal *term); +void term_theme_toggle(struct terminal *term); +const struct color_theme *term_theme_get(const struct terminal *term); + +static inline void term_reset_grapheme_state(struct terminal *term) +{ +#if defined(FOOT_GRAPHEME_CLUSTERING) + term->vt.grapheme_state = 0; +#endif +} diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..9baa064 --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,8 @@ +config_test = executable( + 'test-config', + 'test-config.c', + wl_proto_headers, + link_with: [common, tokenize], + dependencies: [pixman, xkb, fontconfig, wayland_client, fcft, tllist]) + +test('config', config_test) diff --git a/tests/test-config.c b/tests/test-config.c new file mode 100644 index 0000000..16cfb2b --- /dev/null +++ b/tests/test-config.c @@ -0,0 +1,1559 @@ +#if !defined(_DEBUG) + #define _DEBUG +#endif +#undef NDEBUG + +#include "../log.h" + +#include "../config.c" + +#define ALEN(v) (sizeof(v) / sizeof((v)[0])) + +/* + * Stubs + */ + +void +user_notification_add_fmt(user_notifications_t *notifications, + enum user_notification_kind kind, + const char *fmt, ...) +{ +} + +static void +test_invalid_key(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key) +{ + ctx->key = key; + ctx->value = "value for invalid key"; + + if (parse_fun(ctx)) { + BUG("[%s].%s: did not fail to parse as expected" + "(key should be invalid)", ctx->section, ctx->key); + } +} + +static void +test_string(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, char *const *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + const char *value; + bool invalid; + } input[] = { + {"a string", "a string"}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (!streq(*ptr, input[i].value)) { + BUG("[%s].%s=%s: set value (%s) not the expected one (%s)", + ctx->section, ctx->key, ctx->value, + *ptr, input[i].value); + } + } + } +} + +static void +test_c32string(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, char32_t *const *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + const char32_t *value; + bool invalid; + } input[] = { + {"a string", U"a string"}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (c32cmp(*ptr, input[i].value) != 0) { + BUG("[%s].%s=%s: set value (%ls) not the expected one (%ls)", + ctx->section, ctx->key, ctx->value, + (const wchar_t *)*ptr, + (const wchar_t *)input[i].value); + } + } + } +} + +static void +test_boolean(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const bool *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + bool value; + bool invalid; + } input[] = { + {"1", true}, {"0", false}, + {"on", true}, {"off", false}, + {"true", true}, {"false", false}, + {"unittest-invalid-boolean-value", false, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (*ptr != input[i].value) { + BUG("[%s].%s=%s: set value (%s) not the expected one (%s)", + ctx->section, ctx->key, ctx->value, + *ptr ? "true" : "false", + input[i].value ? "true" : "false"); + } + } + } +} + +static void +test_uint16(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const uint16_t *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + uint16_t value; + bool invalid; + } input[] = { + {"0", 0}, {"65535", 65535}, {"65536", 0, true}, + {"abc", 0, true}, {"true", 0, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (*ptr != input[i].value) { + BUG("[%s].%s=%s: set value (%hu) not the expected one (%hu)", + ctx->section, ctx->key, ctx->value, + *ptr, input[i].value); + } + } + } +} + +static void +test_uint32(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const uint32_t *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + uint32_t value; + bool invalid; + } input[] = { + {"0", 0}, {"65536", 65536}, {"4294967295", 4294967295}, + {"4294967296", 0, true}, {"abc", 0, true}, {"true", 0, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (*ptr != input[i].value) { + BUG("[%s].%s=%s: set value (%u) not the expected one (%u)", + ctx->section, ctx->key, ctx->value, + *ptr, input[i].value); + } + } + } +} + +static void +test_float(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const float *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + float value; + bool invalid; + } input[] = { + {"0", 0}, {"0.1", 0.1}, {"1e10", 1e10}, {"-10.7", -10.7}, + {"abc", 0, true}, {"true", 0, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (*ptr != input[i].value) { + BUG("[%s].%s=%s: set value (%f) not the expected one (%f)", + ctx->section, ctx->key, ctx->value, + *ptr, input[i].value); + } + } + } +} + +static void +test_pt_or_px(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const struct pt_or_px *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + struct pt_or_px value; + bool invalid; + } input[] = { + {"12", {.pt = 12}}, {"12px", {.px = 12}}, + {"unittest-invalid-pt-or-px-value", {0}, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (memcmp(ptr, &input[i].value, sizeof(*ptr)) != 0) { + BUG("[%s].%s=%s: " + "set value (pt=%f, px=%d) not the expected one (pt=%f, px=%d)", + ctx->section, ctx->key, ctx->value, + ptr->pt, ptr->px, + input[i].value.pt, input[i].value.px); + } + } + } +} + +static void +test_spawn_template(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const struct config_spawn_template *ptr) +{ + static const char *const args[] = { + "command", "arg1", "arg2", "arg3 has spaces"}; + + ctx->key = key; + ctx->value = "command arg1 arg2 \"arg3 has spaces\""; + + if (!parse_fun(ctx)) + BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); + + if (ptr->argv.args == NULL) + BUG("[%s].%s=%s: argv is NULL", ctx->section, ctx->key, ctx->value); + + for (size_t i = 0; i < ALEN(args); i++) { + if (ptr->argv.args[i] == NULL || !streq(ptr->argv.args[i], args[i])) { + BUG("[%s].%s=%s: set value not the expected one: " + "mismatch of arg #%zu: expected=\"%s\", got=\"%s\"", + ctx->section, ctx->key, ctx->value, i, + args[i], ptr->argv.args[i]); + } + } + + if (ptr->argv.args[ALEN(args)] != NULL) { + BUG("[%s].%s=%s: set value not the expected one: " + "expected NULL terminator at arg #%zu, got=\"%s\"", + ctx->section, ctx->key, ctx->value, + ALEN(args), ptr->argv.args[ALEN(args)]); + } + + /* Trigger parse failure */ + ctx->value = "command with \"unterminated quote"; + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } +} + +static void +test_enum(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, size_t count, const char *enum_strings[static count], + int enum_values[static count], int *ptr) +{ + ctx->key = key; + + for (size_t i = 0; i < count; i++) { + ctx->value = enum_strings[i]; + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + + if (*ptr != enum_values[i]) { + BUG("[%s].%s=%s: set value not the expected one: expected %d, got %d", + ctx->section, ctx->key, ctx->value, enum_values[i], *ptr); + } + } + + ctx->value = "invalid-enum-value"; + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } +} + + +static void +test_color(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, bool alpha_allowed, uint32_t *ptr) +{ + ctx->key = key; + + const struct { + const char *option_string; + uint32_t color; + bool invalid; + } input[] = { + {"000000", 0}, + {"999999", 0x999999}, + {"ffffff", 0xffffff}, + {"ffffffff", 0xffffffff, !alpha_allowed}, + {"aabbccdd", 0xaabbccdd, !alpha_allowed}, + {"00", 0, true}, + {"0000", 0, true}, + {"00000", 0, true}, + {"000000000", 0, true}, + {"unittest-invalid-color", 0, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + + uint32_t color = input[i].color; + if (alpha_allowed && strlen(input[i].option_string) == 6) + color |= 0xff000000; + + if (*ptr != color) { + BUG("[%s].%s=%s: expected 0x%08x, got 0x%08x", + ctx->section, ctx->key, ctx->value, + color, *ptr); + } + } + } +} + +static void +test_two_colors(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, bool alpha_allowed, + uint32_t *ptr1, uint32_t *ptr2) +{ + ctx->key = key; + + const struct { + const char *option_string; + uint32_t color1; + uint32_t color2; + bool invalid; + } input[] = { + {"000000 000000", 0, 0}, + + /* No alpha */ + {"999999 888888", 0x999999, 0x888888}, + {"ffffff aaaaaa", 0xffffff, 0xaaaaaa}, + + /* Both colors have alpha component */ + {"ffffffff 00000000", 0xffffffff, 0x00000000, !alpha_allowed}, + {"aabbccdd, ee112233", 0xaabbccdd, 0xee112233, !alpha_allowed}, + + /* Only one color has alpha component */ + {"ffffffff 112233", 0xffffffff, 0x112233, !alpha_allowed}, + {"ffffff ff112233", 0x00ffffff, 0xff112233, !alpha_allowed}, + + {"unittest-invalid-color", 0, 0, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + + if (*ptr1 != input[i].color1) { + BUG("[%s].%s=%s: expected 0x%08x, got 0x%08x", + ctx->section, ctx->key, ctx->value, + input[i].color1, *ptr1); + } + + if (*ptr2 != input[i].color2) { + BUG("[%s].%s=%s: expected 0x%08x, got 0x%08x", + ctx->section, ctx->key, ctx->value, + input[i].color2, *ptr2); + } + } + } +} + +static void +test_section_main(void) +{ + struct config conf = {0}; + struct context ctx = {.conf = &conf, .section = "main", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_main, "invalid-key"); + + test_string(&ctx, &parse_section_main, "shell", &conf.shell); + test_string(&ctx, &parse_section_main, "term", &conf.term); + test_string(&ctx, &parse_section_main, "app-id", &conf.app_id); + test_string(&ctx, &parse_section_main, "toplevel-tag", &conf.toplevel_tag); + test_string(&ctx, &parse_section_main, "utmp-helper", &conf.utmp_helper_path); + + test_c32string(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters); + + test_boolean(&ctx, &parse_section_main, "login-shell", &conf.login_shell); + test_boolean(&ctx, &parse_section_main, "box-drawings-uses-font-glyphs", &conf.box_drawings_uses_font_glyphs); + test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); + test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware); + test_boolean(&ctx, &parse_section_main, "gamma-correct-blending", &conf.gamma_correct); + test_boolean(&ctx, &parse_section_main, "uppercase-regex-insert", &conf.uppercase_regex_insert); + + test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px); /* TODO: test ‘N%’ values too */ + test_pt_or_px(&ctx, &parse_section_main, "line-height", &conf.line_height); + test_pt_or_px(&ctx, &parse_section_main, "letter-spacing", &conf.letter_spacing); + test_pt_or_px(&ctx, &parse_section_main, "horizontal-letter-offset", &conf.horizontal_letter_offset); + test_pt_or_px(&ctx, &parse_section_main, "vertical-letter-offset", &conf.vertical_letter_offset); + test_pt_or_px(&ctx, &parse_section_main, "underline-thickness", &conf.underline_thickness); + test_pt_or_px(&ctx, &parse_section_main, "strikeout-thickness", &conf.strikeout_thickness); + + test_uint16(&ctx, &parse_section_main, "resize-delay-ms", &conf.resize_delay_ms); + test_uint16(&ctx, &parse_section_main, "workers", &conf.render_worker_count); + + test_enum(&ctx, &parse_section_main, "selection-target", + 4, + (const char *[]){"none", "primary", "clipboard", "both"}, + (int []){SELECTION_TARGET_NONE, + SELECTION_TARGET_PRIMARY, + SELECTION_TARGET_CLIPBOARD, + SELECTION_TARGET_BOTH}, + (int *)&conf.selection_target); + + test_enum( + &ctx, &parse_section_main, "initial-window-mode", + 3, + (const char *[]){"windowed", "maximized", "fullscreen"}, + (int []){STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN}, + (int *)&conf.startup_mode); + + test_enum( + &ctx, &parse_section_main, "initial-color-theme", + 2, + (const char *[]){"dark", "light", "1", "2"}, + (int []){COLOR_THEME_DARK, COLOR_THEME_LIGHT, + COLOR_THEME_DARK, COLOR_THEME_LIGHT}, + (int *)&conf.initial_color_theme); + + /* TODO: font (custom) */ + /* TODO: include (custom) */ + /* TODO: bold-text-in-bright (enum/boolean) */ + /* TODO: pad (geometry + optional string)*/ + /* TODO: initial-window-size-pixels (geometry) */ + /* TODO: initial-window-size-chars (geometry) */ + + config_free(&conf); +} + +static void +test_section_security(void) +{ + struct config conf = {0}; + struct context ctx = {.conf = &conf, .section = "security", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_security, "invalid-key"); + test_enum( + &ctx, &parse_section_security, "osc52", 4, + (const char*[]){"disabled", "copy-enabled", "paste-enabled", "enabled"}, + (int []){OSC52_DISABLED, OSC52_COPY_ENABLED, OSC52_PASTE_ENABLED, OSC52_ENABLED}, + (int *)&conf.security.osc52); + + config_free(&conf); +} + +static void +test_section_bell(void) +{ + struct config conf = {0}; + struct context ctx = {.conf = &conf, .section = "bell", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_bell, "invalid-key"); + + test_boolean(&ctx, &parse_section_bell, "urgent", &conf.bell.urgent); + test_boolean(&ctx, &parse_section_bell, "notify", &conf.bell.notify); + test_boolean(&ctx, &parse_section_bell, "system", &conf.bell.system_bell); + test_boolean(&ctx, &parse_section_bell, "command-focused", + &conf.bell.command_focused); + test_spawn_template(&ctx, &parse_section_bell, "command", + &conf.bell.command); + + config_free(&conf); +} + +static void +test_section_desktop_notifications(void) +{ + struct config conf = {0}; + struct context ctx = {.conf = &conf, .section = "desktop-notifications", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_desktop_notifications, "invalid-key"); + + test_boolean(&ctx, &parse_section_desktop_notifications, "inhibit-when-focused", &conf.desktop_notifications.inhibit_when_focused); + test_spawn_template(&ctx, &parse_section_desktop_notifications, "command", &conf.desktop_notifications.command); + test_spawn_template(&ctx, &parse_section_desktop_notifications, "command-action-argument", &conf.desktop_notifications.command_action_arg); + test_spawn_template(&ctx, &parse_section_desktop_notifications, "close", &conf.desktop_notifications.close); + + config_free(&conf); +} + +static void +test_section_scrollback(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "scrollback", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_scrollback, "invalid-key"); + + test_uint32(&ctx, &parse_section_scrollback, "lines", + &conf.scrollback.lines); + test_float(&ctx, parse_section_scrollback, "multiplier", &conf.scrollback.multiplier); + + test_enum( + &ctx, &parse_section_scrollback, "indicator-position", + 3, + (const char *[]){"none", "fixed", "relative"}, + (int []){SCROLLBACK_INDICATOR_POSITION_NONE, + SCROLLBACK_INDICATOR_POSITION_FIXED, + SCROLLBACK_INDICATOR_POSITION_RELATIVE}, + (int *)&conf.scrollback.indicator.position); + + /* TODO: indicator-format (enum, sort-of) */ + + config_free(&conf); +} + +static void +test_section_url(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "url", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_url, "invalid-key"); + + test_spawn_template(&ctx, &parse_section_url, "launch", &conf.url.launch); + test_enum(&ctx, &parse_section_url, "osc8-underline", + 2, + (const char *[]){"url-mode", "always"}, + (int []){OSC8_UNDERLINE_URL_MODE, OSC8_UNDERLINE_ALWAYS}, + (int *)&conf.url.osc8_underline); + test_enum(&ctx, &parse_section_url, "style", + 6, (const char *[]){"none", "single", "double", "curly", "dotted", "dashed"}, + (int []){UNDERLINE_NONE, UNDERLINE_SINGLE, UNDERLINE_DOUBLE, UNDERLINE_CURLY, UNDERLINE_DOTTED, UNDERLINE_DASHED}, + (int *)&conf.url.style); + test_c32string(&ctx, &parse_section_url, "label-letters", &conf.url.label_letters); + + config_free(&conf); +} + +static void +test_section_cursor(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "cursor", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_cursor, "invalid-key"); + + test_enum( + &ctx, &parse_section_cursor, "style", + 3, + (const char *[]){"block", "beam", "underline"}, + (int []){CURSOR_BLOCK, CURSOR_BEAM, CURSOR_UNDERLINE}, + (int *)&conf.cursor.style); + test_enum( + &ctx, &parse_section_cursor, "unfocused-style", + 3, + (const char *[]){"unchanged", "hollow", "none"}, + (int []){CURSOR_UNFOCUSED_UNCHANGED, CURSOR_UNFOCUSED_HOLLOW, CURSOR_UNFOCUSED_NONE}, + (int *)&conf.cursor.unfocused_style); + test_boolean(&ctx, &parse_section_cursor, "blink", &conf.cursor.blink.enabled); + test_uint32(&ctx, &parse_section_cursor, "blink-rate", &conf.cursor.blink.rate_ms); + test_pt_or_px(&ctx, &parse_section_cursor, "beam-thickness", + &conf.cursor.beam_thickness); + test_pt_or_px(&ctx, &parse_section_cursor, "underline-thickness", + &conf.cursor.underline_thickness); + + /* TODO: color (two RRGGBB values) */ + + config_free(&conf); +} + +static void +test_section_mouse(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "mouse", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_mouse, "invalid-key"); + + test_boolean(&ctx, &parse_section_mouse, "hide-when-typing", + &conf.mouse.hide_when_typing); + test_boolean(&ctx, &parse_section_mouse, "alternate-scroll-mode", + &conf.mouse.alternate_scroll_mode); + + config_free(&conf); +} + +static void +test_section_touch(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "touch", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_touch, "invalid-key"); + + test_uint32(&ctx, &parse_section_touch, "long-press-delay", + &conf.touch.long_press_delay); + + config_free(&conf); +} + +static void +test_section_colors_dark(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "colors-dark", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_colors, "invalid-key"); + + test_color(&ctx, &parse_section_colors_dark, "foreground", false, &conf.colors_dark.fg); + test_color(&ctx, &parse_section_colors_dark, "background", false, &conf.colors_dark.bg); + test_color(&ctx, &parse_section_colors_dark, "regular0", false, &conf.colors_dark.table[0]); + test_color(&ctx, &parse_section_colors_dark, "regular1", false, &conf.colors_dark.table[1]); + test_color(&ctx, &parse_section_colors_dark, "regular2", false, &conf.colors_dark.table[2]); + test_color(&ctx, &parse_section_colors_dark, "regular3", false, &conf.colors_dark.table[3]); + test_color(&ctx, &parse_section_colors_dark, "regular4", false, &conf.colors_dark.table[4]); + test_color(&ctx, &parse_section_colors_dark, "regular5", false, &conf.colors_dark.table[5]); + test_color(&ctx, &parse_section_colors_dark, "regular6", false, &conf.colors_dark.table[6]); + test_color(&ctx, &parse_section_colors_dark, "regular7", false, &conf.colors_dark.table[7]); + test_color(&ctx, &parse_section_colors_dark, "bright0", false, &conf.colors_dark.table[8]); + test_color(&ctx, &parse_section_colors_dark, "bright1", false, &conf.colors_dark.table[9]); + test_color(&ctx, &parse_section_colors_dark, "bright2", false, &conf.colors_dark.table[10]); + test_color(&ctx, &parse_section_colors_dark, "bright3", false, &conf.colors_dark.table[11]); + test_color(&ctx, &parse_section_colors_dark, "bright4", false, &conf.colors_dark.table[12]); + test_color(&ctx, &parse_section_colors_dark, "bright5", false, &conf.colors_dark.table[13]); + test_color(&ctx, &parse_section_colors_dark, "bright6", false, &conf.colors_dark.table[14]); + test_color(&ctx, &parse_section_colors_dark, "bright7", false, &conf.colors_dark.table[15]); + test_color(&ctx, &parse_section_colors_dark, "dim0", false, &conf.colors_dark.dim[0]); + test_color(&ctx, &parse_section_colors_dark, "dim1", false, &conf.colors_dark.dim[1]); + test_color(&ctx, &parse_section_colors_dark, "dim2", false, &conf.colors_dark.dim[2]); + test_color(&ctx, &parse_section_colors_dark, "dim3", false, &conf.colors_dark.dim[3]); + test_color(&ctx, &parse_section_colors_dark, "dim4", false, &conf.colors_dark.dim[4]); + test_color(&ctx, &parse_section_colors_dark, "dim5", false, &conf.colors_dark.dim[5]); + test_color(&ctx, &parse_section_colors_dark, "dim6", false, &conf.colors_dark.dim[6]); + test_color(&ctx, &parse_section_colors_dark, "dim7", false, &conf.colors_dark.dim[7]); + test_color(&ctx, &parse_section_colors_dark, "selection-foreground", false, &conf.colors_dark.selection_fg); + test_color(&ctx, &parse_section_colors_dark, "selection-background", false, &conf.colors_dark.selection_bg); + test_color(&ctx, &parse_section_colors_dark, "urls", false, &conf.colors_dark.url); + test_two_colors(&ctx, &parse_section_colors_dark, "jump-labels", false, + &conf.colors_dark.jump_label.fg, + &conf.colors_dark.jump_label.bg); + test_two_colors(&ctx, &parse_section_colors_dark, "scrollback-indicator", false, + &conf.colors_dark.scrollback_indicator.fg, + &conf.colors_dark.scrollback_indicator.bg); + test_two_colors(&ctx, &parse_section_colors_dark, "search-box-no-match", false, + &conf.colors_dark.search_box.no_match.fg, + &conf.colors_dark.search_box.no_match.bg); + test_two_colors(&ctx, &parse_section_colors_dark, "search-box-match", false, + &conf.colors_dark.search_box.match.fg, + &conf.colors_dark.search_box.match.bg); + + test_two_colors(&ctx, &parse_section_colors_dark, "cursor", false, + &conf.colors_dark.cursor.text, + &conf.colors_dark.cursor.cursor); + + test_enum(&ctx, &parse_section_colors_dark, "alpha-mode", 3, + (const char *[]){"default", "matching", "all"}, + (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, + (int *)&conf.colors_dark.alpha_mode); + + test_enum(&ctx, &parse_section_colors_dark, "dim-blend-towards", 2, + (const char *[]){"black", "white"}, + (int []){DIM_BLEND_TOWARDS_BLACK, DIM_BLEND_TOWARDS_WHITE}, + (int *)&conf.colors_dark.dim_blend_towards); + + for (size_t i = 0; i < 255; i++) { + char key_name[4]; + sprintf(key_name, "%zu", i); + test_color(&ctx, &parse_section_colors_dark, key_name, false, + &conf.colors_dark.table[i]); + } + + test_boolean(&ctx, &parse_section_colors_dark, "blur", &conf.colors_dark.blur); + + test_invalid_key(&ctx, &parse_section_colors_dark, "256"); + + /* TODO: alpha (float in range 0-1, converted to uint16_t) */ + + config_free(&conf); +} + +static void +test_section_colors_light(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "colors-light", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_colors, "invalid-key"); + + test_color(&ctx, &parse_section_colors_light, "foreground", false, &conf.colors_light.fg); + test_color(&ctx, &parse_section_colors_light, "background", false, &conf.colors_light.bg); + test_color(&ctx, &parse_section_colors_light, "regular0", false, &conf.colors_light.table[0]); + test_color(&ctx, &parse_section_colors_light, "regular1", false, &conf.colors_light.table[1]); + test_color(&ctx, &parse_section_colors_light, "regular2", false, &conf.colors_light.table[2]); + test_color(&ctx, &parse_section_colors_light, "regular3", false, &conf.colors_light.table[3]); + test_color(&ctx, &parse_section_colors_light, "regular4", false, &conf.colors_light.table[4]); + test_color(&ctx, &parse_section_colors_light, "regular5", false, &conf.colors_light.table[5]); + test_color(&ctx, &parse_section_colors_light, "regular6", false, &conf.colors_light.table[6]); + test_color(&ctx, &parse_section_colors_light, "regular7", false, &conf.colors_light.table[7]); + test_color(&ctx, &parse_section_colors_light, "bright0", false, &conf.colors_light.table[8]); + test_color(&ctx, &parse_section_colors_light, "bright1", false, &conf.colors_light.table[9]); + test_color(&ctx, &parse_section_colors_light, "bright2", false, &conf.colors_light.table[10]); + test_color(&ctx, &parse_section_colors_light, "bright3", false, &conf.colors_light.table[11]); + test_color(&ctx, &parse_section_colors_light, "bright4", false, &conf.colors_light.table[12]); + test_color(&ctx, &parse_section_colors_light, "bright5", false, &conf.colors_light.table[13]); + test_color(&ctx, &parse_section_colors_light, "bright6", false, &conf.colors_light.table[14]); + test_color(&ctx, &parse_section_colors_light, "bright7", false, &conf.colors_light.table[15]); + test_color(&ctx, &parse_section_colors_light, "dim0", false, &conf.colors_light.dim[0]); + test_color(&ctx, &parse_section_colors_light, "dim1", false, &conf.colors_light.dim[1]); + test_color(&ctx, &parse_section_colors_light, "dim2", false, &conf.colors_light.dim[2]); + test_color(&ctx, &parse_section_colors_light, "dim3", false, &conf.colors_light.dim[3]); + test_color(&ctx, &parse_section_colors_light, "dim4", false, &conf.colors_light.dim[4]); + test_color(&ctx, &parse_section_colors_light, "dim5", false, &conf.colors_light.dim[5]); + test_color(&ctx, &parse_section_colors_light, "dim6", false, &conf.colors_light.dim[6]); + test_color(&ctx, &parse_section_colors_light, "dim7", false, &conf.colors_light.dim[7]); + test_color(&ctx, &parse_section_colors_light, "selection-foreground", false, &conf.colors_light.selection_fg); + test_color(&ctx, &parse_section_colors_light, "selection-background", false, &conf.colors_light.selection_bg); + test_color(&ctx, &parse_section_colors_light, "urls", false, &conf.colors_light.url); + test_two_colors(&ctx, &parse_section_colors_light, "jump-labels", false, + &conf.colors_light.jump_label.fg, + &conf.colors_light.jump_label.bg); + test_two_colors(&ctx, &parse_section_colors_light, "scrollback-indicator", false, + &conf.colors_light.scrollback_indicator.fg, + &conf.colors_light.scrollback_indicator.bg); + test_two_colors(&ctx, &parse_section_colors_light, "search-box-no-match", false, + &conf.colors_light.search_box.no_match.fg, + &conf.colors_light.search_box.no_match.bg); + test_two_colors(&ctx, &parse_section_colors_light, "search-box-match", false, + &conf.colors_light.search_box.match.fg, + &conf.colors_light.search_box.match.bg); + + test_two_colors(&ctx, &parse_section_colors_light, "cursor", false, + &conf.colors_light.cursor.text, + &conf.colors_light.cursor.cursor); + + test_enum(&ctx, &parse_section_colors_light, "alpha-mode", 3, + (const char *[]){"default", "matching", "all"}, + (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, + (int *)&conf.colors_light.alpha_mode); + + test_enum(&ctx, &parse_section_colors_light, "dim-blend-towards", 2, + (const char *[]){"black", "white"}, + (int []){DIM_BLEND_TOWARDS_BLACK, DIM_BLEND_TOWARDS_WHITE}, + (int *)&conf.colors_light.dim_blend_towards); + + for (size_t i = 0; i < 255; i++) { + char key_name[4]; + sprintf(key_name, "%zu", i); + test_color(&ctx, &parse_section_colors_light, key_name, false, + &conf.colors_light.table[i]); + } + + test_boolean(&ctx, &parse_section_colors_light, "blur", &conf.colors_light.blur); + + test_invalid_key(&ctx, &parse_section_colors_light, "256"); + + /* TODO: alpha (float in range 0-1, converted to uint16_t) */ + + config_free(&conf); +} + +static void +test_section_csd(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "csd", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_csd, "invalid-key"); + + test_enum( + &ctx, &parse_section_csd, "preferred", + 3, + (const char *[]){"none", "client", "server"}, + (int []){CONF_CSD_PREFER_NONE, + CONF_CSD_PREFER_CLIENT, + CONF_CSD_PREFER_SERVER}, + (int *)&conf.csd.preferred); + test_uint16(&ctx, &parse_section_csd, "size", &conf.csd.title_height); + test_color(&ctx, &parse_section_csd, "color", true, &conf.csd.color.title); + test_uint16(&ctx, &parse_section_csd, "border-width", + &conf.csd.border_width_visible); + test_color(&ctx, &parse_section_csd, "border-color", true, + &conf.csd.color.border); + test_uint16(&ctx, &parse_section_csd, "button-width", + &conf.csd.button_width); + test_color(&ctx, &parse_section_csd, "button-color", true, + &conf.csd.color.buttons); + test_color(&ctx, &parse_section_csd, "button-minimize-color", true, + &conf.csd.color.minimize); + test_color(&ctx, &parse_section_csd, "button-maximize-color", true, + &conf.csd.color.maximize); + test_color(&ctx, &parse_section_csd, "button-close-color", true, + &conf.csd.color.quit); + test_boolean(&ctx, &parse_section_csd, "hide-when-maximized", + &conf.csd.hide_when_maximized); + test_boolean(&ctx, &parse_section_csd, "double-click-to-maximize", + &conf.csd.double_click_to_maximize); + + /* TODO: verify the ‘set’ bit is actually set for colors */ + /* TODO: font */ + + config_free(&conf); +} + +static bool +have_modifier(const config_modifier_list_t *mods, const char *mod) +{ + tll_foreach(*mods, it) { + if (strcmp(it->item, mod) == 0) + return true; + } + + return false; +} + +static void +test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), + int action, int max_action, const char *const *map, + struct config_key_binding_list *bindings, + enum key_binding_type type, bool need_argv, bool need_section_id) +{ + xassert(map[action] != NULL); + xassert(bindings->count == 0); + + const char *key = map[action]; + + /* “Randomize” which modifiers to enable */ + const bool ctrl = action % 2; + const bool alt = action % 3; + const bool shift = action % 4; + const bool super = action % 5; + const bool argv = need_argv; + const bool section_id = need_section_id; + + xassert(!(argv && section_id)); + + static const char *const args[] = { + "command", "arg1", "arg2", "arg3 has spaces"}; + + /* Generate the modifier part of the ‘value’ */ + char modifier_string[32]; + sprintf(modifier_string, "%s%s%s%s", + ctrl ? XKB_MOD_NAME_CTRL "+" : "", + alt ? XKB_MOD_NAME_ALT "+" : "", + shift ? XKB_MOD_NAME_SHIFT "+" : "", + super ? XKB_MOD_NAME_LOGO "+" : ""); + + /* Use a unique symbol for this action (key bindings) */ + const xkb_keysym_t sym = XKB_KEY_a + action; + + /* Mouse button (mouse bindings) */ + const int button_idx = action % ALEN(button_map); + const int button = button_map[button_idx].code; + const int click_count = action % 3 + 1; + + /* Finally, generate the ‘value’ (e.g. “Control+shift+x”) */ + char value[128] = {0}; + + ctx->key = key; + ctx->value = value; + + /* First, try setting the empty string */ + if (parse_fun(ctx)) { + BUG("[%s].%s=: did not fail to parse as expected", + ctx->section, ctx->key); + } + + switch (type) { + case KEY_BINDING: { + char sym_name[16]; + xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); + + snprintf(value, sizeof(value), "%s%s%s", + argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : section_id ? "[foobar]" : "", + modifier_string, sym_name); + break; + } + + case MOUSE_BINDING: { + const char *const button_name = button_map[button_idx].name; + int chars = snprintf( + value, sizeof(value), "%s%s%s", + argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : section_id ? "[foobar]" : "", + modifier_string, button_name); + + xassert(click_count > 0); + if (click_count > 1) + snprintf(&value[chars], sizeof(value) - chars, "-%d", click_count); + break; + } + } + + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s failed to parse", + ctx->section, ctx->key, ctx->value); + } + + const struct config_key_binding *binding = + &bindings->arr[bindings->count - 1]; + + if (argv) { + if (binding->aux.pipe.args == NULL) { + BUG("[%s].%s=%s: pipe argv is NULL", + ctx->section, ctx->key, ctx->value); + } + + for (size_t i = 0; i < ALEN(args); i++) { + if (binding->aux.pipe.args[i] == NULL || + !streq(binding->aux.pipe.args[i], args[i])) + { + BUG("[%s].%s=%s: pipe argv not the expected one: " + "mismatch of arg #%zu: expected=\"%s\", got=\"%s\"", + ctx->section, ctx->key, ctx->value, i, + args[i], binding->aux.pipe.args[i]); + } + } + + if (binding->aux.pipe.args[ALEN(args)] != NULL) { + BUG("[%s].%s=%s: pipe argv not the expected one: " + "expected NULL terminator at arg #%zu, got=\"%s\"", + ctx->section, ctx->key, ctx->value, + ALEN(args), binding->aux.pipe.args[ALEN(args)]); + } + } else if (section_id) { + if (binding->aux.regex_name == NULL) { + BUG("[%s].%s=%s: regex name is NULL", + ctx->section, ctx->key, ctx->value); + } + + if (!streq(binding->aux.regex_name, "foobar")) { + BUG("[%s].%s=%s: regex name not the expected one: " + "expected=\"%s\", got=\"%s\"", + ctx->section, ctx->key, ctx->value, + "foobar", binding->aux.regex_name); + } + } else { + if (binding->aux.pipe.args != NULL) { + BUG("[%s].%s=%s: pipe argv not NULL", + ctx->section, ctx->key, ctx->value); + } + } + + if (binding->action != action) { + BUG("[%s].%s=%s: action mismatch: %d != %d", + ctx->section, ctx->key, ctx->value, binding->action, action); + } + + bool have_ctrl = have_modifier(&binding->modifiers, XKB_MOD_NAME_CTRL); + bool have_alt = have_modifier(&binding->modifiers, XKB_MOD_NAME_ALT); + bool have_shift = have_modifier(&binding->modifiers, XKB_MOD_NAME_SHIFT); + bool have_super = have_modifier(&binding->modifiers, XKB_MOD_NAME_LOGO); + + if (have_ctrl != ctrl || have_alt != alt || + have_shift != shift || have_super != super) + { + BUG("[%s].%s=%s: modifier mismatch:\n" + " have: ctrl=%d, alt=%d, shift=%d, super=%d\n" + " expected: ctrl=%d, alt=%d, shift=%d, super=%d", + ctx->section, ctx->key, ctx->value, + have_ctrl, have_alt, have_shift, have_super, + ctrl, alt, shift, super); + } + + switch (type) { + case KEY_BINDING: + if (binding->k.sym != sym) { + BUG("[%s].%s=%s: key symbol mismatch: %d != %d", + ctx->section, ctx->key, ctx->value, binding->k.sym, sym); + } + break; + + case MOUSE_BINDING:; + if (binding->m.button != button) { + BUG("[%s].%s=%s: mouse button mismatch: %d != %d", + ctx->section, ctx->key, ctx->value, binding->m.button, button); + } + + if (binding->m.count != click_count) { + BUG("[%s].%s=%s: mouse button click count mismatch: %d != %d", + ctx->section, ctx->key, ctx->value, + binding->m.count, click_count); + } + break; + } + + + free_key_binding_list(bindings); +} + +enum collision_test_mode { + FAIL_DIFFERENT_ACTION, + FAIL_DIFFERENT_ARGV, + FAIL_MOUSE_OVERRIDE, + SUCCEED_SAME_ACTION_AND_ARGV, +}; + +static void +_test_binding_collisions(struct context *ctx, + int max_action, const char *const *map, + enum key_binding_type type, + enum collision_test_mode test_mode) +{ + struct config_key_binding *bindings_array = + xcalloc(2, sizeof(bindings_array[0])); + + struct config_key_binding_list bindings = { + .count = 2, + .arr = bindings_array, + }; + + /* First, verify we get a collision when trying to assign the same + * key combo to multiple actions */ + bindings.arr[0] = (struct config_key_binding){ + .action = (test_mode == FAIL_DIFFERENT_ACTION + ? max_action - 1 : max_action), + .modifiers = tll_init(), + .path = "unittest", + }; + tll_push_back(bindings.arr[0].modifiers, xstrdup(XKB_MOD_NAME_CTRL)); + + bindings.arr[1] = (struct config_key_binding){ + .action = max_action, + .modifiers = tll_init(), + .path = "unittest", + }; + tll_push_back(bindings.arr[1].modifiers, xstrdup(XKB_MOD_NAME_CTRL)); + + switch (type) { + case KEY_BINDING: + bindings.arr[0].k.sym = XKB_KEY_a; + bindings.arr[1].k.sym = XKB_KEY_a; + break; + + case MOUSE_BINDING: + bindings.arr[0].m.button = BTN_LEFT; + bindings.arr[0].m.count = 1; + bindings.arr[1].m.button = BTN_LEFT; + bindings.arr[1].m.count = 1; + break; + } + + switch (test_mode) { + case FAIL_DIFFERENT_ACTION: + break; + + case FAIL_MOUSE_OVERRIDE: + tll_free_and_free(ctx->conf->mouse.selection_override_modifiers, free); + tll_push_back(ctx->conf->mouse.selection_override_modifiers, xstrdup(XKB_MOD_NAME_CTRL)); + break; + + case FAIL_DIFFERENT_ARGV: + case SUCCEED_SAME_ACTION_AND_ARGV: + bindings.arr[0].aux.type = BINDING_AUX_PIPE; + bindings.arr[0].aux.master_copy = true; + bindings.arr[0].aux.pipe.args = xcalloc( + 4, sizeof(bindings.arr[0].aux.pipe.args[0])); + bindings.arr[0].aux.pipe.args[0] = xstrdup("/usr/bin/foobar"); + bindings.arr[0].aux.pipe.args[1] = xstrdup("hello"); + bindings.arr[0].aux.pipe.args[2] = xstrdup("world"); + + bindings.arr[1].aux.type = BINDING_AUX_PIPE; + bindings.arr[1].aux.master_copy = true; + bindings.arr[1].aux.pipe.args = xcalloc( + 4, sizeof(bindings.arr[1].aux.pipe.args[0])); + bindings.arr[1].aux.pipe.args[0] = xstrdup("/usr/bin/foobar"); + bindings.arr[1].aux.pipe.args[1] = xstrdup("hello"); + + if (test_mode == SUCCEED_SAME_ACTION_AND_ARGV) + bindings.arr[1].aux.pipe.args[2] = xstrdup("world"); + break; + } + + bool expected_result = + test_mode == SUCCEED_SAME_ACTION_AND_ARGV ? true : false; + + if (resolve_key_binding_collisions( + ctx->conf, ctx->section, map, &bindings, type) != expected_result) + { + BUG("[%s].%s vs. %s: %s", + ctx->section, map[max_action - 1], map[max_action], + (expected_result == true + ? "invalid key combo collision detected" + : "key combo collision not detected")); + } + + if (expected_result == false) { + if (bindings.count != 1) + BUG("[%s]: colliding binding not removed", ctx->section); + + if (bindings.arr[0].action != + (test_mode == FAIL_DIFFERENT_ACTION ? max_action - 1 : max_action)) + { + BUG("[%s]: wrong binding removed", ctx->section); + } + } + + free_key_binding_list(&bindings); +} + +static void +test_binding_collisions(struct context *ctx, + int max_action, const char *const *map, + enum key_binding_type type) +{ + _test_binding_collisions(ctx, max_action, map, type, FAIL_DIFFERENT_ACTION); + _test_binding_collisions(ctx, max_action, map, type, FAIL_DIFFERENT_ARGV); + _test_binding_collisions(ctx, max_action, map, type, SUCCEED_SAME_ACTION_AND_ARGV); + + if (type == MOUSE_BINDING) { + _test_binding_collisions( + ctx, max_action, map, type, FAIL_MOUSE_OVERRIDE); + } +} + +static void +test_section_key_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "key-bindings", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_key_bindings, "invalid-key"); + + for (int action = 0; action < BIND_ACTION_KEY_COUNT; action++) { + if (binding_action_map[action] == NULL) + continue; + + test_key_binding( + &ctx, &parse_section_key_bindings, + action, BIND_ACTION_KEY_COUNT - 1, + binding_action_map, &conf.bindings.key, KEY_BINDING, + action >= BIND_ACTION_PIPE_SCROLLBACK && action <= BIND_ACTION_PIPE_COMMAND_OUTPUT, + action >= BIND_ACTION_REGEX_LAUNCH && action <= BIND_ACTION_REGEX_COPY); + } + + config_free(&conf); +} + +static void +test_section_key_bindings_collisions(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "key-bindings", .path = "unittest"}; + + test_binding_collisions( + &ctx, BIND_ACTION_KEY_COUNT - 1, binding_action_map, KEY_BINDING); + + config_free(&conf); +} + +static void +test_section_search_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "search-bindings", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_search_bindings, "invalid-key"); + + for (int action = 0; action < BIND_ACTION_SEARCH_COUNT; action++) { + if (search_binding_action_map[action] == NULL) + continue; + + test_key_binding( + &ctx, &parse_section_search_bindings, + action, BIND_ACTION_SEARCH_COUNT - 1, + search_binding_action_map, &conf.bindings.search, KEY_BINDING, + false, false); + } + + config_free(&conf); +} + +static void +test_section_search_bindings_collisions(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "search-bindings", .path = "unittest"}; + + test_binding_collisions( + &ctx, + BIND_ACTION_SEARCH_COUNT - 1, search_binding_action_map, KEY_BINDING); + + config_free(&conf); +} + +static void +test_section_url_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "rul-bindings", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_url_bindings, "invalid-key"); + + for (int action = 0; action < BIND_ACTION_URL_COUNT; action++) { + if (url_binding_action_map[action] == NULL) + continue; + + test_key_binding( + &ctx, &parse_section_url_bindings, + action, BIND_ACTION_URL_COUNT - 1, + url_binding_action_map, &conf.bindings.url, KEY_BINDING, + false, false); + } + + config_free(&conf); +} + +static void +test_section_url_bindings_collisions(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "url-bindings", .path = "unittest"}; + + test_binding_collisions( + &ctx, + BIND_ACTION_URL_COUNT - 1, url_binding_action_map, KEY_BINDING); + + config_free(&conf); +} + +static void +test_section_mouse_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "mouse-bindings", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_mouse_bindings, "invalid-key"); + + for (int action = 0; action < BIND_ACTION_COUNT; action++) { + if (binding_action_map[action] == NULL) + continue; + + test_key_binding( + &ctx, &parse_section_mouse_bindings, + action, BIND_ACTION_COUNT - 1, + binding_action_map, &conf.bindings.mouse, MOUSE_BINDING, + false, false); + } + + config_free(&conf); +} + +static void +test_section_mouse_bindings_collisions(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "mouse-bindings", .path = "unittest"}; + + test_binding_collisions( + &ctx, + BIND_ACTION_COUNT - 1, binding_action_map, MOUSE_BINDING); + + config_free(&conf); +} + +static void +test_section_text_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "text-bindings", .path = "unittest"}; + + ctx.key = "abcd"; + ctx.value = XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT "+x"; + xassert(parse_section_text_bindings(&ctx)); + + ctx.key = "\\x07"; + xassert(parse_section_text_bindings(&ctx)); + + ctx.key = "\\x1g"; + xassert(!parse_section_text_bindings(&ctx)); + + ctx.key = "\\x1"; + xassert(!parse_section_text_bindings(&ctx)); + + ctx.key = "\\x"; + xassert(!parse_section_text_bindings(&ctx)); + + ctx.key = "\\"; + xassert(!parse_section_text_bindings(&ctx)); + + ctx.key = "\\y"; + xassert(!parse_section_text_bindings(&ctx)); + +#if 0 + /* Invalid modifier and key names are detected later, when a + * layout is applied */ + ctx.key = "abcd"; + ctx.value = "InvalidMod+y"; + xassert(!parse_section_text_bindings(&ctx)); +#endif + config_free(&conf); +} + +static void +test_section_environment(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "environment", .path = "unittest"}; + + /* A single variable */ + ctx.key = "FOO"; + ctx.value = "bar"; + xassert(parse_section_environment(&ctx)); + xassert(tll_length(conf.env_vars) == 1); + xassert(streq(tll_front(conf.env_vars).name, "FOO")); + xassert(streq(tll_front(conf.env_vars).value, "bar")); + + /* Add a second variable */ + ctx.key = "BAR"; + ctx.value = "123"; + xassert(parse_section_environment(&ctx)); + xassert(tll_length(conf.env_vars) == 2); + xassert(streq(tll_back(conf.env_vars).name, "BAR")); + xassert(streq(tll_back(conf.env_vars).value, "123")); + + /* Replace the *value* of the first variable */ + ctx.key = "FOO"; + ctx.value = "456"; + xassert(parse_section_environment(&ctx)); + xassert(tll_length(conf.env_vars) == 2); + xassert(streq(tll_front(conf.env_vars).name, "FOO")); + xassert(streq(tll_front(conf.env_vars).value, "456")); + xassert(streq(tll_back(conf.env_vars).name, "BAR")); + xassert(streq(tll_back(conf.env_vars).value, "123")); + + config_free(&conf); +} + +static void +test_section_tweak(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "tweak", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_tweak, "invalid-key"); + + test_enum( + &ctx, &parse_section_tweak, "scaling-filter", + 5, + (const char *[]){"none", "nearest", "bilinear", "cubic", "lanczos3"}, + (int []){FCFT_SCALING_FILTER_NONE, + FCFT_SCALING_FILTER_NEAREST, + FCFT_SCALING_FILTER_BILINEAR, + FCFT_SCALING_FILTER_CUBIC, + FCFT_SCALING_FILTER_LANCZOS3}, + (int *)&conf.tweak.fcft_filter); + + test_boolean(&ctx, &parse_section_tweak, "overflowing-glyphs", + &conf.tweak.overflowing_glyphs); + + test_enum( + &ctx, &parse_section_tweak, "render-timer", + 4, + (const char *[]){"none", "osd", "log", "both"}, + (int []){RENDER_TIMER_NONE, + RENDER_TIMER_OSD, + RENDER_TIMER_LOG, + RENDER_TIMER_BOTH}, + (int *)&conf.tweak.render_timer); + + test_float(&ctx, &parse_section_tweak, "box-drawing-base-thickness", + &conf.tweak.box_drawing_base_thickness); + test_boolean(&ctx, &parse_section_tweak, "box-drawing-solid-shades", + &conf.tweak.box_drawing_solid_shades); + +#if 0 /* Must be less than 16ms */ + test_uint32(&ctx, &parse_section_tweak, "delayed-render-lower", + &conf.tweak.delayed_render_lower_ns); + test_uint32(&ctx, &parse_section_tweak, "delayed-render-upper", + &conf.tweak.delayed_render_upper_ns); +#endif + test_boolean(&ctx, &parse_section_tweak, "damage-whole-window", + &conf.tweak.damage_whole_window); + +#if defined(FOOT_GRAPHEME_CLUSTERING) + test_boolean(&ctx, &parse_section_tweak, "grapheme-shaping", + &conf.tweak.grapheme_shaping); +#else + /* TODO: the setting still exists, but is always forced to ‘false’. */ +#endif + + test_enum( + &ctx, &parse_section_tweak, "grapheme-width-method", + 3, + (const char *[]){"wcswidth", "double-width", "max"}, + (int []){GRAPHEME_WIDTH_WCSWIDTH, + GRAPHEME_WIDTH_DOUBLE, + GRAPHEME_WIDTH_MAX}, + (int *)&conf.tweak.grapheme_width_method); + + test_boolean(&ctx, &parse_section_tweak, "font-monospace-warn", + &conf.tweak.font_monospace_warn); + + test_float(&ctx, &parse_section_tweak, "bold-text-in-bright-amount", + &conf.bold_in_bright.amount); + + test_uint32(&ctx, &parse_section_tweak, "min-stride-alignment", + &conf.tweak.min_stride_alignment); + +#if 0 /* Must be equal to, or less than INT32_MAX */ + test_uint32(&ctx, &parse_section_tweak, "max-shm-pool-size-mb", + &conf.tweak.max_shm_pool_size); +#endif + + config_free(&conf); +} + +int +main(int argc, const char *const *argv) +{ + FcInit(); + log_init(LOG_COLORIZE_AUTO, false, 0, LOG_CLASS_ERROR); + test_section_main(); + test_section_security(); + test_section_bell(); + test_section_desktop_notifications(); + test_section_scrollback(); + test_section_url(); + test_section_cursor(); + test_section_mouse(); + test_section_touch(); + test_section_colors_dark(); + test_section_colors_light(); + test_section_csd(); + test_section_key_bindings(); + test_section_key_bindings_collisions(); + test_section_search_bindings(); + test_section_search_bindings_collisions(); + test_section_url_bindings(); + test_section_url_bindings_collisions(); + test_section_mouse_bindings(); + test_section_mouse_bindings_collisions(); + test_section_text_bindings(); + test_section_environment(); + test_section_tweak(); + log_deinit(); + FcFini(); + return 0; +} diff --git a/themes/aeroroot b/themes/aeroroot new file mode 100644 index 0000000..dbeb2e8 --- /dev/null +++ b/themes/aeroroot @@ -0,0 +1,34 @@ +# -*- conf -*- +# Aero root theme + +[colors-dark] +cursor=1a1a1a 9fd5f5 +foreground=dedeef +background=1a1a1a + +regular0=1a1a1a +regular1=ff3a3a +regular2=3aef3a +regular3=e6e61a +regular4=1a7eff +regular5=df3adf +regular6=3ff0e0 +regular7=dadada + +bright0=5a5a5a +bright1=ffaaaa +bright2=aaf3aa +bright3=f3f35a +bright4=6abaff +bright5=e5aae5 +bright6=aafff0 +bright7=f3f3f3 + +dim0=000000 +dim1=b71a1a +dim2=1ab71a +dim3=b5b50a +dim4=0A4FAA +dim5=a71aa7 +dim6=1AA59F +dim7=a5a5a5 diff --git a/themes/alacritty b/themes/alacritty new file mode 100644 index 0000000..f05683b --- /dev/null +++ b/themes/alacritty @@ -0,0 +1,57 @@ +# -*- conf -*- +# Alacritty + +[colors-dark] +cursor = 181818 d8d8d8 +background= 181818 +foreground= d8d8d8 + +#black +regular0= 181818 + +#red +regular1= ac4242 + +#green +regular2= 90a959 + +#yellow +regular3= f4bf75 + +#blue +regular4= 6a9fb5 + +#magenta +regular5= aa759f + +#cyan +regular6= 75b5aa + +#white/grey +regular7= d8d8d8 + + + +#grey/black +bright0= 6b6b6b + +#red +bright1= c55555 + +#green +bright2= aac474 + +#yellow +bright3= feca88 + +#blue +bright4= 82b8c8 + +#pink +bright5= c28cb8 + +#cyan +bright6= 93d3c3 + +#grey +bright7= f8f8f8 \ No newline at end of file diff --git a/themes/apprentice b/themes/apprentice new file mode 100644 index 0000000..291ab8d --- /dev/null +++ b/themes/apprentice @@ -0,0 +1,25 @@ +# -*- conf -*- +# https://github.com/romainl/Apprentice + +[colors-dark] +cursor=262626 6c6c6c +foreground=bcbcbc +background=262626 +regular0=1c1c1c +regular1=af5f5f +regular2=5f875f +regular3=87875f +regular4=5f87af +regular5=5f5f87 +regular6=5f8787 +regular7=6c6c6c +bright0=444444 +bright1=ff8700 +bright2=87af87 +bright3=ffffaf +bright4=87afd7 +bright5=8787af +bright6=5fafaf +bright7=ffffff +# selection-foreground=bcbcbc +# selection-background=3a3e4e \ No newline at end of file diff --git a/themes/ayu-mirage b/themes/ayu-mirage new file mode 100644 index 0000000..2d9b6b5 --- /dev/null +++ b/themes/ayu-mirage @@ -0,0 +1,26 @@ +# -*- conf -*- +# theme: Ayu Mirage +# description: a theme based on Ayu Mirage for Sublime Text (original: https://github.com/dempfi/ayu) + +[colors-dark] +cursor = ffcc66 665a44 +foreground = cccac2 +background = 242936 + +regular0 = 242936 # black +regular1 = f28779 # red +regular2 = d5ff80 # green +regular3 = ffd173 # yellow +regular4 = 73d0ff # blue +regular5 = dfbfff # magenta +regular6 = 5ccfe6 # cyan +regular7 = cccac2 # white + +bright0 = fcfcfc # bright black +bright1 = f07171 # bright red +bright2 = 86b300 # bright gree +bright3 = f2ae49 # bright yellow +bright4 = 399ee6 # bright blue +bright5 = a37acc # bright magenta +bright6 = 55b4d4 # bright cyan +bright7 = 5c6166 # bright white diff --git a/themes/catppuccin-frappe b/themes/catppuccin-frappe new file mode 100644 index 0000000..3acae60 --- /dev/null +++ b/themes/catppuccin-frappe @@ -0,0 +1,38 @@ +# _*_ conf _*_ +# Catppuccin Frappe + +[colors-dark] +foreground=c6d0f5 +background=303446 + +regular0=51576d +regular1=e78284 +regular2=a6d189 +regular3=e5c890 +regular4=8caaee +regular5=f4b8e4 +regular6=81c8be +regular7=b5bfe2 + +bright0=626880 +bright1=e78284 +bright2=a6d189 +bright3=e5c890 +bright4=8caaee +bright5=f4b8e4 +bright6=81c8be +bright7=a5adce + +cursor=232634 f2d5cf + +16=ef9f76 +17=f2d5cf + +selection-foreground=c6d0f5 +selection-background=4f5369 + +search-box-no-match=232634 e78284 +search-box-match=c6d0f5 414559 + +jump-labels=232634 ef9f76 +urls=8caaee diff --git a/themes/catppuccin-latte b/themes/catppuccin-latte new file mode 100644 index 0000000..ca7a7aa --- /dev/null +++ b/themes/catppuccin-latte @@ -0,0 +1,41 @@ +# _*_ conf _*_ +# Catppuccin Latte + +[main] +initial-color-theme=light + +[colors-light] +foreground=4c4f69 +background=eff1f5 + +regular0=5c5f77 +regular1=d20f39 +regular2=40a02b +regular3=df8e1d +regular4=1e66f5 +regular5=ea76cb +regular6=179299 +regular7=acb0be + +bright0=6c6f85 +bright1=d20f39 +bright2=40a02b +bright3=df8e1d +bright4=1e66f5 +bright5=ea76cb +bright6=179299 +bright7=bcc0cc + +cursor=eff1f5 dc8a78 + +16=fe640b +17=dc8a78 + +selection-foreground=4c4f69 +selection-background=ccced7 + +search-box-no-match=dce0e8 d20f39 +search-box-match=4c4f69 ccd0da + +jump-labels=dce0e8 fe640b +urls=1e66f5 diff --git a/themes/catppuccin-macchiato b/themes/catppuccin-macchiato new file mode 100644 index 0000000..8f5ea36 --- /dev/null +++ b/themes/catppuccin-macchiato @@ -0,0 +1,38 @@ +# _*_ conf _*_ +# Catppuccin Macchiato + +[colors-dark] +foreground=cad3f5 +background=24273a + +regular0=494d64 +regular1=ed8796 +regular2=a6da95 +regular3=eed49f +regular4=8aadf4 +regular5=f5bde6 +regular6=8bd5ca +regular7=b8c0e0 + +bright0=5b6078 +bright1=ed8796 +bright2=a6da95 +bright3=eed49f +bright4=8aadf4 +bright5=f5bde6 +bright6=8bd5ca +bright7=a5adcb + +cursor=181926 f4dbd6 + +16=f5a97f +17=f4dbd6 + +selection-foreground=cad3f5 +selection-background=454a5f + +search-box-no-match=181926 ed8796 +search-box-match=cad3f5 363a4f + +jump-labels=181926 f5a97f +urls=8aadf4 diff --git a/themes/catppuccin-mocha b/themes/catppuccin-mocha new file mode 100644 index 0000000..7d98dc0 --- /dev/null +++ b/themes/catppuccin-mocha @@ -0,0 +1,38 @@ +# _*_ conf _*_ +# Catppuccin Mocha + +[colors-dark] +foreground=cdd6f4 +background=1e1e2e + +regular0=45475a +regular1=f38ba8 +regular2=a6e3a1 +regular3=f9e2af +regular4=89b4fa +regular5=f5c2e7 +regular6=94e2d5 +regular7=bac2de + +bright0=585b70 +bright1=f38ba8 +bright2=a6e3a1 +bright3=f9e2af +bright4=89b4fa +bright5=f5c2e7 +bright6=94e2d5 +bright7=a6adc8 + +cursor=11111b f5e0dc + +16=fab387 +17=f5e0dc + +selection-foreground=cdd6f4 +selection-background=414356 + +search-box-no-match=11111b f38ba8 +search-box-match=cdd6f4 313244 + +jump-labels=11111b fab387 +urls=89b4fa diff --git a/themes/chiba-dark b/themes/chiba-dark new file mode 100644 index 0000000..ffaf6cb --- /dev/null +++ b/themes/chiba-dark @@ -0,0 +1,25 @@ +# -*- conf -*- +# theme: Chiba Dark +# author: ayushnix (https://sr.ht/~ayushnix) +# description: A dark theme with bright cyberpunk colors (WCAG AAA compliant) + +[colors-dark] +cursor = 181818 cdcdcd +foreground = cdcdcd +background = 181818 +regular0 = 181818 +regular1 = ff8599 +regular2 = 00c545 +regular3 = de9d00 +regular4 = 00b4ff +regular5 = fd71f8 +regular6 = 00bfae +regular7 = cdcdcd +bright0 = 262626 +bright1 = ff9eb2 +bright2 = 19de5e +bright3 = f7b619 +bright4 = 19cdff +bright5 = ff8aff +bright6 = 19d8c7 +bright7 = dadada diff --git a/themes/derp b/themes/derp new file mode 100644 index 0000000..42af337 --- /dev/null +++ b/themes/derp @@ -0,0 +1,23 @@ +# -*- conf -*- +# Derp + +[colors-dark] +cursor=000000 ffffff +foreground=ffffff +background=000000 +regular0=111111 +regular1=d36265 +regular2=aece91 +regular3=e7e18c +regular4=5297cf +regular5=963c59 +regular6=5e7175 +regular7=bebebe +bright0=666666 +bright1=ef8171 +bright2=cfefb3 +bright3=fff796 +bright4=74b8ef +bright5=b85e7b +bright6=a3babf +bright7=ffffff diff --git a/themes/deus b/themes/deus new file mode 100644 index 0000000..69c4494 --- /dev/null +++ b/themes/deus @@ -0,0 +1,29 @@ +# -*- conf -*- +# Deus +# Color palette based on: https://github.com/ajmwagar/vim-deus + +[colors-dark] +cursor=2c323b eaeaea +background=2c323b +foreground=eaeaea +regular0=242a32 +regular1=d54e53 +regular2=98c379 +regular3=e5c07b +regular4=83a598 +regular5=c678dd +regular6=70c0ba +regular7=eaeaea +bright0=666666 +bright1=ec3e45 +bright2=90c966 +bright3=edbf69 +bright4=73ba9f +bright5=c858e9 +bright6=2bcec2 +bright7=ffffff + +# Enable if prefer Deus colors instead of inverterd fg/bg for +# highlighting (mouse selection) +# selection-foreground=2c323b +# selection-background=eaeaea diff --git a/themes/dracula b/themes/dracula new file mode 100644 index 0000000..8299420 --- /dev/null +++ b/themes/dracula @@ -0,0 +1,23 @@ +# -*- conf -*- +# Dracula + +[colors-dark] +cursor=282a36 f8f8f2 +foreground=f8f8f2 +background=282a36 +regular0=000000 # black +regular1=ff5555 # red +regular2=50fa7b # green +regular3=f1fa8c # yellow +regular4=bd93f9 # blue +regular5=ff79c6 # magenta +regular6=8be9fd # cyan +regular7=bfbfbf # white +bright0=4d4d4d # bright black +bright1=ff6e67 # bright red +bright2=5af78e # bright green +bright3=f4f99d # bright yellow +bright4=caa9fa # bright blue +bright5=ff92d0 # bright magenta +bright6=9aedfe # bright cyan +bright7=e6e6e6 # bright white \ No newline at end of file diff --git a/themes/dracula-iterm b/themes/dracula-iterm new file mode 100644 index 0000000..b75ddd9 --- /dev/null +++ b/themes/dracula-iterm @@ -0,0 +1,23 @@ +# -*- conf -*- +# Dracula iTerm2 variant + +[colors-dark] +cursor=ffffff bbbbbb +foreground=f8f8f2 +background=1e1f29 +regular0=000000 # black +regular1=ff5555 # red +regular2=50fa7b # green +regular3=f1fa8c # yellow +regular4=bd93f9 # blue +regular5=ff79c6 # magenta +regular6=8be9fd # cyan +regular7=bbbbbb # white +bright0=555555 # bright black +bright1=ff5555 # bright red +bright2=50fa7b # bright green +bright3=f1fa8c # bright yellow +bright4=bd93f9 # bright blue +bright5=ff79c6 # bright magenta +bright6=8be9fd # bright cyan +bright7=ffffff # bright white diff --git a/themes/electrophoretic b/themes/electrophoretic new file mode 100644 index 0000000..8bc022e --- /dev/null +++ b/themes/electrophoretic @@ -0,0 +1,36 @@ +# -*- conf -*- +# Electrophoretic +# Theme for electrophoretic displays (like e-ink) which usually supports +# 16 levels of grays. This theme aims to maximize the contrast between the +# text and the white background. +# author: Eugen Rahaian + +[main] +initial-color-theme=light + +[colors-light] +cursor=ffffff 515151 +background= ffffff +foreground= 000000 + +# The colors are sorted based on their luminance, so we can more easily assign +# them a gray level. +# grayscale order: black_0 blue_4 red_1 magenta_5 green_2 cyan_6 yellow_3 white_7 +regular0= ffffff +regular4= 616161 +regular1= 515151 +regular5= 414141 +regular2= 313131 +regular6= 212121 +regular3= 111111 +regular7= 000000 +# Here, we also stay away from the white background by reusing the dark gray levels +# from above, with small variations +bright0= 818181 +bright4= 717171 +bright1= 616161 +bright5= 515151 +bright2= 414141 +bright6= 313131 +bright3= 212121 +bright7= 111111 diff --git a/themes/gruvbox b/themes/gruvbox new file mode 100644 index 0000000..e44f3ea --- /dev/null +++ b/themes/gruvbox @@ -0,0 +1,42 @@ +# -*- conf -*- +# Gruvbox + +[colors-dark] +background=282828 +foreground=ebdbb2 +regular0=282828 +regular1=cc241d +regular2=98971a +regular3=d79921 +regular4=458588 +regular5=b16286 +regular6=689d6a +regular7=a89984 +bright0=928374 +bright1=fb4934 +bright2=b8bb26 +bright3=fabd2f +bright4=83a598 +bright5=d3869b +bright6=8ec07c +bright7=ebdbb2 + +[colors-light] +background=fbf1c7 +foreground=3c3836 +regular0=fbf1c7 +regular1=cc241d +regular2=98971a +regular3=d79921 +regular4=458588 +regular5=b16286 +regular6=689d6a +regular7=7c6f64 +bright0=928374 +bright1=9d0006 +bright2=79740e +bright3=b57614 +bright4=076678 +bright5=8f3f71 +bright6=427b58 +bright7=3c3836 diff --git a/themes/gruvbox-dark b/themes/gruvbox-dark new file mode 100644 index 0000000..c5dadcc --- /dev/null +++ b/themes/gruvbox-dark @@ -0,0 +1,22 @@ +# -*- conf -*- +# Gruvbox + +[colors-dark] +background=282828 +foreground=ebdbb2 +regular0=282828 +regular1=cc241d +regular2=98971a +regular3=d79921 +regular4=458588 +regular5=b16286 +regular6=689d6a +regular7=a89984 +bright0=928374 +bright1=fb4934 +bright2=b8bb26 +bright3=fabd2f +bright4=83a598 +bright5=d3869b +bright6=8ec07c +bright7=ebdbb2 diff --git a/themes/gruvbox-light b/themes/gruvbox-light new file mode 100644 index 0000000..6b61661 --- /dev/null +++ b/themes/gruvbox-light @@ -0,0 +1,25 @@ +# -*- conf -*- +# Gruvbox - Light + +[main] +initial-color-theme=light + +[colors-light] +background=fbf1c7 +foreground=3c3836 +regular0=fbf1c7 +regular1=cc241d +regular2=98971a +regular3=d79921 +regular4=458588 +regular5=b16286 +regular6=689d6a +regular7=7c6f64 +bright0=928374 +bright1=9d0006 +bright2=79740e +bright3=b57614 +bright4=076678 +bright5=8f3f71 +bright6=427b58 +bright7=3c3836 diff --git a/themes/hacktober b/themes/hacktober new file mode 100644 index 0000000..ecdb18f --- /dev/null +++ b/themes/hacktober @@ -0,0 +1,22 @@ +# -*- conf -*- + +[colors-dark] +cursor=141414 c9c9c9 +foreground=c9c9c9 +background=141414 +regular0=191918 # black +regular1=b34538 # red +regular2=587744 # green +regular3=d08949 # yellow +regular4=206ec5 # blue +regular5=864651 # magenta +regular6=ac9166 # cyan +regular7=f1eee7 # white +bright0=2c2b2a # bright black +bright1=b33323 # bright red +bright2=42824a # bright green +bright3=c75a22 # bright yellow +bright4=5389c5 # bright blue +bright5=e795a5 # bright magenta +bright6=ebc587 # bright cyan +bright7=ffffff # bright white diff --git a/themes/iterm b/themes/iterm new file mode 100644 index 0000000..c5ffc19 --- /dev/null +++ b/themes/iterm @@ -0,0 +1,27 @@ +# -*- conf -*- +# this foot theme is based on alacritty iterm theme: +# https://github.com/alacritty/alacritty-theme/blob/master/themes/iterm.toml + +[colors-dark] +foreground=fffbf6 +background=101421 + +## Normal/regular colors (color palette 0-7) +regular0=2e2e2e # black +regular1=eb4129 # red +regular2=abe047 # green +regular3=f6c744 # yellow +regular4=47a0f3 # blue +regular5=7b5cb0 # magenta +regular6=64dbed # cyan +regular7=e5e9f0 # white + +## Bright colors (color palette 8-15) +bright0=565656 # bright black +bright1=ec5357 # bright red +bright2=c0e17d # bright green +bright3=f9da6a # bright yellow +bright4=49a4f8 # bright blue +bright5=a47de9 # bright magenta +bright6=99faf2 # bright cyan +bright7=ffffff # bright white diff --git a/themes/jetbrains-darcula b/themes/jetbrains-darcula new file mode 100644 index 0000000..0092b79 --- /dev/null +++ b/themes/jetbrains-darcula @@ -0,0 +1,26 @@ +# -*- conf -*- +# JetBrains Darcula +# Palette based on the same theme from https://github.com/dexpota/kitty-themes + +[colors-dark] +cursor=202020 ffffff +background=202020 +foreground=adadad +regular0=000000 # black +regular1=fa5355 # red +regular2=126e00 # green +regular3=c2c300 # yellow +regular4=4581eb # blue +regular5=fa54ff # magenta +regular6=33c2c1 # cyan +regular7=adadad # white +bright0=545454 # bright black +bright1=fb7172 # bright red +bright2=67ff4f # bright green +bright3=ffff00 # bright yellow +bright4=6d9df1 # bright blue +bright5=fb82ff # bright magenta +bright6=60d3d1 # bright cyan +bright7=eeeeee # bright white +# selection-foreground=202020 +# selection-background=1a3272 diff --git a/themes/kitty b/themes/kitty new file mode 100644 index 0000000..81fd003 --- /dev/null +++ b/themes/kitty @@ -0,0 +1,22 @@ +# -*- conf -*- + +[colors-dark] +cursor=111111 cccccc +foreground=dddddd +background=000000 +regular0=000000 # black +regular1=cc0403 # red +regular2=19cb00 # green +regular3=cecb00 # yellow +regular4=0d73cc # blue +regular5=cb1ed1 # magenta +regular6=0dcdcd # cyan +regular7=dddddd # white +bright0=767676 # bright black +bright1=f2201f # bright red +bright2=23fd00 # bright green +bright3=fffd00 # bright yellow +bright4=1a8fff # bright blue +bright5=fd28ff # bright magenta +bright6=14ffff # bright cyan +bright7=ffffff # bright white diff --git a/themes/material-amber b/themes/material-amber new file mode 100644 index 0000000..69126aa --- /dev/null +++ b/themes/material-amber @@ -0,0 +1,41 @@ +# -*- conf -*- +# Material Amber +# Based on material.io guidelines with Amber 50 background + +[main] +initial-color-theme=light + +[colors-light] +cursor=fff8e1 21201d +foreground = 21201d +background = fff8e1 + +regular0 = 21201d # black +regular1 = cd4340 # red +regular2 = 498d49 # green +regular3 = fab32d # yellow +regular4 = 3378c4 # blue +regular5 = b83269 # magenta +regular6 = 21929a # cyan +regular7 = ffd7d7 # white + +bright0 = 66635a # bright black +bright1 = dd7b72 # bright red +bright2 = 82ae78 # bright green +bright3 = fbc870 # bright yellow +bright4 = 73a0cd # bright blue +bright5 = ce6f8e # bright magenta +bright6 = 548c94 # bright cyan +bright7 = ffe1da # bright white + +dim0 = 9e9a8c # dim black +dim1 = e9a99b # dim red +dim2 = b0c99f # dim green +dim3 = fdda9a # dim yellow +dim4 = a6c0d4 # dim blue +dim5 = e0a1ad # dim magenta +dim6 = 3c6064 # dim cyan +dim7 = ffe9dd # dim white + +# selection-foreground=fff8e1 +# selection-background=21201d diff --git a/themes/material-design b/themes/material-design new file mode 100644 index 0000000..bf1d0a6 --- /dev/null +++ b/themes/material-design @@ -0,0 +1,25 @@ +# -*- conf -*- +# Material +# From https://github.com/MartinSeeler/iterm2-material-design + +[colors-dark] +foreground=ECEFF1 +background=263238 +regular0=546E7A # black +regular1=FF5252 # red +regular2=5CF19E # green +regular3=FFD740 # yellow +regular4=40C4FF # blue +regular5=FF4081 # magenta +regular6=64FCDA # cyan +regular7=FFFFFF # white +bright0=B0BEC5 # bright black +bright1=FF8A80 # bright red +bright2=B9F6CA # bright green +bright3=FFE57F # bright yellow +bright4=80D8FF # bright blue +bright5=FF80AB # bright magenta +bright6=A7FDEB # bright cyan +bright7=FFFFFF # bright white +# selection-foreground=ECEFF1 +# selection-background=607D8B diff --git a/themes/modus-operandi b/themes/modus-operandi new file mode 100644 index 0000000..6baca2f --- /dev/null +++ b/themes/modus-operandi @@ -0,0 +1,30 @@ +# -*- conf -*- +# +# modus-operandi +# See: https://protesilaos.com/emacs/modus-themes +# + +[main] +initial-color-theme=light + +[colors-light] +background=ffffff +foreground=000000 +regular0=000000 +regular1=a60000 +regular2=005e00 +regular3=813e00 +regular4=0031a9 +regular5=721045 +regular6=00538b +regular7=bfbfbf +bright0=595959 +bright1=972500 +bright2=315b00 +bright3=70480f +bright4=2544bb +bright5=5317ac +bright6=005a5f +bright7=ffffff + +jump-labels=dce0e8 0000ff diff --git a/themes/modus-vivendi b/themes/modus-vivendi new file mode 100644 index 0000000..9ee670e --- /dev/null +++ b/themes/modus-vivendi @@ -0,0 +1,25 @@ +# -*- conf -*- +# +# modus-vivendi +# See: https://protesilaos.com/emacs/modus-themes +# + +[colors-dark] +background=000000 +foreground=ffffff +regular0=000000 +regular1=ff8059 +regular2=44bc44 +regular3=d0bc00 +regular4=2fafff +regular5=feacd0 +regular6=00d3d0 +regular7=bfbfbf +bright0=595959 +bright1=ef8b50 +bright2=70b900 +bright3=c0c530 +bright4=79a8ff +bright5=b6a0ff +bright6=6ae4b9 +bright7=ffffff diff --git a/themes/modus-vivendi-tinted b/themes/modus-vivendi-tinted new file mode 100644 index 0000000..6a61fc7 --- /dev/null +++ b/themes/modus-vivendi-tinted @@ -0,0 +1,25 @@ +# -*- conf -*- +# +# modus-vivendi-tinted +# See: https://protesilaos.com/emacs/modus-themes +# + +[colors-dark] +background=0d0e1c +foreground=ffffff +regular0=000000 +regular1=ff5f59 +regular2=44bc44 +regular3=d0bc00 +regular4=2fafff +regular5=feacd0 +regular6=00d3d0 +regular7=a6a6a6 +bright0=595959 +bright1=ff6b55 +bright2=ff6b55 +bright3=fec43f +bright4=fec43f +bright5=b6a0ff +bright6=6ae4b9 +bright7=777777 diff --git a/themes/molokai b/themes/molokai new file mode 100644 index 0000000..19e1b6f --- /dev/null +++ b/themes/molokai @@ -0,0 +1,23 @@ +# -*- conf -*- +# Molokai +# Based on zhou13's at https://github.com/zhou13/molokai-terminal/blob/master/xterm/Xresources + +[colors-dark] +background=1B1D1E +foreground=CCCCCC +regular0=1B1D1E +regular1=FF0044 +regular2=82B414 +regular3=FD971F +regular4=266C98 +regular5=AC0CB1 +regular6=AE81FF +regular7=CCCCCC +bright0=808080 +bright1=F92672 +bright2=A6E22E +bright3=E6DB74 +bright4=7070F0 +bright5=D63AE1 +bright6=66D9EF +bright7=F8F8F2 diff --git a/themes/monokai-pro b/themes/monokai-pro new file mode 100644 index 0000000..3044da9 --- /dev/null +++ b/themes/monokai-pro @@ -0,0 +1,22 @@ +# -*- conf -*- +# Monokai Pro + +[colors-dark] +background=2D2A2E +foreground=FCFCFA +regular0=403E41 +regular1=FF6188 +regular2=A9DC76 +regular3=FFD866 +regular4=FC9867 +regular5=AB9DF2 +regular6=78DCE8 +regular7=FCFCFA +bright0=727072 +bright1=FF6188 +bright2=A9DC76 +bright3=FFD866 +bright4=FC9867 +bright5=AB9DF2 +bright6=78DCE8 +bright7=FCFCFA diff --git a/themes/moonfly b/themes/moonfly new file mode 100644 index 0000000..b30e315 --- /dev/null +++ b/themes/moonfly @@ -0,0 +1,29 @@ +# -*- conf -*- +# moonfly +# Based on https://github.com/bluz71/vim-moonfly-colors + +[colors-dark] +cursor = 080808 9e9e9e +foreground = b2b2b2 +background = 080808 + +# selection-foreground = 080808 +# selection-background = b2ceee + +regular0 = 323437 +regular1 = ff5454 +regular2 = 8cc85f +regular3 = e3c78a +regular4 = 80a0ff +regular5 = d183e8 +regular6 = 79dac8 +regular7 = c6c6c6 + +bright0 = 949494 +bright1 = ff5189 +bright2 = 36c692 +bright3 = c2c292 +bright4 = 74b2ff +bright5 = ae81ff +bright6 = 85dc85 +bright7 = e4e4e4 diff --git a/themes/neon b/themes/neon new file mode 100644 index 0000000..74884e0 --- /dev/null +++ b/themes/neon @@ -0,0 +1,27 @@ +# +# vim: ft=dosini +# +# Neon +# +# https://xcolors.net/neon +# + +[colors-dark] +foreground=f8f8f8 +background=171717 +regular0=171717 +regular1=d81765 +regular2=97d01a +regular3=ffa800 +regular4=16b1fb +regular5=ff2491 +regular6=0fdcb6 +regular7=ebebeb +bright0=38252c +bright1=ff0000 +bright2=76b639 +bright3=e1a126 +bright4=289cd5 +bright5=ff2491 +bright6=0a9b81 +bright7=f8f8f8 diff --git a/themes/night-owl b/themes/night-owl new file mode 100644 index 0000000..e9e4040 --- /dev/null +++ b/themes/night-owl @@ -0,0 +1,28 @@ +# _*_ conf _*_ +# Night Owl + +[colors-dark] +cursor=011627 80a4c2 +foreground=d6deeb +background=011627 + +regular0=011627 +regular1=ef5350 +regular2=22da6e +regular3=addb67 +regular4=82aaff +regular5=c792ea +regular6=21c7a8 +regular7=ffffff + +bright0=575656 +bright1=ef5350 +bright2=22da6e +bright3=ffeb95 +bright4=82aaff +bright5=c792ea +bright6=7fdbca +bright7=ffffff + +selection-background=5f7e97 +selection-foreground=dfe5ee diff --git a/themes/nightfly b/themes/nightfly new file mode 100644 index 0000000..ccdd183 --- /dev/null +++ b/themes/nightfly @@ -0,0 +1,29 @@ +# -*- conf -*- +# nightfly +# Based on https://github.com/bluz71/vim-nightfly-guicolors + +[colors-dark] +cursor = 080808 9ca1aa +foreground = acb4c2 +background = 011627 + +# selection-foreground = 080808 +# selection-background = b2ceee + +regular0 = 1d3b53 +regular1 = fc514e +regular2 = a1cd5e +regular3 = e3d18a +regular4 = 82aaff +regular5 = c792ea +regular6 = 7fdbca +regular7 = a1aab8 + +bright0 = 7c8f8f +bright1 = ff5874 +bright2 = 21c7a8 +bright3 = ecc48d +bright4 = 82aaff +bright5 = ae81ff +bright6 = ae81ff +bright7 = d6deeb diff --git a/themes/noirblaze b/themes/noirblaze new file mode 100644 index 0000000..b21055a --- /dev/null +++ b/themes/noirblaze @@ -0,0 +1,29 @@ +# -*- conf -*- +# noirblaze-kitty +# https://github.com/n1ghtmare/noirblaze-kitty + + +[colors-dark] +cursor=121212 ff0088 +foreground=d5d5d5 +background=121212 + +# selection-foreground=121212 +# selection-background=b0b0b0 + +regular0=121212 # black +regular1=ff0088 # red +regular2=00ff77 # green +regular3=ffffff # yellow +regular4=b0b0b0 # blue +regular5=7a7a7a # magenta +regular6=787878 # cyan +regular7=d5d5d5 # white +bright0=737373 # bright black +bright1=FD319E # bright red +bright2=FD319E # bright green +bright3=FDFDFD # bright yellow +bright4=BEBEBE # bright blue +bright5=939393 # bright magenta +bright6=919191 # bright cyan +bright7=f5f5f5 # bright white diff --git a/themes/nord b/themes/nord new file mode 100644 index 0000000..eb2fdf0 --- /dev/null +++ b/themes/nord @@ -0,0 +1,42 @@ +# -*- conf -*- +# theme: Nord +# author: Arctic Ice Studio , Sven Greb +# description: „Nord“ — An arctic, north-bluish color palette +# +# this specific foot theme is based on nord-alacritty: +# https://github.com/arcticicestudio/nord-alacritty/blob/develop/src/nord.yml + +[colors-dark] +cursor = 2e3440 d8dee9 +foreground = d8dee9 +background = 2e3440 + +# selection-foreground = d8dee9 +# selection-background = 4c566a + +regular0 = 3b4252 +regular1 = bf616a +regular2 = a3be8c +regular3 = ebcb8b +regular4 = 81a1c1 +regular5 = b48ead +regular6 = 88c0d0 +regular7 = e5e9f0 + +bright0 = 4c566a +bright1 = bf616a +bright2 = a3be8c +bright3 = ebcb8b +bright4 = 81a1c1 +bright5 = b48ead +bright6 = 8fbcbb +bright7 = eceff4 + +dim0 = 373e4d +dim1 = 94545d +dim2 = 809575 +dim3 = b29e75 +dim4 = 68809a +dim5 = 8c738c +dim6 = 6d96a5 +dim7 = aeb3bb diff --git a/themes/nordiq b/themes/nordiq new file mode 100644 index 0000000..1efccba --- /dev/null +++ b/themes/nordiq @@ -0,0 +1,24 @@ +# -*- conf -*- +# Nordiq + +[colors-dark] +cursor=eeeeee 9f515a +foreground=dbdee9 +background=0e1420 +regular0=5b6272 +regular1=bf616a +regular2=a3be8c +regular3=ebcb8b +regular4=81a1c1 +regular5=b48ead +regular6=88c0d0 +regular7=e5e9f0 +bright0=4c566a +bright1=bf616a +bright2=a3be8c +bright3=ebcb8b +bright4=81a1c1 +bright5=b48ead +bright6=8fbcbb +bright7=eceff4 + diff --git a/themes/nvim b/themes/nvim new file mode 100644 index 0000000..74dd1ac --- /dev/null +++ b/themes/nvim @@ -0,0 +1,56 @@ +# -*- conf -*- +# Neovim Dark theme +# Uses the dark color palette from the default Neovim color scheme +# See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L419 + +[colors-dark] +cursor=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 +foreground=e0e2ea # NvimLightGrey2 +background=14161b # NvimDarkGrey2 + +selection-foreground=e0e2ea # NvimLightGrey2 +selection-background=4f5258 # NvimDarkGrey4 + +regular0=07080d # NvimDarkGrey1 +regular1=ffc0b9 # NvimLightRed +regular2=b3f6c0 # NvimLightGreen +regular3=fce094 # NvimLightYellow +regular4=a6dbff # NvimLightBlue +regular5=ffcaff # NvimLightMagenta +regular6=8cf8f7 # NvimLightCyan +regular7=c4c6cd # NvimLightGrey3 + +bright0=2c2e33 # NvimDarkGrey3 +bright1=ffc0b9 # NvimLightRed +bright2=b3f6c0 # NvimLightGreen +bright3=fce094 # NvimLightYellow +bright4=a6dbff # NvimLightBlue +bright5=ffcaff # NvimLightMagenta +bright6=8cf8f7 # NvimLightCyan +bright7=eef1f8 # NvimLightGrey1 + +[colors-light] +cursor=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 +foreground=14161b # NvimDarkGrey2 +background=e0e2ea # NvimLightGrey2 + +selection-foreground=14161b # NvimDarkGrey2 +selection-background=9b9ea4 # NvimLightGrey4 + +regular0=eef1f8 # NvimLightGrey1 +regular1=590008 # NvimDarkRed +regular2=005523 # NvimDarkGreen +regular3=6b5300 # NvimDarkYellow +regular4=004c73 # NvimDarkBlue +regular5=470045 # NvimDarkMagenta +regular6=007373 # NvimDarkCyan +regular7=2c2e33 # NvimDarkGrey3 + +bright0=c4c6cd # NvimLightGrey3 +bright1=590008 # NvimDarkRed +bright2=005523 # NvimDarkGreen +bright3=6b5300 # NvimDarkYellow +bright4=004c73 # NvimDarkBlue +bright5=470045 # NvimDarkMagenta +bright6=007373 # NvimDarkCyan +bright7=07080d # NvimDarkGrey1 diff --git a/themes/nvim-dark b/themes/nvim-dark new file mode 100644 index 0000000..fe3afb7 --- /dev/null +++ b/themes/nvim-dark @@ -0,0 +1,30 @@ +# -*- conf -*- +# Neovim Dark theme +# Uses the dark color palette from the default Neovim color scheme +# See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L419 + +[colors-dark] +cursor=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 +foreground=e0e2ea # NvimLightGrey2 +background=14161b # NvimDarkGrey2 + +selection-foreground=e0e2ea # NvimLightGrey2 +selection-background=4f5258 # NvimDarkGrey4 + +regular0=07080d # NvimDarkGrey1 +regular1=ffc0b9 # NvimLightRed +regular2=b3f6c0 # NvimLightGreen +regular3=fce094 # NvimLightYellow +regular4=a6dbff # NvimLightBlue +regular5=ffcaff # NvimLightMagenta +regular6=8cf8f7 # NvimLightCyan +regular7=c4c6cd # NvimLightGrey3 + +bright0=2c2e33 # NvimDarkGrey3 +bright1=ffc0b9 # NvimLightRed +bright2=b3f6c0 # NvimLightGreen +bright3=fce094 # NvimLightYellow +bright4=a6dbff # NvimLightBlue +bright5=ffcaff # NvimLightMagenta +bright6=8cf8f7 # NvimLightCyan +bright7=eef1f8 # NvimLightGrey1 diff --git a/themes/nvim-light b/themes/nvim-light new file mode 100644 index 0000000..fd8943b --- /dev/null +++ b/themes/nvim-light @@ -0,0 +1,33 @@ +# -*- conf -*- +# Neovim Light theme +# Uses the light color palette from the default Neovim color scheme +# See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L334 + +[main] +initial-color-theme=light + +[colors-light] +cursor=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 +foreground=14161b # NvimDarkGrey2 +background=e0e2ea # NvimLightGrey2 + +selection-foreground=14161b # NvimDarkGrey2 +selection-background=9b9ea4 # NvimLightGrey4 + +regular0=eef1f8 # NvimLightGrey1 +regular1=590008 # NvimDarkRed +regular2=005523 # NvimDarkGreen +regular3=6b5300 # NvimDarkYellow +regular4=004c73 # NvimDarkBlue +regular5=470045 # NvimDarkMagenta +regular6=007373 # NvimDarkCyan +regular7=2c2e33 # NvimDarkGrey3 + +bright0=c4c6cd # NvimLightGrey3 +bright1=590008 # NvimDarkRed +bright2=005523 # NvimDarkGreen +bright3=6b5300 # NvimDarkYellow +bright4=004c73 # NvimDarkBlue +bright5=470045 # NvimDarkMagenta +bright6=007373 # NvimDarkCyan +bright7=07080d # NvimDarkGrey1 diff --git a/themes/onedark b/themes/onedark new file mode 100644 index 0000000..6d66e87 --- /dev/null +++ b/themes/onedark @@ -0,0 +1,25 @@ +# OneDark +# Palette based on the same theme from https://github.com/dexpota/kitty-themes + +[colors-dark] +cursor=111111 cccccc +foreground=979eab +background=282c34 +regular0=282c34 # black +regular1=e06c75 # red +regular2=98c379 # green +regular3=e5c07b # yellow +regular4=61afef # blue +regular5=be5046 # magenta +regular6=56b6c2 # cyan +regular7=979eab # white +bright0=393e48 # bright black +bright1=d19a66 # bright red +bright2=56b6c2 # bright green +bright3=e5c07b # bright yellow +bright4=61afef # bright blue +bright5=be5046 # bright magenta +bright6=56b6c2 # bright cyan +bright7=abb2bf # bright white +# selection-foreground=282c34 +# selection-background=979eab diff --git a/themes/onehalf-dark b/themes/onehalf-dark new file mode 100644 index 0000000..1faca45 --- /dev/null +++ b/themes/onehalf-dark @@ -0,0 +1,37 @@ +# theme: One Half - dark version +# author: Son A. Pham +# repo: https://github.com/sonph/onehalf +# +# foot theme is based mainly on values from this specific file: +# https://github.com/sonph/onehalf/blob/master/kitty/onehalf-dark.conf +# + cursor colors from: +# https://github.com/sonph/onehalf/blob/master/iterm/OneHalfDark.itermcolors + +[colors-dark] +cursor=dcdfe4 a3b3cc +foreground=dcdfe4 +background=282c34 +regular0=282c34 # black +regular1=e06c75 # red +regular2=98c379 # green +regular3=e5c07b # yellow +regular4=61afef # blue +regular5=c678dd # magenta +regular6=56b6c2 # cyan +regular7=dcdfe4 # white +bright0=5d677a # bright black +bright1=e06c75 # bright red +bright2=98c379 # bright green +bright3=e5c07b # bright yellow +bright4=61afef # bright blue +bright5=c678dd # bright magenta +bright6=56b6c2 # bright cyan +bright7=dcdfe4 # bright white + +# Enable if prefer theme color for URL underline +# urls=0087bd + +# Enable if prefer theme colors instead of inverterd fg/bg for +# highlighting (mouse selection) +# selection-foreground=000000 +# selection-background=fffacd diff --git a/themes/panda b/themes/panda new file mode 100644 index 0000000..2c1dc7c --- /dev/null +++ b/themes/panda @@ -0,0 +1,27 @@ +# -*- conf -*- +# http://panda.siamak.me/ + +[colors-dark] +# alpha=1.0 +background=1D1E20 +foreground=F0F0F0 + +## Normal/regular colors (color palette 0-7) +regular0=1F1F20 # black +regular1=FB055A # red +regular2=26FFD4 # green +regular3=26FFD4 # yellow +regular4=5C9FFF # blue +regular5=FC59A6 # magenta +regular6=26FFD4 # cyan +regular7=F0F0F0 # white + +## Bright colors (color palette 8-15) +bright0=5C6370 # bright black +bright1=FB055A # bright red +bright2=26FFD4 # bright green +bright3=FEBE7E # bright yellow +bright4=55ADFF # bright blue +bright5=FD95D0 # bright magenta +bright6=26FFD4 # bright cyan +bright7=F0F0F0 # bright white \ No newline at end of file diff --git a/themes/paper-color b/themes/paper-color new file mode 100644 index 0000000..0993492 --- /dev/null +++ b/themes/paper-color @@ -0,0 +1,49 @@ +# -*- conf -*- +# PaperColorDark +# Palette based on https://github.com/NLKNguyen/papercolor-theme + +[colors-dark] +cursor=1c1c1c eeeeee +background=1c1c1c +foreground=eeeeee +regular0=1c1c1c # black +regular1=af005f # red +regular2=5faf00 # green +regular3=d7af5f # yellow +regular4=5fafd7 # blue +regular5=808080 # magenta +regular6=d7875f # cyan +regular7=d0d0d0 # white +bright0=bcbcbc # bright black +bright1=5faf5f # bright red +bright2=afd700 # bright green +bright3=af87d7 # bright yellow +bright4=ffaf00 # bright blue +bright5=ff5faf # bright magenta +bright6=00afaf # bright cyan +bright7=5f8787 # bright white +# selection-foreground=1c1c1c +# selection-background=af87d7 + +[colors-light] +cursor=eeeeee 444444 +background=eeeeee +foreground=444444 +regular0=eeeeee # black +regular1=af0000 # red +regular2=008700 # green +regular3=5f8700 # yellow +regular4=0087af # blue +regular5=878787 # magenta +regular6=005f87 # cyan +regular7=764e37 # white +bright0=bcbcbc # bright black +bright1=d70000 # bright red +bright2=d70087 # bright green +bright3=8700af # bright yellow +bright4=d75f00 # bright blue +bright5=d75f00 # bright magenta +bright6=4c7a5d # bright cyan +bright7=005faf # bright white +# selection-foreground=eeeeee +# selection-background=0087af diff --git a/themes/paper-color-dark b/themes/paper-color-dark new file mode 100644 index 0000000..26260c6 --- /dev/null +++ b/themes/paper-color-dark @@ -0,0 +1,26 @@ +# -*- conf -*- +# PaperColorDark +# Palette based on https://github.com/NLKNguyen/papercolor-theme + +[colors-dark] +cursor=1c1c1c eeeeee +background=1c1c1c +foreground=eeeeee +regular0=1c1c1c # black +regular1=af005f # red +regular2=5faf00 # green +regular3=d7af5f # yellow +regular4=5fafd7 # blue +regular5=808080 # magenta +regular6=d7875f # cyan +regular7=d0d0d0 # white +bright0=bcbcbc # bright black +bright1=5faf5f # bright red +bright2=afd700 # bright green +bright3=af87d7 # bright yellow +bright4=ffaf00 # bright blue +bright5=ff5faf # bright magenta +bright6=00afaf # bright cyan +bright7=5f8787 # bright white +# selection-foreground=1c1c1c +# selection-background=af87d7 diff --git a/themes/paper-color-light b/themes/paper-color-light new file mode 100644 index 0000000..554aabc --- /dev/null +++ b/themes/paper-color-light @@ -0,0 +1,29 @@ +# -*- conf -*- +# PaperColor Light +# Palette based on https://github.com/NLKNguyen/papercolor-theme + +[main] +initial-color-theme=light + +[colors-light] +cursor=eeeeee 444444 +background=eeeeee +foreground=444444 +regular0=eeeeee # black +regular1=af0000 # red +regular2=008700 # green +regular3=5f8700 # yellow +regular4=0087af # blue +regular5=878787 # magenta +regular6=005f87 # cyan +regular7=764e37 # white +bright0=bcbcbc # bright black +bright1=d70000 # bright red +bright2=d70087 # bright green +bright3=8700af # bright yellow +bright4=d75f00 # bright blue +bright5=d75f00 # bright magenta +bright6=4c7a5d # bright cyan +bright7=005faf # bright white +# selection-foreground=eeeeee +# selection-background=0087af diff --git a/themes/poimandres b/themes/poimandres new file mode 100644 index 0000000..a2123ac --- /dev/null +++ b/themes/poimandres @@ -0,0 +1,28 @@ +# Based on Poimandres color theme for kitti terminal emulator +# https://github.com/ubmit/poimandres-kitty + +[colors-dark] +cursor=1b1e28 ffffff +foreground=a6accd +background=1b1e28 + +regular0=1b1e28 +regular1=d0679d +regular2=5de4c7 +regular3=fffac2 +regular4=89ddff +regular5=fcc5e9 +regular6=add7ff +regular7=ffffff + +bright0=a6accd +bright1=d0679d +bright2=5de4c7 +bright3=fffac2 +bright4=add7ff +bright5=fae4fc +bright6=89ddff +bright7=ffffff + +selection-background=28344a +selection-foreground=a6accd diff --git a/themes/rezza b/themes/rezza new file mode 100644 index 0000000..62a08cc --- /dev/null +++ b/themes/rezza @@ -0,0 +1,38 @@ +# -*- conf -*- +# theme: rezza +# author: Doug Whiteley (rezza) +# original URL: http://metawire.org/~rezza/index.php +# currently available via: https://web.archive.org/web/20060207133656/http://metawire.org/~rezza/index.php?page=Configs +# this palette was also posted here: +# https://bbs.archlinux.org/viewtopic.php?id=17322 +# +# more description: +# original colors are similar to `phrakture` (https://github.com/f4cket/everycolorschemeivefound/blob/master/phrakture) +# Note: there is also a slightly modified (earlier?) version (also called rezza) which can be found here: +# https://sheet.shiar.nl/source/data/termcol-xcolor.inc.pl +# and also posted here: +# https://forums.debian.net/viewtopic.php?t=29981 + +[colors-dark] +foreground = cccccc +background = 191911 + +# Normal colors +regular0 = 222222 +regular1 = 803232 +regular2 = 5b762f +regular3 = aa9943 +regular4 = 324c80 +regular5 = 706c9a +regular6 = 92b19e +regular7 = ffffff + +# Bright colors +bright0 = 222222 +bright1 = 982b2b +bright2 = 89b83f +bright3 = efef60 +bright4 = 2b4f98 +bright5 = 826ab1 +bright6 = a1cdcd +bright7 = dedede diff --git a/themes/rose-pine b/themes/rose-pine new file mode 100644 index 0000000..b9aa7e2 --- /dev/null +++ b/themes/rose-pine @@ -0,0 +1,28 @@ +# -*- conf -*- +# Rosé Pine + +[colors-dark] +cursor=191724 e0def4 +background=191724 +foreground=e0def4 + +regular0=26233a # black (Overlay) +regular1=eb6f92 # red (Love) +regular2=9ccfd8 # green (Foam) +regular3=f6c177 # yellow (Gold) +regular4=31748f # blue (Pine) +regular5=c4a7e7 # magenta (Iris) +regular6=ebbcba # cyan (Rose) +regular7=e0def4 # white (Text) + +bright0=47435d # bright black (lighter Overlay) +bright1=ff98ba # bright red (lighter Love) +bright2=c5f9ff # bright green (lighter Foam) +bright3=ffeb9e # bright yellow (lighter Gold) +bright4=5b9ab7 # bright blue (lighter Pine) +bright5=eed0ff # bright magenta (lighter Iris) +bright6=ffe5e3 # bright cyan (lighter Rose) +bright7=fefcff # bright white (lighter Text) + +flash=f6c177 # yellow (Gold) + diff --git a/themes/rose-pine-dawn b/themes/rose-pine-dawn new file mode 100644 index 0000000..d2742c7 --- /dev/null +++ b/themes/rose-pine-dawn @@ -0,0 +1,32 @@ +# -*- conf -*- +# Rosé Pine Dawn + +[main] +initial-color-theme=light + + +[colors-light] +cursor=faf4ed 575279 +background=faf4ed +foreground=575279 + +regular0=f2e9e1 # black (Overlay) +regular1=b4637a # red (Love) +regular2=56949f # green (Foam) +regular3=ea9d34 # yellow (Gold) +regular4=286983 # blue (Pine) +regular5=907aa9 # magenta (Iris) +regular6=d7827e # cyan (Rose) +regular7=575279 # white (Text) + +bright0=fffdf5 # bright black (lighter Overlay) +bright1=df8aa0 # bright red (lighter Love) +bright2=7ebcc7 # bright green (lighter Foam) +bright3=ffc55c # bright yellow (lighter Gold) +bright4=538faa # bright blue (lighter Pine) +bright5=b8a1d2 # bright magenta (lighter Iris) +bright6=ffaaa5 # bright cyan (lighter Rose) +bright7=7c76a0 # bright white (lighter Text) + +flash=ea9d34 # yellow (Gold) + diff --git a/themes/rose-pine-moon b/themes/rose-pine-moon new file mode 100644 index 0000000..51b9a33 --- /dev/null +++ b/themes/rose-pine-moon @@ -0,0 +1,28 @@ +# -*- conf -*- +# Rosé Pine Moon + +[colors-dark] +cursor=232136 e0def4 +background=232136 +foreground=e0def4 + +regular0=393552 # black (Overlay) +regular1=eb6f92 # red (Love) +regular2=9ccfd8 # green (Foam) +regular3=f6c177 # yellow (Gold) +regular4=3e8fb0 # blue (Pine) +regular5=c4a7e7 # magenta (Iris) +regular6=ea9a97 # cyan (Rose) +regular7=e0def4 # white (Text) + +bright0=5c5776 # bright black (lighter Overlay) +bright1=ff98ba # bright red (lighter Love) +bright2=c5f9ff # bright green (lighter Foam) +bright3=ffeb9e # bright yellow (lighter Gold) +bright4=6ab7d9 # bright blue (lighter Pine) +bright5=eed0ff # bright magenta (lighter Iris) +bright6=ffc3bf # bright cyan (lighter Rose) +bright7=fefcff # bright white (lighter Text) + +flash=f6c177 # yellow (Gold) + diff --git a/themes/selenized b/themes/selenized new file mode 100644 index 0000000..83fea61 --- /dev/null +++ b/themes/selenized @@ -0,0 +1,48 @@ +# -*- conf -*- +# Selenized dark + +[colors-dark] +cursor = 103c48 53d6c7 +background= 103c48 +foreground= adbcbc + +regular0= 184956 +regular1= fa5750 +regular2= 75b938 +regular3= dbb32d +regular4= 4695f7 +regular5= f275be +regular6= 41c7b9 +regular7= 72898f + +bright0= 2d5b69 +bright1= ff665c +bright2= 84c747 +bright3= ebc13d +bright4= 58a3ff +bright5= ff84cd +bright6= 53d6c7 +bright7= cad8d9 + +[colors-light] +cursor=fbf3db 00978a +background= fbf3db +foreground= 53676d + +regular0= ece3cc +regular1= d2212d +regular2= 489100 +regular3= ad8900 +regular4= 0072d4 +regular5= ca4898 +regular6= 009c8f +regular7= 909995 + +bright0= d5cdb6 +bright1= cc1729 +bright2= 428b00 +bright3= a78300 +bright4= 006dce +bright5= c44392 +bright6= 00978a +bright7= 3a4d53 diff --git a/themes/selenized-black b/themes/selenized-black new file mode 100644 index 0000000..8a93187 --- /dev/null +++ b/themes/selenized-black @@ -0,0 +1,25 @@ +# -*- conf -*- +# Selenized black + +[colors-dark] +cursor = 181818 56d8c9 +background= 181818 +foreground= b9b9b9 + +regular0= 252525 +regular1= ed4a46 +regular2= 70b433 +regular3= dbb32d +regular4= 368aeb +regular5= eb6eb7 +regular6= 3fc5b7 +regular7= 777777 + +bright0= 3b3b3b +bright1= ff5e56 +bright2= 83c746 +bright3= efc541 +bright4= 4f9cfe +bright5= ff81ca +bright6= 56d8c9 +bright7= dedede diff --git a/themes/selenized-dark b/themes/selenized-dark new file mode 100644 index 0000000..8ace1c0 --- /dev/null +++ b/themes/selenized-dark @@ -0,0 +1,25 @@ +# -*- conf -*- +# Selenized dark + +[colors-dark] +cursor = 103c48 53d6c7 +background= 103c48 +foreground= adbcbc + +regular0= 184956 +regular1= fa5750 +regular2= 75b938 +regular3= dbb32d +regular4= 4695f7 +regular5= f275be +regular6= 41c7b9 +regular7= 72898f + +bright0= 2d5b69 +bright1= ff665c +bright2= 84c747 +bright3= ebc13d +bright4= 58a3ff +bright5= ff84cd +bright6= 53d6c7 +bright7= cad8d9 diff --git a/themes/selenized-light b/themes/selenized-light new file mode 100644 index 0000000..c842fc3 --- /dev/null +++ b/themes/selenized-light @@ -0,0 +1,28 @@ +# -*- conf -*- +# Selenized light + +[main] +initial-color-theme=light + +[colors-light] +cursor=fbf3db 00978a +background= fbf3db +foreground= 53676d + +regular0= ece3cc +regular1= d2212d +regular2= 489100 +regular3= ad8900 +regular4= 0072d4 +regular5= ca4898 +regular6= 009c8f +regular7= 909995 + +bright0= d5cdb6 +bright1= cc1729 +bright2= 428b00 +bright3= a78300 +bright4= 006dce +bright5= c44392 +bright6= 00978a +bright7= 3a4d53 diff --git a/themes/selenized-white b/themes/selenized-white new file mode 100644 index 0000000..659bf81 --- /dev/null +++ b/themes/selenized-white @@ -0,0 +1,28 @@ +# -*- conf -*- +# Selenized white + +[main] +initial-color-theme=light + +[colors-light] +cursor=ffffff 009a8a +background= ffffff +foreground= 474747 + +regular0= ebebeb +regular1= d6000c +regular2= 1d9700 +regular3= c49700 +regular4= 0064e4 +regular5= dd0f9d +regular6= 00ad9c +regular7= 878787 + +bright0= cdcdcd +bright1= bf0000 +bright2= 008400 +bright3= af8500 +bright4= 0054cf +bright5= c7008b +bright6= 009a8a +bright7= 282828 diff --git a/themes/solarized b/themes/solarized new file mode 100644 index 0000000..f1844b3 --- /dev/null +++ b/themes/solarized @@ -0,0 +1,47 @@ +# -*- conf -*- +# Solarized dark+light + +# Dark +[colors-dark] +cursor= 002b36 93a1a1 +background= 002b36 +foreground= 839496 +regular0= 073642 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= eee8d5 +bright0= 002b36 +bright1= cb4b16 +bright2= 586e75 +bright3= 657b83 +bright4= 839496 +bright5= 6c71c4 +bright6= 93a1a1 +bright7= fdf6e3 + + +# Light +[colors-light] +cursor= fdf6e3 586e75 +background= fdf6e3 +foreground= 657b83 +regular0= eee8d5 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= 073642 +bright0= cb4b16 +bright1= fdf6e3 +bright2= 93a1a1 +bright3= 839496 +bright4= 657b83 +bright5= 6c71c4 +bright6= 586e75 +bright7= 002b36 diff --git a/themes/solarized-dark b/themes/solarized-dark new file mode 100644 index 0000000..6335fa0 --- /dev/null +++ b/themes/solarized-dark @@ -0,0 +1,28 @@ +# -*- conf -*- +# Solarized dark + +[colors-dark] +cursor= 002b36 93a1a1 +background= 002b36 +foreground= 839496 +regular0= 073642 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= eee8d5 +bright0= 002b36 +bright1= cb4b16 +bright2= 586e75 +bright3= 657b83 +bright4= 839496 +bright5= 6c71c4 +bright6= 93a1a1 +bright7= fdf6e3 + +# Enable if prefer solarized colors instead of inverterd fg/bg for +# highlighting (mouse selection) +# selection-foreground=93a1a1 +# selection-background=073642 diff --git a/themes/solarized-dark-normal-brights b/themes/solarized-dark-normal-brights new file mode 100644 index 0000000..7b60811 --- /dev/null +++ b/themes/solarized-dark-normal-brights @@ -0,0 +1,30 @@ +# -*- conf -*- +# Solarized dark + +[colors-dark] +cursor= 002b36 93a1a1 +background= 002b36 +foreground= 839496 +regular0= 073642 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= eee8d5 + +# regularN brightened by increasing luminance by 20% +bright0= 08404f +bright1= e35f5c +bright2= 9fb700 +bright3= d9a400 +bright4= 4ba1de +bright5= dc619d +bright6= 32c1b6 +bright7= ffffff + +# Enable if prefer solarized colors instead of inverterd fg/bg for +# highlighting (mouse selection) +# selection-foreground=93a1a1 +# selection-background=073642 diff --git a/themes/solarized-light b/themes/solarized-light new file mode 100644 index 0000000..db27be4 --- /dev/null +++ b/themes/solarized-light @@ -0,0 +1,26 @@ +# -*- conf -*- +# Solarized light + +[main] +initial-color-theme=light + +[colors-light] +cursor= fdf6e3 586e75 +background= fdf6e3 +foreground= 657b83 +regular0= eee8d5 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= 073642 +bright0= cb4b16 +bright1= fdf6e3 +bright2= 93a1a1 +bright3= 839496 +bright4= 657b83 +bright5= 6c71c4 +bright6= 586e75 +bright7= 002b36 diff --git a/themes/solarized-normal-brights b/themes/solarized-normal-brights new file mode 100644 index 0000000..3bd3c18 --- /dev/null +++ b/themes/solarized-normal-brights @@ -0,0 +1,54 @@ +# -*- conf -*- +# Solarized dark+light +# +# Bright colors are brighter versions of the regular colors, instead +# of the normal solarized palette. +# +# They were generated by taking the regular colors, decoding from sRGB +# to linear, multiplying the linear RGB values by 1.8, and then +# encoding to sRGB again. + +# Dark +[colors-dark] +cursor= 002b36 93a1a1 +background= 002b36 +foreground= 839496 +regular0= 073642 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= eee8d5 +bright0= 0c4958 +bright1= ff4440 +bright2= aec700 +bright3= ebb300 +bright4= 34b5ff +bright5= ff49aa +bright6= 3ad2c6 +bright7= ffffff + + +# Light +[colors-light] +cursor= fdf6e3 586e75 +background= fdf6e3 +foreground= 657b83 +regular0= eee8d5 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= 073642 +bright0= ffffff +bright1= ff4440 +bright2= aec700 +bright3= ebb300 +bright4= 34b5ff +bright5= ff49aa +bright6= 3ad2c6 +bright7= 0c4958 diff --git a/themes/srcery b/themes/srcery new file mode 100644 index 0000000..612c82c --- /dev/null +++ b/themes/srcery @@ -0,0 +1,26 @@ +# srcery + +[colors-dark] +background= 1c1b19 +foreground= fce8c3 +regular0= 1c1b19 +regular1= ef2f27 +regular2= 519f50 +regular3= fbb829 +regular4= 2c78bf +regular5= e02c6d +regular6= 0aaeb3 +regular7= baa67f +bright0= 918175 +bright1= f75341 +bright2= 98bc37 +bright3= fed06e +bright4= 68a8e4 +bright5= ff5c8f +bright6= 2be4d0 +bright7= fce8c3 + +## Enable if prefer solarized colors instead of inverterd fg/bg for +## highlighting (mouse selection) +# selection-foreground=93a1a1 +# selection-background=073642 diff --git a/themes/starlight b/themes/starlight new file mode 100644 index 0000000..81ce1a5 --- /dev/null +++ b/themes/starlight @@ -0,0 +1,24 @@ +# -*- conf -*- +# Theme: starlight V4 (https://github.com/CosmicToast/starlight) + +[colors-dark] +foreground = FFFFFF +background = 242424 + +regular0 = 242424 +regular1 = f62b5a +regular2 = 47b413 +regular3 = e3c401 +regular4 = 24acd4 +regular5 = f2affd +regular6 = 13c299 +regular7 = e6e6e6 + +bright0 = 616161 +bright1 = ff4d51 +bright2 = 35d450 +bright3 = e9e836 +bright4 = 5dc5f8 +bright5 = feabf2 +bright6 = 24dfc4 +bright7 = ffffff diff --git a/themes/tango b/themes/tango new file mode 100644 index 0000000..5ea43f6 --- /dev/null +++ b/themes/tango @@ -0,0 +1,23 @@ +# -*- conf -*- +# Tango + +[colors-dark] +cursor=000000 babdb6 +foreground=babdb6 +background=000000 +regular0=2e3436 +regular1=cc0000 +regular2=4e9a06 +regular3=c4a000 +regular4=3465a4 +regular5=75507b +regular6=06989a +regular7=d3d7cf +bright0=555753 +bright1=ef2929 +bright2=8ae234 +bright3=fce94f +bright4=729fcf +bright5=ad7fa8 +bright6=34e2e2 +bright7=eeeeec diff --git a/themes/tempus-autumn b/themes/tempus-autumn new file mode 100644 index 0000000..214478b --- /dev/null +++ b/themes/tempus-autumn @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Autumn +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with a palette inspired by earthly colours (WCAG AA compliant) + +[colors-dark] +#cursor = 302420 a9a2a6 +foreground = a9a2a6 +background = 302420 +regular0 = 302420 +regular1 = f46f55 +regular2 = 85a400 +regular3 = b09640 +regular4 = 799aca +regular5 = df798e +regular6 = 52a885 +regular7 = a8948a +bright0 = 36302a +bright1 = e27e3d +bright2 = 43aa7a +bright3 = ba9400 +bright4 = 958fdf +bright5 = ce7dc4 +bright6 = 2fa6b7 +bright7 = a9a2a6 +# selection-foreground = a8948a +# selection-background = 36302a diff --git a/themes/tempus-classic b/themes/tempus-classic new file mode 100644 index 0000000..95b37b7 --- /dev/null +++ b/themes/tempus-classic @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Classic +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with warm hues (WCAG AA compliant) + +[colors-dark] +#cursor = 232323 aeadaf +foreground = aeadaf +background = 232323 +regular0 = 232323 +regular1 = d4823d +regular2 = 8c9e3d +regular3 = b1942b +regular4 = 6e9cb0 +regular5 = b58d88 +regular6 = 6da280 +regular7 = 949d9f +bright0 = 312e30 +bright1 = d0913d +bright2 = 96a42d +bright3 = a8a030 +bright4 = 8e9cc0 +bright5 = d58888 +bright6 = 7aa880 +bright7 = aeadaf +# selection-foreground = 949d9f +# selection-background = 312e30 diff --git a/themes/tempus-dawn b/themes/tempus-dawn new file mode 100644 index 0000000..c288544 --- /dev/null +++ b/themes/tempus-dawn @@ -0,0 +1,31 @@ +# -*- conf -*- +# theme: Tempus Dawn +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Light theme with a soft, slightly desaturated palette (WCAG AA compliant) + +[main] +initial-color-theme=light + + +[colors-light] +#cursor = eff0f2 4a4b4e +foreground = 4a4b4e +background = eff0f2 +regular0 = 4a4b4e +regular1 = a32a3a +regular2 = 206620 +regular3 = 745300 +regular4 = 4b529a +regular5 = 8d377e +regular6 = 086784 +regular7 = dee2e0 +bright0 = 676364 +bright1 = a64822 +bright2 = 187408 +bright3 = 8b590a +bright4 = 5c59b2 +bright5 = 8e45a8 +bright6 = 3f649c +bright7 = eff0f2 +# selection-foreground = 676364 +# selection-background = dee2e0 diff --git a/themes/tempus-day b/themes/tempus-day new file mode 100644 index 0000000..03454f0 --- /dev/null +++ b/themes/tempus-day @@ -0,0 +1,30 @@ +# -*- conf -*- +# theme: Tempus Day +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Light theme with warm colours (WCAG AA compliant) + +[main] +initial-color-theme=light + +[colors-light] +#cursor = f8f2e5 464340 +foreground = 464340 +background = f8f2e5 +regular0 = 464340 +regular1 = c81000 +regular2 = 107410 +regular3 = 806000 +regular4 = 385dc4 +regular5 = b63052 +regular6 = 007070 +regular7 = e7e3d7 +bright0 = 68607d +bright1 = b24000 +bright2 = 427040 +bright3 = 6f6600 +bright4 = 0f64c4 +bright5 = 8050a7 +bright6 = 336c87 +bright7 = f8f2e5 +# selection-foreground = 68607d +# selection-background = e7e3d7 diff --git a/themes/tempus-dusk b/themes/tempus-dusk new file mode 100644 index 0000000..cd27aaa --- /dev/null +++ b/themes/tempus-dusk @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Dusk +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with a deep blue-ish, slightly desaturated palette (WCAG AA compliant) + +[colors-dark] +#cursor = 1f252d a2a8ba +foreground = a2a8ba +background = 1f252d +regular0 = 1f252d +regular1 = cb8d56 +regular2 = 8ba089 +regular3 = a79c46 +regular4 = 8c9abe +regular5 = b190af +regular6 = 8e9aba +regular7 = a29899 +bright0 = 2c3150 +bright1 = d39d74 +bright2 = 80b48f +bright3 = bda75a +bright4 = 9ca5de +bright5 = c69ac6 +bright6 = 8caeb6 +bright7 = a2a8ba +# selection-foreground = a29899 +# selection-background = 2c3150 diff --git a/themes/tempus-fugit b/themes/tempus-fugit new file mode 100644 index 0000000..b9dce35 --- /dev/null +++ b/themes/tempus-fugit @@ -0,0 +1,30 @@ +# -*- conf -*- +# theme: Tempus Fugit +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Light, pleasant theme optimised for long writing/coding sessions (WCAG AA compliant) + +[main] +initial-color-theme=light + +[colors-light] +#cursor = fff5f3 4d595f +foreground = 4d595f +background = fff5f3 +regular0 = 4d595f +regular1 = c61a14 +regular2 = 357200 +regular3 = 825e00 +regular4 = 1666b0 +regular5 = a83884 +regular6 = 007072 +regular7 = efe6e4 +bright0 = 796271 +bright1 = b93f1a +bright2 = 437520 +bright3 = 985900 +bright4 = 485adf +bright5 = a234c0 +bright6 = 00756a +bright7 = fff5f3 +# selection-foreground = 796271 +# selection-background = efe6e4 diff --git a/themes/tempus-future b/themes/tempus-future new file mode 100644 index 0000000..1f8c3c7 --- /dev/null +++ b/themes/tempus-future @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Future +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with colours inspired by concept art of outer space (WCAG AAA compliant) + +[colors-dark] +#cursor = 090a18 b4abac +foreground = b4abac +background = 090a18 +regular0 = 090a18 +regular1 = ff7e8f +regular2 = 6aba39 +regular3 = bfa51a +regular4 = 4ab2d7 +regular5 = e58f84 +regular6 = 2ab7bb +regular7 = a7a2c4 +bright0 = 2b1329 +bright1 = f78e2f +bright2 = 60ba80 +bright3 = de9b1d +bright4 = 8ba7ea +bright5 = e08bd6 +bright6 = 2cbab6 +bright7 = b4abac +# selection-foreground = a7a2c4 +# selection-background = 2b1329 diff --git a/themes/tempus-night b/themes/tempus-night new file mode 100644 index 0000000..aae80f0 --- /dev/null +++ b/themes/tempus-night @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Night +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: High contrast dark theme with bright colours (WCAG AAA compliant) + +[colors-dark] +#cursor = 1a1a1a e0e0e0 +foreground = e0e0e0 +background = 1a1a1a +regular0 = 1a1a1a +regular1 = ff929f +regular2 = 5fc940 +regular3 = c5b300 +regular4 = 5fb8ff +regular5 = ef91df +regular6 = 1dc5c3 +regular7 = c4bdaf +bright0 = 242536 +bright1 = f69d6a +bright2 = 88c400 +bright3 = d7ae00 +bright4 = 8cb4f0 +bright5 = de99f0 +bright6 = 00ca9a +bright7 = e0e0e0 +# selection-foreground = c4bdaf +# selection-background = 242536 diff --git a/themes/tempus-past b/themes/tempus-past new file mode 100644 index 0000000..5f90ddf --- /dev/null +++ b/themes/tempus-past @@ -0,0 +1,30 @@ +# -*- conf -*- +# theme: Tempus Past +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Light theme inspired by old vaporwave concept art (WCAG AA compliant) + +[main] +initial-color-theme=light + +[colors-light] +#cursor = f3f2f4 53545b +foreground = 53545b +background = f3f2f4 +regular0 = 53545b +regular1 = c00c50 +regular2 = 0a7040 +regular3 = a6403a +regular4 = 1763aa +regular5 = b02874 +regular6 = 096a83 +regular7 = eae2de +bright0 = 80565d +bright1 = bd3133 +bright2 = 337243 +bright3 = 8d554a +bright4 = 5559bb +bright5 = b022a7 +bright6 = 07707a +bright7 = f3f2f4 +# selection-foreground = 80565d +# selection-background = eae2de diff --git a/themes/tempus-rift b/themes/tempus-rift new file mode 100644 index 0000000..8add657 --- /dev/null +++ b/themes/tempus-rift @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Rift +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with a subdued palette on the green side of the spectrum (WCAG AA compliant) + +[colors-dark] +#cursor = 162c22 bbbcbc +foreground = bbbcbc +background = 162c22 +regular0 = 162c22 +regular1 = c19904 +regular2 = 34b534 +regular3 = 7fad00 +regular4 = 30aeb0 +regular5 = c8954c +regular6 = 5fad8f +regular7 = ab9aa9 +bright0 = 283431 +bright1 = d2a634 +bright2 = 6ac134 +bright3 = 82bd00 +bright4 = 56bdad +bright5 = cca0ba +bright6 = 10c480 +bright7 = bbbcbc +# selection-foreground = ab9aa9 +# selection-background = 283431 diff --git a/themes/tempus-spring b/themes/tempus-spring new file mode 100644 index 0000000..eb15a1b --- /dev/null +++ b/themes/tempus-spring @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Spring +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with a palette inspired by early spring colours (WCAG AA compliant) + +[colors-dark] +#cursor = 283a37 b5b8b7 +foreground = b5b8b7 +background = 283a37 +regular0 = 283a37 +regular1 = ff8b5f +regular2 = 5ec04d +regular3 = b0b01a +regular4 = 39bace +regular5 = e99399 +regular6 = 36c08e +regular7 = 99afae +bright0 = 2a453d +bright1 = e19e00 +bright2 = 73be0d +bright3 = c6a843 +bright4 = 70afef +bright5 = d095e2 +bright6 = 3cbfaf +bright7 = b5b8b7 +# selection-foreground = 99afae +# selection-background = 2a453d diff --git a/themes/tempus-summer b/themes/tempus-summer new file mode 100644 index 0000000..74c8faa --- /dev/null +++ b/themes/tempus-summer @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Summer +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with colours inspired by summer evenings by the sea (WCAG AA compliant) + +[colors-dark] +#cursor = 202c3d a0abae +foreground = a0abae +background = 202c3d +regular0 = 202c3d +regular1 = fe6f70 +regular2 = 4eb075 +regular3 = ba9a0a +regular4 = 60a1e6 +regular5 = d285ad +regular6 = 3dae9f +regular7 = 949cbf +bright0 = 39304f +bright1 = ec7f4f +bright2 = 5baf4f +bright3 = be981f +bright4 = 8599ef +bright5 = cc82d7 +bright6 = 2aacbf +bright7 = a0abae +# selection-foreground = 949cbf +# selection-background = 39304f diff --git a/themes/tempus-tempest b/themes/tempus-tempest new file mode 100644 index 0000000..f1cf55b --- /dev/null +++ b/themes/tempus-tempest @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Tempest +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: A green-scale, subtle theme for late night hackers (WCAG AAA compliant) + +[colors-dark] +#cursor = 282b2b b6e0ca +foreground = b6e0ca +background = 282b2b +regular0 = 282b2b +regular1 = cfc80a +regular2 = 7ad97a +regular3 = bfcc4a +regular4 = 60d7cd +regular5 = c5c4af +regular6 = 8bd0bf +regular7 = b0c8ca +bright0 = 323535 +bright1 = d1d933 +bright2 = 99e299 +bright3 = bbde4f +bright4 = 74e4cd +bright5 = d2d4aa +bright6 = 9bdfc4 +bright7 = b6e0ca +# selection-foreground = b0c8ca +# selection-background = 323535 diff --git a/themes/tempus-totus b/themes/tempus-totus new file mode 100644 index 0000000..fae6ede --- /dev/null +++ b/themes/tempus-totus @@ -0,0 +1,30 @@ +# -*- conf -*- +# theme: Tempus Totus +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Light theme for prose or for coding in an open space (WCAG AAA compliant) + +[main] +initial-color-theme=light + +[colors-light] +#cursor = ffffff 4a484d +foreground = 4a484d +background = ffffff +regular0 = 4a484d +regular1 = a50000 +regular2 = 005d26 +regular3 = 714700 +regular4 = 1d3ccf +regular5 = 88267a +regular6 = 185570 +regular7 = efefef +bright0 = 5e4b4f +bright1 = 992030 +bright2 = 4a5500 +bright3 = 8a3600 +bright4 = 2d45b0 +bright5 = 700dc9 +bright6 = 005289 +bright7 = ffffff +# selection-foreground = 5e4b4f +# selection-background = efefef diff --git a/themes/tempus-warp b/themes/tempus-warp new file mode 100644 index 0000000..906b3f3 --- /dev/null +++ b/themes/tempus-warp @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Warp +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with a vibrant palette (WCAG AA compliant) + +[colors-dark] +#cursor = 001514 a29fa0 +foreground = a29fa0 +background = 001514 +regular0 = 001514 +regular1 = ff3737 +regular2 = 169c16 +regular3 = 9f8500 +regular4 = 5781ef +regular5 = da4ebf +regular6 = 009880 +regular7 = 968282 +bright0 = 261c2c +bright1 = F0681A +bright2 = 3aa73a +bright3 = ba8a00 +bright4 = 8887f0 +bright5 = d85cf2 +bright6 = 1da1af +bright7 = a29fa0 +# selection-foreground = 968282 +# selection-background = 261c2c diff --git a/themes/tempus-winter b/themes/tempus-winter new file mode 100644 index 0000000..dc95128 --- /dev/null +++ b/themes/tempus-winter @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Winter +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with a palette inspired by winter nights at the city (WCAG AA compliant) + +[colors-dark] +#cursor = 202427 8da3b8 +foreground = 8da3b8 +background = 202427 +regular0 = 202427 +regular1 = ed6e5a +regular2 = 4aa920 +regular3 = 9a9921 +regular4 = 7b91df +regular5 = d17e80 +regular6 = 4fa394 +regular7 = 91959b +bright0 = 2a2e38 +bright1 = de7b28 +bright2 = 00ab5f +bright3 = af9155 +bright4 = 329fcb +bright5 = ca77c5 +bright6 = 1ba6a4 +bright7 = 8da3b8 +# selection-foreground = 91959b +# selection-background = 2a2e38 diff --git a/themes/tokyonight-light b/themes/tokyonight-light new file mode 100644 index 0000000..359a31b --- /dev/null +++ b/themes/tokyonight-light @@ -0,0 +1,28 @@ +# -*- conf -*- + +# Reference: https://github.com/tokyo-night/tokyo-night-vscode-theme/blob/master/themes/tokyo-night-light-color-theme.json + +[main] +initial-color-theme=light + +[colors-light] +background=d6d8df +foreground=343b58 +regular0=343b58 +regular1=8c4351 +regular2=33635c +regular3=8f5e15 +regular4=2959aa +regular5=7b43ba +regular6=006c86 +regular7=707280 +bright0=343b58 +bright1=8c4351 +bright2=33635c +bright3=8f5e15 +bright4=2959aa +bright5=7b43ba +bright6=006c86 +bright7=707280 + +jump-labels=343b58 e19d37 # brighter yellow than regular3 diff --git a/themes/tokyonight-night b/themes/tokyonight-night new file mode 100644 index 0000000..58037f7 --- /dev/null +++ b/themes/tokyonight-night @@ -0,0 +1,21 @@ +# -*- conf -*- + +[colors-dark] +background=1a1b26 +foreground=c0caf5 +regular0=15161E +regular1=f7768e +regular2=9ece6a +regular3=e0af68 +regular4=7aa2f7 +regular5=bb9af7 +regular6=7dcfff +regular7=a9b1d6 +bright0=414868 +bright1=f7768e +bright2=9ece6a +bright3=e0af68 +bright4=7aa2f7 +bright5=bb9af7 +bright6=7dcfff +bright7=c0caf5 diff --git a/themes/tokyonight-storm b/themes/tokyonight-storm new file mode 100644 index 0000000..4dbbf6c --- /dev/null +++ b/themes/tokyonight-storm @@ -0,0 +1,21 @@ +# -*- conf -*- + +[colors-dark] +background=24283b +foreground=c0caf5 +regular0=1D202F +regular1=f7768e +regular2=9ece6a +regular3=e0af68 +regular4=7aa2f7 +regular5=bb9af7 +regular6=7dcfff +regular7=a9b1d6 +bright0=414868 +bright1=f7768e +bright2=9ece6a +bright3=e0af68 +bright4=7aa2f7 +bright5=bb9af7 +bright6=7dcfff +bright7=c0caf5 diff --git a/themes/visibone b/themes/visibone new file mode 100644 index 0000000..b989b36 --- /dev/null +++ b/themes/visibone @@ -0,0 +1,23 @@ +# -*- conf -*- +# VisiBone + +[colors-dark] +cursor=010101 ffffff +foreground=ffffff +background=010101 +regular0=666666 +regular1=cc6666 +regular2=66cc99 +regular3=cc9966 +regular4=6699cc +regular5=cc6699 +regular6=66cccc +regular7=cccccc +bright0=999999 +bright1=ff9999 +bright2=99ffcc +bright3=ffcc99 +bright4=99ccff +bright5=ff99cc +bright6=99ffff +bright7=ffffff diff --git a/themes/xterm b/themes/xterm new file mode 100644 index 0000000..a9382fd --- /dev/null +++ b/themes/xterm @@ -0,0 +1,22 @@ +# -*- conf -*- +# The default palette of xterm. + +[colors-dark] +foreground=e5e5e5 +background=000000 +regular0=000000 # black +regular1=cd0000 # red +regular2=00cd00 # green +regular3=cdcd00 # yellow +regular4=0000ee # blue +regular5=cd00cd # magenta +regular6=00cdcd # cyan +regular7=e5e5e5 # white +bright0=7f7f7f # bright black +bright1=ff0000 # bright red +bright2=00ff00 # bright green +bright3=ffff00 # bright yellow +bright4=5c5cff # bright blue +bright5=ff00ff # bright magenta +bright6=00ffff # bright cyan +bright7=ffffff # bright white diff --git a/themes/zenburn b/themes/zenburn new file mode 100644 index 0000000..37a2681 --- /dev/null +++ b/themes/zenburn @@ -0,0 +1,25 @@ +# -*- conf -*- + +[colors-dark] +foreground=dcdccc +background=111111 + +## Normal/regular colors (color palette 0-7) +regular0=222222 # black +regular1=cc9393 # red +regular2=7f9f7f # green +regular3=d0bf8f # yellow +regular4=6ca0a3 # blue +regular5=dc8cc3 # magenta +regular6=93e0e3 # cyan +regular7=dcdccc # white + +## Bright colors (color palette 8-15) +bright0=666666 # bright black +bright1=dca3a3 # bright red +bright2=bfebbf # bright green +bright3=f0dfaf # bright yellow +bright4=8cd0d3 # bright blue +bright5=fcace3 # bright magenta +bright6=b3ffff # bright cyan +bright7=ffffff # bright white diff --git a/tokenize.c b/tokenize.c new file mode 100644 index 0000000..70ceb39 --- /dev/null +++ b/tokenize.c @@ -0,0 +1,103 @@ +#include "tokenize.h" + +#include +#include + +#define LOG_MODULE "tokenize" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "xmalloc.h" + +static bool +push_argv(char ***argv, size_t *size, const char *arg, size_t len, size_t *argc) +{ + if (arg != NULL && arg[0] == '%') + return true; + + if (*argc >= *size) { + size_t new_size = *size > 0 ? 2 * *size : 10; + char **new_argv = realloc(*argv, new_size * sizeof(new_argv[0])); + + if (new_argv == NULL) + return false; + + *argv = new_argv; + *size = new_size; + } + + (*argv)[(*argc)++] = arg != NULL ? xstrndup(arg, len) : NULL; + return true; +} + +bool +tokenize_cmdline(const char *cmdline, char ***argv) +{ + *argv = NULL; + size_t argv_size = 0; + + const char *final_end = cmdline + strlen(cmdline) + 1; + + bool first_token_is_quoted = cmdline[0] == '"' || cmdline[0] == '\''; + char delim = first_token_is_quoted ? cmdline[0] : ' '; + + const char *p = first_token_is_quoted ? &cmdline[1] : &cmdline[0]; + const char *search_start = p; + + size_t idx = 0; + while (*p != '\0') { + char *end = (char *)strchr(search_start, delim); + if (end == NULL) { + if (delim != ' ') { + LOG_ERR("unterminated %s quote", delim == '"' ? "double" : "single"); + goto err; + } + + if (!push_argv(argv, &argv_size, p, final_end - p, &idx) || + !push_argv(argv, &argv_size, NULL, 0, &idx)) + { + goto err; + } else + return true; + } + + if (end > p && *(end - 1) == '\\') { + /* Escaped quote, remove one level of escaping and + * continue searching for "our" closing quote */ + memmove(end - 1, end, strlen(end)); + end[strlen(end) - 1] = '\0'; + search_start = end; + continue; + } + + //*end = '\0'; + + if (!push_argv(argv, &argv_size, p, end - p, &idx)) + goto err; + + p = end + 1; + while (*p == delim) + p++; + + while (*p == ' ') + p++; + + if (*p == '"' || *p == '\'') { + delim = *p; + p++; + } else + delim = ' '; + search_start = p; + } + + if (!push_argv(argv, &argv_size, NULL, 0, &idx)) + goto err; + + return true; + +err: + for (size_t i = 0; i < idx; i++) + free((*argv)[i]); + free(*argv); + *argv = NULL; + return false; +} diff --git a/tokenize.h b/tokenize.h new file mode 100644 index 0000000..9e5de6c --- /dev/null +++ b/tokenize.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +bool tokenize_cmdline(const char *cmdline, char ***argv); diff --git a/unicode-mode.c b/unicode-mode.c new file mode 100644 index 0000000..1acdc66 --- /dev/null +++ b/unicode-mode.c @@ -0,0 +1,107 @@ +#include "unicode-mode.h" + +#define LOG_MODULE "unicode-input" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "render.h" +#include "search.h" + +void +unicode_mode_activate(struct terminal *term) +{ + if (term->unicode_mode.active) + return; + + term->unicode_mode.active = true; + term->unicode_mode.character = u'\0'; + term->unicode_mode.count = 0; + unicode_mode_updated(term); +} + +void +unicode_mode_deactivate(struct terminal *term) +{ + if (!term->unicode_mode.active) + return; + + term->unicode_mode.active = false; + unicode_mode_updated(term); +} + +void +unicode_mode_updated(struct terminal *term) +{ + if (term == NULL) + return; + + if (term->is_searching) + render_refresh_search(term); + else + render_refresh(term); +} + +void +unicode_mode_input(struct seat *seat, struct terminal *term, + xkb_keysym_t sym) +{ + if (sym == XKB_KEY_Return || + sym == XKB_KEY_space || + sym == XKB_KEY_KP_Enter || + sym == XKB_KEY_KP_Space) + { + char utf8[MB_CUR_MAX]; + size_t chars = c32rtomb( + utf8, term->unicode_mode.character, &(mbstate_t){0}); + + LOG_DBG("Unicode input: 0x%06x -> %.*s", + term->unicode_mode.character, (int)chars, utf8); + + if (chars != (size_t)-1) { + if (term->is_searching) + search_add_chars(term, utf8, chars); + else + term_to_slave(term, utf8, chars); + } + + unicode_mode_deactivate(term); + } + + else if (sym == XKB_KEY_Escape || + sym == XKB_KEY_q || + (seat->kbd.ctrl && (sym == XKB_KEY_c || + sym == XKB_KEY_d || + sym == XKB_KEY_g))) + { + unicode_mode_deactivate(term); + } + + else if (sym == XKB_KEY_BackSpace) { + if (term->unicode_mode.count > 0) { + term->unicode_mode.character >>= 4; + term->unicode_mode.count--; + unicode_mode_updated(term); + } + } + + else if (term->unicode_mode.count < 6) { + int digit = -1; + + /* 0-9, a-f, A-F */ + if (sym >= XKB_KEY_0 && sym <= XKB_KEY_9) + digit = sym - XKB_KEY_0; + else if (sym >= XKB_KEY_KP_0 && sym <= XKB_KEY_KP_9) + digit = sym - XKB_KEY_KP_0; + else if (sym >= XKB_KEY_a && sym <= XKB_KEY_f) + digit = 0xa + (sym - XKB_KEY_a); + else if (sym >= XKB_KEY_A && sym <= XKB_KEY_F) + digit = 0xa + (sym - XKB_KEY_A); + + if (digit >= 0) { + xassert(digit >= 0 && digit <= 0xf); + term->unicode_mode.character <<= 4; + term->unicode_mode.character |= digit; + term->unicode_mode.count++; + unicode_mode_updated(term); + } + } +} diff --git a/unicode-mode.h b/unicode-mode.h new file mode 100644 index 0000000..2f8d2b3 --- /dev/null +++ b/unicode-mode.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "terminal.h" + +void unicode_mode_activate(struct terminal *term); +void unicode_mode_deactivate(struct terminal *term); +void unicode_mode_updated(struct terminal *term); +void unicode_mode_input(struct seat *seat, struct terminal *term, + xkb_keysym_t sym); diff --git a/unicode/emoji-variation-sequences.txt b/unicode/emoji-variation-sequences.txt new file mode 100644 index 0000000..4373835 --- /dev/null +++ b/unicode/emoji-variation-sequences.txt @@ -0,0 +1,757 @@ +# emoji-variation-sequences.txt +# Date: 2024-05-01, 21:25:24 GMT +# © 2024 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use and license, see https://www.unicode.org/terms_of_use.html +# +# Emoji Variation Sequences for UTS #51 +# Used with Emoji Version 16.0 and subsequent minor revisions (if any) +# +# For documentation and usage, see https://www.unicode.org/reports/tr51 +# +0023 FE0E ; text style; # (1.1) NUMBER SIGN +0023 FE0F ; emoji style; # (1.1) NUMBER SIGN +002A FE0E ; text style; # (1.1) ASTERISK +002A FE0F ; emoji style; # (1.1) ASTERISK +0030 FE0E ; text style; # (1.1) DIGIT ZERO +0030 FE0F ; emoji style; # (1.1) DIGIT ZERO +0031 FE0E ; text style; # (1.1) DIGIT ONE +0031 FE0F ; emoji style; # (1.1) DIGIT ONE +0032 FE0E ; text style; # (1.1) DIGIT TWO +0032 FE0F ; emoji style; # (1.1) DIGIT TWO +0033 FE0E ; text style; # (1.1) DIGIT THREE +0033 FE0F ; emoji style; # (1.1) DIGIT THREE +0034 FE0E ; text style; # (1.1) DIGIT FOUR +0034 FE0F ; emoji style; # (1.1) DIGIT FOUR +0035 FE0E ; text style; # (1.1) DIGIT FIVE +0035 FE0F ; emoji style; # (1.1) DIGIT FIVE +0036 FE0E ; text style; # (1.1) DIGIT SIX +0036 FE0F ; emoji style; # (1.1) DIGIT SIX +0037 FE0E ; text style; # (1.1) DIGIT SEVEN +0037 FE0F ; emoji style; # (1.1) DIGIT SEVEN +0038 FE0E ; text style; # (1.1) DIGIT EIGHT +0038 FE0F ; emoji style; # (1.1) DIGIT EIGHT +0039 FE0E ; text style; # (1.1) DIGIT NINE +0039 FE0F ; emoji style; # (1.1) DIGIT NINE +00A9 FE0E ; text style; # (1.1) COPYRIGHT SIGN +00A9 FE0F ; emoji style; # (1.1) COPYRIGHT SIGN +00AE FE0E ; text style; # (1.1) REGISTERED SIGN +00AE FE0F ; emoji style; # (1.1) REGISTERED SIGN +203C FE0E ; text style; # (1.1) DOUBLE EXCLAMATION MARK +203C FE0F ; emoji style; # (1.1) DOUBLE EXCLAMATION MARK +2049 FE0E ; text style; # (3.0) EXCLAMATION QUESTION MARK +2049 FE0F ; emoji style; # (3.0) EXCLAMATION QUESTION MARK +2122 FE0E ; text style; # (1.1) TRADE MARK SIGN +2122 FE0F ; emoji style; # (1.1) TRADE MARK SIGN +2139 FE0E ; text style; # (3.0) INFORMATION SOURCE +2139 FE0F ; emoji style; # (3.0) INFORMATION SOURCE +2194 FE0E ; text style; # (1.1) LEFT RIGHT ARROW +2194 FE0F ; emoji style; # (1.1) LEFT RIGHT ARROW +2195 FE0E ; text style; # (1.1) UP DOWN ARROW +2195 FE0F ; emoji style; # (1.1) UP DOWN ARROW +2196 FE0E ; text style; # (1.1) NORTH WEST ARROW +2196 FE0F ; emoji style; # (1.1) NORTH WEST ARROW +2197 FE0E ; text style; # (1.1) NORTH EAST ARROW +2197 FE0F ; emoji style; # (1.1) NORTH EAST ARROW +2198 FE0E ; text style; # (1.1) SOUTH EAST ARROW +2198 FE0F ; emoji style; # (1.1) SOUTH EAST ARROW +2199 FE0E ; text style; # (1.1) SOUTH WEST ARROW +2199 FE0F ; emoji style; # (1.1) SOUTH WEST ARROW +21A9 FE0E ; text style; # (1.1) LEFTWARDS ARROW WITH HOOK +21A9 FE0F ; emoji style; # (1.1) LEFTWARDS ARROW WITH HOOK +21AA FE0E ; text style; # (1.1) RIGHTWARDS ARROW WITH HOOK +21AA FE0F ; emoji style; # (1.1) RIGHTWARDS ARROW WITH HOOK +231A FE0E ; text style; # (1.1) WATCH +231A FE0F ; emoji style; # (1.1) WATCH +231B FE0E ; text style; # (1.1) HOURGLASS +231B FE0F ; emoji style; # (1.1) HOURGLASS +2328 FE0E ; text style; # (1.1) KEYBOARD +2328 FE0F ; emoji style; # (1.1) KEYBOARD +23CF FE0E ; text style; # (4.0) EJECT SYMBOL +23CF FE0F ; emoji style; # (4.0) EJECT SYMBOL +23E9 FE0E ; text style; # (6.0) BLACK RIGHT-POINTING DOUBLE TRIANGLE +23E9 FE0F ; emoji style; # (6.0) BLACK RIGHT-POINTING DOUBLE TRIANGLE +23EA FE0E ; text style; # (6.0) BLACK LEFT-POINTING DOUBLE TRIANGLE +23EA FE0F ; emoji style; # (6.0) BLACK LEFT-POINTING DOUBLE TRIANGLE +23EB FE0E ; text style; # (6.0) BLACK UP-POINTING DOUBLE TRIANGLE +23EB FE0F ; emoji style; # (6.0) BLACK UP-POINTING DOUBLE TRIANGLE +23EC FE0E ; text style; # (6.0) BLACK DOWN-POINTING DOUBLE TRIANGLE +23EC FE0F ; emoji style; # (6.0) BLACK DOWN-POINTING DOUBLE TRIANGLE +23ED FE0E ; text style; # (6.0) BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR +23ED FE0F ; emoji style; # (6.0) BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR +23EE FE0E ; text style; # (6.0) BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR +23EE FE0F ; emoji style; # (6.0) BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR +23EF FE0E ; text style; # (6.0) BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR +23EF FE0F ; emoji style; # (6.0) BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR +23F0 FE0E ; text style; # (6.0) ALARM CLOCK +23F0 FE0F ; emoji style; # (6.0) ALARM CLOCK +23F1 FE0E ; text style; # (6.0) STOPWATCH +23F1 FE0F ; emoji style; # (6.0) STOPWATCH +23F2 FE0E ; text style; # (6.0) TIMER CLOCK +23F2 FE0F ; emoji style; # (6.0) TIMER CLOCK +23F3 FE0E ; text style; # (6.0) HOURGLASS WITH FLOWING SAND +23F3 FE0F ; emoji style; # (6.0) HOURGLASS WITH FLOWING SAND +23F8 FE0E ; text style; # (7.0) DOUBLE VERTICAL BAR +23F8 FE0F ; emoji style; # (7.0) DOUBLE VERTICAL BAR +23F9 FE0E ; text style; # (7.0) BLACK SQUARE FOR STOP +23F9 FE0F ; emoji style; # (7.0) BLACK SQUARE FOR STOP +23FA FE0E ; text style; # (7.0) BLACK CIRCLE FOR RECORD +23FA FE0F ; emoji style; # (7.0) BLACK CIRCLE FOR RECORD +24C2 FE0E ; text style; # (1.1) CIRCLED LATIN CAPITAL LETTER M +24C2 FE0F ; emoji style; # (1.1) CIRCLED LATIN CAPITAL LETTER M +25AA FE0E ; text style; # (1.1) BLACK SMALL SQUARE +25AA FE0F ; emoji style; # (1.1) BLACK SMALL SQUARE +25AB FE0E ; text style; # (1.1) WHITE SMALL SQUARE +25AB FE0F ; emoji style; # (1.1) WHITE SMALL SQUARE +25B6 FE0E ; text style; # (1.1) BLACK RIGHT-POINTING TRIANGLE +25B6 FE0F ; emoji style; # (1.1) BLACK RIGHT-POINTING TRIANGLE +25C0 FE0E ; text style; # (1.1) BLACK LEFT-POINTING TRIANGLE +25C0 FE0F ; emoji style; # (1.1) BLACK LEFT-POINTING TRIANGLE +25FB FE0E ; text style; # (3.2) WHITE MEDIUM SQUARE +25FB FE0F ; emoji style; # (3.2) WHITE MEDIUM SQUARE +25FC FE0E ; text style; # (3.2) BLACK MEDIUM SQUARE +25FC FE0F ; emoji style; # (3.2) BLACK MEDIUM SQUARE +25FD FE0E ; text style; # (3.2) WHITE MEDIUM SMALL SQUARE +25FD FE0F ; emoji style; # (3.2) WHITE MEDIUM SMALL SQUARE +25FE FE0E ; text style; # (3.2) BLACK MEDIUM SMALL SQUARE +25FE FE0F ; emoji style; # (3.2) BLACK MEDIUM SMALL SQUARE +2600 FE0E ; text style; # (1.1) BLACK SUN WITH RAYS +2600 FE0F ; emoji style; # (1.1) BLACK SUN WITH RAYS +2601 FE0E ; text style; # (1.1) CLOUD +2601 FE0F ; emoji style; # (1.1) CLOUD +2602 FE0E ; text style; # (1.1) UMBRELLA +2602 FE0F ; emoji style; # (1.1) UMBRELLA +2603 FE0E ; text style; # (1.1) SNOWMAN +2603 FE0F ; emoji style; # (1.1) SNOWMAN +2604 FE0E ; text style; # (1.1) COMET +2604 FE0F ; emoji style; # (1.1) COMET +260E FE0E ; text style; # (1.1) BLACK TELEPHONE +260E FE0F ; emoji style; # (1.1) BLACK TELEPHONE +2611 FE0E ; text style; # (1.1) BALLOT BOX WITH CHECK +2611 FE0F ; emoji style; # (1.1) BALLOT BOX WITH CHECK +2614 FE0E ; text style; # (4.0) UMBRELLA WITH RAIN DROPS +2614 FE0F ; emoji style; # (4.0) UMBRELLA WITH RAIN DROPS +2615 FE0E ; text style; # (4.0) HOT BEVERAGE +2615 FE0F ; emoji style; # (4.0) HOT BEVERAGE +2618 FE0E ; text style; # (4.1) SHAMROCK +2618 FE0F ; emoji style; # (4.1) SHAMROCK +261D FE0E ; text style; # (1.1) WHITE UP POINTING INDEX +261D FE0F ; emoji style; # (1.1) WHITE UP POINTING INDEX +2620 FE0E ; text style; # (1.1) SKULL AND CROSSBONES +2620 FE0F ; emoji style; # (1.1) SKULL AND CROSSBONES +2622 FE0E ; text style; # (1.1) RADIOACTIVE SIGN +2622 FE0F ; emoji style; # (1.1) RADIOACTIVE SIGN +2623 FE0E ; text style; # (1.1) BIOHAZARD SIGN +2623 FE0F ; emoji style; # (1.1) BIOHAZARD SIGN +2626 FE0E ; text style; # (1.1) ORTHODOX CROSS +2626 FE0F ; emoji style; # (1.1) ORTHODOX CROSS +262A FE0E ; text style; # (1.1) STAR AND CRESCENT +262A FE0F ; emoji style; # (1.1) STAR AND CRESCENT +262E FE0E ; text style; # (1.1) PEACE SYMBOL +262E FE0F ; emoji style; # (1.1) PEACE SYMBOL +262F FE0E ; text style; # (1.1) YIN YANG +262F FE0F ; emoji style; # (1.1) YIN YANG +2638 FE0E ; text style; # (1.1) WHEEL OF DHARMA +2638 FE0F ; emoji style; # (1.1) WHEEL OF DHARMA +2639 FE0E ; text style; # (1.1) WHITE FROWNING FACE +2639 FE0F ; emoji style; # (1.1) WHITE FROWNING FACE +263A FE0E ; text style; # (1.1) WHITE SMILING FACE +263A FE0F ; emoji style; # (1.1) WHITE SMILING FACE +2640 FE0E ; text style; # (1.1) FEMALE SIGN +2640 FE0F ; emoji style; # (1.1) FEMALE SIGN +2642 FE0E ; text style; # (1.1) MALE SIGN +2642 FE0F ; emoji style; # (1.1) MALE SIGN +2648 FE0E ; text style; # (1.1) ARIES +2648 FE0F ; emoji style; # (1.1) ARIES +2649 FE0E ; text style; # (1.1) TAURUS +2649 FE0F ; emoji style; # (1.1) TAURUS +264A FE0E ; text style; # (1.1) GEMINI +264A FE0F ; emoji style; # (1.1) GEMINI +264B FE0E ; text style; # (1.1) CANCER +264B FE0F ; emoji style; # (1.1) CANCER +264C FE0E ; text style; # (1.1) LEO +264C FE0F ; emoji style; # (1.1) LEO +264D FE0E ; text style; # (1.1) VIRGO +264D FE0F ; emoji style; # (1.1) VIRGO +264E FE0E ; text style; # (1.1) LIBRA +264E FE0F ; emoji style; # (1.1) LIBRA +264F FE0E ; text style; # (1.1) SCORPIUS +264F FE0F ; emoji style; # (1.1) SCORPIUS +2650 FE0E ; text style; # (1.1) SAGITTARIUS +2650 FE0F ; emoji style; # (1.1) SAGITTARIUS +2651 FE0E ; text style; # (1.1) CAPRICORN +2651 FE0F ; emoji style; # (1.1) CAPRICORN +2652 FE0E ; text style; # (1.1) AQUARIUS +2652 FE0F ; emoji style; # (1.1) AQUARIUS +2653 FE0E ; text style; # (1.1) PISCES +2653 FE0F ; emoji style; # (1.1) PISCES +265F FE0E ; text style; # (1.1) BLACK CHESS PAWN +265F FE0F ; emoji style; # (1.1) BLACK CHESS PAWN +2660 FE0E ; text style; # (1.1) BLACK SPADE SUIT +2660 FE0F ; emoji style; # (1.1) BLACK SPADE SUIT +2663 FE0E ; text style; # (1.1) BLACK CLUB SUIT +2663 FE0F ; emoji style; # (1.1) BLACK CLUB SUIT +2665 FE0E ; text style; # (1.1) BLACK HEART SUIT +2665 FE0F ; emoji style; # (1.1) BLACK HEART SUIT +2666 FE0E ; text style; # (1.1) BLACK DIAMOND SUIT +2666 FE0F ; emoji style; # (1.1) BLACK DIAMOND SUIT +2668 FE0E ; text style; # (1.1) HOT SPRINGS +2668 FE0F ; emoji style; # (1.1) HOT SPRINGS +267B FE0E ; text style; # (3.2) BLACK UNIVERSAL RECYCLING SYMBOL +267B FE0F ; emoji style; # (3.2) BLACK UNIVERSAL RECYCLING SYMBOL +267E FE0E ; text style; # (4.1) PERMANENT PAPER SIGN +267E FE0F ; emoji style; # (4.1) PERMANENT PAPER SIGN +267F FE0E ; text style; # (4.1) WHEELCHAIR SYMBOL +267F FE0F ; emoji style; # (4.1) WHEELCHAIR SYMBOL +2692 FE0E ; text style; # (4.1) HAMMER AND PICK +2692 FE0F ; emoji style; # (4.1) HAMMER AND PICK +2693 FE0E ; text style; # (4.1) ANCHOR +2693 FE0F ; emoji style; # (4.1) ANCHOR +2694 FE0E ; text style; # (4.1) CROSSED SWORDS +2694 FE0F ; emoji style; # (4.1) CROSSED SWORDS +2695 FE0E ; text style; # (4.1) STAFF OF AESCULAPIUS +2695 FE0F ; emoji style; # (4.1) STAFF OF AESCULAPIUS +2696 FE0E ; text style; # (4.1) SCALES +2696 FE0F ; emoji style; # (4.1) SCALES +2697 FE0E ; text style; # (4.1) ALEMBIC +2697 FE0F ; emoji style; # (4.1) ALEMBIC +2699 FE0E ; text style; # (4.1) GEAR +2699 FE0F ; emoji style; # (4.1) GEAR +269B FE0E ; text style; # (4.1) ATOM SYMBOL +269B FE0F ; emoji style; # (4.1) ATOM SYMBOL +269C FE0E ; text style; # (4.1) FLEUR-DE-LIS +269C FE0F ; emoji style; # (4.1) FLEUR-DE-LIS +26A0 FE0E ; text style; # (4.0) WARNING SIGN +26A0 FE0F ; emoji style; # (4.0) WARNING SIGN +26A1 FE0E ; text style; # (4.0) HIGH VOLTAGE SIGN +26A1 FE0F ; emoji style; # (4.0) HIGH VOLTAGE SIGN +26A7 FE0E ; text style; # (4.1) MALE WITH STROKE AND MALE AND FEMALE SIGN +26A7 FE0F ; emoji style; # (4.1) MALE WITH STROKE AND MALE AND FEMALE SIGN +26AA FE0E ; text style; # (4.1) MEDIUM WHITE CIRCLE +26AA FE0F ; emoji style; # (4.1) MEDIUM WHITE CIRCLE +26AB FE0E ; text style; # (4.1) MEDIUM BLACK CIRCLE +26AB FE0F ; emoji style; # (4.1) MEDIUM BLACK CIRCLE +26B0 FE0E ; text style; # (4.1) COFFIN +26B0 FE0F ; emoji style; # (4.1) COFFIN +26B1 FE0E ; text style; # (4.1) FUNERAL URN +26B1 FE0F ; emoji style; # (4.1) FUNERAL URN +26BD FE0E ; text style; # (5.2) SOCCER BALL +26BD FE0F ; emoji style; # (5.2) SOCCER BALL +26BE FE0E ; text style; # (5.2) BASEBALL +26BE FE0F ; emoji style; # (5.2) BASEBALL +26C4 FE0E ; text style; # (5.2) SNOWMAN WITHOUT SNOW +26C4 FE0F ; emoji style; # (5.2) SNOWMAN WITHOUT SNOW +26C5 FE0E ; text style; # (5.2) SUN BEHIND CLOUD +26C5 FE0F ; emoji style; # (5.2) SUN BEHIND CLOUD +26C8 FE0E ; text style; # (5.2) THUNDER CLOUD AND RAIN +26C8 FE0F ; emoji style; # (5.2) THUNDER CLOUD AND RAIN +26CE FE0E ; text style; # (6.0) OPHIUCHUS +26CE FE0F ; emoji style; # (6.0) OPHIUCHUS +26CF FE0E ; text style; # (5.2) PICK +26CF FE0F ; emoji style; # (5.2) PICK +26D1 FE0E ; text style; # (5.2) HELMET WITH WHITE CROSS +26D1 FE0F ; emoji style; # (5.2) HELMET WITH WHITE CROSS +26D3 FE0E ; text style; # (5.2) CHAINS +26D3 FE0F ; emoji style; # (5.2) CHAINS +26D4 FE0E ; text style; # (5.2) NO ENTRY +26D4 FE0F ; emoji style; # (5.2) NO ENTRY +26E9 FE0E ; text style; # (5.2) SHINTO SHRINE +26E9 FE0F ; emoji style; # (5.2) SHINTO SHRINE +26EA FE0E ; text style; # (5.2) CHURCH +26EA FE0F ; emoji style; # (5.2) CHURCH +26F0 FE0E ; text style; # (5.2) MOUNTAIN +26F0 FE0F ; emoji style; # (5.2) MOUNTAIN +26F1 FE0E ; text style; # (5.2) UMBRELLA ON GROUND +26F1 FE0F ; emoji style; # (5.2) UMBRELLA ON GROUND +26F2 FE0E ; text style; # (5.2) FOUNTAIN +26F2 FE0F ; emoji style; # (5.2) FOUNTAIN +26F3 FE0E ; text style; # (5.2) FLAG IN HOLE +26F3 FE0F ; emoji style; # (5.2) FLAG IN HOLE +26F4 FE0E ; text style; # (5.2) FERRY +26F4 FE0F ; emoji style; # (5.2) FERRY +26F5 FE0E ; text style; # (5.2) SAILBOAT +26F5 FE0F ; emoji style; # (5.2) SAILBOAT +26F7 FE0E ; text style; # (5.2) SKIER +26F7 FE0F ; emoji style; # (5.2) SKIER +26F8 FE0E ; text style; # (5.2) ICE SKATE +26F8 FE0F ; emoji style; # (5.2) ICE SKATE +26F9 FE0E ; text style; # (5.2) PERSON WITH BALL +26F9 FE0F ; emoji style; # (5.2) PERSON WITH BALL +26FA FE0E ; text style; # (5.2) TENT +26FA FE0F ; emoji style; # (5.2) TENT +26FD FE0E ; text style; # (5.2) FUEL PUMP +26FD FE0F ; emoji style; # (5.2) FUEL PUMP +2702 FE0E ; text style; # (1.1) BLACK SCISSORS +2702 FE0F ; emoji style; # (1.1) BLACK SCISSORS +2705 FE0E ; text style; # (6.0) WHITE HEAVY CHECK MARK +2705 FE0F ; emoji style; # (6.0) WHITE HEAVY CHECK MARK +2708 FE0E ; text style; # (1.1) AIRPLANE +2708 FE0F ; emoji style; # (1.1) AIRPLANE +2709 FE0E ; text style; # (1.1) ENVELOPE +2709 FE0F ; emoji style; # (1.1) ENVELOPE +270A FE0E ; text style; # (6.0) RAISED FIST +270A FE0F ; emoji style; # (6.0) RAISED FIST +270B FE0E ; text style; # (6.0) RAISED HAND +270B FE0F ; emoji style; # (6.0) RAISED HAND +270C FE0E ; text style; # (1.1) VICTORY HAND +270C FE0F ; emoji style; # (1.1) VICTORY HAND +270D FE0E ; text style; # (1.1) WRITING HAND +270D FE0F ; emoji style; # (1.1) WRITING HAND +270F FE0E ; text style; # (1.1) PENCIL +270F FE0F ; emoji style; # (1.1) PENCIL +2712 FE0E ; text style; # (1.1) BLACK NIB +2712 FE0F ; emoji style; # (1.1) BLACK NIB +2714 FE0E ; text style; # (1.1) HEAVY CHECK MARK +2714 FE0F ; emoji style; # (1.1) HEAVY CHECK MARK +2716 FE0E ; text style; # (1.1) HEAVY MULTIPLICATION X +2716 FE0F ; emoji style; # (1.1) HEAVY MULTIPLICATION X +271D FE0E ; text style; # (1.1) LATIN CROSS +271D FE0F ; emoji style; # (1.1) LATIN CROSS +2721 FE0E ; text style; # (1.1) STAR OF DAVID +2721 FE0F ; emoji style; # (1.1) STAR OF DAVID +2728 FE0E ; text style; # (6.0) SPARKLES +2728 FE0F ; emoji style; # (6.0) SPARKLES +2733 FE0E ; text style; # (1.1) EIGHT SPOKED ASTERISK +2733 FE0F ; emoji style; # (1.1) EIGHT SPOKED ASTERISK +2734 FE0E ; text style; # (1.1) EIGHT POINTED BLACK STAR +2734 FE0F ; emoji style; # (1.1) EIGHT POINTED BLACK STAR +2744 FE0E ; text style; # (1.1) SNOWFLAKE +2744 FE0F ; emoji style; # (1.1) SNOWFLAKE +2747 FE0E ; text style; # (1.1) SPARKLE +2747 FE0F ; emoji style; # (1.1) SPARKLE +274C FE0E ; text style; # (6.0) CROSS MARK +274C FE0F ; emoji style; # (6.0) CROSS MARK +274E FE0E ; text style; # (6.0) NEGATIVE SQUARED CROSS MARK +274E FE0F ; emoji style; # (6.0) NEGATIVE SQUARED CROSS MARK +2753 FE0E ; text style; # (6.0) BLACK QUESTION MARK ORNAMENT +2753 FE0F ; emoji style; # (6.0) BLACK QUESTION MARK ORNAMENT +2754 FE0E ; text style; # (6.0) WHITE QUESTION MARK ORNAMENT +2754 FE0F ; emoji style; # (6.0) WHITE QUESTION MARK ORNAMENT +2755 FE0E ; text style; # (6.0) WHITE EXCLAMATION MARK ORNAMENT +2755 FE0F ; emoji style; # (6.0) WHITE EXCLAMATION MARK ORNAMENT +2757 FE0E ; text style; # (5.2) HEAVY EXCLAMATION MARK SYMBOL +2757 FE0F ; emoji style; # (5.2) HEAVY EXCLAMATION MARK SYMBOL +2763 FE0E ; text style; # (1.1) HEAVY HEART EXCLAMATION MARK ORNAMENT +2763 FE0F ; emoji style; # (1.1) HEAVY HEART EXCLAMATION MARK ORNAMENT +2764 FE0E ; text style; # (1.1) HEAVY BLACK HEART +2764 FE0F ; emoji style; # (1.1) HEAVY BLACK HEART +2795 FE0E ; text style; # (6.0) HEAVY PLUS SIGN +2795 FE0F ; emoji style; # (6.0) HEAVY PLUS SIGN +2796 FE0E ; text style; # (6.0) HEAVY MINUS SIGN +2796 FE0F ; emoji style; # (6.0) HEAVY MINUS SIGN +2797 FE0E ; text style; # (6.0) HEAVY DIVISION SIGN +2797 FE0F ; emoji style; # (6.0) HEAVY DIVISION SIGN +27A1 FE0E ; text style; # (1.1) BLACK RIGHTWARDS ARROW +27A1 FE0F ; emoji style; # (1.1) BLACK RIGHTWARDS ARROW +27B0 FE0E ; text style; # (6.0) CURLY LOOP +27B0 FE0F ; emoji style; # (6.0) CURLY LOOP +27BF FE0E ; text style; # (6.0) DOUBLE CURLY LOOP +27BF FE0F ; emoji style; # (6.0) DOUBLE CURLY LOOP +2934 FE0E ; text style; # (3.2) ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS +2934 FE0F ; emoji style; # (3.2) ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS +2935 FE0E ; text style; # (3.2) ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS +2935 FE0F ; emoji style; # (3.2) ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS +2B05 FE0E ; text style; # (4.0) LEFTWARDS BLACK ARROW +2B05 FE0F ; emoji style; # (4.0) LEFTWARDS BLACK ARROW +2B06 FE0E ; text style; # (4.0) UPWARDS BLACK ARROW +2B06 FE0F ; emoji style; # (4.0) UPWARDS BLACK ARROW +2B07 FE0E ; text style; # (4.0) DOWNWARDS BLACK ARROW +2B07 FE0F ; emoji style; # (4.0) DOWNWARDS BLACK ARROW +2B1B FE0E ; text style; # (5.1) BLACK LARGE SQUARE +2B1B FE0F ; emoji style; # (5.1) BLACK LARGE SQUARE +2B1C FE0E ; text style; # (5.1) WHITE LARGE SQUARE +2B1C FE0F ; emoji style; # (5.1) WHITE LARGE SQUARE +2B50 FE0E ; text style; # (5.1) WHITE MEDIUM STAR +2B50 FE0F ; emoji style; # (5.1) WHITE MEDIUM STAR +2B55 FE0E ; text style; # (5.2) HEAVY LARGE CIRCLE +2B55 FE0F ; emoji style; # (5.2) HEAVY LARGE CIRCLE +3030 FE0E ; text style; # (1.1) WAVY DASH +3030 FE0F ; emoji style; # (1.1) WAVY DASH +303D FE0E ; text style; # (3.2) PART ALTERNATION MARK +303D FE0F ; emoji style; # (3.2) PART ALTERNATION MARK +3297 FE0E ; text style; # (1.1) CIRCLED IDEOGRAPH CONGRATULATION +3297 FE0F ; emoji style; # (1.1) CIRCLED IDEOGRAPH CONGRATULATION +3299 FE0E ; text style; # (1.1) CIRCLED IDEOGRAPH SECRET +3299 FE0F ; emoji style; # (1.1) CIRCLED IDEOGRAPH SECRET +1F004 FE0E ; text style; # (5.1) MAHJONG TILE RED DRAGON +1F004 FE0F ; emoji style; # (5.1) MAHJONG TILE RED DRAGON +1F170 FE0E ; text style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER A +1F170 FE0F ; emoji style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER A +1F171 FE0E ; text style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER B +1F171 FE0F ; emoji style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER B +1F17E FE0E ; text style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER O +1F17E FE0F ; emoji style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER O +1F17F FE0E ; text style; # (5.2) NEGATIVE SQUARED LATIN CAPITAL LETTER P +1F17F FE0F ; emoji style; # (5.2) NEGATIVE SQUARED LATIN CAPITAL LETTER P +1F202 FE0E ; text style; # (6.0) SQUARED KATAKANA SA +1F202 FE0F ; emoji style; # (6.0) SQUARED KATAKANA SA +1F21A FE0E ; text style; # (5.2) SQUARED CJK UNIFIED IDEOGRAPH-7121 +1F21A FE0F ; emoji style; # (5.2) SQUARED CJK UNIFIED IDEOGRAPH-7121 +1F22F FE0E ; text style; # (5.2) SQUARED CJK UNIFIED IDEOGRAPH-6307 +1F22F FE0F ; emoji style; # (5.2) SQUARED CJK UNIFIED IDEOGRAPH-6307 +1F237 FE0E ; text style; # (6.0) SQUARED CJK UNIFIED IDEOGRAPH-6708 +1F237 FE0F ; emoji style; # (6.0) SQUARED CJK UNIFIED IDEOGRAPH-6708 +1F30D FE0E ; text style; # (6.0) EARTH GLOBE EUROPE-AFRICA +1F30D FE0F ; emoji style; # (6.0) EARTH GLOBE EUROPE-AFRICA +1F30E FE0E ; text style; # (6.0) EARTH GLOBE AMERICAS +1F30E FE0F ; emoji style; # (6.0) EARTH GLOBE AMERICAS +1F30F FE0E ; text style; # (6.0) EARTH GLOBE ASIA-AUSTRALIA +1F30F FE0F ; emoji style; # (6.0) EARTH GLOBE ASIA-AUSTRALIA +1F315 FE0E ; text style; # (6.0) FULL MOON SYMBOL +1F315 FE0F ; emoji style; # (6.0) FULL MOON SYMBOL +1F31C FE0E ; text style; # (6.0) LAST QUARTER MOON WITH FACE +1F31C FE0F ; emoji style; # (6.0) LAST QUARTER MOON WITH FACE +1F321 FE0E ; text style; # (7.0) THERMOMETER +1F321 FE0F ; emoji style; # (7.0) THERMOMETER +1F324 FE0E ; text style; # (7.0) WHITE SUN WITH SMALL CLOUD +1F324 FE0F ; emoji style; # (7.0) WHITE SUN WITH SMALL CLOUD +1F325 FE0E ; text style; # (7.0) WHITE SUN BEHIND CLOUD +1F325 FE0F ; emoji style; # (7.0) WHITE SUN BEHIND CLOUD +1F326 FE0E ; text style; # (7.0) WHITE SUN BEHIND CLOUD WITH RAIN +1F326 FE0F ; emoji style; # (7.0) WHITE SUN BEHIND CLOUD WITH RAIN +1F327 FE0E ; text style; # (7.0) CLOUD WITH RAIN +1F327 FE0F ; emoji style; # (7.0) CLOUD WITH RAIN +1F328 FE0E ; text style; # (7.0) CLOUD WITH SNOW +1F328 FE0F ; emoji style; # (7.0) CLOUD WITH SNOW +1F329 FE0E ; text style; # (7.0) CLOUD WITH LIGHTNING +1F329 FE0F ; emoji style; # (7.0) CLOUD WITH LIGHTNING +1F32A FE0E ; text style; # (7.0) CLOUD WITH TORNADO +1F32A FE0F ; emoji style; # (7.0) CLOUD WITH TORNADO +1F32B FE0E ; text style; # (7.0) FOG +1F32B FE0F ; emoji style; # (7.0) FOG +1F32C FE0E ; text style; # (7.0) WIND BLOWING FACE +1F32C FE0F ; emoji style; # (7.0) WIND BLOWING FACE +1F336 FE0E ; text style; # (7.0) HOT PEPPER +1F336 FE0F ; emoji style; # (7.0) HOT PEPPER +1F378 FE0E ; text style; # (6.0) COCKTAIL GLASS +1F378 FE0F ; emoji style; # (6.0) COCKTAIL GLASS +1F37D FE0E ; text style; # (7.0) FORK AND KNIFE WITH PLATE +1F37D FE0F ; emoji style; # (7.0) FORK AND KNIFE WITH PLATE +1F393 FE0E ; text style; # (6.0) GRADUATION CAP +1F393 FE0F ; emoji style; # (6.0) GRADUATION CAP +1F396 FE0E ; text style; # (7.0) MILITARY MEDAL +1F396 FE0F ; emoji style; # (7.0) MILITARY MEDAL +1F397 FE0E ; text style; # (7.0) REMINDER RIBBON +1F397 FE0F ; emoji style; # (7.0) REMINDER RIBBON +1F399 FE0E ; text style; # (7.0) STUDIO MICROPHONE +1F399 FE0F ; emoji style; # (7.0) STUDIO MICROPHONE +1F39A FE0E ; text style; # (7.0) LEVEL SLIDER +1F39A FE0F ; emoji style; # (7.0) LEVEL SLIDER +1F39B FE0E ; text style; # (7.0) CONTROL KNOBS +1F39B FE0F ; emoji style; # (7.0) CONTROL KNOBS +1F39E FE0E ; text style; # (7.0) FILM FRAMES +1F39E FE0F ; emoji style; # (7.0) FILM FRAMES +1F39F FE0E ; text style; # (7.0) ADMISSION TICKETS +1F39F FE0F ; emoji style; # (7.0) ADMISSION TICKETS +1F3A7 FE0E ; text style; # (6.0) HEADPHONE +1F3A7 FE0F ; emoji style; # (6.0) HEADPHONE +1F3AC FE0E ; text style; # (6.0) CLAPPER BOARD +1F3AC FE0F ; emoji style; # (6.0) CLAPPER BOARD +1F3AD FE0E ; text style; # (6.0) PERFORMING ARTS +1F3AD FE0F ; emoji style; # (6.0) PERFORMING ARTS +1F3AE FE0E ; text style; # (6.0) VIDEO GAME +1F3AE FE0F ; emoji style; # (6.0) VIDEO GAME +1F3C2 FE0E ; text style; # (6.0) SNOWBOARDER +1F3C2 FE0F ; emoji style; # (6.0) SNOWBOARDER +1F3C4 FE0E ; text style; # (6.0) SURFER +1F3C4 FE0F ; emoji style; # (6.0) SURFER +1F3C6 FE0E ; text style; # (6.0) TROPHY +1F3C6 FE0F ; emoji style; # (6.0) TROPHY +1F3CA FE0E ; text style; # (6.0) SWIMMER +1F3CA FE0F ; emoji style; # (6.0) SWIMMER +1F3CB FE0E ; text style; # (7.0) WEIGHT LIFTER +1F3CB FE0F ; emoji style; # (7.0) WEIGHT LIFTER +1F3CC FE0E ; text style; # (7.0) GOLFER +1F3CC FE0F ; emoji style; # (7.0) GOLFER +1F3CD FE0E ; text style; # (7.0) RACING MOTORCYCLE +1F3CD FE0F ; emoji style; # (7.0) RACING MOTORCYCLE +1F3CE FE0E ; text style; # (7.0) RACING CAR +1F3CE FE0F ; emoji style; # (7.0) RACING CAR +1F3D4 FE0E ; text style; # (7.0) SNOW CAPPED MOUNTAIN +1F3D4 FE0F ; emoji style; # (7.0) SNOW CAPPED MOUNTAIN +1F3D5 FE0E ; text style; # (7.0) CAMPING +1F3D5 FE0F ; emoji style; # (7.0) CAMPING +1F3D6 FE0E ; text style; # (7.0) BEACH WITH UMBRELLA +1F3D6 FE0F ; emoji style; # (7.0) BEACH WITH UMBRELLA +1F3D7 FE0E ; text style; # (7.0) BUILDING CONSTRUCTION +1F3D7 FE0F ; emoji style; # (7.0) BUILDING CONSTRUCTION +1F3D8 FE0E ; text style; # (7.0) HOUSE BUILDINGS +1F3D8 FE0F ; emoji style; # (7.0) HOUSE BUILDINGS +1F3D9 FE0E ; text style; # (7.0) CITYSCAPE +1F3D9 FE0F ; emoji style; # (7.0) CITYSCAPE +1F3DA FE0E ; text style; # (7.0) DERELICT HOUSE BUILDING +1F3DA FE0F ; emoji style; # (7.0) DERELICT HOUSE BUILDING +1F3DB FE0E ; text style; # (7.0) CLASSICAL BUILDING +1F3DB FE0F ; emoji style; # (7.0) CLASSICAL BUILDING +1F3DC FE0E ; text style; # (7.0) DESERT +1F3DC FE0F ; emoji style; # (7.0) DESERT +1F3DD FE0E ; text style; # (7.0) DESERT ISLAND +1F3DD FE0F ; emoji style; # (7.0) DESERT ISLAND +1F3DE FE0E ; text style; # (7.0) NATIONAL PARK +1F3DE FE0F ; emoji style; # (7.0) NATIONAL PARK +1F3DF FE0E ; text style; # (7.0) STADIUM +1F3DF FE0F ; emoji style; # (7.0) STADIUM +1F3E0 FE0E ; text style; # (6.0) HOUSE BUILDING +1F3E0 FE0F ; emoji style; # (6.0) HOUSE BUILDING +1F3ED FE0E ; text style; # (6.0) FACTORY +1F3ED FE0F ; emoji style; # (6.0) FACTORY +1F3F3 FE0E ; text style; # (7.0) WAVING WHITE FLAG +1F3F3 FE0F ; emoji style; # (7.0) WAVING WHITE FLAG +1F3F5 FE0E ; text style; # (7.0) ROSETTE +1F3F5 FE0F ; emoji style; # (7.0) ROSETTE +1F3F7 FE0E ; text style; # (7.0) LABEL +1F3F7 FE0F ; emoji style; # (7.0) LABEL +1F408 FE0E ; text style; # (6.0) CAT +1F408 FE0F ; emoji style; # (6.0) CAT +1F415 FE0E ; text style; # (6.0) DOG +1F415 FE0F ; emoji style; # (6.0) DOG +1F41F FE0E ; text style; # (6.0) FISH +1F41F FE0F ; emoji style; # (6.0) FISH +1F426 FE0E ; text style; # (6.0) BIRD +1F426 FE0F ; emoji style; # (6.0) BIRD +1F43F FE0E ; text style; # (7.0) CHIPMUNK +1F43F FE0F ; emoji style; # (7.0) CHIPMUNK +1F441 FE0E ; text style; # (7.0) EYE +1F441 FE0F ; emoji style; # (7.0) EYE +1F442 FE0E ; text style; # (6.0) EAR +1F442 FE0F ; emoji style; # (6.0) EAR +1F446 FE0E ; text style; # (6.0) WHITE UP POINTING BACKHAND INDEX +1F446 FE0F ; emoji style; # (6.0) WHITE UP POINTING BACKHAND INDEX +1F447 FE0E ; text style; # (6.0) WHITE DOWN POINTING BACKHAND INDEX +1F447 FE0F ; emoji style; # (6.0) WHITE DOWN POINTING BACKHAND INDEX +1F448 FE0E ; text style; # (6.0) WHITE LEFT POINTING BACKHAND INDEX +1F448 FE0F ; emoji style; # (6.0) WHITE LEFT POINTING BACKHAND INDEX +1F449 FE0E ; text style; # (6.0) WHITE RIGHT POINTING BACKHAND INDEX +1F449 FE0F ; emoji style; # (6.0) WHITE RIGHT POINTING BACKHAND INDEX +1F44D FE0E ; text style; # (6.0) THUMBS UP SIGN +1F44D FE0F ; emoji style; # (6.0) THUMBS UP SIGN +1F44E FE0E ; text style; # (6.0) THUMBS DOWN SIGN +1F44E FE0F ; emoji style; # (6.0) THUMBS DOWN SIGN +1F453 FE0E ; text style; # (6.0) EYEGLASSES +1F453 FE0F ; emoji style; # (6.0) EYEGLASSES +1F46A FE0E ; text style; # (6.0) FAMILY +1F46A FE0F ; emoji style; # (6.0) FAMILY +1F47D FE0E ; text style; # (6.0) EXTRATERRESTRIAL ALIEN +1F47D FE0F ; emoji style; # (6.0) EXTRATERRESTRIAL ALIEN +1F4A3 FE0E ; text style; # (6.0) BOMB +1F4A3 FE0F ; emoji style; # (6.0) BOMB +1F4B0 FE0E ; text style; # (6.0) MONEY BAG +1F4B0 FE0F ; emoji style; # (6.0) MONEY BAG +1F4B3 FE0E ; text style; # (6.0) CREDIT CARD +1F4B3 FE0F ; emoji style; # (6.0) CREDIT CARD +1F4BB FE0E ; text style; # (6.0) PERSONAL COMPUTER +1F4BB FE0F ; emoji style; # (6.0) PERSONAL COMPUTER +1F4BF FE0E ; text style; # (6.0) OPTICAL DISC +1F4BF FE0F ; emoji style; # (6.0) OPTICAL DISC +1F4CB FE0E ; text style; # (6.0) CLIPBOARD +1F4CB FE0F ; emoji style; # (6.0) CLIPBOARD +1F4DA FE0E ; text style; # (6.0) BOOKS +1F4DA FE0F ; emoji style; # (6.0) BOOKS +1F4DF FE0E ; text style; # (6.0) PAGER +1F4DF FE0F ; emoji style; # (6.0) PAGER +1F4E4 FE0E ; text style; # (6.0) OUTBOX TRAY +1F4E4 FE0F ; emoji style; # (6.0) OUTBOX TRAY +1F4E5 FE0E ; text style; # (6.0) INBOX TRAY +1F4E5 FE0F ; emoji style; # (6.0) INBOX TRAY +1F4E6 FE0E ; text style; # (6.0) PACKAGE +1F4E6 FE0F ; emoji style; # (6.0) PACKAGE +1F4EA FE0E ; text style; # (6.0) CLOSED MAILBOX WITH LOWERED FLAG +1F4EA FE0F ; emoji style; # (6.0) CLOSED MAILBOX WITH LOWERED FLAG +1F4EB FE0E ; text style; # (6.0) CLOSED MAILBOX WITH RAISED FLAG +1F4EB FE0F ; emoji style; # (6.0) CLOSED MAILBOX WITH RAISED FLAG +1F4EC FE0E ; text style; # (6.0) OPEN MAILBOX WITH RAISED FLAG +1F4EC FE0F ; emoji style; # (6.0) OPEN MAILBOX WITH RAISED FLAG +1F4ED FE0E ; text style; # (6.0) OPEN MAILBOX WITH LOWERED FLAG +1F4ED FE0F ; emoji style; # (6.0) OPEN MAILBOX WITH LOWERED FLAG +1F4F7 FE0E ; text style; # (6.0) CAMERA +1F4F7 FE0F ; emoji style; # (6.0) CAMERA +1F4F9 FE0E ; text style; # (6.0) VIDEO CAMERA +1F4F9 FE0F ; emoji style; # (6.0) VIDEO CAMERA +1F4FA FE0E ; text style; # (6.0) TELEVISION +1F4FA FE0F ; emoji style; # (6.0) TELEVISION +1F4FB FE0E ; text style; # (6.0) RADIO +1F4FB FE0F ; emoji style; # (6.0) RADIO +1F4FD FE0E ; text style; # (7.0) FILM PROJECTOR +1F4FD FE0F ; emoji style; # (7.0) FILM PROJECTOR +1F508 FE0E ; text style; # (6.0) SPEAKER +1F508 FE0F ; emoji style; # (6.0) SPEAKER +1F50D FE0E ; text style; # (6.0) LEFT-POINTING MAGNIFYING GLASS +1F50D FE0F ; emoji style; # (6.0) LEFT-POINTING MAGNIFYING GLASS +1F512 FE0E ; text style; # (6.0) LOCK +1F512 FE0F ; emoji style; # (6.0) LOCK +1F513 FE0E ; text style; # (6.0) OPEN LOCK +1F513 FE0F ; emoji style; # (6.0) OPEN LOCK +1F549 FE0E ; text style; # (7.0) OM SYMBOL +1F549 FE0F ; emoji style; # (7.0) OM SYMBOL +1F54A FE0E ; text style; # (7.0) DOVE OF PEACE +1F54A FE0F ; emoji style; # (7.0) DOVE OF PEACE +1F550 FE0E ; text style; # (6.0) CLOCK FACE ONE OCLOCK +1F550 FE0F ; emoji style; # (6.0) CLOCK FACE ONE OCLOCK +1F551 FE0E ; text style; # (6.0) CLOCK FACE TWO OCLOCK +1F551 FE0F ; emoji style; # (6.0) CLOCK FACE TWO OCLOCK +1F552 FE0E ; text style; # (6.0) CLOCK FACE THREE OCLOCK +1F552 FE0F ; emoji style; # (6.0) CLOCK FACE THREE OCLOCK +1F553 FE0E ; text style; # (6.0) CLOCK FACE FOUR OCLOCK +1F553 FE0F ; emoji style; # (6.0) CLOCK FACE FOUR OCLOCK +1F554 FE0E ; text style; # (6.0) CLOCK FACE FIVE OCLOCK +1F554 FE0F ; emoji style; # (6.0) CLOCK FACE FIVE OCLOCK +1F555 FE0E ; text style; # (6.0) CLOCK FACE SIX OCLOCK +1F555 FE0F ; emoji style; # (6.0) CLOCK FACE SIX OCLOCK +1F556 FE0E ; text style; # (6.0) CLOCK FACE SEVEN OCLOCK +1F556 FE0F ; emoji style; # (6.0) CLOCK FACE SEVEN OCLOCK +1F557 FE0E ; text style; # (6.0) CLOCK FACE EIGHT OCLOCK +1F557 FE0F ; emoji style; # (6.0) CLOCK FACE EIGHT OCLOCK +1F558 FE0E ; text style; # (6.0) CLOCK FACE NINE OCLOCK +1F558 FE0F ; emoji style; # (6.0) CLOCK FACE NINE OCLOCK +1F559 FE0E ; text style; # (6.0) CLOCK FACE TEN OCLOCK +1F559 FE0F ; emoji style; # (6.0) CLOCK FACE TEN OCLOCK +1F55A FE0E ; text style; # (6.0) CLOCK FACE ELEVEN OCLOCK +1F55A FE0F ; emoji style; # (6.0) CLOCK FACE ELEVEN OCLOCK +1F55B FE0E ; text style; # (6.0) CLOCK FACE TWELVE OCLOCK +1F55B FE0F ; emoji style; # (6.0) CLOCK FACE TWELVE OCLOCK +1F55C FE0E ; text style; # (6.0) CLOCK FACE ONE-THIRTY +1F55C FE0F ; emoji style; # (6.0) CLOCK FACE ONE-THIRTY +1F55D FE0E ; text style; # (6.0) CLOCK FACE TWO-THIRTY +1F55D FE0F ; emoji style; # (6.0) CLOCK FACE TWO-THIRTY +1F55E FE0E ; text style; # (6.0) CLOCK FACE THREE-THIRTY +1F55E FE0F ; emoji style; # (6.0) CLOCK FACE THREE-THIRTY +1F55F FE0E ; text style; # (6.0) CLOCK FACE FOUR-THIRTY +1F55F FE0F ; emoji style; # (6.0) CLOCK FACE FOUR-THIRTY +1F560 FE0E ; text style; # (6.0) CLOCK FACE FIVE-THIRTY +1F560 FE0F ; emoji style; # (6.0) CLOCK FACE FIVE-THIRTY +1F561 FE0E ; text style; # (6.0) CLOCK FACE SIX-THIRTY +1F561 FE0F ; emoji style; # (6.0) CLOCK FACE SIX-THIRTY +1F562 FE0E ; text style; # (6.0) CLOCK FACE SEVEN-THIRTY +1F562 FE0F ; emoji style; # (6.0) CLOCK FACE SEVEN-THIRTY +1F563 FE0E ; text style; # (6.0) CLOCK FACE EIGHT-THIRTY +1F563 FE0F ; emoji style; # (6.0) CLOCK FACE EIGHT-THIRTY +1F564 FE0E ; text style; # (6.0) CLOCK FACE NINE-THIRTY +1F564 FE0F ; emoji style; # (6.0) CLOCK FACE NINE-THIRTY +1F565 FE0E ; text style; # (6.0) CLOCK FACE TEN-THIRTY +1F565 FE0F ; emoji style; # (6.0) CLOCK FACE TEN-THIRTY +1F566 FE0E ; text style; # (6.0) CLOCK FACE ELEVEN-THIRTY +1F566 FE0F ; emoji style; # (6.0) CLOCK FACE ELEVEN-THIRTY +1F567 FE0E ; text style; # (6.0) CLOCK FACE TWELVE-THIRTY +1F567 FE0F ; emoji style; # (6.0) CLOCK FACE TWELVE-THIRTY +1F56F FE0E ; text style; # (7.0) CANDLE +1F56F FE0F ; emoji style; # (7.0) CANDLE +1F570 FE0E ; text style; # (7.0) MANTELPIECE CLOCK +1F570 FE0F ; emoji style; # (7.0) MANTELPIECE CLOCK +1F573 FE0E ; text style; # (7.0) HOLE +1F573 FE0F ; emoji style; # (7.0) HOLE +1F574 FE0E ; text style; # (7.0) MAN IN BUSINESS SUIT LEVITATING +1F574 FE0F ; emoji style; # (7.0) MAN IN BUSINESS SUIT LEVITATING +1F575 FE0E ; text style; # (7.0) SLEUTH OR SPY +1F575 FE0F ; emoji style; # (7.0) SLEUTH OR SPY +1F576 FE0E ; text style; # (7.0) DARK SUNGLASSES +1F576 FE0F ; emoji style; # (7.0) DARK SUNGLASSES +1F577 FE0E ; text style; # (7.0) SPIDER +1F577 FE0F ; emoji style; # (7.0) SPIDER +1F578 FE0E ; text style; # (7.0) SPIDER WEB +1F578 FE0F ; emoji style; # (7.0) SPIDER WEB +1F579 FE0E ; text style; # (7.0) JOYSTICK +1F579 FE0F ; emoji style; # (7.0) JOYSTICK +1F587 FE0E ; text style; # (7.0) LINKED PAPERCLIPS +1F587 FE0F ; emoji style; # (7.0) LINKED PAPERCLIPS +1F58A FE0E ; text style; # (7.0) LOWER LEFT BALLPOINT PEN +1F58A FE0F ; emoji style; # (7.0) LOWER LEFT BALLPOINT PEN +1F58B FE0E ; text style; # (7.0) LOWER LEFT FOUNTAIN PEN +1F58B FE0F ; emoji style; # (7.0) LOWER LEFT FOUNTAIN PEN +1F58C FE0E ; text style; # (7.0) LOWER LEFT PAINTBRUSH +1F58C FE0F ; emoji style; # (7.0) LOWER LEFT PAINTBRUSH +1F58D FE0E ; text style; # (7.0) LOWER LEFT CRAYON +1F58D FE0F ; emoji style; # (7.0) LOWER LEFT CRAYON +1F590 FE0E ; text style; # (7.0) RAISED HAND WITH FINGERS SPLAYED +1F590 FE0F ; emoji style; # (7.0) RAISED HAND WITH FINGERS SPLAYED +1F5A5 FE0E ; text style; # (7.0) DESKTOP COMPUTER +1F5A5 FE0F ; emoji style; # (7.0) DESKTOP COMPUTER +1F5A8 FE0E ; text style; # (7.0) PRINTER +1F5A8 FE0F ; emoji style; # (7.0) PRINTER +1F5B1 FE0E ; text style; # (7.0) THREE BUTTON MOUSE +1F5B1 FE0F ; emoji style; # (7.0) THREE BUTTON MOUSE +1F5B2 FE0E ; text style; # (7.0) TRACKBALL +1F5B2 FE0F ; emoji style; # (7.0) TRACKBALL +1F5BC FE0E ; text style; # (7.0) FRAME WITH PICTURE +1F5BC FE0F ; emoji style; # (7.0) FRAME WITH PICTURE +1F5C2 FE0E ; text style; # (7.0) CARD INDEX DIVIDERS +1F5C2 FE0F ; emoji style; # (7.0) CARD INDEX DIVIDERS +1F5C3 FE0E ; text style; # (7.0) CARD FILE BOX +1F5C3 FE0F ; emoji style; # (7.0) CARD FILE BOX +1F5C4 FE0E ; text style; # (7.0) FILE CABINET +1F5C4 FE0F ; emoji style; # (7.0) FILE CABINET +1F5D1 FE0E ; text style; # (7.0) WASTEBASKET +1F5D1 FE0F ; emoji style; # (7.0) WASTEBASKET +1F5D2 FE0E ; text style; # (7.0) SPIRAL NOTE PAD +1F5D2 FE0F ; emoji style; # (7.0) SPIRAL NOTE PAD +1F5D3 FE0E ; text style; # (7.0) SPIRAL CALENDAR PAD +1F5D3 FE0F ; emoji style; # (7.0) SPIRAL CALENDAR PAD +1F5DC FE0E ; text style; # (7.0) COMPRESSION +1F5DC FE0F ; emoji style; # (7.0) COMPRESSION +1F5DD FE0E ; text style; # (7.0) OLD KEY +1F5DD FE0F ; emoji style; # (7.0) OLD KEY +1F5DE FE0E ; text style; # (7.0) ROLLED-UP NEWSPAPER +1F5DE FE0F ; emoji style; # (7.0) ROLLED-UP NEWSPAPER +1F5E1 FE0E ; text style; # (7.0) DAGGER KNIFE +1F5E1 FE0F ; emoji style; # (7.0) DAGGER KNIFE +1F5E3 FE0E ; text style; # (7.0) SPEAKING HEAD IN SILHOUETTE +1F5E3 FE0F ; emoji style; # (7.0) SPEAKING HEAD IN SILHOUETTE +1F5E8 FE0E ; text style; # (7.0) LEFT SPEECH BUBBLE +1F5E8 FE0F ; emoji style; # (7.0) LEFT SPEECH BUBBLE +1F5EF FE0E ; text style; # (7.0) RIGHT ANGER BUBBLE +1F5EF FE0F ; emoji style; # (7.0) RIGHT ANGER BUBBLE +1F5F3 FE0E ; text style; # (7.0) BALLOT BOX WITH BALLOT +1F5F3 FE0F ; emoji style; # (7.0) BALLOT BOX WITH BALLOT +1F5FA FE0E ; text style; # (7.0) WORLD MAP +1F5FA FE0F ; emoji style; # (7.0) WORLD MAP +1F610 FE0E ; text style; # (6.0) NEUTRAL FACE +1F610 FE0F ; emoji style; # (6.0) NEUTRAL FACE +1F687 FE0E ; text style; # (6.0) METRO +1F687 FE0F ; emoji style; # (6.0) METRO +1F68D FE0E ; text style; # (6.0) ONCOMING BUS +1F68D FE0F ; emoji style; # (6.0) ONCOMING BUS +1F691 FE0E ; text style; # (6.0) AMBULANCE +1F691 FE0F ; emoji style; # (6.0) AMBULANCE +1F694 FE0E ; text style; # (6.0) ONCOMING POLICE CAR +1F694 FE0F ; emoji style; # (6.0) ONCOMING POLICE CAR +1F698 FE0E ; text style; # (6.0) ONCOMING AUTOMOBILE +1F698 FE0F ; emoji style; # (6.0) ONCOMING AUTOMOBILE +1F6AD FE0E ; text style; # (6.0) NO SMOKING SYMBOL +1F6AD FE0F ; emoji style; # (6.0) NO SMOKING SYMBOL +1F6B2 FE0E ; text style; # (6.0) BICYCLE +1F6B2 FE0F ; emoji style; # (6.0) BICYCLE +1F6B9 FE0E ; text style; # (6.0) MENS SYMBOL +1F6B9 FE0F ; emoji style; # (6.0) MENS SYMBOL +1F6BA FE0E ; text style; # (6.0) WOMENS SYMBOL +1F6BA FE0F ; emoji style; # (6.0) WOMENS SYMBOL +1F6BC FE0E ; text style; # (6.0) BABY SYMBOL +1F6BC FE0F ; emoji style; # (6.0) BABY SYMBOL +1F6CB FE0E ; text style; # (7.0) COUCH AND LAMP +1F6CB FE0F ; emoji style; # (7.0) COUCH AND LAMP +1F6CD FE0E ; text style; # (7.0) SHOPPING BAGS +1F6CD FE0F ; emoji style; # (7.0) SHOPPING BAGS +1F6CE FE0E ; text style; # (7.0) BELLHOP BELL +1F6CE FE0F ; emoji style; # (7.0) BELLHOP BELL +1F6CF FE0E ; text style; # (7.0) BED +1F6CF FE0F ; emoji style; # (7.0) BED +1F6E0 FE0E ; text style; # (7.0) HAMMER AND WRENCH +1F6E0 FE0F ; emoji style; # (7.0) HAMMER AND WRENCH +1F6E1 FE0E ; text style; # (7.0) SHIELD +1F6E1 FE0F ; emoji style; # (7.0) SHIELD +1F6E2 FE0E ; text style; # (7.0) OIL DRUM +1F6E2 FE0F ; emoji style; # (7.0) OIL DRUM +1F6E3 FE0E ; text style; # (7.0) MOTORWAY +1F6E3 FE0F ; emoji style; # (7.0) MOTORWAY +1F6E4 FE0E ; text style; # (7.0) RAILWAY TRACK +1F6E4 FE0F ; emoji style; # (7.0) RAILWAY TRACK +1F6E5 FE0E ; text style; # (7.0) MOTOR BOAT +1F6E5 FE0F ; emoji style; # (7.0) MOTOR BOAT +1F6E9 FE0E ; text style; # (7.0) SMALL AIRPLANE +1F6E9 FE0F ; emoji style; # (7.0) SMALL AIRPLANE +1F6F0 FE0E ; text style; # (7.0) SATELLITE +1F6F0 FE0F ; emoji style; # (7.0) SATELLITE +1F6F3 FE0E ; text style; # (7.0) PASSENGER SHIP +1F6F3 FE0F ; emoji style; # (7.0) PASSENGER SHIP + +#Total sequences: 371 + +#EOF diff --git a/uri.c b/uri.c new file mode 100644 index 0000000..e09f7a9 --- /dev/null +++ b/uri.c @@ -0,0 +1,271 @@ +#include "uri.h" + +#include +#include +#include +#include + +#define LOG_MODULE "uri" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "util.h" +#include "xmalloc.h" + +bool +uri_parse(const char *uri, size_t len, + char **scheme, char **user, char **password, char **host, + uint16_t *port, char **path, char **query, char **fragment) +{ + LOG_DBG("parse URI: \"%.*s\"", (int)len, uri); + + if (scheme != NULL) *scheme = NULL; + if (user != NULL) *user = NULL; + if (password != NULL) *password = NULL; + if (host != NULL) *host = NULL; + if (port != NULL) *port = 0; + if (path != NULL) *path = NULL; + if (query != NULL) *query = NULL; + if (fragment != NULL) *fragment = NULL; + + size_t left = len; + const char *start = uri; + const char *end = NULL; + + if ((end = memchr(start, ':', left)) == NULL) + goto err; + + size_t scheme_len = end - start; + if (scheme_len == 0) + goto err; + + if (scheme != NULL) + *scheme = xstrndup(start, scheme_len); + + LOG_DBG("scheme: \"%.*s\"", (int)scheme_len, start); + + start = end + 1; + left = len - (start - uri); + + /* Authinfo */ + if (left >= 2 && start[0] == '/' && start[1] == '/') { + start += 2; + left -= 2; + + /* [user[:password]@]@host[:port] */ + + /* Find beginning of path segment (required component + * following the authinfo) */ + const char *path_segment = memchr(start, '/', left); + if (path_segment == NULL) + goto err; + + size_t auth_left = path_segment - start; + + /* Do we have a user (and optionally a password)? */ + const char *user_pw_end = memchr(start, '@', auth_left); + if (user_pw_end != NULL) { + size_t user_pw_len = user_pw_end - start; + + /* Do we have a password? */ + const char *user_end = memchr(start, ':', user_pw_end - start); + if (user_end != NULL) { + size_t user_len = user_end - start; + if (user_len == 0) + goto err; + + if (user != NULL) + *user = xstrndup(start, user_len); + + const char *pw = user_end + 1; + size_t pw_len = user_pw_end - pw; + if (pw_len == 0) + goto err; + + if (password != NULL) + *password = xstrndup(pw, pw_len); + + LOG_DBG("user: \"%.*s\"", (int)user_len, start); + LOG_DBG("password: \"%.*s\"", (int)pw_len, pw); + } else { + size_t user_len = user_pw_end - start; + if (user_len == 0) + goto err; + + if (user != NULL) + *user = xstrndup(start, user_len); + + LOG_DBG("user: \"%.*s\"", (int)user_len, start); + } + + start = user_pw_end + 1; + left = len - (start - uri); + auth_left -= user_pw_len + 1; + } + + const char *host_end = memchr(start, ':', auth_left); + if (host_end != NULL) { + size_t host_len = host_end - start; + if (host != NULL) + *host = xstrndup(start, host_len); + + const char *port_str = host_end + 1; + size_t port_len = path_segment - port_str; + if (port_len == 0) + goto err; + + uint16_t _port = 0; + for (size_t i = 0; i < port_len; i++) { + if (!(port_str[i] >= '0' && port_str[i] <= '9')) + goto err; + + _port *= 10; + _port += port_str[i] - '0'; + } + + if (port != NULL) + *port = _port; + + LOG_DBG("host: \"%.*s\"", (int)host_len, start); + LOG_DBG("port: \"%.*s\" (%hu)", (int)port_len, port_str, _port); + } else { + size_t host_len = path_segment - start; + if (host != NULL) + *host = xstrndup(start, host_len); + + LOG_DBG("host: \"%.*s\"", (int)host_len, start); + } + + start = path_segment; + left = len - (start - uri); + } + + /* Do we have a query? */ + const char *query_start = memchr(start, '?', left); + const char *fragment_start = memchr(start, '#', left); + + if (streq(*scheme, "file")) { + /* Don't try to parse query/fragment in file URIs, just treat + the remaining text as path */ + query_start = NULL; + fragment_start = NULL; + } + + else if (query_start != NULL && fragment_start != NULL && + fragment_start < query_start) + { + /* Invalid URI - for now, ignore, and treat is as part of path */ + query_start = NULL; + fragment_start = NULL; + } + + size_t path_len = + query_start != NULL ? query_start - start : + fragment_start != NULL ? fragment_start - start : + left; + + if (path_len == 0) + goto err; + + /* Path - decode %xx encoded characters */ + if (path != NULL) { + const char *encoded = start; + char *decoded = xmalloc(path_len + 1); + char *p = decoded; + + size_t encoded_len = path_len; + size_t UNUSED decoded_len = 0; + + while (true) { + /* Find next '%' */ + const char *next = memchr(encoded, '%', encoded_len); + + if (next == NULL) { + strncpy(p, encoded, encoded_len); + decoded_len += encoded_len; + p += encoded_len; + break; + } + + /* Copy everything leading up to the '%' */ + size_t prefix_len = next - encoded; + memcpy(p, encoded, prefix_len); + + p += prefix_len; + encoded_len -= prefix_len; + decoded_len += prefix_len; + + if (hex2nibble(next[1]) <= 15 && hex2nibble(next[2]) <= 15) { + *p++ = hex2nibble(next[1]) << 4 | hex2nibble(next[2]); + decoded_len++; + encoded_len -= 3; + encoded = next + 3; + } else { + *p++ = *next; + decoded_len++; + encoded_len -= 1; + encoded = next + 1; + } + } + + *p = '\0'; + *path = decoded; + + LOG_DBG("path: encoded=\"%.*s\", decoded=\"%s\"", (int)path_len, start, decoded); + } else + LOG_DBG("path: encoded=\"%.*s\", decoded=", (int)path_len, start); + + start = query_start != NULL ? query_start + 1 : fragment_start != NULL ? fragment_start + 1 : uri + len; + left = len - (start - uri); + + if (query_start != NULL) { + size_t query_len = fragment_start != NULL + ? fragment_start - start : left; + + if (query_len == 0) + goto err; + + if (query != NULL) + *query = xstrndup(start, query_len); + + LOG_DBG("query: \"%.*s\"", (int)query_len, start); + + start = fragment_start != NULL ? fragment_start + 1 : uri + len; + left = len - (start - uri); + } + + if (fragment_start != NULL) { + if (left == 0) + goto err; + + if (fragment != NULL) + *fragment = xstrndup(start, left); + + LOG_DBG("fragment: \"%.*s\"", (int)left, start); + } + + return true; + +err: + if (scheme != NULL) free(*scheme); + if (user != NULL) free(*user); + if (password != NULL) free(*password); + if (host != NULL) free(*host); + if (path != NULL) free(*path); + if (query != NULL) free(*query); + if (fragment != NULL) free(*fragment); + return false; +} + +bool +hostname_is_localhost(const char *hostname) +{ + char this_host[_POSIX_HOST_NAME_MAX]; + if (gethostname(this_host, sizeof(this_host)) < 0) + this_host[0] = '\0'; + + return (hostname != NULL && ( + streq(hostname, "") || + streq(hostname, "localhost") || + streq(hostname, this_host))); +} diff --git a/uri.h b/uri.h new file mode 100644 index 0000000..b63290c --- /dev/null +++ b/uri.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include +#include + +bool uri_parse(const char *uri, size_t len, + char **scheme, char **user, char **password, char **host, + uint16_t *port, char **path, char **query, char **fragment); + +bool hostname_is_localhost(const char *hostname); diff --git a/url-mode.c b/url-mode.c new file mode 100644 index 0000000..44809f5 --- /dev/null +++ b/url-mode.c @@ -0,0 +1,830 @@ +#include "url-mode.h" + +#include +#include +#include +#include +#include + +#include +#include + +#define LOG_MODULE "url-mode" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" +#include "grid.h" +#include "key-binding.h" +#include "quirks.h" +#include "render.h" +#include "selection.h" +#include "spawn.h" +#include "terminal.h" +#include "uri.h" +#include "util.h" +#include "xmalloc.h" + +static void url_destroy(struct url *url); + +static bool +execute_binding(struct seat *seat, struct terminal *term, + const struct key_binding *binding, uint32_t serial) +{ + const enum bind_action_url action = binding->action; + + switch (action) { + case BIND_ACTION_URL_NONE: + return false; + + case BIND_ACTION_URL_CANCEL: + urls_reset(term); + return true; + + case BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL: + term->urls_show_uri_on_jump_label = !term->urls_show_uri_on_jump_label; + render_refresh_urls(term); + return true; + + case BIND_ACTION_URL_COUNT: + return false; + + } + return true; +} + +static bool +spawn_url_launcher_with_token(struct terminal *term, + const char *url, + const char *xdg_activation_token) +{ + size_t argc; + char **argv; + + int dev_null = open("/dev/null", O_RDWR); + + if (dev_null < 0) { + LOG_ERRNO("failed to open /dev/null"); + return false; + } + + xassert(term->url_launch != NULL); + bool ret = false; + + if (spawn_expand_template( + term->url_launch, 2, + (const char *[]){"url", "match"}, + (const char *[]){url, url}, + &argc, &argv)) + { + ret = spawn( + term->reaper, term->cwd, argv, + dev_null, dev_null, dev_null, NULL, NULL, xdg_activation_token) >= 0; + + for (size_t i = 0; i < argc; i++) + free(argv[i]); + free(argv); + } + + close(dev_null); + return ret; +} + +struct spawn_activation_context { + struct terminal *term; + char *url; +}; + +static void +activation_token_done(const char *token, void *data) +{ + struct spawn_activation_context *ctx = data; + + spawn_url_launcher_with_token(ctx->term, ctx->url, token); + free(ctx->url); + free(ctx); +} + +static bool +spawn_url_launcher(struct seat *seat, struct terminal *term, const char *url, + uint32_t serial) +{ + xassert(term->url_launch != NULL); + + struct spawn_activation_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct spawn_activation_context){ + .term = term, + .url = xstrdup(url), + }; + + if (wayl_get_activation_token( + seat->wayl, seat, serial, term->window, &activation_token_done, ctx)) + { + /* Context free:d by callback */ + return true; + } + + free(ctx->url); + free(ctx); + + return spawn_url_launcher_with_token(term, url, NULL); +} + +static void +activate_url(struct seat *seat, struct terminal *term, const struct url *url, + uint32_t serial, bool paste_url_to_self) +{ + char *url_string = NULL; + + char *scheme, *host, *path; + if (uri_parse(url->url, strlen(url->url), &scheme, NULL, NULL, + &host, NULL, &path, NULL, NULL)) + { + if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { + /* + * This is a file in *this* computer. Pass only the + * filename to the URL-launcher. + * + * I.e. strip the ‘file://user@host/’ prefix. + */ + url_string = path; + } else + free(path); + + free(scheme); + free(host); + } + + if (url_string == NULL) + url_string = xstrdup(url->url); + + switch (url->action) { + case URL_ACTION_COPY: + if (paste_url_to_self) { + if (term->bracketed_paste) + term_to_slave(term, "\033[200~", 6); + + term_to_slave(term, url_string, strlen(url_string)); + + if (term->bracketed_paste) + term_to_slave(term, "\033[201~", 6); + } + if (text_to_clipboard(seat, term, url_string, seat->kbd.serial)) { + /* Now owned by our clipboard “manager” */ + url_string = NULL; + } + break; + + case URL_ACTION_LAUNCH: + case URL_ACTION_PERSISTENT: { + spawn_url_launcher(seat, term, url_string, serial); + break; + } + } + + free(url_string); +} + +void +urls_input(struct seat *seat, struct terminal *term, + const struct key_binding_set *bindings, uint32_t key, + xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, + const xkb_keysym_t *raw_syms, size_t raw_count, + uint32_t serial) +{ + /* + * Key bindings + */ + + /* Match untranslated symbols */ + tll_foreach(bindings->url, it) { + const struct key_binding *bind = &it->item; + if (bind->mods != mods || bind->mods == 0) + continue; + + for (size_t i = 0; i < raw_count; i++) { + if (bind->k.sym == raw_syms[i]) { + execute_binding(seat, term, bind, serial); + return; + } + } + } + + /* Match translated symbol */ + tll_foreach(bindings->url, it) { + const struct key_binding *bind = &it->item; + + if (bind->k.sym == sym && + bind->mods == (mods & ~consumed)) + { + execute_binding(seat, term, bind, serial); + return; + } + + } + + /* Match raw key code */ + tll_foreach(bindings->url, it) { + const struct key_binding *bind = &it->item; + if (bind->mods != mods || bind->mods == 0) + continue; + + /* Match raw key code */ + tll_foreach(bind->k.key_codes, code) { + if (code->item == key) { + execute_binding(seat, term, bind, serial); + return; + } + } + } + + size_t seq_len = c32len(term->url_keys); + + if (sym == XKB_KEY_BackSpace) { + if (seq_len > 0) { + term->url_keys[seq_len - 1] = U'\0'; + render_refresh_urls(term); + } + + return; + } + + if (mods & ~consumed) + return; + + char32_t wc = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); + + /* + * Determine if this is a "valid" key. I.e. if there is a URL + * label with a key combo where this key is the next in + * sequence. + */ + + bool is_valid = false; + const struct url *match = NULL; + + tll_foreach(term->urls, it) { + if (it->item.key == NULL) + continue; + + const struct url *url = &it->item; + const size_t key_len = c32len(it->item.key); + + if (key_len >= seq_len + 1 && + c32ncasecmp(url->key, term->url_keys, seq_len) == 0 && + toc32lower(url->key[seq_len]) == toc32lower(wc)) + { + is_valid = true; + if (key_len == seq_len + 1) { + match = url; + break; + } + } + } + + if (match) { + // If the last hint character was uppercase, copy and paste + bool insert = term->conf->uppercase_regex_insert && wc == toc32upper(wc); + activate_url(seat, term, match, serial, insert); + + switch (match->action) { + case URL_ACTION_COPY: + case URL_ACTION_LAUNCH: + urls_reset(term); + break; + + case URL_ACTION_PERSISTENT: + term->url_keys[0] = U'\0'; + render_refresh_urls(term); + break; + } + } + + else if (is_valid) { + xassert(seq_len + 1 <= ALEN(term->url_keys)); + term->url_keys[seq_len] = wc; + render_refresh_urls(term); + } +} + +struct vline { + char *utf8; + size_t len; /* Length of utf8[] */ + size_t sz; /* utf8[] allocated size */ + struct coord *map; /* Maps utf8[ofs] to grid coordinates */ +}; + +static void +regex_detected(const struct terminal *term, enum url_action action, + const regex_t *preg, url_list_t *urls) +{ + /* + * Use regcomp()+regexec() to find patterns. + * + * Since we can't feed regexec() one character at a time, and + * since it doesn't accept wide characters, we need to build utf8 + * strings. + * + * Each string represents a logical line (i.e. handle line-wrap). + * To be able to map regex matches back to the grid, we store the + * grid coordinates of *each* character, in the line struct as + * well. This is offset based; utf8[ofs] has its grid coordinates + * in map[ofs. + */ + + /* There is *at most* term->rows logical lines */ + struct vline vlines[term->rows]; + size_t vline_idx = 0; + + memset(vlines, 0, sizeof(vlines)); + struct vline *vline = &vlines[vline_idx]; + + mbstate_t ps = {0}; + + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + + for (int c = 0; c < term->cols; c++) { + const struct cell *cell = &row->cells[c]; + const char32_t *wc = &cell->wc; + size_t wc_count = 1; + + /* Expand combining characters */ + if (wc[0] >= CELL_COMB_CHARS_LO && wc[0] <= CELL_COMB_CHARS_HI) { + const struct composed *composed = + composed_lookup(term->composed, wc[0] - CELL_COMB_CHARS_LO); + xassert(composed != NULL); + + wc = composed->chars; + wc_count = composed->count; + } + + else if (wc[0] >= CELL_SPACER) + continue; + + /* Convert wide character to utf8 */ + for (size_t i = 0; i < wc_count; i++) { + char buf[16]; + size_t char_len = c32rtomb(buf, wc[i], &ps); + + if (char_len == (size_t)-1) + continue; + + + for (size_t j = 0; j < char_len; j++) { + const size_t requires_size = vline->len + char_len; + + /* Need to grow? Remember to save at least one byte for terminator */ + if (vline->sz == 0 || requires_size > vline->sz - 1) { + const size_t new_size = requires_size * 2; + vline->utf8 = xreallocarray(vline->utf8, new_size, 1); + vline->map = xreallocarray(vline->map, new_size, sizeof(vline->map[0])); + vline->sz = new_size; + } + + vline->utf8[vline->len + j] = + (buf[j] == '\0') ? ' ' : buf[j]; + vline->map[vline->len + j] = (struct coord){c, term->grid->view + r}; + } + + vline->len += char_len; + } + } + + if (row->linebreak) { + if (vline->len > 0) { + vline->utf8[vline->len++] = '\0'; + ps = (mbstate_t){0}; + + vline_idx++; + vline = &vlines[vline_idx]; + } + } + } + + /* Terminate the last line, if necessary */ + if (vline_idx < ALEN(vlines) && + vline->len > 0 && vline->utf8[vline->len - 1] != '\0') + { + vline->utf8[vline->len++] = '\0'; + } + + for (size_t i = 0; i < ALEN(vlines); i++) { + const struct vline *v = &vlines[i]; + if (v->utf8 == NULL) + continue; + + const char *search_string = v->utf8; + while (true) { + regmatch_t matches[preg->re_nsub + 1]; + int r = regexec(preg, search_string, preg->re_nsub + 1, matches, 0); + + if (r == REG_NOMATCH) + break; + + const size_t mlen = matches[1].rm_eo - matches[1].rm_so; + const size_t start = &search_string[matches[1].rm_so] - v->utf8; + const size_t end = start + mlen; + + LOG_DBG( + "regex match at row %d: %.*s (%zu bytes), row/col = %dx%d", + matches[1].rm_so, (int)mlen, &search_string[matches[1].rm_so], + mlen, v->map[start].row, v->map[start].col); + + tll_push_back( + *urls, + ((struct url){ + .id = (uint64_t)rand() << 32 | rand(), + .url = xstrndup(&v->utf8[start], mlen), + .range = { + .start = v->map[start], + .end = v->map[end - 1], /* Inclusive */ + }, + .action = action, + .osc8 = false})); + + search_string += matches[0].rm_eo; + } + + free(v->utf8); + free(v->map); + } +} + +static void +osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) +{ + bool dont_touch_url_attr = false; + + switch (term->conf->url.osc8_underline) { + case OSC8_UNDERLINE_URL_MODE: + dont_touch_url_attr = false; + break; + + case OSC8_UNDERLINE_ALWAYS: + dont_touch_url_attr = true; + break; + } + + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + const struct row_data *extra = row->extra; + + if (extra == NULL) + continue; + + for (size_t i = 0; i < extra->uri_ranges.count; i++) { + const struct row_range *range = &extra->uri_ranges.v[i]; + + struct coord start = { + .col = range->start, + .row = r + term->grid->view, + }; + struct coord end = { + .col = range->end, + .row = r + term->grid->view, + }; + tll_push_back( + *urls, + ((struct url){ + .id = range->uri.id, + .url = xstrdup(range->uri.uri), + .range = { + .start = start, + .end = end, + }, + .action = action, + .url_mode_dont_change_url_attr = dont_touch_url_attr, + .osc8 = true})); + } + } +} + +static void +remove_overlapping(url_list_t *urls, int cols) +{ + tll_foreach(*urls, outer) { + tll_foreach(*urls, inner) { + if (outer == inner) + continue; + + const struct url *out = &outer->item; + const struct url *in = &inner->item; + + uint64_t in_start = in->range.start.row * cols + in->range.start.col; + uint64_t in_end = in->range.end.row * cols + in->range.end.col; + + uint64_t out_start = out->range.start.row * cols + out->range.start.col; + uint64_t out_end = out->range.end.row * cols + out->range.end.col; + + if ((in_start <= out_start && in_end >= out_start) || + (in_start <= out_end && in_end >= out_end) || + (in_start >= out_start && in_end <= out_end)) + { + /* + * OSC-8 URLs can't overlap with each + * other. + * + * Similarly, auto-detected URLs cannot overlap with + * each other. + * + * But OSC-8 URLs can overlap with auto-detected ones. + */ + xassert(in->osc8 || out->osc8); + + if (in->osc8) + outer->item.duplicate = true; + else + inner->item.duplicate = true; + } + } + } + + tll_foreach(*urls, it) { + if (it->item.duplicate) { + url_destroy(&it->item); + tll_remove(*urls, it); + } + } +} + +void +urls_collect(const struct terminal *term, enum url_action action, + const regex_t *preg, bool osc8, url_list_t *urls) +{ + xassert(tll_length(term->urls) == 0); + if (osc8) + osc8_uris(term, action, urls); + regex_detected(term, action, preg, urls); + remove_overlapping(urls, term->grid->num_cols); +} + +static void +generate_key_combos(const struct config *conf, + size_t count, char32_t *combos[static count]) +{ + const char32_t *alphabet = conf->url.label_letters; + const size_t alphabet_len = c32len(alphabet); + + size_t hints_count = 1; + char32_t **hints = xmalloc(hints_count * sizeof(hints[0])); + + hints[0] = xc32dup(U""); + + size_t offset = 0; + do { + const char32_t *prefix = hints[offset++]; + const size_t prefix_len = c32len(prefix); + + hints = xrealloc(hints, (hints_count + alphabet_len) * sizeof(hints[0])); + + const char32_t *wc = &alphabet[0]; + for (size_t i = 0; i < alphabet_len; i++, wc++) { + char32_t *hint = xmalloc((prefix_len + 1 + 1) * sizeof(char32_t)); + hints[hints_count + i] = hint; + + /* Will be reversed later */ + hint[0] = *wc; + c32cpy(&hint[1], prefix); + } + hints_count += alphabet_len; + } while (hints_count - offset < count); + + xassert(hints_count - offset >= count); + + /* Copy slice of 'hints' array to the caller provided array */ + for (size_t i = 0; i < hints_count; i++) { + if (i >= offset && i < offset + count) + combos[i - offset] = hints[i]; + else + free(hints[i]); + } + free(hints); + + /* Reverse all strings */ + for (size_t i = 0; i < count; i++) { + const size_t len = c32len(combos[i]); + for (size_t j = 0; j < len / 2; j++) { + char32_t tmp = combos[i][j]; + combos[i][j] = combos[i][len - j - 1]; + combos[i][len - j - 1] = tmp; + } + } +} + +void +urls_assign_key_combos(const struct config *conf, url_list_t *urls) +{ + const size_t count = tll_length(*urls); + if (count == 0) + return; + + char32_t *combos[count]; + generate_key_combos(conf, count, combos); + + size_t combo_idx = 0; + + tll_rforeach(*urls, it) { + bool id_already_seen = false; + + /* Look for already processed URLs where both the URI and the + * ID matches */ + tll_rforeach(*urls, it2) { + if (&it->item == &it2->item) + break; + + if (it->item.id == it2->item.id && + streq(it->item.url, it2->item.url)) + { + id_already_seen = true; + break; + } + } + + if (id_already_seen) + continue; + + /* + * Scan previous URLs, and check if *this* URL matches any of + * them; if so, reuse the *same* key combo. + */ + bool url_already_seen = false; + tll_rforeach(*urls, it2) { + if (&it->item == &it2->item) + break; + + if (streq(it->item.url, it2->item.url)) { + it->item.key = xc32dup(it2->item.key); + url_already_seen = true; + break; + } + } + + if (!url_already_seen) + it->item.key = combos[combo_idx++]; + } + + /* Free combos we didn't use up */ + for (size_t i = combo_idx; i < count; i++) + free(combos[i]); + +#if defined(_DEBUG) && LOG_ENABLE_DBG + tll_rforeach(*urls, it) { + if (it->item.key == NULL) + continue; + + char *key = ac32tombs(it->item.key); + xassert(key != NULL); + + LOG_DBG("URL: %s (key=%s, id=%"PRIu64")", it->item.url, key, it->item.id); + free(key); + } +#endif +} + +static void +tag_cells_for_url(struct terminal *term, const struct url *url, bool value) +{ + if (url->url_mode_dont_change_url_attr) + return; + + struct grid *grid = term->url_grid_snapshot; + xassert(grid != NULL); + + const struct coord *start = &url->range.start; + const struct coord *end = &url->range.end; + + size_t end_r = end->row & (grid->num_rows - 1); + + size_t r = start->row & (grid->num_rows - 1); + size_t c = start->col; + + struct row *row = grid->rows[r]; + row->dirty = true; + + while (true) { + struct cell *cell = &row->cells[c]; + cell->attrs.url = value; + cell->attrs.clean = 0; + + if (r == end_r && c == end->col) + break; + + if (++c >= term->cols) { + r = (r + 1) & (grid->num_rows - 1); + c = 0; + + row = grid->rows[r]; + if (row == NULL) { + /* Un-allocated scrollback. This most likely means a + * runaway OSC-8 URL. */ + break; + } + row->dirty = true; + } + } +} + +void +urls_render(struct terminal *term, const struct config_spawn_template *launch) +{ + struct wl_window *win = term->window; + + if (tll_length(win->term->urls) == 0) + return; + + /* Disable IME while in URL-mode */ + if (term_ime_is_enabled(term)) { + term->ime_reenable_after_url_mode = true; + term_ime_disable(term); + } + + /* Dirty the last cursor, to ensure it is erased */ + { + struct row *cursor_row = term->render.last_cursor.row; + if (cursor_row != NULL) { + struct cell *cell = &cursor_row->cells[term->render.last_cursor.col]; + cell->attrs.clean = 0; + cursor_row->dirty = true; + } + } + term->render.last_cursor.row = NULL; + + /* Clear scroll damage, to ensure we don't apply it twice (once on + * the snapshot:ed grid, and then later again on the real grid) */ + tll_free(term->grid->scroll_damage); + + /* Damage the entire view, to ensure a full screen redraw, both + * now, when entering URL mode, and later, when exiting it. */ + term_damage_view(term); + + /* Snapshot the current grid */ + term->url_grid_snapshot = grid_snapshot(term->grid); + + /* Remember which launcher to use */ + term->url_launch = launch; + + xassert(tll_length(win->urls) == 0); + tll_foreach(win->term->urls, it) { + struct wl_url url = {.url = &it->item}; + wayl_win_subsurface_new(win, &url.surf, false); + + tll_push_back(win->urls, url); + tag_cells_for_url(term, &it->item, true); + } + + render_refresh_urls(term); + render_refresh(term); +} + +static void +url_destroy(struct url *url) +{ + free(url->url); + free(url->key); +} + +void +urls_reset(struct terminal *term) +{ + if (likely(tll_length(term->urls) == 0)) { + xassert(term->url_grid_snapshot == NULL); + return; + } + + grid_free(term->url_grid_snapshot); + free(term->url_grid_snapshot); + term->url_grid_snapshot = NULL; + + /* + * Make sure "last cursor" doesn't point to a row in the just + * free:d snapshot grid. + * + * Note that it will still be erased properly (if hasn't already), + * since we marked the cell as dirty *before* taking the grid + * snapshot. + */ + term->render.last_cursor.row = NULL; + + if (term->window != NULL) { + tll_foreach(term->window->urls, it) { + wayl_win_subsurface_destroy(&it->item.surf); + tll_remove(term->window->urls, it); + } + } + + tll_foreach(term->urls, it) { + url_destroy(&it->item); + tll_remove(term->urls, it); + } + + term->urls_show_uri_on_jump_label = false; + memset(term->url_keys, 0, sizeof(term->url_keys)); + + /* Re-enable IME, if it was enabled before we entered URL-mode */ + if (term->ime_reenable_after_url_mode) { + term->ime_reenable_after_url_mode = false; + term_ime_enable(term); + } + + render_refresh(term); +} diff --git a/url-mode.h b/url-mode.h new file mode 100644 index 0000000..758cd92 --- /dev/null +++ b/url-mode.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +#include "config.h" +#include "key-binding.h" +#include "terminal.h" + +static inline bool urls_mode_is_active(const struct terminal *term) +{ + return tll_length(term->urls) > 0; +} + +void urls_collect( + const struct terminal *term, enum url_action action, const regex_t *preg, + bool osc8, url_list_t *urls); +void urls_assign_key_combos(const struct config *conf, url_list_t *urls); + +void urls_render(struct terminal *term, const struct config_spawn_template *launch); +void urls_reset(struct terminal *term); + +void urls_input(struct seat *seat, struct terminal *term, + const struct key_binding_set *bindings, uint32_t key, + xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, + const xkb_keysym_t *raw_syms, size_t raw_count, + uint32_t serial); diff --git a/user-notification.c b/user-notification.c new file mode 100644 index 0000000..6c1157d --- /dev/null +++ b/user-notification.c @@ -0,0 +1,14 @@ +#include "user-notification.h" +#include +#include "xmalloc.h" + +void +user_notification_add_fmt(user_notifications_t *notifications, + enum user_notification_kind kind, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + char *text = xvasprintf(fmt, ap); + va_end(ap); + user_notification_add(notifications, kind, text); +} diff --git a/user-notification.h b/user-notification.h new file mode 100644 index 0000000..df2390c --- /dev/null +++ b/user-notification.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "macros.h" + +enum user_notification_kind { + USER_NOTIFICATION_DEPRECATED, + USER_NOTIFICATION_WARNING, + USER_NOTIFICATION_ERROR, +}; + +struct user_notification { + enum user_notification_kind kind; + char *text; +}; + +typedef tll(struct user_notification) user_notifications_t; + +static inline void +user_notifications_free(user_notifications_t *notifications) +{ + tll_foreach(*notifications, it) + free(it->item.text); + tll_free(*notifications); +} + +static inline void +user_notification_add(user_notifications_t *notifications, + enum user_notification_kind kind, char *text) +{ + struct user_notification notification = { + .kind = kind, + .text = text + }; + tll_push_back(*notifications, notification); +} + +void user_notification_add_fmt(user_notifications_t *notifications, + enum user_notification_kind kind, + const char *fmt, ...) PRINTF(3); diff --git a/util.h b/util.h new file mode 100644 index 0000000..3746e26 --- /dev/null +++ b/util.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include + +#define ALEN(v) (sizeof(v) / sizeof((v)[0])) +#define min(x, y) ((x) < (y) ? (x) : (y)) +#define max(x, y) ((x) > (y) ? (x) : (y)) + +static inline bool +streq(const char *a, const char *b) +{ + return strcmp(a, b) == 0; +} + +static inline const char * +thrd_err_as_string(int thrd_err) +{ + switch (thrd_err) { + case thrd_success: return "success"; + case thrd_busy: return "busy"; + case thrd_nomem: return "no memory"; + case thrd_timedout: return "timedout"; + + case thrd_error: + default: return "unknown error"; + } + + return "unknown error"; +} + +static inline uint64_t +sdbm_hash(const char *s) +{ + uint64_t hash = 0; + + for (; *s != '\0'; s++) { + int c = *s; + hash = c + (hash << 6) + (hash << 16) - hash; + } + + return hash; +} + +enum { + HEX_DIGIT_INVALID = 16 +}; + +static inline uint8_t +hex2nibble(char c) +{ + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + return c - '0'; + + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + return c - 'a' + 10; + + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + return c - 'A' + 10; + } + + return HEX_DIGIT_INVALID; +} diff --git a/utils/meson.build b/utils/meson.build new file mode 100644 index 0000000..2836788 --- /dev/null +++ b/utils/meson.build @@ -0,0 +1 @@ +executable('xtgettcap', 'xtgettcap.c') diff --git a/utils/xtgettcap.c b/utils/xtgettcap.c new file mode 100644 index 0000000..82ee008 --- /dev/null +++ b/utils/xtgettcap.c @@ -0,0 +1,199 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static struct termios orig_termios; + +static void +disable_raw_mode(void) +{ + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) < 0) + exit(__LINE__); +} + +static void +enable_raw_mode(void) +{ + if (tcgetattr(STDIN_FILENO, &orig_termios) < 0) + exit(__LINE__); + + atexit(disable_raw_mode); + + struct termios raw = orig_termios; + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + raw.c_oflag &= ~(OPOST); + raw.c_cflag |= (CS8); + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 1; + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) < 0) + exit(__LINE__); +} + +static const char * +hexlify(const char *s) +{ + static char buf[1024]; + + const size_t len = strlen(s); + for (size_t i = 0; i < len; i++) + sprintf(&buf[i * 2], "%02x", s[i]); + buf[len * 2 + 1] = '\0'; + + return buf; +} + +static size_t +unhexlify(char *dst, const char *src) +{ + size_t count = 0; + for (const char *p = src; *p != '\0'; p += 2, dst++, count++) + sscanf(p, "%02hhx", (unsigned char *)dst); + + *dst = '\0'; + return count; +} + +int +main(int argc, const char *const *argv) +{ + const size_t query_count = argc - 1; + + if (query_count == 0) + return 0; + + enable_raw_mode(); + + printf("\x1bP+q"); + for (int i = 1; i < argc; i++) + printf("%s%s", i > 1 ? ";" : "", hexlify(argv[i])); + printf("\033\\"); + + fflush(NULL); + + size_t replies = 0; + while (replies < query_count) { + struct pollfd fds[] = {{.fd = STDIN_FILENO, .events = POLLIN}}; + int r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); + if (r < 0) + exit(__LINE__); + + char buf[1024] = {0}; + ssize_t count = read(STDIN_FILENO, buf, sizeof(buf)); + + if (count < 0) + exit(__LINE__); + + if (count == 1 && buf[0] == 'q') + break; + + printf("reply: (%zd chars): ", count); + + for (size_t i = 0; i < (size_t)count; i++) { + if (isprint(buf[i])) + printf("%c", buf[i]); + else if (buf[i] == '\033') + printf("\033[1;31m\033[m"); + else + printf("%02x", (uint8_t)buf[i]); + } + printf("\r\n"); + + const char *p = buf; + const char *end = buf + count; + + while (p < end) { + + const char *ST = strstr(p, "\033\\"); + if (ST == NULL) + break; + + if (count < 5 || + (strncmp(p, "\033P1+r", 5) != 00 && + strncmp(p, "\033P0+r", 5) != 0)) + { + break; + } + + const bool success = p[2] == '1'; + + char decoded[1024]; + char copy[ST - &p[5] + 1]; + strncpy(copy, &p[5], ST - &p[5]); + copy[ST - &p[5]] = '\0'; + + char *saveptr = NULL; + for (char *key_value = strtok_r(copy, "; ", &saveptr); + key_value != NULL; + key_value = strtok_r(NULL, "; ", &saveptr)) + { + // printf("key-value=%s\n", key_value); + const char *key = strtok(key_value, "="); + const char *value = strtok(NULL, "="); + + if (key == NULL) + continue; + +#if 0 + assert((success && value != NULL) || + (!success && value == NULL)); +#endif + + //printf("key=%s, value=%s\n", key, value); + size_t len = unhexlify(decoded, key); + + if (value != NULL) { + decoded[len++] = '='; + len += unhexlify(&decoded[len], value); + } + + const int color = success ? 39 : 31; + + printf(" \033[%dm", color); + for (size_t i = 0 ; i < len; i++) { + if (isprint(decoded[i])) { + /* All printable characters */ + printf("%c", decoded[i]); + } + + else if (decoded[i] == '\033') { + /* ESC */ + printf("\033[1;31m\033[22;%dm", color); + } + + else if (decoded[i] >= '\x00' && decoded[i] <= '\x5f') { + /* Control characters, e.g. ^G etc */ + printf("\033[1m^%c\033[22m", decoded[i] + '@'); + } + + else if (decoded[i] == '\x7f') { + /* Control character ^? */ + printf("\033[1m^?\033[22m"); + } + + else { + /* Unknown: print hex representation */ + printf("\033[1m%02x\033[22m", (uint8_t)decoded[i]); + } + } + printf("\033[m\r\n"); + replies++; + } + + p = ST + 2; + } + + } + + return 0; +} diff --git a/vt.c b/vt.c new file mode 100644 index 0000000..1d8297b --- /dev/null +++ b/vt.c @@ -0,0 +1,1133 @@ +#include "vt.h" + +#include +#include +#include + +#if defined(FOOT_GRAPHEME_CLUSTERING) + #include +#endif + +#define LOG_MODULE "vt" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" +#include "config.h" +#include "csi.h" +#include "dcs.h" +#include "debug.h" +#include "osc.h" +#include "sixel.h" +#include "util.h" +#include "xmalloc.h" + +#define UNHANDLED() LOG_DBG("unhandled: %s", esc_as_string(term, final)) + +/* https://vt100.net/emu/dec_ansi_parser */ + +enum state { + STATE_GROUND, + STATE_ESCAPE, + STATE_ESCAPE_INTERMEDIATE, + + STATE_CSI_ENTRY, + STATE_CSI_PARAM, + STATE_CSI_INTERMEDIATE, + STATE_CSI_IGNORE, + + STATE_OSC_STRING, + + STATE_DCS_ENTRY, + STATE_DCS_PARAM, + STATE_DCS_INTERMEDIATE, + STATE_DCS_IGNORE, + STATE_DCS_PASSTHROUGH, + + STATE_SOS_PM_APC_STRING, + + STATE_UTF8_21, + STATE_UTF8_31, + STATE_UTF8_32, + STATE_UTF8_41, + STATE_UTF8_42, + STATE_UTF8_43, +}; + +#if defined(_DEBUG) && defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG && 0 +static const char *const state_names[] = { + [STATE_GROUND] = "ground", + + [STATE_ESCAPE] = "escape", + [STATE_ESCAPE_INTERMEDIATE] = "escape intermediate", + + [STATE_CSI_ENTRY] = "CSI entry", + [STATE_CSI_PARAM] = "CSI param", + [STATE_CSI_INTERMEDIATE] = "CSI intermediate", + [STATE_CSI_IGNORE] = "CSI ignore", + + [STATE_OSC_STRING] = "OSC string", + + [STATE_DCS_ENTRY] = "DCS entry", + [STATE_DCS_PARAM] = "DCS param", + [STATE_DCS_INTERMEDIATE] = "DCS intermediate", + [STATE_DCS_IGNORE] = "DCS ignore", + [STATE_DCS_PASSTHROUGH] = "DCS passthrough", + + [STATE_SOS_PM_APC_STRING] = "sos/pm/apc string", + + [STATE_UTF8_21] = "UTF8 2-byte 1/2", + [STATE_UTF8_31] = "UTF8 3-byte 1/3", + [STATE_UTF8_32] = "UTF8 3-byte 2/3", +}; +#endif + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG +static const char * +esc_as_string(struct terminal *term, uint8_t final) +{ + static char msg[1024]; + int c = snprintf(msg, sizeof(msg), "\\E"); + + for (size_t i = 0; i < sizeof(term->vt.private); i++) { + char value = (term->vt.private >> (i * 8)) & 0xff; + if (value == 0) + break; + c += snprintf(&msg[c], sizeof(msg) - c, "%c", value); + } + + xassert(term->vt.params.idx == 0); + + snprintf(&msg[c], sizeof(msg) - c, "%c", final); + return msg; + +} +#endif + +static void +action_ignore(struct terminal *term) +{ +} + +static void +action_clear(struct terminal *term) +{ + term->vt.params.idx = 0; + term->vt.private = 0; +} + +static void +action_execute(struct terminal *term, uint8_t c) +{ + LOG_DBG("execute: 0x%02x", c); + switch (c) { + + /* + * 7-bit C0 control characters + */ + + case '\0': + break; + + case '\a': + /* BEL - bell */ + term_bell(term); + break; + + case '\b': + /* backspace */ +#if 0 + /* + * This is the "correct" BS behavior. However, it doesn't play + * nicely with bw/auto_left_margin, hence the alternative + * implementation below. + * + * Note that it breaks vttest "1. Test of cursor movements -> + * Test of autowrap" + */ + term_cursor_left(term, 1); +#else + if (term->grid->cursor.lcf) + term->grid->cursor.lcf = false; + else { + /* Reverse wrap */ + if (unlikely(term->grid->cursor.point.col == 0) && + likely(term->reverse_wrap && term->auto_margin)) + { + if (term->grid->cursor.point.row <= term->scroll_region.start) { + /* Don't wrap past, or inside, the scrolling region(?) */ + } else + term_cursor_to( + term, + term->grid->cursor.point.row - 1, + term->cols - 1); + } else + term_cursor_left(term, 1); + } +#endif + break; + + case '\t': { + /* HT - horizontal tab */ + int start_col = term->grid->cursor.point.col; + int new_col = term->cols - 1; + + tll_foreach(term->tab_stops, it) { + if (it->item > start_col) { + new_col = it->item; + break; + } + } + xassert(new_col >= start_col); + xassert(new_col < term->cols); + + struct row *row = term->grid->cur_row; + + bool emit_tab_char = (row->cells[start_col].wc == 0 || + row->cells[start_col].wc == U' '); + + /* Check if all cells from here until the next tab stop are empty */ + for (const struct cell *cell = &row->cells[start_col + 1]; + cell < &row->cells[new_col]; + cell++) + { + if (!(cell->wc == 0 || cell->wc == U' ')) { + emit_tab_char = false; + break; + } + } + + /* + * Emit a tab in current cell, and write spaces to the + * subsequent cells, all the way until the next tab stop. + */ + if (emit_tab_char) { + row->dirty = true; + + row->cells[start_col].wc = U'\t'; + row->cells[start_col].attrs.clean = 0; + + for (struct cell *cell = &row->cells[start_col + 1]; + cell < &row->cells[new_col]; + cell++) + { + cell->wc = U' '; + cell->attrs.clean = 0; + } + } + + /* According to the specification, HT _should_ cancel LCF. But + * XTerm, and nearly all other emulators, don't. So we follow + * suit */ + bool lcf = term->grid->cursor.lcf; + term_cursor_right(term, new_col - start_col); + term->grid->cursor.lcf = lcf; + break; + } + + case '\n': + case '\v': + case '\f': + /* LF - \n - line feed */ + /* VT - \v - vertical tab */ + /* FF - \f - form feed */ + term_linefeed(term); + break; + + case '\r': + /* CR - carriage ret */ + term_carriage_return(term); + break; + + case '\x0e': + /* SO - shift out */ + term->charsets.selected = G1; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); + break; + + case '\x0f': + /* SI - shift in */ + term->charsets.selected = G0; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); + break; + + /* + * 8-bit C1 control characters + * + * We ignore these, but keep them here for reference, along + * with their corresponding 7-bit variants. + * + * As far as I can tell, XTerm also ignores these _when in + * UTF-8 mode_. Which would be the normal mode of operation + * these days. And since we _only_ support UTF-8... + */ + +#if 0 + case '\x84': /* IND -> ESC D */ + case '\x85': /* NEL -> ESC E */ + case '\x88': /* Tab Set -> ESC H */ + case '\x8d': /* RI -> ESC M */ + case '\x8e': /* SS2 -> ESC N */ + case '\x8f': /* SS3 -> ESC O */ + case '\x90': /* DCS -> ESC P */ + case '\x96': /* SPA -> ESC V */ + case '\x97': /* EPA -> ESC W */ + case '\x98': /* SOS -> ESC X */ + case '\x9a': /* DECID -> ESC Z (obsolete form of CSI c) */ + case '\x9b': /* CSI -> ESC [ */ + case '\x9c': /* ST -> ESC \ */ + case '\x9d': /* OSC -> ESC ] */ + case '\x9e': /* PM -> ESC ^ */ + case '\x9f': /* APC -> ESC _ */ + break; +#endif + + default: + break; + } +} + +static void +action_print(struct terminal *term, uint8_t c) +{ + term_reset_grapheme_state(term); + term->ascii_printer(term, c); +} + +static void +action_param_lazy_init(struct terminal *term) +{ + if (term->vt.params.idx == 0) { + struct vt_param *param = &term->vt.params.v[0]; + + term->vt.params.cur = param; + param->value = 0; + param->sub.idx = 0; + param->sub.cur = NULL; + term->vt.params.idx = 1; + } +} + +static void +action_param_new(struct terminal *term, uint8_t c) +{ + xassert(c == ';'); + action_param_lazy_init(term); + + const size_t max_params + = sizeof(term->vt.params.v) / sizeof(term->vt.params.v[0]); + + struct vt_param *param; + + if (unlikely(term->vt.params.idx >= max_params)) { + static bool have_warned = false; + if (!have_warned) { + have_warned = true; + LOG_WARN( + "unsupported: escape with more than %zu parameters " + "(will not warn again)", + sizeof(term->vt.params.v) / sizeof(term->vt.params.v[0])); + } + param = &term->vt.params.dummy; + } else + param = &term->vt.params.v[term->vt.params.idx++]; + + term->vt.params.cur = param; + param->value = 0; + param->sub.idx = 0; + param->sub.cur = NULL; +} + +static void +action_param_new_subparam(struct terminal *term, uint8_t c) +{ + xassert(c == ':'); + action_param_lazy_init(term); + + const size_t max_sub_params + = sizeof(term->vt.params.v[0].sub.value) / sizeof(term->vt.params.v[0].sub.value[0]); + + struct vt_param *param = term->vt.params.cur; + unsigned *sub_param_value; + + if (unlikely(param->sub.idx >= max_sub_params)) { + static bool have_warned = false; + if (!have_warned) { + have_warned = true; + LOG_WARN( + "unsupported: escape with more than %zu sub-parameters " + "(will not warn again)", + sizeof(term->vt.params.v[0].sub.value) / sizeof(term->vt.params.v[0].sub.value[0])); + } + + sub_param_value = ¶m->sub.dummy; + } else + sub_param_value = ¶m->sub.value[param->sub.idx++]; + + param->sub.cur = sub_param_value; + *sub_param_value = 0; +} + +static void +action_param(struct terminal *term, uint8_t c) +{ + action_param_lazy_init(term); + xassert(term->vt.params.cur != NULL); + + struct vt_param *param = term->vt.params.cur; + unsigned *value; + + if (unlikely(param->sub.cur != NULL)) + value = param->sub.cur; + else + value = ¶m->value; + + unsigned v = *value; + v *= 10; + v += c - '0'; + *value = v; +} + +static void +action_collect(struct terminal *term, uint8_t c) +{ + LOG_DBG("collect: %c", c); + + /* + * Having more than one private is *very* rare. Foot only supports + * a *single* escape with two privates, and none with three or + * more. + * + * As such, we optimize *reading* the private(s), and *resetting* + * them (in action_clear()). Writing is ok if it's a bit slow. + */ + + if ((term->vt.private & 0xff) == 0) + term->vt.private = c; + else if (((term->vt.private >> 8) & 0xff) == 0) + term->vt.private |= c << 8; + else if (((term->vt.private >> 16) & 0xff) == 0) + term->vt.private |= c << 16; + else if (((term->vt.private >> 24) & 0xff) == 0) + term->vt.private |= c << 24; + else + LOG_WARN("only four private/intermediate characters supported"); +} + +UNITTEST +{ + struct terminal term = {.vt = {.private = 0}}; + uint32_t expected = ' '; + action_collect(&term, ' '); + xassert(term.vt.private == expected); + + expected |= '/' << 8; + action_collect(&term, '/'); + xassert(term.vt.private == expected); + + expected |= '<' << 16; + action_collect(&term, '<'); + xassert(term.vt.private == expected); + + expected |= '?' << 24; + action_collect(&term, '?'); + xassert(term.vt.private == expected); + + action_collect(&term, '?'); + xassert(term.vt.private == expected); +} + +static void +tab_set(struct terminal *term) +{ + int col = term->grid->cursor.point.col; + + if (tll_length(term->tab_stops) == 0 || tll_back(term->tab_stops) < col) { + tll_push_back(term->tab_stops, col); + return; + } + + tll_foreach(term->tab_stops, it) { + if (it->item < col) { + continue; + } + if (it->item > col) { + tll_insert_before(term->tab_stops, it, col); + } + break; + } +} + +static void +action_esc_dispatch(struct terminal *term, uint8_t final) +{ + LOG_DBG("ESC: %s", esc_as_string(term, final)); + + switch (term->vt.private) { + case 0: + switch (final) { + case '7': + term_save_cursor(term); + break; + + case '8': + term_restore_cursor(term, &term->grid->saved_cursor); + break; + + case 'c': + term_reset(term, true); + break; + + case 'n': + /* LS2 - Locking Shift 2 */ + term->charsets.selected = G2; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); + break; + + case 'o': + /* LS3 - Locking Shift 3 */ + term->charsets.selected = G3; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); + break; + + case 'D': + term_linefeed(term); + break; + + case 'E': + term_carriage_return(term); + term_linefeed(term); + break; + + case 'H': + tab_set(term); + break; + + case 'M': + term_reverse_index(term); + break; + + case 'N': + /* SS2 - Single Shift 2 */ + term_single_shift(term, G2); + break; + + case 'O': + /* SS3 - Single Shift 3 */ + term_single_shift(term, G3); + break; + + case '\\': + /* ST - String Terminator */ + break; + + case '=': + term->keypad_keys_mode = KEYPAD_APPLICATION; + break; + + case '>': + term->keypad_keys_mode = KEYPAD_NUMERICAL; + break; + + default: + UNHANDLED(); + break; + } + break; /* private[0] == 0 */ + + // Designate character set + case '(': // G0 + case ')': // G1 + case '*': // G2 + case '+': // G3 + switch (final) { + case '0': { + size_t idx = term->vt.private - '('; + xassert(idx <= G3); + term->charsets.set[idx] = CHARSET_GRAPHIC; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); + break; + } + + case 'B': { + size_t idx = term->vt.private - '('; + xassert(idx <= G3); + term->charsets.set[idx] = CHARSET_ASCII; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); + break; + } + } + break; + + case '#': + switch (final) { + case '8': /* DECALN */ + sixel_overwrite_by_rectangle(term, 0, 0, term->rows, term->cols); + + term->scroll_region.start = 0; + term->scroll_region.end = term->rows; + + for (int r = 0; r < term->rows; r++) + term_fill(term, r, 0, 'E', term->cols, false); + + term_cursor_home(term); + break; + } + break; /* private[0] == '#' */ + + } +} + +static void +action_csi_dispatch(struct terminal *term, uint8_t c) +{ + csi_dispatch(term, c); +} + +static void +action_osc_start(struct terminal *term, uint8_t c) +{ + term->vt.osc.idx = 0; +} + +static void +action_osc_end(struct terminal *term, uint8_t c) +{ + struct vt *vt = &term->vt; + + if (!osc_ensure_size(term, vt->osc.idx + 1)) + return; + + vt->osc.data[vt->osc.idx] = '\0'; + vt->osc.bel = c == '\a'; + osc_dispatch(term); + + if (unlikely(vt->osc.idx >= 4096)) { + free(vt->osc.data); + vt->osc.data = NULL; + vt->osc.size = 0; + } +} + +static void +action_osc_put(struct terminal *term, uint8_t c) +{ + if (!osc_ensure_size(term, term->vt.osc.idx + 1)) + return; + term->vt.osc.data[term->vt.osc.idx++] = c; +} + +static void +action_hook(struct terminal *term, uint8_t c) +{ + dcs_hook(term, c); +} + +static void +action_unhook(struct terminal *term, uint8_t c) +{ + dcs_unhook(term); +} + +static void +action_put(struct terminal *term, uint8_t c) +{ + dcs_put(term, c); +} + +static void +action_utf8_print(struct terminal *term, char32_t wc) +{ + term_process_and_print_non_ascii(term, wc); +} + +static void +action_utf8_21(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 0x1f) << 6) | (utf8[1] & 0x3f) + term->vt.utf8 = (c & 0x1f) << 6; +} + +static void +action_utf8_22(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 0x1f) << 6) | (utf8[1] & 0x3f) + term->vt.utf8 |= c & 0x3f; + action_utf8_print(term, term->vt.utf8); +} + +static void +action_utf8_31(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 0xf) << 12) | ((utf8[1] & 0x3f) << 6) | (utf8[2] & 0x3f) + term->vt.utf8 = (c & 0x0f) << 12; +} + +static void +action_utf8_32(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 0xf) << 12) | ((utf8[1] & 0x3f) << 6) | (utf8[2] & 0x3f) + term->vt.utf8 |= (c & 0x3f) << 6; +} + +static void +action_utf8_33(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 0xf) << 12) | ((utf8[1] & 0x3f) << 6) | (utf8[2] & 0x3f) + term->vt.utf8 |= c & 0x3f; + + const char32_t utf32 = term->vt.utf8; + if (unlikely(utf32 >= 0xd800 && utf32 <= 0xdfff)) { + /* Invalid sequence - invalid UTF-16 surrogate halves */ + return; + } + + /* Note: the E0 range contains overlong encodings. We don't try to + detect, as they'll still decode to valid UTF-32. */ + + action_utf8_print(term, term->vt.utf8); +} + +static void +action_utf8_41(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f); + term->vt.utf8 = (c & 0x07) << 18; +} + +static void +action_utf8_42(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f); + term->vt.utf8 |= (c & 0x3f) << 12; +} + +static void +action_utf8_43(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f); + term->vt.utf8 |= (c & 0x3f) << 6; +} + +static void +action_utf8_44(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f); + term->vt.utf8 |= c & 0x3f; + + const char32_t utf32 = term->vt.utf8; + + if (unlikely(utf32 > 0x10FFFF)) { + /* Invalid UTF-8 */ + return; + } + + /* Note: the F0 range contains overlong encodings. We don't try to + detect, as they'll still decode to valid UTF-32. */ + + action_utf8_print(term, term->vt.utf8); +} + +IGNORE_WARNING("-Wpedantic") + +static enum state +anywhere(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x18: action_execute(term, data); return STATE_GROUND; + case 0x1a: action_execute(term, data); return STATE_GROUND; + case 0x1b: action_clear(term); return STATE_ESCAPE; + + /* 8-bit C1 control characters (not supported) */ + case 0x80 ... 0x9f: return STATE_GROUND; + } + + return term->vt.state; +} + +static enum state +state_ground_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_GROUND; + + /* modified from 0x20..0x7f to 0x20..0x7e, since 0x7f is DEL, which is a zero-width character */ + case 0x20 ... 0x7e: action_print(term, data); return STATE_GROUND; + + case 0xc2 ... 0xdf: action_utf8_21(term, data); return STATE_UTF8_21; + case 0xe0 ... 0xef: action_utf8_31(term, data); return STATE_UTF8_31; + case 0xf0 ... 0xf4: action_utf8_41(term, data); return STATE_UTF8_41; + } + + return anywhere(term, data); +} + +static enum state +state_escape_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_ESCAPE; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_ESCAPE_INTERMEDIATE; + case 0x30 ... 0x4f: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x50: action_clear(term); return STATE_DCS_ENTRY; + case 0x51 ... 0x57: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x58: return STATE_SOS_PM_APC_STRING; + case 0x59: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x5a: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x5b: action_clear(term); return STATE_CSI_ENTRY; + case 0x5c: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x5d: action_osc_start(term, data); return STATE_OSC_STRING; + case 0x5e ... 0x5f: return STATE_SOS_PM_APC_STRING; + case 0x60 ... 0x7e: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x7f: action_ignore(term); return STATE_ESCAPE; + } + + return anywhere(term, data); +} + +static enum state +state_escape_intermediate_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_ESCAPE_INTERMEDIATE; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_ESCAPE_INTERMEDIATE; + case 0x30 ... 0x7e: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x7f: action_ignore(term); return STATE_ESCAPE_INTERMEDIATE; + } + + return anywhere(term, data); +} + +static enum state +state_csi_entry_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_ENTRY; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE; + case 0x30 ... 0x39: action_param(term, data); return STATE_CSI_PARAM; + case 0x3a: action_param_new_subparam(term, data); return STATE_CSI_PARAM; + case 0x3b: action_param_new(term, data); return STATE_CSI_PARAM; + + case 0x3c ... 0x3f: action_collect(term, data); return STATE_CSI_PARAM; + case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND; + case 0x7f: action_ignore(term); return STATE_CSI_ENTRY; + } + + return anywhere(term, data); +} + +static enum state +state_csi_param_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_PARAM; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE; + + case 0x30 ... 0x39: action_param(term, data); return STATE_CSI_PARAM; + case 0x3a: action_param_new_subparam(term, data); return STATE_CSI_PARAM; + case 0x3b: action_param_new(term, data); return STATE_CSI_PARAM; + + case 0x3c ... 0x3f: return STATE_CSI_IGNORE; + case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND; + case 0x7f: action_ignore(term); return STATE_CSI_PARAM; + } + + return anywhere(term, data); +} + +static enum state +state_csi_intermediate_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_INTERMEDIATE; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE; + case 0x30 ... 0x3f: return STATE_CSI_IGNORE; + case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND; + case 0x7f: action_ignore(term); return STATE_CSI_INTERMEDIATE; + } + + return anywhere(term, data); +} + +static enum state +state_csi_ignore_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_IGNORE; + + case 0x20 ... 0x3f: action_ignore(term); return STATE_CSI_IGNORE; + case 0x40 ... 0x7e: return STATE_GROUND; + case 0x7f: action_ignore(term); return STATE_CSI_IGNORE; + } + + return anywhere(term, data); +} + +static enum state +state_osc_string_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + + /* Note: original was 20-7f, but I changed to 20-ff to include utf-8. Don't forget to add EXECUTE to 8-bit C1 if we implement that. */ + default: action_osc_put(term, data); return STATE_OSC_STRING; + + case 0x07: action_osc_end(term, data); return STATE_GROUND; + + case 0x00 ... 0x06: + case 0x08 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_ignore(term); return STATE_OSC_STRING; + + + case 0x18: + case 0x1a: action_osc_end(term, data); action_execute(term, data); return STATE_GROUND; + + case 0x1b: action_osc_end(term, data); action_clear(term); return STATE_ESCAPE; + } +} + +static enum state +state_dcs_entry_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_ignore(term); return STATE_DCS_ENTRY; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE; + case 0x30 ... 0x39: action_param(term, data); return STATE_DCS_PARAM; + case 0x3a: return STATE_DCS_IGNORE; + case 0x3b: action_param_new(term, data); return STATE_DCS_PARAM; + case 0x3c ... 0x3f: action_collect(term, data); return STATE_DCS_PARAM; + case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH; + case 0x7f: action_ignore(term); return STATE_DCS_ENTRY; + } + + return anywhere(term, data); +} + +static enum state +state_dcs_param_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_ignore(term); return STATE_DCS_PARAM; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE; + case 0x30 ... 0x39: action_param(term, data); return STATE_DCS_PARAM; + case 0x3a: return STATE_DCS_IGNORE; + case 0x3b: action_param_new(term, data); return STATE_DCS_PARAM; + case 0x3c ... 0x3f: return STATE_DCS_IGNORE; + case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH; + case 0x7f: action_ignore(term); return STATE_DCS_PARAM; + } + + return anywhere(term, data); +} + +static enum state +state_dcs_intermediate_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_ignore(term); return STATE_DCS_INTERMEDIATE; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE; + case 0x30 ... 0x3f: return STATE_DCS_IGNORE; + case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH; + case 0x7f: action_ignore(term); return STATE_DCS_INTERMEDIATE; + } + + return anywhere(term, data); +} + +static enum state +state_dcs_ignore_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: + case 0x20 ... 0x7f: action_ignore(term); return STATE_DCS_IGNORE; + } + + return anywhere(term, data); +} + +static enum state +state_dcs_passthrough_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x7e: action_put(term, data); return STATE_DCS_PASSTHROUGH; + + case 0x7f: action_ignore(term); return STATE_DCS_PASSTHROUGH; + + /* Anywhere */ + case 0x18: action_unhook(term, data); action_execute(term, data); return STATE_GROUND; + case 0x1a: action_unhook(term, data); action_execute(term, data); return STATE_GROUND; + case 0x1b: action_unhook(term, data); action_clear(term); return STATE_ESCAPE; + + /* 8-bit C1 control characters (not supported) */ + case 0x80 ... 0x9f: action_unhook(term, data); return STATE_GROUND; + + default: return STATE_DCS_PASSTHROUGH; + } +} + +static enum state +state_sos_pm_apc_string_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x7f: action_ignore(term); return STATE_SOS_PM_APC_STRING; + } + + return anywhere(term, data); +} + +static enum state +state_utf8_21_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x80 ... 0xbf: action_utf8_22(term, data); return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); + } +} + +static enum state +state_utf8_31_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x80 ... 0xbf: action_utf8_32(term, data); return STATE_UTF8_32; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); + } +} + +static enum state +state_utf8_32_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x80 ... 0xbf: action_utf8_33(term, data); return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); + } +} + +static enum state +state_utf8_41_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x80 ... 0xbf: action_utf8_42(term, data); return STATE_UTF8_42; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); + } +} + +static enum state +state_utf8_42_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x80 ... 0xbf: action_utf8_43(term, data); return STATE_UTF8_43; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); + } +} + +static enum state +state_utf8_43_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x80 ... 0xbf: action_utf8_44(term, data); return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); + } +} + +UNIGNORE_WARNINGS + +void +vt_from_slave(struct terminal *term, const uint8_t *data, size_t len) +{ + enum state current_state = term->vt.state; + + const uint8_t *p = data; + for (size_t i = 0; i < len; i++, p++) { + switch (current_state) { + case STATE_GROUND: current_state = state_ground_switch(term, *p); break; + case STATE_ESCAPE: current_state = state_escape_switch(term, *p); break; + case STATE_ESCAPE_INTERMEDIATE: current_state = state_escape_intermediate_switch(term, *p); break; + case STATE_CSI_ENTRY: current_state = state_csi_entry_switch(term, *p); break; + case STATE_CSI_PARAM: current_state = state_csi_param_switch(term, *p); break; + case STATE_CSI_INTERMEDIATE: current_state = state_csi_intermediate_switch(term, *p); break; + case STATE_CSI_IGNORE: current_state = state_csi_ignore_switch(term, *p); break; + case STATE_OSC_STRING: current_state = state_osc_string_switch(term, *p); break; + case STATE_DCS_ENTRY: current_state = state_dcs_entry_switch(term, *p); break; + case STATE_DCS_PARAM: current_state = state_dcs_param_switch(term, *p); break; + case STATE_DCS_INTERMEDIATE: current_state = state_dcs_intermediate_switch(term, *p); break; + case STATE_DCS_IGNORE: current_state = state_dcs_ignore_switch(term, *p); break; + case STATE_DCS_PASSTHROUGH: current_state = state_dcs_passthrough_switch(term, *p); break; + case STATE_SOS_PM_APC_STRING: current_state = state_sos_pm_apc_string_switch(term, *p); break; + + case STATE_UTF8_21: current_state = state_utf8_21_switch(term, *p); break; + case STATE_UTF8_31: current_state = state_utf8_31_switch(term, *p); break; + case STATE_UTF8_32: current_state = state_utf8_32_switch(term, *p); break; + case STATE_UTF8_41: current_state = state_utf8_41_switch(term, *p); break; + case STATE_UTF8_42: current_state = state_utf8_42_switch(term, *p); break; + case STATE_UTF8_43: current_state = state_utf8_43_switch(term, *p); break; + } + + term->vt.state = current_state; + } +} diff --git a/vt.h b/vt.h new file mode 100644 index 0000000..6695846 --- /dev/null +++ b/vt.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +#include "terminal.h" + +void vt_from_slave(struct terminal *term, const uint8_t *data, size_t len); + +static inline int +vt_param_get(const struct terminal *term, size_t idx, int default_value) +{ + /* + * We zero excess bits in parsed param values. In most cases this will + * effectively be a no-op; but it prevents negative returns for edge + * cases involving unusually large values. + */ + static_assert(INT_MAX >= 0x7fffffff, "POSIX requires INT_MAX >= 0x7fffffff"); + const unsigned value_mask = 0x7fffffff; + + if (term->vt.params.idx > idx) { + unsigned value = term->vt.params.v[idx].value & value_mask; + return value != 0 ? (int)value : default_value; + } + + return default_value; +} diff --git a/wayland.c b/wayland.c new file mode 100644 index 0000000..c2b6194 --- /dev/null +++ b/wayland.c @@ -0,0 +1,2858 @@ +#include "wayland.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#define LOG_MODULE "wayland" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "config.h" +#include "terminal.h" +#include "ime.h" +#include "input.h" +#include "render.h" +#include "selection.h" +#include "shm.h" +#include "shm-formats.h" +#include "util.h" +#include "xmalloc.h" + +static void +csd_reload_font(struct wl_window *win, float old_scale) +{ + struct terminal *term = win->term; + const struct config *conf = term->conf; + + const float scale = term->scale; + + bool enable_csd = win->csd_mode == CSD_YES && !win->is_fullscreen; + if (!enable_csd) + return; + if (win->csd.font != NULL && scale == old_scale) + return; + + fcft_destroy(win->csd.font); + + const char *patterns[conf->csd.font.count]; + for (size_t i = 0; i < conf->csd.font.count; i++) + patterns[i] = conf->csd.font.arr[i].pattern; + + char pixelsize[32]; + snprintf(pixelsize, sizeof(pixelsize), "pixelsize=%u", + (int)roundf(conf->csd.title_height * scale * 1 / 2)); + + LOG_DBG("loading CSD font \"%s:%s\" (old-scale=%.2f, scale=%.2f)", + patterns[0], pixelsize, old_scale, scale); + + win->csd.font = fcft_from_name(conf->csd.font.count, patterns, pixelsize); +} + +static void +csd_instantiate(struct wl_window *win) +{ + struct wayland *wayl = win->term->wl; + xassert(wayl != NULL); + + for (size_t i = 0; i < CSD_SURF_MINIMIZE; i++) { + bool ret = wayl_win_subsurface_new(win, &win->csd.surface[i], true); + xassert(ret); + } + + for (size_t i = CSD_SURF_MINIMIZE; i < CSD_SURF_COUNT; i++) { + bool ret = wayl_win_subsurface_new_with_custom_parent( + win, win->csd.surface[CSD_SURF_TITLE].surface.surf, &win->csd.surface[i], + true); + xassert(ret); + } + + csd_reload_font(win, -1.); +} + +static void +csd_destroy(struct wl_window *win) +{ + struct terminal *term = win->term; + + fcft_destroy(term->window->csd.font); + term->window->csd.font = NULL; + + for (size_t i = 0; i < ALEN(win->csd.surface); i++) + wayl_win_subsurface_destroy(&win->csd.surface[i]); + shm_purge(term->render.chains.csd); +} + +static void +seat_add_data_device(struct seat *seat) +{ + if (seat->wayl->data_device_manager == NULL) + return; + + if (seat->data_device != NULL) { + /* TODO: destroy old device + clipboard data? */ + return; + } + + struct wl_data_device *data_device = wl_data_device_manager_get_data_device( + seat->wayl->data_device_manager, seat->wl_seat); + + if (data_device == NULL) + return; + + seat->data_device = data_device; + wl_data_device_add_listener(data_device, &data_device_listener, seat); +} + +static void +seat_add_primary_selection(struct seat *seat) +{ + if (seat->wayl->primary_selection_device_manager == NULL) + return; + + if (seat->primary_selection_device != NULL) + return; + + struct zwp_primary_selection_device_v1 *primary_selection_device + = zwp_primary_selection_device_manager_v1_get_device( + seat->wayl->primary_selection_device_manager, seat->wl_seat); + + if (primary_selection_device == NULL) + return; + + seat->primary_selection_device = primary_selection_device; + zwp_primary_selection_device_v1_add_listener( + primary_selection_device, &primary_selection_device_listener, seat); +} + +static void +seat_add_text_input(struct seat *seat) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (seat->wayl->text_input_manager == NULL) + return; + + struct zwp_text_input_v3 *text_input + = zwp_text_input_manager_v3_get_text_input( + seat->wayl->text_input_manager, seat->wl_seat); + + if (text_input == NULL) + return; + + seat->wl_text_input = text_input; + zwp_text_input_v3_add_listener(text_input, &text_input_listener, seat); +#endif +} + +static void +seat_add_key_bindings(struct seat *seat) +{ + key_binding_new_for_seat(seat->wayl->key_binding_manager, seat); +} + +static void +seat_destroy(struct seat *seat) +{ + if (seat == NULL) + return; + + tll_free(seat->mouse.buttons); + key_binding_remove_seat(seat->wayl->key_binding_manager, seat); + + if (seat->kbd.xkb_compose_state != NULL) + xkb_compose_state_unref(seat->kbd.xkb_compose_state); + if (seat->kbd.xkb_compose_table != NULL) + xkb_compose_table_unref(seat->kbd.xkb_compose_table); + if (seat->kbd.xkb_keymap != NULL) + xkb_keymap_unref(seat->kbd.xkb_keymap); + if (seat->kbd.xkb_state != NULL) + xkb_state_unref(seat->kbd.xkb_state); + if (seat->kbd.xkb != NULL) + xkb_context_unref(seat->kbd.xkb); + + if (seat->kbd.repeat.fd >= 0) + fdm_del(seat->wayl->fdm, seat->kbd.repeat.fd); + + if (seat->pointer.theme != NULL) + wl_cursor_theme_destroy(seat->pointer.theme); + if (seat->pointer.surface.surf != NULL) + wl_surface_destroy(seat->pointer.surface.surf); + if (seat->pointer.surface.viewport != NULL) + wp_viewport_destroy(seat->pointer.surface.viewport); + if (seat->pointer.xcursor_callback != NULL) + wl_callback_destroy(seat->pointer.xcursor_callback); + + if (seat->clipboard.data_source != NULL) + wl_data_source_destroy(seat->clipboard.data_source); + if (seat->clipboard.data_offer != NULL) + wl_data_offer_destroy(seat->clipboard.data_offer); + if (seat->primary.data_source != NULL) + zwp_primary_selection_source_v1_destroy(seat->primary.data_source); + if (seat->primary.data_offer != NULL) + zwp_primary_selection_offer_v1_destroy(seat->primary.data_offer); + if (seat->primary_selection_device != NULL) + zwp_primary_selection_device_v1_destroy(seat->primary_selection_device); + if (seat->data_device != NULL) + wl_data_device_release(seat->data_device); + if (seat->pointer.shape_device != NULL) + wp_cursor_shape_device_v1_destroy(seat->pointer.shape_device); + if (seat->wl_keyboard != NULL) + wl_keyboard_release(seat->wl_keyboard); + if (seat->wl_pointer != NULL) + wl_pointer_release(seat->wl_pointer); + if (seat->wl_touch != NULL) + wl_touch_release(seat->wl_touch); + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (seat->wl_text_input != NULL) + zwp_text_input_v3_destroy(seat->wl_text_input); +#endif + + if (seat->wl_seat != NULL) + wl_seat_release(seat->wl_seat); + + ime_reset_pending(seat); + free(seat->clipboard.text); + free(seat->primary.text); + free(seat->pointer.last_custom_xcursor); + free(seat->name); +} + +static void +shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) +{ + struct wayland *wayl = data; + + switch (format) { + case WL_SHM_FORMAT_ARGB2101010: wayl->shm_have_argb2101010 = true; break; + case WL_SHM_FORMAT_ABGR2101010: wayl->shm_have_abgr2101010 = true; break; + case WL_SHM_FORMAT_ABGR16161616: wayl->shm_have_abgr161616 = true; break; + } + +#if defined(_DEBUG) && LOG_ENABLE_DBG == 1 + bool have_description = false; + const char c4 = (format >> 24) & 0xff; + const char c3 = (format >> 16) & 0xff; + const char c2 = (format >> 8) & 0xff; + const char c1 = (format >> 0) & 0xff; + + for (size_t i = 0; i < ALEN(shm_formats); i++) { + if (shm_formats[i].format == format) { + LOG_DBG("shm: 0x%08x: %c%c%c%c - %s", + format, + isprint(c1) ? c1 : ' ', + isprint(c2) ? c2 : ' ', + isprint(c3) ? c3 : ' ', + isprint(c4) ? c4 : ' ', + shm_formats[i].description); + have_description = true; + break; + } + } + + if (!have_description) + LOG_DBG("shm: 0x%08x: %c%c%c%c - unknown", + format, + isprint(c1) ? c1 : ' ', + isprint(c2) ? c2 : ' ', + isprint(c3) ? c3 : ' ', + isprint(c4) ? c4 : ' '); +#endif +} + +static const struct wl_shm_listener shm_listener = { + .format = &shm_format, +}; + +static void +xdg_wm_base_ping(void *data, struct xdg_wm_base *shell, uint32_t serial) +{ + LOG_DBG("wm base ping"); + xdg_wm_base_pong(shell, serial); +} + +static const struct xdg_wm_base_listener xdg_wm_base_listener = { + .ping = &xdg_wm_base_ping, +}; + +static void +seat_handle_capabilities(void *data, struct wl_seat *wl_seat, + enum wl_seat_capability caps) +{ + struct seat *seat = data; + xassert(seat->wl_seat == wl_seat); + + LOG_DBG("%s: keyboard=%s, pointer=%s, touch=%s", seat->name, + (caps & WL_SEAT_CAPABILITY_KEYBOARD) ? "yes" : "no", + (caps & WL_SEAT_CAPABILITY_POINTER) ? "yes" : "no", + (caps & WL_SEAT_CAPABILITY_TOUCH) ? "yes" : "no"); + + if (caps & WL_SEAT_CAPABILITY_KEYBOARD) { + if (seat->wl_keyboard == NULL) { + seat->wl_keyboard = wl_seat_get_keyboard(wl_seat); + wl_keyboard_add_listener(seat->wl_keyboard, &keyboard_listener, seat); + } + } else { + if (seat->wl_keyboard != NULL) { + wl_keyboard_release(seat->wl_keyboard); + seat->wl_keyboard = NULL; + } + } + + if (caps & WL_SEAT_CAPABILITY_POINTER) { + if (seat->wl_pointer == NULL) { + xassert(seat->pointer.surface.surf == NULL); + seat->pointer.surface.surf = + wl_compositor_create_surface(seat->wayl->compositor); + + if (seat->pointer.surface.surf == NULL) { + LOG_ERR("%s: failed to create pointer surface", seat->name); + return; + } + + if (seat->wayl->viewporter != NULL) { + xassert(seat->pointer.surface.viewport == NULL); + seat->pointer.surface.viewport = wp_viewporter_get_viewport( + seat->wayl->viewporter, seat->pointer.surface.surf); + + if (seat->pointer.surface.viewport == NULL) { + LOG_ERR("%s: failed to create pointer viewport", seat->name); + wl_surface_destroy(seat->pointer.surface.surf); + seat->pointer.surface.surf = NULL; + return; + } + } + + seat->wl_pointer = wl_seat_get_pointer(wl_seat); + wl_pointer_add_listener(seat->wl_pointer, &pointer_listener, seat); + + if (seat->wayl->cursor_shape_manager != NULL) { + xassert(seat->pointer.shape_device == NULL); + seat->pointer.shape_device = wp_cursor_shape_manager_v1_get_pointer( + seat->wayl->cursor_shape_manager, seat->wl_pointer); + } + } + } else { + if (seat->wl_pointer != NULL) { + if (seat->pointer.shape_device != NULL) { + wp_cursor_shape_device_v1_destroy(seat->pointer.shape_device); + seat->pointer.shape_device = NULL; + } + + wl_pointer_release(seat->wl_pointer); + wl_surface_destroy(seat->pointer.surface.surf); + + if (seat->pointer.surface.viewport != NULL) { + wp_viewport_destroy(seat->pointer.surface.viewport); + seat->pointer.surface.viewport = NULL; + } + + if (seat->pointer.theme != NULL) + wl_cursor_theme_destroy(seat->pointer.theme); + + if (seat->wl_touch != NULL && + seat->touch.state == TOUCH_STATE_INHIBITED) + { + seat->touch.state = TOUCH_STATE_IDLE; + } + + seat->wl_pointer = NULL; + seat->pointer.surface.surf = NULL; + seat->pointer.theme = NULL; + seat->pointer.cursor = NULL; + } + } + + if (caps & WL_SEAT_CAPABILITY_TOUCH) { + if (seat->wl_touch == NULL) { + seat->wl_touch = wl_seat_get_touch(wl_seat); + wl_touch_add_listener(seat->wl_touch, &touch_listener, seat); + + seat->touch.state = TOUCH_STATE_IDLE; + } + } else { + if (seat->wl_touch != NULL) { + wl_touch_release(seat->wl_touch); + seat->wl_touch = NULL; + } + + seat->touch.state = TOUCH_STATE_INHIBITED; + } +} + +static void +seat_handle_name(void *data, struct wl_seat *wl_seat, const char *name) +{ + struct seat *seat = data; + free(seat->name); + seat->name = xstrdup(name); +} + +static const struct wl_seat_listener seat_listener = { + .capabilities = seat_handle_capabilities, + .name = seat_handle_name, +}; + +static void +update_term_for_output_change(struct terminal *term) +{ + const float old_scale = term->scale; + const float logical_width = term->width / old_scale; + const float logical_height = term->height / old_scale; + + /* Note: order matters! term_update_scale() must come first */ + bool scale_updated = term_update_scale(term); + bool fonts_updated = term_font_dpi_changed(term, old_scale); + term_font_subpixel_changed(term); + + csd_reload_font(term->window, old_scale); + + enum resize_options resize_opts = RESIZE_KEEP_GRID; + + if (fonts_updated) { + /* + * If the fonts have been updated, the cell dimensions have + * changed. This requires a "forced" resize, since the surface + * buffer dimensions may not have been updated (in which case + * render_resize() normally shortcuts and returns early). + */ + resize_opts |= RESIZE_FORCE; + } else if (!scale_updated) { + /* No need to resize if neither scale nor fonts have changed */ + return; + } else if (term->conf->dpi_aware) { + /* + * If fonts are sized according to DPI, it is possible for the cell + * size to remain the same when display scale changes. This will not + * change the surface buffer dimensions, but will change the logical + * size of the window. To ensure that the compositor is made aware of + * the proper logical size, force a resize rather than allowing + * render_resize() to shortcut the notification if the buffer + * dimensions remain the same. + */ + resize_opts |= RESIZE_FORCE; + } + + render_resize( + term, + (int)roundf(logical_width), + (int)roundf(logical_height), + resize_opts); +} + +static void +update_terms_on_monitor(struct monitor *mon) +{ + struct wayland *wayl = mon->wayl; + + tll_foreach(wayl->terms, it) { + struct terminal *term = it->item; + + tll_foreach(term->window->on_outputs, it2) { + if (it2->item == mon) { + update_term_for_output_change(term); + break; + } + } + } +} + +static void +output_update_ppi(struct monitor *mon) +{ + if (mon->dim.mm.width <= 0 || mon->dim.mm.height <= 0) + return; + + double x_inches = mon->dim.mm.width * 0.03937008; + double y_inches = mon->dim.mm.height * 0.03937008; + + const int width = mon->dim.px_real.width; + const int height = mon->dim.px_real.height; + + mon->ppi.real.x = mon->dim.px_real.width / x_inches; + mon->ppi.real.y = mon->dim.px_real.height / y_inches; + + /* The *logical* size is affected by the transform */ + switch (mon->transform) { + case WL_OUTPUT_TRANSFORM_90: + case WL_OUTPUT_TRANSFORM_270: + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + case WL_OUTPUT_TRANSFORM_FLIPPED_270: { + int swap = x_inches; + x_inches = y_inches; + y_inches = swap; + break; + } + + case WL_OUTPUT_TRANSFORM_NORMAL: + case WL_OUTPUT_TRANSFORM_180: + case WL_OUTPUT_TRANSFORM_FLIPPED: + case WL_OUTPUT_TRANSFORM_FLIPPED_180: + break; + } + + const int scaled_width = mon->dim.px_scaled.width; + const int scaled_height = mon->dim.px_scaled.height; + + mon->ppi.scaled.x = scaled_width / x_inches; + mon->ppi.scaled.y = scaled_height / y_inches; + + const double px_diag_physical = sqrt(pow(width, 2) + pow(height, 2)); + mon->dpi.physical = width == 0 && height == 0 + ? 96. + : px_diag_physical / mon->inch; + + const double px_diag_scaled = sqrt(pow(scaled_width, 2) + pow(scaled_height, 2)); + mon->dpi.scaled = scaled_width == 0 && scaled_height == 0 + ? 96. + : px_diag_scaled / mon->inch * mon->scale; + + if (mon->dpi.physical > 1000) { + if (mon->name != NULL) { + LOG_WARN("%s: DPI=%f (physical) is unreasonable, using 96 instead", + mon->name, mon->dpi.physical); + } + mon->dpi.physical = 96; + } + + if (mon->dpi.scaled > 1000) { + if (mon->name != NULL) { + LOG_WARN("%s: DPI=%f (logical) is unreasonable, using 96 instead", + mon->name, mon->dpi.scaled); + } + mon->dpi.scaled = 96; + } +} + +static void +output_geometry(void *data, struct wl_output *wl_output, int32_t x, int32_t y, + int32_t physical_width, int32_t physical_height, + int32_t subpixel, const char *make, const char *model, + int32_t transform) +{ + struct monitor *mon = data; + + free(mon->make); + free(mon->model); + + mon->dim.mm.width = physical_width; + mon->dim.mm.height = physical_height; + mon->inch = sqrt(pow(mon->dim.mm.width, 2) + pow(mon->dim.mm.height, 2)) * 0.03937008; + mon->make = make != NULL ? xstrdup(make) : NULL; + mon->model = model != NULL ? xstrdup(model) : NULL; + mon->subpixel = subpixel; + mon->transform = transform; + + output_update_ppi(mon); +} + +static void +output_mode(void *data, struct wl_output *wl_output, uint32_t flags, + int32_t width, int32_t height, int32_t refresh) +{ + if ((flags & WL_OUTPUT_MODE_CURRENT) == 0) + return; + + struct monitor *mon = data; + mon->refresh = (float)refresh / 1000; + mon->dim.px_real.width = width; + mon->dim.px_real.height = height; + output_update_ppi(mon); +} + +static void +output_done(void *data, struct wl_output *wl_output) +{ + struct monitor *mon = data; + update_terms_on_monitor(mon); +} + +static void +output_scale(void *data, struct wl_output *wl_output, int32_t factor) +{ + struct monitor *mon = data; + mon->scale = factor; + output_update_ppi(mon); +} + +#if defined(WL_OUTPUT_NAME_SINCE_VERSION) +static void +output_name(void *data, struct wl_output *wl_output, const char *name) +{ + struct monitor *mon = data; + free(mon->name); + mon->name = name != NULL ? xstrdup(name) : NULL; +} +#endif + +#if defined(WL_OUTPUT_DESCRIPTION_SINCE_VERSION) +static void +output_description(void *data, struct wl_output *wl_output, + const char *description) +{ + struct monitor *mon = data; + free(mon->description); + mon->description = description != NULL ? xstrdup(description) : NULL; +} +#endif + +static const struct wl_output_listener output_listener = { + .geometry = &output_geometry, + .mode = &output_mode, + .done = &output_done, + .scale = &output_scale, +#if defined(WL_OUTPUT_NAME_SINCE_VERSION) + .name = &output_name, +#endif +#if defined(WL_OUTPUT_DESCRIPTION_SINCE_VERSION) + .description = &output_description, +#endif +}; + +static void +xdg_output_handle_logical_position( + void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) +{ + struct monitor *mon = data; + mon->x = x; + mon->y = y; +} + +static void +xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, + int32_t width, int32_t height) +{ + struct monitor *mon = data; + mon->dim.px_scaled.width = width; + mon->dim.px_scaled.height = height; + output_update_ppi(mon); +} + +static void +xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) +{ + struct monitor *mon = data; + update_terms_on_monitor(mon); +} + +static void +xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, + const char *name) +{ + struct monitor *mon = data; + free(mon->name); + mon->name = name != NULL ? xstrdup(name) : NULL; +} + +static void +xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, + const char *description) +{ + struct monitor *mon = data; + free(mon->description); + mon->description = description != NULL ? xstrdup(description) : NULL; +} + +static const struct zxdg_output_v1_listener xdg_output_listener = { + .logical_position = xdg_output_handle_logical_position, + .logical_size = xdg_output_handle_logical_size, + .done = xdg_output_handle_done, + .name = xdg_output_handle_name, + .description = xdg_output_handle_description, +}; + +static void +clock_id(void *data, struct wp_presentation *wp_presentation, uint32_t clk_id) +{ + struct wayland *wayl = data; + wayl->presentation_clock_id = clk_id; + LOG_DBG("presentation clock ID: %u", clk_id); +} + +static const struct wp_presentation_listener presentation_listener = { + .clock_id = &clock_id, +}; + +static void +color_manager_create_image_description(struct wayland *wayl) +{ + if (!wayl->color_management.have_feat_parametric) + return; + + if (!wayl->color_management.have_primaries_srgb) + return; + + if (!wayl->color_management.have_tf_ext_linear) + return; + + struct wp_image_description_creator_params_v1 *params = + wp_color_manager_v1_create_parametric_creator(wayl->color_management.manager); + + wp_image_description_creator_params_v1_set_tf_named( + params, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR); + + wp_image_description_creator_params_v1_set_primaries_named( + params, WP_COLOR_MANAGER_V1_PRIMARIES_SRGB); + + wayl->color_management.img_description = + wp_image_description_creator_params_v1_create(params); +} + +static void +color_manager_supported_intent(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t render_intent) +{ + struct wayland *wayl = data; + if (render_intent == WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL) + wayl->color_management.have_intent_perceptual = true; +} + +static void +color_manager_supported_feature(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t feature) +{ + struct wayland *wayl = data; + + if (feature == WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC) + wayl->color_management.have_feat_parametric = true; +} + +static void +color_manager_supported_tf_named(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t tf) +{ + struct wayland *wayl = data; + if (tf == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR) + wayl->color_management.have_tf_ext_linear = true; +} + +static void +color_manager_supported_primaries_named(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t primaries) +{ + struct wayland *wayl = data; + if (primaries == WP_COLOR_MANAGER_V1_PRIMARIES_SRGB) + wayl->color_management.have_primaries_srgb = true; +} + +static void +color_manager_done(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1) +{ + struct wayland *wayl = data; + color_manager_create_image_description(wayl); +} + +static const struct wp_color_manager_v1_listener color_manager_listener = { + .supported_intent = &color_manager_supported_intent, + .supported_feature = &color_manager_supported_feature, + .supported_primaries_named = &color_manager_supported_primaries_named, + .supported_tf_named = &color_manager_supported_tf_named, + .done = &color_manager_done, +}; + +static bool +verify_iface_version(const char *iface, uint32_t version, uint32_t wanted) +{ + if (version >= wanted) + return true; + + LOG_ERR("%s: need interface version %u, but compositor only implements %u", + iface, wanted, version); + return false; +} + +static void +surface_enter(void *data, struct wl_surface *wl_surface, + struct wl_output *wl_output) +{ + struct wl_window *win = data; + struct terminal *term = win->term; + + tll_foreach(term->wl->monitors, it) { + if (it->item.output == wl_output) { + LOG_DBG("mapped on %s", it->item.name); + tll_push_back(term->window->on_outputs, &it->item); + update_term_for_output_change(term); + return; + } + } + + LOG_ERR("mapped on unknown output"); +} + +static void +surface_leave(void *data, struct wl_surface *wl_surface, + struct wl_output *wl_output) +{ + struct wl_window *win = data; + struct terminal *term = win->term; + + tll_foreach(term->window->on_outputs, it) { + if (it->item->output != wl_output) + continue; + + LOG_DBG("unmapped from %s", it->item->name); + tll_remove(term->window->on_outputs, it); + update_term_for_output_change(term); + return; + } + + LOG_WARN("unmapped from unknown output"); +} + +#if defined(WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) +static void +surface_preferred_buffer_scale(void *data, struct wl_surface *surface, + int32_t scale) +{ + struct wl_window *win = data; + + if (win->preferred_buffer_scale == scale) + return; + + LOG_DBG("wl_surface preferred scale: %d -> %d", win->preferred_buffer_scale, scale); + + win->preferred_buffer_scale = scale; + update_term_for_output_change(win->term); +} + +static void +surface_preferred_buffer_transform(void *data, struct wl_surface *surface, + uint32_t transform) +{ + +} +#endif + +static const struct wl_surface_listener surface_listener = { + .enter = &surface_enter, + .leave = &surface_leave, +#if defined(WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) + .preferred_buffer_scale = &surface_preferred_buffer_scale, + .preferred_buffer_transform = &surface_preferred_buffer_transform, +#endif +}; + +static void +xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, + int32_t width, int32_t height, struct wl_array *states) +{ + bool is_activated = false; + bool is_fullscreen = false; + bool is_maximized = false; + bool is_resizing = false; + bool is_tiled_top = false; + bool is_tiled_bottom = false; + bool is_tiled_left = false; + bool is_tiled_right = false; + bool is_constrained_top = false; + bool is_constrained_bottom = false; + bool is_constrained_left = false; + bool is_constrained_right = false; + bool is_suspended UNUSED = false; + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + char state_str[2048]; + int state_chars = 0; + + static const char *const strings[] = { + [XDG_TOPLEVEL_STATE_MAXIMIZED] = "maximized", + [XDG_TOPLEVEL_STATE_FULLSCREEN] = "fullscreen", + [XDG_TOPLEVEL_STATE_RESIZING] = "resizing", + [XDG_TOPLEVEL_STATE_ACTIVATED] = "activated", + [XDG_TOPLEVEL_STATE_TILED_LEFT] = "tiled:left", + [XDG_TOPLEVEL_STATE_TILED_RIGHT] = "tiled:right", + [XDG_TOPLEVEL_STATE_TILED_TOP] = "tiled:top", + [XDG_TOPLEVEL_STATE_TILED_BOTTOM] = "tiled:bottom", +#if defined(XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION) /* wayland-protocols >= 1.32 */ + [XDG_TOPLEVEL_STATE_SUSPENDED] = "suspended", +#endif +#if defined(XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION) + [XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT] = "constrained:left", + [XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT] = "constrained:right", + [XDG_TOPLEVEL_STATE_CONSTRAINED_TOP] = "constrained:top", + [XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM] = "constrained:bottom", +#endif + }; +#endif + + enum xdg_toplevel_state *state; + wl_array_for_each(state, states) { + switch (*state) { + case XDG_TOPLEVEL_STATE_MAXIMIZED: is_maximized = true; break; + case XDG_TOPLEVEL_STATE_FULLSCREEN: is_fullscreen = true; break; + case XDG_TOPLEVEL_STATE_RESIZING: is_resizing = true; break; + case XDG_TOPLEVEL_STATE_ACTIVATED: is_activated = true; break; + case XDG_TOPLEVEL_STATE_TILED_LEFT: is_tiled_left = true; break; + case XDG_TOPLEVEL_STATE_TILED_RIGHT: is_tiled_right = true; break; + case XDG_TOPLEVEL_STATE_TILED_TOP: is_tiled_top = true; break; + case XDG_TOPLEVEL_STATE_TILED_BOTTOM: is_tiled_bottom = true; break; + +#if defined(XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION) + case XDG_TOPLEVEL_STATE_SUSPENDED: is_suspended = true; break; +#endif +#if defined(XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION) + case XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT: is_constrained_left = true; break; + case XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT: is_constrained_right = true; break; + case XDG_TOPLEVEL_STATE_CONSTRAINED_TOP: is_constrained_top = true; break; + case XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM: is_constrained_bottom = true; break; +#endif + } + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + if (*state >= 0 && *state < ALEN(strings)) { + state_chars += snprintf( + &state_str[state_chars], sizeof(state_str) - state_chars, + "%s, ", + strings[*state] != NULL ? strings[*state] : ""); + } +#endif + } + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + if (state_chars > 2) + state_str[state_chars - 2] = '\0'; + else + state_str[0] = '\0'; + + LOG_DBG("xdg-toplevel: configure: size=%dx%d, states=[%s]", + width, height, state_str); +#endif + + /* + * Changes done here are ignored until the configure event has + * been ack:ed in xdg_surface_configure(). + * + * So, just store the config data and apply it later, in + * xdg_surface_configure() after we've ack:ed the event. + */ + struct wl_window *win = data; + + win->configure.is_activated = is_activated; + win->configure.is_fullscreen = is_fullscreen; + win->configure.is_maximized = is_maximized; + win->configure.is_resizing = is_resizing; + win->configure.is_tiled_top = is_tiled_top; + win->configure.is_tiled_bottom = is_tiled_bottom; + win->configure.is_tiled_left = is_tiled_left; + win->configure.is_tiled_right = is_tiled_right; + win->configure.is_constrained_top = is_constrained_top; + win->configure.is_constrained_bottom = is_constrained_bottom; + win->configure.is_constrained_left = is_constrained_left; + win->configure.is_constrained_right = is_constrained_right; + win->configure.width = width; + win->configure.height = height; +} + +static void +xdg_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel) +{ + struct wl_window *win = data; + struct terminal *term = win->term; + LOG_DBG("xdg-toplevel: close"); + term_shutdown(term); +} + +#if defined(XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION) +static void +xdg_toplevel_configure_bounds(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, int32_t height) +{ + /* TODO: ensure we don't pick a bigger size */ +} +#endif + +#if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) +static void +xdg_toplevel_wm_capabilities(void *data, + struct xdg_toplevel *xdg_toplevel, + struct wl_array *caps) +{ + struct wl_window *win = data; + + win->wm_capabilities.maximize = false; + win->wm_capabilities.minimize = false; + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + char cap_str[2048]; + int cap_chars = 0; + + static const char *const strings[] = { + [XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU] = "window-menu", + [XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE] = "maximize", + [XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN] = "fullscreen", + [XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE] = "minimize", + }; +#endif + + enum xdg_toplevel_wm_capabilities *cap; + wl_array_for_each(cap, caps) { + switch (*cap) { + case XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE: + win->wm_capabilities.maximize = true; + break; + + case XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE: + win->wm_capabilities.minimize = true; + break; + + case XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU: + case XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN: + break; + } + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + if (*cap >= 0 && *cap < ALEN(strings)) { + cap_chars += snprintf( + &cap_str[cap_chars], sizeof(cap_str) - cap_chars, + "%s, ", + strings[*cap] != NULL ? strings[*cap] : ""); + } +#endif + } + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + if (cap_chars > 2) + cap_str[cap_chars - 2] = '\0'; + else + cap_str[0] = '\0'; + + LOG_DBG("xdg-toplevel: wm-capabilities=[%s]", cap_str); +#endif +} +#endif + +static const struct xdg_toplevel_listener xdg_toplevel_listener = { + .configure = &xdg_toplevel_configure, + /*.close = */&xdg_toplevel_close, /* epoll-shim defines a macro 'close'... */ +#if defined(XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION) + .configure_bounds = &xdg_toplevel_configure_bounds, +#endif +#if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) + .wm_capabilities = xdg_toplevel_wm_capabilities, +#endif +}; + +static void +xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, + uint32_t serial) +{ + LOG_DBG("xdg-surface: configure"); + + struct wl_window *win = data; + struct terminal *term = win->term; + + if (win->unmapped) { + /* + * https://codeberg.org/dnkl/foot/issues/1249 + * https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/3487 + * https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/3719 + * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/108 + */ + return; + } + + bool wasnt_configured = !win->is_configured; + bool was_resizing = win->is_resizing; + bool was_fullscreen = win->is_fullscreen; + bool csd_was_enabled = win->csd_mode == CSD_YES && !win->is_fullscreen; + int new_width = win->configure.width; + int new_height = win->configure.height; + + win->is_configured = true; + win->is_maximized = win->configure.is_maximized; + win->is_fullscreen = win->configure.is_fullscreen; + win->is_resizing = win->configure.is_resizing; + + win->is_tiled_top = win->configure.is_tiled_top; + win->is_tiled_bottom = win->configure.is_tiled_bottom; + win->is_tiled_left = win->configure.is_tiled_left; + win->is_tiled_right = win->configure.is_tiled_right; + + win->is_constrained_top = win->configure.is_constrained_top; + win->is_constrained_bottom = win->configure.is_constrained_bottom; + win->is_constrained_left = win->configure.is_constrained_left; + win->is_constrained_right = win->configure.is_constrained_right; + + win->is_tiled = (win->is_tiled_top || + win->is_tiled_bottom || + win->is_tiled_left || + win->is_tiled_right); + + win->csd_mode = win->configure.csd_mode; + + bool enable_csd = win->csd_mode == CSD_YES && !win->is_fullscreen; + if (!csd_was_enabled && enable_csd) + csd_instantiate(win); + else if (csd_was_enabled && !enable_csd) + csd_destroy(win); + + if (enable_csd && new_width > 0 && new_height > 0) { + if (wayl_win_csd_titlebar_visible(win)) + new_height -= win->term->conf->csd.title_height; + + if (wayl_win_csd_borders_visible(win)) { + new_height -= 2 * win->term->conf->csd.border_width_visible; + new_width -= 2 * win->term->conf->csd.border_width_visible; + } + } + + xdg_surface_ack_configure(xdg_surface, serial); + + enum resize_options opts = RESIZE_BY_CELLS; + +#if 1 + /* + * TODO: decide if we should do the last "forced" call when ending + * an interactive resize. + * + * Without it, the last TIOCSWINSZ sent to the client will be a + * scheduled one. I.e. there will be a small delay after the user + * has *stopped* resizing, and the client application receives the + * final size. + * + * Note: if we also disable content centering while resizing, then + * the last, forced, resize *is* necessary. + */ + if (was_resizing && !win->is_resizing) + opts |= RESIZE_FORCE; +#endif + + bool resized = render_resize(term, new_width, new_height, opts); + + if (win->configure.is_activated) + term_visual_focus_in(term); + else + term_visual_focus_out(term); + + /* + * Update opaque region if fullscreen state changed, also need to + * render, since we use different buffer types with and without + * alpha + */ + if (was_fullscreen != win->is_fullscreen) { + wayl_win_alpha_changed(win); + render_refresh(term); + } + + const bool will_render_soon = resized || + term->render.refresh.grid || + term->render.pending.grid; + + if (!will_render_soon) { + /* + * If we didn't resize, and aren't refreshing for other + * reasons, we won't be committing a new surface anytime + * soon. Some compositors require a commit in combination with + * an ack - make them happy. + */ + wl_surface_commit(win->surface.surf); + } + + if (wasnt_configured) + term_window_configured(term); +} + +static const struct xdg_surface_listener xdg_surface_listener = { + .configure = &xdg_surface_configure, +}; + +static void +xdg_toplevel_decoration_configure(void *data, + struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, + uint32_t mode) +{ + struct wl_window *win = data; + + xassert(win->term->conf->csd.preferred != CONF_CSD_PREFER_NONE); + switch (mode) { + case ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE: + LOG_INFO("using CSD decorations"); + win->configure.csd_mode = CSD_YES; + break; + + case ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE: + LOG_INFO("using SSD decorations"); + win->configure.csd_mode = CSD_NO; + break; + + default: + LOG_ERR("unimplemented: unknown XDG toplevel decoration mode: %u", mode); + break; + } +} + +static const struct zxdg_toplevel_decoration_v1_listener xdg_toplevel_decoration_listener = { + .configure = &xdg_toplevel_decoration_configure, +}; + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) +static void +ext_background_capabilities( + void *data, + struct ext_background_effect_manager_v1 *ext_background_effect_manager_v1, + uint32_t flags) +{ + struct wayland *wayl = data; + + wayl->have_background_blur = + !!(flags & EXT_BACKGROUND_EFFECT_MANAGER_V1_CAPABILITY_BLUR); + + LOG_DBG("compositor supports background blur: %s", + wayl->have_background_blur ? "yes" : "no"); +} + +static const struct ext_background_effect_manager_v1_listener background_manager_listener = { + .capabilities = &ext_background_capabilities, +}; +#endif /* HAVE_EXT_BACKGROUND_EFFECT */ + +static bool +fdm_repeat(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct seat *seat = data; + uint64_t expiration_count; + ssize_t ret = read( + seat->kbd.repeat.fd, &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read repeat key from repeat timer fd"); + return false; + } + + seat->kbd.repeat.dont_re_repeat = true; + for (size_t i = 0; i < expiration_count; i++) + input_repeat(seat, seat->kbd.repeat.key); + seat->kbd.repeat.dont_re_repeat = false; + return true; +} + +static void +handle_global(void *data, struct wl_registry *registry, + uint32_t name, const char *interface, uint32_t version) +{ + LOG_DBG("global: 0x%08x, interface=%s, version=%u", name, interface, version); + struct wayland *wayl = data; + + if (streq(interface, wl_compositor_interface.name)) { + const uint32_t required = 4; + if (!verify_iface_version(interface, version, required)) + return; + +#if defined (WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) + const uint32_t preferred = WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + wayl->compositor = wl_registry_bind( + wayl->registry, name, &wl_compositor_interface, min(version, preferred)); + } + + else if (streq(interface, wl_subcompositor_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->sub_compositor = wl_registry_bind( + wayl->registry, name, &wl_subcompositor_interface, required); + } + + else if (streq(interface, wl_shm_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + +#if defined(WL_SHM_RELEASE_SINCE_VERSION) + const uint32_t preferred = WL_SHM_RELEASE_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + + wayl->shm = wl_registry_bind( + wayl->registry, name, &wl_shm_interface, min(version, preferred)); + wl_shm_add_listener(wayl->shm, &shm_listener, wayl); +#if defined(WL_SHM_RELEASE_SINCE_VERSION) + wayl->use_shm_release = version >= WL_SHM_RELEASE_SINCE_VERSION; +#else + wayl->use_shm_release = false; +#endif + } + + else if (streq(interface, xdg_wm_base_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + /* + * We *require* version 1, but _can_ use version 2, 5 or 7, if + * available. + * + * Version 2 adds 'tiled' window states. We use this + * information to restore the window size when window is + * un-tiled. + * + * Version 5 adds 'wm_capabilities'. We use this information + * to draw window decorations. + * + * Version 7 adds 'constrained' window states. We use this + * information to determine whether to allow window resize + * (via CSDs) or not. + */ +#if defined(XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION) + const uint32_t preferred = XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION; +#elif defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) + const uint32_t preferred = XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION; +#elif defined(XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION) + const uint32_t preferred = XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + + wayl->shell = wl_registry_bind( + wayl->registry, name, &xdg_wm_base_interface, min(version, preferred)); + xdg_wm_base_add_listener(wayl->shell, &xdg_wm_base_listener, wayl); + } + + else if (streq(interface, zxdg_decoration_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->xdg_decoration_manager = wl_registry_bind( + wayl->registry, name, &zxdg_decoration_manager_v1_interface, required); + } + + else if (streq(interface, wl_seat_interface.name)) { + const uint32_t required = 5; + if (!verify_iface_version(interface, version, required)) + return; + +#if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION) + const uint32_t preferred = WL_POINTER_AXIS_VALUE120_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + + int repeat_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (repeat_fd == -1) { + LOG_ERRNO("failed to create keyboard repeat timer FD"); + return; + } + + struct wl_seat *wl_seat = wl_registry_bind( + wayl->registry, name, &wl_seat_interface, min(version, preferred)); + + tll_push_back(wayl->seats, ((struct seat){ + .wayl = wayl, + .wl_seat = wl_seat, + .wl_name = name, + .kbd = { + .repeat = { + .fd = repeat_fd, + }, + }})); + + struct seat *seat = &tll_back(wayl->seats); + + if (!fdm_add(wayl->fdm, repeat_fd, EPOLLIN, &fdm_repeat, seat)) { + close(repeat_fd); + seat->kbd.repeat.fd = -1; + seat_destroy(seat); + return; + } + + seat->kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if (seat->kbd.xkb != NULL) { + seat->kbd.xkb_compose_table = xkb_compose_table_new_from_locale( + seat->kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); + + if (seat->kbd.xkb_compose_table != NULL) { + seat->kbd.xkb_compose_state = xkb_compose_state_new( + seat->kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); + } else { + LOG_WARN("failed to instantiate compose table; dead keys (compose) will not work"); + } + } + + seat_add_data_device(seat); + seat_add_primary_selection(seat); + seat_add_text_input(seat); + seat_add_key_bindings(seat); + wl_seat_add_listener(wl_seat, &seat_listener, seat); + } + + else if (streq(interface, zxdg_output_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->xdg_output_manager = wl_registry_bind( + wayl->registry, name, &zxdg_output_manager_v1_interface, + min(version, 2)); + + tll_foreach(wayl->monitors, it) { + struct monitor *mon = &it->item; + mon->xdg = zxdg_output_manager_v1_get_xdg_output( + wayl->xdg_output_manager, mon->output); + zxdg_output_v1_add_listener(mon->xdg, &xdg_output_listener, mon); + } + } + + else if (streq(interface, wl_output_interface.name)) { + const uint32_t required = 2; + if (!verify_iface_version(interface, version, required)) + return; + +#if defined(WL_OUTPUT_NAME_SINCE_VERSION) + const uint32_t preferred = WL_OUTPUT_NAME_SINCE_VERSION; +#elif defined(WL_OUTPUT_RELEASE_SINCE_VERSION) + const uint32_t preferred = WL_OUTPUT_RELEASE_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + + struct wl_output *output = wl_registry_bind( + wayl->registry, name, &wl_output_interface, min(version, preferred)); + + tll_push_back( + wayl->monitors, + ((struct monitor){.wayl = wayl, .output = output, .wl_name = name, + .scale = 1, + .use_output_release = version >= WL_OUTPUT_RELEASE_SINCE_VERSION})); + + struct monitor *mon = &tll_back(wayl->monitors); + wl_output_add_listener(output, &output_listener, mon); + + if (wayl->xdg_output_manager != NULL) { + mon->xdg = zxdg_output_manager_v1_get_xdg_output( + wayl->xdg_output_manager, mon->output); + zxdg_output_v1_add_listener(mon->xdg, &xdg_output_listener, mon); + } + } + + else if (streq(interface, wl_data_device_manager_interface.name)) { + const uint32_t required = 3; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->data_device_manager = wl_registry_bind( + wayl->registry, name, &wl_data_device_manager_interface, required); + + tll_foreach(wayl->seats, it) + seat_add_data_device(&it->item); + } + + else if (streq(interface, zwp_primary_selection_device_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->primary_selection_device_manager = wl_registry_bind( + wayl->registry, name, + &zwp_primary_selection_device_manager_v1_interface, required); + + tll_foreach(wayl->seats, it) + seat_add_primary_selection(&it->item); + } + + else if (streq(interface, wp_presentation_interface.name)) { + if (wayl->presentation_timings) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->presentation = wl_registry_bind( + wayl->registry, name, &wp_presentation_interface, required); + wp_presentation_add_listener( + wayl->presentation, &presentation_listener, wayl); + } + } + + else if (streq(interface, xdg_activation_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->xdg_activation = wl_registry_bind( + wayl->registry, name, &xdg_activation_v1_interface, required); + } + + else if (streq(interface, wp_viewporter_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->viewporter = wl_registry_bind( + wayl->registry, name, &wp_viewporter_interface, required); + } + + else if (streq(interface, wp_fractional_scale_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->fractional_scale_manager = wl_registry_bind( + wayl->registry, name, + &wp_fractional_scale_manager_v1_interface, required); + } + + else if (streq(interface, wp_cursor_shape_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION) /* 1.42 */ + const uint32_t preferred = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + + wayl->shape_manager_version = min(required, preferred); + wayl->cursor_shape_manager = wl_registry_bind( + wayl->registry, name, &wp_cursor_shape_manager_v1_interface, + min(required, preferred)); + } + + else if (streq(interface, wp_single_pixel_buffer_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->single_pixel_manager = wl_registry_bind( + wayl->registry, name, + &wp_single_pixel_buffer_manager_v1_interface, required); + } + + else if (streq(interface, xdg_toplevel_icon_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->toplevel_icon_manager = wl_registry_bind( + wayl->registry, name, &xdg_toplevel_icon_manager_v1_interface, required); + } + + else if (streq(interface, xdg_system_bell_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->system_bell = wl_registry_bind( + wayl->registry, name, &xdg_system_bell_v1_interface, required); + } + + else if (streq(interface, wp_color_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->color_management.manager = wl_registry_bind( + wayl->registry, name, &wp_color_manager_v1_interface, required); + + wp_color_manager_v1_add_listener( + wayl->color_management.manager, &color_manager_listener, wayl); + } + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + else if (streq(interface, xdg_toplevel_tag_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->toplevel_tag_manager = wl_registry_bind( + wayl->registry, name, &xdg_toplevel_tag_manager_v1_interface, required); + } +#endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + else if (streq(interface, ext_background_effect_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->background_effect_manager = wl_registry_bind( + wayl->registry, name, + &ext_background_effect_manager_v1_interface, required); + + ext_background_effect_manager_v1_add_listener( + wayl->background_effect_manager, &background_manager_listener, wayl); + } +#endif + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->text_input_manager = wl_registry_bind( + wayl->registry, name, &zwp_text_input_manager_v3_interface, required); + + tll_foreach(wayl->seats, it) + seat_add_text_input(&it->item); + } +#endif + +} + +static void +monitor_destroy(struct monitor *mon) +{ + if (mon->xdg != NULL) + zxdg_output_v1_destroy(mon->xdg); + if (mon->output != NULL) { + if (mon->use_output_release) + wl_output_release(mon->output); + else + wl_output_destroy(mon->output); + } + free(mon->make); + free(mon->model); + free(mon->name); + free(mon->description); +} + +static void +handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) +{ + LOG_DBG("global removed: 0x%08x", name); + + struct wayland *wayl = data; + + /* Check if this is an output */ + tll_foreach(wayl->monitors, it) { + struct monitor *mon = &it->item; + + if (mon->wl_name != name) + continue; + + LOG_INFO("monitor unplugged or disabled: %s", mon->name); + + /* + * Update all terminals that are mapped here. On Sway 1.4, + * surfaces are *not* unmapped before the output is removed + */ + + tll_foreach(wayl->terms, t) { + tll_foreach(t->item->window->on_outputs, o) { + if (o->item->output == mon->output) { + surface_leave(t->item->window, NULL, mon->output); + break; + } + } + } + + monitor_destroy(mon); + tll_remove(wayl->monitors, it); + return; + } + + /* A seat? */ + tll_foreach(wayl->seats, it) { + struct seat *seat = &it->item; + + if (seat->wl_name != name) + continue; + + LOG_INFO("seat destroyed: %s", seat->name); + + if (seat->kbd_focus != NULL) { + LOG_WARN("compositor destroyed seat '%s' " + "without sending a keyboard leave event", + seat->name); + + if (seat->wl_keyboard != NULL) + keyboard_listener.leave( + seat, seat->wl_keyboard, -1, seat->kbd_focus->window->surface.surf); + } + + if (seat->mouse_focus != NULL) { + LOG_WARN("compositor destroyed seat '%s' " + "without sending a pointer leave event", + seat->name); + + if (seat->wl_pointer != NULL) + pointer_listener.leave( + seat, seat->wl_pointer, -1, seat->mouse_focus->window->surface.surf); + } + + seat_destroy(seat); + tll_remove(wayl->seats, it); + return; + } + + LOG_WARN("unknown global removed: 0x%08x", name); +} + +static const struct wl_registry_listener registry_listener = { + .global = &handle_global, + .global_remove = &handle_global_remove, +}; + +static void +fdm_hook(struct fdm *fdm, void *data) +{ + struct wayland *wayl = data; + wayl_flush(wayl); +} + +static bool +fdm_wayl(struct fdm *fdm, int fd, int events, void *data) +{ + struct wayland *wayl = data; + int event_count = 0; + + if (events & EPOLLIN) { + if (wl_display_read_events(wayl->display) < 0) { + LOG_ERRNO("failed to read events from the Wayland socket"); + return false; + } + + wl_display_dispatch_pending(wayl->display); + + while (wl_display_prepare_read(wayl->display) != 0) { + if (wl_display_dispatch_pending(wayl->display) < 0) { + LOG_ERRNO("failed to dispatch pending Wayland events"); + return false; + } + } + } + + if (events & EPOLLHUP) { + LOG_WARN("disconnected from Wayland"); + /* + * Do *not* call wl_display_cancel_read() here. + * + * Doing so causes later calls to wayl_roundtrip() (called + * from term_destroy() -> wayl_win_destroy()) to hang + * indefinitely. + * + * https://codeberg.org/dnkl/foot/issues/651 + */ + return false; + } + + return event_count != -1; +} + +struct wayland * +wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, + bool presentation_timings) +{ + struct wayland *wayl = calloc(1, sizeof(*wayl)); + if (unlikely(wayl == NULL)) { + LOG_ERRNO("calloc() failed"); + return NULL; + } + + wayl->fdm = fdm; + wayl->key_binding_manager = key_binding_manager; + wayl->fd = -1; + wayl->presentation_timings = presentation_timings; + + if (!fdm_hook_add(fdm, &fdm_hook, wayl, FDM_HOOK_PRIORITY_LOW)) { + LOG_ERR("failed to add FDM hook"); + goto out; + } + + wayl->display = wl_display_connect(NULL); + if (wayl->display == NULL) { + LOG_ERR("failed to connect to wayland; no compositor running?"); + goto out; + } + + wayl->registry = wl_display_get_registry(wayl->display); + if (wayl->registry == NULL) { + LOG_ERR("failed to get wayland registry"); + goto out; + } + + wl_registry_add_listener(wayl->registry, ®istry_listener, wayl); + wl_display_roundtrip(wayl->display); + + if (wayl->compositor == NULL) { + LOG_ERR("no compositor"); + goto out; + } + if (wayl->sub_compositor == NULL) { + LOG_ERR("no sub compositor"); + goto out; + } + if (wayl->shm == NULL) { + LOG_ERR("no shared memory buffers interface"); + goto out; + } + if (wayl->shell == NULL) { + LOG_ERR("no XDG shell interface"); + goto out; + } + if (wayl->data_device_manager == NULL) { + LOG_ERR("no clipboard available " + "(wl_data_device_manager not implemented by server)"); + goto out; + } + if (tll_length(wayl->seats) == 0) { + LOG_ERR("no seats available (wl_seat interface too old?)"); + goto out; + } + if (tll_length(wayl->monitors) == 0) { + LOG_ERR("no monitors available"); + goto out; + } + + if (presentation_timings && wayl->presentation == NULL) { + LOG_ERR("compositor does not implement the presentation time interface"); + goto out; + } + + if (wayl->primary_selection_device_manager == NULL) + LOG_WARN("compositor does not implement the primary selection interface"); + + if (wayl->xdg_activation == NULL) { + LOG_WARN( + "compositor does not implement XDG activation, " + "bell.urgent will fall back to coloring the window margins red"); + } + + if (wayl->fractional_scale_manager == NULL || wayl->viewporter == NULL) + LOG_WARN("compositor does not implement fractional scaling"); + + if (wayl->cursor_shape_manager == NULL) { + LOG_WARN("compositor does not implement server-side cursors, " + "falling back to client-side cursors"); + } + + if (wayl->toplevel_icon_manager == NULL) { + LOG_WARN("compositor does not implement the xdg-toplevel-icon protocol"); + } + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (wayl->text_input_manager == NULL) { + LOG_WARN("text input interface not implemented by compositor; " + "IME will be disabled"); + } +#endif + + /* Trigger listeners registered when handling globals */ + wl_display_roundtrip(wayl->display); + + tll_foreach(wayl->monitors, it) { + LOG_INFO( + "%s: %dx%d+%dx%d@%dHz %s %.2f\" scale=%d, DPI=%.2f/%.2f (physical/scaled)", + it->item.name, it->item.dim.px_real.width, it->item.dim.px_real.height, + it->item.x, it->item.y, (int)roundf(it->item.refresh), + it->item.model != NULL ? it->item.model : it->item.description, + it->item.inch, it->item.scale, + it->item.dpi.physical, it->item.dpi.scaled); + } + + wayl->fd = wl_display_get_fd(wayl->display); + if (fcntl(wayl->fd, F_SETFL, fcntl(wayl->fd, F_GETFL) | O_NONBLOCK) < 0) { + LOG_ERRNO("failed to make Wayland socket non-blocking"); + goto out; + } + + if (!fdm_add(fdm, wayl->fd, EPOLLIN, &fdm_wayl, wayl)) + goto out; + + if (wl_display_prepare_read(wayl->display) != 0) { + LOG_ERRNO("failed to prepare for reading wayland events"); + goto out; + } + + return wayl; + +out: + if (wayl != NULL) + wayl_destroy(wayl); + return NULL; +} + +void +wayl_destroy(struct wayland *wayl) +{ + if (wayl == NULL) + return; + + tll_foreach(wayl->terms, it) { + static bool have_warned = false; + if (!have_warned) { + have_warned = true; + LOG_WARN("there are terminals still running"); + term_destroy(it->item); + } + } + + tll_free(wayl->terms); + + fdm_hook_del(wayl->fdm, &fdm_hook, FDM_HOOK_PRIORITY_LOW); + + tll_foreach(wayl->monitors, it) { + monitor_destroy(&it->item); + tll_remove(wayl->monitors, it); + } + + tll_foreach(wayl->seats, it) { + seat_destroy(&it->item); + tll_remove(wayl->seats, it); + } + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (wayl->text_input_manager != NULL) + zwp_text_input_manager_v3_destroy(wayl->text_input_manager); +#endif + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + if (wayl->toplevel_tag_manager != NULL) + xdg_toplevel_tag_manager_v1_destroy(wayl->toplevel_tag_manager); +#endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (wayl->background_effect_manager != NULL) + ext_background_effect_manager_v1_destroy(wayl->background_effect_manager); +#endif + + if (wayl->color_management.img_description != NULL) + wp_image_description_v1_destroy(wayl->color_management.img_description); + if (wayl->color_management.manager != NULL) + wp_color_manager_v1_destroy(wayl->color_management.manager); + if (wayl->system_bell != NULL) + xdg_system_bell_v1_destroy(wayl->system_bell); + if (wayl->toplevel_icon_manager != NULL) + xdg_toplevel_icon_manager_v1_destroy(wayl->toplevel_icon_manager); + if (wayl->single_pixel_manager != NULL) + wp_single_pixel_buffer_manager_v1_destroy(wayl->single_pixel_manager); + if (wayl->fractional_scale_manager != NULL) + wp_fractional_scale_manager_v1_destroy(wayl->fractional_scale_manager); + if (wayl->viewporter != NULL) + wp_viewporter_destroy(wayl->viewporter); + if (wayl->cursor_shape_manager != NULL) + wp_cursor_shape_manager_v1_destroy(wayl->cursor_shape_manager); + if (wayl->xdg_activation != NULL) + xdg_activation_v1_destroy(wayl->xdg_activation); + if (wayl->xdg_output_manager != NULL) + zxdg_output_manager_v1_destroy(wayl->xdg_output_manager); + if (wayl->shell != NULL) + xdg_wm_base_destroy(wayl->shell); + if (wayl->xdg_decoration_manager != NULL) + zxdg_decoration_manager_v1_destroy(wayl->xdg_decoration_manager); + if (wayl->presentation != NULL) + wp_presentation_destroy(wayl->presentation); + if (wayl->data_device_manager != NULL) + wl_data_device_manager_destroy(wayl->data_device_manager); + if (wayl->primary_selection_device_manager != NULL) + zwp_primary_selection_device_manager_v1_destroy(wayl->primary_selection_device_manager); + if (wayl->shm != NULL) { +#if defined(WL_SHM_RELEASE_SINCE_VERSION) + if (wayl->use_shm_release) + wl_shm_release(wayl->shm); + else +#endif + wl_shm_destroy(wayl->shm); + } + if (wayl->sub_compositor != NULL) + wl_subcompositor_destroy(wayl->sub_compositor); + if (wayl->compositor != NULL) + wl_compositor_destroy(wayl->compositor); + if (wayl->registry != NULL) + wl_registry_destroy(wayl->registry); + if (wayl->fd != -1) + fdm_del_no_close(wayl->fdm, wayl->fd); + if (wayl->display != NULL) { + wayl_flush(wayl); + wl_display_disconnect(wayl->display); + } + + free(wayl); +} + +static void +fractional_scale_preferred_scale( + void *data, struct wp_fractional_scale_v1 *wp_fractional_scale_v1, + uint32_t scale) +{ + struct wl_window *win = data; + + const float new_scale = (float)scale / 120.; + + if (win->scale == new_scale) + return; + + LOG_DBG("fractional scale: %.2f -> %.2f", win->scale, new_scale); + + win->scale = new_scale; + update_term_for_output_change(win->term); +} + +static const struct wp_fractional_scale_v1_listener fractional_scale_listener = { + .preferred_scale = &fractional_scale_preferred_scale, +}; + +struct wl_window * +wayl_win_init(struct terminal *term, const char *token) +{ + struct wayland *wayl = term->wl; + const struct config *conf = term->conf; + + struct wl_window *win = calloc(1, sizeof(*win)); + if (unlikely(win == NULL)) { + LOG_ERRNO("calloc() failed"); + return NULL; + } + + win->term = term; + win->csd_mode = CSD_UNKNOWN; + win->csd.move_timeout_fd = -1; + win->resize_timeout_fd = -1; + win->scale = -1.; + + win->wm_capabilities.maximize = true; + win->wm_capabilities.minimize = true; + + win->surface.surf = wl_compositor_create_surface(wayl->compositor); + if (win->surface.surf == NULL) { + LOG_ERR("failed to create wayland surface"); + goto out; + } + + wl_surface_add_listener(win->surface.surf, &surface_listener, win); + + if (wayl->fractional_scale_manager != NULL && wayl->viewporter != NULL) { + win->surface.viewport = wp_viewporter_get_viewport(wayl->viewporter, win->surface.surf); + + win->fractional_scale = + wp_fractional_scale_manager_v1_get_fractional_scale( + wayl->fractional_scale_manager, win->surface.surf); + wp_fractional_scale_v1_add_listener( + win->fractional_scale, &fractional_scale_listener, win); + } + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (wayl->background_effect_manager != NULL) { + win->surface.background_effect = + ext_background_effect_manager_v1_get_background_effect( + wayl->background_effect_manager, win->surface.surf); + } +#endif + + wayl_win_alpha_changed(win); + + win->xdg_surface = xdg_wm_base_get_xdg_surface(wayl->shell, win->surface.surf); + xdg_surface_add_listener(win->xdg_surface, &xdg_surface_listener, win); + + win->xdg_toplevel = xdg_surface_get_toplevel(win->xdg_surface); + xdg_toplevel_add_listener(win->xdg_toplevel, &xdg_toplevel_listener, win); + + xdg_toplevel_set_app_id(win->xdg_toplevel, conf->app_id); + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + if (conf->toplevel_tag != NULL && conf->toplevel_tag[0] != '\0') { + if (wayl->toplevel_tag_manager != NULL) { + xdg_toplevel_tag_manager_v1_set_toplevel_tag( + wayl->toplevel_tag_manager, win->xdg_toplevel, conf->toplevel_tag); + + /* TODO: the description is recommended to be the tag, but translated */ + xdg_toplevel_tag_manager_v1_set_toplevel_description( + wayl->toplevel_tag_manager, win->xdg_toplevel, conf->toplevel_tag); + } else { + LOG_WARN("compositor does not implement the xdg-toplevel-tag protocol"); + } + } +#endif + + if (wayl->toplevel_icon_manager != NULL) { + const char *app_id = + term->app_id != NULL ? term->app_id : term->conf->app_id; + + struct xdg_toplevel_icon_v1 *icon = + xdg_toplevel_icon_manager_v1_create_icon(wayl->toplevel_icon_manager); + xdg_toplevel_icon_v1_set_name(icon, streq( + app_id, "footclient") ? "foot" : app_id); + xdg_toplevel_icon_manager_v1_set_icon( + wayl->toplevel_icon_manager, win->xdg_toplevel, icon); + xdg_toplevel_icon_v1_destroy(icon); + } + + if (term->conf->gamma_correct) { + if (wayl->color_management.img_description != NULL) { + xassert(wayl->color_management.manager != NULL); + + win->surface.color_management = wp_color_manager_v1_get_surface( + term->wl->color_management.manager, win->surface.surf); + + wp_color_management_surface_v1_set_image_description( + win->surface.color_management, wayl->color_management.img_description, + WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); + } else { + if (wayl->color_management.manager == NULL) { + LOG_WARN( + "gamma-corrected-blending: disabling; " + "compositor does not implement the color-management protocol"); + } else { + LOG_WARN( + "gamma-corrected-blending: disabling; " + "compositor does not implement all required color-management features"); + LOG_WARN("use e.g. 'wayland-info' and verify the compositor implements:"); + LOG_WARN(" - feature: parametric"); + LOG_WARN(" - render intent: perceptual"); + LOG_WARN(" - TF: ext_linear"); + LOG_WARN(" - primaries: sRGB"); + } + } + } + + if (conf->csd.preferred == CONF_CSD_PREFER_NONE) { + /* User specifically do *not* want decorations */ + win->csd_mode = CSD_NO; + LOG_INFO("window decorations disabled by user"); + } else if (wayl->xdg_decoration_manager != NULL) { + win->xdg_toplevel_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration( + wayl->xdg_decoration_manager, win->xdg_toplevel); + + LOG_INFO("requesting %s decorations", + conf->csd.preferred == CONF_CSD_PREFER_SERVER ? "SSD" : "CSD"); + + zxdg_toplevel_decoration_v1_set_mode( + win->xdg_toplevel_decoration, + (conf->csd.preferred == CONF_CSD_PREFER_SERVER + ? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE + : ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE)); + + zxdg_toplevel_decoration_v1_add_listener( + win->xdg_toplevel_decoration, &xdg_toplevel_decoration_listener, win); + } else { + /* No decoration manager - thus we *must* draw our own decorations */ + win->configure.csd_mode = CSD_YES; + LOG_WARN("no decoration manager available - using CSDs unconditionally"); + } + + wl_surface_commit(win->surface.surf); + + /* Complete XDG startup notification */ + wayl_activate(wayl, win, token); + + if (!wayl_win_subsurface_new(win, &win->overlay, false)) { + LOG_ERR("failed to create overlay surface"); + goto out; + } + + if (conf->tabs.enabled) { + if (!wayl_win_subsurface_new(win, &win->tab_bar, true)) { + LOG_ERR("failed to create tab bar surface"); + goto out; + } + wl_subsurface_set_desync(win->tab_bar.sub); + + if (!wayl_win_subsurface_new(win, &win->tab_overview, true)) { + LOG_ERR("failed to create tab overview surface"); + goto out; + } + /* desync so commits aren't blocked on parent surface */ + wl_subsurface_set_desync(win->tab_overview.sub); + } + + /* Initialize tab list with primary terminal */ + win->tabs[0] = term; + win->tab_count = 1; + win->active_tab = 0; + win->tab_overview_state.hover_idx = -1; + + switch (conf->tweak.render_timer) { + case RENDER_TIMER_OSD: + case RENDER_TIMER_BOTH: + if (!wayl_win_subsurface_new(win, &win->render_timer, false)) { + LOG_ERR("failed to create render timer surface"); + goto out; + } + break; + + case RENDER_TIMER_NONE: + case RENDER_TIMER_LOG: + break; + } + + return win; + +out: + if (win != NULL) + wayl_win_destroy(win); + return NULL; +} + +void +wayl_win_destroy(struct wl_window *win) +{ + if (win == NULL) + return; + + struct terminal *term = win->term; + + if (win->csd.move_timeout_fd != -1) + close(win->csd.move_timeout_fd); + + /* + * First, unmap all surfaces to trigger things like + * keyboard_leave() and wl_pointer_leave(). + * + * This ensures we remove all references to *this* window from the + * global wayland struct (since it no longer has neither keyboard + * nor mouse focus). + */ + + if (win->render_timer.surface.surf != NULL) { + wl_surface_attach(win->render_timer.surface.surf, NULL, 0, 0); + wl_surface_commit(win->render_timer.surface.surf); + } + + if (win->scrollback_indicator.surface.surf != NULL) { + wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); + wl_surface_commit(win->scrollback_indicator.surface.surf); + } + + /* Scrollback search */ + if (win->search.surface.surf != NULL) { + wl_surface_attach(win->search.surface.surf, NULL, 0, 0); + wl_surface_commit(win->search.surface.surf); + } + + /* Tab bar */ + if (win->tab_bar.surface.surf != NULL) { + wl_surface_attach(win->tab_bar.surface.surf, NULL, 0, 0); + wl_surface_commit(win->tab_bar.surface.surf); + } + + /* Tab overview */ + if (win->tab_overview.surface.surf != NULL) { + wl_surface_attach(win->tab_overview.surface.surf, NULL, 0, 0); + wl_surface_commit(win->tab_overview.surface.surf); + } + + /* URLs */ + tll_foreach(win->urls, it) { + wl_surface_attach(it->item.surf.surface.surf, NULL, 0, 0); + wl_surface_commit(it->item.surf.surface.surf); + } + + /* CSD */ + for (size_t i = 0; i < ALEN(win->csd.surface); i++) { + if (win->csd.surface[i].surface.surf != NULL) { + wl_surface_attach(win->csd.surface[i].surface.surf, NULL, 0, 0); + wl_surface_commit(win->csd.surface[i].surface.surf); + } + } + + wayl_roundtrip(win->term->wl); + + /* Main window */ + win->unmapped = true; + wl_surface_attach(win->surface.surf, NULL, 0, 0); + wl_surface_commit(win->surface.surf); + wayl_roundtrip(win->term->wl); + + tll_free(win->on_outputs); + + tll_foreach(win->urls, it) { + wayl_win_subsurface_destroy(&it->item.surf); + tll_remove(win->urls, it); + } + + render_wait_for_preapply_damage(term); + + csd_destroy(win); + wayl_win_subsurface_destroy(&win->search); + wayl_win_subsurface_destroy(&win->scrollback_indicator); + wayl_win_subsurface_destroy(&win->render_timer); + wayl_win_subsurface_destroy(&win->overlay); + wayl_win_subsurface_destroy(&win->tab_bar); + wayl_win_subsurface_destroy(&win->tab_overview); + + shm_purge(term->render.chains.search); + shm_purge(term->render.chains.scrollback_indicator); + shm_purge(term->render.chains.render_timer); + shm_purge(term->render.chains.grid); + shm_purge(term->render.chains.url); + shm_purge(term->render.chains.csd); + shm_purge(term->render.chains.tab_bar); + shm_purge(term->render.chains.tab_overview); + + tll_foreach(win->xdg_tokens, it) { + xdg_activation_token_v1_destroy(it->item->xdg_token); + free(it->item); + + tll_remove(win->xdg_tokens, it); + } + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (win->surface.background_effect != NULL) + ext_background_effect_surface_v1_destroy(win->surface.background_effect); +#endif + + if (win->surface.color_management != NULL) + wp_color_management_surface_v1_destroy(win->surface.color_management); + if (win->fractional_scale != NULL) + wp_fractional_scale_v1_destroy(win->fractional_scale); + if (win->surface.viewport != NULL) + wp_viewport_destroy(win->surface.viewport); + if (win->frame_callback != NULL) + wl_callback_destroy(win->frame_callback); + if (win->xdg_toplevel_decoration != NULL) + zxdg_toplevel_decoration_v1_destroy(win->xdg_toplevel_decoration); + if (win->xdg_toplevel != NULL) + xdg_toplevel_destroy(win->xdg_toplevel); + if (win->xdg_surface != NULL) + xdg_surface_destroy(win->xdg_surface); + if (win->surface.surf != NULL) + wl_surface_destroy(win->surface.surf); + + wayl_roundtrip(win->term->wl); + + if (win->resize_timeout_fd >= 0) + fdm_del(win->term->wl->fdm, win->resize_timeout_fd); + free(win); +} + +bool +wayl_reload_xcursor_theme(struct seat *seat, float new_scale) +{ + if (seat->pointer.theme != NULL && seat->pointer.scale == new_scale) { + /* We already have a theme loaded, and the scale hasn't changed */ + return true; + } + + if (seat->pointer.theme != NULL) { + xassert(seat->pointer.scale != new_scale); + wl_cursor_theme_destroy(seat->pointer.theme); + seat->pointer.theme = NULL; + seat->pointer.cursor = NULL; + } + + if (seat->pointer.shape_device != NULL) { + /* Using server side cursors */ + return true; + } + + int xcursor_size = 24; + + { + const char *env_cursor_size = getenv("XCURSOR_SIZE"); + if (env_cursor_size != NULL) { + errno = 0; + char *end; + int size = (int)strtol(env_cursor_size, &end, 10); + + if (errno == 0 && *end == '\0' && size > 0) + xcursor_size = size; + else + LOG_WARN("XCURSOR_SIZE '%s' is invalid, defaulting to 24", + env_cursor_size); + } + } + + const char *xcursor_theme = getenv("XCURSOR_THEME"); + + LOG_INFO("cursor theme: %s, size: %d, scale: %.2f", + xcursor_theme ? xcursor_theme : "(null)", + xcursor_size, new_scale); + + seat->pointer.theme = wl_cursor_theme_load( + xcursor_theme, xcursor_size * new_scale, seat->wayl->shm); + + if (seat->pointer.theme == NULL) { + LOG_ERR("failed to load cursor theme"); + return false; + } + + seat->pointer.scale = new_scale; + return true; +} + +void +wayl_flush(struct wayland *wayl) +{ + while (true) { + int r = wl_display_flush(wayl->display); + if (r >= 0) { + /* Most likely code path - the flush succeed */ + return; + } + + if (errno == EINTR) { + /* Unlikely */ + continue; + } + + if (errno != EAGAIN) { + const int saved_errno = errno; + + if (errno == EPIPE) { + wl_display_read_events(wayl->display); + wl_display_dispatch_pending(wayl->display); + } + + LOG_ERRNO_P(saved_errno, "failed to flush wayland socket"); + return; + } + + /* Socket buffer is full - need to wait for it to become + writeable again */ + xassert(errno == EAGAIN); + + while (true) { + struct pollfd fds[] = {{.fd = wayl->fd, .events = POLLOUT}}; + + r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); + + if (r < 0) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + return; + } + + if (fds[0].revents & POLLHUP) + return; + + xassert(fds[0].revents & POLLOUT); + break; + } + } +} + +void +wayl_roundtrip(struct wayland *wayl) +{ + wl_display_cancel_read(wayl->display); + if (wl_display_roundtrip(wayl->display) < 0) { + LOG_ERRNO("failed to roundtrip Wayland display"); + return; + } + + /* I suspect the roundtrip above clears the pending queue, and + * that prepare_read() will always succeed in the first call. But, + * better safe than sorry... */ + + while (wl_display_prepare_read(wayl->display) != 0) { + if (wl_display_dispatch_pending(wayl->display) < 0) { + LOG_ERRNO("failed to dispatch pending Wayland events"); + return; + } + } + wayl_flush(wayl); +} + +static void +surface_scale_explicit_width_height( + const struct wl_window *win, const struct wayl_surface *surf, + int width, int height, float scale, bool verify) +{ + if (term_fractional_scaling(win->term)) { + LOG_DBG("scaling by a factor of %.2f using fractional scaling " + "(width=%d, height=%d) ", scale, width, height); + + if (verify) { + if ((int)roundf(scale * (int)roundf(width / scale)) != width) { + BUG("width=%d is not valid with scaling factor %.2f (%d != %d)", + width, scale, + (int)roundf(scale * (int)roundf(width / scale)), + width); + } + + if ((int)roundf(scale * (int)roundf(height / scale)) != height) { + BUG("height=%d is not valid with scaling factor %.2f (%d != %d)", + height, scale, + (int)roundf(scale * (int)roundf(height / scale)), + height); + } + } + + xassert(surf->viewport != NULL); + wl_surface_set_buffer_scale(surf->surf, 1); + wp_viewport_set_destination( + surf->viewport, roundf(width / scale), roundf(height / scale)); + } else { + const char *mode UNUSED = term_preferred_buffer_scale(win->term) + ? "wl_surface.preferred_buffer_scale" + : "legacy mode"; + LOG_DBG("scaling by a factor of %.2f using %s " + "(width=%d, height=%d)" , scale, mode, width, height); + + xassert(scale == floorf(scale)); + const int iscale = (int)floorf(scale); + + if (verify) { + if (width % iscale != 0) { + BUG("width=%d is not valid with scaling factor %.2f (%d %% %d != 0)", + width, scale, width, iscale); + } + + if (height % iscale != 0) { + BUG("height=%d is not valid with scaling factor %.2f (%d %% %d != 0)", + height, scale, height, iscale); + } + } + + wl_surface_set_buffer_scale(surf->surf, iscale); + } +} + +void +wayl_surface_scale_explicit_width_height( + const struct wl_window *win, const struct wayl_surface *surf, + int width, int height, float scale) +{ + surface_scale_explicit_width_height(win, surf, width, height, scale, false); +} + +void +wayl_surface_scale(const struct wl_window *win, const struct wayl_surface *surf, + const struct buffer *buf, float scale) +{ + surface_scale_explicit_width_height( + win, surf, buf->width, buf->height, scale, true); +} + +void +wayl_win_scale(struct wl_window *win, const struct buffer *buf) +{ + const struct terminal *term = win->term; + const float scale = term->scale; + + wayl_surface_scale(win, &win->surface, buf, scale); +} + +void +wayl_win_alpha_changed(struct wl_window *win) +{ + struct terminal *term = win->term; + struct wayland *wayl = term->wl; + + /* + * When fullscreened, transparency is disabled (see render.c). + * Update the opaque region to match. + */ + const bool is_opaque = term->colors.alpha == 0xffff || win->is_fullscreen; + + if (is_opaque) { + struct wl_region *region = wl_compositor_create_region(wayl->compositor); + + if (region != NULL) { + wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX); + wl_surface_set_opaque_region(win->surface.surf, region); + wl_region_destroy(region); + } + } else + wl_surface_set_opaque_region(win->surface.surf, NULL); + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (term_theme_get(term)->blur) { + if (wayl->have_background_blur) { + xassert(win->surface.background_effect != NULL); + + if (is_opaque) { + /* No transparency, disable blur */ + LOG_DBG("disabling background blur"); + ext_background_effect_surface_v1_set_blur_region( + win->surface.background_effect, NULL); + } else { + /* We have transparency, enable blur if user has enabled it */ + struct wl_region *region = wl_compositor_create_region(wayl->compositor); + if (region != NULL) { + LOG_DBG("enabling background blur"); + + wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX); + ext_background_effect_surface_v1_set_blur_region( + win->surface.background_effect, region); + wl_region_destroy(region); + } + } + } else { + static bool have_warned = false; + if (!have_warned) { + LOG_WARN("background blur requested, but compositor does not support it"); + have_warned = true; + } + } + } +#endif /* HAVE_EXT_BACKGROUND_EFFECT */ +} + +static void +activation_token_for_urgency_done(const char *token, void *data) +{ + struct wl_window *win = data; + struct wayland *wayl = win->term->wl; + + win->urgency_token_is_pending = false; + xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); +} + +bool +wayl_win_set_urgent(struct wl_window *win) +{ + if (win->urgency_token_is_pending) { + /* We already have a pending token. Don't request another one, + * to avoid flooding the Wayland socket */ + return true; + } + + bool success = wayl_get_activation_token( + win->term->wl, NULL, 0, win, &activation_token_for_urgency_done, win); + + if (success) { + win->urgency_token_is_pending = true; + return true; + } + + return false; +} + +bool +wayl_win_ring_bell(const struct wl_window *win) +{ + if (win->term->wl->system_bell == NULL) { + static bool have_warned = false; + + if (!have_warned) { + LOG_WARN("compositor does not implement the XDG system bell protocol"); + have_warned = true; + } + + return false; + } + + xdg_system_bell_v1_ring(win->term->wl->system_bell, win->surface.surf); + return true; +} + +bool +wayl_win_csd_titlebar_visible(const struct wl_window *win) +{ + return win->csd_mode == CSD_YES && + !win->is_fullscreen && + !(win->is_maximized && win->term->conf->csd.hide_when_maximized); +} + +bool +wayl_win_csd_borders_visible(const struct wl_window *win) +{ + return win->csd_mode == CSD_YES && + !win->is_fullscreen && + !win->is_maximized; +} + +bool +wayl_win_subsurface_new_with_custom_parent( + struct wl_window *win, struct wl_surface *parent, + struct wayl_sub_surface *surf, bool allow_pointer_input) +{ + struct wayland *wayl = win->term->wl; + + surf->surface.surf = NULL; + surf->surface.viewport = NULL; + surf->sub = NULL; + + struct wl_surface *main_surface + = wl_compositor_create_surface(wayl->compositor); + + if (main_surface == NULL) { + LOG_ERR("failed to instantiate surface for sub-surface"); + return false; + } + + surf->surface.color_management = NULL; + if (win->term->conf->gamma_correct && + wayl->color_management.img_description != NULL) + { + xassert(wayl->color_management.manager != NULL); + + surf->surface.color_management = wp_color_manager_v1_get_surface( + wayl->color_management.manager, main_surface); + + wp_color_management_surface_v1_set_image_description( + surf->surface.color_management, wayl->color_management.img_description, + WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); + } + + struct wl_subsurface *sub = wl_subcompositor_get_subsurface( + wayl->sub_compositor, main_surface, parent); + + if (sub == NULL) { + LOG_ERR("failed to instantiate sub-surface"); + wl_surface_destroy(main_surface); + return false; + } + + struct wp_viewport *viewport = NULL; + if (wayl->viewporter != NULL) { + viewport = wp_viewporter_get_viewport(wayl->viewporter, main_surface); + if (viewport == NULL) { + LOG_ERR("failed to instantiate viewport for sub-surface"); + wl_subsurface_destroy(sub); + wl_surface_destroy(main_surface); + return false; + } + } + + wl_surface_set_user_data(main_surface, win); + wl_subsurface_set_sync(sub); + + /* Disable pointer and touch events */ + if (!allow_pointer_input) { + struct wl_region *empty = + wl_compositor_create_region(wayl->compositor); + wl_surface_set_input_region(main_surface, empty); + wl_region_destroy(empty); + } + + surf->surface.surf = main_surface; + surf->sub = sub; + surf->surface.viewport = viewport; + return true; +} + +bool +wayl_win_subsurface_new(struct wl_window *win, struct wayl_sub_surface *surf, + bool allow_pointer_input) +{ + return wayl_win_subsurface_new_with_custom_parent( + win, win->surface.surf, surf, allow_pointer_input); +} + +void +wayl_win_subsurface_destroy(struct wayl_sub_surface *surf) +{ + if (surf == NULL) + return; + + if (surf->surface.color_management != NULL) { + wp_color_management_surface_v1_destroy(surf->surface.color_management); + surf->surface.color_management = NULL; + } + + if (surf->surface.viewport != NULL) { + wp_viewport_destroy(surf->surface.viewport); + surf->surface.viewport = NULL; + } + + if (surf->sub != NULL) { + wl_subsurface_destroy(surf->sub); + surf->sub = NULL; + } + if (surf->surface.surf != NULL) { + wl_surface_destroy(surf->surface.surf); + surf->surface.surf = NULL; + } +} + +static void +activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, + const char *token) +{ + LOG_DBG("XDG activation token done: %s", token); + + struct xdg_activation_token_context *ctx = data; + struct wl_window *win = ctx->win; + + ctx->cb(token, ctx->cb_data); + + tll_foreach(win->xdg_tokens, it) { + if (it->item->xdg_token != xdg_token) + continue; + + xassert(win == it->item->win); + + free(ctx); + xdg_activation_token_v1_destroy(xdg_token); + tll_remove(win->xdg_tokens, it); + return; + } + + BUG("activation token not found in list"); +} + +static const struct +xdg_activation_token_v1_listener activation_token_listener = { + .done = &activation_token_done, +}; + +bool +wayl_get_activation_token( + struct wayland *wayl, struct seat *seat, uint32_t serial, + struct wl_window *win, + void (*cb)(const char *token, void *data), void *cb_data) +{ + if (wayl->xdg_activation == NULL) + return false; + + struct xdg_activation_token_v1 *token = + xdg_activation_v1_get_activation_token(wayl->xdg_activation); + + if (token == NULL) { + LOG_ERR("failed to retrieve XDG activation token"); + return false; + } + + struct xdg_activation_token_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct xdg_activation_token_context){ + .win = win, + .xdg_token = token, + .cb = cb, + .cb_data = cb_data, + }; + tll_push_back(win->xdg_tokens, ctx); + + if (seat != NULL && serial != 0) + xdg_activation_token_v1_set_serial(token, serial, seat->wl_seat); + + xdg_activation_token_v1_set_surface(token, win->surface.surf); + xdg_activation_token_v1_add_listener(token, &activation_token_listener, ctx); + xdg_activation_token_v1_commit(token); + return true; +} + +void +wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token) +{ + if (wayl->xdg_activation == NULL) + return; + + if (token == NULL) + return; + + xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); +} + +bool +wayl_do_linear_blending(const struct wayland *wayl, const struct config *conf) +{ + return conf->gamma_correct && + wayl->color_management.img_description != NULL; +} diff --git a/wayland.h b/wayland.h new file mode 100644 index 0000000..ef1cf54 --- /dev/null +++ b/wayland.h @@ -0,0 +1,598 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +/* Wayland protocols */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + #include +#endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + #include +#endif + +#include +#include + +#include "config.h" +#include "cursor-shape.h" +#include "fdm.h" + +/* Forward declarations */ +struct terminal; +struct buffer; + +/* Mime-types we support when dealing with data offers (e.g. copy-paste, or DnD) */ +enum data_offer_mime_type { + DATA_OFFER_MIME_UNSET, + DATA_OFFER_MIME_TEXT_PLAIN, + DATA_OFFER_MIME_TEXT_UTF8, + DATA_OFFER_MIME_URI_LIST, + + DATA_OFFER_MIME_TEXT_TEXT, + DATA_OFFER_MIME_TEXT_STRING, + DATA_OFFER_MIME_TEXT_UTF8_STRING, +}; + +enum touch_state { + TOUCH_STATE_INHIBITED = -1, + TOUCH_STATE_IDLE, + TOUCH_STATE_HELD, + TOUCH_STATE_DRAGGING, + TOUCH_STATE_SCROLLING, +}; + +struct wayl_surface { + struct wl_surface *surf; + struct wp_viewport *viewport; + struct wp_color_management_surface_v1 *color_management; + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + struct ext_background_effect_surface_v1 *background_effect; +#endif +}; + +struct wayl_sub_surface { + struct wayl_surface surface; + struct wl_subsurface *sub; +}; + +struct wl_window; +struct wl_clipboard { + struct wl_window *window; /* For DnD */ + struct wl_data_source *data_source; + struct wl_data_offer *data_offer; + enum data_offer_mime_type mime_type; + char *text; + uint32_t serial; +}; + +struct wl_primary { + struct zwp_primary_selection_source_v1 *data_source; + struct zwp_primary_selection_offer_v1 *data_offer; + enum data_offer_mime_type mime_type; + char *text; + uint32_t serial; +}; + +/* Maps a mouse button to its "owning" surface */ +struct button_tracker { + int button; + int surf_kind; /* TODO: this is really an "enum term_surface" */ + bool send_to_client; /* Only valid when surface is the main grid surface */ +}; + +struct rect { + int x; + int y; + int width; + int height; +}; + +struct seat { + struct wayland *wayl; + struct wl_seat *wl_seat; + uint32_t wl_name; + char *name; + + /* Focused terminals */ + struct terminal *kbd_focus; + struct terminal *mouse_focus; + struct terminal *ime_focus; + + /* Keyboard state */ + struct wl_keyboard *wl_keyboard; + struct { + uint32_t serial; + + struct xkb_context *xkb; + struct xkb_keymap *xkb_keymap; + struct xkb_state *xkb_state; + struct xkb_compose_table *xkb_compose_table; + struct xkb_compose_state *xkb_compose_state; + struct { + int fd; + + bool dont_re_repeat; + int32_t delay; + int32_t rate; + uint32_t key; + } repeat; + + xkb_mod_index_t mod_shift; + xkb_mod_index_t mod_alt; + xkb_mod_index_t mod_ctrl; + xkb_mod_index_t mod_super; + xkb_mod_index_t mod_caps; + xkb_mod_index_t mod_num; + + xkb_mod_mask_t legacy_significant; /* Significant modifiers for the legacy keyboard protocol */ + xkb_mod_mask_t kitty_significant; /* Significant modifiers for the kitty keyboard protocol */ + + xkb_mod_mask_t virtual_modifiers; /* Set of modifiers to completely ignore */ + + xkb_keycode_t key_arrow_up; + xkb_keycode_t key_arrow_down; + + /* Enabled modifiers */ + bool shift; + bool alt; + bool ctrl; + bool super; + + xkb_keysym_t last_shortcut_sym; + } kbd; + + /* Pointer state */ + struct wl_pointer *wl_pointer; + struct { + uint32_t serial; + + /* Client-side cursor */ + struct wayl_surface surface; + struct wl_cursor_theme *theme; + struct wl_cursor *cursor; + + /* Server-side cursor */ + struct wp_cursor_shape_device_v1 *shape_device; + + float scale; + bool hidden; + enum cursor_shape shape; + char *last_custom_xcursor; + + struct wl_callback *xcursor_callback; + bool xcursor_pending; + } pointer; + + /* Touch state */ + struct wl_touch *wl_touch; + struct { + enum touch_state state; + + uint32_t serial; + uint32_t time; + struct wl_surface *surface; + int surface_kind; + int32_t id; + } touch; + + struct { + int x; + int y; + int col; + int row; + + /* Mouse buttons currently being pressed, and their "owning" surfaces */ + tll(struct button_tracker) buttons; + + /* Double- and triple click state */ + int count; + int last_released_button; + struct timespec last_time; + + /* We used a discrete axis event in the current pointer frame */ + double aggregated[2]; + double aggregated_120[2]; + bool have_discrete; + } mouse; + + /* Clipboard */ + struct wl_data_device *data_device; + struct zwp_primary_selection_device_v1 *primary_selection_device; + + struct wl_clipboard clipboard; + struct wl_primary primary; + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + /* Input Method Editor */ + struct zwp_text_input_v3 *wl_text_input; + struct { + struct { + struct rect pending; + struct rect sent; + } cursor_rect; + + struct { + struct { + char *text; + int32_t cursor_begin; + int32_t cursor_end; + } pending; + + char32_t *text; + struct cell *cells; + int count; + struct { + bool hidden; + int start; /* Cell index, inclusive */ + int end; /* Cell index, exclusive */ + } cursor; + } preedit; + + struct { + struct { + char *text; + } pending; + } commit; + + struct { + struct { + uint32_t before_length; + uint32_t after_length; + } pending; + } surrounding; + + uint32_t serial; + } ime; +#endif +}; + +enum csd_surface { + CSD_SURF_TITLE, + CSD_SURF_LEFT, + CSD_SURF_RIGHT, + CSD_SURF_TOP, + CSD_SURF_BOTTOM, + CSD_SURF_MINIMIZE, + CSD_SURF_MAXIMIZE, + CSD_SURF_CLOSE, + CSD_SURF_COUNT, +}; + +struct monitor { + struct wayland *wayl; + struct wl_output *output; + struct zxdg_output_v1 *xdg; + uint32_t wl_name; + + int x; + int y; + + struct { + /* Physical size, in mm */ + struct { + int width; + int height; + } mm; + + /* Physical size, in pixels */ + struct { + int width; + int height; + } px_real; + + /* Scaled size, in pixels */ + struct { + int width; + int height; + } px_scaled; + } dim; + + struct { + /* PPI, based on physical size */ + struct { + int x; + int y; + } real; + + /* PPI, logical, based on scaled size */ + struct { + int x; + int y; + } scaled; + } ppi; + + struct { + float scaled; + float physical; + } dpi; + + int scale; + float refresh; + enum wl_output_subpixel subpixel; + enum wl_output_transform transform; + + /* From wl_output */ + char *make; + char *model; + + /* From xdg_output */ + char *name; + char *description; + + float inch; /* e.g. 24" */ + + bool use_output_release; +}; + +struct wl_url { + const struct url *url; + struct wayl_sub_surface surf; +}; + +enum csd_mode {CSD_UNKNOWN, CSD_NO, CSD_YES}; + +typedef void (*activation_token_cb_t)(const char *token, void *data); + +/* + * This context holds data used both in the token::done callback, and + * when cleaning up created, by not-yet-done tokens in + * wayl_win_destroy(). + */ +struct xdg_activation_token_context { + struct wl_window *win; /* Need for win->xdg_tokens */ + struct xdg_activation_token_v1 *xdg_token; /* Used to match token in done() */ + activation_token_cb_t cb; /* User provided callback */ + void *cb_data; /* Callback user pointer */ +}; + +struct wayland; +struct wl_window { + struct terminal *term; + struct wayl_surface surface; + struct xdg_surface *xdg_surface; + struct xdg_toplevel *xdg_toplevel; + struct wp_fractional_scale_v1 *fractional_scale; + + tll(struct xdg_activation_token_context *) xdg_tokens; + bool urgency_token_is_pending; + + bool unmapped; + float scale; + int preferred_buffer_scale; + + struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; + + enum csd_mode csd_mode; + + struct { + struct wayl_sub_surface surface[CSD_SURF_COUNT]; + struct fcft_font *font; + int move_timeout_fd; + uint32_t serial; + } csd; + + struct { + bool maximize:1; + bool minimize:1; + } wm_capabilities; + + struct wayl_sub_surface search; + struct wayl_sub_surface scrollback_indicator; + struct wayl_sub_surface render_timer; + struct wayl_sub_surface overlay; + struct wayl_sub_surface tab_bar; + struct wayl_sub_surface tab_overview; + + /* Tab management */ +#define TAB_MAX 32 + struct terminal *tabs[TAB_MAX]; + size_t tab_count; + size_t active_tab; + + /* Tab bar geometry cache, filled by render_tab_bar, read by hit-testing */ + struct { + int xs[TAB_MAX]; + int ws[TAB_MAX]; + int y; + int h; + size_t count; + } tab_layout; + + /* Tab overview state. progress 0=closed, 1=open; target is what we + * animate toward. Coordinates here are in buffer pixels. */ + struct { + bool target_open; /* user wants it open */ + bool visible; /* sub-surface is currently committed (not hidden) */ + float progress; /* 0.0..1.0 eased animation value */ + uint64_t anim_start_ns; /* CLOCK_MONOTONIC, when animation began */ + float anim_from; /* progress value at anim_start */ + size_t sel_idx; /* keyboard-selected card */ + int hover_idx; /* -1 if no hover, else card index */ + /* Card geometry cache (buffer pixels), filled by render_tab_overview */ + struct { + int x, y, w, h; /* card outer rect */ + } cards[TAB_MAX]; + size_t card_count; + } tab_overview_state; + + struct wl_callback *frame_callback; + + tll(const struct monitor *) on_outputs; /* Outputs we're mapped on */ + tll(struct wl_url) urls; + + bool is_configured; + bool is_fullscreen; + bool is_maximized; + bool is_resizing; + bool is_tiled_top; + bool is_tiled_bottom; + bool is_tiled_left; + bool is_tiled_right; + bool is_tiled; /* At least one of is_tiled_{top,bottom,left,right} is true */ + + bool is_constrained_top; + bool is_constrained_bottom; + bool is_constrained_left; + bool is_constrained_right; + + struct { + int width; + int height; + bool is_activated:1; + bool is_fullscreen:1; + bool is_maximized:1; + bool is_resizing:1; + + bool is_tiled_top:1; + bool is_tiled_bottom:1; + bool is_tiled_left:1; + bool is_tiled_right:1; + + bool is_constrained_top:1; + bool is_constrained_bottom:1; + bool is_constrained_left:1; + bool is_constrained_right:1; + + enum csd_mode csd_mode; + } configure; + + int resize_timeout_fd; +}; + +struct terminal; +struct wayland { + struct fdm *fdm; + struct key_binding_manager *key_binding_manager; + + int fd; + + struct wl_display *display; + struct wl_registry *registry; + struct wl_compositor *compositor; + struct wl_subcompositor *sub_compositor; + struct wl_shm *shm; + + struct zxdg_output_manager_v1 *xdg_output_manager; + + struct xdg_wm_base *shell; + struct zxdg_decoration_manager_v1 *xdg_decoration_manager; + + struct wl_data_device_manager *data_device_manager; + struct zwp_primary_selection_device_manager_v1 *primary_selection_device_manager; + + struct xdg_activation_v1 *xdg_activation; + + struct wp_viewporter *viewporter; + struct wp_fractional_scale_manager_v1 *fractional_scale_manager; + + struct wp_cursor_shape_manager_v1 *cursor_shape_manager; + int shape_manager_version; + + struct wp_single_pixel_buffer_manager_v1 *single_pixel_manager; + + struct xdg_toplevel_icon_manager_v1 *toplevel_icon_manager; + + struct xdg_system_bell_v1 *system_bell; + + struct { + struct wp_color_manager_v1 *manager; + struct wp_image_description_v1 *img_description; + bool have_intent_perceptual; + bool have_feat_parametric; + bool have_tf_ext_linear; + bool have_primaries_srgb; + } color_management; + + bool presentation_timings; + struct wp_presentation *presentation; + uint32_t presentation_clock_id; + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + struct xdg_toplevel_tag_manager_v1 *toplevel_tag_manager; +#endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + struct ext_background_effect_manager_v1 *background_effect_manager; + bool have_background_blur; +#endif + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + struct zwp_text_input_manager_v3 *text_input_manager; +#endif + + tll(struct monitor) monitors; /* All available outputs */ + tll(struct seat) seats; + + tll(struct terminal *) terms; + + /* WL_SHM >= 2 */ + bool use_shm_release; + + bool shm_have_argb2101010:1; + bool shm_have_abgr2101010:1; + bool shm_have_abgr161616:1; +}; + +struct wayland *wayl_init( + struct fdm *fdm, struct key_binding_manager *key_binding_manager, + bool presentation_timings); +void wayl_destroy(struct wayland *wayl); + +bool wayl_reload_xcursor_theme(struct seat *seat, float new_scale); + +void wayl_flush(struct wayland *wayl); +void wayl_roundtrip(struct wayland *wayl); + +bool wayl_fractional_scaling(const struct wayland *wayl); +void wayl_surface_scale( + const struct wl_window *win, const struct wayl_surface *surf, + const struct buffer *buf, float scale); +void wayl_surface_scale_explicit_width_height( + const struct wl_window *win, const struct wayl_surface *surf, + int width, int height, float scale); + +struct wl_window *wayl_win_init(struct terminal *term, const char *token); +void wayl_win_destroy(struct wl_window *win); + +void wayl_win_scale(struct wl_window *win, const struct buffer *buf); +void wayl_win_alpha_changed(struct wl_window *win); +bool wayl_win_set_urgent(struct wl_window *win); +bool wayl_win_ring_bell(const struct wl_window *win); + +bool wayl_win_csd_titlebar_visible(const struct wl_window *win); +bool wayl_win_csd_borders_visible(const struct wl_window *win); + +bool wayl_win_subsurface_new( + struct wl_window *win, struct wayl_sub_surface *surf, + bool allow_pointer_input); +bool wayl_win_subsurface_new_with_custom_parent( + struct wl_window *win, struct wl_surface *parent, + struct wayl_sub_surface *surf, bool allow_pointer_input); +void wayl_win_subsurface_destroy(struct wayl_sub_surface *surf); + +bool wayl_get_activation_token( + struct wayland *wayl, struct seat *seat, uint32_t serial, + struct wl_window *win, activation_token_cb_t cb, void *cb_data); +void wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token); + +bool wayl_do_linear_blending(const struct wayland *wayl, const struct config *conf); diff --git a/xkbcommon-vmod.h b/xkbcommon-vmod.h new file mode 100644 index 0000000..44d818e --- /dev/null +++ b/xkbcommon-vmod.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +/* Added in libxkbcommon 1.8.0 */ +#if !defined(XKB_VMOD_NAME_ALT) +/* Common *virtual* modifiers, encoded in xkeyboard-config in the compat and + * symbols files. They have been stable since the beginning of the project and + * are unlikely to ever change. */ +#define XKB_VMOD_NAME_ALT "Alt" +#define XKB_VMOD_NAME_HYPER "Hyper" +#define XKB_VMOD_NAME_LEVEL3 "LevelThree" +#define XKB_VMOD_NAME_LEVEL5 "LevelFive" +#define XKB_VMOD_NAME_META "Meta" +#define XKB_VMOD_NAME_NUM "NumLock" +#define XKB_VMOD_NAME_SCROLL "ScrollLock" +#define XKB_VMOD_NAME_SUPER "Super" +#endif diff --git a/xmalloc.c b/xmalloc.c new file mode 100644 index 0000000..ccfb5c4 --- /dev/null +++ b/xmalloc.c @@ -0,0 +1,96 @@ +#include +#include +#include +#include "xmalloc.h" +#include "debug.h" + +static void * +check_alloc(void *alloc) +{ + if (unlikely(alloc == NULL)) { + FATAL_ERROR(__func__, ENOMEM); + } + return alloc; +} + +void * +xmalloc(size_t size) +{ + if (unlikely(size == 0)) { + size = 1; + } + return check_alloc(malloc(size)); +} + +void * +xcalloc(size_t nmemb, size_t size) +{ + xassert(size != 0); + return check_alloc(calloc(likely(nmemb) ? nmemb : 1, size)); +} + +void * +xrealloc(void *ptr, size_t size) +{ + xassert(size != 0); + void *alloc = realloc(ptr, size); + return check_alloc(alloc); +} + +void * +xreallocarray(void *ptr, size_t n, size_t size) +{ + xassert(n != 0 && size != 0); + void *alloc = reallocarray(ptr, n, size); + return check_alloc(alloc); +} + +char * +xstrdup(const char *str) +{ + return check_alloc(strdup(str)); +} + +char * +xstrndup(const char *str, size_t n) +{ + return check_alloc(strndup(str, n)); +} + +char32_t * +xc32dup(const char32_t *str) +{ + return check_alloc(c32dup(str)); +} + +static VPRINTF(2) int +xvasprintf_(char **strp, const char *format, va_list ap) +{ + va_list ap2; + va_copy(ap2, ap); + int n = vsnprintf(NULL, 0, format, ap2); + if (unlikely(n < 0)) { + FATAL_ERROR("vsnprintf", EILSEQ); + } + va_end(ap2); + *strp = xmalloc(n + 1); + return vsnprintf(*strp, n + 1, format, ap); +} + +char * +xvasprintf(const char *format, va_list ap) +{ + char *str; + xvasprintf_(&str, format, ap); + return str; +} + +char * +xasprintf(const char *format, ...) +{ + va_list ap; + va_start(ap, format); + char *str = xvasprintf(format, ap); + va_end(ap); + return str; +} diff --git a/xmalloc.h b/xmalloc.h new file mode 100644 index 0000000..03e6eb0 --- /dev/null +++ b/xmalloc.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "char32.h" +#include "macros.h" + +void *xmalloc(size_t size) XMALLOC; +void *xcalloc(size_t nmemb, size_t size) XMALLOC; +void *xrealloc(void *ptr, size_t size); +void *xreallocarray(void *ptr, size_t n, size_t size); +char *xstrdup(const char *str) XSTRDUP; +char *xstrndup(const char *str, size_t n) XSTRDUP; +char *xasprintf(const char *format, ...) PRINTF(1) XMALLOC; +char *xvasprintf(const char *format, va_list va) VPRINTF(1) XMALLOC; +char32_t *xc32dup(const char32_t *str) XSTRDUP; + +static inline void * +xmemdup(const void *ptr, size_t size) +{ + return memcpy(xmalloc(size), ptr, size); +} + +static inline char * +xstrjoin(const char *s1, const char *s2) +{ + size_t n1 = strlen(s1); + size_t n2 = strlen(s2); + char *joined = xmalloc(n1 + n2 + 1); + memcpy(joined, s1, n1); + memcpy(joined + n1, s2, n2 + 1); + return joined; +} + +static inline char * +xstrjoin3(const char *s1, const char *s2, const char *s3) +{ + size_t n1 = strlen(s1); + size_t n2 = strlen(s2); + size_t n3 = strlen(s3); + char *joined = xmalloc(n1 + n2 + n3 + 1); + memcpy(joined, s1, n1); + memcpy(joined + n1, s2, n2); + memcpy(joined + n1 + n2, s3, n3 + 1); + return joined; +} diff --git a/xsnprintf.c b/xsnprintf.c new file mode 100644 index 0000000..2f6f849 --- /dev/null +++ b/xsnprintf.c @@ -0,0 +1,55 @@ +#include "xsnprintf.h" + +#include +#include +#include +#include "debug.h" +#include "macros.h" + +/* + * ISO C doesn't require vsnprintf(3) to set errno on failure, but + * POSIX does: + * + * "If an output error was encountered, these functions shall return + * a negative value and set errno to indicate the error." + * + * The mandated errors of interest are: + * + * - EILSEQ: A wide-character code does not correspond to a valid character + * - EOVERFLOW: The value of n is greater than INT_MAX + * - EOVERFLOW: The value to be returned is greater than INT_MAX + * + * ISO C11 states: + * + * "The vsnprintf function returns the number of characters that would + * have been written had n been sufficiently large, not counting the + * terminating null character, or a negative value if an encoding error + * occurred. Thus, the null-terminated output has been completely + * written if and only if the returned value is nonnegative and less + * than n." + * + * See also: + * + * - ISO C11 §7.21.6.12p3 + * - https://pubs.opengroup.org/onlinepubs/9699919799/functions/vsnprintf.html + * - https://pubs.opengroup.org/onlinepubs/9699919799/functions/snprintf.html + */ +static size_t +xvsnprintf(char *restrict buf, size_t n, const char *restrict format, va_list ap) +{ + int len = vsnprintf(buf, n, format, ap); + if (unlikely(len < 0 || len >= (int)n)) { + FATAL_ERROR(__func__, (len < 0) ? errno : ENOBUFS); + } + return (size_t)len; +} + +size_t +xsnprintf(char *restrict buf, size_t n, const char *restrict format, ...) +{ + va_list ap; + va_start(ap, format); + size_t len = xvsnprintf(buf, n, format, ap); + va_end(ap); + return len; +} diff --git a/xsnprintf.h b/xsnprintf.h new file mode 100644 index 0000000..9463a34 --- /dev/null +++ b/xsnprintf.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include +#include "macros.h" + +size_t xsnprintf(char *restrict buf, size_t n, const char *restrict format, ...) PRINTF(3) NONNULL_ARGS;