diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml.disabled similarity index 92% rename from .builds/alpine-x64.yml rename to .builds/alpine-x64.yml.disabled index 2e2ec2c4..d1336b64 100644 --- a/.builds/alpine-x64.yml +++ b/.builds/alpine-x64.yml.disabled @@ -1,4 +1,4 @@ -image: alpine/latest +image: alpine/edge packages: - musl-dev - eudev-libs @@ -47,6 +47,9 @@ tasks: 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/freebsd-x64.yml b/.builds/freebsd-x64.yml index 9642f96d..77775ac3 100644 --- a/.builds/freebsd-x64.yml +++ b/.builds/freebsd-x64.yml @@ -19,7 +19,7 @@ packages: - noto-emoji sources: - - https://git.sr.ht/~dnkl/foot + - https://codeberg.org/dnkl/foot.git # triggers: # - action: email @@ -29,11 +29,12 @@ sources: 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 --buildtype=debug -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot 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 @@ -41,7 +42,7 @@ tasks: - release: | mkdir -p bld/release - meson --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 + 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 diff --git a/.forgejo/issue_template/bug.yml b/.forgejo/issue_template/bug.yml new file mode 100644 index 00000000..921bd68f --- /dev/null +++ b/.forgejo/issue_template/bug.yml @@ -0,0 +1,127 @@ +name: Bug Report +description: File a bug report +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Please provide as many details as possible, we must be able to + understand the bug in order to fix it. + + Don't forget to search the issue tracker in case there is + already an open issue for the bug you found. + - type: input + id: version + attributes: + label: Foot Version + description: "The output of `foot --version`" + placeholder: "foot version: 1.17.2-11-gc4f13809 (May 20 2024, branch 'master') +pgo +ime +graphemes -assertions" + validations: + required: true + - type: input + id: term + attributes: + label: TERM environment variable + description: "The output of `echo $TERM`" + placeholder: "foot" + validations: + required: true + - type: input + id: compositor + attributes: + label: Compositor Name and Version + description: "The name and version of your compositor" + placeholder: "sway version 1.9" + validations: + required: true + - type: input + id: distro + attributes: + label: Distribution + description: "The name of the Linux distribution, or BSD flavor, you are running. And, if applicable, the version" + placeholder: "Fedora Workstation 41" + validations: + required: true + - type: input + id: multiplexer + attributes: + label: Terminal multiplexer + description: "Terminal multiplexers are terminal emulators themselves, therefore the issue may be in the multiplexer, not foot. Please list which multiplexer(s) you use here (and mention in the problem description below if the issue only occurs in the multiplexer, but not in bare metal foot)" + placeholder: "tmux, zellij" + - type: input + id: application + attributes: + label: Shell, TUI, application + description: "Application(s) in which the problem occurs (list all known)" + placeholder: "bash, neovim" + - type: checkboxes + id: server + attributes: + label: Server/standalone mode + description: Does the issue occur in foot server, or standalone mode, or both? Note that you **cannot** test standalone mode by manually running `foot` from a `footclient` instance, since then the standalone foot will simply inherit the server process' context. + options: + - label: Standalone + - label: Server + - type: textarea + id: config + attributes: + label: Foot config + description: Paste your entire `foot.ini` here (do not forget to sanitize it!) + render: ini + validations: + required: true + - type: textarea + id: repro + attributes: + label: Description of Bug and Steps to Reproduce + description: | + Exactly what steps can someone else take to see the bug + themselves? What happens? + validations: + required: true + - type: markdown + attributes: + value: | + Please provide as many details as possible, we must be able to + understand the bug in order to fix it. + + Other software + -------------- + + **Compositors**: have you tested other compositors? Does the + issue happen on all of them, or only your main compositor? + + **Terminal multiplexers**: are you using tmux, zellij, or any + other terminal multiplexer? Does the bug happen in a plain + foot instance? + + **IME** do you use an IME (e.g. fcitx5, ibus etc)? Which one? + Does the bug happen if you disable the IME? + + Obtaining logs and stacktraces + ------------------------------ + + Use a [debug + build](https://codeberg.org/dnkl/foot/src/branch/master/INSTALL.md#debug-build) + of foot if possible, to get a better quality stacktrace in + case of a crash. + + Run foot with logging enabled: + ```sh + foot -d info 2> foot.log + ``` + + In many cases, tracing the Wayland communication is extremely helpful: + ```sh + WAYLAND_DEBUG=1 foot -d info 2> foot.wayland.log + ``` + + Reproduce your problem as quickly as possible, and then exit foot. + - type: textarea + id: logs + attributes: + label: Relevant logs, stacktraces, etc. + - type: markdown + attributes: + value: | + Please attach files instead of pasting the logs, if the logs are large diff --git a/.forgejo/issue_template/config.yml b/.forgejo/issue_template/config.yml new file mode 100644 index 00000000..a1d59354 --- /dev/null +++ b/.forgejo/issue_template/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: IRC + url: https://web.libera.chat/?channels=#foot + about: Join the IRC channel for foot-related discussion and support diff --git a/.forgejo/issue_template/feature_request.yml b/.forgejo/issue_template/feature_request.yml new file mode 100644 index 00000000..52dd09d8 --- /dev/null +++ b/.forgejo/issue_template/feature_request.yml @@ -0,0 +1,26 @@ +name: Feature Request +description: Request a new feature +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Please search the man page (`foot.ini(5)` and `foot(1)`); + maybe the feature already exists? + + If the feature does not exist in your installed version of + foot, please check the **latest** version of foot; maybe the + feature has already been added? + + Please describe your feature request in as much details as + possible. Describe your use case. Explain why the existing + feature set is not sufficient. Foot is (trying to be) a + minimalistic terminal emulator; explain how your desired + feature does not add bloat. + - type: textarea + id: request + attributes: + label: Describe your feature request + validations: + required: true + diff --git a/.woodpecker.yml b/.woodpecker.yaml similarity index 53% rename from .woodpecker.yml rename to .woodpecker.yaml index 06631f89..900251a7 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yaml @@ -1,22 +1,33 @@ -pipeline: - codespell: +# -*- yaml -*- + +steps: + - name: pychecks when: - branch: - - master - - releases/* - image: alpine:latest + - 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 - - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + - pip install mypy + - pip install ruff + - codespell + - mypy + - ruff check + - deactivate - subprojects: + - name: subprojects when: - branch: - - master - - releases/* - image: alpine:latest + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + image: alpine:edge commands: - apk add git - mkdir -p subprojects && cd subprojects @@ -24,13 +35,13 @@ pipeline: - git clone https://codeberg.org/dnkl/fcft.git - cd .. - x64: + - name: x64 when: - branch: - - master - - releases/* - group: build - image: alpine:latest + - 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 @@ -43,7 +54,7 @@ pipeline: # Debug - mkdir -p bld/debug-x64 - cd bld/debug-x64 - - meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - 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 @@ -53,7 +64,7 @@ pipeline: # Release (gcc) - mkdir -p bld/release-x64 - cd bld/release-x64 - - meson --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - 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 @@ -63,7 +74,7 @@ pipeline: # Release (clang) - mkdir -p bld/release-x64-clang - cd bld/release-x64-clang - - CC=clang meson --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - 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 @@ -74,20 +85,20 @@ pipeline: - apk del harfbuzz harfbuzz-dev utf8proc utf8proc-dev - mkdir -p bld/debug - cd bld/debug - - meson --buildtype=debug -Dgrapheme-clustering=disabled -Dfcft:grapheme-shaping=disabled -Dfcft:run-shaping=disabled -Dfcft:test-text-shaping=false ../.. + - 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 ../.. - x86: + - name: x86 when: - branch: - - master - - releases/* - group: build - image: i386/alpine:latest + - 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 @@ -100,7 +111,7 @@ pipeline: # Debug - mkdir -p bld/debug-x86 - cd bld/debug-x86 - - meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - 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 @@ -110,7 +121,7 @@ pipeline: # Release (gcc) - mkdir -p bld/release-x86 - cd bld/release-x86 - - meson --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - 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 @@ -120,7 +131,7 @@ pipeline: # Release (clang) - mkdir -p bld/release-x86-clang - cd bld/release-x86-clang - - CC=clang meson --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 846735d5..f554124b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # 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) @@ -41,6 +69,1475 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed + +* Other output (key presses, query replies etc) being mixed with paste + data, both interactive pastes and OSC-52 ([#2307][2307]). + +[2307]: https://codeberg.org/dnkl/foot/issues/2307 + + +### 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 @@ -58,11 +1555,11 @@ ([#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`). +* "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 - set in the slave process. + unset in the slave process. [1136]: https://codeberg.org/dnkl/foot/issues/1136 [1225]: https://codeberg.org/dnkl/foot/issues/1225 @@ -100,10 +1597,10 @@ 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 re-use (e.g. KDE/plasma) + 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 re-use + compositors that does not allow Wayland buffer reuse (e.g. KDE/plasma). * Nanosecs "overflow" when calculating timeout value for `resize-delay-ms` option. @@ -112,12 +1609,12 @@ * 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’ +* 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]). +* 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 @@ -188,7 +1685,7 @@ * 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 +* 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]) @@ -228,7 +1725,7 @@ ### Changed -* Use `$HOME` instead of `getpwuid()` to retrieve the user’s home +* 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]). @@ -292,7 +1789,7 @@ ### Added * Workaround for Sway bug [#6960][sway-6960]: scrollback search and - the OSC-555 (“flash”) escape sequence leaves dimmed (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 @@ -305,7 +1802,7 @@ ### Changed -* Scrollback search’s `extend-to-word-boundary` no longer stops at +* Scrollback search's `extend-to-word-boundary` no longer stops at space-to-word boundaries, making selection extension feel more natural. @@ -346,7 +1843,7 @@ ([#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 + 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. @@ -402,7 +1899,7 @@ * Build: missing `wayland_client` dependency in `test-config` ([#918][918]). -* “(null)” being logged as font-name (for some fonts) when warning +* "(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]). @@ -424,7 +1921,7 @@ ([#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 +* 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]). @@ -490,15 +1987,15 @@ * 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, +* `[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 +* 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. @@ -526,7 +2023,7 @@ ### Fixed -* Font size adjustment (“zooming”) when font is configured with a +* 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 @@ -600,7 +2097,7 @@ * 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 +* "Window menu" (compositor provided) on right clicks on the CSD title bar. @@ -637,7 +2134,7 @@ ### Fixed -* Regression: `letter-spacing` resulting in a “not a valid option” +* 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, @@ -672,7 +2169,7 @@ * `[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].dim0-7` options, allowing you to configure custom "dim" colors ([#776](https://codeberg.org/dnkl/foot/issues/776)). @@ -688,9 +2185,9 @@ 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”. + "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)). @@ -705,7 +2202,7 @@ ### Fixed -* ‘Sticky’ modifiers in input handling; when determining modifier +* '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. @@ -713,7 +2210,7 @@ ([#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 +* Don't propagate window title updates to the Wayland compositor unless the new title is different from the old title. @@ -733,7 +2230,7 @@ ### 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 + 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)). @@ -761,11 +2258,11 @@ 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 + 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 +* `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. @@ -814,12 +2311,12 @@ 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. +* 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 + 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 @@ -829,7 +2326,7 @@ ### Removed * `km`/`smm`/`rmm` from terminfo; foot prefixes Alt-key combinations - with `ESC`, and not by setting the 8:th “meta” bit, regardless of + 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)). @@ -996,10 +2493,10 @@ For full support, the following is required: 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 +it use fcft's grapheme shaping capabilities to shape combining characters. -This feature is _experimental_ mostly due to the “wcwidth” problem; +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** @@ -1077,9 +2574,9 @@ supported. * 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 +* 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 + 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)). @@ -1132,7 +2629,7 @@ supported. 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 +* 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 @@ -1141,7 +2638,7 @@ supported. 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”_. + 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)). @@ -1337,7 +2834,7 @@ supported. 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 +* Reported sixel "max geometry" from current window size, to the configured maximum size (defaulting to 10000x10000). @@ -1436,7 +2933,7 @@ supported. * 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 +* 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. @@ -1513,7 +3010,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See 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 + 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 @@ -1593,7 +3090,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See `\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 + _"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`. @@ -1641,7 +3138,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ([#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 + **tweak.allow-overflowing-double-width-glyphs** to 'no' to disable this. ### Fixed @@ -1845,7 +3342,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See 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 +* 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 @@ -2022,7 +3519,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * 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 re-use in sixel images. Previously, the palette was reset +* 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 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..26ab32a5 --- /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 index 6cc51750..7df8d0b8 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -45,7 +45,8 @@ subprojects. * wayland (_client_ and _cursor_ libraries) * xkbcommon * utf8proc (_optional_, needed for grapheme clustering) -* libutempter (_optional_, needed for utmp logging) +* 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 @@ -93,24 +94,24 @@ 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 +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, +* 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. +* There may be future additions or changes to foot's terminfo. -As of ncurses 2021-07-31, ncurses includes a version of foot’s +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 +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 +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 +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. @@ -123,10 +124,10 @@ 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 +Hence the recommendation to simply use ncurses' terminfo definitions if available. -If packaging “our” terminfo definitions, I recommend doing that as a +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. @@ -142,17 +143,19 @@ mkdir -p bld/release && cd bld/release 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 | -| `-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 | -| `-Ddefault-utempter-path` | feature | `auto` | Default path to utempter binary (‘none’ disables default) | libutempter | +| 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. @@ -163,9 +166,19 @@ under a different name. Setting this changes the default value of `$TERM`, and the names of the terminfo files (if `-Dterminfo=enabled`). -`-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 +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 @@ -181,7 +194,7 @@ 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). +build script isn't shared with the 'foot' package). Example: @@ -256,7 +269,7 @@ reason there are a number of helper scripts available. 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 +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. @@ -357,7 +370,7 @@ 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 +`${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. @@ -437,7 +450,7 @@ sed 's/@default_terminfo@/foot/g' foot.info | \ tic -o -x -e foot,foot-direct - ``` -Where _”output-directory”_ **must** match the value passed to +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. diff --git a/README.md b/README.md index 8d037af3..985c7e33 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,8 @@ The fast, lightweight and minimalistic Wayland terminal emulator. [![CI status](https://ci.codeberg.org/api/badges/dnkl/foot/status.svg)](https://ci.codeberg.org/dnkl/foot) -[![Pipeline status](https://gitlab.com/dnkl/foot/badges/master/pipeline.svg)](https://gitlab.com/dnkl/foot/commits/master) -[![builds.sr.ht status](https://builds.sr.ht/~dnkl/foot.svg)](https://builds.sr.ht/~dnkl/foot?) -[![Packaging status](https://repology.org/badge/vertical-allrepos/foot.svg)](https://repology.org/project/foot/versions) +[![Packaging status](https://repology.org/badge/vertical-allrepos/foot.svg?columns=4)](https://repology.org/project/foot/versions) ## Index @@ -22,11 +20,13 @@ The fast, lightweight and minimalistic Wayland terminal emulator. 1. [Normal mode](#normal-mode) 1. [Scrollback search](#scrollback-search) 1. [Mouse](#mouse) + 1. [Touchscreen](#touchscreen) 1. [Server (daemon) mode](#server-daemon-mode) 1. [URLs](#urls) 1. [Shell integration](#shell-integration) 1. [Current working directory](#current-working-directory) 1. [Jumping between prompts](#jumping-between-prompts) + 1. [Piping last command's output](#piping-last-command-s-output) 1. [Alt/meta](#alt-meta) 1. [Backspace](#backspace) 1. [Keypad](#keypad) @@ -35,6 +35,7 @@ The fast, lightweight and minimalistic Wayland terminal emulator. 1. [Programmatically checking if running in foot](#programmatically-checking-if-running-in-foot) 1. [XTGETTCAP](#xtgettcap) 1. [Credits](#Credits) +1. [Code of Conduct](#code-of-conduct) 1. [Bugs](#bugs) 1. [Contact](#contact) 1. [IRC](#irc) @@ -60,10 +61,11 @@ The fast, lightweight and minimalistic Wayland terminal emulator. * IME (via `text-input-v3`) * Multi-seat * True Color (24bpp) +* [Styled and colored underlines](https://sw.kovidgoyal.net/kitty/underlines/) * [Synchronized Updates](https://gitlab.freedesktop.org/terminal-wg/specifications/-/merge_requests/2) support * [Sixel image support](https://en.wikipedia.org/wiki/Sixel) - ![wow](doc/sixel-wow.png "Sixel screenshot") + ![tux-with-foot](doc/sixel-tux-foot.png "Sixel screenshot") # Installing @@ -149,10 +151,10 @@ These are the default shortcuts. See `man foot.ini` and the example : Start a scrollback search ctrl++, ctrl+= -: Increase font size by 0,5pt +: Increase font size ctrl+- -: Decrease font size by 0,5pt +: Decrease font size ctrl+0 : Reset font size @@ -163,10 +165,13 @@ These are the default shortcuts. See `man foot.ini` and the example sequence](https://codeberg.org/dnkl/foot/wiki#user-content-spawning-new-terminal-instances-in-the-current-working-directory), the new terminal will start in the current working directory. -ctrl+shift+u +ctrl+shift+o : Enter URL mode, where all currently visible URLs are tagged with a jump label with a key sequence that will open the URL. +ctrl+shift+u +: Enter Unicode input mode. + ctrl+shift+z : Jump to the previous, currently not visible, prompt. Requires [shell integration](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts). @@ -232,7 +237,11 @@ These are the default shortcuts. See `man foot.ini` and the example under the pointer up to, and until, the next space characters. left - **triple-click** -: Selects the entire row +: 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 _primary_ selection @@ -242,9 +251,27 @@ These are the default shortcuts. See `man foot.ini` and the example 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. + ## Server (daemon) mode @@ -277,7 +304,7 @@ when starting your Wayland compositor (i.e. logging in to your desktop), and then run `footclient` instead of `foot` whenever you want to launch a new terminal. -Foot support socket activation, which means `foot --server` will only be +Foot supports socket activation, which means `foot --server` will only be started the first time you'll run `footclient`. (systemd user units are included, but it can work with other supervision suites). @@ -287,10 +314,10 @@ 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+u enters _“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. +Pressing ctrl+shift+o enters _"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 the @@ -313,7 +340,7 @@ the jump label key sequences can be configured. New foot terminal instances (bound to ctrl+shift+n by default) will open in -the current working directory, **if** the shell in the “parent” +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 @@ -344,6 +371,42 @@ 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): + +```ini +[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: + +```fish +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-command-s-output) +for details, and examples for other shells + ## Alt/meta @@ -365,13 +428,13 @@ mode_, `\E[?1034l`), and enabled again with `smm` (_set meta mode_, ## Backspace Foot transmits DEL (`^?`) on backspace. This corresponds to -XTerm's `backarrowKey` option set to `false`, and to DECBKM being -_reset_. +XTerm's `backarrowKey` option set to `false`, and to +[`DECBKM`](https://vt100.net/docs/vt510-rm/DECBKM.html) being _reset_. To instead transmit BS (`^H`), press ctrl+backspace. -Note that foot does **not** implement DECBKM, and that the behavior +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 @@ -411,27 +474,53 @@ This is not how it is meant to be. Fonts are measured in _point sizes_ **for a reason**; a given point size should have the same height on all mediums, be it printers or monitors, regardless of their DPI. -Foot’s default behavior is to use the monitor’s DPI to size fonts when -output scaling has been disabled on **all** monitors. If at least one -monitor has output scaling enabled, fonts will instead by sized using -the scaling factor. +That said, on Wayland, Hi-DPI monitors are typically handled by +configuring a _"scaling factor"_ in the compositor. This is usually +expressed as either a rational value (e.g. _1.5_), or as a percentage +(e.g. _150%_), by which all fonts and window sizes are supposed to be +multiplied. -This can be changed to either **always** use the monitor’s DPI -(regardless of scaling factor), or to **never** use it, with the -`dpi-aware` option in `foot.ini`. See the man page, **foot.ini**(5) -for more information. +For this reason, and because of the new _fractional scaling_ protocol +(see below for details), and because this is how Wayland applications +are expected to behave, foot >= 1.15 will default to scaling fonts +using the compositor's scaling factor, and **not** the monitor +DPI. -When fonts are sized using the monitor’s DPI, glyphs should always -have the same physical height, regardless of monitor. +This means the (assuming the monitors are at the same viewing +distance) the font size will appear to change when you move the foot +window across different monitors, **unless** you have configured the +monitors' scaling factors correctly in the compositor. -Furthermore, foot will re-size the fonts on-the-fly when the window is -moved between screens with different DPIs values. If the window covers -multiple screens, with different DPIs, the highest DPI will be used. +This can be changed by setting the `dpi-aware` option to `yes` in +`foot.ini`. When enabled, fonts will **not** be sized using the +scaling factor, but will instead be sized using the monitor's +DPI. When the foot window is moved across monitors, the font size is +updated for the current monitor's DPI. + +This means that, assuming the monitors are **at the same viewing +distance**, the font size will appear to be the same, at all times. _Note_: if you configure **pixelsize**, rather than **size**, then DPI changes will **not** change the font size. Pixels are always pixels. +### Fractional scaling on Wayland + +For a long time, there was no **true** support for _fractional +scaling_. That is, values like 1.5 (150%), 1.8 (180%) etc, only +integer values, like 2 (200%). + +Compositors that _did_ support fractional scaling did so using a hack; +all applications were told to scale to 200%, and then the compositor +would down-scale the rendered image to e.g. 150%. This works OK for +everything **except fonts**, which ended up blurry. + +With _wayland-protocols 1.32_, a new protocol was introduced, that +allows compositors to tell applications the _actual_ scaling +factor. Applications can then scale the image using a _viewport_ +object, instead of setting a scale factor on the raw pixel buffer. + + ## Supported OSCs OSC, _Operating System Command_, are escape sequences that interacts @@ -458,11 +547,12 @@ with the terminal emulator itself. Foot implements the following OSCs: * `OSC 117` - reset highlight background color * `OSC 119` - reset highlight foreground color * `OSC 133` - [shell integration](#shell-integration) +* `OSC 176` - set app ID * `OSC 555` - flash screen (**foot specific**) * `OSC 777` - desktop notification (only the `;notify` sub-command of OSC 777 is supported.) -See the **foot-ctlseq**(7) man page for a complete list of supported +See the **foot-ctlseqs**(7) man page for a complete list of supported control sequences. @@ -496,7 +586,7 @@ emulator actually responded to. Starting with version 1.7.0, foot also implements `XTVERSION`, to which it will reply with `\EP>|foot(version)\E\\`. Version is -e.g. “1.8.2” for a regular release, or “1.8.2-36-g7db8e06f” for a git +e.g. "1.8.2" for a regular release, or "1.8.2-36-g7db8e06f" for a git build. @@ -509,9 +599,9 @@ It allows querying the terminal for terminfo capabilities. Applications using this feature do not need to use the 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. +foot's terminfo on remote hosts you SSH into. -XTerm’s implementation (as of XTerm-370) only supports querying key +XTerm's implementation (as of XTerm-370) only supports querying key (as in keyboard keys) capabilities, and three custom capabilities: * `TN` - terminal name @@ -523,7 +613,7 @@ 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 +boolean capabilities. This means foot's entire terminfo can be queried via `XTGETTCAP`. Note that both Kitty and foot handles **responses** to @@ -535,7 +625,7 @@ 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. + 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 @@ -547,6 +637,14 @@ 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. +All replies are in `tigetstr()` format. That is, given the same +capability name, foot's reply is identical to what `tigetstr()` would +have returned. + +In addition to queries for terminfo entries, the `query-os-name` query +will be answered with a response of the form `uname=$(uname -s)`, +where `$(uname -s)` is the name of the OS foot was compiled for. + # Credits @@ -554,6 +652,11 @@ queried capabilities are **always** sent. No queries are ever dropped. contributing foot's [logo](icons/hicolor/48x48/apps/foot.png). +# Code of Conduct + +See [Code of Conduct](CODE_OF_CONDUCT.md) + + # Bugs Please report bugs to https://codeberg.org/dnkl/foot/issues @@ -577,20 +680,24 @@ The report should contain the following: ## IRC Ask questions, hang out, sing praise or just say hi in the `#foot` -channel on [irc.libera.chat](https://libera.chat). Logs are available -at https://libera.irclog.whitequark.org/foot. +channel on +[irc.libera.chat](https://web.libera.chat/?channels=#foot). Logs are +available at https://libera.irclog.whitequark.org/foot. ## Mastodon Every now and then I post foot related updates on -[@dnkl@linuxrocks.online](https://linuxrocks.online/@dnkl) +[@dnkl@social.treehouse.systems](https://social.treehouse.systems/@dnkl) # Sponsoring/donations +* Liberapay: https://liberapay.com/dnkl * GitHub Sponsors: https://github.com/sponsors/dnkl +[![Donate using Liberapay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/dnkl/donate) + # License diff --git a/base64.c b/base64.c index fe89e9fa..db697cb0 100644 --- a/base64.c +++ b/base64.c @@ -36,16 +36,13 @@ static const uint8_t reverse_lookup[256] = { }; static const char lookup[64] = { - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', - 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', - 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '+', '/', + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/" }; char * -base64_decode(const char *s) +base64_decode(const char *s, size_t *size) { const size_t len = strlen(s); if (unlikely(len % 4 != 0)) { @@ -57,6 +54,9 @@ base64_decode(const char *s) 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]]; @@ -71,6 +71,13 @@ base64_decode(const char *s) 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; } diff --git a/base64.h b/base64.h index d4042512..3fa3d078 100644 --- a/base64.h +++ b/base64.h @@ -3,6 +3,6 @@ #include #include -char *base64_decode(const char *s); +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 index 3962e341..e69d9648 100644 --- a/box-drawing.c +++ b/box-drawing.c @@ -2,7 +2,6 @@ #include #include -#include #include #define LOG_MODULE "box-drawing" @@ -33,9 +32,12 @@ struct buf { int thickness[2]; - /* For sextants and wedges */ + /* 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}; @@ -1459,14 +1461,12 @@ draw_box_drawings_light_arc(struct buf *buf, char32_t wc) */ for (double i = y_min*16; i <= y_max*16; i++) { errno = 0; - feclearexcept(FE_ALL_EXCEPT); 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 || - fetestexcept(FE_INVALID | FE_DIVBYZERO | FE_OVERFLOW | FE_UNDERFLOW)) + if (errno != 0) { continue; } @@ -2098,7 +2098,7 @@ draw_braille(struct buf *buf, char32_t wc) 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) */ + /* 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; } @@ -2213,6 +2213,7 @@ draw_sextant(struct buf *buf, char32_t wc) LOWER_RIGHT = 1 << 5, }; + /* TODO: move this to a separate file? */ static const uint8_t matrix[60] = { /* U+1fb00 - U+1fb0f */ UPPER_LEFT, @@ -2308,6 +2309,398 @@ draw_sextant(struct buf *buf, char32_t wc) 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) { @@ -2856,6 +3249,7 @@ draw_glyph(struct buf *buf, char32_t wc) 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: @@ -2957,24 +3351,51 @@ box_drawing(const struct terminal *term, char32_t wc) (double)term->conf->tweak.box_drawing_base_thickness * scale * cell_size * dpi / 72.0; base_thickness = max(base_thickness, 1); - int y0 = 0, y1 = 0; + int y_third_0 = 0, y_third_1 = 0; switch (height % 3) { case 0: - y0 = height / 3; - y1 = 2 * height / 3; + y_third_0 = height / 3; + y_third_1 = 2 * height / 3; break; case 1: - y0 = height / 3; - y1 = 2 * height / 3 + 1; + y_third_0 = height / 3; + y_third_1 = 2 * height / 3 + 1; break; case 2: - y0 = height / 3 + 1; - y1 = y0 + height / 3; + 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, @@ -2996,8 +3417,14 @@ box_drawing(const struct terminal *term, char32_t wc) }, .y_thirds = { - y0, /* Endpoint first third, start point second third */ - y1, /* Endpoint second third, start point last third */ + 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, }, }; @@ -3011,7 +3438,7 @@ box_drawing(const struct terminal *term, char32_t wc) .cols = 1, .pix = buf.pix, .x = -term->font_x_ofs, - .y = term->font_y_ofs + term->fonts[0]->ascent, + .y = term->font_baseline, .width = width, .height = height, .advance = { diff --git a/char32.c b/char32.c index e25db3f1..be5bf229 100644 --- a/char32.c +++ b/char32.c @@ -34,7 +34,7 @@ _Static_assert( #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__) +#if (!defined(__STDC_ISO_10646__) || !__STDC_ISO_10646__) && !defined(__FreeBSD__) && !defined(__OpenBSD__) #error "wchar_t does not use UTF-32" #endif @@ -53,6 +53,14 @@ UNITTEST 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]; @@ -129,11 +137,25 @@ UNITTEST UNITTEST { - char32_t *c = c32dup(U"foobar"); + 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 = c32dup(U""); + c = xc32dup(U""); xassert(c32cmp(c, U"") == 0); free(c); } @@ -176,7 +198,7 @@ done: return chars; err: - return (char32_t)-1; + return (size_t)-1; } UNITTEST diff --git a/char32.h b/char32.h index 6324c9a0..dcb412ce 100644 --- a/char32.h +++ b/char32.h @@ -8,6 +8,10 @@ #include #include +#if defined(FOOT_GRAPHEME_CLUSTERING) + #include +#endif + static inline size_t c32len(const char32_t *s) { return wcslen((const wchar_t *)s); } @@ -16,6 +20,10 @@ 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); } @@ -56,6 +64,10 @@ 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); } @@ -68,12 +80,30 @@ 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); diff --git a/client-protocol.h b/client-protocol.h index 505825f6..efd601d7 100644 --- a/client-protocol.h +++ b/client-protocol.h @@ -29,3 +29,17 @@ struct client_data { } __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 index 6954d17e..befd3ab0 100644 --- a/client.c +++ b/client.c @@ -1,12 +1,13 @@ -#include -#include -#include -#include -#include -#include -#include -#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -22,7 +23,6 @@ #include "foot-features.h" #include "macros.h" #include "util.h" -#include "version.h" #include "xmalloc.h" extern char **environ; @@ -34,13 +34,20 @@ struct string { typedef tll(struct string) string_list_t; static volatile sig_atomic_t aborted = 0; +static volatile sig_atomic_t sigusr = 0; static void -sig_handler(int signo) +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) { @@ -62,19 +69,6 @@ sendall(int sock, const void *_buf, size_t len) return len; } -static const char * -version_and_features(void) -{ - static char buf[256]; - snprintf(buf, sizeof(buf), "version: %s %cpgo %cime %cgraphemes %cassertions", - FOOT_VERSION, - feature_pgo() ? '+' : '-', - feature_ime() ? '+' : '-', - feature_graphemes() ? '+' : '-', - feature_assertions() ? '+' : '-'); - return buf; -} - static void print_usage(const char *prog_name) { @@ -83,6 +77,7 @@ print_usage(const char *prog_name) " -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" @@ -144,11 +139,15 @@ send_string_list(int fd, const string_list_t *string_list) 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 failiing */ + * itself failing, and the client application failing */ static const int foot_exit_failure = -36; int ret = foot_exit_failure; @@ -158,6 +157,7 @@ main(int argc, char *const *argv) {"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'}, @@ -227,6 +227,12 @@ main(int argc, char *const *argv) 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; @@ -314,11 +320,11 @@ main(int argc, char *const *argv) } case 'l': - if (optarg == NULL || strcmp(optarg, "auto") == 0) + if (optarg == NULL || streq(optarg, "auto")) log_colorize = LOG_COLORIZE_AUTO; - else if (strcmp(optarg, "never") == 0) + else if (streq(optarg, "never")) log_colorize = LOG_COLORIZE_NEVER; - else if (strcmp(optarg, "always") == 0) + 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); @@ -327,7 +333,7 @@ main(int argc, char *const *argv) break; case 'v': - printf("footclient %s\n", version_and_features()); + print_version_and_features("footclient "); ret = EXIT_SUCCESS; goto err; @@ -371,16 +377,19 @@ main(int argc, char *const *argv) const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); if (xdg_runtime != NULL) { const char *wayland_display = getenv("WAYLAND_DISPLAY"); - if (wayland_display != NULL) + if (wayland_display != NULL) { snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/foot-%s.sock", xdg_runtime, wayland_display); - else + 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); - - if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0) - connected = true; - else + 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); } @@ -395,10 +404,10 @@ main(int argc, char *const *argv) const char *cwd = custom_cwd; if (cwd == NULL) { - errno = 0; 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; @@ -415,7 +424,7 @@ main(int argc, char *const *argv) if (resolved_path_cwd != NULL && resolved_path_pwd != NULL && - strcmp(resolved_path_cwd, resolved_path_pwd) == 0) + streq(resolved_path_cwd, resolved_path_pwd)) { /* * The resolved path of $PWD matches the resolved path of @@ -518,15 +527,63 @@ main(int argc, char *const *argv) if (!send_string_list(fd, &envp)) goto err; - struct sigaction sa = {.sa_handler = &sig_handler}; - sigemptyset(&sa.sa_mask); - if (sigaction(SIGINT, &sa, NULL) < 0 || sigaction(SIGTERM, &sa, NULL) < 0) { + 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 = recv(fd, &exit_code, sizeof(exit_code), 0); + 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); diff --git a/commands.c b/commands.c index 7b93f044..a3e48458 100644 --- a/commands.c +++ b/commands.c @@ -23,7 +23,7 @@ cmd_scrollback_up(struct terminal *term, int rows) 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 */ + * 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); diff --git a/completions/bash/foot b/completions/bash/foot index eb17dad1..e27be2fa 100644 --- a/completions/bash/foot +++ b/completions/bash/foot @@ -6,6 +6,7 @@ _foot() local cur prev flags word commands match previous_words i offset flags=( "--app-id" + "--toplevel-tag" "--check-config" "--config" "--font" @@ -19,6 +20,7 @@ _foot() "--maximized" "--override" "--print-pid" + "--pty" "--server" "--term" "--title" @@ -39,7 +41,7 @@ _foot() 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|--config|--font|--log-level|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; 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 @@ -74,7 +76,7 @@ _foot() COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; --log-colorize|-l) COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; - --app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) + --app-id|--toplevel-tag|--help|--override|--pty|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) # Don't autocomplete for these flags : ;; *) diff --git a/completions/bash/footclient b/completions/bash/footclient index 62abdd65..c7f1df4e 100644 --- a/completions/bash/footclient +++ b/completions/bash/footclient @@ -6,6 +6,7 @@ _footclient() local cur prev flags word commands match previous_words i offset flags=( "--app-id" + "--toplevel-tag" "--fullscreen" "--help" "--hold" @@ -35,7 +36,7 @@ _footclient() 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|--log-level|--server-socket|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; 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 @@ -67,7 +68,7 @@ _footclient() COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; --log-colorize|-l) COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; - --app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|-[ahoTvWw]) + --app-id|--toplevel-tag|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|-[ahoTvWw]) # Don't autocomplete for these flags : ;; *) diff --git a/completions/fish/foot.fish b/completions/fish/foot.fish index 86f6616d..21b42d3d 100644 --- a/completions/fish/foot.fish +++ b/completions/fish/foot.fish @@ -6,6 +6,7 @@ complete -c foot -x -s f -l font -a "(fc-list : family | sed 's/,/ 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" @@ -18,5 +19,6 @@ complete -c foot -r -s p -l print-pid 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 index df3e1273..03624796 100644 --- a/completions/fish/footclient.fish +++ b/completions/fish/footclient.fish @@ -2,6 +2,7 @@ complete -c footclient -x -a "(__fish_complete_subcom 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" diff --git a/completions/zsh/_foot b/completions/zsh/_foot index b9f46cdc..0fd83b3c 100644 --- a/completions/zsh/_foot +++ b/completions/zsh/_foot @@ -9,6 +9,7 @@ _arguments \ '(-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]' \ @@ -18,6 +19,7 @@ _arguments \ '(-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)]' \ diff --git a/completions/zsh/_footclient b/completions/zsh/_footclient index c14d65d5..12f29d7a 100644 --- a/completions/zsh/_footclient +++ b/completions/zsh/_footclient @@ -5,6 +5,7 @@ _arguments \ '(-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]' \ diff --git a/composed.c b/composed.c index 442325ea..fc7dfa00 100644 --- a/composed.c +++ b/composed.c @@ -4,8 +4,54 @@ #include #include "debug.h" +#include "terminal.h" -struct composed * +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; @@ -20,6 +66,41 @@ composed_lookup(struct composed *root, uint32_t key) 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) { diff --git a/composed.h b/composed.h index 17158407..18afb146 100644 --- a/composed.h +++ b/composed.h @@ -10,9 +10,16 @@ struct composed { uint32_t key; uint8_t count; uint8_t width; + uint8_t forced_width; }; -struct composed *composed_lookup(struct composed *root, uint32_t key); +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 index 2ede1aa5..12c594bc 100644 --- a/config.c +++ b/config.c @@ -30,8 +30,8 @@ #include "xmalloc.h" #include "xsnprintf.h" -static const uint32_t default_foreground = 0x839496; -static const uint32_t default_background = 0x002b36; +static const uint32_t default_foreground = 0xffffff; +static const uint32_t default_background = 0x242424; static const size_t min_csd_border_width = 5; @@ -48,23 +48,23 @@ static const size_t min_csd_border_width = 5; static const uint32_t default_color_table[256] = { // Regular - 0x073642, - 0xdc322f, - 0x859900, - 0xb58900, - 0x268bd2, - 0xd33682, - 0x2aa198, - 0xeee8d5, + 0x242424, + 0xf62b5a, + 0x47b413, + 0xe3c401, + 0x24acd4, + 0xf2affd, + 0x13c299, + 0xe6e6e6, // Bright - 0x08404f, - 0xe35f5c, - 0x9fb700, - 0xd9a400, - 0x4ba1de, - 0xdc619d, - 0x32c1b6, + 0x616161, + 0xff4d51, + 0x35d450, + 0xe9e836, + 0x5dc5f8, + 0xfeabf2, + 0x24dfc4, 0xffffff, // 6x6x6 RGB cube @@ -86,6 +86,26 @@ static const uint32_t default_color_table[256] = { 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", @@ -111,6 +131,7 @@ static const char *const binding_action_map[] = { [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", @@ -118,19 +139,38 @@ static const char *const binding_action_map[] = { [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", /* 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", @@ -145,8 +185,16 @@ static const char *const search_binding_action_map[] = { [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", @@ -168,6 +216,7 @@ static_assert(ALEN(url_binding_action_map) == BIND_ACTION_URL_COUNT, struct context { struct config *conf; const char *section; + const char *section_suffix; const char *key; const char *value; @@ -218,8 +267,9 @@ log_contextual(struct context *ctx, enum log_class log_class, char *formatted_msg = xvasprintf(fmt, va); va_end(va); - bool print_dot = ctx->key != NULL; - bool print_colon = ctx->value != NULL; + 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 = ""; @@ -227,10 +277,15 @@ log_contextual(struct context *ctx, enum log_class log_class, 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", - ctx->path, ctx->lineno, ctx->section, print_dot ? "." : "", - ctx->key, print_colon ? ": " : "", ctx->value, formatted_msg); + 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); } @@ -266,10 +321,19 @@ log_contextual_errno(struct context *ctx, const char *file, int lineno, 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", - ctx->path, ctx->lineno, ctx->section, ctx->key, ctx->value, - formatted_msg); + 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); } @@ -328,9 +392,9 @@ open_config(void) /* First, check XDG_CONFIG_HOME (or .config, if unset) */ if (xdg_config_home != NULL && xdg_config_home[0] != '\0') - path = xasprintf("%s/foot/foot.ini", xdg_config_home); + path = xstrjoin(xdg_config_home, "/foot/foot.ini"); else if (home_dir != NULL) - path = xasprintf("%s/.config/foot/foot.ini", home_dir); + path = xstrjoin(home_dir, "/.config/foot/foot.ini"); if (path != NULL) { LOG_DBG("checking for %s", path); @@ -355,7 +419,7 @@ open_config(void) conf_dir = strtok(NULL, ":")) { free(path); - path = xasprintf("%s/foot/foot.ini", conf_dir); + path = xstrjoin(conf_dir, "/foot/foot.ini"); LOG_DBG("checking for %s", path); int fd = open(path, O_RDONLY | O_CLOEXEC); @@ -372,14 +436,6 @@ done: return ret; } -static int -c32cmp_single(const void *_a, const void *_b) -{ - const char32_t *a = _a; - const char32_t *b = _b; - return *a - *b; -} - static bool str_has_prefix(const char *str, const char *prefix) { @@ -420,8 +476,12 @@ str_to_ulong(const char *s, int base, unsigned long *res) errno = 0; char *end = NULL; - *res = strtoul(s, &end, base); - return errno == 0 && *end == '\0'; + unsigned long v = strtoul(s, &end, base); + if (!(errno == 0 && *end == '\0')) + return false; + + *res = v; + return true; } static bool NOINLINE @@ -480,7 +540,7 @@ value_to_dimensions(struct context *ctx, uint32_t *x, uint32_t *y) } static bool NOINLINE -value_to_double(struct context *ctx, float *res) +value_to_float(struct context *ctx, float *res) { const char *s = ctx->value; @@ -490,12 +550,13 @@ value_to_double(struct context *ctx, float *res) errno = 0; char *end = NULL; - *res = strtof(s, &end); + float v = strtof(s, &end); if (!(errno == 0 && *end == '\0')) { LOG_CONTEXTUAL_ERR("invalid decimal value"); return false; } + *res = v; return true; } @@ -513,7 +574,7 @@ value_to_str(struct context *ctx, char **res) * * - key="value" OK * - key=abc "quote" def NOT OK - * - key=’value’ OK + * - key='value' OK * * Finally, we support escaping the quote character, and the * escape character itself: @@ -587,23 +648,34 @@ value_to_enum(struct context *ctx, const char **value_map, int *res) valid_values[idx - 2] = '\0'; LOG_CONTEXTUAL_ERR("not one of %s", valid_values); - *res = -1; return false; } static bool NOINLINE -value_to_color(struct context *ctx, uint32_t *color, bool allow_alpha) +value_to_color(struct context *ctx, uint32_t *result, bool allow_alpha) { - if (!str_to_uint32(ctx->value, 16, color)) { - LOG_CONTEXTUAL_ERR("not a valid color value"); + 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 && (*color & 0xff000000) != 0) { - LOG_CONTEXTUAL_ERR("color value must not have an alpha component"); - return false; + if (allow_alpha && component_count == 3) { + /* If user left out the alpha component, assume non-transparency */ + color |= 0xff000000; } + *result = color; return true; } @@ -624,14 +696,18 @@ value_to_two_colors(struct context *ctx, goto out; } + uint32_t a, b; + ctx->value = first_as_str; - if (!value_to_color(ctx, first, allow_alpha)) + if (!value_to_color(ctx, &a, allow_alpha)) goto out; ctx->value = second_as_str; - if (!value_to_color(ctx, second, allow_alpha)) + if (!value_to_color(ctx, &b, allow_alpha)) goto out; + *first = a; + *second = b; ret = true; out: @@ -659,7 +735,7 @@ value_to_pt_or_px(struct context *ctx, struct pt_or_px *res) res->px = value; } else { float value; - if (!value_to_double(ctx, &value)) + if (!value_to_float(ctx, &value)) return false; res->pt = value; res->px = 0; @@ -766,6 +842,11 @@ value_to_spawn_template(struct context *ctx, 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; @@ -786,7 +867,7 @@ parse_section_main(struct context *ctx) const char *value = ctx->value; bool errors_are_fatal = ctx->errors_are_fatal; - if (strcmp(key, "include") == 0) { + if (streq(key, "include")) { char *_include_path = NULL; const char *include_path = NULL; @@ -798,7 +879,7 @@ parse_section_main(struct context *ctx) return false; } - _include_path = xasprintf("%s/%s", home_dir, value + 2); + _include_path = xstrjoin3(home_dir, "/", value + 2); include_path = _include_path; } else include_path = value; @@ -826,25 +907,28 @@ parse_section_main(struct context *ctx) return ret; } - else if (strcmp(key, "term") == 0) + else if (streq(key, "term")) return value_to_str(ctx, &conf->term); - else if (strcmp(key, "shell") == 0) + else if (streq(key, "shell")) return value_to_str(ctx, &conf->shell); - else if (strcmp(key, "login-shell") == 0) + else if (streq(key, "login-shell")) return value_to_bool(ctx, &conf->login_shell); - else if (strcmp(key, "title") == 0) + else if (streq(key, "title")) return value_to_str(ctx, &conf->title); - else if (strcmp(key, "locked-title") == 0) + else if (streq(key, "locked-title")) return value_to_bool(ctx, &conf->locked_title); - else if (strcmp(key, "app-id") == 0) + else if (streq(key, "app-id")) return value_to_str(ctx, &conf->app_id); - else if (strcmp(key, "initial-window-size-pixels") == 0) { + 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; @@ -852,7 +936,7 @@ parse_section_main(struct context *ctx) return true; } - else if (strcmp(key, "initial-window-size-chars") == 0) { + else if (streq(key, "initial-window-size-chars")) { if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height)) return false; @@ -860,31 +944,67 @@ parse_section_main(struct context *ctx) return true; } - else if (strcmp(key, "pad") == 0) { - unsigned x, y; - char mode[16] = {0}; + 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; - int ret = sscanf(value, "%ux%u %15s", &x, &y, mode); - bool center = strcasecmp(mode, "center") == 0; - bool invalid_mode = !center && mode[0] != '\0'; + 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 != 3) || invalid_mode) { + if ((ret < 2 || ret > 5) || center == CENTER_INVALID) { LOG_CONTEXTUAL_ERR( - "invalid padding (must be in the form PAD_XxPAD_Y [center])"); + "invalid padding (must be in the form RIGHTxTOPxLEFTxBOTTOM or XxY " + "[center|" + "center-when-fullscreen|" + "center-when-maximized-and-fullscreen])"); return false; } - conf->pad_x = x; - conf->pad_y = y; - conf->center = center; + 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 (strcmp(key, "resize-delay-ms") == 0) + else if (streq(key, "resize-delay-ms")) return value_to_uint16(ctx, 10, &conf->resize_delay_ms); - else if (strcmp(key, "bold-text-in-bright") == 0) { - if (strcmp(value, "palette-based") == 0) { + 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 { @@ -895,7 +1015,7 @@ parse_section_main(struct context *ctx) return true; } - else if (strcmp(key, "initial-window-mode") == 0) { + else if (streq(key, "initial-window-mode")) { _Static_assert(sizeof(conf->startup_mode) == sizeof(int), "enum is not 32-bit"); @@ -905,16 +1025,16 @@ parse_section_main(struct context *ctx) (int *)&conf->startup_mode); } - else if (strcmp(key, "font") == 0 || - strcmp(key, "font-bold") == 0 || - strcmp(key, "font-italic") == 0 || - strcmp(key, "font-bold-italic") == 0) + else if (streq(key, "font") || + streq(key, "font-bold") || + streq(key, "font-italic") || + streq(key, "font-bold-italic")) { size_t idx = - strcmp(key, "font") == 0 ? 0 : - strcmp(key, "font-bold") == 0 ? 1 : - strcmp(key, "font-italic") == 0 ? 2 : 3; + 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) @@ -925,7 +1045,7 @@ parse_section_main(struct context *ctx) return true; } - else if (strcmp(key, "font-size-adjustment") == 0) { + else if (streq(key, "font-size-adjustment")) { const size_t len = strlen(ctx->value); if (len >= 1 && ctx->value[len - 1] == '%') { errno = 0; @@ -950,53 +1070,41 @@ parse_section_main(struct context *ctx) } } - else if (strcmp(key, "line-height") == 0) + else if (streq(key, "line-height")) return value_to_pt_or_px(ctx, &conf->line_height); - else if (strcmp(key, "letter-spacing") == 0) + else if (streq(key, "letter-spacing")) return value_to_pt_or_px(ctx, &conf->letter_spacing); - else if (strcmp(key, "horizontal-letter-offset") == 0) + else if (streq(key, "horizontal-letter-offset")) return value_to_pt_or_px(ctx, &conf->horizontal_letter_offset); - else if (strcmp(key, "vertical-letter-offset") == 0) + else if (streq(key, "vertical-letter-offset")) return value_to_pt_or_px(ctx, &conf->vertical_letter_offset); - else if (strcmp(key, "underline-offset") == 0) { + 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 (strcmp(key, "underline-thickness") == 0) + else if (streq(key, "underline-thickness")) return value_to_pt_or_px(ctx, &conf->underline_thickness); - else if (strcmp(key, "dpi-aware") == 0) { - if (strcmp(value, "auto") == 0) - conf->dpi_aware = DPI_AWARE_AUTO; - else { - bool value; - if (!value_to_bool(ctx, &value)) - return false; - conf->dpi_aware = value ? DPI_AWARE_YES : DPI_AWARE_NO; - } - return true; - } + else if (streq(key, "strikeout-thickness")) + return value_to_pt_or_px(ctx, &conf->strikeout_thickness); - else if (strcmp(key, "workers") == 0) + 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 (strcmp(key, "word-delimiters") == 0) + else if (streq(key, "word-delimiters")) return value_to_wchars(ctx, &conf->word_delimiters); - else if (strcmp(key, "notify") == 0) - return value_to_spawn_template(ctx, &conf->notify); - - else if (strcmp(key, "notify-focus-inhibit") == 0) - return value_to_bool(ctx, &conf->notify_focus_inhibit); - - else if (strcmp(key, "selection-target") == 0) { + else if (streq(key, "selection-target")) { _Static_assert(sizeof(conf->selection_target) == sizeof(int), "enum is not 32-bit"); @@ -1006,40 +1114,110 @@ parse_section_main(struct context *ctx) (int *)&conf->selection_target); } - else if (strcmp(key, "box-drawings-uses-font-glyphs") == 0) + else if (streq(key, "box-drawings-uses-font-glyphs")) return value_to_bool(ctx, &conf->box_drawings_uses_font_glyphs); - else if (strcmp(key, "utempter") == 0) { - if (!value_to_str(ctx, &conf->utempter_path)) + else if (streq(key, "utmp-helper")) { + if (!value_to_str(ctx, &conf->utmp_helper_path)) return false; - if (strcmp(conf->utempter_path, "none") == 0) { - free(conf->utempter_path); - conf->utempter_path = NULL; + 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 (strcmp(key, "urgent") == 0) + if (streq(key, "urgent")) return value_to_bool(ctx, &conf->bell.urgent); - else if (strcmp(key, "notify") == 0) + else if (streq(key, "notify")) return value_to_bool(ctx, &conf->bell.notify); - else if (strcmp(key, "command") == 0) + 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 (strcmp(key, "command-focused") == 0) + 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); @@ -1047,6 +1225,30 @@ parse_section_bell(struct context *ctx) } } +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) { @@ -1054,10 +1256,10 @@ parse_section_scrollback(struct context *ctx) const char *key = ctx->key; const char *value = ctx->value; - if (strcmp(key, "lines") == 0) + if (streq(key, "lines")) return value_to_uint32(ctx, 10, &conf->scrollback.lines); - else if (strcmp(key, "indicator-position") == 0) { + else if (streq(key, "indicator-position")) { _Static_assert( sizeof(conf->scrollback.indicator.position) == sizeof(int), "enum is not 32-bit"); @@ -1068,12 +1270,12 @@ parse_section_scrollback(struct context *ctx) (int *)&conf->scrollback.indicator.position); } - else if (strcmp(key, "indicator-format") == 0) { - if (strcmp(value, "percentage") == 0) { + else if (streq(key, "indicator-format")) { + if (streq(value, "percentage")) { conf->scrollback.indicator.format = SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE; return true; - } else if (strcmp(value, "line") == 0) { + } else if (streq(value, "line")) { conf->scrollback.indicator.format = SCROLLBACK_INDICATOR_FORMAT_LINENO; return true; @@ -1081,8 +1283,8 @@ parse_section_scrollback(struct context *ctx) return value_to_wchars(ctx, &conf->scrollback.indicator.text); } - else if (strcmp(key, "multiplier") == 0) - return value_to_double(ctx, &conf->scrollback.multiplier); + else if (streq(key, "multiplier")) + return value_to_float(ctx, &conf->scrollback.multiplier); else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); @@ -1095,15 +1297,14 @@ parse_section_url(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; - const char *value = ctx->value; - if (strcmp(key, "launch") == 0) + if (streq(key, "launch")) return value_to_spawn_template(ctx, &conf->url.launch); - else if (strcmp(key, "label-letters") == 0) + else if (streq(key, "label-letters")) return value_to_wchars(ctx, &conf->url.label_letters); - else if (strcmp(key, "osc8-underline") == 0) { + else if (streq(key, "osc8-underline")) { _Static_assert(sizeof(conf->url.osc8_underline) == sizeof(int), "enum is not 32-bit"); @@ -1113,67 +1314,30 @@ parse_section_url(struct context *ctx) (int *)&conf->url.osc8_underline); } - else if (strcmp(key, "protocols") == 0) { - for (size_t i = 0; i < conf->url.prot_count; i++) - free(conf->url.protocols[i]); - free(conf->url.protocols); + else if (streq(key, "regex")) { + const char *regex = ctx->value; + regex_t preg; - conf->url.max_prot_len = 0; - conf->url.prot_count = 0; - conf->url.protocols = NULL; + int r = regcomp(&preg, regex, REG_EXTENDED); - char *copy = xstrdup(value); - - for (char *prot = strtok(copy, ","); - prot != NULL; - prot = strtok(NULL, ",")) - { - - /* Strip leading whitespace */ - while (isspace(prot[0])) - prot++; - - /* Strip trailing whitespace */ - size_t len = strlen(prot); - while (isspace(prot[len - 1])) - len--; - prot[len] = '\0'; - - size_t chars = mbsntoc32(NULL, prot, len, 0); - if (chars == (size_t)-1) { - ctx->value = prot; - LOG_CONTEXTUAL_ERRNO("invalid protocol"); - return false; - } - - conf->url.prot_count++; - conf->url.protocols = xrealloc( - conf->url.protocols, - conf->url.prot_count * sizeof(conf->url.protocols[0])); - - size_t idx = conf->url.prot_count - 1; - conf->url.protocols[idx] = xmalloc((chars + 1 + 3) * sizeof(char32_t)); - mbsntoc32(conf->url.protocols[idx], prot, len, chars + 1); - c32cpy(&conf->url.protocols[idx][chars], U"://"); - - chars += 3; /* Include the "://" */ - if (chars > conf->url.max_prot_len) - conf->url.max_prot_len = chars; + 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; } - free(copy); - return true; - } - - else if (strcmp(key, "uri-characters") == 0) { - if (!value_to_wchars(ctx, &conf->url.uri_characters)) + if (preg.re_nsub == 0) { + LOG_CONTEXTUAL_ERR("invalid regex: no marked subexpression(s)"); + regfree(&preg); return false; + } - qsort( - conf->url.uri_characters, - c32len(conf->url.uri_characters), - sizeof(conf->url.uri_characters[0]), - &c32cmp_single); + regfree(&conf->url.preg); + free(conf->url.regex); + + conf->url.regex = xstrdup(regex); + conf->url.preg = preg; return true; } @@ -1184,114 +1348,206 @@ parse_section_url(struct context *ctx) } static bool -parse_section_colors(struct context *ctx) +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(conf->colors.table)) - { + 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(conf->colors.table)); + key, ALEN(theme->table)); return false; } - color = &conf->colors.table[index]; + color = &theme->table[index]; } else if (key_len == 8 && str_has_prefix(key, "regular") && last_digit < 8) - color = &conf->colors.table[last_digit]; + color = &theme->table[last_digit]; else if (key_len == 7 && str_has_prefix(key, "bright") && last_digit < 8) - color = &conf->colors.table[8 + last_digit]; + color = &theme->table[8 + last_digit]; else if (key_len == 4 && str_has_prefix(key, "dim") && last_digit < 8) { - if (!value_to_color(ctx, &conf->colors.dim[last_digit], false)) + if (!value_to_color(ctx, &theme->dim[last_digit], false)) return false; - conf->colors.use_custom.dim |= 1 << last_digit; + theme->use_custom.dim |= 1 << last_digit; return true; } - else if (strcmp(key, "foreground") == 0) color = &conf->colors.fg; - else if (strcmp(key, "background") == 0) color = &conf->colors.bg; - else if (strcmp(key, "selection-foreground") == 0) color = &conf->colors.selection_fg; - else if (strcmp(key, "selection-background") == 0) color = &conf->colors.selection_bg; + 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 (strcmp(key, "jump-labels") == 0) { + 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, - &conf->colors.jump_label.fg, - &conf->colors.jump_label.bg, + &theme->jump_label.fg, + &theme->jump_label.bg, false)) { return false; } - conf->colors.use_custom.jump_label = true; + theme->use_custom.jump_label = true; return true; } - else if (strcmp(key, "scrollback-indicator") == 0) { + else if (streq(key, "scrollback-indicator")) { if (!value_to_two_colors( ctx, - &conf->colors.scrollback_indicator.fg, - &conf->colors.scrollback_indicator.bg, + &theme->scrollback_indicator.fg, + &theme->scrollback_indicator.bg, false)) { return false; } - conf->colors.use_custom.scrollback_indicator = true; + theme->use_custom.scrollback_indicator = true; return true; } - else if (strcmp(key, "search-box-no-match") == 0) { + else if (streq(key, "search-box-no-match")) { if (!value_to_two_colors( ctx, - &conf->colors.search_box.no_match.fg, - &conf->colors.search_box.no_match.bg, + &theme->search_box.no_match.fg, + &theme->search_box.no_match.bg, false)) { return false; } - conf->colors.use_custom.search_box_no_match = true; + theme->use_custom.search_box_no_match = true; return true; } - else if (strcmp(key, "search-box-match") == 0) { + else if (streq(key, "search-box-match")) { if (!value_to_two_colors( ctx, - &conf->colors.search_box.match.fg, - &conf->colors.search_box.match.bg, + &theme->search_box.match.fg, + &theme->search_box.match.bg, false)) { return false; } - conf->colors.use_custom.search_box_match = true; + theme->use_custom.search_box_match = true; return true; } - else if (strcmp(key, "urls") == 0) { - if (!value_to_color(ctx, &conf->colors.url, false)) + 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; - conf->colors.use_custom.url = true; + theme->use_custom.url = true; return true; } - else if (strcmp(key, "alpha") == 0) { + else if (streq(key, "alpha")) { float alpha; - if (!value_to_double(ctx, &alpha)) + if (!value_to_float(ctx, &alpha)) return false; if (alpha < 0. || alpha > 1.) { @@ -1299,10 +1555,47 @@ parse_section_colors(struct context *ctx) return false; } - conf->colors.alpha = alpha * 65535.; + 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; @@ -1316,44 +1609,82 @@ parse_section_colors(struct context *ctx) 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 (strcmp(key, "style") == 0) { + 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", NULL}, + (const char *[]){"block", "underline", "beam", "hollow", NULL}, (int *)&conf->cursor.style); } - else if (strcmp(key, "blink") == 0) - return value_to_bool(ctx, &conf->cursor.blink); + else if (streq(key, "unfocused-style")) { + _Static_assert(sizeof(conf->cursor.unfocused_style) == sizeof(int), + "enum is not 32-bit"); - else if (strcmp(key, "color") == 0) { - if (!value_to_two_colors( - ctx, - &conf->cursor.color.text, - &conf->cursor.color.cursor, - false)) - { - return false; - } - - conf->cursor.color.text |= 1u << 31; - conf->cursor.color.cursor |= 1u << 31; - return true; + return value_to_enum( + ctx, + (const char *[]){"unchanged", "hollow", "none", NULL}, + (int *)&conf->cursor.unfocused_style); } - else if (strcmp(key, "beam-thickness") == 0) + 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 (strcmp(key, "underline-thickness") == 0) + else if (streq(key, "underline-thickness")) return value_to_pt_or_px(ctx, &conf->cursor.underline_thickness); else { @@ -1368,10 +1699,10 @@ parse_section_mouse(struct context *ctx) struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "hide-when-typing") == 0) + if (streq(key, "hide-when-typing")) return value_to_bool(ctx, &conf->mouse.hide_when_typing); - else if (strcmp(key, "alternate-scroll-mode") == 0) + else if (streq(key, "alternate-scroll-mode")) return value_to_bool(ctx, &conf->mouse.alternate_scroll_mode); else { @@ -1386,7 +1717,7 @@ parse_section_csd(struct context *ctx) struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "preferred") == 0) { + if (streq(key, "preferred")) { _Static_assert(sizeof(conf->csd.preferred) == sizeof(int), "enum is not 32-bit"); @@ -1396,7 +1727,7 @@ parse_section_csd(struct context *ctx) (int *)&conf->csd.preferred); } - else if (strcmp(key, "font") == 0) { + else if (streq(key, "font")) { struct config_font_list new_list = value_to_fonts(ctx); if (new_list.arr == NULL) return false; @@ -1406,7 +1737,7 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "color") == 0) { + else if (streq(key, "color")) { uint32_t color; if (!value_to_color(ctx, &color, true)) return false; @@ -1416,13 +1747,13 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "size") == 0) + else if (streq(key, "size")) return value_to_uint16(ctx, 10, &conf->csd.title_height); - else if (strcmp(key, "button-width") == 0) + else if (streq(key, "button-width")) return value_to_uint16(ctx, 10, &conf->csd.button_width); - else if (strcmp(key, "button-color") == 0) { + else if (streq(key, "button-color")) { if (!value_to_color(ctx, &conf->csd.color.buttons, true)) return false; @@ -1430,7 +1761,7 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "button-minimize-color") == 0) { + else if (streq(key, "button-minimize-color")) { if (!value_to_color(ctx, &conf->csd.color.minimize, true)) return false; @@ -1438,7 +1769,7 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "button-maximize-color") == 0) { + else if (streq(key, "button-maximize-color")) { if (!value_to_color(ctx, &conf->csd.color.maximize, true)) return false; @@ -1446,7 +1777,7 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "button-close-color") == 0) { + else if (streq(key, "button-close-color")) { if (!value_to_color(ctx, &conf->csd.color.quit, true)) return false; @@ -1454,7 +1785,7 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "border-color") == 0) { + else if (streq(key, "border-color")) { if (!value_to_color(ctx, &conf->csd.color.border, true)) return false; @@ -1462,12 +1793,15 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "border-width") == 0) + else if (streq(key, "border-width")) return value_to_uint16(ctx, 10, &conf->csd.border_width_visible); - else if (strcmp(key, "hide-when-maximized") == 0) + 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; @@ -1484,6 +1818,7 @@ free_binding_aux(struct binding_aux *aux) 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; } } @@ -1491,6 +1826,7 @@ 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 @@ -1506,43 +1842,26 @@ free_key_binding_list(struct config_key_binding_list *bindings) bindings->count = 0; } -static bool NOINLINE -parse_modifiers(struct context *ctx, const char *text, size_t len, - struct config_key_modifiers *modifiers) +static void NOINLINE +parse_modifiers(const char *text, size_t len, config_modifier_list_t *modifiers) { - bool ret = false; - - *modifiers = (struct config_key_modifiers){0}; + tll_free_and_free(*modifiers, free); /* Handle "none" separately because e.g. none+shift is nonsense */ if (strncmp(text, "none", len) == 0) - return true; + return; char *copy = xstrndup(text, len); - for (char *tok_ctx = NULL, *key = strtok_r(copy, "+", &tok_ctx); + for (char *ctx = NULL, *key = strtok_r(copy, "+", &ctx); key != NULL; - key = strtok_r(NULL, "+", &tok_ctx)) + key = strtok_r(NULL, "+", &ctx)) { - if (strcmp(key, XKB_MOD_NAME_SHIFT) == 0) - modifiers->shift = true; - else if (strcmp(key, XKB_MOD_NAME_CTRL) == 0) - modifiers->ctrl = true; - else if (strcmp(key, XKB_MOD_NAME_ALT) == 0) - modifiers->alt = true; - else if (strcmp(key, XKB_MOD_NAME_LOGO) == 0) - modifiers->super = true; - else { - LOG_CONTEXTUAL_ERR("not a valid modifier name: %s", key); - goto out; - } + tll_push_back(*modifiers, xstrdup(key)); } - ret = true; - -out: free(copy); - return ret; + tll_sort(*modifiers, strcmp); } static int NOINLINE @@ -1589,7 +1908,10 @@ binding_aux_equal(const struct binding_aux *a, case BINDING_AUX_TEXT: return a->text.len == b->text.len && - memcmp(a->text.data, b->text.data, a->text.len) == 0; + 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); @@ -1634,6 +1956,7 @@ static const struct { const char *name; int code; } button_map[] = { + /* System defined */ {"BTN_LEFT", BTN_LEFT}, {"BTN_RIGHT", BTN_RIGHT}, {"BTN_MIDDLE", BTN_MIDDLE}, @@ -1642,13 +1965,19 @@ static const struct { {"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 (strcmp(button_map[i].name, name) == 0) + if (streq(button_map[i].name, name)) return button_map[i].code; } return -1; @@ -1678,6 +2007,7 @@ value_to_key_combos(struct context *ctx, int action, /* 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, ' ')) @@ -1693,7 +2023,7 @@ value_to_key_combos(struct context *ctx, int action, for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx); combo != NULL; combo = strtok_r(NULL, " ", &tok_ctx), - idx++) + idx++, used_combos++) { struct config_key_binding *new_combo = &new_combos[idx]; new_combo->action = action; @@ -1704,6 +2034,7 @@ value_to_key_combos(struct context *ctx, int action, 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; @@ -1712,11 +2043,9 @@ value_to_key_combos(struct context *ctx, int action, if (key == NULL) { /* No modifiers */ key = combo; - new_combo->modifiers = (struct config_key_modifiers){0}; } else { *key = '\0'; - if (!parse_modifiers(ctx, combo, key - combo, &new_combo->modifiers)) - goto err; + parse_modifiers(combo, key - combo, &new_combo->modifiers); key++; /* Skip past the '+' */ } @@ -1767,7 +2096,8 @@ value_to_key_combos(struct context *ctx, int action, if (idx == 0) { LOG_CONTEXTUAL_ERR( "empty binding not allowed (set to 'none' to unmap)"); - goto err; + free(copy); + return false; } remove_from_key_bindings_list(bindings, action, aux); @@ -1785,41 +2115,93 @@ value_to_key_combos(struct context *ctx, int action, 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 struct config_key_modifiers *mods1, - const struct config_key_modifiers *mods2) +modifiers_equal(const config_modifier_list_t *mods1, + const config_modifier_list_t *mods2) { - 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; + 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 struct config_key_modifiers *mods1, - const struct config_key_modifiers *mods2) +modifiers_disjoint(const config_modifier_list_t *mods1, + const config_modifier_list_t *mods2) { - 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); + return !modifiers_equal(mods1, mods2); } static char * NOINLINE -modifiers_to_str(const struct config_key_modifiers *mods) +modifiers_to_str(const config_modifier_list_t *mods, bool strip_last_plus) { - char *ret = xasprintf( - "%s%s%s%s", - mods->ctrl ? XKB_MOD_NAME_CTRL "+" : "", - mods->alt ? XKB_MOD_NAME_ALT "+": "", - mods->super ? XKB_MOD_NAME_LOGO "+": "", - mods->shift ? XKB_MOD_NAME_SHIFT "+": ""); + 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; } @@ -1878,28 +2260,97 @@ pipe_argv_from_value(struct context *ctx, struct argv *argv) 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) { - 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 (int action = 0; action < action_count; action++) { if (action_map[action] == NULL) continue; - if (strcmp(ctx->key, action_map[action]) != 0) + 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; @@ -1909,7 +2360,6 @@ parse_key_binding_section(struct context *ctx, } LOG_CONTEXTUAL_ERR("not a valid action: %s", ctx->key); - free_binding_aux(&aux); return false; } @@ -1961,10 +2411,13 @@ UNITTEST 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(bindings.arr[1].modifiers.ctrl); + 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(bindings.arr[2].modifiers.ctrl && bindings.arr[2].modifiers.shift); + 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 @@ -1980,10 +2433,12 @@ UNITTEST 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(bindings.arr[2].modifiers.alt); + 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(bindings.arr[3].modifiers.shift); + xassert(tll_length(bindings.arr[3].modifiers) == 1); + xassert(strcmp(tll_front(bindings.arr[3].modifiers), XKB_MOD_NAME_SHIFT) == 0); /* * REMOVE bar @@ -2050,7 +2505,7 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name, struct config_key_binding *binding1 = &bindings->arr[i]; xassert(binding1->action != BIND_ACTION_NONE); - const struct config_key_modifiers *mods1 = &binding1->modifiers; + const config_modifier_list_t *mods1 = &binding1->modifiers; /* Does our modifiers collide with the selection override mods? */ if (type == MOUSE_BINDING && @@ -2074,7 +2529,7 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name, continue; } - const struct config_key_modifiers *mods2 = &binding2->modifiers; + const config_modifier_list_t *mods2 = &binding2->modifiers; bool mods_equal = modifiers_equal(mods1, mods2); bool sym_equal; @@ -2102,7 +2557,7 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name, } if (collision_type != COLLISION_NONE) { - char *modifier_names = modifiers_to_str(mods1); + char *modifier_names = modifiers_to_str(mods1, false); char sym_name[64]; switch (type){ @@ -2144,7 +2599,7 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name, case COLLISION_OVERRIDE: { char *override_names = modifiers_to_str( - &conf->mouse.selection_override_modifiers); + &conf->mouse.selection_override_modifiers, true); if (override_names[0] != '\0') override_names[strlen(override_names) - 1] = '\0'; @@ -2198,14 +2653,10 @@ parse_section_mouse_bindings(struct context *ctx) const char *key = ctx->key; const char *value = ctx->value; - if (strcmp(key, "selection-override-modifiers") == 0) { - if (!parse_modifiers( - ctx, ctx->value, strlen(value), - &conf->mouse.selection_override_modifiers)) - { - LOG_CONTEXTUAL_ERR("%s: invalid modifiers '%s'", key, ctx->value); - return false; - } + if (streq(key, "selection-override-modifiers")) { + parse_modifiers( + ctx->value, strlen(value), + &conf->mouse.selection_override_modifiers); return true; } @@ -2225,7 +2676,7 @@ parse_section_mouse_bindings(struct context *ctx) if (binding_action_map[action] == NULL) continue; - if (strcmp(key, binding_action_map[action]) != 0) + if (!streq(key, binding_action_map[action])) continue; if (!value_to_key_combos( @@ -2300,7 +2751,7 @@ parse_section_text_bindings(struct context *ctx) struct binding_aux aux = { .type = BINDING_AUX_TEXT, .text = { - .data = data, + .data = data, /* data is now owned by value_to_key_combos() */ .len = data_len, }, }; @@ -2308,7 +2759,8 @@ parse_section_text_bindings(struct context *ctx) if (!value_to_key_combos(ctx, BIND_ACTION_TEXT_BINDING, &aux, &conf->bindings.key, KEY_BINDING)) { - goto err; + /* Do *not* free(data) - it is handled by value_to_key_combos() */ + return false; } return true; @@ -2326,7 +2778,7 @@ parse_section_environment(struct context *ctx) /* Check for pre-existing env variable */ tll_foreach(conf->env_vars, it) { - if (strcmp(it->item.name, key) == 0) + if (streq(it->item.name, key)) return value_to_str(ctx, &it->item.value); } @@ -2348,13 +2800,20 @@ parse_section_tweak(struct context *ctx) struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "scaling-filter") == 0) { + 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, }; @@ -2364,13 +2823,13 @@ parse_section_tweak(struct context *ctx) return value_to_enum(ctx, filters, (int *)&conf->tweak.fcft_filter); } - else if (strcmp(key, "overflowing-glyphs") == 0) + else if (streq(key, "overflowing-glyphs")) return value_to_bool(ctx, &conf->tweak.overflowing_glyphs); - else if (strcmp(key, "damage-whole-window") == 0) + else if (streq(key, "damage-whole-window")) return value_to_bool(ctx, &conf->tweak.damage_whole_window); - else if (strcmp(key, "grapheme-shaping") == 0) { + else if (streq(key, "grapheme-shaping")) { if (!value_to_bool(ctx, &conf->tweak.grapheme_shaping)) return false; @@ -2393,7 +2852,7 @@ parse_section_tweak(struct context *ctx) return true; } - else if (strcmp(key, "grapheme-width-method") == 0) { + else if (streq(key, "grapheme-width-method")) { _Static_assert(sizeof(conf->tweak.grapheme_width_method) == sizeof(int), "enum is not 32-bit"); @@ -2403,7 +2862,7 @@ parse_section_tweak(struct context *ctx) (int *)&conf->tweak.grapheme_width_method); } - else if (strcmp(key, "render-timer") == 0) { + else if (streq(key, "render-timer")) { _Static_assert(sizeof(conf->tweak.render_timer) == sizeof(int), "enum is not 32-bit"); @@ -2413,7 +2872,7 @@ parse_section_tweak(struct context *ctx) (int *)&conf->tweak.render_timer); } - else if (strcmp(key, "delayed-render-lower") == 0) { + else if (streq(key, "delayed-render-lower")) { uint32_t ns; if (!value_to_uint32(ctx, 10, &ns)) return false; @@ -2427,7 +2886,7 @@ parse_section_tweak(struct context *ctx) return true; } - else if (strcmp(key, "delayed-render-upper") == 0) { + else if (streq(key, "delayed-render-upper")) { uint32_t ns; if (!value_to_uint32(ctx, 10, &ns)) return false; @@ -2441,7 +2900,7 @@ parse_section_tweak(struct context *ctx) return true; } - else if (strcmp(key, "max-shm-pool-size-mb") == 0) { + else if (streq(key, "max-shm-pool-size-mb")) { uint32_t mb; if (!value_to_uint32(ctx, 10, &mb)) return false; @@ -2450,18 +2909,47 @@ parse_section_tweak(struct context *ctx) return true; } - else if (strcmp(key, "box-drawing-base-thickness") == 0) - return value_to_double(ctx, &conf->tweak.box_drawing_base_thickness); + else if (streq(key, "box-drawing-base-thickness")) + return value_to_float(ctx, &conf->tweak.box_drawing_base_thickness); - else if (strcmp(key, "box-drawing-solid-shades") == 0) + else if (streq(key, "box-drawing-solid-shades")) return value_to_bool(ctx, &conf->tweak.box_drawing_solid_shades); - else if (strcmp(key, "font-monospace-warn") == 0) + else if (streq(key, "font-monospace-warn")) return value_to_bool(ctx, &conf->tweak.font_monospace_warn); - else if (strcmp(key, "sixel") == 0) + 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; @@ -2469,7 +2957,21 @@ parse_section_tweak(struct context *ctx) } static bool -parse_key_value(char *kv, const char **section, const char **key, const char **value) +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; @@ -2533,10 +3035,14 @@ parse_key_value(char *kv, const char **section, const char **key, const char **v enum section { SECTION_MAIN, + SECTION_SECURITY, SECTION_BELL, + SECTION_DESKTOP_NOTIFICATIONS, SECTION_SCROLLBACK, SECTION_URL, - SECTION_COLORS, + SECTION_REGEX, + SECTION_COLORS_DARK, + SECTION_COLORS_LIGHT, SECTION_CURSOR, SECTION_MOUSE, SECTION_CSD, @@ -2547,6 +3053,12 @@ enum section { SECTION_TEXT_BINDINGS, SECTION_ENVIRONMENT, SECTION_TWEAK, + SECTION_TOUCH, + + /* Deprecated */ + SECTION_COLORS, + SECTION_COLORS2, + SECTION_COUNT, }; @@ -2556,12 +3068,17 @@ 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_COLORS] = {&parse_section_colors, "colors"}, + [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"}, @@ -2572,16 +3089,39 @@ static const struct { [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"}, + + /* 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(const char *str) +str_to_section(char *str, char **suffix) { + *suffix = NULL; + for (enum section section = SECTION_MAIN; section < SECTION_COUNT; ++section) { - if (strcmp(str, section_info[section].name) == 0) + 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; } @@ -2605,10 +3145,12 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar } 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, @@ -2689,7 +3231,8 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar error_or_continue(); } - section = str_to_section(key_value); + 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); @@ -2698,8 +3241,11 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar } 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; @@ -2739,6 +3285,7 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar done: free(section_name); + free(section_suffix); free(_line); return ret; } @@ -2752,124 +3299,143 @@ get_server_socket_path(void) const char *wayland_display = getenv("WAYLAND_DISPLAY"); if (wayland_display == NULL) { - return xasprintf("%s/foot.sock", xdg_runtime); + return xstrjoin(xdg_runtime, "/foot.sock"); } return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display); } -#define m_none {0} -#define m_alt {.alt = true} -#define m_ctrl {.ctrl = true} -#define m_shift {.shift = true} -#define m_ctrl_shift {.ctrl = true, .shift = true} +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) { - static const struct config_key_binding bindings[] = { - {BIND_ACTION_SCROLLBACK_UP_PAGE, m_shift, {{XKB_KEY_Prior}}}, - {BIND_ACTION_SCROLLBACK_DOWN_PAGE, m_shift, {{XKB_KEY_Next}}}, - {BIND_ACTION_CLIPBOARD_COPY, m_ctrl_shift, {{XKB_KEY_c}}}, - {BIND_ACTION_CLIPBOARD_COPY, m_none, {{XKB_KEY_XF86Copy}}}, - {BIND_ACTION_CLIPBOARD_PASTE, m_ctrl_shift, {{XKB_KEY_v}}}, - {BIND_ACTION_CLIPBOARD_PASTE, m_none, {{XKB_KEY_XF86Paste}}}, - {BIND_ACTION_PRIMARY_PASTE, m_shift, {{XKB_KEY_Insert}}}, - {BIND_ACTION_SEARCH_START, m_ctrl_shift, {{XKB_KEY_r}}}, - {BIND_ACTION_FONT_SIZE_UP, m_ctrl, {{XKB_KEY_plus}}}, - {BIND_ACTION_FONT_SIZE_UP, m_ctrl, {{XKB_KEY_equal}}}, - {BIND_ACTION_FONT_SIZE_UP, m_ctrl, {{XKB_KEY_KP_Add}}}, - {BIND_ACTION_FONT_SIZE_DOWN, m_ctrl, {{XKB_KEY_minus}}}, - {BIND_ACTION_FONT_SIZE_DOWN, m_ctrl, {{XKB_KEY_KP_Subtract}}}, - {BIND_ACTION_FONT_SIZE_RESET, m_ctrl, {{XKB_KEY_0}}}, - {BIND_ACTION_FONT_SIZE_RESET, m_ctrl, {{XKB_KEY_KP_0}}}, - {BIND_ACTION_SPAWN_TERMINAL, m_ctrl_shift, {{XKB_KEY_n}}}, - {BIND_ACTION_SHOW_URLS_LAUNCH, m_ctrl_shift, {{XKB_KEY_u}}}, - {BIND_ACTION_PROMPT_PREV, m_ctrl_shift, {{XKB_KEY_z}}}, - {BIND_ACTION_PROMPT_NEXT, m_ctrl_shift, {{XKB_KEY_x}}}, + 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_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 = xmalloc(sizeof(bindings)); - memcpy(conf->bindings.key.arr, bindings, sizeof(bindings)); + conf->bindings.key.arr = xmemdup(bindings, sizeof(bindings)); } static void add_default_search_bindings(struct config *conf) { - static const struct config_key_binding bindings[] = { - {BIND_ACTION_SEARCH_CANCEL, m_ctrl, {{XKB_KEY_c}}}, - {BIND_ACTION_SEARCH_CANCEL, m_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_FIND_PREV, m_ctrl, {{XKB_KEY_r}}}, - {BIND_ACTION_SEARCH_FIND_NEXT, m_ctrl, {{XKB_KEY_s}}}, - {BIND_ACTION_SEARCH_EDIT_LEFT, m_none, {{XKB_KEY_Left}}}, - {BIND_ACTION_SEARCH_EDIT_LEFT, m_ctrl, {{XKB_KEY_b}}}, - {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m_ctrl, {{XKB_KEY_Left}}}, - {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m_alt, {{XKB_KEY_b}}}, - {BIND_ACTION_SEARCH_EDIT_RIGHT, m_none, {{XKB_KEY_Right}}}, - {BIND_ACTION_SEARCH_EDIT_RIGHT, m_ctrl, {{XKB_KEY_f}}}, - {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m_ctrl, {{XKB_KEY_Right}}}, - {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m_alt, {{XKB_KEY_f}}}, - {BIND_ACTION_SEARCH_EDIT_HOME, m_none, {{XKB_KEY_Home}}}, - {BIND_ACTION_SEARCH_EDIT_HOME, m_ctrl, {{XKB_KEY_a}}}, - {BIND_ACTION_SEARCH_EDIT_END, m_none, {{XKB_KEY_End}}}, - {BIND_ACTION_SEARCH_EDIT_END, m_ctrl, {{XKB_KEY_e}}}, - {BIND_ACTION_SEARCH_DELETE_PREV, m_none, {{XKB_KEY_BackSpace}}}, - {BIND_ACTION_SEARCH_DELETE_PREV_WORD, m_ctrl, {{XKB_KEY_BackSpace}}}, - {BIND_ACTION_SEARCH_DELETE_PREV_WORD, m_alt, {{XKB_KEY_BackSpace}}}, - {BIND_ACTION_SEARCH_DELETE_NEXT, m_none, {{XKB_KEY_Delete}}}, - {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m_ctrl, {{XKB_KEY_Delete}}}, - {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m_alt, {{XKB_KEY_d}}}, - {BIND_ACTION_SEARCH_EXTEND_WORD, m_ctrl, {{XKB_KEY_w}}}, - {BIND_ACTION_SEARCH_EXTEND_WORD_WS, m_ctrl_shift, {{XKB_KEY_w}}}, - {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl, {{XKB_KEY_v}}}, - {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl_shift, {{XKB_KEY_v}}}, - {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl, {{XKB_KEY_y}}}, - {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_none, {{XKB_KEY_XF86Paste}}}, - {BIND_ACTION_SEARCH_PRIMARY_PASTE, m_shift, {{XKB_KEY_Insert}}}, + 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}}}, }; conf->bindings.search.count = ALEN(bindings); - conf->bindings.search.arr = xmalloc(sizeof(bindings)); - memcpy(conf->bindings.search.arr, bindings, sizeof(bindings)); + conf->bindings.search.arr = xmemdup(bindings, sizeof(bindings)); } static void add_default_url_bindings(struct config *conf) { - static const struct config_key_binding bindings[] = { - {BIND_ACTION_URL_CANCEL, m_ctrl, {{XKB_KEY_c}}}, - {BIND_ACTION_URL_CANCEL, m_ctrl, {{XKB_KEY_g}}}, - {BIND_ACTION_URL_CANCEL, m_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}}}, + 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 = xmalloc(sizeof(bindings)); - memcpy(conf->bindings.url.arr, bindings, sizeof(bindings)); + conf->bindings.url.arr = xmemdup(bindings, sizeof(bindings)); } static void add_default_mouse_bindings(struct config *conf) { - static const struct config_key_binding bindings[] = { - {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_ctrl, {.m = {BTN_LEFT, 1}}}, - {BIND_ACTION_SELECT_EXTEND, m_none, {.m = {BTN_RIGHT, 1}}}, - {BIND_ACTION_SELECT_EXTEND_CHAR_WISE, m_ctrl, {.m = {BTN_RIGHT, 1}}}, - {BIND_ACTION_SELECT_WORD, m_none, {.m = {BTN_LEFT, 2}}}, - {BIND_ACTION_SELECT_WORD_WS, m_ctrl, {.m = {BTN_LEFT, 2}}}, - {BIND_ACTION_SELECT_ROW, m_none, {.m = {BTN_LEFT, 3}}}, + 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 = xmalloc(sizeof(bindings)); - memcpy(conf->bindings.mouse.arr, bindings, sizeof(bindings)); + conf->bindings.mouse.arr = xmemdup(bindings, sizeof(bindings)); } static void NOINLINE @@ -2889,28 +3455,38 @@ config_font_list_clone(struct config_font_list *dst, bool config_load(struct config *conf, const char *conf_path, user_notifications_t *initial_user_notifications, - config_override_t *overrides, bool errors_are_fatal) + config_override_t *overrides, bool errors_are_fatal, + bool as_server) { - bool ret = false; + 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 = 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_x = 0, - .pad_y = 0, + .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}}, @@ -2922,10 +3498,18 @@ config_load(struct config *conf, const char *conf_path, .use_custom_underline_offset = false, .box_drawings_uses_font_glyphs = false, .underline_thickness = {.pt = 0., .px = -1}, - .dpi_aware = DPI_AWARE_AUTO, /* DPI-aware when scaling-factor == 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}, }, @@ -2933,9 +3517,9 @@ config_load(struct config *conf, const char *conf_path, }, .url = { .label_letters = xc32dup(U"sadfjklewcmpgh"), - .uri_characters = xc32dup(U"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.,~:;/?#@!$&%*+=\"'()[]"), .osc8_underline = OSC8_UNDERLINE_URL_MODE, }, + .custom_regexes = tll_init(), .can_shape_grapheme = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING, .scrollback = { .lines = 1000, @@ -2946,26 +3530,34 @@ config_load(struct config *conf, const char *conf_path, }, .multiplier = 3., }, - .colors = { + .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 = { - .selection = false, .jump_label = false, .scrollback_indicator = false, .url = false, }, + .blur = false, }, - + .initial_color_theme = COLOR_THEME_DARK, .cursor = { .style = CURSOR_BLOCK, - .blink = false, - .color = { - .text = 0, - .cursor = 0, + .unfocused_style = CURSOR_UNFOCUSED_HOLLOW, + .blink = { + .enabled = false, + .rate_ms = 500, }, .beam_thickness = {.pt = 1.5}, .underline_thickness = {.pt = 0., .px = -1}, @@ -2973,17 +3565,13 @@ config_load(struct config *conf, const char *conf_path, .mouse = { .hide_when_typing = false, .alternate_scroll_mode = true, - .selection_override_modifiers = { - .shift = true, - .alt = false, - .ctrl = false, - .super = false, - }, + .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, @@ -2995,10 +3583,18 @@ config_load(struct config *conf, const char *conf_path, .presentation_timings = false, .selection_target = SELECTION_TARGET_PRIMARY, .hold_at_exit = false, - .notify = { - .argv = {.args = NULL}, + .desktop_notifications = { + .command = { + .argv = {.args = NULL}, + }, + .command_action_arg = { + .argv = {.args = NULL}, + }, + .close = { + .argv = {.args = NULL}, + }, + .inhibit_when_focused = true, }, - .notify_focus_inhibit = true, .tweak = { .fcft_filter = FCFT_SCALING_FILTER_LANCZOS3, @@ -3016,50 +3612,86 @@ config_load(struct config *conf, const char *conf_path, .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, }, .env_vars = tll_init(), - .utempter_path = (strlen(FOOT_DEFAULT_UTEMPTER_PATH) > 0 - ? xstrdup(FOOT_DEFAULT_UTEMPTER_PATH) - : NULL), +#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(), }; - memcpy(conf->colors.table, default_color_table, sizeof(default_color_table)); + 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; - tokenize_cmdline("notify-send -a ${app-id} -i ${app-id} ${title} ${body}", - &conf->notify.argv.args); + 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); - static const char32_t *url_protocols[] = { - U"http://", - U"https://", - U"ftp://", - U"ftps://", - U"file://", - U"gemini://", - U"gopher://", - U"irc://", - U"ircs://", - }; - conf->url.protocols = xmalloc( - ALEN(url_protocols) * sizeof(conf->url.protocols[0])); - conf->url.prot_count = ALEN(url_protocols); - conf->url.max_prot_len = 0; + { + 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:/?#@!$&*+,;=.~_%^\\-]*'" + ")" + ")"; - for (size_t i = 0; i < ALEN(url_protocols); i++) { - size_t len = c32len(url_protocols[i]); - if (len > conf->url.max_prot_len) - conf->url.max_prot_len = len; - conf->url.protocols[i] = xc32dup(url_protocols[i]); + 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); } - qsort( - conf->url.uri_characters, - c32len(conf->url.uri_characters), - sizeof(conf->url.uri_characters[0]), - &c32cmp_single); - tll_foreach(*initial_user_notifications, it) { tll_push_back(conf->notifications, it->item); tll_remove(*initial_user_notifications, it); @@ -3076,45 +3708,37 @@ config_load(struct config *conf, const char *conf_path, if (fd < 0) { LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_path); ret = !errors_are_fatal; - goto out; + } else { + conf_file.path = xstrdup(conf_path); + conf_file.fd = fd; } - - 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; - goto out; } } - xassert(conf_file.path != NULL); - xassert(conf_file.fd >= 0); - LOG_INFO("loading configuration from %s", conf_file.path); + 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; - goto out; + 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 (!parse_config_file(f, conf, conf_file.path, errors_are_fatal) || - !config_override_apply(conf, overrides, errors_are_fatal)) - { + if (!config_override_apply(conf, overrides, errors_are_fatal)) ret = !errors_are_fatal; - } else - ret = true; - fclose(f); - - conf->colors.use_custom.selection = - conf->colors.selection_fg >> 24 == 0 && - conf->colors.selection_bg >> 24 == 0; - -out: if (ret && conf->fonts[0].count == 0) { struct config_font font; if (!config_font_parse("monospace", &font)) { @@ -3122,7 +3746,7 @@ out: ret = false; } else { conf->fonts[0].count = 1; - conf->fonts[0].arr = malloc(sizeof(font)); + conf->fonts[0].arr = xmalloc(sizeof(font)); conf->fonts[0].arr[0] = font; } } @@ -3150,6 +3774,8 @@ 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", @@ -3161,8 +3787,7 @@ config_override_apply(struct config *conf, config_override_t *overrides, tll_foreach(*overrides, it) { context.lineno++; - if (!parse_key_value( - it->item, &context.section, &context.key, &context.value)) + 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"); @@ -3171,20 +3796,26 @@ config_override_apply(struct config *conf, config_override_t *overrides, continue; } - if (context.section[0] == '\0') { + if (section_name[0] == '\0') { LOG_CONTEXTUAL_ERR("empty section name"); if (errors_are_fatal) return false; continue; } - enum section section = str_to_section(context.section); + 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", context.section); + 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); @@ -3220,6 +3851,7 @@ key_binding_list_clone(struct config_key_binding_list *dst, 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])); @@ -3229,6 +3861,9 @@ key_binding_list_clone(struct config_key_binding_list *dst, 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: @@ -3253,8 +3888,7 @@ key_binding_list_clone(struct config_key_binding_list *dst, if (old->aux.master_copy) { const size_t len = old->aux.text.len; new->aux.text.len = len; - new->aux.text.data = xmalloc(len); - memcpy(new->aux.text.data, old->aux.text.data, len); + new->aux.text.data = xmemdup(old->aux.text.data, len); last_master_text_len = len; last_master_text_data = new->aux.text.data; @@ -3265,6 +3899,16 @@ key_binding_list_clone(struct config_key_binding_list *dst, } 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; } } } @@ -3275,33 +3919,58 @@ 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->notify, &old->notify); + 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); - conf->url.uri_characters = xc32dup(old->url.uri_characters); spawn_template_clone(&conf->url.launch, &old->url.launch); - conf->url.protocols = xmalloc( - old->url.prot_count * sizeof(conf->url.protocols[0])); - for (size_t i = 0; i < old->url.prot_count; i++) - conf->url.protocols[i] = xc32dup(old->url.protocols[i]); + 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), @@ -3310,8 +3979,8 @@ config_clone(const struct config *old) tll_push_back(conf->env_vars, copy); } - conf->utempter_path = - old->utempter_path != NULL ? xstrdup(old->utempter_path) : NULL; + 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; @@ -3329,16 +3998,20 @@ UNITTEST user_notifications_t nots = tll_init(); config_override_t overrides = tll_init(); - bool ret = config_load(&original, "/dev/null", ¬s, &overrides, false); + 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); + //struct config *clone = config_clone(&original); + //xassert(clone != NULL); + //xassert(clone != &original); config_free(&original); - config_free(clone); - free(clone); + //config_free(clone); + //free(clone); + + fcft_fini(); tll_free(overrides); tll_free(nots); @@ -3347,14 +4020,18 @@ UNITTEST 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->notify); + 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); @@ -3363,15 +4040,23 @@ config_free(struct config *conf) free(conf->url.label_letters); spawn_template_free(&conf->url.launch); - for (size_t i = 0; i < conf->url.prot_count; i++) - free(conf->url.protocols[i]); - free(conf->url.protocols); - free(conf->url.uri_characters); + 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); @@ -3379,7 +4064,7 @@ config_free(struct config *conf) tll_remove(conf->env_vars, it); } - free(conf->utempter_path); + free(conf->utmp_helper_path); user_notifications_free(&conf->notifications); } @@ -3392,7 +4077,7 @@ config_font_parse(const char *pattern, struct config_font *font) /* * First look for user specified {pixel}size option - * e.g. “font-name:size=12” + * e.g. "font-name:size=12" */ double pt_size = -1.0; @@ -3403,14 +4088,15 @@ config_font_parse(const char *pattern, struct config_font *font) if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) { /* - * Apply fontconfig config. Can’t do that until we’ve first + * 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 + * 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(NULL, pat_copy, FcMatchPattern)) + !FcConfigSubstitute(fc_conf, pat_copy, FcMatchPattern)) { LOG_WARN("%s: failed to do config substitution", pattern); } else { @@ -3419,6 +4105,7 @@ config_font_parse(const char *pattern, struct config_font *font) } FcPatternDestroy(pat_copy); + FcConfigDestroy(fc_conf); if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) pt_size = 8.0; @@ -3432,6 +4119,11 @@ config_font_parse(const char *pattern, struct config_font *font) 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, @@ -3502,6 +4194,7 @@ check_if_font_is_monospaced(const char *pattern, return is_monospaced; } +#if 0 xkb_mod_mask_t conf_modifiers_to_mask(const struct seat *seat, const struct config_key_modifiers *modifiers) @@ -3517,3 +4210,4 @@ conf_modifiers_to_mask(const struct seat *seat, mods |= modifiers->super << seat->kbd.mod_super; return mods; } +#endif diff --git a/config.h b/config.h index 31dddc64..a3522f44 100644 --- a/config.h +++ b/config.h @@ -1,7 +1,8 @@ #pragma once -#include +#include #include +#include #include #include @@ -27,7 +28,12 @@ struct font_size_adjustment { float percent; }; -enum cursor_style { CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BEAM }; +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}; @@ -38,12 +44,14 @@ struct config_font { }; DEFINE_LIST(struct config_font); +#if 0 struct config_key_modifiers { bool shift; bool alt; bool ctrl; bool super; }; +#endif struct argv { char **args; @@ -53,6 +61,7 @@ enum binding_aux_type { BINDING_AUX_NONE, BINDING_AUX_PIPE, BINDING_AUX_TEXT, + BINDING_AUX_REGEX, }; struct binding_aux { @@ -66,6 +75,8 @@ struct binding_aux { uint8_t *data; size_t len; } text; + + char *regex_name; }; }; @@ -74,9 +85,12 @@ enum key_binding_type { MOUSE_BINDING, }; +typedef tll(char *) config_modifier_list_t; + struct config_key_binding { - int action; /* One of the varios bind_action_* enums from wayland.h */ - struct config_key_modifiers modifiers; + 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 { @@ -110,11 +124,107 @@ struct env_var { }; 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, +}; + 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; @@ -125,19 +235,32 @@ struct config { uint32_t height; } size; - unsigned pad_x; - unsigned pad_y; - bool center; + 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; - enum {DPI_AWARE_AUTO, DPI_AWARE_YES, DPI_AWARE_NO} dpi_aware; + bool dpi_aware; + bool gamma_correct; + bool uppercase_regex_insert; struct config_font_list fonts[4]; struct font_size_adjustment font_size_adjustment; @@ -153,12 +276,25 @@ struct config { 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; @@ -192,63 +328,23 @@ struct config { OSC8_UNDERLINE_ALWAYS, } osc8_underline; - char32_t **protocols; - char32_t *uri_characters; - size_t prot_count; - size_t max_prot_len; + char *regex; + regex_t preg; } url; - struct { - uint32_t fg; - uint32_t bg; - uint32_t table[256]; - uint16_t alpha; - uint32_t selection_fg; - uint32_t selection_bg; - uint32_t url; + tll(struct custom_regex) custom_regexes; - uint32_t dim[8]; - - 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 selection: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; - } colors; + struct color_theme colors_dark; + struct color_theme colors_light; + enum which_color_theme initial_color_theme; struct { enum cursor_style style; - bool blink; + enum cursor_unfocused_style unfocused_style; struct { - uint32_t text; - uint32_t cursor; - } color; + bool enabled; + uint32_t rate_ms; + } blink; struct pt_or_px beam_thickness; struct pt_or_px underline_thickness; } cursor; @@ -256,7 +352,8 @@ struct config { struct { bool hide_when_typing; bool alternate_scroll_mode; - struct config_key_modifiers selection_override_modifiers; + //struct config_key_modifiers selection_override_modifiers; + config_modifier_list_t selection_override_modifiers; } mouse; struct { @@ -285,6 +382,7 @@ struct config { uint16_t button_width; bool hide_when_maximized; + bool double_click_to_maximize; struct { bool title_set:1; @@ -297,7 +395,7 @@ struct config { uint32_t buttons; uint32_t minimize; uint32_t maximize; - uint32_t quit; /* ‘close’ collides with #define in epoll-shim */ + uint32_t quit; /* 'close' collides with #define in epoll-shim */ uint32_t border; } color; @@ -315,12 +413,16 @@ struct config { SELECTION_TARGET_BOTH } selection_target; - struct config_spawn_template notify; - bool notify_focus_inhibit; + 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 *utempter_path; + char *utmp_helper_path; struct { enum fcft_scaling_filter fcft_filter; @@ -345,8 +447,15 @@ struct config { 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; + user_notifications_t notifications; }; @@ -355,17 +464,19 @@ bool config_override_apply(struct config *conf, config_override_t *overrides, bool config_load( struct config *conf, const char *path, user_notifications_t *initial_user_notifications, - config_override_t *overrides, bool errors_are_fatal); + 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 index d22c1da5..87af215e 100644 --- a/csi.c +++ b/csi.c @@ -3,7 +3,6 @@ #include #include #include -#include #if defined(_DEBUG) #include @@ -32,7 +31,12 @@ static void sgr_reset(struct terminal *term) { - memset(&term->vt.attrs, 0, sizeof(term->vt.attrs)); + 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 * @@ -88,17 +92,58 @@ csi_sgr(struct terminal *term) 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; 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: break; /* double-underline, not implemented */ + 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; 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; @@ -119,7 +164,8 @@ csi_sgr(struct terminal *term) break; case 38: - case 48: { + case 48: + case 58: { uint32_t color; enum color_source src; @@ -194,7 +240,12 @@ csi_sgr(struct terminal *term) break; } - if (param == 38) { + 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 { @@ -226,6 +277,13 @@ csi_sgr(struct terminal *term) 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: @@ -324,6 +382,11 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) 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; @@ -359,6 +422,8 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) case 1004: term->focus_events = enable; + if (enable) + term_to_slave(term, term->kbd_focus ? "\033[I" : "\033[O", 3); break; case 1005: @@ -473,6 +538,9 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) 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; @@ -491,6 +559,23 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) 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; @@ -498,8 +583,10 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) case 737769: if (enable) term_ime_enable(term); - else + else { term_ime_disable(term); + term->ime_reenable_after_url_mode = false; + } break; default: @@ -551,6 +638,7 @@ decrqm(const struct terminal *term, unsigned param) 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; @@ -572,6 +660,11 @@ decrqm(const struct terminal *term, unsigned param) 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)); } @@ -593,6 +686,7 @@ xtsave(struct terminal *term, unsigned param) 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; @@ -614,6 +708,9 @@ xtsave(struct terminal *term, unsigned param) 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; } @@ -634,6 +731,7 @@ xtrestore(struct terminal *term, unsigned param) 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; @@ -655,6 +753,9 @@ xtrestore(struct terminal *term, unsigned param) 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; @@ -664,6 +765,24 @@ xtrestore(struct terminal *term, unsigned param) 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) { @@ -682,10 +801,20 @@ csi_dispatch(struct terminal *term, uint8_t final) int count = vt_param_get(term, 0, 1); LOG_DBG("REP: '%lc' %d times", (wint_t)term->vt.last_printed, count); - const int width = c32width(term->vt.last_printed); + 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); + term_print(term, term->vt.last_printed, width, false); } } break; @@ -723,6 +852,7 @@ csi_dispatch(struct terminal *term, uint8_t final) * - 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 @@ -733,13 +863,15 @@ csi_dispatch(struct terminal *term, uint8_t final) * * Note: tertiary DA responds with "FOOT". */ - if (term->conf->tweak.sixel) { - static const char reply[] = "\033[?62;4;22c"; - term_to_slave(term, reply, sizeof(reply) - 1); - } else { - static const char reply[] = "\033[?62;22c"; - term_to_slave(term, reply, sizeof(reply) - 1); - } + 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; } @@ -815,7 +947,7 @@ csi_dispatch(struct terminal *term, uint8_t final) case 'G': { /* Cursor horizontal absolute */ int col = min(vt_param_get(term, 0, 1), term->cols) - 1; - term_cursor_to(term, term->grid->cursor.point.row, col); + term_cursor_col(term, col); break; } @@ -1072,44 +1204,30 @@ csi_dispatch(struct terminal *term, uint8_t final) break; case 'h': - /* Set mode */ - switch (vt_param_get(term, 0, 0)) { - case 2: /* Keyboard Action Mode - AM */ - LOG_WARN("unimplemented: keyboard action mode (AM)"); - break; - - case 4: /* Insert Mode - IRM */ - term->insert_mode = true; + 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; + } - case 12: /* Send/receive Mode - SRM */ - LOG_WARN("unimplemented: send/receive mode (SRM)"); - break; - - case 20: /* Automatic Newline Mode - LNM */ - /* TODO: would be easy to implemented; when active - * term_linefeed() would _also_ do a - * term_carriage_return() */ - LOG_WARN("unimplemented: automatic newline mode (LNM)"); - break; - } - break; - - case 'l': - /* Reset mode */ - switch (vt_param_get(term, 0, 0)) { - case 4: /* Insert Mode - IRM */ - term->insert_mode = false; - term_update_ascii_printer(term); - break; - - case 2: /* Keyboard Action Mode - AM */ - case 12: /* Send/receive Mode - SRM */ - case 20: /* Automatic Newline Mode - LNM */ - 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); @@ -1156,7 +1274,6 @@ csi_dispatch(struct terminal *term, uint8_t final) 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 21: LOG_WARN("unimplemented: report window title"); break; case 24: LOG_WARN("unimplemented: resize window (DECSLPP)"); break; case 11: /* report if window is iconified */ @@ -1206,8 +1323,8 @@ csi_dispatch(struct terminal *term, uint8_t final) if (width >= 0 && height >= 0) { char reply[64]; - size_t n = xsnprintf(reply, sizeof(reply), "\033[4;%d;%dt", - height / term->scale, width / term->scale); + size_t n = xsnprintf( + reply, sizeof(reply), "\033[4;%d;%dt", height, width); term_to_slave(term, reply, n); } break; @@ -1217,8 +1334,8 @@ csi_dispatch(struct terminal *term, uint8_t final) 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_scaled.height, - it->item->dim.px_scaled.width); + it->item->dim.px_real.height, + it->item->dim.px_real.width); term_to_slave(term, reply, n); break; } @@ -1229,9 +1346,9 @@ csi_dispatch(struct terminal *term, uint8_t final) 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->scale, - term->cell_width / term->scale); + size_t n = xsnprintf( + reply, sizeof(reply), "\033[6;%d;%dt", + term->cell_height, term->cell_width); term_to_slave(term, reply, n); break; } @@ -1247,9 +1364,10 @@ csi_dispatch(struct terminal *term, uint8_t final) 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 / term->scale, - it->item->dim.px_real.width / term->cell_width / term->scale); + 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; } @@ -1259,6 +1377,18 @@ csi_dispatch(struct terminal *term, uint8_t final) 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); @@ -1398,6 +1528,82 @@ csi_dispatch(struct terminal *term, uint8_t final) 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]; @@ -1438,10 +1644,10 @@ csi_dispatch(struct terminal *term, uint8_t final) * 64 - vt520 * 65 - vt525 * - * Param 2 - firmware version - * xterm uses its version number. We use an xterm - * version number too, since e.g. Emacs uses this to - * determine level of support. + * 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. @@ -1489,7 +1695,7 @@ csi_dispatch(struct terminal *term, uint8_t final) break; default: - LOG_WARN("invalid resource %d in %s", + LOG_WARN("XTMODKEYS: invalid resource '%d' in '%s'", resource, csi_as_string(term, final, -1)); break; } @@ -1505,8 +1711,8 @@ csi_dispatch(struct terminal *term, uint8_t final) break; case 4: /* modifyOtherKeys */ - /* We don’t support fully disabling modifyOtherKeys, - * but simply revert back to mode ‘1’ */ + /* 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; @@ -1584,7 +1790,7 @@ csi_dispatch(struct terminal *term, uint8_t final) break; } } - break; /* private[0] == ‘<’ */ + break; /* private[0] == '<' */ } case ' ': { @@ -1594,13 +1800,14 @@ csi_dispatch(struct terminal *term, uint8_t final) 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; + 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 = CURSOR_BLOCK; + term->cursor_style = term->conf->cursor.style == CURSOR_HOLLOW + ? CURSOR_HOLLOW : CURSOR_BLOCK; break; case 3: /* blinking underline */ @@ -1704,6 +1911,293 @@ csi_dispatch(struct terminal *term, uint8_t final) 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': { @@ -1731,7 +2225,7 @@ csi_dispatch(struct terminal *term, uint8_t final) break; } - break; /* private[0] == ‘?’ && private[1] == ‘$’ */ + break; /* private[0] == '?' && private[1] == '$' */ default: UNHANDLED(); diff --git a/cursor-shape.c b/cursor-shape.c new file mode 100644 index 00000000..c195a554 --- /dev/null +++ b/cursor-shape.c @@ -0,0 +1,128 @@ +#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_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_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 00000000..13690588 --- /dev/null +++ b/cursor-shape.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +enum cursor_shape { + CURSOR_SHAPE_NONE, + CURSOR_SHAPE_CUSTOM, + CURSOR_SHAPE_HIDDEN, + + CURSOR_SHAPE_LEFT_PTR, + 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 index fb4a14b6..376c73bd 100644 --- a/dcs.c +++ b/dcs.c @@ -9,6 +9,7 @@ #include "util.h" #include "vt.h" #include "xmalloc.h" +#include "xsnprintf.h" static bool ensure_size(struct terminal *term, size_t required_size) @@ -111,14 +112,11 @@ 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) - goto err; + if (name == NULL) { + LOG_WARN("XTGETTCAP: invalid hex encoding, ignoring capability"); + return; + } -#if 0 - const struct foot_terminfo_entry *entry = - bsearch(name, terminfo_capabilities, ALEN(terminfo_capabilities), - sizeof(*entry), &terminfo_entry_compar); -#endif const char *value; bool valid_capability = lookup_capability(name, &value); xassert(!valid_capability || value != NULL); @@ -141,12 +139,12 @@ xtgettcap_reply(struct terminal *term, const char *hex_cap_name, size_t len) /* * Reply format: * \EP 1 + r cap=value \E\\ - * Where ‘cap’ and ‘value are hex encoded ascii strings + * Where 'cap' and 'value are hex encoded ascii strings */ char *reply = xmalloc( 5 + /* DCS 1 + r (\EP1+r) */ len + /* capability name, hex encoded */ - 1 + /* ‘=’ */ + 1 + /* '=' */ strlen(value) * 2 + /* capability value, hex encoded */ 2 + /* ST (\E\\) */ 1); @@ -203,6 +201,12 @@ xtgettcap_unhook(struct terminal *term) 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; @@ -242,7 +246,7 @@ decrqss_put(struct terminal *term, uint8_t c) return; struct vt *vt = &term->vt; - if (vt->dcs.idx > 2) + if (vt->dcs.idx >= 2) return; vt->dcs.data[vt->dcs.idx++] = c; } @@ -256,8 +260,8 @@ decrqss_unhook(struct terminal *term) /* * 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”. + * 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): @@ -267,9 +271,9 @@ decrqss_unhook(struct terminal *term) if (n == 1 && query[0] == 'r') { /* DECSTBM - Set Top and Bottom Margins */ char reply[64]; - int len = snprintf(reply, sizeof(reply), "\033P1$r%d;%dr\033\\", - term->scroll_region.start + 1, - term->scroll_region.end); + 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); } @@ -293,8 +297,15 @@ decrqss_unhook(struct terminal *term) append_sgr_attr("2"); if (a->italic) append_sgr_attr("3"); - if (a->underline) - append_sgr_attr("4"); + 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) @@ -310,7 +321,7 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE16: { char value[4]; - int val_len = snprintf( + 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); @@ -319,7 +330,7 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE256: { char value[16]; - int val_len = snprintf(value, sizeof(value), "38:5:%u", a->fg); + size_t val_len = xsnprintf(value, sizeof(value), "38:5:%u", a->fg); append_sgr_attr_n(&reply, &len, value, val_len); break; } @@ -330,7 +341,7 @@ decrqss_unhook(struct terminal *term) uint8_t b = a->fg >> 0; char value[32]; - int val_len = snprintf( + 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; @@ -343,7 +354,7 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE16: { char value[4]; - int val_len = snprintf( + 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); @@ -352,7 +363,7 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE256: { char value[16]; - int val_len = snprintf(value, sizeof(value), "48:5:%u", a->bg); + size_t val_len = xsnprintf(value, sizeof(value), "48:5:%u", a->bg); append_sgr_attr_n(&reply, &len, value, val_len); break; } @@ -363,13 +374,39 @@ decrqss_unhook(struct terminal *term) uint8_t b = a->bg >> 0; char value[32]; - int val_len = snprintf( + 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'; @@ -385,6 +422,7 @@ decrqss_unhook(struct terminal *term) 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; @@ -395,7 +433,7 @@ decrqss_unhook(struct terminal *term) mode--; char reply[16]; - int len = snprintf(reply, sizeof(reply), "\033P1$r%d q\033\\", mode); + size_t len = xsnprintf(reply, sizeof(reply), "\033P1$r%d q\033\\", mode); term_to_slave(term, reply, len); } @@ -424,11 +462,10 @@ dcs_hook(struct terminal *term, uint8_t final) break; } int p1 = vt_param_get(term, 0, 0); - int p2 = vt_param_get(term, 1,0); + int p2 = vt_param_get(term, 1, 0); int p3 = vt_param_get(term, 2, 0); - sixel_init(term, p1, p2, p3); - term->vt.dcs.put_handler = &sixel_put; + term->vt.dcs.put_handler = sixel_init(term, p1, p2, p3); term->vt.dcs.unhook_handler = &sixel_unhook; break; } diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index ec970127..40906ebf 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -150,7 +150,7 @@ m*. | 3 : Italic | 4 -: Underline +: Underline, including styled underlines | 5 : Blink | 7 @@ -159,6 +159,8 @@ m*. : Conceal; text is not visible, but is copiable | 9 : Crossed-out/strike +| 21 +: Double underline | 22 : Disable *bold* and *dim* | 23 @@ -176,15 +178,19 @@ m*. | 30-37 : Select foreground color (using *regularN* in *foot.ini*(5)) | 38 -: See "indexed and RGB colors" below +: 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 -: See "indexed and RGB colors" below +: 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 @@ -328,6 +334,15 @@ that corresponds to one of the following modes: | 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 @@ -376,15 +391,24 @@ manipulation sequences. The generic format is: | 19 : - : Report screen size, in characters. +| 20 +: - +: Report icon label. | 22 : - -: Push window title+icon. Foot does not support pushing the icon. +: Push window title+icon. +| 22 +: 1 +: Push window icon. | 22 : 2 : Push window title. | 23 : - -: Pop window title+icon. Foot does not support popping the icon. +: Pop window title+icon. +| 23 +: 1 +: Pop window icon. | 23 : 2 : Pop window title. @@ -440,13 +464,13 @@ manipulation sequences. The generic format is: | \\E[ _Pm_ h : SM : VT100 -: Set mode. _Pm_=4 -> enable IRM (insert mode). All other values of - _Pm_ are unsupported. +: 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 (insert mode). All other values of - _Pm_ are unsupported. +: Reset mode. _Pm_=4 -> disable IRM (Insertion Replacement Mode). All + other values of _Pm_ are unsupported. | \\E[ _Ps_ n : DSR : VT100 @@ -484,7 +508,39 @@ manipulation sequences. The generic format is: | \\E[ ? _Ps_ $ p : DECRQM : VT320 -: Request DEC private mode. +: 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 @@ -565,6 +621,10 @@ manipulation sequences. The generic format is: : 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 @@ -587,6 +647,26 @@ manipulation sequences. The generic format is: : : 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 @@ -598,8 +678,10 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. :< *Description* | \\E] 0 ; _Pt_ \\E\\ : xterm -: Set window icon and title to _Pt_ (foot does not support setting the - icon) +: 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_ @@ -657,6 +739,13 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. : 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 @@ -680,12 +769,24 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. | \\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 *notify* in *foot.ini*(5). +: Desktop notification, uses *desktop-notifications.command* in + *foot.ini*(5). # DCS @@ -696,7 +797,7 @@ and are terminated by *\\E\\* (ST). :< *Description* | \\EP q \\E\\ : Emit a sixel image at the current cursor position -| \\P $ q \\E\\ +| \\EP $ q \\E\\ : Request selection or setting (DECRQSS). Implemented queries: DECSTBM, SGR and DECSCUSR. | \\EP = _C_ s \\E\\ diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 51c53130..a190db9b 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -20,13 +20,16 @@ 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 sh -c "echo hello world && sleep 5"* + *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*). @@ -65,7 +68,12 @@ the foot command line *-a*,*--app-id*=_ID_ Value to set the *app-id* property on the Wayland window - to. Default: _foot_. + 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* @@ -78,6 +86,13 @@ the foot command line *-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_. @@ -121,14 +136,14 @@ the foot command line 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. They need to be instantiated with the value of - $WAYLAND_DISPLAY (multiples instances can co-exists). + 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, if you need specific - environment variables, you'll need to import them using *systemctl --user - import-environment* or use a drop-in for the foot-server service. + 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. @@ -183,16 +198,16 @@ default) available; see *foot.ini*(5). Paste from _clipboard_ *shift*+*insert* - Paste from the _primary selection_. + Paste from the _primary selection_ *ctrl*+*shift*+*r* Start a scrollback search *ctrl*+*+*, *ctrl*+*=* - Increase font size by 0.5pt + Increase font size *ctrl*+*-* - Decrease font size by 0.5pt + Decrease font size *ctrl*+*0* Reset font size @@ -202,9 +217,12 @@ default) available; see *foot.ini*(5). _OSC 7_ escape sequence, the new terminal will start in the current working directory. -*ctrl*+*shift*+*u* +*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. @@ -214,6 +232,8 @@ default) available; see *foot.ini*(5). ## 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. @@ -222,7 +242,13 @@ default) available; see *foot.ini*(5). Search _forward_ for the next match. If the search string is empty, the last searched-for string is used. -*ctrl*+*w* +*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. @@ -231,6 +257,15 @@ default) available; see *foot.ini*(5). 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. @@ -245,6 +280,46 @@ default) available; see *foot.ini*(5). 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* @@ -270,6 +345,10 @@ default) available; see *foot.ini*(5). 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* @@ -280,9 +359,28 @@ default) available; see *foot.ini*(5). 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 @@ -298,10 +396,10 @@ 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*+*u* enters _“URL mode”_, where all currently +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. +_"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 @@ -383,7 +481,7 @@ For more information, see *foot.ini*(5). 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. +"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 @@ -409,6 +507,38 @@ 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 @@ -449,10 +579,10 @@ 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 +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 +XTerm's implementation (as of XTerm-370) only supports querying key (as in keyboard keys) capabilities, and three custom capabilities: - TN - terminal name @@ -464,7 +594,7 @@ 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 +boolean capabilities. This means foot's entire terminfo can be queried via *XTGETTCAP*. Note that both Kitty and foot handles responses to multi-capability @@ -475,7 +605,7 @@ 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 +- 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. @@ -539,27 +669,46 @@ In all other cases, the exit code is that of the client application set according to either the *--term* command-line option or the *term* config option in *foot.ini*(5). -*PWD* - Current working directory (at the time of launching foot) - *COLORTERM* This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. -*TERM_PROGRAM* - Always set to *foot*. This can be used by client applications to - check which terminal is in use, but with the caveat that it may - have been inherited from a parent process in other terminals that - aren't known to set the variable. +*PWD* + Current working directory (at the time of launching foot) -*TERM_PROGRAM_VERSION* - Set to the foot version string, in the format _major_*.*_minor_*.*_patch_ - or _major_*.*_minor_*.*_patch_*-*_revision_*-\g*_commit_ for inter-release - builds. The same caveat as for *TERM_PROGRAM* applies. +*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 diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 5ef62045..a1ee326f 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -24,12 +24,12 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. Options are set using KEY=VALUE pairs: - *\[colors\]*++ + *\[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=""*) +empty string to be set, but it must be quoted: *KEY=""* # SECTION: main @@ -50,14 +50,23 @@ empty string to be set, but it must be quoted: *KEY=""*) *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*, which is used to set the - font size. Note that the font size is also affected by the - *dpi-aware* option. + 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 @@ -77,17 +86,22 @@ empty string to be set, but it must be quoted: *KEY=""*) 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 our out. + the font size when zooming in or out. Examples: ``` font-size-adjustment=0.5 # Adjust by 0.5 points - font-size-adjustment=10xp # Adjust by 10 pixels + font-size-adjustment=10px # Adjust by 10 pixels font-size-adjustment=7.5% # Adjust by 7.5 percent ``` @@ -114,6 +128,12 @@ empty string to be set, but it must be quoted: *KEY=""*) 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_. @@ -141,7 +161,7 @@ empty string to be set, but it must be quoted: *KEY=""*) *underline-offset* Use a custom offset for underlines. The offset is, by default, in - _points_ and relative the font's baseline. A positive value + _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. @@ -166,9 +186,62 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _unset_ -*box-drawings-uses-font-glyphs* Boolean. When disabled, foot generates - box/line drawing characters itself. The are several advantages to - doing this instead of using font glyphs: +*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. @@ -182,10 +255,18 @@ empty string to be set, but it must be quoted: *KEY=""*) 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* - *auto*, *yes*, or *no*. + 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 @@ -199,14 +280,8 @@ empty string to be set, but it must be quoted: *KEY=""*) instead sized using the monitor's scaling factor; doubling the scaling factor *does* double the font size. - Finally, if set to *auto*, fonts will be sized using the monitor's - DPI if _all_ monitors have a scaling factor of 1. If at least one - monitor as a scaling factor larger than 1 (regardless of whether - the foot window is mapped on that monitor or not), fonts will be - scaled using the scaling factor. - Note that this option typically does not work with bitmap fonts, - which only contains a pre-defined set of sizes, and cannot be + 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. @@ -217,22 +292,44 @@ empty string to be set, but it must be quoted: *KEY=""*) to size the font (*dpi-aware=no*), the font's pixel size will be multiplied with the scaling factor. - Default: _auto_ + Default: _no_ *pad* Padding between border and glyphs, in pixels (subject to output - scaling), in the form _XxY_. + scaling), in the form - This will add _at least_ X pixels on both the left and right - sides, and Y pixels on the top and bottom sides. The grid content - will be anchored in 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. + ``` + _XxY_ [center | center-when-fullscreen | center-when-maximized-and-fullscreen] + ``` + or + ``` + RIGHTxTOPxLEFTxBOTTOM [center | center-when-fullscreen | center-when-maximized-and-fullscreen] + ``` - To instead center the grid content, append *center* (e.g. *pad=5x5 - center*). + - `_XxY_` adds _at least_: + - X pixels on the left and right sides. + - Y pixels on the top and bottom sides. - Default: _0x0_. + - `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* @@ -251,28 +348,79 @@ empty string to be set, but it must be quoted: *KEY=""*) 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 is sent to the client, without any delays. + 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*. Default: _700x500_. + *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*.' + *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* @@ -289,12 +437,18 @@ empty string to be set, but it must be quoted: *KEY=""*) *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_. + 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 increasing its luminance. + 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 @@ -308,31 +462,6 @@ empty string to be set, but it must be quoted: *KEY=""*) text. Note that whitespace characters are _always_ word delimiters, regardless of this setting. Default: _,│`|:"'()[]{}<>_ -*notify* - Command to execute to display a notification. _${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*. - - _${window-title}_ is replaced with the current window title. - - Applications can trigger notifications in the following ways: - - - OSC 777: *\\e]777;notify;;<body>\\e\\\\* - - By default, notifications are *inhibited* if the foot window - has keyboard focus. See _notify-focus-inhibit_. - - Default: _notify-send -a ${app-id} -i ${app-id} ${title} ${body}_. - -*notify-focus-inhibit* - Boolean. If enabled, foot will not display notifications if the - terminal window has keyboard focus. - - Default: _yes_ - *selection-target* Clipboard target to automatically copy selected text to. One of *none*, *primary*, *clipboard* or *both*. Default: _primary_. @@ -342,10 +471,29 @@ empty string to be set, but it must be quoted: *KEY=""*) 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. -*utempter* - Path to utempter helper binary. Set to *none* to disable utmp - records. Default: _@utempter@_. +*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 @@ -360,12 +508,46 @@ The format is simply: 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* - 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. + 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. @@ -376,20 +558,267 @@ Note: do not set *TERM* here; use the *term* option in the main Default: _no_ *notify* - 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 _notify-focus-inhibit_. + 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* - Whether to run the command on *BEL* even while focused. Default: - _no_ + 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;<title>;<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 @@ -418,6 +847,9 @@ Note: do not set *TERM* here; use the *term* option in the main # 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}_. @@ -445,19 +877,48 @@ Note: do not set *TERM* here; use the *term* option in the main Default: _sadfjklewcmpgh_. -*protocols* - Comma separated list of protocols (schemes) that should be - recognized in URL mode. Note that only auto-detected URLs are - affected by this option. OSC-8 URLs are always enabled, regardless - of protocol. Default: _http, https, ftp, ftps, file, gemini, - gopher, irc, ircs_. - -*uri-characters* - Set of characters allowed in auto-detected URLs. Any character not - included in this set constitutes a URL delimiter. +*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: - _abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-\_.,~:;/?#@!$&%\*+="'()[]_ + 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 @@ -466,21 +927,26 @@ applications can change these at runtime. *style* Configures the default cursor style, and is one of: *block*, - *beam* or *underline*. Note that this can be overridden by - applications. Default: _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. Default: _no_. + by applications. Related option: *blink-rate*. Default: _no_. -*color* - 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. +*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 @@ -524,16 +990,42 @@ applications can change these at runtime. Default: _yes_. -# SECTION: colors +# SECTION: touch -This section controls the 16 ANSI colors, the default foreground and -background colors, and the extended 256 color palette. Note that +*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_. @@ -544,23 +1036,24 @@ can configure the background transparency with the _alpha_ option. *regular0*, *regular1* *..* *regular7* The eight basic ANSI colors (Black, Red, Green, Yellow, Blue, - Magenta, Cyan, White). Default: _073642_, _dc322f_, _859900_, - _b58900_, _268bd2_, _d33682_, _2aa198_ and _eee8d5_ (a variant of - the _solarized dark_ theme). + 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: _08404f_, _e35f5c_, _9fb700_, - _d9a400_, _4ba1de_, _dc619d_, _32c1b6_ and _ffffff_ (a variant of - the _solarized dark_ theme). + 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 reducing the luminance of the - current color. This is a generic approach that applies to both + 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 @@ -572,7 +1065,7 @@ can configure the background transparency with the _alpha_ option. the corresponding *regularN* color will be used. If the current color does not match any known color, it is dimmed - by reducing the luminance (i.e. the same behavior as if the *dimN* + 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. @@ -590,14 +1083,50 @@ can configure the background transparency with the _alpha_ option. 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. Note that *both* options must be set, or the default will be - used. Default: _inverse foreground/background_. + text. Default: _inverse foreground/background_. *jump-labels* Two color values specifying the foreground (text) and background @@ -623,6 +1152,13 @@ can configure the background transparency with the _alpha_ option. 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 @@ -670,9 +1206,13 @@ Examples: *hide-when-maximized* Boolean. When enabled, the CSD titlebar is hidden when the window - is maximized. The completely disable the titlebar, set *size* to 0 + 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 @@ -688,8 +1228,8 @@ Examples: minimize/maximize/close buttons. Default: _26_. *button-color* - Foreground color on the minimize/maximize/close buttons. Default: - use the default _background_ 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 @@ -716,11 +1256,42 @@ 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. -Note that *Alt* is usually called *Mod1*. +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. -A key combination can only be mapped to *one* action. Lets say you +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_; @@ -728,32 +1299,33 @@ e.g. *search-start=none*. *noop* All key combinations listed here will not be sent to the - application. Default: _not bound_. + application. Default: _none_. *scrollback-up-page* - Scrolls up/back one page in history. Default: _Shift+Page\_Up_. + 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: _not bound_. + Scrolls up/back half of a page in history. Default: _none_. *scrollback-up-line* - Scrolls up/back a single line in history. Default: _not bound_. + 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+Page\_Down Shift+KP\_Page\_Down_. *scrollback-down-half-page* - Scroll down/forward half of a page in history. Default: _not bound_. + Scroll down/forward half of a page in history. Default: _none_. *scrollback-down-line* - Scroll down/forward a single line in history. Default: _not bound_. + Scroll down/forward a single line in history. Default: _none_. *scrollback-home* - Scroll to the beginning of the scrollback. Default: _not bound_. + Scroll to the beginning of the scrollback. Default: _none_. *scrollback-end* - Scroll to the end (bottom) of the scrollback. Default: _not bound_. + Scroll to the end (bottom) of the scrollback. Default: _none_. *clipboard-copy* Copies the current selection into the _clipboard_. Default: _Control+Shift+c_ @@ -771,11 +1343,11 @@ e.g. *search-start=none*. *font-increase* Increases the font size by 0.5pt. Default: _Control+plus - Control+equal Control+KP\_Add_. + 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_. + Control+KP\_Subtract_ (also defined in *mouse-bindings*). *font-reset* Resets the font size to the default. Default: _Control+0 Control+KP\_0_. @@ -786,19 +1358,20 @@ e.g. *search-start=none*. current working directory. Default: _Control+Shift+n_. *minimize* - Minimizes the window. Default: _not bound_. + Minimizes the window. Default: _none_. *maximize* - Toggle the maximized state. Default: _not bound_. + Toggle the maximized state. Default: _none_. *fullscreen* - Toggles the fullscreen state. Default: _not bound_. + Toggles the fullscreen state. Default: _none_. -*pipe-visible*, *pipe-scrollback*, *pipe-selected* - Pipes the currently visible text, the entire scrollback, or the - currently selected text 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). +*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. @@ -806,16 +1379,21 @@ e.g. *search-start=none*. Note that the command is *not* automatically run inside a shell; use *sh -c "command line"* if you need that. - Example: - *pipe-visible=[sh -c "xurls | uniq | tac | fuzzel | xargs -r - firefox"] Control+Print* + 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* - Default: _not bound_ + 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+u_. + URL mode). Default: _Control+Shift+o_. *show-urls-persistent* Similar to *show-urls-launch*, but does not automatically exit URL @@ -824,14 +1402,41 @@ e.g. *search-start=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. Default: _none_. + 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 the next prompt (requires shell integration, see + Jump to the next prompt (requires shell integration, see *foot*(1)). Default: _Control+Shift+x_. *unicode-input* @@ -858,7 +1463,29 @@ e.g. *search-start=none*. fallback. The preferred way of entering Unicode characters, emojis etc is by using an IME. - Default: _none_. + 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 @@ -874,7 +1501,8 @@ scrollback search mode. The syntax is exactly the same as the regular *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_. + the regular _clipboard_, use *Control+Shift+c*. Default: _Return + KP_Enter_. *find-prev* Search **backwards** in the scrollback history for the next @@ -922,17 +1550,45 @@ scrollback search mode. The syntax is exactly the same as the regular 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 next word boundary. Default: - _Control+w_. + 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 next whitespace. Default: - _Control+Shift+w_. + 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+v Control+y Control+Shift+v XF86Paste_. *primary-paste* Paste from the _primary selection_ into the search @@ -942,6 +1598,32 @@ scrollback search mode. The syntax is exactly the same as the regular 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 @@ -1009,11 +1691,19 @@ 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* is optional and specifies the click count -required to trigger the binding. The default if *COUNT* is omitted is -_1_. +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_. -A modifier+button combination can only be mapped to *one* action. Lets +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 @@ -1033,6 +1723,22 @@ _action=none_; e.g. *primary-paste=none*. 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 @@ -1057,10 +1763,37 @@ actions listed under *key-bindings* can be used here as well. 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-3_. + is released. Default: _BTN\_LEFT-4_. *select-extend* Interactively extend an existing selection, using the original @@ -1077,6 +1810,15 @@ actions listed under *key-bindings* can be used here as well. *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 @@ -1094,9 +1836,8 @@ 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*, *cubic* or *lanczos3*. *cubic* and *lanczos3* produce - the best results, but are slower (with *lanczos3* being the best - _and_ slowest). + *bilinear*, *impulse*, *box*, *linear*, *cubic*, *gaussian*, + *lanczos2*, *lanczos3* or *lanczos3-stretched*. Default: _lanczos3_. @@ -1164,8 +1905,8 @@ any of these options. must be patched to use it. Until this has happened, foot offers an interim workaround; an - attempt to mitigate the screen flicker *without* affecting neither - performance nor latency. + 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 @@ -1236,6 +1977,8 @@ any of these options. - 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_ @@ -1313,8 +2056,102 @@ any of these options. 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_ + 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 diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index 189d9e3c..ad865913 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -31,7 +31,12 @@ terminal has terminated. *-a*,*--app-id*=_ID_ Value to set the *app-id* property on the Wayland window - to. Default: _foot_. + 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_. @@ -73,6 +78,11 @@ terminal has terminated. 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_. @@ -146,6 +156,11 @@ terminfo entries manually, by copying *foot* and *foot-direct* to 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* @@ -158,20 +173,42 @@ terminfo entries manually, by copying *foot* and *foot-direct* to This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. -*TERM_PROGRAM* - Always set to *foot*. This can be used by client applications to - check which terminal is in use, but with the caveat that it may - have been inherited from a parent process in other terminals that - aren't known to set the variable. +*PWD* + Current working directory (at the time of launching foot) -*TERM_PROGRAM_VERSION* - Set to the foot version string, in the format _major_*.*_minor_*.*_patch_ - or _major_*.*_minor_*.*_patch_*-*_revision_*-\g*_commit_ for inter-release - builds. The same caveat as for *TERM_PROGRAM* applies. +*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 index 86e75952..37972652 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -1,17 +1,25 @@ -sh = find_program('sh', native: true) - scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) -if utempter_path == '' - default_utempter_value = 'not set' +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 - default_utempter_value = utempter_path + 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'), - 'utempter': default_utempter_value, + 'utmp_backend': utmp_backend, + 'utmp_add_args': utmp_add_args, + 'utmp_del_args': utmp_del_args, + 'utmp_helper_path': utmp_path, } ) @@ -33,8 +41,9 @@ foreach man_src : [{'name': 'foot', 'section' : 1}, out, output: out, input: preprocessed, - command: [sh, '-c', '@0@ < @INPUT@'.format(scdoc_prog.full_path())], + 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 00000000..ce30fe8f Binary files /dev/null and b/doc/sixel-tux-foot.png differ diff --git a/doc/sixel-wow.png b/doc/sixel-wow.png deleted file mode 100644 index da481ac8..00000000 Binary files a/doc/sixel-wow.png and /dev/null differ diff --git a/doc/tux-foot-ok.png b/doc/tux-foot-ok.png new file mode 100644 index 00000000..5d814623 Binary files /dev/null and b/doc/tux-foot-ok.png differ diff --git a/extract.c b/extract.c index 31c32248..cd9a0c95 100644 --- a/extract.c +++ b/extract.c @@ -256,8 +256,8 @@ extract_one(const struct terminal *term, const struct row *row, } } - xassert(next_tab_stop >= col); - ctx->tab_spaces_left = next_tab_stop - col; + if (next_tab_stop > col) + ctx->tab_spaces_left = next_tab_stop - col - 1; } } diff --git a/fdm.c b/fdm.c index ea30443b..4822cd97 100644 --- a/fdm.c +++ b/fdm.c @@ -18,6 +18,18 @@ #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; @@ -113,7 +125,8 @@ fdm_destroy(struct fdm *fdm) for (int i = 0; i < SIGRTMAX; i++) { if (fdm->signal_handlers[i].callback != NULL) - LOG_WARN("handler for signal %d not removed", i); + LOG_WARN("handler for signal %d (SIG%s) not removed", + i, sigabbrev_np(i)); } if (tll_length(fdm->hooks_low) > 0 || @@ -338,7 +351,8 @@ 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 already has a handler", signo); + LOG_ERR("signal %d (SIG%s) already has a handler", + signo, sigabbrev_np(signo)); return false; } @@ -347,14 +361,16 @@ fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *d sigaddset(&mask, signo); if (sigprocmask(SIG_BLOCK, &mask, &original) < 0) { - LOG_ERRNO("failed to block signal %d", signo); + 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", signo); + LOG_ERRNO("failed to set signal handler for signal %d (SIG%s)", + signo, sigabbrev_np(signo)); sigprocmask(SIG_SETMASK, &original, NULL); return false; } @@ -374,7 +390,8 @@ fdm_signal_del(struct fdm *fdm, int signo) 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", signo); + LOG_ERRNO("failed to restore signal handler for signal %d (SIG%s)", + signo, sigabbrev_np(signo)); return false; } @@ -386,7 +403,8 @@ fdm_signal_del(struct fdm *fdm, int signo) sigemptyset(&mask); sigaddset(&mask, signo); if (sigprocmask(SIG_UNBLOCK, &mask, NULL) < 0) { - LOG_ERRNO("failed to unblock signal %d", signo); + LOG_ERRNO("failed to unblock signal %d (SIG%s)", + signo, sigabbrev_np(signo)); return false; } diff --git a/foot-features.c b/foot-features.c new file mode 100644 index 00000000..8e332517 --- /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 index ad447767..49cc56ed 100644 --- a/foot-features.h +++ b/foot-features.h @@ -1,39 +1,13 @@ #pragma once -#include <stdbool.h> +#include <stdio.h> -static inline bool feature_assertions(void) -{ -#if defined(NDEBUG) - return false; -#else - return true; -#endif -} +extern const char version_and_features[]; -static inline bool feature_ime(void) +static inline void +print_version_and_features(const char *prefix) { -#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - return true; -#else - return false; -#endif -} - -static inline bool feature_pgo(void) -{ -#if defined(FOOT_PGO_ENABLED) && FOOT_PGO_ENABLED - return true; -#else - return false; -#endif -} - -static inline bool feature_graphemes(void) -{ -#if defined(FOOT_GRAPHEME_CLUSTERING) && FOOT_GRAPHEME_CLUSTERING - return true; -#else - return false; -#endif + fputs(prefix, stdout); + fputs(version_and_features, stdout); + fputc('\n', stdout); } diff --git a/org.codeberg.dnkl.foot-server.desktop b/foot-server.desktop similarity index 100% rename from org.codeberg.dnkl.foot-server.desktop rename to foot-server.desktop diff --git a/foot-server@.service.in b/foot-server.service.in similarity index 51% rename from foot-server@.service.in rename to foot-server.service.in index c40bb454..118b19ab 100644 --- a/foot-server@.service.in +++ b/foot-server.service.in @@ -1,13 +1,15 @@ [Service] ExecStart=@bindir@/foot --server=3 -Environment=WAYLAND_DISPLAY=%i UnsetEnvironment=LISTEN_PID LISTEN_FDS LISTEN_FDNAMES NonBlocking=true [Unit] Requires=%N.socket -Description=Foot terminal server mode for WAYLAND_DISPLAY=%i +Description=Foot terminal server mode Documentation=man:foot(1) +PartOf=graphical-session.target +After=graphical-session.target +ConditionEnvironment=WAYLAND_DISPLAY [Install] -WantedBy=wayland-instance@.target +WantedBy=graphical-session.target diff --git a/foot-server.socket b/foot-server.socket new file mode 100644 index 00000000..0c7c1b8f --- /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-server@.socket b/foot-server@.socket deleted file mode 100644 index 71db51cb..00000000 --- a/foot-server@.socket +++ /dev/null @@ -1,5 +0,0 @@ -[Socket] -ListenStream=%t/foot-%i.sock - -[Install] -WantedBy=wayland-instance@.target diff --git a/org.codeberg.dnkl.foot.desktop b/foot.desktop similarity index 100% rename from org.codeberg.dnkl.foot.desktop rename to foot.desktop diff --git a/foot.info b/foot.info index cf81d721..13f4403c 100644 --- a/foot.info +++ b/foot.info @@ -13,6 +13,7 @@ @default_terminfo@+base|foot base fragment, AX, + Su, Tc, XF, XT, @@ -38,9 +39,13 @@ 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, + Sync=\E[?2026%?%p1%{1}%-%tl%eh%;, + TS=\E]2;, XM=\E[?1006;1000%?%p1%{1}%=%th%el%;, XR=\E[>0q, acsc=``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, @@ -73,6 +78,8 @@ ed=\E[J, el1=\E[1K, el=\E[K, + fd=\E[?1004l, + fe=\E[?1004h, flash=\E]555\E\\, fsl=\E\\, home=\E[H, @@ -227,6 +234,7 @@ kri=\E[1;2A, kxIN=\E[I, kxOUT=\E[O, + nel=\EE, oc=\E]104\E\\, op=\E[39;49m, rc=\E8, @@ -240,13 +248,15 @@ 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\\[[0-9]+;[0-9]+;[0-9]+c, + 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, @@ -257,6 +267,7 @@ 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, @@ -268,7 +279,7 @@ u9=\E[c, vpa=\E[%i%p1%dd, xm=\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;, - xr=\EP>\\|[ -~]+\E\\\\, + 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 index 8266b01b..a9b4b83d 100644 --- a/foot.ini +++ b/foot.ini @@ -4,7 +4,7 @@ # term=foot (or xterm-256color if built with -Dterminfo=disabled) # login-shell=no -# app-id=foot +# app-id=foot # globally set wayland app-id. Default values are "foot" and "footclient" for desktop and server mode # title=foot # locked-title=no @@ -19,32 +19,50 @@ # 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=auto +# 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 # optionally append 'center' +# pad=0x0 center-when-maximized-and-fullscreen +# resize-by-cells=yes +# resize-keep-grid=yes # resize-delay-ms=100 -# notify=notify-send -a ${app-id} -i ${app-id} ${title} ${body} - # bold-text-in-bright=no # word-delimiters=,│`|:"'()[]{}<> # selection-target=primary # workers=<number of logical CPUs> -# utempter=/usr/lib/utempter/utempter +# 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 @@ -55,13 +73,24 @@ # launch=xdg-open ${url} # label-letters=sadfjklewcmpgh # osc8-underline=url-mode -# protocols=http, https, ftp, ftps, file, gemini, gopher -# uri-characters=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.,~:;/?#@!$&%*+="'()[] +# 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 -# color=<inverse foreground/background> # blink=no +# blink-rate=500 # beam-thickness=1.5 # underline-thickness=<font underline thickness> @@ -69,32 +98,41 @@ # hide-when-typing=no # alternate-scroll-mode=yes -[colors] +[touch] +# long-press-delay=400 + +[colors-dark] # alpha=1.0 -# background=002b36 -# foreground=839496 +# 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=073642 # black -# regular1=dc322f # red -# regular2=859900 # green -# regular3=b58900 # yellow -# regular4=268bd2 # blue -# regular5=d33682 # magenta -# regular6=2aa198 # cyan -# regular7=eee8d5 # white +# 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=08404f # bright black -# bright1=e35f5c # bright red -# bright2=9fb700 # bright green -# bright3=d9a400 # bright yellow -# bright4=4ba1de # bright blue -# bright5=dc619d # bright magenta -# bright6=32c1b6 # bright cyan +# 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> @@ -104,6 +142,24 @@ # ... # 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> @@ -113,12 +169,18 @@ # 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 + [csd] # preferred=server # size=26 # font=<primary font> # color=<foreground color> -# hide-when-typing=no +# hide-when-maximized=no +# double-click-to-maximize=yes # border-width=0 # border-color=<csd.color> # button-width=26 @@ -128,12 +190,14 @@ # button-close-color=<regular1> [key-bindings] -# scrollback-up-page=Shift+Page_Up +# 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 +# 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 @@ -148,17 +212,22 @@ # 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 -# show-urls-launch=Control+Shift+u +# 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=none +# unicode-input=Control+Shift+u +# color-theme-switch-1=none +# color-theme-switch-2=none +# color-theme-toggle=none # noop=none +# quit=none [search-bindings] # cancel=Control+g Control+c Escape -# commit=Return +# commit=Return KP_Enter # find-prev=Control+r # find-next=Control+s # cursor-left=Left Control+b @@ -171,11 +240,27 @@ # delete-prev-word=Mod1+BackSpace Control+BackSpace # delete-next=Delete # delete-next-word=Mod1+d Control+Delete -# extend-to-word-boundary=Control+w +# 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 @@ -185,6 +270,10 @@ # \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 @@ -193,6 +282,7 @@ # select-extend-character-wise=Control+BTN_RIGHT # select-word=BTN_LEFT-2 # select-word-whitespace=Control+BTN_LEFT-2 -# select-row=BTN_LEFT-3 +# select-quote = BTN_LEFT-3 +# select-row=BTN_LEFT-4 # vim: ft=dosini diff --git a/org.codeberg.dnkl.footclient.desktop b/footclient.desktop similarity index 100% rename from org.codeberg.dnkl.footclient.desktop rename to footclient.desktop diff --git a/generate-version.sh b/generate-version.sh index 3772008b..a030d512 100755 --- a/generate-version.sh +++ b/generate-version.sh @@ -41,7 +41,6 @@ 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_VERSION_SHORT \"${git_version:-${default_version}}\" #define FOOT_MAJOR ${major} #define FOOT_MINOR ${minor} #define FOOT_PATCH ${patch} diff --git a/grid.c b/grid.c index e1c4d28b..df7ef61c 100644 --- a/grid.c +++ b/grid.c @@ -1,5 +1,6 @@ #include "grid.h" +#include <limits.h> #include <stdlib.h> #include <string.h> @@ -15,8 +16,12 @@ #define TIME_REFLOW 0 +#if defined(TIME_REFLOW) +#include "misc.h" +#endif + /* - * “sb” (scrollback relative) coordinates + * "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 @@ -35,7 +40,8 @@ grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row) return rebased_row; } -int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_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; @@ -84,22 +90,32 @@ ensure_row_has_extra_data(struct row *row) } static void -verify_no_overlapping_uris(const struct row_data *extra) +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 < extra->uri_ranges.count; i++) { - const struct row_uri_range *r1 = &extra->uri_ranges.v[i]; + for (size_t i = 0; i < ranges->count; i++) { + const struct row_range *r1 = &ranges->v[i]; - for (size_t j = i + 1; j < extra->uri_ranges.count; j++) { - const struct row_uri_range *r2 = &extra->uri_ranges.v[j]; + 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)) { - BUG("OSC-8 URI overlap: %s: %d-%d: %s: %d-%d", - r1->uri, r1->start, r1->end, - r2->uri, r2->start, 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; + } } } } @@ -107,20 +123,38 @@ verify_no_overlapping_uris(const struct row_data *extra) } static void -verify_uris_are_sorted(const struct row_data *extra) +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_uri_range *last = NULL; + const struct row_range *last = NULL; - for (size_t i = 0; i < extra->uri_ranges.count; i++) { - const struct row_uri_range *r = &extra->uri_ranges.v[i]; + 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) { - BUG("OSC-8 URI not sorted correctly: " - "%s: %d-%d came before %s: %d-%d", - last->uri, last->start, last->end, - r->uri, r->start, 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; + } } } @@ -130,16 +164,21 @@ verify_uris_are_sorted(const struct row_data *extra) } static void -uri_range_ensure_size(struct row_data *extra, uint32_t count_to_add) +verify_ranges_are_sorted(const struct row_data *extra) { - if (extra->uri_ranges.count + count_to_add > extra->uri_ranges.size) { - extra->uri_ranges.size = extra->uri_ranges.count + count_to_add; - extra->uri_ranges.v = xrealloc( - extra->uri_ranges.v, - extra->uri_ranges.size * sizeof(extra->uri_ranges.v[0])); + 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(extra->uri_ranges.count + count_to_add <= extra->uri_ranges.size); + xassert(ranges->count + count_to_add <= ranges->size); } /* @@ -147,58 +186,88 @@ uri_range_ensure_size(struct row_data *extra, uint32_t count_to_add) * invalidating pointers into it. */ static void -uri_range_insert(struct row_data *extra, size_t idx, int start, int end, - uint64_t id, const char *uri) +range_insert(struct row_ranges *ranges, size_t idx, int start, int end, + enum row_range_type type, const union row_range_data *data) { - uri_range_ensure_size(extra, 1); + range_ensure_size(ranges, 1); - xassert(idx <= extra->uri_ranges.count); + xassert(idx <= ranges->count); - const size_t move_count = extra->uri_ranges.count - idx; - memmove(&extra->uri_ranges.v[idx + 1], - &extra->uri_ranges.v[idx], - move_count * sizeof(extra->uri_ranges.v[0])); + const size_t move_count = ranges->count - idx; + memmove(&ranges->v[idx + 1], + &ranges->v[idx], + move_count * sizeof(ranges->v[0])); - extra->uri_ranges.count++; - extra->uri_ranges.v[idx] = (struct row_uri_range){ - .start = start, - .end = end, - .id = id, - .uri = xstrdup(uri), - }; + 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 -uri_range_append_no_strdup(struct row_data *extra, int start, int end, - uint64_t id, char *uri) +range_append_by_ref(struct row_ranges *ranges, int start, int end, + enum row_range_type type, const union row_range_data *data) { - uri_range_ensure_size(extra, 1); - extra->uri_ranges.v[extra->uri_ranges.count++] = (struct row_uri_range){ - .start = start, - .end = end, - .id = id, - .uri = uri, - }; + 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 -uri_range_append(struct row_data *extra, int start, int end, uint64_t id, - const char *uri) +range_append(struct row_ranges *ranges, int start, int end, + enum row_range_type type, const union row_range_data *data) { - uri_range_append_no_strdup(extra, start, end, id, xstrdup(uri)); + 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 -uri_range_delete(struct row_data *extra, size_t idx) +range_delete(struct row_ranges *ranges, enum row_range_type type, size_t idx) { - xassert(idx < extra->uri_ranges.count); - grid_row_uri_range_destroy(&extra->uri_ranges.v[idx]); + xassert(idx < ranges->count); + grid_row_range_destroy(&ranges->v[idx], type); - const size_t move_count = extra->uri_ranges.count - idx - 1; - memmove(&extra->uri_ranges.v[idx], - &extra->uri_ranges.v[idx + 1], - move_count * sizeof(extra->uri_ranges.v[0])); - extra->uri_ranges.count--; + 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 * @@ -231,7 +300,7 @@ grid_snapshot(const struct grid *grid) clone_row->cells = xmalloc(grid->num_cols * sizeof(clone_row->cells[0])); clone_row->linebreak = row->linebreak; clone_row->dirty = row->dirty; - clone_row->prompt_marker = row->prompt_marker; + clone_row->shell_integration = row->shell_integration; for (int c = 0; c < grid->num_cols; c++) clone_row->cells[c] = row->cells[c]; @@ -242,40 +311,87 @@ grid_snapshot(const struct grid *grid) struct row_data *clone_extra = xcalloc(1, sizeof(*clone_extra)); clone_row->extra = clone_extra; - uri_range_ensure_size(clone_extra, extra->uri_ranges.count); + range_ensure_size(&clone_extra->uri_ranges, extra->uri_ranges.count); + range_ensure_size(&clone_extra->underline_ranges, extra->underline_ranges.count); - for (size_t i = 0; i < extra->uri_ranges.count; i++) { - const struct row_uri_range *range = &extra->uri_ranges.v[i]; - uri_range_append( - clone_extra, - range->start, range->end, range->id, range->uri); + 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 width = it->item.width; - int height = it->item.height; - pixman_image_t *pix = it->item.pix; - pixman_format_code_t pix_fmt = pixman_image_get_format(pix); - int stride = stride_for_format_and_width(pix_fmt, width); + 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 size = stride * height; - void *new_data = xmalloc(size); - memcpy(new_data, it->item.data, size); + size_t original_size = original_stride * original_height; + void *new_original_data = xmemdup(it->item.original.data, original_size); - pixman_image_t *new_pix = pixman_image_create_bits_no_clear( - pix_fmt, width, height, new_data, stride); + 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 = { - .data = new_data, - .pix = new_pix, - .width = width, - .height = height, + .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); @@ -323,9 +439,11 @@ grid_row_alloc(int cols, bool initialize) { struct row *row = xmalloc(sizeof(*row)); row->dirty = false; - row->linebreak = false; + row->linebreak = true; row->extra = NULL; - row->prompt_marker = false; + 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])); @@ -383,8 +501,9 @@ grid_resize_without_reflow( sizeof(struct cell) * min(old_cols, new_cols)); new_row->dirty = old_row->dirty; - new_row->linebreak = false; - new_row->prompt_marker = old_row->prompt_marker; + 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 */ @@ -420,10 +539,11 @@ grid_resize_without_reflow( ensure_row_has_extra_data(new_row); struct row_data *new_extra = new_row->extra; - uri_range_ensure_size(new_extra, old_extra->uri_ranges.count); + 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 (size_t i = 0; i < old_extra->uri_ranges.count; i++) { - const struct row_uri_range *range = &old_extra->uri_ranges.v[i]; + 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 */ @@ -432,9 +552,22 @@ grid_resize_without_reflow( const int start = range->start; const int end = min(range->end, new_cols - 1); - uri_range_append(new_extra, start, end, range->id, range->uri); + 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++) { @@ -454,8 +587,8 @@ grid_resize_without_reflow( if (row->extra == NULL) continue; - verify_no_overlapping_uris(row->extra); - verify_uris_are_sorted(row->extra); + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); } #endif @@ -505,27 +638,60 @@ grid_resize_without_reflow( } static void -reflow_uri_range_start(struct row_uri_range *range, struct row *new_row, - int new_col_idx) +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); - uri_range_append_no_strdup - (new_row->extra, new_col_idx, -1, range->id, range->uri); - range->uri = NULL; + + 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_uri_range_end(struct row_uri_range *range, struct row *new_row, - int new_col_idx) +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; - xassert(extra->uri_ranges.count > 0); + struct row_ranges *ranges = NULL; - struct row_uri_range *new_range = - &extra->uri_ranges.v[extra->uri_ranges.count - 1]; + switch (type) { + case ROW_RANGE_URI: ranges = &extra->uri_ranges; break; + case ROW_RANGE_UNDERLINE: ranges = &extra->underline_ranges; break; + } - xassert(new_range->id == range->id); + 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; } @@ -543,10 +709,11 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, new_row = grid_row_alloc(col_count, false); new_grid[*row_idx] = new_row; } else { - /* Scrollback is full, need to re-use a row */ + /* Scrollback is full, need to reuse a row */ grid_row_reset_extra(new_row); - new_row->linebreak = false; - new_row->prompt_marker = false; + 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) { @@ -556,9 +723,9 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, } /* - * TODO: detect if the re-used row is covered by the + * 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 + * don't know if we've translated the selection coordinates * yet. */ } @@ -568,12 +735,12 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, return new_row; /* - * URI ranges are per row. Thus, we need to ‘close’ the still-open + * 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_uri_range *range = + struct row_range *range = &extra->uri_ranges.v[extra->uri_ranges.count - 1]; if (range->end < 0) { @@ -583,7 +750,24 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, /* Open a new range on the new/current row */ ensure_row_has_extra_data(new_row); - uri_range_append(new_row->extra, 0, -1, range->id, range->uri); + 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); } } @@ -630,7 +814,7 @@ tp_cmp(const void *_a, const void *_b) void grid_resize_and_reflow( - struct grid *grid, int new_rows, int new_cols, + 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]) @@ -708,6 +892,8 @@ grid_resize_and_reflow( i, tracking_points[i]->row, tracking_points[i]->col); } + int coalesced_linebreaks = 0; + /* * Walk the old grid */ @@ -748,8 +934,9 @@ grid_resize_and_reflow( } if (!old_row->linebreak && col_count > 0) { - /* Don’t truncate logical lines */ - col_count = old_cols; + /* 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); @@ -771,199 +958,203 @@ grid_resize_and_reflow( tp = NULL; /* Does this row have any URIs? */ - struct row_uri_range *range, *range_terminator; - struct row_data *extra = old_row->extra; + 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) { - range = &extra->uri_ranges.v[0]; - range_terminator = &extra->uri_ranges.v[extra->uri_ranges.count]; + 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_uri_range *last_on_row = + 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 - range = range_terminator = NULL; + uri_range = uri_range_terminator = NULL; - for (int start = 0, left = col_count; left > 0;) { - int end; - bool tp_break = false; - bool uri_break = false; + 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; + } /* - * Set end-coordinate for this chunk, by finding the next - * point-of-interest on this row. - * - * If there are no more tracking points, or URI ranges, - * the end-coordinate will be at the end of the row, - */ - if (range != range_terminator) { - int uri_col = (range->start >= start ? range->start : range->end) + 1; - - if (tp != NULL) { - int tp_col = tp->col + 1; - end = min(tp_col, uri_col); - - tp_break = end == tp_col; - uri_break = end == uri_col; - LOG_DBG("tp+uri break at %d (%d, %d)", end, tp_col, uri_col); - } else { - end = uri_col; - uri_break = true; - LOG_DBG("uri break at %d", end); + * 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}; } - } else if (tp != NULL) { - end = tp->col + 1; - tp_break = true; - LOG_DBG("TP break at %d", end); - } else - end = col_count; + line_wrap(); + } - int cols = end - start; - xassert(cols > 0); - xassert(start + cols <= old_cols); + new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; - /* - * Copy the row chunk to the new grid. Note that there may - * be fewer cells left on the new row than what we have in - * the chunk. I.e. the chunk may have to be split up into - * multiple memcpy:ies. - */ + 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); + } - for (int count = cols, from = start; count > 0;) { - xassert(new_col_idx <= new_cols); - int new_row_cells_left = new_cols - new_col_idx; - - /* Row full, emit newline and get a new, fresh, row */ - if (new_row_cells_left <= 0) { - line_wrap(); - new_row_cells_left = new_cols; + 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++; + } } - /* Number of cells we can copy */ - int amount = min(count, new_row_cells_left); - xassert(amount > 0); + 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; /* - * If we’re going to reach the end of the new row, we - * need to make sure we don’t end in the middle of a - * multi-column character. + * 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. */ - int spacers = 0; - if (new_col_idx + amount >= new_cols) { - /* - * While the cell *after* the last cell is a CELL_SPACER - * - * This means we have a multi-column character - * that doesn’t fit on the current row. We need to - * push it to the next row, and insert CELL_SPACER - * cells as padding. - */ - while ( - unlikely( - amount > 1 && - from + amount < old_cols && - old_row->cells[from + amount].wc >= CELL_SPACER + 1)) - { - amount--; - spacers++; - } - - xassert( - amount == 1 || - old_row->cells[from + amount - 1].wc <= CELL_SPACER + 1); - } - - xassert(new_col_idx + amount <= new_cols); - xassert(from + amount <= old_cols); - - if (from == 0) - new_row->prompt_marker = old_row->prompt_marker; - - memcpy( - &new_row->cells[new_col_idx], &old_row->cells[from], - amount * sizeof(struct cell)); - - count -= amount; - from += amount; - new_col_idx += amount; - - xassert(new_col_idx <= new_cols); - - if (unlikely(spacers > 0)) { - xassert(new_col_idx + spacers == new_cols); - - const struct cell *cell = &old_row->cells[from - 1]; - - for (int i = 0; i < spacers; i++, new_col_idx++) { - new_row->cells[new_col_idx].wc = CELL_SPACER; - new_row->cells[new_col_idx].attrs = cell->attrs; - } - } + new_row->linebreak = false; + old++; + c++; } - - xassert(new_col_idx > 0); - - if (tp_break) { - do { - xassert(tp != NULL); - xassert(tp->row == old_row_idx); - xassert(tp->col == end - 1); - - tp->row = new_row_idx; - tp->col = new_col_idx - 1; - - next_tp++; - tp = *next_tp; - } while (tp->row == old_row_idx && tp->col == end - 1); - - 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 (uri_break) { - xassert(range != NULL); - - if (range->start == end - 1) - reflow_uri_range_start(range, new_row, new_col_idx - 1); - - if (range->end == end - 1) { - reflow_uri_range_end(range, new_row, new_col_idx - 1); - grid_row_uri_range_destroy(range); - range++; - } - } - - left -= cols; - start += cols; } if (old_row->linebreak) { - /* 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 (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) - line_wrap(); - else if (new_row->extra != NULL && - new_row->extra->uri_ranges.count > 0) - { + 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 { /* - * 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). + * 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) */ - uint32_t last_idx = new_row->extra->uri_ranges.count - 1; - xassert(new_row->extra->uri_ranges.v[last_idx].end >= 0); + coalesced_linebreaks++; } } @@ -984,7 +1175,7 @@ grid_resize_and_reflow( xassert(old_rows == 0 || *next_tp == &terminator); #if defined(_DEBUG) - /* Verify all URI ranges have been “closed” */ + /* Verify all URI ranges have been "closed" */ for (int r = 0; r < new_rows; r++) { const struct row *row = new_grid[r]; @@ -995,9 +1186,11 @@ grid_resize_and_reflow( 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_uris(row->extra); - verify_uris_are_sorted(row->extra); + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); } /* Verify all old rows have been free:d */ @@ -1028,7 +1221,7 @@ grid_resize_and_reflow( grid->num_cols = new_cols; /* - * Set new viewport, making sure it’s not too far down. + * 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). @@ -1051,15 +1244,26 @@ grid_resize_and_reflow( 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; - 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); @@ -1078,25 +1282,60 @@ grid_resize_and_reflow( #endif } -void -grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) +static bool +ranges_match(const struct row_range *r1, const struct row_range *r2, + enum row_range_type type) { - ensure_row_has_extra_data(row); + 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; - struct row_data *extra = row->extra; - for (ssize_t i = (ssize_t)extra->uri_ranges.count - 1; i >= 0; i--) { - struct row_uri_range *r = &extra->uri_ranges.v[i]; + for (int i = ranges->count - 1; i >= 0; i--) { + struct row_range *r = &ranges->v[i]; - const bool matching_id = r->id == id; + const bool matching = range_match_data(r, data, type); - if (matching_id && r->end + 1 == col) { - /* Extend existing URI’s tail */ + if (matching && r->end + 1 == col) { + /* Extend existing range tail */ r->end++; - goto out; + return; } else if (r->end < col) { @@ -1111,8 +1350,8 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) xassert(r->start <= col); xassert(r->end >= col); - if (matching_id) - goto out; + if (matching) + return; if (r->start == r->end) { replace = true; @@ -1130,11 +1369,17 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) xassert(r->start < col); xassert(r->end > col); - uri_range_insert(extra, i + 1, col + 1, r->end, r->id, r->uri); + 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 = &extra->uri_ranges.v[i]; + * 'old' pointer invalid */ + r = &ranges->v[i]; r->end = col - 1; xassert(r->start <= r->end); @@ -1145,35 +1390,68 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) } } - xassert(insert_idx <= extra->uri_ranges.count); + xassert(insert_idx <= ranges->count); if (replace) { - grid_row_uri_range_destroy(&extra->uri_ranges.v[insert_idx]); - extra->uri_ranges.v[insert_idx] = (struct row_uri_range){ + grid_row_range_destroy(&ranges->v[insert_idx], type); + ranges->v[insert_idx] = (struct row_range){ .start = col, .end = col, - .id = id, - .uri = xstrdup(uri), }; + + 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 - uri_range_insert(extra, insert_idx, col, col, id, uri); + range_insert(ranges, insert_idx, col, col, type, data); if (run_merge_pass) { - for (size_t i = 1; i < extra->uri_ranges.count; i++) { - struct row_uri_range *r1 = &extra->uri_ranges.v[i - 1]; - struct row_uri_range *r2 = &extra->uri_ranges.v[i]; + 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 (r1->id == r2->id && r1->end + 1 == r2->start) { + if (ranges_match(r1, r2, type) && r1->end + 1 == r2->start) { r1->end = r2->end; - uri_range_delete(extra, i); + range_delete(ranges, type, i); i--; } } } +} -out: - verify_no_overlapping_uris(extra); - verify_uris_are_sorted(extra); +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 @@ -1186,7 +1464,7 @@ UNITTEST 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].id == _id); \ + xassert(row_data.uri_ranges.v[idx].uri.id == _id); \ } while (0) grid_row_uri_range_put(&row, 0, "http://foo.bar", 123); @@ -1241,17 +1519,15 @@ UNITTEST #undef verify_range } -void -grid_row_uri_range_erase(struct row *row, int start, int end) +static void +grid_row_range_erase(struct row_ranges *ranges, enum row_range_type type, + int start, int end) { - xassert(row->extra != NULL); xassert(start <= end); - struct row_data *extra = row->extra; - /* Split up, or remove, URI ranges affected by the erase */ - for (ssize_t i = (ssize_t)extra->uri_ranges.count - 1; i >= 0; i--) { - struct row_uri_range *old = &extra->uri_ranges.v[i]; + for (int i = ranges->count - 1; i >= 0; i--) { + struct row_range *old = &ranges->v[i]; if (old->end < start) return; @@ -1261,17 +1537,23 @@ grid_row_uri_range_erase(struct row *row, int start, int end) if (start <= old->start && end >= old->end) { /* Erase range covers URI completely - remove it */ - uri_range_delete(extra, i); + range_delete(ranges, type, i); } else if (start > old->start && end < old->end) { - /* Erase range erases a part in the middle of the URI */ - uri_range_insert( - extra, i + 1, end + 1, old->end, old->id, old->uri); + /* + * 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 = &extra->uri_ranges.v[i]; + * 'old' pointer invalid */ + old = &ranges->v[i]; old->end = start - 1; return; /* There can be no more URIs affected by the erase range */ } @@ -1291,59 +1573,79 @@ grid_row_uri_range_erase(struct row *row, int start, int end) } } +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); - uri_range_append(&row_data, 1, 10, 0, "dummy"); - uri_range_append(&row_data, 11, 20, 0, "dummy"); + 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_uris(&row_data); - verify_uris_are_sorted(&row_data); + 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_uris(&row_data); - verify_uris_are_sorted(&row_data); + 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 */ - uri_range_append(&row_data, 1, 10, 0, "dummy"); - uri_range_append(&row_data, 11, 20, 0, "dummy"); + 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_uris(&row_data); - verify_uris_are_sorted(&row_data); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); - grid_row_uri_range_destroy(&row_data.uri_ranges.v[0]); - grid_row_uri_range_destroy(&row_data.uri_ranges.v[1]); + 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 */ - uri_range_append(&row_data, 1, 10, 0, "dummy"); + 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_uris(&row_data); - verify_uris_are_sorted(&row_data); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); - grid_row_uri_range_destroy(&row_data.uri_ranges.v[0]); - grid_row_uri_range_destroy(&row_data.uri_ranges.v[1]); + 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; /* @@ -1353,23 +1655,22 @@ UNITTEST * 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 + * 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 + * (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; - uri_range_append(&row_data, 1, 10, 0, "dummy"); + 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); - for (size_t i = 0; i < row_data.uri_ranges.count; i++) - grid_row_uri_range_destroy(&row_data.uri_ranges.v[i]); + 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 index 0664409c..71bdc29e 100644 --- a/grid.h +++ b/grid.h @@ -16,7 +16,7 @@ void grid_resize_without_reflow( int old_screen_rows, int new_screen_rows); void grid_resize_and_reflow( - struct grid *grid, int new_rows, int new_cols, + 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]); @@ -86,13 +86,38 @@ grid_row_in_view(struct grid *grid, int row_no) void grid_row_uri_range_put( struct row *row, int col, const char *uri, uint64_t id); -void grid_row_uri_range_add(struct row *row, struct row_uri_range range); 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_uri_range *range) +grid_row_uri_range_destroy(struct row_range *range) { - free(range->uri); + 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 @@ -103,9 +128,10 @@ grid_row_reset_extra(struct row *row) if (likely(extra == NULL)) return; - for (size_t i = 0; i < extra->uri_ranges.count; i++) - grid_row_uri_range_destroy(&extra->uri_ranges.v[i]); + 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 index 3ebe4beb..1a8c919e 100644 --- a/hsl.c +++ b/hsl.c @@ -2,41 +2,6 @@ #include <math.h> -#include "util.h" - -void -rgb_to_hsl(uint32_t rgb, int *hue, int *sat, int *lum) -{ - double r = (double)((rgb >> 16) & 0xff) / 255.; - double g = (double)((rgb >> 8) & 0xff) / 255.; - double b = (double)((rgb >> 0) & 0xff) / 255.; - - double x_max = max(max(r, g), b); - double x_min = min(min(r, g), b); - double V = x_max; - - double C = x_max - x_min; - double L = (x_max + x_min) / 2.; - - *lum = 100 * L; - - if (C == 0.0) - *hue = 0; - else if (V == r) - *hue = 60. * (0. + (g - b) / C); - else if (V == g) - *hue = 60. * (2. + (b - r) / C); - else if (V == b) - *hue = 60. * (4. + (r - g) / C); - if (*hue < 0) - *hue += 360; - - double S = C == 0.0 - ? 0 - : C / (1. - fabs(2. * L - 1.)); - *sat = 100 * S; -} - uint32_t hsl_to_rgb(int hue, int sat, int lum) { @@ -83,7 +48,7 @@ hsl_to_rgb(int hue, int sat, int lum) b += m; return ( - (int)round(r * 255.) << 16 | - (int)round(g * 255.) << 8 | - (int)round(b * 255.) << 0); + (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 index 2a46c117..1aaf7e66 100644 --- a/hsl.h +++ b/hsl.h @@ -2,5 +2,4 @@ #include <stdint.h> -void rgb_to_hsl(uint32_t rgb, int *hue, int *sat, int *lum); uint32_t hsl_to_rgb(int hue, int sat, int lum); diff --git a/ime.c b/ime.c index f3a3ec18..c6ccb479 100644 --- a/ime.c +++ b/ime.c @@ -68,6 +68,16 @@ enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, /* 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); } @@ -175,7 +185,7 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, } /* - * 2. Delete requested surroundin text + * 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 diff --git a/input.c b/input.c index 7e5d204d..aa6b7f1d 100644 --- a/input.c +++ b/input.c @@ -40,6 +40,7 @@ #include "url-mode.h" #include "util.h" #include "vt.h" +#include "xkbcommon-vmod.h" #include "xmalloc.h" #include "xsnprintf.h" @@ -81,9 +82,11 @@ pipe_closed: 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) + const struct key_binding *binding, uint32_t serial, int amount) { const enum bind_action_normal action = binding->action; @@ -115,6 +118,18 @@ execute_binding(struct seat *seat, struct terminal *term, } 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); @@ -136,6 +151,18 @@ execute_binding(struct seat *seat, struct terminal *term, } 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); @@ -209,7 +236,8 @@ execute_binding(struct seat *seat, struct terminal *term, break; /* FALLTHROUGH */ case BIND_ACTION_PIPE_VIEW: - case BIND_ACTION_PIPE_SELECTED: { + case BIND_ACTION_PIPE_SELECTED: + case BIND_ACTION_PIPE_COMMAND_OUTPUT: { if (binding->aux->type != BINDING_AUX_PIPE) return true; @@ -251,6 +279,10 @@ execute_binding(struct seat *seat, struct terminal *term, 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; @@ -283,8 +315,8 @@ execute_binding(struct seat *seat, struct terminal *term, } } - if (!spawn(term->reaper, term->cwd, binding->aux->pipe.args, - pipe_fd[0], stdout_fd, stderr_fd, NULL)) + 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 */ @@ -326,9 +358,9 @@ execute_binding(struct seat *seat, struct terminal *term, action == BIND_ACTION_SHOW_URLS_LAUNCH ? URL_ACTION_LAUNCH : URL_ACTION_PERSISTENT; - urls_collect(term, url_action, &term->urls); + urls_collect(term, url_action, &term->conf->url.preg, true, &term->urls); urls_assign_key_combos(term->conf, &term->urls); - urls_render(term); + urls_render(term, &term->conf->url.launch); return true; } @@ -359,7 +391,7 @@ execute_binding(struct seat *seat, struct terminal *term, const struct row *row = grid->rows[r_abs]; xassert(row != NULL); - if (!row->prompt_marker) + if (!row->shell_integration.prompt_marker) continue; grid->view = r_abs; @@ -391,9 +423,9 @@ execute_binding(struct seat *seat, struct terminal *term, const struct row *row = grid->rows[r_abs]; xassert(row != NULL); - if (!row->prompt_marker) { + if (!row->shell_integration.prompt_marker) { if (r_abs == grid->offset + term->rows - 1) { - /* We’ve reached the bottom of the scrollback */ + /* We've reached the bottom of the scrollback */ break; } continue; @@ -411,14 +443,68 @@ execute_binding(struct seat *seat, struct terminal *term, term_damage_view(term); render_refresh(term); - break; + break; } return true; } case BIND_ACTION_UNICODE_INPUT: - unicode_mode_activate(seat); + unicode_mode_activate(term); + return true; + + case BIND_ACTION_QUIT: + term_shutdown(term); + 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: @@ -454,6 +540,11 @@ execute_binding(struct seat *seat, struct terminal *term, 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); @@ -481,14 +572,6 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, * Free old keymap state */ - if (seat->kbd.xkb_compose_state != NULL) { - xkb_compose_state_unref(seat->kbd.xkb_compose_state); - seat->kbd.xkb_compose_state = NULL; - } - if (seat->kbd.xkb_compose_table != NULL) { - xkb_compose_table_unref(seat->kbd.xkb_compose_table); - seat->kbd.xkb_compose_table = NULL; - } if (seat->kbd.xkb_keymap != NULL) { xkb_keymap_unref(seat->kbd.xkb_keymap); seat->kbd.xkb_keymap = NULL; @@ -497,55 +580,40 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, xkb_state_unref(seat->kbd.xkb_state); seat->kbd.xkb_state = NULL; } - if (seat->kbd.xkb != NULL) { - xkb_context_unref(seat->kbd.xkb); - seat->kbd.xkb = 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: - return; + goto err; case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1: break; default: LOG_WARN("unrecognized keymap format: %u", format); - return; + 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"); - close(fd); - return; + goto err; } while (map_str[size - 1] == '\0') size--; - seat->kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - 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); - /* Compose (dead keys) */ - 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) { - LOG_WARN("failed to instantiate compose table; dead keys will not work"); - } else { - seat->kbd.xkb_compose_state = xkb_compose_state_new( - seat->kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); - } } + munmap(map_str, size); + if (seat->kbd.xkb_keymap != NULL) { seat->kbd.xkb_state = xkb_state_new(seat->kbd.xkb_keymap); @@ -556,30 +624,80 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, 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); - seat->kbd.bind_significant = 0; + /* Significant modifiers in the legacy keyboard protocol */ + seat->kbd.legacy_significant = 0; if (seat->kbd.mod_shift != XKB_MOD_INVALID) - seat->kbd.bind_significant |= 1 << seat->kbd.mod_shift; + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_shift; if (seat->kbd.mod_alt != XKB_MOD_INVALID) - seat->kbd.bind_significant |= 1 << seat->kbd.mod_alt; + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_alt; if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) - seat->kbd.bind_significant |= 1 << seat->kbd.mod_ctrl; + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_ctrl; if (seat->kbd.mod_super != XKB_MOD_INVALID) - seat->kbd.bind_significant |= 1 << seat->kbd.mod_super; + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_super; - seat->kbd.kitty_significant = seat->kbd.bind_significant; + /* 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"); } - munmap(map_str, size); - close(fd); - key_binding_load_keymap(wayl->key_binding_manager, seat); + +err: + close(fd); } static void @@ -669,9 +787,17 @@ keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, 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); @@ -831,7 +957,7 @@ UNITTEST const struct key_data *info = keymap_lookup(&term, XKB_KEY_ISO_Left_Tab, MOD_SHIFT | MOD_CTRL); xassert(info != NULL); - xassert(strcmp(info->seq, "\033[27;6;9~") == 0); + xassert(streq(info->seq, "\033[27;6;9~")); } UNITTEST @@ -842,18 +968,19 @@ UNITTEST const struct key_data *info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); xassert(info != NULL); - xassert(strcmp(info->seq, "\033\r") == 0); + 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(strcmp(info->seq, "\033[27;3;13~") == 0); + 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) + xkb_mod_mask_t *consumed, uint32_t key, + bool filter_locked) { if (unlikely(seat->kbd.xkb_state == NULL)) { if (effective != NULL) @@ -863,24 +990,27 @@ get_current_modifiers(const struct seat *seat, } 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; } } } -static xkb_mod_mask_t -get_locked_modifiers(const struct seat *seat) -{ - return xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); -} - struct kbd_ctx { xkb_layout_index_t layout; xkb_keycode_t key; @@ -935,8 +1065,8 @@ legacy_kbd_protocol(struct seat *seat, struct terminal *term, #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); + //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])); @@ -945,24 +1075,24 @@ legacy_kbd_protocol(struct seat *seat, struct terminal *term, if (term->modify_other_keys_2) { /* - * Try to mimic XTerm’s behavior, when holding shift: + * 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 + * - 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-a ('A') emits a CSI + * - Shift-, (';') emits ';' * - Shift-Alt-, (Alt-;) emits a CSI - * - Shift-ö (‘Ö’) emits ‘Ö’ + * - 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.bind_significant) != 0) + if ((ctx->mods & ~shift_mask & seat->kbd.legacy_significant) != 0) modify_other_keys2_in_effect = true; else { @@ -970,9 +1100,9 @@ legacy_kbd_protocol(struct seat *seat, struct terminal *term, seat->kbd.xkb_state, ctx->key); /* - * Get pressed key’s base symbol. - * - for ‘A’ (shift-a), that’s ‘a’ - * - for ‘;’ (shift-,), that’s ‘,’ + * 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( @@ -1105,29 +1235,98 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, if (!report_events && released) return false; - if (composed && released) - return false; - - /* TODO: should we even bother with this, or just say it’s not supported? */ + /* 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_mod_mask_t mods = ctx->mods & seat->kbd.kitty_significant; - const xkb_mod_mask_t consumed = xkb_state_key_get_consumed_mods2( - seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_GTK) & seat->kbd.kitty_significant; - const xkb_mod_mask_t effective = mods & ~consumed; - const xkb_mod_mask_t caps_num = - (seat->kbd.mod_caps != XKB_MOD_INVALID ? 1 << seat->kbd.mod_caps : 0) | - (seat->kbd.mod_num != XKB_MOD_INVALID ? 1 << seat->kbd.mod_num : 0); - 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; - bool is_text = count > 0 && utf32 != NULL && (effective & ~caps_num) == 0; + /* 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 (!iswprint(utf32[i])) { + if (!isc32print(utf32[i])) { is_text = false; break; } @@ -1136,12 +1335,6 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, const bool report_associated_text = (flags & KITTY_KBD_REPORT_ASSOCIATED) && is_text && !released; - /* 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); - if (composing) { /* We never emit anything while composing, *except* modifiers * (and only in report-all-keys-as-escape-codes mode) */ @@ -1154,11 +1347,22 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, if (report_all_as_escapes) goto emit_escapes; - if (effective == 0) { + if ((mods & ~locked & ~consumed) == 0) { switch (sym) { - case XKB_KEY_Return: term_to_slave(term, "\r", 1); return true; - case XKB_KEY_BackSpace: term_to_slave(term, "\x7f", 1); return true; - case XKB_KEY_Tab: term_to_slave(term, "\t", 1); return true; + 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; } } @@ -1185,105 +1389,93 @@ emit_escapes: encoded_mods |= mods & (1 << seat->kbd.mod_num) ? (1 << 7) : 0; encoded_mods++; - int key = -1, alternate = -1, base = -1; + /* + * 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) { - key = info->key; + shifted = info->key; final = info->final; } } else { - /* - * Use keysym (typically its Unicode codepoint value). - * - * 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) - */ - - /* Get the key’s shift level */ - xkb_level_index_t lvl = xkb_state_key_get_level( - seat->kbd.xkb_state, ctx->key, ctx->layout); - - /* And get all modifier combinations that, combined with - * the pressed key, results in the current shift level */ - xkb_mod_mask_t masks[32]; - size_t mask_count = xkb_keymap_key_get_mods_for_level( - seat->kbd.xkb_keymap, ctx->key, ctx->layout, lvl, - masks, ALEN(masks)); - - /* Check modifier combinations - if a combination has - * modifiers not in our set of ‘significant’ modifiers, - * use key sym as-is */ - bool use_level0_sym = true; - for (size_t i = 0; i < mask_count; i++) { - if ((masks[i] & ~seat->kbd.kitty_significant) > 0) { - use_level0_sym = false; - break; - } - } - - xkb_keysym_t sym_to_use = use_level0_sym && ctx->level0_syms.count > 0 - ? ctx->level0_syms.syms[0] - : sym; + /* Use keysym (typically its Unicode codepoint value) */ if (composed) - key = utf32[0]; /* TODO: what if there are multiple codepoints? */ - else { - key = xkb_keysym_to_utf32(sym_to_use); - if (key == 0) - return false; - - /* The *shifted* key. May be the same as the unshifted - * key - if so, this is filtered out below, when - * emitting the CSI */ - alternate = xkb_keysym_to_utf32(sym); - } - - /* 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]); + shifted = utf32[0]; /* TODO: what if there are multiple codepoints? */ + else + shifted = xkb_keysym_to_utf32(sym); final = 'u'; } - if (key < 0) + 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 */ + * ":1" subparameter for key press events */ event[0] = ':'; event[1] = '0' + (pressed ? 1 : repeating ? 2 : 3); event[2] = '\0'; @@ -1294,13 +1486,16 @@ emit_escapes: 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; + bool emit_base = base > 0 && base != key && base != alternate && isc32print(base); if (emit_alternate) { bytes = snprintf(p, left, ":%u", alternate); @@ -1361,6 +1556,34 @@ keysym_is_modifier(xkb_keysym_t keysym) 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, "<none>"); + } +} +#endif + static void key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, uint32_t key, uint32_t state) @@ -1382,6 +1605,9 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, 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); @@ -1404,13 +1630,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, const bool composed = compose_status == XKB_COMPOSE_COMPOSED; xkb_mod_mask_t mods, consumed; - get_current_modifiers(seat, &mods, &consumed, key); - - const xkb_mod_mask_t locked = get_locked_modifiers(seat); - const xkb_mod_mask_t bind_mods - = mods & seat->kbd.bind_significant & ~locked; - const xkb_mod_mask_t bind_consumed = - consumed & seat->kbd.bind_significant & ~locked; + get_current_modifiers(seat, &mods, &consumed, key, true); xkb_layout_index_t layout_idx = xkb_state_key_get_layout(seat->kbd.xkb_state, key); @@ -1424,7 +1644,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, xassert(bindings != NULL); if (pressed) { - if (seat->unicode_mode.active) { + if (term->unicode_mode.active) { unicode_mode_input(seat, term, sym); return; } @@ -1434,7 +1654,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, start_repeater(seat, key); search_input( - seat, term, bindings, key, sym, mods, consumed, locked, + seat, term, bindings, key, sym, mods, consumed, raw_syms, raw_count, serial); return; } @@ -1444,66 +1664,104 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, start_repeater(seat, key); urls_input( - seat, term, bindings, key, sym, mods, consumed, locked, + seat, term, bindings, key, sym, mods, consumed, raw_syms, raw_count, serial); return; } } -#if 0 - for (size_t i = 0; i < 32; i++) { - if (mods & (1 << i)) { - LOG_INFO("%s", xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i)); - } - } -#endif - #if defined(_DEBUG) && defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG char sym_name[100]; xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); - LOG_DBG("%s (%u/0x%x): seat=%s, term=%p, serial=%u, " - "mods=0x%08x, consumed=0x%08x, repeats=%d", - sym_name, sym, sym, seat->name, (void *)term, serial, - mods, consumed, should_repeat); + 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; - /* Match translated symbol */ - if (bind->k.sym == sym && - bind->mods == (bind_mods & ~bind_consumed) && - execute_binding(seat, term, bind, serial)) - { - goto maybe_repeat; - } - - if (bind->mods != bind_mods || bind_mods != (mods & ~locked)) + if (bind->mods != mods || bind->mods == 0) continue; - /* Match untranslated symbols */ for (size_t i = 0; i < raw_count; i++) { if (bind->k.sym == raw_syms[i] && - execute_binding(seat, term, bind, serial)) - { - goto maybe_repeat; - } - } - - /* Match raw key code */ - tll_foreach(bind->k.key_codes, code) { - if (code->item == key && - execute_binding(seat, term, bind, serial)) + 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; + } + } + } + } + + 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; } /* @@ -1573,7 +1831,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, if (utf8 != buf) free(utf8); - if (handled) { + if (handled && !keysym_is_modifier(sym)) { term_reset_view(term); selection_cancel(term); } @@ -1603,8 +1861,25 @@ keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, { struct seat *seat = data; - LOG_DBG("modifiers: depressed=0x%x, latched=0x%x, locked=0x%x, group=%u", - mods_depressed, mods_latched, mods_locked, group); + 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( @@ -1633,6 +1908,398 @@ keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, 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 = <backspace>, 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 = <enter>, 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 = <tab>, 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) @@ -1667,7 +2334,7 @@ is_top_left(const struct terminal *term, int x, int y) { int csd_border_size = term->conf->csd.border_width; return ( - (!term->window->is_tiled_top && !term->window->is_tiled_left) && + (!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))); } @@ -1677,7 +2344,7 @@ is_top_right(const struct terminal *term, int x, int y) { int csd_border_size = term->conf->csd.border_width; return ( - (!term->window->is_tiled_top && !term->window->is_tiled_right) && + (!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))); } @@ -1688,7 +2355,7 @@ 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_tiled_bottom && !term->window->is_tiled_left) && + (!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))); } @@ -1699,28 +2366,96 @@ 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_tiled_bottom && !term->window->is_tiled_right) && + (!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))); } -const char * +enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y) { - if (is_top_left(term, x, y)) return XCURSOR_TOP_LEFT_CORNER; - else if (is_top_right(term, x, y)) return XCURSOR_TOP_RIGHT_CORNER; - else if (is_bottom_left(term, x, y)) return XCURSOR_BOTTOM_LEFT_CORNER; - else if (is_bottom_right(term, x, y)) return XCURSOR_BOTTOM_RIGHT_CORNER; - else if (term->active_surface == TERM_SURF_BORDER_LEFT) return XCURSOR_LEFT_SIDE; - else if (term->active_surface == TERM_SURF_BORDER_RIGHT) return XCURSOR_RIGHT_SIDE; - else if (term->active_surface == TERM_SURF_BORDER_TOP) return XCURSOR_TOP_SIDE; - else if (term->active_surface == TERM_SURF_BORDER_BOTTOM) return XCURSOR_BOTTOM_SIDE; + 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 NULL; + 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, @@ -1733,9 +2468,16 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, } 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; @@ -1751,30 +2493,12 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, xassert(tll_length(seat->mouse.buttons) == 0); - seat->mouse_focus = term; - term->active_surface = term_surface_kind(term, surface); - 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: { - /* - * 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 || x >= term->width - term->margins.right) - seat->mouse.col = -1; - else - seat->mouse.col = (x - term->margins.left) / term->cell_width; - - if (y < term->margins.top || y >= term->height - term->margins.bottom) - seat->mouse.row = -1; - else - seat->mouse.row = (y - term->margins.top) / term->cell_height; - + mouse_coord_pixel_to_cell(seat, term, x, y); break; } @@ -1802,6 +2526,23 @@ 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( @@ -1819,15 +2560,12 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, } /* Reset last-set-xcursor, to ensure we update it on a pointer-enter event */ - seat->pointer.xcursor = NULL; + seat->pointer.shape = CURSOR_SHAPE_NONE; /* Reset mouse state */ seat->mouse.x = seat->mouse.y = 0; seat->mouse.col = seat->mouse.row = 0; - 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)); + 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; @@ -1874,11 +2612,35 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, } } +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; @@ -1887,7 +2649,7 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, * 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). */ + * can't map the enter event to a specific window). */ return; } @@ -1901,16 +2663,42 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, 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: + break; + } + seat->pointer.hidden = false; seat->mouse.x = x; seat->mouse.y = y; term_xcursor_update_for_seat(term, seat); - enum term_surface surf_kind = term->active_surface; - int button = 0; - bool send_to_client = false; - if (tll_length(seat->mouse.buttons) > 0) { const struct button_tracker *tracker = &tll_front(seat->mouse.buttons); surf_kind = tracker->surf_kind; @@ -1920,9 +2708,21 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, 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: @@ -1946,49 +2746,10 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, int old_col = seat->mouse.col; int old_row = seat->mouse.row; - /* - * While the seat's mouse coordinates must always be on the - * grid, or -1, we allow updating the selection even when the - * mouse is outside the grid (could also be outside the - * terminal window). - */ - int selection_col; - int selection_row; + mouse_coord_pixel_to_cell(seat, term, seat->mouse.x, seat->mouse.y); - if (x < term->margins.left) { - seat->mouse.col = -1; - selection_col = 0; - } else if (x >= term->width - term->margins.right) { - seat->mouse.col = -1; - selection_col = term->cols - 1; - } else { - seat->mouse.col = (x - term->margins.left) / term->cell_width; - selection_col = seat->mouse.col; - } - - if (y < term->margins.top) { - seat->mouse.row = -1; - selection_row = 0; - } else if (y >= term->height - term->margins.bottom) { - seat->mouse.row = -1; - selection_row = term->rows - 1; - } else { - seat->mouse.row = (y - term->margins.top) / term->cell_height; - selection_row = seat->mouse.row; - } - - /* - * If client is receiving events (because the button was - * pressed while the cursor was inside the grid area), then - * make sure it receives valid coordinates. - */ - if (send_to_client) { - seat->mouse.col = selection_col; - seat->mouse.row = selection_row; - } - - xassert(seat->mouse.col == -1 || (seat->mouse.col >= 0 && seat->mouse.col < term->cols)); - xassert(seat->mouse.row == -1 || (seat->mouse.row >= 0 && seat->mouse.row < term->rows)); + 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 @@ -1996,7 +2757,7 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, if (cursor_is_on_new_cell) { /* Prevent multiple/different mouse bindings from - * triggering if the mouse has moved “too much” (to + * triggering if the mouse has moved "too much" (to * another cell) */ seat->mouse.count = 0; } @@ -2005,9 +2766,13 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, const bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; enum selection_scroll_direction auto_scroll_direction - = y < term->margins.top ? SELECTION_SCROLL_UP - : y > term->height - term->margins.bottom ? SELECTION_SCROLL_DOWN - : SELECTION_SCROLL_NOT; + = 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); @@ -2016,14 +2781,14 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, if (!term->is_searching) { if (auto_scroll_direction != SELECTION_SCROLL_NOT) { /* - * Start ‘selection auto-scrolling’ + * 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’. + * 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. @@ -2039,14 +2804,18 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, selection_start_scroll_timer( term, 400000000 / (divisor > 0 ? divisor : 1), - auto_scroll_direction, selection_col); + auto_scroll_direction, seat->mouse.col); } - if (term->selection.ongoing && ( - cursor_is_on_new_cell || - term->selection.coords.end.row < 0)) + 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, selection_col, selection_row); + selection_update(term, seat->mouse.col, seat->mouse.row); } } @@ -2092,6 +2861,93 @@ fdm_csd_move(struct fdm *fdm, int fd, int events, void *data) 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) @@ -2102,6 +2958,11 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, 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; @@ -2114,6 +2975,10 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, 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); @@ -2134,7 +2999,7 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, * 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): + * 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 @@ -2166,12 +3031,12 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, * - 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 + * - 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). + * again (seat's button list already contains LMB). * - * Note: I’ve also observed variants of the above + * Note: I've also observed variants of the above */ tll_foreach(seat->mouse.buttons, it) { if (it->item.button == button) { @@ -2215,6 +3080,12 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, } } + 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 @@ -2239,7 +3110,10 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, struct wl_window *win = term->window; /* Toggle maximized state on double-click */ - if (button == BTN_LEFT && seat->mouse.count == 2) { + 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 @@ -2288,15 +3162,8 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, case TERM_SURF_BORDER_RIGHT: case TERM_SURF_BORDER_TOP: case TERM_SURF_BORDER_BOTTOM: { - static const enum xdg_toplevel_resize_edge map[] = { - [TERM_SURF_BORDER_LEFT] = XDG_TOPLEVEL_RESIZE_EDGE_LEFT, - [TERM_SURF_BORDER_RIGHT] = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT, - [TERM_SURF_BORDER_TOP] = XDG_TOPLEVEL_RESIZE_EDGE_TOP, - [TERM_SURF_BORDER_BOTTOM] = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM, - }; - if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) { - enum xdg_toplevel_resize_edge resize_type; + enum xdg_toplevel_resize_edge resize_type = XDG_TOPLEVEL_RESIZE_EDGE_NONE; int x = seat->mouse.x; int y = seat->mouse.y; @@ -2309,22 +3176,54 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, 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 - resize_type = map[term->active_surface]; + else { + if (term->active_surface == TERM_SURF_BORDER_LEFT && + !term->window->is_constrained_left) + { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_LEFT; + } - xdg_toplevel_resize( - term->window->xdg_toplevel, seat->wl_seat, serial, resize_type); + 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 && state == WL_POINTER_BUTTON_STATE_PRESSED) + 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 && state == WL_POINTER_BUTTON_STATE_PRESSED) { + 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 @@ -2333,8 +3232,12 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, break; case TERM_SURF_BUTTON_CLOSE: - if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) + 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: { @@ -2348,86 +3251,11 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, bool consumed = false; if (cursor_is_on_grid && term_mouse_grabbed(term, seat)) { - if (seat->wl_keyboard != NULL && seat->kbd.xkb_state != NULL) { - /* Seat has keyboard - use mouse bindings *with* modifiers */ + const struct key_binding *match = + match_mouse_binding(seat, term, button); - const struct key_binding_set *bindings = key_binding_for( - wayl->key_binding_manager, term->conf, seat); - xassert(bindings != NULL); - - xkb_mod_mask_t mods; - get_current_modifiers(seat, &mods, NULL, 0); - mods &= seat->kbd.bind_significant; - - /* 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; - } - - if (match != NULL) - consumed = execute_binding(seat, term, match, serial); - } - - 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; - } - - const struct config_key_modifiers no_mods = {0}; - if (memcmp(&binding->modifiers, &no_mods, sizeof(no_mods)) != 0) { - /* Binding has modifiers */ - continue; - } - - if (match == NULL || binding->m.count > match->m.count) - match = binding; - } - - if (match != NULL) { - struct key_binding bind = { - .action = match->action, - .aux = &match->aux, - }; - consumed = execute_binding(seat, term, &bind, serial); - } - } + if (match != NULL) + consumed = execute_binding(seat, term, match, serial, 1); } send_to_client = !consumed && cursor_is_on_grid; @@ -2497,31 +3325,20 @@ mouse_scroll(struct seat *seat, int amount, enum wl_pointer_axis axis) xassert(term != NULL); int button = axis == WL_POINTER_AXIS_VERTICAL_SCROLL - ? amount < 0 ? BTN_BACK : BTN_FORWARD + ? 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)) { - if (term->grid == &term->alt) { - if (term->alt_scrolling) { - switch (button) { - case BTN_BACK: - case BTN_FORWARD: - alternate_scroll(seat, amount, button); - break; - } - } - } else { - switch (button) { - case BTN_BACK: - cmd_scrollback_up(term, amount); - break; + seat->mouse.count = 1; - case BTN_FORWARD: - cmd_scrollback_down(term, amount); - break; - } - } + 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) { @@ -2544,7 +3361,7 @@ mouse_scroll(struct seat *seat, int amount, enum wl_pointer_axis axis) } } -static float +static double mouse_scroll_multiplier(const struct terminal *term, const struct seat *seat) { return (term->grid == &term->normal || @@ -2559,6 +3376,9 @@ wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, { struct seat *seat = data; + if (touch_is_active(seat)) + return; + if (seat->mouse.have_discrete) return; @@ -2585,11 +3405,15 @@ wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, static void wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, - uint32_t axis, int32_t discrete) + enum wl_pointer_axis axis, int32_t discrete) { + LOG_DBG("axis_discrete: %d", discrete); struct seat *seat = data; - seat->mouse.have_discrete = true; + if (touch_is_active(seat)) + return; + + seat->mouse.have_discrete = true; int amount = discrete; if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) { @@ -2600,10 +3424,58 @@ wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, 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; } @@ -2619,18 +3491,194 @@ wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, { 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, + .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 index ea488a86..34342bbf 100644 --- a/input.h +++ b/input.h @@ -3,34 +3,38 @@ #include <stdint.h> #include <wayland-client.h> -#include "wayland.h" +#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’ + * 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. + * 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); + uint32_t key, bool filter_locked); -const char *xcursor_for_csd_border(struct terminal *term, int x, int y); +enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y); diff --git a/key-binding.c b/key-binding.c index 1dffd3ee..a2883ed5 100644 --- a/key-binding.c +++ b/key-binding.c @@ -11,14 +11,24 @@ #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; @@ -44,6 +54,50 @@ key_binding_manager_destroy(struct key_binding_manager *mgr) 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) @@ -67,6 +121,7 @@ key_binding_new_for_seat(struct key_binding_manager *mgr, }; 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), @@ -107,6 +162,7 @@ key_binding_new_for_conf(struct key_binding_manager *mgr, }; tll_push_back(mgr->binding_sets, set); + initialize_vmod_mappings(&tll_back(mgr->binding_sets)); load_keymap(&tll_back(mgr->binding_sets)); @@ -243,27 +299,27 @@ maybe_repair_key_combo(const struct seat *seat, * 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 + * 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). + * 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’) + * - "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 + * (shifted) levels where it produces 'sym'. If there are, check * *which* sets of modifiers are needed to produce it, and compare - * with ‘mods’. + * with 'mods'. * - * If there is at least one common modifier, it means ‘sym’ is a - * “shifted” symbol, with the corresponding shifting modifier + * 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’ + * 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 @@ -283,7 +339,7 @@ maybe_repair_key_combo(const struct seat *seat, 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’ */ + /* No unshifted symbols, or unshifted symbol is same as 'sym' */ continue; } @@ -313,7 +369,7 @@ maybe_repair_key_combo(const struct seat *seat, seat->kbd.xkb_keymap, code, layout_idx, level_idx, mod_masks, ALEN(mod_masks)); - /* Check if key combo’s modifier set intersects */ + /* 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; @@ -359,19 +415,19 @@ key_cmp(struct key_binding a, struct key_binding b) * 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. + * 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. + * 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. + * 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 */ @@ -404,6 +460,41 @@ 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, @@ -411,7 +502,8 @@ convert_key_binding(struct key_set *set, { const struct seat *seat = set->seat; - xkb_mod_mask_t mods = conf_modifiers_to_mask(seat, &conf_binding->modifiers); + 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 = { @@ -469,7 +561,7 @@ convert_mouse_binding(struct key_set *set, .type = MOUSE_BINDING, .action = conf_binding->action, .aux = &conf_binding->aux, - .mods = conf_modifiers_to_mask(set->seat, &conf_binding->modifiers), + .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, @@ -509,8 +601,9 @@ load_keymap(struct key_set *set) convert_url_bindings(set); convert_mouse_bindings(set); - set->public.selection_overrides = conf_modifiers_to_mask( - set->seat, &set->conf->mouse.selection_override_modifiers); + set->public.selection_overrides = mods_to_mask( + set->seat, set->vmods, ALEN(set->vmods), + &set->conf->mouse.selection_override_modifiers); } void @@ -520,8 +613,10 @@ key_binding_load_keymap(struct key_binding_manager *mgr, tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; - if (set->seat == seat) + if (set->seat == seat) { + initialize_vmod_mappings(set); load_keymap(set); + } } } diff --git a/key-binding.h b/key-binding.h index f607644f..c4a04e99 100644 --- a/key-binding.h +++ b/key-binding.h @@ -32,6 +32,7 @@ enum bind_action_normal { 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, @@ -39,22 +40,41 @@ enum bind_action_normal { 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, /* 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_UNICODE_INPUT + 1, + BIND_ACTION_KEY_COUNT = BIND_ACTION_THEME_TOGGLE + 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, @@ -69,8 +89,16 @@ enum bind_action_search { 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, diff --git a/kitty-keymap.h b/kitty-keymap.h index eba4923a..3420d01f 100644 --- a/kitty-keymap.h +++ b/kitty-keymap.h @@ -13,7 +13,7 @@ struct kitty_key_data { _Static_assert(sizeof(struct kitty_key_data) == 7, "bad size"); -/* Note! *Must* Be kept sorted (on ‘sym’) */ +/* 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}, @@ -70,7 +70,7 @@ static const struct kitty_key_data kitty_keymap[] = { {XKB_KEY_F1, 1, 'P', false}, {XKB_KEY_F2, 1, 'Q', false}, - {XKB_KEY_F3, 1, 'R', false}, + {XKB_KEY_F3, 13, '~', false}, {XKB_KEY_F4, 1, 'S', false}, {XKB_KEY_F5, 15, '~', false}, {XKB_KEY_F6, 17, '~', false}, diff --git a/log.c b/log.c index 360ca1c0..ebf411ec 100644 --- a/log.c +++ b/log.c @@ -40,7 +40,13 @@ log_init(enum log_colorize _colorize, bool _do_syslog, [LOG_FACILITY_DAEMON] = LOG_DAEMON, }; - colorize = _colorize == LOG_COLORIZE_ALWAYS || (_colorize == LOG_COLORIZE_AUTO && isatty(STDERR_FILENO)); + /* 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; @@ -105,6 +111,9 @@ _sys_log(enum log_class log_class, const char *module, 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; @@ -199,7 +208,7 @@ log_level_from_string(const char *str) return -1; for (int i = 0, n = map_len(); i < n; i++) - if (strcmp(str, log_level_map[i].name) == 0) + if (streq(str, log_level_map[i].name)) return i; return -1; diff --git a/main.c b/main.c index 4af200fd..9db77d0c 100644 --- a/main.c +++ b/main.c @@ -1,8 +1,8 @@ #include <stdlib.h> #include <stdio.h> #include <string.h> -#include <ctype.h> #include <stdbool.h> +#include <limits.h> #include <locale.h> #include <getopt.h> #include <signal.h> @@ -31,12 +31,9 @@ #include "shm.h" #include "terminal.h" #include "util.h" -#include "version.h" #include "xmalloc.h" #include "xsnprintf.h" -#include "char32.h" - #if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__ #error "char32_t does not use UTF-32" #endif @@ -48,17 +45,31 @@ fdm_sigint(struct fdm *fdm, int signo, void *data) return true; } -static const char * -version_and_features(void) +struct sigusr_context { + struct terminal *term; + struct server *server; +}; + +static bool +fdm_sigusr(struct fdm *fdm, int signo, void *data) { - static char buf[256]; - snprintf(buf, sizeof(buf), "version: %s %cpgo %cime %cgraphemes %cassertions", - FOOT_VERSION, - feature_pgo() ? '+' : '-', - feature_ime() ? '+' : '-', - feature_graphemes() ? '+' : '-', - feature_assertions() ? '+' : '-'); - return buf; + 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 @@ -73,9 +84,11 @@ print_usage(const char *prog_name) " -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" @@ -85,7 +98,7 @@ print_usage(const char *prog_name) " -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" + " -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"; @@ -171,11 +184,16 @@ sanitize_signals(void) 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 failiing */ + * itself failing, and the client application failing */ static const int foot_exit_failure = -26; int ret = foot_exit_failure; @@ -198,6 +216,7 @@ main(int argc, char *const *argv) {"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'}, @@ -208,6 +227,7 @@ main(int argc, char *const *argv) {"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'}, @@ -219,21 +239,12 @@ main(int argc, char *const *argv) bool check_config = false; const char *conf_path = NULL; - const char *conf_term = NULL; - const char *conf_title = NULL; - const char *conf_app_id = NULL; const char *custom_cwd = NULL; - bool login_shell = false; - tll(char *) conf_fonts = tll_init(); - enum conf_size_type conf_size_type = CONF_SIZE_PX; - int conf_width = -1; - int conf_height = -1; + const char *pty_path = NULL; bool as_server = false; const char *conf_server_socket_path = NULL; bool presentation_timings = false; bool hold = false; - bool maximized = false; - bool fullscreen = false; bool unlink_pid_file = false; const char *pid_file = NULL; enum log_class log_level = LOG_CLASS_WARNING; @@ -258,23 +269,27 @@ main(int argc, char *const *argv) break; case 'o': - tll_push_back(overrides, optarg); + tll_push_back(overrides, xstrdup(optarg)); break; case 't': - conf_term = optarg; + tll_push_back(overrides, xstrjoin("term=", optarg)); break; case 'L': - login_shell = true; + tll_push_back(overrides, xstrdup("login-shell=yes")); break; case 'T': - conf_title = optarg; + tll_push_back(overrides, xstrjoin("title=", optarg)); break; case 'a': - conf_app_id = optarg; + tll_push_back(overrides, xstrjoin("app-id=", optarg)); + break; + + case TOPLEVEL_TAG_OPTION: + tll_push_back(overrides, xstrjoin("toplevel-tag=", optarg)); break; case 'D': { @@ -287,27 +302,11 @@ main(int argc, char *const *argv) break; } - case 'f': - tll_free_and_free(conf_fonts, free); - for (char *font = strtok(optarg, ","); font != NULL; font = strtok(NULL, ",")) { - - /* Strip leading spaces */ - while (*font != '\0' && isspace(*font)) - font++; - - /* Strip trailing spaces */ - char *end = font + strlen(font); - xassert(*end == '\0'); - end--; - while (end > font && isspace(*end)) - *(end--) = '\0'; - - if (strlen(font) == 0) - continue; - - tll_push_back(conf_fonts, font); - } + case 'f': { + char *font_override = xstrjoin("font=", optarg); + tll_push_back(overrides, font_override); break; + } case 'w': { unsigned width, height; @@ -316,9 +315,9 @@ main(int argc, char *const *argv) return ret; } - conf_size_type = CONF_SIZE_PX; - conf_width = width; - conf_height = height; + tll_push_back( + overrides, xasprintf("initial-window-size-pixels=%ux%u", + width, height)); break; } @@ -329,9 +328,9 @@ main(int argc, char *const *argv) return ret; } - conf_size_type = CONF_SIZE_CELLS; - conf_width = width; - conf_height = height; + tll_push_back( + overrides, xasprintf("initial-window-size-chars=%ux%u", + width, height)); break; } @@ -341,6 +340,10 @@ main(int argc, char *const *argv) conf_server_socket_path = optarg; break; + case PTY_OPTION: + pty_path = optarg; + break; + case 'P': presentation_timings = true; break; @@ -350,13 +353,11 @@ main(int argc, char *const *argv) break; case 'm': - maximized = true; - fullscreen = false; + tll_push_back(overrides, xstrdup("initial-window-mode=maximized")); break; case 'F': - fullscreen = true; - maximized = false; + tll_push_back(overrides, xstrdup("initial-window-mode=fullscreen")); break; case 'p': @@ -378,11 +379,11 @@ main(int argc, char *const *argv) } case 'l': - if (optarg == NULL || strcmp(optarg, "auto") == 0) + if (optarg == NULL || streq(optarg, "auto")) log_colorize = LOG_COLORIZE_AUTO; - else if (strcmp(optarg, "never") == 0) + else if (streq(optarg, "never")) log_colorize = LOG_COLORIZE_NEVER; - else if (strcmp(optarg, "always") == 0) + 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); @@ -395,7 +396,7 @@ main(int argc, char *const *argv) break; case 'v': - printf("foot %s\n", version_and_features()); + print_version_and_features("foot "); return EXIT_SUCCESS; case 'h': @@ -410,6 +411,11 @@ main(int argc, char *const *argv) } } + 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); @@ -418,7 +424,7 @@ main(int argc, char *const *argv) argv += optind; } - LOG_INFO("%s", version_and_features()); + LOG_INFO("%s", version_and_features); { struct utsname name; @@ -438,35 +444,46 @@ main(int argc, char *const *argv) * that does not exist on this system, then the above call may return * NULL. We should just continue with the fallback method below. */ - LOG_WARN("setlocale() failed"); - locale = "C"; + 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); + LOG_INFO("locale: %s", locale != NULL ? locale : "<invalid>"); - bool bad_locale = !locale_is_utf8(); + 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 + * 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) { - LOG_WARN("'%s' is not a UTF-8 locale, using '%s' instead", - locale, fallback_locale); + 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, using '%s' instead", - 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; @@ -474,22 +491,31 @@ main(int argc, char *const *argv) } if (bad_locale) { - LOG_ERR( - "'%s' is not a UTF-8 locale, and failed to find a fallback", - 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", - 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); + &conf, conf_path, &user_notifications, &overrides, check_config, as_server); - tll_free(overrides); + tll_free_and_free(overrides, free); if (!conf_successful) { config_free(&conf); return ret; @@ -508,55 +534,11 @@ main(int argc, char *const *argv) (enum fcft_log_colorize)log_colorize, as_server && log_syslog, (enum fcft_log_class)log_level); - fcft_set_scaling_filter(conf.tweak.fcft_filter); - if (conf_term != NULL) { - free(conf.term); - conf.term = xstrdup(conf_term); - } - if (conf_title != NULL) { - free(conf.title); - conf.title = xstrdup(conf_title); - } - if (conf_app_id != NULL) { - free(conf.app_id); - conf.app_id = xstrdup(conf_app_id); - } - if (login_shell) - conf.login_shell = true; - if (tll_length(conf_fonts) > 0) { - for (size_t i = 0; i < ALEN(conf.fonts); i++) - config_font_list_destroy(&conf.fonts[i]); - - struct config_font_list *font_list = &conf.fonts[0]; - xassert(font_list->count == 0); - xassert(font_list->arr == NULL); - - font_list->arr = xmalloc( - tll_length(conf_fonts) * sizeof(font_list->arr[0])); - - tll_foreach(conf_fonts, it) { - struct config_font font; - if (!config_font_parse(it->item, &font)) { - LOG_ERR("%s: invalid font specification", it->item); - } else - font_list->arr[font_list->count++] = font; - } - tll_free(conf_fonts); - } - if (conf_width > 0 && conf_height > 0) { - conf.size.type = conf_size_type; - conf.size.width = conf_width; - conf.size.height = conf_height; - } if (conf_server_socket_path != NULL) { free(conf.server_socket_path); conf.server_socket_path = xstrdup(conf_server_socket_path); } - if (maximized) - conf.startup_mode = STARTUP_MAXIMIZED; - else if (fullscreen) - conf.startup_mode = STARTUP_FULLSCREEN; conf.presentation_timings = presentation_timings; conf.hold_at_exit = hold; @@ -586,10 +568,10 @@ main(int argc, char *const *argv) char *_cwd = NULL; if (cwd == NULL) { - errno = 0; 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; @@ -606,7 +588,7 @@ main(int argc, char *const *argv) if (resolved_path_cwd != NULL && resolved_path_pwd != NULL && - strcmp(resolved_path_cwd, resolved_path_pwd) == 0) + streq(resolved_path_cwd, resolved_path_pwd)) { /* * The resolved path of $PWD matches the resolved path of @@ -622,6 +604,7 @@ main(int argc, char *const *argv) } 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; @@ -642,7 +625,7 @@ main(int argc, char *const *argv) goto out; if (!as_server && (term = term_init( - &conf, fdm, reaper, wayl, "foot", cwd, token, + &conf, fdm, reaper, wayl, "foot", cwd, token, pty_path, argc, argv, NULL, &term_shutdown_cb, &shutdown_ctx)) == NULL) { goto out; @@ -660,6 +643,17 @@ main(int argc, char *const *argv) 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 || @@ -695,6 +689,8 @@ out: 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); @@ -709,3 +705,22 @@ out: 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 index 6e7e7fcf..a0e602bb 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project('foot', 'c', - version: '1.14.0', + version: '1.26.1', license: 'MIT', - meson_version: '>=0.58.0', + meson_version: '>=0.59.0', default_options: [ 'c_std=c11', 'warning_level=1', @@ -12,34 +12,78 @@ is_debug_build = get_option('buildtype').startswith('debug') cc = meson.get_compiler('c') -if cc.has_function('memfd_create') +# 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 <sys/mman.h>') add_project_arguments('-DMEMFD_CREATE', language: 'c') endif -utempter_path = get_option('default-utempter-path') -if utempter_path == '' - utempter = find_program( - 'utempter', - required: false, - dirs: [join_paths(get_option('prefix'), get_option('libdir'), 'utempter'), - join_paths(get_option('prefix'), get_option('libexecdir'), 'utempter'), - '/usr/lib/utempter', - '/usr/libexec/utempter', - '/lib/utempter'] - ) - if utempter.found() - utempter_path = utempter.full_path() +# Missing on DragonFly, FreeBSD < 14.1 +if cc.has_function('execvpe', + args: ['-D_GNU_SOURCE'], + prefix: '#include <unistd.h>') + add_project_arguments('-DEXECVPE', language: 'c') +endif + +if cc.has_function('sigabbrev_np', + args: ['-D_GNU_SOURCE'], + prefix: '#include <string.h>') + 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 - utempter_path = '' + utmp_backend = 'none' endif -elif utempter_path == 'none' - utempter_path = '' +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')), - '-DFOOT_DEFAULT_UTEMPTER_PATH="@0@"'.format(utempter_path)] + + '-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')]) + @@ -99,7 +143,9 @@ 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') +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') @@ -110,8 +156,12 @@ if utf8proc.found() add_project_arguments('-DFOOT_GRAPHEME_CLUSTERING=1', language: 'c') endif -tllist = dependency('tllist', version: '>=1.0.4', fallback: 'tllist') -fcft = dependency('fcft', version: ['>=3.0.1', '<4.0.0'], fallback: 'fcft') +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') @@ -122,17 +172,30 @@ wscanner_prog = find_program( 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 / '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.21') - add_project_arguments('-DHAVE_XDG_ACTIVATION', language: 'c') - wl_proto_xml += [wayland_protocols_datadir + '/staging/xdg-activation/xdg-activation-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 @@ -167,13 +230,30 @@ builtin_terminfo = custom_target( '@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' + 'xsnprintf.c', 'xsnprintf.h', + dependencies: [utf8proc] ) misc = static_library( @@ -181,20 +261,24 @@ misc = static_library( 'hsl.c', 'hsl.h', 'macros.h', 'misc.c', 'misc.h', - 'uri.c', 'uri.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, wl_proto_src + wl_proto_headers, + builtin_terminfo, srgb_funcs, + wl_proto_src + wl_proto_headers, version, dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc], link_with: [common, misc], @@ -205,11 +289,19 @@ pgolib = static_library( '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', @@ -228,7 +320,7 @@ executable( 'commands.c', 'commands.h', 'extract.c', 'extract.h', 'fdm.c', 'fdm.h', - 'foot-features.h', + 'foot-features.c', 'foot-features.h', 'ime.c', 'ime.h', 'input.c', 'input.h', 'key-binding.c', 'key-binding.h', @@ -247,7 +339,8 @@ executable( 'url-mode.c', 'url-mode.h', 'user-notification.c', 'user-notification.h', 'wayland.c', 'wayland.h', 'shm-formats.h', - wl_proto_src + wl_proto_headers, version, + '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, @@ -256,16 +349,16 @@ executable( executable( 'footclient', 'client.c', 'client-protocol.h', - 'foot-features.h', + 'foot-features.c', 'foot-features.h', 'macros.h', 'util.h', version, - dependencies: [tllist], + dependencies: [tllist, utf8proc], link_with: common, install: true) install_data( - 'org.codeberg.dnkl.foot.desktop', 'org.codeberg.dnkl.foot-server.desktop', 'org.codeberg.dnkl.footclient.desktop', + 'foot.desktop', 'foot-server.desktop', 'footclient.desktop', install_dir: join_paths(get_option('datadir'), 'applications')) systemd = dependency('systemd', required: false) @@ -283,13 +376,13 @@ if systemd.found() or custom_systemd_units_dir != '' configure_file( configuration: configuration, - input: 'foot-server@.service.in', + input: 'foot-server.service.in', output: '@BASENAME@', install_dir: systemd_units_dir ) install_data( - 'foot-server@.socket', + 'foot-server.socket', install_dir: systemd_units_dir) endif @@ -306,11 +399,16 @@ 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': get_option('default-terminfo'), + 'default_terminfo': terminfo_base_name } ) @@ -321,9 +419,9 @@ if tic.found() ) custom_target( 'terminfo', - output: get_option('default-terminfo')[0], + output: terminfo_base_name[0], input: preprocessed, - command: [tic, '-x', '-o', '@OUTDIR@', '-e', '@0@,@0@-direct'.format(get_option('default-terminfo')), '@INPUT@'], + command: [tic, '-x', '-o', '@OUTDIR@', '-e', '@0@,@0@-direct'.format(terminfo_base_name), '@INPUT@'], install: true, install_dir: terminfo_install_location ) @@ -343,8 +441,10 @@ summary( 'Themes': get_option('themes'), 'IME': get_option('ime'), 'Grapheme clustering': utf8proc.found(), - 'Utempter path': utempter_path, + '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') != '', diff --git a/meson_options.txt b/meson_options.txt index c38a8ca8..ab7a07be 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -15,12 +15,15 @@ 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('default-utempter-path', type: 'string', value: '', - description: 'Default path to utempter helper binary. Default: auto-detect') +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 index a81aa9e4..1369df03 100644 --- a/misc.c +++ b/misc.c @@ -1,5 +1,6 @@ #include "misc.h" #include "char32.h" +#include <stdlib.h> bool isword(char32_t wc, bool spaces_only, const char32_t *delimiters) @@ -42,3 +43,21 @@ timespec_sub(const struct timespec *a, const struct timespec *b, 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 index 648bb65f..6c77c484 100644 --- a/misc.h +++ b/misc.h @@ -8,3 +8,5 @@ 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/notify.c b/notify.c index 8180477d..e454b03b 100644 --- a/notify.c +++ b/notify.c @@ -1,9 +1,11 @@ #include "notify.h" +#include <errno.h> #include <stdlib.h> #include <string.h> #include <unistd.h> +#include <sys/epoll.h> #include <sys/stat.h> #include <fcntl.h> @@ -12,43 +14,574 @@ #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_notify(const struct terminal *term, const char *title, const char *body) +notify_free(struct terminal *term, struct notification *notif) { - LOG_DBG("notify: title=\"%s\", msg=\"%s\"", title, body); + if (notif->pid > 0) + fdm_del(term->fdm, notif->stdout_fd); - if (term->conf->notify_focus_inhibit && term->kbd_focus) { - /* No notifications while we’re focused */ - return; + 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); } - if (title == NULL || body == NULL) - return; + memset(notif, 0, sizeof(*notif)); +} - if (term->conf->notify.argv.args == NULL) +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 : "<unset>"); + + 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->notify, 4, - (const char *[]){"app-id", "window-title", "title", "body"}, - (const char *[]){term->conf->app_id, term->window_title, title, body}, - &argc, &argv)) + &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; + 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); - spawn(term->reaper, NULL, argv, devnull, -1, -1, NULL); + 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); @@ -56,4 +589,177 @@ notify_notify(const struct terminal *term, const char *title, const char *body) 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 <path>). + * + * 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 index ce60562f..89b51238 100644 --- a/notify.h +++ b/notify.h @@ -1,6 +1,95 @@ #pragma once +#include <stdbool.h> +#include <stdint.h> +#include <unistd.h> -#include "terminal.h" +#include <tllist.h> -void notify_notify( - const struct terminal *term, const char *title, const char *body); +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/org.codeberg.dnkl.foot.metainfo.xml b/org.codeberg.dnkl.foot.metainfo.xml deleted file mode 100644 index 1c0b7985..00000000 --- a/org.codeberg.dnkl.foot.metainfo.xml +++ /dev/null @@ -1,45 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<component type="desktop-application"> - <id>org.codeberg.dnkl.foot</id> - <metadata_license>MIT</metadata_license> - <project_license>MIT</project_license> - <developer_name>dnkl</developer_name> - <name>foot</name> - <summary>The fast, lightweight and minimalistic Wayland terminal emulator.</summary> - <description> - <ul> - <li>Fast</li> - <li>Lightweight, in dependencies, on-disk and in-memory</li> - <li>Wayland native</li> - <li>DE agnostic</li> - <li>Server/daemon mode</li> - <li>User configurable font fallback</li> - <li>On-the-fly font resize</li> - <li>On-the-fly DPI font size adjustment</li> - <li>Scrollback search</li> - <li>Keyboard driven URL detection</li> - <li>Color emoji support</li> - <li>IME (via text-input-v3)</li> - <li>Multi-seat</li> - <li>True Color (24bpp)</li> - <li>Synchronized Updates support</li> - <li>Sixel image support</li> - </ul> - </description> - <screenshots> - <screenshot type="default"> - <caption>Foot with sixel graphics</caption> - <image>https://codeberg.org/dnkl/foot/media/branch/master/doc/sixel-wow.png</image> - </screenshot> - </screenshots> - <releases> - <release version="1.13.1" date="2022-08-31"> - </release> - <release version="1.13.0" date="2022-08-07"> - </release> - </releases> - <launchable type="desktop-id">org.codeberg.dnkl.foot.desktop</launchable> - <url type="homepage">https://codeberg.org/dnkl/foot</url> - <url type="bugtracker">https://codeberg.org/dnkl/foot/issues</url> - <content_rating type="oars-1.1"/> -</component> diff --git a/osc.c b/osc.c index 55cfcf84..82793fb5 100644 --- a/osc.c +++ b/osc.c @@ -5,20 +5,19 @@ #include <ctype.h> #include <errno.h> +#include <sys/epoll.h> + #define LOG_MODULE "osc" #define LOG_ENABLE_DBG 0 #include "log.h" #include "base64.h" #include "config.h" -#include "grid.h" #include "macros.h" #include "notify.h" -#include "render.h" #include "selection.h" #include "terminal.h" #include "uri.h" #include "util.h" -#include "vt.h" #include "xmalloc.h" #include "xsnprintf.h" @@ -65,17 +64,28 @@ osc_to_clipboard(struct terminal *term, const char *target, return; } - char *decoded = base64_decode(base64_data); - if (decoded == NULL) { - if (errno == EINVAL) - LOG_WARN("OSC: invalid clipboard data: %s", base64_data); - else - LOG_ERRNO("base64_decode() failed"); + 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; } @@ -124,7 +134,7 @@ from_clipboard_cb(char *text, size_t size, void *user) xassert(chunk != NULL); xassert(strlen(chunk) == 4); - term_to_slave(term, chunk, 4); + term_paste_data_to_slave(term, chunk, 4); free(chunk); ctx->idx = 0; @@ -144,7 +154,7 @@ from_clipboard_cb(char *text, size_t size, void *user) char *chunk = base64_encode((const uint8_t *)t, left / 3 * 3); xassert(chunk != NULL); xassert(strlen(chunk) % 4 == 0); - term_to_slave(term, chunk, strlen(chunk)); + term_paste_data_to_slave(term, chunk, strlen(chunk)); free(chunk); } @@ -157,13 +167,19 @@ from_clipboard_done(void *user) if (ctx->idx > 0) { char res[4]; base64_encode_final(ctx->buf, ctx->idx, res); - term_to_slave(term, res, 4); + term_paste_data_to_slave(term, res, 4); } if (term->vt.osc.bel) - term_to_slave(term, "\a", 1); + term_paste_data_to_slave(term, "\a", 1); else - term_to_slave(term, "\033\\", 2); + 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); } @@ -185,6 +201,13 @@ osc_from_clipboard(struct terminal *term, const char *source) 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'; @@ -214,21 +237,36 @@ osc_from_clipboard(struct terminal *term, const char *source) if (!from_clipboard && !from_primary) return; - term_to_slave(term, "\033]52;", 5); - term_to_slave(term, &src, 1); - term_to_slave(term, ";", 1); + 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, &from_clipboard_cb, &from_clipboard_done, ctx); + seat, term, true, &from_clipboard_cb, &from_clipboard_done, ctx); } if (from_primary) { text_from_primary( - seat, term, &from_clipboard_cb, &from_clipboard_done, ctx); + seat, term, true, &from_clipboard_cb, &from_clipboard_done, ctx); } } @@ -353,7 +391,7 @@ parse_rgb(const char *string, uint32_t *color, bool *_have_alpha, return false; } - /* Verify prefix is “rgb:” or “rgba:” */ + /* Verify prefix is "rgb:" or "rgba:" */ if (have_alpha) { if (strncmp(string, "rgba:", 5) != 0) return false; @@ -426,7 +464,7 @@ osc_set_pwd(struct terminal *term, char *string) return; } - if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { + if (streq(scheme, "file") && hostname_is_localhost(host)) { LOG_DBG("OSC7: pwd: %s", path); free(term->cwd); term->cwd = path; @@ -443,9 +481,9 @@ osc_uri(struct terminal *term, char *string) /* * \E]8;<params>;URI\e\\ * - * Params are key=value pairs, separated by ‘:’. + * Params are key=value pairs, separated by ':'. * - * The only defined key (as of 2020-05-31) is ‘id’, which is used + * The only defined key (as of 2020-05-31) is 'id', which is used * to group split-up URIs: * * ╔═ file1 ════╗ @@ -475,7 +513,7 @@ osc_uri(struct terminal *term, char *string) key_value = strtok_r(NULL, ":", &ctx)) { const char *key = key_value; - char *operator = strchr(key_value, '='); + char *operator = (char *)strchr(key_value, '='); if (operator == NULL) continue; @@ -483,16 +521,18 @@ osc_uri(struct terminal *term, char *string) const char *value = operator + 1; - if (strcmp(key, "id") == 0) + if (streq(key, "id")) id = sdbm_hash(value); } - LOG_DBG("OSC-8: URL=%s, id=%" PRIu64, uri, id); - if (uri[0] == '\0') + if (uri[0] == '\0') { + LOG_DBG("OSC-8: close"); term_osc8_close(term); - else + } else { + LOG_DBG("OSC-8: URL=%s, id=%" PRIu64, uri, id); term_osc8_open(term, id, uri); + } } static void @@ -524,7 +564,707 @@ osc_notify(struct terminal *term, char *string) const char *title = strtok_r(string, ";", &ctx); const char *msg = strtok_r(NULL, "\x00", &ctx); - notify_notify(term, title, msg != NULL ? msg : ""); + 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 @@ -556,9 +1296,16 @@ osc_dispatch(struct terminal *term) char *string = (char *)&term->vt.osc.data[data_ofs]; switch (param) { - case 0: term_set_window_title(term, string); break; /* icon + title */ - case 1: break; /* icon */ - case 2: term_set_window_title(term, string); break; /* title */ + 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<idx> */ @@ -615,47 +1362,7 @@ osc_dispatch(struct terminal *term) idx, term->colors.table[idx], color); term->colors.table[idx] = color; - - /* Dirty visible, affected cells */ - for (int r = 0; r < term->rows; r++) { - struct row *row = grid_row_in_view(term->grid, r); - struct cell *cell = &row->cells[0]; - - for (int c = 0; c < term->cols; c++, cell++) { - bool dirty = false; - - switch (cell->attrs.fg_src) { - case COLOR_BASE16: - case COLOR_BASE256: - if (cell->attrs.fg == idx) - dirty = true; - break; - - case COLOR_DEFAULT: - case COLOR_RGB: - /* Not affected */ - break; - } - - switch (cell->attrs.bg_src) { - case COLOR_BASE16: - case COLOR_BASE256: - if (cell->attrs.bg == idx) - dirty = true; - break; - - case COLOR_DEFAULT: - case COLOR_RGB: - /* Not affected */ - break; - } - - if (dirty) { - cell->attrs.clean = 0; - row->dirty = true; - } - } - } + term_damage_color(term, COLOR_BASE256, idx); } } @@ -671,20 +1378,42 @@ osc_dispatch(struct terminal *term) osc_uri(term, string); break; - case 9: + 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: - case 11: - case 17: - case 19: { + 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 : term->colors.bg; + 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; @@ -718,6 +1447,7 @@ osc_dispatch(struct terminal *term) 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); @@ -725,67 +1455,43 @@ osc_dispatch(struct terminal *term) 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) - term->colors.alpha = alpha; + 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; - term->colors.use_custom_selection = true; break; case 19: term->colors.selection_fg = color; - term->colors.use_custom_selection = true; break; } - term_damage_view(term); - term_damage_margins(term); break; } - case 12: /* Set cursor color */ - - /* Client queried for current value */ - if (string[0] == '?' && string[1] == '\0') { - uint8_t r = (term->cursor_color.cursor >> 16) & 0xff; - uint8_t g = (term->cursor_color.cursor >> 8) & 0xff; - uint8_t b = (term->cursor_color.cursor >> 0) & 0xff; - const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; - - char reply[32]; - size_t n = xsnprintf( - reply, sizeof(reply), "\033]12;rgb:%02x/%02x/%02x%s", - r, g, b, terminator); - - term_to_slave(term, reply, n); - break; - } - - uint32_t color; - - if (string[0] == '#' || string[0] == '[' - ? !parse_legacy_color(string, &color, NULL, NULL) - : !parse_rgb(string, &color, NULL, NULL)) - { - break; - } - - LOG_DBG("change cursor color to %06x", color); - - if (color == 0) - term->cursor_color.cursor = 0; /* Invert fg/bg */ - else - term->cursor_color.cursor = 1u << 31 | color; - - term_damage_cursor(term); - break; - case 22: /* Set mouse cursor */ term_set_user_mouse_cursor(term, string); break; @@ -797,13 +1503,23 @@ osc_dispatch(struct terminal *term) 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"); - for (size_t i = 0; i < ALEN(term->colors.table); i++) - term->colors.table[i] = term->conf->colors.table[i]; + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); + term_damage_view(term); } else { @@ -824,12 +1540,11 @@ osc_dispatch(struct terminal *term) } LOG_DBG("resetting color #%u", idx); - term->colors.table[idx] = term->conf->colors.table[idx]; + term->colors.table[idx] = theme->table[idx]; + term_damage_color(term, COLOR_BASE256, idx); } } - - term_damage_view(term); break; } @@ -838,36 +1553,62 @@ osc_dispatch(struct terminal *term) case 110: /* Reset default text foreground color */ LOG_DBG("resetting foreground color"); - term->colors.fg = term->conf->colors.fg; - term_damage_view(term); + + 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 */ + case 111: { /* Reset default text background color */ LOG_DBG("resetting background color"); - term->colors.bg = term->conf->colors.bg; - term->colors.alpha = term->conf->colors.alpha; - term_damage_view(term); + + 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: + case 112: { LOG_DBG("resetting cursor color"); - term->cursor_color.text = term->conf->cursor.color.text; - term->cursor_color.cursor = term->conf->cursor.color.cursor; + + 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: + case 117: { LOG_DBG("resetting selection background color"); - term->colors.selection_bg = term->conf->colors.selection_bg; - term->colors.use_custom_selection = term->conf->colors.use_custom.selection; - break; - case 119: - LOG_DBG("resetting selection foreground color"); - term->colors.selection_fg = term->conf->colors.selection_fg; - term->colors.use_custom_selection = term->conf->colors.use_custom.selection; + 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: /* @@ -886,7 +1627,7 @@ osc_dispatch(struct terminal *term) term->grid->cursor.point.row, term->grid->cursor.point.col); - term->grid->cur_row->prompt_marker = true; + term->grid->cur_row->shell_integration.prompt_marker = true; break; case 'B': @@ -894,15 +1635,41 @@ osc_dispatch(struct terminal *term) break; case 'C': - LOG_DBG("FTCS_COMMAND_EXECUTED"); + 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"); + 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; diff --git a/pgo/full-headless-cage.sh b/pgo/full-headless-cage.sh index eacb0c33..50fc7509 100755 --- a/pgo/full-headless-cage.sh +++ b/pgo/full-headless-cage.sh @@ -10,5 +10,5 @@ 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 +# 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.sh b/pgo/full-headless-sway.sh index 48dbcb94..8f6812b3 100755 --- a/pgo/full-headless-sway.sh +++ b/pgo/full-headless-sway.sh @@ -17,8 +17,8 @@ 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}" +# 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 +# Sway's exit code doesn't reflect our script's exit code [ -f "${blddir}"/pgo-ok ] || exit 1 diff --git a/pgo/pgo.c b/pgo/pgo.c index b41b5850..4ff4111c 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -60,7 +60,8 @@ fdm_event_del(struct fdm *fdm, int fd, int events) } bool -render_resize_force(struct terminal *term, int width, int height) +render_resize( + struct terminal *term, int width, int height, uint8_t resize_options) { return true; } @@ -68,6 +69,12 @@ render_resize_force(struct terminal *term, int width, int height) 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_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) @@ -76,15 +83,15 @@ render_xcursor_is_valid(const struct seat *seat, const char *cursor) } bool -render_xcursor_set(struct seat *seat, struct terminal *term, const char *xcursor) +render_xcursor_set(struct seat *seat, struct terminal *term, enum cursor_shape shape) { return true; } -const char * +enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y) { - return XCURSOR_LEFT_PTR; + return CURSOR_SHAPE_LEFT_PTR; } struct wl_window * @@ -94,14 +101,17 @@ wayl_win_init(struct terminal *term, const char *token) } 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; } -bool +pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[], int stdin_fd, int stdout_fd, int stderr_fd, - const char *xdg_activation_token) + reaper_cb cb, void *cb_data, const char *xdg_activation_token) { - return true; + return 2; } pid_t @@ -120,6 +130,12 @@ 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) { @@ -147,8 +163,36 @@ 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_notify(const struct terminal *term, const char *title, const char *body) +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) { } @@ -159,9 +203,13 @@ 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 wl_shm *shm, bool scrollable, size_t pix_instances) +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; } @@ -171,7 +219,8 @@ 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) {} + xkb_mod_mask_t *consumed, uint32_t key, + bool filter_locked) {} static struct key_binding_set kbd; static bool kbd_initialized = false; diff --git a/pgo/pgo.sh b/pgo/pgo.sh index 2f409268..24597c82 100755 --- a/pgo/pgo.sh +++ b/pgo/pgo.sh @@ -54,13 +54,10 @@ case ${mode} in ;; auto) - # TODO: once Sway 1.6.2 has been released, prefer - # full-headless-sway - if [ -n "${WAYLAND_DISPLAY+x}" ]; then mode=full-current-session - # elif command -v sway > /dev/null; then # Requires 1.6.2 - # mode=full-headless-sway + elif command -v sway > /dev/null; then + mode=full-headless-sway elif command -v cage > /dev/null; then mode=full-headless-cage else @@ -82,6 +79,7 @@ set -x # echo "CFLAGS: ${CFLAGS}" export CFLAGS +export CCACHE_DISABLE=1 meson setup --buildtype=release -Db_lto=true "${@}" "${blddir}" "${srcdir}" if [ ${do_pgo} = yes ]; then @@ -97,11 +95,12 @@ if [ ${do_pgo} = yes ]; then 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. + # 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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..f5fc08a2 --- /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 index e4fe4a1f..67cb587e 100644 --- a/quirks.c +++ b/quirks.c @@ -66,3 +66,21 @@ quirk_weston_csd_off(struct terminal *term) 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/render.c b/render.c index f16898b4..c47133b3 100644 --- a/render.c +++ b/render.c @@ -1,9 +1,9 @@ #include "render.h" -#include <string.h> -#include <wctype.h> -#include <unistd.h> +#include <limits.h> #include <signal.h> +#include <string.h> +#include <unistd.h> #include <sys/ioctl.h> #include <sys/time.h> @@ -13,15 +13,16 @@ #include "macros.h" #if HAS_INCLUDE(<pthread_np.h>) -#include <pthread_np.h> -#define pthread_setname_np(thread, name) (pthread_set_name_np(thread, name), 0) + #include <pthread_np.h> + #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) + #define pthread_setname_np(thread, name) pthread_setname_np(thread, "%s", (void *)name) #endif +#include <presentation-time.h> #include <wayland-cursor.h> #include <xdg-shell.h> -#include <presentation-time.h> +#include <xdg-toplevel-icon-v1.h> #include <fcft/fcft.h> @@ -31,14 +32,15 @@ #include "box-drawing.h" #include "char32.h" #include "config.h" +#include "cursor-shape.h" #include "grid.h" -#include "hsl.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" @@ -227,58 +229,95 @@ attrs_to_font(const struct terminal *term, const struct attributes *attrs) return term->fonts[idx]; } -static inline pixman_color_t -color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha) +static pixman_color_t +color_hex_to_pixman_srgb(uint32_t color, uint16_t alpha) { return (pixman_color_t){ - .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)) * alpha / 0xffff, - .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)) * alpha / 0xffff, - .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)) * alpha / 0xffff, - .alpha = alpha, + .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(uint32_t color) +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); + 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_decrease_luminance(uint32_t color) +color_blend_towards(uint32_t from, uint32_t to, float amount) { - uint32_t alpha = color & 0xff000000; - int hue, sat, lum; - rgb_to_hsl(color, &hue, &sat, &lum); - return alpha | hsl_to_rgb(hue, sat, lum / 1.5); + 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.use_custom.dim; + const uint8_t custom_dim = conf->colors_dark.use_custom.dim; - if (likely(custom_dim == 0)) - return color_decrease_luminance(color); + if (unlikely(custom_dim != 0)) { + for (size_t i = 0; i < 8; i++) { + if (((custom_dim >> i) & 1) == 0) + continue; - 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]; + } - if (term->colors.table[0 + i] == color) { - /* “Regular” color, return the corresponding “dim” */ - return conf->colors.dim[i]; - } - - else if (term->colors.table[8 + i] == color) { - /* “Bright” color, return the corresponding “regular” */ - return term->colors.table[i]; + else if (term->colors.table[8 + i] == color) { + /* "Bright" color, return the corresponding "regular" */ + return term->colors.table[i]; + } } } - return color_decrease_luminance(color); + 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 @@ -296,22 +335,14 @@ color_brighten(const struct terminal *term, uint32_t color) return color; } - int hue, sat, lum; - rgb_to_hsl(color, &hue, &sat, &lum); - return hsl_to_rgb(hue, sat, min(100, lum * 1.3)); -} - -static inline int -font_baseline(const struct terminal *term) -{ - return term->font_y_ofs + term->fonts[0]->ascent; + return color_blend_towards(color, 0x00ffffff, term->conf->bold_in_bright.amount); } static void -draw_unfocused_block(const struct terminal *term, pixman_image_t *pix, - const pixman_color_t *color, int x, int y, int cell_cols) +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 = term->scale; + const int scale = (int)roundf(term->scale); const int width = min(min(scale, term->cell_width), term->cell_height); pixman_image_fill_rectangles( @@ -329,7 +360,7 @@ 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 + font_baseline(term) - term->fonts[0]->ascent; + int baseline = y + term->font_baseline - term->fonts[0]->ascent; pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){ @@ -341,7 +372,7 @@ draw_beam_cursor(const struct terminal *term, pixman_image_t *pix, static int underline_offset(const struct terminal *term, const struct fcft_font *font) { - return font_baseline(term) - + return term->font_baseline - (term->conf->use_custom_underline_offset ? -term_pt_or_px_as_pixels(term, &term->conf->underline_offset) : font->underline.position); @@ -387,45 +418,208 @@ draw_underline(const struct terminal *term, pixman_image_t *pix, 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 + font_baseline(term) - font->strikeout.position, - cols * term->cell_width, font->strikeout.thickness}); + 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) + const pixman_color_t *fg, const pixman_color_t *bg, + pixman_color_t *cursor_color, pixman_color_t *text_color, + bool gamma_correct) { - bool is_selected = cell->attrs.selected; - - if (term->cursor_color.cursor >> 31) { - *cursor_color = color_hex_to_pixman(term->cursor_color.cursor); - *text_color = color_hex_to_pixman( - term->cursor_color.text >> 31 - ? term->cursor_color.text : term->colors.bg); - - if (cell->attrs.reverse ^ is_selected) { - pixman_color_t swap = *cursor_color; - *cursor_color = *text_color; - *text_color = swap; - } - } else { + if (term->colors.cursor_bg >> 31) + *cursor_color = color_hex_to_pixman(term->colors.cursor_bg, gamma_correct); + else *cursor_color = *fg; - *text_color = *bg; - if (unlikely(text_color->alpha != 0xffff)) { - /* The *only* color that can have transparency is the - * default background color */ - *text_color = color_hex_to_pixman(term->colors.bg); - } + 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); } } @@ -436,14 +630,28 @@ draw_cursor(const struct terminal *term, const struct cell *cell, { pixman_color_t cursor_color; pixman_color_t text_color; - cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &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 (unlikely(!term->kbd_focus)) - draw_unfocused_block(term, pix, &cursor_color, x, y, cols); - - else if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) { + 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, @@ -466,12 +674,18 @@ draw_cursor(const struct terminal *term, const struct cell *cell, 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, - struct row *row, int col, int row_no, bool has_cursor) + 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) @@ -485,62 +699,140 @@ render_cell(struct terminal *term, pixman_image_t *pix, const int x = term->margins.left + col * width; const int y = term->margins.top + row_no * height; - bool is_selected = cell->attrs.selected; - 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; + } - if (is_selected && term->colors.use_custom_selection) { - _fg = term->colors.selection_fg; - _bg = term->colors.selection_bg; } else { - /* 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 (cell->attrs.reverse ^ is_selected) { + if (unlikely(cell->attrs.reverse)) { uint32_t swap = _fg; _fg = _bg; _bg = swap; - } else if (cell->attrs.bg_src == COLOR_DEFAULT) - alpha = term->colors.alpha; - } + } - if (unlikely(is_selected && _fg == _bg)) { - /* Invert bg when selected/highlighted text has same fg/bg */ - _bg = ~_bg; - alpha = 0xffff; + 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) @@ -549,10 +841,11 @@ render_cell(struct terminal *term, pixman_image_t *pix, _fg = color_brighten(term, _fg); if (cell->attrs.blink && term->blink.state == BLINK_OFF) - _fg = color_decrease_luminance(_fg); + _fg = color_blend_towards(_fg, 0x00000000, term->conf->dim.amount); - pixman_color_t fg = color_hex_to_pixman(_fg); - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); + 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; @@ -581,7 +874,15 @@ render_cell(struct terminal *term, pixman_image_t *pix, * Note, the full range is U+1FB00 - U+1FBF9 */ (base >= GLYPH_LEGACY_FIRST && - base <= GLYPH_LEGACY_LAST)) && + 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)) { @@ -593,6 +894,10 @@ render_cell(struct terminal *term, pixman_image_t *pix, 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; @@ -637,11 +942,16 @@ render_cell(struct terminal *term, pixman_image_t *pix, } if (grapheme != NULL) { - cell_cols = composed->width; + 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); } } @@ -658,7 +968,9 @@ render_cell(struct terminal *term, pixman_image_t *pix, } else { glyph_count = 1; glyphs = &single; - cell_cols = single->cols; + + const size_t forced_width = composed != NULL ? composed->forced_width : 0; + cell_cols = forced_width > 0 ? forced_width : single->cols; } } } @@ -698,6 +1010,12 @@ render_cell(struct terminal *term, pixman_image_t *pix, &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 */ @@ -712,8 +1030,10 @@ render_cell(struct terminal *term, pixman_image_t *pix, mtx_unlock(&term->render.workers.lock); } - if (unlikely(has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus)) - draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols); + 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)) @@ -734,30 +1054,30 @@ render_cell(struct terminal *term, pixman_image_t *pix, int g_x = glyph->x; int g_y = glyph->y; - if (i > 0 && glyph->x >= 0) + if (i > 0 && glyph->x >= 0 && cell_cols == 1) g_x -= term->cell_width; - if (unlikely(pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8)) { + 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 + font_baseline(term) - g_y, + 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 + font_baseline(term) - g_y, + 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 i = 1; i < composed->count; i++) { + for (size_t j = 1; j < composed->count; j++) { const struct fcft_glyph *g = fcft_rasterize_char_utf32( - font, composed->chars[i], term->font_subpixel); + font, composed->chars[j], term->font_subpixel); if (g == NULL) continue; @@ -779,63 +1099,112 @@ render_cell(struct terminal *term, pixman_image_t *pix, * somewhat deal with double-width glyphs we use * an offset of *one* cell. */ - int x_ofs = g->x < 0 - ? cell_cols * term->cell_width - : (cell_cols - 1) * term->cell_width; + 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 + x_ofs + g->x, - y + font_baseline(term) - g->y, - g->width, g->height); + pen_x + letter_x_ofs + x_ofs + g->x, + y + term->font_baseline - g->y, g->width, g->height); } } } - pen_x += glyph->advance.x; + pen_x += cell_cols > 1 ? term->cell_width : glyph->advance.x; } pixman_image_unref(clr_pix); /* Underline */ - if (cell->attrs.underline) - draw_underline(term, pix, font, &fg, x, y, cell_cols); + 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)) { pixman_color_t url_color = color_hex_to_pixman( - term->conf->colors.use_custom.url - ? term->conf->colors.url - : term->colors.table[3] - ); + term->conf->colors_dark.use_custom.url + ? term->conf->colors_dark.url + : term->colors.table[3], + gamma_correct); draw_underline(term, pix, font, &url_color, x, y, cell_cols); } draw_cursor: - if (has_cursor && (term->cursor_style != CURSOR_BLOCK || !term->kbd_focus)) - draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols); + 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, struct row *row, +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, row, col, row_no, cursor_col == 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); + 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)); @@ -866,8 +1235,16 @@ render_margin(struct terminal *term, struct buffer *buf, const int bmargin = term->height - term->margins.bottom; const int line_count = end_line - start_line; - uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, term->colors.alpha); + 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, @@ -893,33 +1270,33 @@ render_margin(struct terminal *term, struct buffer *buf, /* Ensure the updated regions are copied to the next frame's * buffer when we're double buffering */ pixman_region32_union_rect( - &buf->dirty, &buf->dirty, 0, 0, term->width, term->margins.top); + &buf->dirty[0], &buf->dirty[0], 0, 0, term->width, term->margins.top); pixman_region32_union_rect( - &buf->dirty, &buf->dirty, 0, bmargin, term->width, term->margins.bottom); + &buf->dirty[0], &buf->dirty[0], 0, bmargin, term->width, term->margins.bottom); pixman_region32_union_rect( - &buf->dirty, &buf->dirty, 0, 0, term->margins.left, term->height); + &buf->dirty[0], &buf->dirty[0], 0, 0, term->margins.left, term->height); pixman_region32_union_rect( - &buf->dirty, &buf->dirty, + &buf->dirty[0], &buf->dirty[0], rmargin, 0, term->margins.right, term->height); if (apply_damage) { /* Top */ wl_surface_damage_buffer( - term->window->surface, 0, 0, term->width, term->margins.top); + term->window->surface.surf, 0, 0, term->width, term->margins.top); /* Bottom */ wl_surface_damage_buffer( - term->window->surface, 0, bmargin, term->width, term->margins.bottom); + term->window->surface.surf, 0, bmargin, term->width, term->margins.bottom); /* Left */ wl_surface_damage_buffer( - term->window->surface, + 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, + term->window->surface.surf, rmargin, term->margins.top + start_line * term->cell_height, term->margins.right, line_count * term->cell_height); } @@ -1027,15 +1404,15 @@ grid_render_scroll(struct terminal *term, struct buffer *buf, #endif wl_surface_damage_buffer( - term->window->surface, term->margins.left, dst_y, + 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() + * last frame's damage (see reapply_old_damage() */ pixman_region32_union_rect( - &buf->dirty, &buf->dirty, 0, dst_y, buf->width, height); + &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); } static void @@ -1104,19 +1481,20 @@ grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, #endif wl_surface_damage_buffer( - term->window->surface, term->margins.left, dst_y, + 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() + * last frame's damage (see reapply_old_damage() */ pixman_region32_union_rect( - &buf->dirty, &buf->dirty, 0, dst_y, buf->width, height); + &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); } static void -render_sixel_chunk(struct terminal *term, pixman_image_t *pix, const struct sixel *sixel, +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 */ @@ -1153,13 +1531,19 @@ render_sixel_chunk(struct terminal *term, pixman_image_t *pix, const struct sixe x, y, width, height); - wl_surface_damage_buffer(term->window->surface, 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, - const struct coord *cursor, const struct sixel *sixel) + 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; @@ -1171,7 +1555,7 @@ render_sixel(struct terminal *term, pixman_image_t *pix, #define maybe_emit_sixel_chunk_then_reset() \ if (chunk_row_count != 0) { \ render_sixel_chunk( \ - term, pix, sixel, \ + term, pix, damage, sixel, \ chunk_term_start, chunk_img_start, chunk_row_count); \ chunk_term_start = chunk_img_start = -1; \ chunk_row_count = 0; \ @@ -1242,14 +1626,13 @@ render_sixel(struct terminal *term, pixman_image_t *pix, * 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 + * 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 */ - int cursor_col = cursor->row == term_row_no ? cursor->col : -1; - render_row(term, pix, row, term_row_no, cursor_col); + 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); @@ -1264,7 +1647,7 @@ render_sixel(struct terminal *term, pixman_image_t *pix, if ((last_row_needs_erase && last_row) || (last_col_needs_erase && last_col)) { - render_cell(term, pix, row, col, term_row_no, cursor_col == col); + render_cell(term, pix, damage, row, term_row_no, col, cursor_col == col); } else { cell->attrs.clean = 1; cell->attrs.confined = 1; @@ -1288,7 +1671,7 @@ render_sixel(struct terminal *term, pixman_image_t *pix, static void render_sixel_images(struct terminal *term, pixman_image_t *pix, - const struct coord *cursor) + pixman_region32_t *damage, const struct coord *cursor) { if (likely(tll_length(term->grid->sixel_images)) == 0) return; @@ -1324,7 +1707,8 @@ render_sixel_images(struct terminal *term, pixman_image_t *pix, break; } - render_sixel(term, pix, cursor, &it->item); + sixel_sync_cache(term, &it->item); + render_sixel(term, pix, damage, cursor, &it->item); } } @@ -1339,6 +1723,8 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, 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; @@ -1356,8 +1742,8 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, { /* 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 + * 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++; } @@ -1413,7 +1799,7 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, * 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 = malloc(cells_used * sizeof(real_cells[0])); + 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]; @@ -1434,7 +1820,7 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, break; row->cells[col_idx + i] = *cell; - render_cell(term, buf->pix[0], row, col_idx + i, row_idx, false); + render_cell(term, buf->pix[0], NULL, row, row_idx, col_idx + i, false); } int start = seat->ime.preedit.cursor.start - ime_ofs; @@ -1443,12 +1829,12 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, 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); - pixman_color_t bg = color_hex_to_pixman(term->colors.bg); + 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); + 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; @@ -1466,7 +1852,7 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, /* Hollow cursor */ if (start >= 0 && end <= term->cols) { int cols = end - start; - draw_unfocused_block(term, buf->pix[0], &cursor_color, x, y, cols); + draw_hollow_block(term, buf->pix[0], &cursor_color, x, y, cols); } term_ime_set_cursor_rect( @@ -1479,12 +1865,14 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, 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, - term->margins.left, - term->margins.top + row_idx * term->cell_height, - term->width - term->margins.left - term->margins.right, - 1 * term->cell_height); + term->window->surface.surf, + damage_x, damage_y, damage_w, damage_h); } #endif @@ -1500,19 +1888,79 @@ render_ime_preedit(struct terminal *term, struct buffer *buf) } 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 wl_surf_subsurf *overlay = &term->window->overlay; - bool unicode_mode_active = false; - - /* Check if unicode mode is active on at least one seat focusing - * this terminal instance */ - tll_foreach(term->wl->seats, it) { - if (it->item.unicode_mode.active) { - unicode_mode_active = true; - break; - } - } + struct wayl_sub_surface *overlay = &term->window->overlay; + const bool unicode_mode_active = term->unicode_mode.active; const enum overlay_style style = term->is_searching ? OVERLAY_SEARCH : @@ -1523,35 +1971,48 @@ render_overlay(struct terminal *term) if (likely(style == OVERLAY_NONE)) { if (term->render.last_overlay_style != OVERLAY_NONE) { /* Unmap overlay sub-surface */ - wl_surface_attach(overlay->surf, NULL, 0, 0); - wl_surface_commit(overlay->surf); + 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; } - struct buffer *buf = shm_get_buffer( - term->render.chains.overlay, term->width, term->height); - - pixman_image_set_clip_region32(buf->pix[0], NULL); - pixman_color_t color; switch (style) { - case OVERLAY_NONE: - break; - case OVERLAY_SEARCH: case OVERLAY_UNICODE_MODE: color = (pixman_color_t){0, 0, 0, 0x7fff}; break; case OVERLAY_FLASH: - color = (pixman_color_t){.red=0x7fff, .green=0x7fff, .blue=0, .alpha=0x7fff}; + 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; @@ -1560,23 +2021,23 @@ render_overlay(struct terminal *term) * 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 + * - clearing/erasing cells that are now selected, but weren't * in the last frame - * - dimming cells that were selected, but aren’t anymore + * - dimming cells that were selected, but aren't anymore * - * To do this, we save the last frame’s selected cells as a + * 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. + * frame's selected cells. * - * Last frame’s region minus this frame’s region gives us the + * 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 + * 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 + * 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. @@ -1589,12 +2050,12 @@ render_overlay(struct terminal *term) buf->age == 0; if (!buffer_reuse) { - /* Can’t re-use last frame’s damage - set to full window, + /* 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 */ + /* Use last frame's saved region */ pixman_region32_init(&old_see_through); pixman_region32_copy(&old_see_through, see_through); } @@ -1687,17 +2148,18 @@ render_overlay(struct terminal *term) &(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_set_buffer_scale(overlay->surf, term->scale); - wl_surface_attach(overlay->surf, buf->wl_buf, 0, 0); + wl_surface_attach(overlay->surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer( - overlay->surf, + 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->surf); + wl_surface_commit(overlay->surface.surf); quirk_weston_subsurface_desync_off(overlay->sub); buf->age = 0; @@ -1731,6 +2193,7 @@ render_worker_thread(void *_ctx) sem_wait(start); struct buffer *buf = term->render.workers.buf; + bool frame_done = false; /* Translate offset-relative cursor row to view-relative */ @@ -1751,12 +2214,11 @@ render_worker_thread(void *_ctx) switch (row_no) { default: { - xassert(buf != NULL); - 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], row, row_no, cursor_col); + render_row(term, buf->pix[my_id], &buf->dirty[my_id], + row, row_no, cursor_col); break; } @@ -1767,6 +2229,56 @@ render_worker_thread(void *_ctx) 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; + } } } }; @@ -1774,6 +2286,22 @@ render_worker_thread(void *_ctx) 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) { @@ -1782,38 +2310,59 @@ get_csd_data(const struct terminal *term, enum csd_surface surf_idx) const bool borders_visible = wayl_win_csd_borders_visible(term->window); const bool title_visible = wayl_win_csd_titlebar_visible(term->window); - /* Only title bar is rendered in maximized mode */ + const float scale = term->scale; + const int border_width = borders_visible - ? term->conf->csd.border_width * term->scale : 0; + ? roundf(term->conf->csd.border_width * scale) : 0; const int title_height = title_visible - ? term->conf->csd.title_height * term->scale : 0; + ? roundf(term->conf->csd.title_height * scale) : 0; const int button_width = title_visible - ? term->conf->csd.button_width * term->scale : 0; + ? roundf(term->conf->csd.button_width * scale) : 0; - const int button_close_width = term->width >= 1 * button_width - ? button_width : 0; + int remaining_width = term->width; - const int button_maximize_width = - term->width >= 2 * button_width && term->window->wm_capabilities.maximize - ? button_width : 0; + 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_minimize_width = - term->width >= 3 * button_width && term->window->wm_capabilities.minimize - ? button_width : 0; + 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, title_height + term->height}; - case CSD_SURF_RIGHT: return (struct csd_data){ term->width, -title_height, border_width, title_height + term->height}; - case CSD_SURF_TOP: return (struct csd_data){-border_width, -title_height - border_width, term->width + 2 * border_width, border_width}; - case CSD_SURF_BOTTOM: return (struct csd_data){-border_width, term->height, term->width + 2 * border_width, border_width}; + 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){term->width - 3 * button_width, 0, button_minimize_width, title_height}; - case CSD_SURF_MAXIMIZE: return (struct csd_data){term->width - 2 * button_width, 0, button_maximize_width, title_height}; - case CSD_SURF_CLOSE: return (struct csd_data){term->width - 1 * button_width, 0, button_close_width, title_height}; + 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; @@ -1824,15 +2373,12 @@ get_csd_data(const struct terminal *term, enum csd_surface surf_idx) } static void -csd_commit(struct terminal *term, struct wl_surface *surf, struct buffer *buf) +csd_commit(struct terminal *term, struct wayl_surface *surf, struct buffer *buf) { - xassert(buf->width % term->scale == 0); - xassert(buf->height % term->scale == 0); - - wl_surface_attach(surf, buf->wl_buf, 0, 0); - wl_surface_damage_buffer(surf, 0, 0, buf->width, buf->height); - wl_surface_set_buffer_scale(surf, term->scale); - wl_surface_commit(surf); + 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 @@ -1848,24 +2394,24 @@ render_csd_part(struct terminal *term, } static void -render_osd(struct terminal *term, - struct wl_surface *surf, struct wl_subsurface *sub_surf, +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, unsigned y) + 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); + 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); + 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); @@ -1900,18 +2446,27 @@ render_osd(struct terminal *term, 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 (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { + 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_y_ofs + font->ascent - glyph->y, + 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 + term->font_y_ofs + font->ascent - glyph->y, + x + x_ofs + glyph->x, y - glyph->y, glyph->width, glyph->height); } @@ -1922,23 +2477,23 @@ render_osd(struct terminal *term, pixman_image_unref(src); pixman_image_set_clip_region32(buf->pix[0], NULL); - xassert(buf->width % term->scale == 0); - xassert(buf->height % term->scale == 0); + 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); - quirk_weston_subsurface_desync_on(sub_surf); - wl_surface_attach(surf, buf->wl_buf, 0, 0); - wl_surface_damage_buffer(surf, 0, 0, buf->width, buf->height); - wl_surface_set_buffer_scale(surf, term->scale); + 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); - 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(surf, region); - wl_region_destroy(region); - } - - wl_surface_commit(surf); - quirk_weston_subsurface_desync_off(sub_surf); + wl_surface_commit(sub_surf->surface.surf); + quirk_weston_subsurface_desync_off(sub_surf->sub); } static void @@ -1947,19 +2502,16 @@ render_csd_title(struct terminal *term, const struct csd_data *info, { xassert(term->window->csd_mode == CSD_YES); - struct wl_surf_subsurf *surf = &term->window->csd.surface[CSD_SURF_TITLE]; + struct wayl_sub_surface *surf = &term->window->csd.surface[CSD_SURF_TITLE]; if (info->width == 0 || info->height == 0) return; - xassert(info->width % term->scale == 0); - xassert(info->height % term->scale == 0); - uint32_t bg = term->conf->csd.color.title_set ? term->conf->csd.color.title - : 0xffu << 24 | term->conf->colors.fg; + : 0xffu << 24 | term->conf->colors_dark.fg; uint32_t fg = term->conf->csd.color.buttons_set ? term->conf->csd.color.buttons - : term->conf->colors.bg; + : term->conf->colors_dark.bg; if (!term->visual_focus) { bg = color_dim(term, bg); @@ -1976,11 +2528,8 @@ render_csd_title(struct terminal *term, const struct csd_data *info, const int margin = M != NULL ? M->advance.x : win->csd.font->max_advance.x; - render_osd(term, surf->surf, surf->sub, win->csd.font, - buf, title_text, fg, bg, margin, - (buf->height - win->csd.font->height) / 2); - - csd_commit(term, surf->surf, buf); + render_osd(term, surf, win->csd.font, buf, title_text, fg, bg, margin); + csd_commit(term, &surf->surface, buf); free(_title_text); } @@ -1991,26 +2540,26 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, xassert(term->window->csd_mode == CSD_YES); xassert(surf_idx >= CSD_SURF_LEFT && surf_idx <= CSD_SURF_BOTTOM); - struct wl_surface *surf = term->window->csd.surface[surf_idx].surf; + struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; if (info->width == 0 || info->height == 0) return; - xassert(info->width % term->scale == 0); - xassert(info->height % term->scale == 0); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); { - pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0); - render_csd_part(term, surf, buf, info->width, info->height, &color); + /* 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. + * The "visible" border. */ - int scale = term->scale; - int bwidth = term->conf->csd.border_width * scale; - int vwidth = term->conf->csd.border_width_visible * scale; /* Visible size */ + 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); @@ -2056,13 +2605,13 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, 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.fg; + 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); - + 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, @@ -2073,9 +2622,10 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, } static pixman_color_t -get_csd_button_fg_color(const struct config *conf) +get_csd_button_fg_color(const struct terminal *term) { - uint32_t _color = conf->colors.bg; + const struct config *conf = term->conf; + uint32_t _color = conf->colors_dark.bg; uint16_t alpha = 0xffff; if (conf->csd.color.buttons_set) { @@ -2083,50 +2633,34 @@ get_csd_button_fg_color(const struct config *conf) alpha = _color >> 24 | (_color >> 24 << 8); } - return color_hex_to_pixman_with_alpha(_color, alpha); + 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->conf); + 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 / 2; - const int max_width = buf->width / 2; + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; - int width = max_width; - int height = max_width / 2; + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); - if (height > max_height) { - height = max_height; - width = height * 2; - } + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; - xassert(width <= max_width); - xassert(height <= max_height); + 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} + }); - int x_margin = (buf->width - width) / 2.; - int y_margin = (buf->height - height) / 2.; - - pixman_triangle_t tri = { - .p1 = { - .x = pixman_int_to_fixed(x_margin), - .y = pixman_int_to_fixed(y_margin), - }, - .p2 = { - .x = pixman_int_to_fixed(x_margin + width), - .y = pixman_int_to_fixed(y_margin), - }, - .p3 = { - .x = pixman_int_to_fixed(buf->width / 2), - .y = pixman_int_to_fixed(y_margin + height), - }, - }; - - pixman_composite_triangles( - PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1, - 0, 0, 0, 0, 1, &tri); pixman_image_unref(src); } @@ -2134,7 +2668,39 @@ static void render_csd_button_maximize_maximized( struct terminal *term, struct buffer *buf) { - pixman_color_t color = get_csd_button_fg_color(term->conf); + 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; @@ -2152,58 +2718,12 @@ render_csd_button_maximize_maximized( pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 4, - (pixman_rectangle16_t[]){ + (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_window( - struct terminal *term, struct buffer *buf) -{ - pixman_color_t color = get_csd_button_fg_color(term->conf); - pixman_image_t *src = pixman_image_create_solid_fill(&color); - - const int max_height = buf->height / 2; - const int max_width = buf->width / 2; - - int width = max_width; - int height = max_width / 2; - - if (height > max_height) { - height = max_height; - width = height * 2; - } - - xassert(width <= max_width); - xassert(height <= max_height); - - int x_margin = (buf->width - width) / 2.; - int y_margin = (buf->height - height) / 2.; - - pixman_triangle_t tri = { - .p1 = { - .x = pixman_int_to_fixed(buf->width / 2), - .y = pixman_int_to_fixed(y_margin), - }, - .p2 = { - .x = pixman_int_to_fixed(x_margin), - .y = pixman_int_to_fixed(y_margin + height), - }, - .p3 = { - .x = pixman_int_to_fixed(x_margin + width), - .y = pixman_int_to_fixed(y_margin + height), - }, - }; - - pixman_composite_triangles( - PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1, - 0, 0, 0, 0, 1, &tri); + { 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); } @@ -2220,24 +2740,116 @@ render_csd_button_maximize(struct terminal *term, struct buffer *buf) static void render_csd_button_close(struct terminal *term, struct buffer *buf) { - pixman_color_t color = get_csd_button_fg_color(term->conf); + 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; - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, buf->pix[0], &color, 1, - &(pixman_rectangle16_t){x_margin, y_margin, width, width}); + 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) @@ -2245,14 +2857,11 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, xassert(term->window->csd_mode == CSD_YES); xassert(surf_idx >= CSD_SURF_MINIMIZE && surf_idx <= CSD_SURF_CLOSE); - struct wl_surface *surf = term->window->csd.surface[surf_idx].surf; + struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; if (info->width == 0 || info->height == 0) return; - xassert(info->width % term->scale == 0); - xassert(info->height % term->scale == 0); - uint32_t _color; uint16_t alpha = 0xffff; bool is_active = false; @@ -2261,24 +2870,27 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, switch (surf_idx) { case CSD_SURF_MINIMIZE: - _color = term->conf->colors.table[4]; /* blue */ + _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; + 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.table[2]; /* green */ + _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; + 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.table[1]; /* red */ + _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; + is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE && + any_pointer_is_on_button(term, CSD_SURF_CLOSE); break; default: @@ -2299,14 +2911,14 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, if (!term->visual_focus) _color = color_dim(term, _color); - pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); - render_csd_part(term, surf, buf, info->width, info->height, &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; - break; default: BUG("unhandled surface type: %u", (unsigned)surf_idx); @@ -2324,6 +2936,7 @@ render_csd(struct terminal *term) 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]; @@ -2335,7 +2948,7 @@ render_csd(struct terminal *term) const int width = infos[i].width; const int height = infos[i].height; - struct wl_surface *surf = term->window->csd.surface[i].surf; + struct wl_surface *surf = term->window->csd.surface[i].surface.surf; struct wl_subsurface *sub = term->window->csd.surface[i].sub; xassert(surf != NULL); @@ -2351,8 +2964,7 @@ render_csd(struct terminal *term) widths[i] = width; heights[i] = height; - - wl_subsurface_set_position(sub, x / term->scale, y / term->scale); + wl_subsurface_set_position(sub, roundf(x / scale), roundf(y / scale)); } struct buffer *bufs[CSD_SURF_COUNT]; @@ -2374,12 +2986,12 @@ render_scrollback_position(struct terminal *term) struct wl_window *win = term->window; if (term->grid->view == term->grid->offset) { - if (win->scrollback_indicator.surf != NULL) + if (win->scrollback_indicator.surface.surf != NULL) wayl_win_subsurface_destroy(&win->scrollback_indicator); return; } - if (win->scrollback_indicator.surf == NULL) { + if (win->scrollback_indicator.surface.surf == NULL) { if (!wayl_win_subsurface_new( win, &win->scrollback_indicator, false)) { @@ -2388,7 +3000,7 @@ render_scrollback_position(struct terminal *term) } } - xassert(win->scrollback_indicator.surf != NULL); + xassert(win->scrollback_indicator.surface.surf != NULL); xassert(win->scrollback_indicator.sub != NULL); /* Find absolute row number of the scrollback start */ @@ -2438,7 +3050,7 @@ render_scrollback_position(struct terminal *term) char lineno_str[64]; snprintf(lineno_str, sizeof(lineno_str), "%d", rebased_view + 1); mbstoc32(_text, lineno_str, ALEN(_text)); - cell_count = ceil(log10(term->grid->num_rows)); + cell_count = (int)ceilf(log10f(term->grid->num_rows)); break; } @@ -2448,13 +3060,14 @@ render_scrollback_position(struct terminal *term) break; } - const int scale = term->scale; - const int margin = 3 * scale; + const float scale = term->scale; + const int margin = (int)roundf(3. * scale); - const int width = - (2 * margin + cell_count * term->cell_width + scale - 1) / scale * scale; - const int height = - (2 * margin + term->cell_height + scale - 1) / scale * 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; @@ -2482,12 +3095,15 @@ render_scrollback_position(struct terminal *term) } } - const int x = (term->width - margin - width) / scale * scale; - const int y = (term->margins.top + surf_top) / scale * scale; + 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.surf, NULL, 0, 0); - wl_surface_commit(win->scrollback_indicator.surf); + wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); + wl_surface_commit(win->scrollback_indicator.surface.surf); return; } @@ -2495,22 +3111,21 @@ render_scrollback_position(struct terminal *term) struct buffer *buf = shm_get_buffer(chain, width, height); wl_subsurface_set_position( - win->scrollback_indicator.sub, x / scale, y / scale); + 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.use_custom.scrollback_indicator) { - fg = term->conf->colors.scrollback_indicator.fg; - bg = term->conf->colors.scrollback_indicator.bg; + 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.surf, - win->scrollback_indicator.sub, + &win->scrollback_indicator, term->fonts[0], buf, text, fg, 0xffu << 24 | bg, - width - margin - c32len(text) * term->cell_width, margin); + width - margin - c32len(text) * term->cell_width); } static void @@ -2525,29 +3140,30 @@ render_render_timer(struct terminal *term, struct timespec render_time) char32_t text[256]; mbstoc32(text, usecs_str, ALEN(text)); - const int scale = term->scale; + const float scale = term->scale; const int cell_count = c32len(text); - const int margin = 3 * scale; - const int width = - (2 * margin + cell_count * term->cell_width + scale - 1) / scale * scale; - const int height = - (2 * margin + term->cell_height + scale - 1) / scale * 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)); 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, - margin / term->scale, - (term->margins.top + term->cell_height - margin) / term->scale); + roundf(margin / scale), + roundf((term->margins.top + term->cell_height - margin) / scale)); render_osd( term, - win->render_timer.surf, - win->render_timer.sub, + &win->render_timer, term->fonts[0], buf, text, term->colors.table[0], 0xffu << 24 | term->colors.table[8 + 1], - margin, margin); + margin); } static void frame_callback( @@ -2568,14 +3184,6 @@ force_full_repaint(struct terminal *term, struct buffer *buf) static void reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old) { - static int counter = 0; - static bool have_warned = false; - if (!have_warned && ++counter > 5) { - LOG_WARN("compositor is not releasing buffers immediately; " - "expect lower rendering performance"); - have_warned = true; - } - if (new->age > 1) { memcpy(new->data, old->data, new->height * new->stride); return; @@ -2585,21 +3193,21 @@ reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old pixman_region32_init(&dirty); /* - * Figure out current frame’s damage region + * 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 + * 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 + * 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 + * 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 + * a full re-rendering, and don't copy anything from the old * frame. */ bool full_repaint_needed = true; @@ -2607,6 +3215,11 @@ reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old 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) { @@ -2632,29 +3245,29 @@ reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old } /* - * TODO: re-apply last frame’s scroll damage + * 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 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. + * 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, + * current frame's scroll damage *first*. This is done later, * when rendering the frame. */ - pixman_region32_subtract(&dirty, &old->dirty, &dirty); + 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); + /* Copy *all* of last frame's damaged areas */ + pixman_image_set_clip_region32(new->pix[0], &old->dirty[0]); } pixman_image_composite32( @@ -2701,7 +3314,18 @@ grid_render(struct terminal *term) if (term->shutdown.in_progress) return; - struct timespec start_time, start_double_buffering = {0}, stop_double_buffering = {0}; + 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); @@ -2712,10 +3336,12 @@ grid_render(struct terminal *term) 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 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 || @@ -2732,9 +3358,27 @@ grid_render(struct terminal *term) 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) { @@ -2831,11 +3475,11 @@ grid_render(struct terminal *term) * they are overflowing. * * As soon as we see a non-overflowing cell we can - * stop, since it isn’t affecting the string of + * 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 + * 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--) { @@ -2853,9 +3497,9 @@ grid_render(struct terminal *term) * 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. + * we're dealing with right now. * - * For performance, this iterates the *outer* loop’s + * For performance, this iterates the *outer* loop's * cell pointer - no point in re-checking all these * glyphs again, in the outer loop. */ @@ -2868,7 +3512,34 @@ grid_render(struct terminal *term) } } - render_sixel_images(term, buf->pix[0], &cursor); +#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); @@ -2879,28 +3550,11 @@ grid_render(struct terminal *term) xassert(tll_length(term->render.workers.queue) == 0); } - int first_dirty_row = -1; for (int r = 0; r < term->rows; r++) { struct row *row = grid_row_in_view(term->grid, r); - if (!row->dirty) { - if (first_dirty_row >= 0) { - int x = term->margins.left; - int y = term->margins.top + first_dirty_row * term->cell_height; - int width = term->width - term->margins.left - term->margins.right; - int height = (r - first_dirty_row) * term->cell_height; - - wl_surface_damage_buffer( - term->window->surface, x, y, width, height); - pixman_region32_union_rect( - &buf->dirty, &buf->dirty, 0, y, buf->width, height); - } - first_dirty_row = -1; + if (!row->dirty) continue; - } - - if (first_dirty_row < 0) - first_dirty_row = r; row->dirty = false; @@ -2908,21 +3562,12 @@ grid_render(struct terminal *term) 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], row, r, cursor_col); + render_row(term, buf->pix[0], &damage, row, r, cursor_col); } } - if (first_dirty_row >= 0) { - int x = term->margins.left; - int y = term->margins.top + first_dirty_row * term->cell_height; - int width = term->width - term->margins.left - term->margins.right; - int height = (term->rows - first_dirty_row) * term->cell_height; - - wl_surface_damage_buffer(term->window->surface, x, y, width, height); - pixman_region32_union_rect(&buf->dirty, &buf->dirty, 0, y, buf->width, height); - } - /* Signal workers the frame is done */ if (term->render.workers.count > 0) { for (size_t i = 0; i < term->render.workers.count; i++) @@ -2934,6 +3579,25 @@ grid_render(struct terminal *term) 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); @@ -2942,27 +3606,40 @@ grid_render(struct terminal *term) 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 rendering, %lds %9ldns double buffering)", + "(%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); + double_buffering_time.tv_nsec, + (long)preapply_damage.tv_sec, + preapply_damage.tv_nsec); break; case RENDER_TIMER_OSD: @@ -2986,17 +3663,17 @@ grid_render(struct terminal *term) 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); + term->window->frame_callback = wl_surface_frame(term->window->surface.surf); wl_callback_add_listener(term->window->frame_callback, &frame_listener, term); - wl_surface_set_buffer_scale(term->window->surface, term->scale); + 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); + term->wl->presentation, term->window->surface.surf); if (feedback == NULL) { LOG_WARN("failed to create presentation feedback"); @@ -3020,14 +3697,11 @@ grid_render(struct terminal *term) if (term->conf->tweak.damage_whole_window) { wl_surface_damage_buffer( - term->window->surface, 0, 0, INT32_MAX, INT32_MAX); + term->window->surface.surf, 0, 0, INT32_MAX, INT32_MAX); } - xassert(buf->width % term->scale == 0); - xassert(buf->height % term->scale == 0); - - wl_surface_attach(term->window->surface, buf->wl_buf, 0, 0); - wl_surface_commit(term->window->surface); + wl_surface_attach(term->window->surface.surf, buf->wl_buf, 0, 0); + wl_surface_commit(term->window->surface.surf); } static void @@ -3037,9 +3711,9 @@ render_search_box(struct terminal *term) /* * 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. + * is, a glyph is either 1 or 2 (or more) "cells" wide. * - * The search ‘length’, and ‘cursor’ (position) is in + * 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 @@ -3088,18 +3762,21 @@ render_search_box(struct terminal *term) const size_t total_cells = c32swidth(text, text_len); const size_t wanted_visible_cells = max(20, total_cells); - xassert(term->scale >= 1); - const int scale = term->scale; + const float scale = term->scale; + xassert(scale >= 1.); + const size_t margin = (size_t)roundf(3 * scale); - const size_t margin = 3 * scale; - - const size_t width = term->width - 2 * margin; - const size_t visible_width = min( - term->width - 2 * margin, - (2 * margin + wanted_visible_cells * term->cell_width + scale - 1) / scale * scale); - const size_t height = min( + size_t width = term->width - 2 * margin; + size_t height = min( term->height - 2 * margin, - (2 * margin + 1 * term->cell_height + scale - 1) / scale * scale); + margin + 1 * term->cell_height + margin); + + width = roundf(scale * ceilf((term->width - 2 * margin) / scale)); + height = roundf(scale * ceilf(height / scale)); + + size_t visible_width = min( + term->width - 2 * margin, + margin + wanted_visible_cells * term->cell_width + margin); const size_t visible_cells = (visible_width - 2 * margin) / term->cell_width; size_t glyph_offset = term->render.search_glyph_offset; @@ -3117,24 +3794,26 @@ render_search_box(struct terminal *term) const bool is_match = term->search.match_len == text_len; const bool custom_colors = is_match - ? term->conf->colors.use_custom.search_box_match - : term->conf->colors.use_custom.search_box_no_match; + ? term->conf->colors_dark.use_custom.search_box_match + : term->conf->colors_dark.use_custom.search_box_no_match; /* Background - yellow on empty/match, red on mismatch (default) */ + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); const pixman_color_t color = color_hex_to_pixman( is_match ? (custom_colors - ? term->conf->colors.search_box.match.bg + ? term->conf->colors_dark.search_box.match.bg : term->colors.table[3]) : (custom_colors - ? term->conf->colors.search_box.no_match.bg - : term->colors.table[1])); + ? term->conf->colors_dark.search_box.no_match.bg + : term->colors.table[1]), + gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 1, &(pixman_rectangle16_t){width - visible_width, 0, visible_width, height}); - pixman_color_t transparent = color_hex_to_pixman_with_alpha(0, 0); + pixman_color_t transparent = color_hex_to_pixman_with_alpha(0, 0, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &transparent, 1, &(pixman_rectangle16_t){0, 0, width - visible_width, height}); @@ -3144,12 +3823,14 @@ render_search_box(struct terminal *term) const int x_ofs = term->font_x_ofs; int x = x_left; int y = margin; + pixman_color_t fg = color_hex_to_pixman( custom_colors ? (is_match - ? term->conf->colors.search_box.match.fg - : term->conf->colors.search_box.no_match.fg) - : term->colors.table[0]); + ? term->conf->colors_dark.search_box.match.fg + : term->conf->colors_dark.search_box.no_match.fg) + : term->colors.table[0], + 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++) { @@ -3204,7 +3885,7 @@ render_search_box(struct terminal *term) } /* - * Render the search string, starting at ‘glyph_offset’. Note that + * Render the search string, starting at 'glyph_offset'. Note that * glyph_offset is in cells, not characters */ for (size_t i = 0, @@ -3267,7 +3948,7 @@ render_search_box(struct terminal *term) /* TODO: how do we handle a partially hidden rectangle? */ if (start >= 0 && end <= visible_cells) { - draw_unfocused_block( + draw_hollow_block( term, buf->pix[0], &fg, x + start * term->cell_width, y, end - start); } term_ime_set_cursor_rect(term, @@ -3305,11 +3986,10 @@ render_search_box(struct terminal *term) continue; } - if (unlikely(pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8)) { - /* Glyph surface is a pre-rendered image (typically a color emoji...) */ + 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 + font_baseline(term) - glyph->y, + x + x_ofs + glyph->x, y + term->font_baseline - glyph->y, glyph->width, glyph->height); } else { int combining_ofs = width == 0 @@ -3321,7 +4001,7 @@ render_search_box(struct terminal *term) pixman_image_composite32( PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, x + x_ofs + combining_ofs + glyph->x, - y + font_baseline(term) - glyph->y, + y + term->font_baseline - glyph->y, glyph->width, glyph->height); pixman_image_unref(src); } @@ -3346,24 +4026,21 @@ render_search_box(struct terminal *term) /* TODO: this is only necessary on a window resize */ wl_subsurface_set_position( term->window->search.sub, - margin / scale, - max(0, (int32_t)term->height - height - margin) / scale); + roundf(margin / scale), + roundf(max(0, (int32_t)term->height - height - margin) / scale)); - xassert(buf->width % scale == 0); - xassert(buf->height % scale == 0); - - wl_surface_attach(term->window->search.surf, buf->wl_buf, 0, 0); - wl_surface_damage_buffer(term->window->search.surf, 0, 0, width, height); - wl_surface_set_buffer_scale(term->window->search.surf, 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, width - visible_width, 0, visible_width, height); - wl_surface_set_opaque_region(term->window->search.surf, region); + wl_surface_set_opaque_region(term->window->search.surface.surf, region); wl_region_destroy(region); } - wl_surface_commit(term->window->search.surf); + 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 @@ -3379,9 +4056,9 @@ render_urls(struct terminal *term) struct wl_window *win = term->window; xassert(tll_length(win->urls) > 0); - const int scale = term->scale; - const int x_margin = 2 * scale; - const int y_margin = 1 * scale; + 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 @@ -3444,7 +4121,7 @@ render_urls(struct terminal *term) continue; } - struct wl_surface *surf = it->item.surf.surf; + struct wl_surface *surf = it->item.surf.surface.surf; struct wl_subsurface *sub_surf = it->item.surf.sub; if (surf == NULL || sub_surf == NULL) @@ -3480,7 +4157,7 @@ render_urls(struct terminal *term) 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 */ + /* Don't position it outside our window */ if (x < -term->margins.left) x = -term->margins.left; if (y < -term->margins.top) @@ -3521,12 +4198,12 @@ render_urls(struct terminal *term) label[i] = U' '; /* - * Don’t extend outside our window + * Don't extend outside our window * - * Truncate label so that it doesn’t extend outside our + * 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 + * Do it in a way such that we don't cut the label in the * middle of a double-width character. */ @@ -3551,10 +4228,11 @@ render_urls(struct terminal *term) if (cols == 0) continue; - const int width = - (2 * x_margin + cols * term->cell_width + scale - 1) / scale * scale; - const int height = - (2 * y_margin + term->cell_height + scale - 1) / scale * scale; + 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); @@ -3571,32 +4249,31 @@ render_urls(struct terminal *term) struct buffer *bufs[render_count]; shm_get_many(chain, render_count, widths, heights, bufs); - uint32_t fg = term->conf->colors.use_custom.jump_label - ? term->conf->colors.jump_label.fg + 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.use_custom.jump_label - ? term->conf->colors.jump_label.bg + 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++) { - struct wl_surface *surf = info[i].url->surf.surf; - struct wl_subsurface *sub_surf = info[i].url->surf.sub; + 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(surf != NULL); - xassert(sub_surf != NULL); + xassert(sub_surf->surface.surf != NULL); + xassert(sub_surf->sub != NULL); wl_subsurface_set_position( - sub_surf, - (term->margins.left + x) / term->scale, - (term->margins.top + y) / term->scale); + sub_surf->sub, + roundf((term->margins.left + x) / scale), + roundf((term->margins.top + y) / scale)); render_osd( - term, surf, sub_surf, term->fonts[0], bufs[i], label, - fg, 0xffu << 24 | bg, x_margin, y_margin); + term, sub_surf, term->fonts[0], bufs[i], label, + fg, 0xffu << 24 | bg, x_margin); free(info[i].text); } @@ -3680,6 +4357,8 @@ tiocswinsz(struct terminal *term) { LOG_ERRNO("TIOCSWINSZ"); } + + term_send_size_notification(term); } } @@ -3699,13 +4378,13 @@ delayed_reflow_of_normal_grid(struct terminal *term) /* Reflow the original (since before the resize was started) grid, * to the *current* dimensions */ grid_resize_and_reflow( - term->interactive_resizing.grid, + 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 + /* Replace the current, truncated, "normal" grid with the * correctly reflowed one */ grid_free(&term->normal); term->normal = *term->interactive_resizing.grid; @@ -3720,6 +4399,7 @@ delayed_reflow_of_normal_grid(struct terminal *term) 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; @@ -3768,7 +4448,7 @@ send_dimensions_to_client(struct terminal *term) win->resize_timeout_fd = -1; } } else { - /* Send new dimensions to client “in a while” */ + /* Send new dimensions to client "in a while" */ assert(win->is_resizing && term->conf->resize_delay_ms > 0); int fd = win->resize_timeout_fd; @@ -3812,9 +4492,32 @@ send_dimensions_to_client(struct terminal *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? */ -static bool -maybe_resize(struct terminal *term, int width, int height, bool force) +bool +render_resize(struct terminal *term, int width, int height, uint8_t opts) { if (term->shutdown.in_progress) return false; @@ -3825,79 +4528,82 @@ maybe_resize(struct terminal *term, int width, int height, bool force) if (term->cell_width == 0 && term->cell_height == 0) return false; - int scale = -1; - tll_foreach(term->window->on_outputs, it) { - if (it->item->scale > scale) - scale = it->item->scale; + 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 (scale < 0) { - /* Haven't 'entered' an output yet? */ - scale = term->scale; - } - - width *= scale; - height *= scale; - - if (width == 0 && height == 0) { - /* - * The compositor is letting us choose the size - * - * If we have a "last" used size - use that. Otherwise, use - * the size from the user configuration. - */ - if (term->stashed_width != 0 && term->stashed_height != 0) { + 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; - height = term->stashed_height; } 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)) { + if (wayl_win_csd_borders_visible(term->window)) height -= 2 * term->conf->csd.border_width_visible; - width -= 2 * term->conf->csd.border_width_visible; - } - width *= scale; height *= scale; break; case CONF_SIZE_CELLS: - width = term->conf->size.width * term->cell_width; - height = term->conf->size.height * term->cell_height; - - width += 2 * term->conf->pad_x * scale; - height += 2 * term->conf->pad_y * scale; - - /* - * Ensure we can scale to logical size, and back to - * pixels without truncating. - */ - if (width % scale) - width += scale - width % scale; - if (height % scale) - height += scale - height % scale; - - xassert(width % scale == 0); - xassert(height % scale == 0); + 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 = 2; + const int min_cols = 1; const int min_rows = 1; /* Minimum window size (must be divisible by the scaling factor)*/ - const int min_width = (min_cols * term->cell_width + scale - 1) / scale * scale; - const int min_height = (min_rows * term->cell_height + scale - 1) / scale * scale; + 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); @@ -3905,11 +4611,32 @@ maybe_resize(struct terminal *term, int width, int height, bool force) /* Padding */ const int max_pad_x = (width - min_width) / 2; const int max_pad_y = (height - min_height) / 2; - const int pad_x = min(max_pad_x, scale * term->conf->pad_x); - const int pad_y = min(max_pad_y, scale * term->conf->pad_y); + 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 * term->conf->pad_top); + const int pad_bottom= min(max_pad_y, scale * term->conf->pad_bottom); - if (!force && width == term->width && height == term->height && scale == term->scale) + 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); @@ -3917,23 +4644,50 @@ maybe_resize(struct terminal *term, int width, int height, bool force) /* 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; - term->scale = scale; - - const uint32_t scrollback_lines = term->render.scrollback_lines; /* 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 - 2 * pad_x) / term->cell_width; - const int new_rows = (term->height - 2 * pad_y) / term->cell_height; + 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 = 1 << (32 - __builtin_clz(new_rows + scrollback_lines - 1)); - const int new_alt_grid_rows = 1 << (32 - __builtin_clz(new_rows)); + 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); @@ -3944,20 +4698,27 @@ maybe_resize(struct terminal *term, int width, int height, bool force) const int total_x_pad = term->width - grid_width; const int total_y_pad = term->height - grid_height; - if (term->conf->center && !term->window->is_resizing) { + 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 = total_x_pad / 2; term->margins.top = total_y_pad / 2; } else { - term->margins.left = pad_x; - term->margins.top = pad_y; + 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_x); - xassert(term->margins.right >= pad_x); - xassert(term->margins.top >= pad_y); - xassert(term->margins.bottom >= pad_y); + 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"); @@ -3967,9 +4728,9 @@ maybe_resize(struct terminal *term, int width, int height, bool force) /* - * 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 re-use the TIOCSWINSZ timer to handle this. See + * 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 @@ -3979,16 +4740,18 @@ maybe_resize(struct terminal *term, int width, int height, bool force) if (term->interactive_resizing.grid == NULL) { term_ptmx_pause(term); - /* Stash the current ‘normal’ grid, as-is, to be used when + /* 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; - term->interactive_resizing.selection_coords = term->selection.coords; + + if (term->grid == &term->normal) + term->interactive_resizing.selection_coords = term->selection.coords; } else { - /* We’ll replace the current temporary grid, with a new + /* We'll replace the current temporary grid, with a new * one (again based on the original grid) */ grid_free(&term->normal); } @@ -3998,12 +4761,12 @@ maybe_resize(struct terminal *term, int width, int height, bool force) /* * 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 + * 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 + * 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). * @@ -4035,6 +4798,29 @@ maybe_resize(struct terminal *term, int width, int height, bool force) 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; @@ -4045,7 +4831,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) selection_cancel(term); else { /* - * Don’t cancel, but make sure there aren’t any ongoing + * Don't cancel, but make sure there aren't any ongoing * selections after the resize. */ tll_foreach(term->wl->seats, it) { @@ -4057,7 +4843,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) /* * 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 + * selection's pivot point coordinates *must* be added to the * tracking points list. */ /* Resize grids */ @@ -4074,8 +4860,10 @@ maybe_resize(struct terminal *term, int width, int height, bool force) } else { /* Full text reflow */ + int old_normal_rows = old_rows; + if (term->interactive_resizing.grid != NULL) { - /* Throw away the current, truncated, “normal” grid, and + /* Throw away the current, truncated, "normal" grid, and * use the original grid instead (from before the resize * started) */ grid_free(&term->normal); @@ -4085,7 +4873,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) term->hide_cursor = term->interactive_resizing.old_hide_cursor; term->selection.coords = term->interactive_resizing.selection_coords; - old_rows = term->interactive_resizing.old_screen_rows; + old_normal_rows = term->interactive_resizing.old_screen_rows; term->interactive_resizing.grid = NULL; term->interactive_resizing.old_screen_rows = 0; @@ -4101,7 +4889,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) }; grid_resize_and_reflow( - &term->normal, new_normal_grid_rows, new_cols, old_rows, new_rows, + &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); } @@ -4119,18 +4907,19 @@ maybe_resize(struct terminal *term, int width, int height, bool force) sixel_reflow(term); -#if defined(_DEBUG) && LOG_ENABLE_DBG - LOG_DBG("resize: %dx%d, grid: cols=%d, rows=%d " + LOG_DBG("resized: grid: cols=%d, rows=%d " "(left-margin=%d, right-margin=%d, top-margin=%d, bottom-margin=%d)", - term->width, term->height, term->cols, term->rows, - term->margins.left, term->margins.right, term->margins.top, term->margins.bottom); -#endif + 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 >= old_rows) + 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; @@ -4138,10 +4927,7 @@ damage_view: /* Signal TIOCSWINSZ */ send_dimensions_to_client(term); - if (!term->window->is_maximized && - !term->window->is_fullscreen && - !term->window->is_tiled) - { + 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; @@ -4152,27 +4938,49 @@ damage_view: 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_height = - title_shown ? term->conf->csd.title_height : 0; - const int border_width = - border_shown ? term->conf->csd.border_width_visible : 0; + 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, - min_width / scale + 2 * border_width, - min_height / scale + title_height + 2 * border_width); + toplevel_min_width, toplevel_min_height); xdg_surface_set_window_geometry( term->window->xdg_surface, - -border_width, - -title_height - border_width, - term->width / term->scale + 2 * border_width, - term->height / term->scale + title_height + 2 * border_width); + 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); @@ -4183,18 +4991,6 @@ damage_view: return true; } -bool -render_resize(struct terminal *term, int width, int height) -{ - return maybe_resize(term, width, height, false); -} - -bool -render_resize_force(struct terminal *term, int width, int height) -{ - return maybe_resize(term, width, height, true); -} - static void xcursor_callback( void *data, struct wl_callback *wl_callback, uint32_t callback_data); static const struct wl_callback_listener xcursor_listener = { @@ -4206,6 +5002,8 @@ 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; } @@ -4216,38 +5014,93 @@ render_xcursor_update(struct seat *seat) if (!seat->mouse_focus) return; - xassert(seat->pointer.xcursor != NULL); + xassert(seat->pointer.shape != CURSOR_SHAPE_NONE); - if (seat->pointer.xcursor == XCURSOR_HIDDEN) { + if (seat->pointer.shape == CURSOR_SHAPE_HIDDEN) { /* Hide cursor */ - wl_surface_attach(seat->pointer.surface, NULL, 0, 0); - wl_surface_commit(seat->pointer.surface); + 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; } - xassert(seat->pointer.cursor != NULL); + const enum cursor_shape shape = seat->pointer.shape; + const char *const xcursor = seat->pointer.last_custom_xcursor; - const int scale = seat->pointer.scale; + 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); - wl_surface_attach( - seat->pointer.surface, wl_cursor_image_get_buffer(image), 0, 0); + 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, + seat->pointer.surface.surf, image->hotspot_x / scale, image->hotspot_y / scale); wl_surface_damage_buffer( - seat->pointer.surface, 0, 0, INT32_MAX, INT32_MAX); - - wl_surface_set_buffer_scale(seat->pointer.surface, scale); + 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); + 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); + wl_surface_commit(seat->pointer.surface.surf); } static void @@ -4342,9 +5195,6 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) void render_refresh_title(struct terminal *term) { - if (term->render.title.is_armed) - return; - struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) return; @@ -4366,6 +5216,70 @@ render_refresh_title(struct terminal *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) { @@ -4394,13 +5308,14 @@ render_refresh_urls(struct terminal *term) } bool -render_xcursor_set(struct seat *seat, struct terminal *term, const char *xcursor) +render_xcursor_set(struct seat *seat, struct terminal *term, + enum cursor_shape shape) { - if (seat->pointer.theme == NULL) + if (seat->pointer.theme == NULL && seat->pointer.shape_device == NULL) return false; if (seat->mouse_focus == NULL) { - seat->pointer.xcursor = NULL; + seat->pointer.shape = CURSOR_SHAPE_NONE; return true; } @@ -4409,26 +5324,132 @@ render_xcursor_set(struct seat *seat, struct terminal *term, const char *xcursor return true; } - if (seat->pointer.xcursor == xcursor) + if (seat->pointer.shape == shape && + !(shape == CURSOR_SHAPE_CUSTOM && + !streq(seat->pointer.last_custom_xcursor, + term->mouse_user_cursor))) + { return true; + } - if (xcursor != XCURSOR_HIDDEN) { - seat->pointer.cursor = wl_cursor_theme_get_cursor( - seat->pointer.theme, xcursor); + 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); - if (seat->pointer.cursor == NULL) { - seat->pointer.cursor = wl_cursor_theme_get_cursor( - seat->pointer.theme, XCURSOR_TEXT_FALLBACK ); - if (seat->pointer.cursor == NULL) { - LOG_ERR("failed to load xcursor pointer '%s', and fallback '%s'", xcursor, XCURSOR_TEXT_FALLBACK); - return false; - } - } - } else 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.xcursor = xcursor; + 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 index d2c673ee..e6674ab2 100644 --- a/render.h +++ b/render.h @@ -10,18 +10,29 @@ struct renderer; struct renderer *render_init(struct fdm *fdm, struct wayland *wayl); void render_destroy(struct renderer *renderer); -bool render_resize(struct terminal *term, int width, int height); -bool render_resize_force(struct terminal *term, int width, int height); +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); bool render_xcursor_set( - struct seat *seat, struct terminal *term, const char *xcursor); + 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; @@ -36,3 +47,6 @@ struct csd_data { }; 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 index 5483dac1..fe820d9b 100755 --- a/scripts/benchmark.py +++ b/scripts/benchmark.py @@ -11,7 +11,7 @@ import termios from datetime import datetime -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('files', type=argparse.FileType('rb'), nargs='+') parser.add_argument('--iterations', type=int, default=20) @@ -24,12 +24,12 @@ def main(): termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))) - times = {name: [] for name in [f.name for f in args.files]} + 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 i in range(args.iterations): + for _ in range(args.iterations): start = datetime.now() sys.stdout.buffer.write(bench_bytes) stop = datetime.now() @@ -48,4 +48,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/scripts/generate-alt-random-writes.py b/scripts/generate-alt-random-writes.py index 812b0213..7ad1460c 100755 --- a/scripts/generate-alt-random-writes.py +++ b/scripts/generate-alt-random-writes.py @@ -8,6 +8,8 @@ import struct import sys import termios +from typing import Any + class ColorVariant(enum.IntEnum): NONE = enum.auto() @@ -17,7 +19,7 @@ class ColorVariant(enum.IntEnum): RGB = enum.auto() -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument( 'out', type=argparse.FileType(mode='w'), nargs='?', help='name of output file') @@ -38,10 +40,16 @@ def main(): 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): + def dummy(*args: Any) -> None: """Need a handler installed for sigwait() to trigger.""" + _ = args pass signal.signal(signal.SIGWINCH, dummy) @@ -53,6 +61,9 @@ def main(): 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 @@ -71,9 +82,11 @@ def main(): 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: @@ -190,8 +203,8 @@ def main(): # The sixel 'alphabet' sixels = '?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~' - last_pos = None - last_size = None + 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): @@ -207,8 +220,9 @@ def main(): six_height, six_width = last_size six_rows = (six_height + 5) // 6 # Round up; each sixel is 6 pixels - # Begin sixel - out.write('\033Pq') + # 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. @@ -253,4 +267,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index acbf5279..c10373d3 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -1,14 +1,12 @@ #!/usr/bin/env python3 import argparse +import os import re -import sys - -from typing import Dict, Union class Capability: - def __init__(self, name: str, value: Union[bool, int, str]): + def __init__(self, name: str, value: bool | int | str): self._name = name self._value = value @@ -17,25 +15,37 @@ class Capability: return self._name @property - def value(self) -> Union[bool, int, str]: + def value(self) -> bool | int | str: return self._value - def __lt__(self, other): + def __lt__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name < other._name - def __le__(self, other): + def __le__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name <= other._name - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name == other._name - def __ne__(self, other): - 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): - return 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): + def __ge__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name >= other._name @@ -50,27 +60,33 @@ class IntCapability(Capability): class StringCapability(Capability): def __init__(self, name: str, value: str): - # Expand \E to literal ESC in non-parameterized capabilities - if '%' not in value: - # Ensure e.g. \E7 doesn’t get translated to “\0337”, which - # would be interpreted as octal 337 by the C compiler - value = re.sub(r'\\E([0-7])', r'\\033" "\1', value) + # see terminfo(5) for valid escape sequences - # Replace \E with an actual escape - value = re.sub(r'\\E', r'\\033', value) + # 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) - # Don’t escape ‘:’ - value = value.replace('\\:', ':') + # 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) - else: - value = value.replace("\\", "\\\\") - # # Need to double-escape backslashes. These only occur in - # # ‘\E\’ combos. Note that \E itself is updated below - # value = value.replace('\\E\\\\', '\\E\\\\\\\\') + # Replace \E and \e with ESC + value = re.sub(r'\\E|\\e', r'\\033', value) - # # Need to double-escape \E in C string literals - # value = value.replace('\\E', '\\\\E') + # 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) @@ -79,7 +95,7 @@ class Fragment: def __init__(self, name: str, description: str): self._name = name self._description = description - self._caps = {} + self._caps = dict[str, Capability]() @property def name(self) -> str: @@ -90,18 +106,18 @@ class Fragment: return self._description @property - def caps(self) -> Dict[str, Capability]: + def caps(self) -> dict[str, Capability]: return self._caps - def add_capability(self, cap: Capability): + 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): + def del_capability(self, name: str) -> None: del self._caps[name] -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('source_entry_name') parser.add_argument('source', type=argparse.FileType('r')) @@ -114,15 +130,15 @@ def main(): source = opts.source target = opts.target - lines = [] - for l in source.readlines(): - l = l.strip() - if l.startswith('#'): + lines = list[str]() + for line in source.readlines(): + line = line.strip() + if line.startswith('#'): continue - lines.append(l) + lines.append(line) - fragments = {} - cur_fragment = None + fragments = dict[str, Fragment]() + cur_fragment: Fragment | None = None for m in re.finditer( r'(?P<name>(?P<entry_name>[-+\w@]+)\|(?P<entry_desc>.+?),)|' @@ -141,17 +157,20 @@ def main(): 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') - value = int(m.group('int_val'), 0) - cur_fragment.add_capability(IntCapability(name, value)) + 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') - value = m.group('str_val') - cur_fragment.add_capability(StringCapability(name, value)) + str_value = m.group('str_val') + assert cur_fragment is not None + cur_fragment.add_capability(StringCapability(name, str_value)) else: assert False @@ -160,6 +179,9 @@ def main(): 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) @@ -179,8 +201,9 @@ def main(): 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 = [] + terminfo_parts = list[str]() for cap in sorted(entry.caps.values()): name = cap.name value = str(cap.value) @@ -204,4 +227,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/scripts/generate-emoji-variation-sequences.py b/scripts/generate-emoji-variation-sequences.py new file mode 100644 index 00000000..57e881c7 --- /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 <stdint.h>\n') + opts.output.write('#include <stdbool.h>\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 00000000..a6aa0f4a --- /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 <stdint.h>\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 index 59765c2e..5386ffd3 100644 --- a/search.c +++ b/search.c @@ -9,12 +9,14 @@ #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" @@ -81,7 +83,14 @@ search_ensure_size(struct terminal *term, size_t wanted_size) } static bool -has_wrapped_around(const struct terminal *term, int abs_row_no) +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; @@ -117,11 +126,6 @@ search_cancel_keep_selection(struct terminal *term) term_xcursor_update(term); render_refresh(term); - - /* Work around Sway bug - unmapping a sub-surface does not damage - * the underlying surface */ - term_damage_margins(term); - term_damage_view(term); } void @@ -239,13 +243,19 @@ search_update_selection(struct terminal *term, const struct range *match) } if (start_row != term->search.match.row || - start_col != term->search.match.col) + 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 */ @@ -273,20 +283,25 @@ matches_cell(const struct terminal *term, const struct cell *cell, size_t search if (composed == NULL && base == 0 && term->search.buf[search_ofs] == U' ') return 1; - if (c32ncasecmp(&base, &term->search.buf[search_ofs], 1) != 0) - return -1; + if (hasc32upper(term->search.buf)) { + 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 + 1 + composed->count > term->search.len) + 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 + 1 + j]) + if (composed->chars[j] != term->search.buf[search_ofs + j]) return -1; } } - return composed != NULL ? 1 + composed->count : 1; + return composed != NULL ? composed->count : 1; } static bool @@ -373,8 +388,11 @@ find_next(struct terminal *term, enum search_direction direction, match_len += additional_chars; match_end_col++; - while (match_row->cells[match_end_col].wc > CELL_SPACER) + while (match_end_col < term->cols && + match_row->cells[match_end_col].wc > CELL_SPACER) + { match_end_col++; + } } if (match_len != term->search.len) { @@ -550,6 +568,7 @@ search_matches_next(struct search_match_iterator *iter) 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) @@ -569,11 +588,6 @@ search_matches_next(struct search_match_iterator *iter) match.start.row, match.start.col, match.end.row, match.end.col, grid->view); - xassert(match.start.row >= 0); - xassert(match.start.row < term->rows); - xassert(match.end.row >= 0); - xassert(match.end.row < term->rows); - /* Assert match end comes *after* the match start */ xassert(match.end.row > match.start.row || (match.end.row == match.start.row && @@ -646,67 +660,299 @@ search_add_chars(struct terminal *term, const char *src, size_t count) 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_match_to_end_of_word(struct terminal *term, bool spaces_only) +search_extend_left(struct terminal *term, const struct coord *target) { if (term->search.match_len == 0) return; - xassert(term->selection.coords.end.row >= 0); + const struct coord last_coord = selection_get_start(term); + struct coord pos = *target; + const struct row *row = term->grid->rows[pos.row]; - struct grid *grid = term->grid; - const bool move_cursor = term->search.cursor == term->search.len; + const bool move_cursor = term->search.cursor != 0; - struct coord old_end = selection_get_end(term); - struct coord new_end = old_end; - struct row *row = NULL; - - xassert(new_end.row >= 0); - xassert(new_end.row < grid->num_rows); - - /* Advances a coordinate by one column, to the right. Returns - * false if we’ve reached the scrollback wrap-around */ -#define advance_pos(coord) __extension__ \ - ({ \ - bool wrapped_around = false; \ - if (++(coord).col >= term->cols) { \ - (coord).row = ((coord).row + 1) & (grid->num_rows - 1); \ - (coord).col = 0; \ - row = grid->rows[(coord).row]; \ - if (has_wrapped_around(term, (coord.row))) \ - wrapped_around = true; \ - } \ - !wrapped_around; \ - }) - - /* First character to consider is the *next* character */ - if (!advance_pos(new_end)) + struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); + if (ctx == NULL) return; - xassert(new_end.row >= 0); - xassert(new_end.row < grid->num_rows); - xassert(grid->rows[new_end.row] != NULL); + 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; + } - /* Find next word boundary */ - new_end.row -= grid->view + grid->num_rows; - new_end.row &= grid->num_rows - 1; - selection_find_word_boundary_right(term, &new_end, spaces_only, false); - new_end.row += grid->view; - new_end.row &= grid->num_rows - 1; + char32_t *new_text; + size_t new_len; - struct coord pos = old_end; - row = grid->rows[pos.row]; + 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 (!advance_pos(pos)) + if (!coord_advance_right(term, &pos, &row)) break; if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) break; - } while (pos.col != new_end.col || pos.row != new_end.row); + } while (pos.col != target->col || pos.row != target->row); char32_t *new_text; size_t new_len; @@ -732,12 +978,9 @@ search_match_to_end_of_word(struct terminal *term, bool spaces_only) if (move_cursor) term->search.cursor = term->search.len; - struct range match = {.start = term->search.match, .end = new_end}; + struct range match = {.start = term->search.match, .end = *target}; search_update_selection(term, &match); - term->search.match_len = term->search.len; - -#undef advance_pos } static size_t @@ -826,6 +1069,62 @@ execute_binding(struct seat *seat, struct terminal *term, 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; @@ -833,6 +1132,7 @@ execute_binding(struct seat *seat, struct terminal *term, grid->view = ensure_view_is_allocated( term, term->search.original_view); } + term_damage_view(term); search_cancel(term); return true; @@ -970,32 +1270,123 @@ execute_binding(struct seat *seat, struct terminal *term, return true; } - case BIND_ACTION_SEARCH_EXTEND_WORD: - search_match_to_end_of_word(term, false); - *update_search_result = false; - *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)); - case BIND_ACTION_SEARCH_EXTEND_WORD_WS: - search_match_to_end_of_word(term, true); - *update_search_result = false; - *redraw = true; + 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, &from_clipboard_cb, &from_clipboard_done, term); + 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, &from_clipboard_cb, &from_clipboard_done, term); + 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(seat); + unicode_mode_activate(term); return true; case BIND_ACTION_SEARCH_COUNT: @@ -1011,17 +1402,12 @@ 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, - xkb_mod_mask_t locked, 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); - const xkb_mod_mask_t bind_mods = - mods & seat->kbd.bind_significant & ~locked; - const xkb_mod_mask_t bind_consumed = - consumed & seat->kbd.bind_significant & ~locked; 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; @@ -1030,27 +1416,17 @@ search_input(struct seat *seat, struct terminal *term, bool update_search_result = false; bool redraw = false; - /* Key bindings */ + /* + * Key bindings + */ + + /* Match untranslated symbols */ tll_foreach(bindings->search, it) { const struct key_binding *bind = &it->item; - /* Match translated symbol */ - if (bind->k.sym == sym && - bind->mods == (bind_mods & ~bind_consumed)) { - - if (execute_binding(seat, term, bind, serial, - &update_search_result, &search_direction, - &redraw)) - { - goto update_search; - } - return; - } - - if (bind->mods != bind_mods || bind_mods != (mods & ~locked)) + if (bind->mods != mods || bind->mods == 0) continue; - /* Match untranslated symbols */ for (size_t i = 0; i < raw_count; i++) { if (bind->k.sym == raw_syms[i]) { if (execute_binding(seat, term, bind, serial, @@ -1062,8 +1438,32 @@ search_input(struct seat *seat, struct terminal *term, 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; - /* Match raw key code */ tll_foreach(bind->k.key_codes, code) { if (code->item == key) { if (execute_binding(seat, term, bind, serial, @@ -1084,7 +1484,8 @@ search_input(struct seat *seat, struct terminal *term, 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) { + } else if (compose_status == XKB_COMPOSE_CANCELLED || + compose_status == XKB_COMPOSE_COMPOSING) { count = 0; } else { count = xkb_state_key_get_utf8( diff --git a/search.h b/search.h index d5d4162b..ee8ecd76 100644 --- a/search.h +++ b/search.h @@ -11,7 +11,6 @@ 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, - xkb_mod_mask_t locked, 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); diff --git a/selection.c b/selection.c index f6349d7f..0a479ee8 100644 --- a/selection.c +++ b/selection.c @@ -19,6 +19,7 @@ #include "char32.h" #include "commands.h" #include "config.h" +#include "debug.h" #include "extract.h" #include "grid.h" #include "misc.h" @@ -298,6 +299,7 @@ foreach_selected( 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; @@ -339,16 +341,19 @@ selection_to_text(const struct terminal *term) return extract_finish(ctx, &text, NULL) ? text : NULL; } +/* Coordinates are in *absolute* row numbers (NOT view local) */ void -selection_find_word_boundary_left(struct terminal *term, struct coord *pos, +selection_find_word_boundary_left(const struct terminal *term, struct coord *pos, bool spaces_only) { - xassert(pos->row >= 0); - xassert(pos->row < term->rows); + 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_row_in_view(term->grid, pos->row); + const struct row *r = grid->rows[pos->row]; char32_t c = r->cells[pos->col].wc; while (c >= CELL_SPACER) { @@ -372,15 +377,22 @@ selection_find_word_boundary_left(struct terminal *term, struct coord *pos, int next_col = pos->col - 1; int next_row = pos->row; - const struct row *row = grid_row_in_view(term->grid, next_row); + const struct row *row = grid->rows[next_row]; /* Linewrap */ if (next_col < 0) { next_col = term->cols - 1; - if (--next_row < 0) - break; - row = grid_row_in_view(term->grid, next_row); + 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 */ @@ -417,17 +429,20 @@ selection_find_word_boundary_left(struct terminal *term, struct coord *pos, } } +/* Coordinates are in *absolute* row numbers (NOT view local) */ void -selection_find_word_boundary_right(struct terminal *term, struct coord *pos, +selection_find_word_boundary_right(const struct terminal *term, struct coord *pos, bool spaces_only, bool stop_on_space_to_word_boundary) { - xassert(pos->row >= 0); - xassert(pos->row < term->rows); + 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_row_in_view(term->grid, pos->row); + const struct row *r = grid->rows[pos->row]; char32_t c = r->cells[pos->col].wc; while (c >= CELL_SPACER) { @@ -452,7 +467,7 @@ selection_find_word_boundary_right(struct terminal *term, struct coord *pos, int next_col = pos->col + 1; int next_row = pos->row; - const struct row *row = grid_row_in_view(term->grid, next_row); + const struct row *row = term->grid->rows[next_row]; /* Linewrap */ if (next_col >= term->cols) { @@ -462,10 +477,14 @@ selection_find_word_boundary_right(struct terminal *term, struct coord *pos, } next_col = 0; - if (++next_row >= term->rows) - break; + next_row = (next_row + 1) & (grid->num_rows - 1); - row = grid_row_in_view(term->grid, next_row); + 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; @@ -508,9 +527,92 @@ selection_find_word_boundary_right(struct terminal *term, struct coord *pos, } } -void -selection_find_line_boundary_left(struct terminal *term, struct coord *pos, - bool spaces_only) +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; @@ -530,9 +632,8 @@ selection_find_line_boundary_left(struct terminal *term, struct coord *pos, } } -void -selection_find_line_boundary_right(struct terminal *term, struct coord *pos, - bool spaces_only) +static void +selection_find_line_boundary_right(struct terminal *term, struct coord *pos) { int next_row = pos->row; pos->col = term->cols - 1; @@ -562,6 +663,7 @@ selection_start(struct terminal *term, int col, int row, 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" : "<unknown>", row, col); @@ -581,24 +683,84 @@ selection_start(struct terminal *term, int col, int row, break; case SELECTION_WORD_WISE: { - struct coord start = {col, row}, end = {col, row}; + 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 = (struct coord){ - start.col, term->grid->view + start.row}; + term->selection.coords.start = start; term->selection.pivot.start = term->selection.coords.start; - term->selection.pivot.end = (struct coord){end.col, term->grid->view + end.row}; + term->selection.pivot.end = end; - selection_update(term, end.col, end.row); + /* + * 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, spaces_only); - selection_find_line_boundary_right(term, &end, spaces_only); + 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}; @@ -703,8 +865,8 @@ pixman_region_for_coords_block(const struct terminal *term, return region; } -/* Returns a pixman region representing the selection between ‘start’ - * and ‘end’ (given the current selection kind), in *scrollback +/* 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, @@ -766,17 +928,17 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, * 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 + * "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. + * (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 + * "Fix" by *never* highlighting selected empty * cells (they still get converted to spaces when * copied, if followed by non-empty cells). */ @@ -789,8 +951,8 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, * * 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 + * 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 @@ -802,7 +964,7 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, * 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', 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 @@ -810,6 +972,7 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, */ cell->attrs.clean = false; cell->attrs.selected = false; + row->dirty = true; continue; } @@ -817,8 +980,10 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, xassert(c - j >= 0); struct cell *cell = &row->cells[c - j]; - if (dirty_cells) + if (dirty_cells) { cell->attrs.clean = false; + row->dirty = true; + } cell->attrs.selected = selected; } @@ -887,7 +1052,7 @@ set_pivot_point_for_block_and_char_wise(struct terminal *term, *pivot_start = start; - /* First, make sure ‘start’ isn’t in the middle of a + /* 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)]; @@ -896,7 +1061,7 @@ set_pivot_point_for_block_and_char_wise(struct terminal *term, if (cell->wc < CELL_SPACER) break; - /* Multi-column chars don’t cross rows */ + /* Multi-column chars don't cross rows */ xassert(pivot_start->col > 0); if (pivot_start->col == 0) break; @@ -962,16 +1127,16 @@ selection_update(struct terminal *term, int col, int row) if (!term->selection.ongoing) return; - 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, - row, col); - 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; @@ -1031,41 +1196,39 @@ selection_update(struct terminal *term, int col, int row) case SELECTION_WORD_WISE: switch (term->selection.direction) { - case SELECTION_LEFT: { - struct coord end = {col, row}; + case SELECTION_LEFT: + new_end = (struct coord){col, term->grid->view + row}; selection_find_word_boundary_left( - term, &end, term->selection.spaces_only); - new_end = (struct coord){end.col, term->grid->view + end.row}; + term, &new_end, term->selection.spaces_only); break; - } - case SELECTION_RIGHT: { - struct coord end = {col, row}; + case SELECTION_RIGHT: + new_end = (struct coord){col, term->grid->view + row}; selection_find_word_boundary_right( - term, &end, term->selection.spaces_only, true); - new_end = (struct coord){end.col, term->grid->view + end.row}; + 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, term->selection.spaces_only); + 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, term->selection.spaces_only); + selection_find_line_boundary_right(term, &end); new_end = (struct coord){end.col, term->grid->view + end.row}; break; } @@ -1215,16 +1378,19 @@ selection_extend_normal(struct terminal *term, int col, int row, xassert(new_kind == SELECTION_CHAR_WISE || new_kind == SELECTION_WORD_WISE); - struct coord pivot_start = {new_start.col, new_start.row - term->grid->view}; + 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 = - (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}; + 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; } @@ -1235,8 +1401,8 @@ selection_extend_normal(struct terminal *term, int col, int row, 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, spaces_only); - selection_find_line_boundary_right(term, &pivot_end, spaces_only); + 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}; @@ -1362,6 +1528,7 @@ selection_extend(struct seat *seat, struct terminal *term, 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; @@ -1662,7 +1829,7 @@ send_clipboard_or_primary(struct seat *seat, int fd, const char *selection, return; } - size_t len = strlen(selection); + size_t len = selection != NULL ? strlen(selection) : 0; size_t async_idx = 0; switch (async_write(fd, selection, len, &async_idx)) { @@ -1701,7 +1868,6 @@ send(void *data, struct wl_data_source *wl_data_source, const char *mime_type, struct seat *seat = data; const struct wl_clipboard *clipboard = &seat->clipboard; - xassert(clipboard->text != NULL); send_clipboard_or_primary(seat, fd, clipboard->text, "clipboard"); } @@ -1720,7 +1886,7 @@ cancelled(void *data, struct wl_data_source *wl_data_source) clipboard->text = NULL; } -/* We don’t support dragging *from* */ +/* We don't support dragging *from* */ static void dnd_drop_performed(void *data, struct wl_data_source *wl_data_source) { @@ -1756,7 +1922,6 @@ primary_send(void *data, struct seat *seat = data; const struct wl_primary *primary = &seat->primary; - xassert(primary->text != NULL); send_clipboard_or_primary(seat, fd, primary->text, "primary"); } @@ -1841,6 +2006,7 @@ struct clipboard_receive { 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); @@ -1925,7 +2091,7 @@ decode_one_uri(struct clipboard_receive *ctx, char *uri, size_t len) ctx->cb(" ", 1, ctx->user); ctx->add_space = true; - if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { + if (streq(scheme, "file") && hostname_is_localhost(host)) { if (ctx->quote_paths) ctx->cb("'", 1, ctx->user); @@ -1988,6 +2154,8 @@ 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; @@ -2039,13 +2207,14 @@ fdm_receive(struct fdm *fdm, int fd, int events, void *data) break; case '\n': - if (!ctx->bracketed) + if (!no_strip && !bracketed) { p[i] = '\r'; + } break; case '\r': /* Convert \r\n -> \r */ - if (!ctx->bracketed && i + 1 < left && p[i + 1] == '\n') { + if (!no_strip && !bracketed && i + 1 < left && p[i + 1] == '\n') { i++; skip_one(); goto again; @@ -2058,16 +2227,19 @@ fdm_receive(struct fdm *fdm, int fd, int events, void *data) 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': - skip_one(); - goto again; + if (!no_strip) { + skip_one(); + goto again; + } + break; /* * In addition to stripping non-formatting C0 controls, - * XTerm has an option, “disallowedPasteControls”, that + * 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 + * 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 @@ -2077,7 +2249,7 @@ fdm_receive(struct fdm *fdm, int fd, int events, void *data) * handled above. */ case '\b': case '\x7f': case '\x00': - if (!ctx->bracketed) { + if (!no_strip && !bracketed) { skip_one(); goto again; } @@ -2098,8 +2270,8 @@ done: } static void -begin_receive_clipboard(struct terminal *term, int read_fd, - enum data_offer_mime_type mime_type, +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) { @@ -2132,6 +2304,7 @@ begin_receive_clipboard(struct terminal *term, int 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 @@ -2161,6 +2334,7 @@ err: 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) { @@ -2193,7 +2367,8 @@ text_from_clipboard(struct seat *seat, struct terminal *term, /* Don't keep our copy of the write-end open (or we'll never get EOF) */ close(write_fd); - begin_receive_clipboard(term, read_fd, clipboard->mime_type, cb, done, user); + begin_receive_clipboard( + term, no_strip, read_fd, clipboard->mime_type, cb, done, user); } static void @@ -2236,7 +2411,8 @@ selection_from_clipboard(struct seat *seat, struct terminal *term, uint32_t seri if (term->bracketed_paste) term_paste_data_to_slave(term, "\033[200~", 6); - text_from_clipboard(seat, term, &receive_offer, &receive_offer_done, term); + text_from_clipboard( + seat, term, false, &receive_offer, &receive_offer_done, term); } bool @@ -2305,7 +2481,7 @@ selection_to_primary(struct seat *seat, struct terminal *term, uint32_t serial) void text_from_primary( - struct seat *seat, struct terminal *term, + struct seat *seat, struct terminal *term, bool no_strip, void (*cb)(char *data, size_t size, void *user), void (*done)(void *user), void *user) { @@ -2343,7 +2519,8 @@ text_from_primary( /* Don't keep our copy of the write-end open (or we'll never get EOF) */ close(write_fd); - begin_receive_clipboard(term, read_fd, primary->mime_type, cb, done, user); + begin_receive_clipboard( + term, no_strip, read_fd, primary->mime_type, cb, done, user); } void @@ -2365,7 +2542,8 @@ selection_from_primary(struct seat *seat, struct terminal *term) if (term->bracketed_paste) term_paste_data_to_slave(term, "\033[200~", 6); - text_from_primary(seat, term, &receive_offer, &receive_offer_done, term); + text_from_primary( + seat, term, false, &receive_offer, &receive_offer_done, term); } static void @@ -2379,7 +2557,7 @@ select_mime_type_for_offer(const char *_mime_type, if (mime_type_map[i] == NULL) continue; - if (strcmp(_mime_type, mime_type_map[i]) == 0) { + if (streq(_mime_type, mime_type_map[i])) { mime_type = i; break; } @@ -2561,7 +2739,7 @@ enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial, reject_offer: /* Either terminal is already busy sending paste data, or mouse - * pointer isn’t over the grid */ + * pointer isn't over the grid */ seat->clipboard.window = NULL; wl_data_offer_accept(offer, serial, NULL); wl_data_offer_set_actions( @@ -2654,10 +2832,10 @@ drop(void *data, struct wl_data_device *wl_data_device) term_paste_data_to_slave(term, "\033[200~", 6); begin_receive_clipboard( - term, read_fd, clipboard->mime_type, + term, false, read_fd, clipboard->mime_type, &receive_dnd, &receive_dnd_done, ctx); - /* data offer is now “owned” by the receive context */ + /* data offer is now "owned" by the receive context */ clipboard->data_offer = NULL; clipboard->mime_type = DATA_OFFER_MIME_UNSET; } diff --git a/selection.h b/selection.h index c6d7f968..b6ad099a 100644 --- a/selection.h +++ b/selection.h @@ -63,12 +63,12 @@ bool text_to_primary( * point). */ void text_from_clipboard( - struct seat *seat, struct terminal *term, + 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, + struct seat *seat, struct terminal *term, bool no_strip, void (*cb)(char *data, size_t size, void *user), void (*dont)(void *user), void *user); @@ -78,9 +78,9 @@ void selection_start_scroll_timer( void selection_stop_scroll_timer(struct terminal *term); void selection_find_word_boundary_left( - struct terminal *term, struct coord *pos, bool spaces_only); + const struct terminal *term, struct coord *pos, bool spaces_only); void selection_find_word_boundary_right( - struct terminal *term, struct coord *pos, bool spaces_only, + 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); diff --git a/server.c b/server.c index ca55b8f3..25963325 100644 --- a/server.c +++ b/server.c @@ -4,6 +4,7 @@ #include <fcntl.h> #include <unistd.h> #include <errno.h> +#include <limits.h> #include <sys/types.h> #include <sys/socket.h> @@ -18,17 +19,18 @@ #include "log.h" #include "client-protocol.h" -#include "shm.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 { - const struct config *conf; + struct config *conf; struct fdm *fdm; struct reaper *reaper; struct wayland *wayl; @@ -154,10 +156,61 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) xassert(events & EPOLLIN); if (client->instance != NULL) { - uint8_t dummy[128]; - ssize_t count = read(fd, dummy, sizeof(dummy)); - LOG_WARN("client unexpectedly sent %zd bytes", count); - return true; /* TODO: shutdown instead? */ + 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) { @@ -212,6 +265,12 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) 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); @@ -301,7 +360,7 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) #undef CHECK_BUF_AND_NULL #undef CHECK_BUF - struct terminal_instance *instance = malloc(sizeof(struct terminal_instance)); + struct terminal_instance *instance = xmalloc(sizeof(struct terminal_instance)); const bool need_to_clone_conf = tll_length(overrides)> 0 || @@ -332,7 +391,8 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) instance->terminal = term_init( conf != NULL ? conf : server->conf, server->fdm, server->reaper, server->wayl, "footclient", cwd, token, - cdata.argc, argv, envp, &term_shutdown_handler, instance); + NULL, cdata.argc, argv, (const char *const *)envp, + &term_shutdown_handler, instance); if (instance->terminal == NULL) { LOG_ERR("failed to instantiate new terminal"); @@ -468,7 +528,7 @@ prepare_socket(int fd) } int const socket_options[] = { SO_DOMAIN, SO_ACCEPTCONN, SO_TYPE }; - int const socket_options_values[] = { AF_UNIX, 1, SOCK_STREAM}; + 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)); @@ -483,6 +543,8 @@ prepare_socket(int fd) 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]); @@ -494,7 +556,7 @@ prepare_socket(int fd) } struct server * -server_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, +server_init(struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl) { int fd; @@ -606,3 +668,23 @@ server_destroy(struct server *server) 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 index 50797540..683ad74d 100644 --- a/server.h +++ b/server.h @@ -6,6 +6,9 @@ #include "wayland.h" struct server; -struct server *server_init(const struct config *conf, struct fdm *fdm, +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 index 3ada8266..a73ba1f2 100644 --- a/shm-formats.h +++ b/shm-formats.h @@ -117,5 +117,22 @@ static const struct shm_formats { {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 index 4394dbe9..5c1573ad 100644 --- a/shm.c +++ b/shm.c @@ -13,7 +13,6 @@ #include <pixman.h> -#include <fcft/stride.h> #include <tllist.h> #define LOG_MODULE "shm" @@ -21,12 +20,17 @@ #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 @@ -57,6 +61,8 @@ 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; @@ -80,6 +86,9 @@ struct buffer_private { size_t size; bool scrollable; + + void (*release_cb)(struct buffer *buf, void *data); + void *cb_data; }; struct buffer_chain { @@ -87,6 +96,12 @@ struct buffer_chain { 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; @@ -102,6 +117,12 @@ 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) { @@ -110,6 +131,7 @@ buffer_destroy_dont_close(struct buffer *buf) if (buf->pix[i] != NULL) pixman_image_unref(buf->pix[i]); } + if (buf->wl_buf != NULL) wl_buffer_destroy(buf->wl_buf); @@ -151,7 +173,9 @@ buffer_destroy(struct buffer_private *buf) pool_unref(buf->pool); buf->pool = NULL; - pixman_region32_fini(&buf->public.dirty); + for (size_t i = 0; i < buf->public.pix_instances; i++) + pixman_region32_fini(&buf->public.dirty[i]); + free(buf->public.dirty); free(buf); } @@ -210,6 +234,10 @@ buffer_release(void *data, struct wl_buffer *wl_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); + } } } @@ -217,7 +245,6 @@ static const struct wl_buffer_listener buffer_listener = { .release = &buffer_release, }; -#if __SIZEOF_POINTER__ == 8 static size_t page_size(void) { @@ -234,7 +261,6 @@ page_size(void) xassert(size > 0); return size; } -#endif static bool instantiate_offset(struct buffer_private *buf, off_t new_offset) @@ -248,14 +274,14 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) void *mmapped = MAP_FAILED; struct wl_buffer *wl_buf = NULL; - pixman_image_t **pix = xcalloc(buf->public.pix_instances, sizeof(*pix)); + 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, - WL_SHM_FORMAT_ARGB8888); + buf->chain->shm_format); if (wl_buf == NULL) { LOG_ERR("failed to create SHM buffer"); @@ -265,8 +291,10 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) /* 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( - PIXMAN_a8r8g8b8, buf->public.width, buf->public.height, + 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; @@ -316,7 +344,15 @@ get_new_buffers(struct buffer_chain *chain, size_t count, size_t total_size = 0; for (size_t i = 0; i < count; i++) { - stride[i] = stride_for_format_and_width(PIXMAN_a8r8g8b8, widths[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]; } @@ -331,7 +367,20 @@ get_new_buffers(struct buffer_chain *chain, size_t count, /* Backing memory for SHM */ #if defined(MEMFD_CREATE) - pool_fd = memfd_create("foot-wayland-shm-buffer-pool", MFD_CLOEXEC | MFD_ALLOW_SEALING); + /* + * 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); @@ -345,9 +394,11 @@ get_new_buffers(struct buffer_chain *chain, size_t count, 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_size() - 1) + ? (max_pool_size / 4) & ~(page_sz - 1) : 0; off_t memfd_size = chain->scrollable && max_pool_size > 0 ? max_pool_size @@ -357,7 +408,8 @@ get_new_buffers(struct buffer_chain *chain, size_t count, off_t memfd_size = total_size; #endif - xassert(chain->scrollable || (offset == 0 && memfd_size == total_size)); + /* Page align */ + memfd_size = (memfd_size + page_sz - 1) & ~(page_sz - 1); LOG_DBG("memfd-size: %lu, initial offset: %lu", memfd_size, offset); @@ -389,6 +441,9 @@ get_new_buffers(struct buffer_chain *chain, size_t count, 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; @@ -458,6 +513,8 @@ get_new_buffers(struct buffer_chain *chain, size_t count, .offset = 0, .size = sizes[i], .scrollable = chain->scrollable, + .release_cb = chain->release_cb, + .cb_data = chain->cb_data, }; if (!instantiate_offset(buf, offset)) { @@ -470,7 +527,12 @@ get_new_buffers(struct buffer_chain *chain, size_t count, else tll_push_front(chain->bufs, buf); - pixman_region32_init(&buf->public.dirty); + 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; @@ -487,7 +549,7 @@ get_new_buffers(struct buffer_chain *chain, size_t count, #endif if (!(bufs[0] && shm_can_scroll(bufs[0]))) { - /* We only need to keep the pool FD open if we’re going to SHM + /* We only need to keep the pool FD open if we're going to SHM * scroll it */ close(pool_fd); pool->fd = -1; @@ -527,7 +589,7 @@ struct buffer * shm_get_buffer(struct buffer_chain *chain, int width, int height) { LOG_DBG( - "chain=%p: looking for a re-usable %dx%d buffer " + "chain=%p: looking for a reusable %dx%d buffer " "among %zu potential buffers", (void *)chain, width, height, tll_length(chain->bufs)); @@ -546,16 +608,16 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height) buf->public.age++; else #if FORCED_DOUBLE_BUFFERING - if (buf->age == 0) - buf->age++; + if (buf->public.age == 0) + buf->public.age++; else #endif { - if (cached == NULL) + if (cached == NULL) { cached = buf; - else { + } else { /* We have multiple buffers eligible for - * re-use. Pick the “youngest” one, and mark the + * reuse. Pick the "youngest" one, and mark the * other one for purging */ if (buf->public.age < cached->public.age) { shm_unref(&cached->public); @@ -565,8 +627,8 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height) * TODO: I think we _can_ use shm_unref() * here... * - * shm_unref() may remove ‘it’, but that - * should be safe; “our” tll_foreach() already + * 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)) @@ -577,9 +639,10 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height) } if (cached != NULL) { - LOG_DBG("re-using buffer %p from cache", (void *)cached); + LOG_DBG("reusing buffer %p from cache", (void *)cached); cached->busy = true; - pixman_region32_clear(&cached->public.dirty); + 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; } @@ -927,14 +990,90 @@ shm_unref(struct buffer *_buf) } struct buffer_chain * -shm_chain_new(struct wl_shm *shm, bool scrollable, size_t pix_instances) +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 = shm, + .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; } @@ -954,3 +1093,17 @@ shm_chain_free(struct buffer_chain *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 index 440cfa1d..c58a8531 100644 --- a/shm.h +++ b/shm.h @@ -9,6 +9,9 @@ #include <tllist.h> +#include "config.h" +#include "wayland.h" + struct damage; struct buffer { @@ -24,21 +27,39 @@ struct buffer { unsigned age; - pixman_region32_t dirty; + /* + * 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 wl_shm *shm, bool scrollable, size_t pix_instances); + 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 + * 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. * @@ -46,7 +67,7 @@ void shm_chain_free(struct buffer_chain *chain); */ struct buffer *shm_get_buffer(struct buffer_chain *chain, int width, int height); /* - * Returns many buffers, described by ‘info’, all sharing the same SHM + * Returns many buffers, described by 'info', all sharing the same SHM * buffer pool. * * Never returns cached buffers. However, the newly created buffers diff --git a/sixel.c b/sixel.c index 592f48f8..187f1348 100644 --- a/sixel.c +++ b/sixel.c @@ -10,12 +10,50 @@ #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) { @@ -24,11 +62,17 @@ sixel_fini(struct terminal *term) free(term->sixel.shared_palette); } -void +sixel_put sixel_init(struct terminal *term, int p1, int p2, int p3) { /* - * P1: pixel aspect ratio - unimplemented + * 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) @@ -38,30 +82,99 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) 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.max_non_empty_row_no = -1; - term->sixel.row_byte_ofs = 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 = xmalloc(1 * 6 * sizeof(term->sixel.image.data[0])); - term->sixel.image.width = 1; - term->sixel.image.height = 6; + 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; - /* TODO: default palette */ + /* + * 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 */ } @@ -69,42 +182,38 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.palette = term->sixel.shared_palette; } - uint32_t bg = 0; - - switch (term->vt.attrs.bg_src) { - case COLOR_RGB: - bg = term->vt.attrs.bg; - break; - - case COLOR_BASE16: - case COLOR_BASE256: - bg = term->colors.table[term->vt.attrs.bg]; - break; - - case COLOR_DEFAULT: - bg = term->colors.bg; - break; - } - - term->sixel.default_bg = term->sixel.transparent_bg - ? 0x00000000u - : 0xffu << 24 | bg; - - for (size_t i = 0; i < 1 * 6; i++) - term->sixel.image.data[i] = term->sixel.default_bg; - 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) { - if (sixel->pix != NULL) - pixman_image_unref(sixel->pix); + sixel_invalidate_cache(sixel); - free(sixel->data); - sixel->pix = NULL; - sixel->data = NULL; + if (sixel->original.pix != NULL) + pixman_image_unref(sixel->original.pix); + + free(sixel->original.data); + sixel->original.pix = NULL; + sixel->original.data = NULL; } void @@ -132,7 +241,7 @@ sixel_erase(struct terminal *term, struct sixel *sixel) row->dirty = true; - for (int c = sixel->pos.col; c < min(sixel->cols, term->cols); c++) + for (int c = sixel->pos.col; c < min(sixel->pos.col + sixel->cols, term->cols); c++) row->cells[c].attrs.clean = 0; } @@ -331,6 +440,8 @@ sixel_scroll_up(struct terminal *term, int rows) } } + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); verify_sixels(term); } @@ -355,6 +466,8 @@ sixel_scroll_down(struct terminal *term, int rows) break; } + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); verify_sixels(term); } @@ -367,10 +480,14 @@ blend_new_image_over_old(const struct terminal *term, xassert(pix != NULL); xassert(opaque != NULL); - const int six_ofs_x = six->pos.col * term->cell_width; - const int six_ofs_y = six->pos.row * term->cell_height; - const int img_ofs_x = col * term->cell_width; - const int img_ofs_y = row * term->cell_height; + /* + * 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); @@ -400,7 +517,7 @@ blend_new_image_over_old(const struct terminal *term, */ pixman_image_composite32( PIXMAN_OP_OVER_REVERSE, - six->pix, NULL, *pix, + 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, @@ -417,15 +534,15 @@ blend_new_image_over_old(const struct terminal *term, * old image, or the next cell boundary, whichever comes * first. */ - int bounding_x = six_ofs_x + six->width > img_ofs_x + img_width + int bounding_x = six_ofs_x + six->original.width > img_ofs_x + img_width ? min( - six_ofs_x + six->width, - (box->x2 + term->cell_width - 1) / term->cell_width * term->cell_width) + 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->height > img_ofs_y + img_height + int bounding_y = six_ofs_y + six->original.height > img_ofs_y + img_height ? min( - six_ofs_y + six->height, - (box->y2 + term->cell_height - 1) / term->cell_height * term->cell_height) + 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 */ @@ -449,7 +566,7 @@ blend_new_image_over_old(const struct terminal *term, 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( - PIXMAN_a8r8g8b8, new_width, new_height, new_data, stride); + term->sixel.pixman_fmt, new_width, new_height, new_data, stride); #if defined(_DEBUG) /* Fill new image with an easy-to-recognize color (green) */ @@ -465,7 +582,7 @@ blend_new_image_over_old(const struct terminal *term, /* Copy the bottom tile of the old sixel image into the new pixmap */ pixman_image_composite32( PIXMAN_OP_SRC, - six->pix, NULL, pix2, + 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, @@ -474,7 +591,7 @@ blend_new_image_over_old(const struct terminal *term, /* Copy the right tile of the old sixel image into the new pixmap */ pixman_image_composite32( PIXMAN_OP_SRC, - six->pix, NULL, pix2, + 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, @@ -548,27 +665,27 @@ sixel_overwrite(struct terminal *term, struct sixel *six, pixman_region32_t six_rect; pixman_region32_init_rect( &six_rect, - six->pos.col * term->cell_width, six->pos.row * term->cell_height, - six->width, six->height); + 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 * term->cell_width, row * term->cell_height, - width * term->cell_width, height * term->cell_height); + 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(&cell_intersection)); + 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); @@ -583,12 +700,12 @@ sixel_overwrite(struct terminal *term, struct sixel *six, 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 % term->cell_width == 0); - xassert(boxes[i].y1 % term->cell_height == 0); + 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 / term->cell_width; - const int new_row = boxes[i].y1 / term->cell_height; + 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); @@ -597,33 +714,45 @@ sixel_overwrite(struct terminal *term, struct sixel *six, 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->data; + 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 * term->cell_width; - const int y_ofs = boxes[i].y1 - six->pos.row * term->cell_height; + 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->width + x_ofs], + &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( - PIXMAN_a8r8g8b8, - new_width, new_height, new_data, new_width * sizeof(uint32_t)); + term->sixel.pixman_fmt, new_width, new_height, new_data, new_width * sizeof(uint32_t)); struct sixel new_six = { - .data = new_data, - .pix = new_pix, - .width = new_width, - .height = new_height, + .pix = NULL, + .width = -1, + .height = -1, .pos = {.col = new_col, .row = new_row}, - .cols = (new_width + term->cell_width - 1) / term->cell_width, - .rows = (new_height + term->cell_height - 1) / term->cell_height, + .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) @@ -746,6 +875,8 @@ sixel_overwrite_by_rectangle( } 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); } @@ -802,6 +933,8 @@ sixel_overwrite_by_row(struct terminal *term, int _row, int col, int width) } } + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); } @@ -818,23 +951,94 @@ sixel_overwrite_at_cursor(struct terminal *term, int width) void sixel_cell_size_changed(struct terminal *term) { - struct grid *g = term->grid; + tll_foreach(term->normal.sixel_images, it) + sixel_invalidate_cache(&it->item); - term->grid = &term->normal; - tll_foreach(term->normal.sixel_images, it) { - struct sixel *six = &it->item; - six->rows = (six->height + term->cell_height - 1) / term->cell_height; - six->cols = (six->width + term->cell_width - 1) / term->cell_width; + 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; } - term->grid = &term->alt; - tll_foreach(term->alt.sixel_images, it) { - struct sixel *six = &it->item; - six->rows = (six->height + term->cell_height - 1) / term->cell_height; - six->cols = (six->width + term->cell_width - 1) / term->cell_width; - } + /* Cache should be invalid */ + xassert(six->scaled.data == NULL); + xassert(six->scaled.pix == NULL); + xassert(six->scaled.width < 0); + xassert(six->scaled.height < 0); - term->grid = g; + 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 @@ -844,7 +1048,7 @@ sixel_reflow_grid(struct terminal *term, struct grid *grid) struct grid *active_grid = term->grid; term->grid = grid; - /* Need the “real” list to be empty from the beginning */ + /* 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); @@ -893,18 +1097,19 @@ sixel_reflow_grid(struct terminal *term, struct grid *grid) continue; } - /* Sixels that didn’t overlap may now do so, which isn’t + /* 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.pix, &it->item.opaque); + &it->item.original.pix, &it->item.opaque); - if (it->item.data != pixman_image_get_data(it->item.pix)) { - it->item.data = pixman_image_get_data(it->item.pix); - it->item.width = pixman_image_get_width(it->item.pix); - it->item.height = pixman_image_get_height(it->item.pix); - it->item.cols = (it->item.width + term->cell_width - 1) / term->cell_width; - it->item.rows = (it->item.height + term->cell_height - 1) / term->cell_height; + 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); @@ -926,11 +1131,79 @@ sixel_reflow(struct terminal *term) void sixel_unhook(struct terminal *term) { - if (term->sixel.image.height > term->sixel.max_non_empty_row_no + 1) { - LOG_DBG( - "last row only partially filled, reducing image height: %d -> %d", - term->sixel.image.height, term->sixel.max_non_empty_row_no + 1); - term->sixel.image.height = term->sixel.max_non_empty_row_no + 1; + 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; @@ -967,10 +1240,9 @@ sixel_unhook(struct terminal *term) 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 (+ optional newline at the end) */ + /* Total number of rows needed by image */ const int rows_needed = - (term->sixel.image.height + term->cell_height - 1) / term->cell_height + - (term->sixel.cursor_right_of_graphics ? 0 : 1); + (term->sixel.image.height + term->cell_height - 1) / term->cell_height; bool free_image_data = true; @@ -1005,13 +1277,27 @@ sixel_unhook(struct terminal *term) } struct sixel image = { - .data = img_data, - .width = width, - .height = height, + .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); @@ -1019,16 +1305,65 @@ sixel_unhook(struct terminal *term) LOG_DBG("generating %s %dx%d pixman image at %d-%d", image.opaque ? "opaque" : "transparent", - image.width, image.height, + image.original.width, image.original.height, image.pos.row, image.pos.row + image.rows); - image.pix = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, image.width, image.height, img_data, stride); + 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]; @@ -1041,38 +1376,19 @@ sixel_unhook(struct terminal *term) row->cells[col].attrs.clean = 0; } - if (do_scroll) { - /* - * Linefeed, *unless* we're on the very last row of - * the final image (not just this chunk) and private - * mode 8452 (leave cursor at the right of graphics) - * is enabled. - */ - if (term->sixel.cursor_right_of_graphics && - rows_avail == 0 && - i >= image.rows - 1) - { - term_cursor_to( - term, - term->grid->cursor.point.row, - min(image.pos.col + image.cols, term->cols - 1)); - } else { - term_linefeed(term); - term_carriage_return(term); - } - } } _sixel_overwrite_by_rectangle( term, image.pos.row, image.pos.col, image.rows, image.cols, - &image.pix, &image.opaque); + &image.original.pix, &image.opaque); - if (image.data != pixman_image_get_data(image.pix)) { - image.data = pixman_image_get_data(image.pix); - image.width = pixman_image_get_width(image.pix); - image.height = pixman_image_get_height(image.pix); - image.cols = (image.width + term->cell_width - 1) / term->cell_width; - image.rows = (image.height + term->cell_height - 1) / term->cell_height; + 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); @@ -1087,6 +1403,7 @@ sixel_unhook(struct terminal *term) 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}; @@ -1097,58 +1414,82 @@ sixel_unhook(struct terminal *term) 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 -resize_horizontally(struct terminal *term, int new_width) +static void ALWAYS_INLINE inline +memset_u32(uint32_t *data, uint32_t value, size_t count) { - LOG_DBG("resizing image horizontally: %dx(%d) -> %dx(%d)", - term->sixel.image.width, term->sixel.image.height, - new_width, term->sixel.image.height); + static_assert(sizeof(wchar_t) == 4, "wchar_t is not 4 bytes"); + wmemset((wchar_t *)data, (wchar_t)value, count); +} - if (unlikely(new_width > term->sixel.max_width)) { +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 = term->sixel.max_width; + new_width_mutable = term->sixel.max_width; } - if (unlikely(term->sixel.image.width == new_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 height = term->sixel.image.height; + const int new_width = new_width_mutable; - int alloc_height = (height + 6 - 1) / 6 * 6; + 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.default_bg; + uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; /* Copy old rows, and initialize new columns to background color */ - for (int r = 0; r < height; r++) { - memcpy(&new_data[r * new_width], - &old_data[r * old_width], - old_width * sizeof(uint32_t)); - - for (int c = old_width; c < new_width; c++) - new_data[r * new_width + c] = bg; + 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; - term->sixel.row_byte_ofs = term->sixel.pos.row * 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, int new_height) +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, @@ -1162,12 +1503,19 @@ resize_vertically(struct terminal *term, int new_height) 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 + 6 - 1) / 6 * 6; + int alloc_height = (new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; - xassert(width > 0); 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)); @@ -1176,53 +1524,91 @@ resize_vertically(struct terminal *term, int new_height) return false; } - uint32_t bg = term->sixel.default_bg; + const uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; - /* Initialize new rows to background color */ - for (int r = old_height; r < new_height; r++) { - for (int c = 0; c < width; c++) - new_data[r * width + c] = bg; - } + 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.height = new_height; + term->sixel.image.p = &term->sixel.image.data[ofs]; + return true; } static bool -resize(struct terminal *term, int new_width, int new_height) +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, new_height); + new_width_mutable, new_height_mutable); - if (unlikely(new_width > term->sixel.max_width)) { + if (unlikely(new_width_mutable > term->sixel.max_width)) { LOG_WARN("maximum image width exceeded, truncating"); - new_width = term->sixel.max_width; + new_width_mutable = term->sixel.max_width; } - if (unlikely(new_height > term->sixel.max_height)) { + if (unlikely(new_height_mutable > term->sixel.max_height)) { LOG_WARN("maximum image height exceeded, truncating"); - new_height = term->sixel.max_height; + 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; - int alloc_new_width = new_width; - int alloc_new_height = (new_height + 6 - 1) / 6 * 6; xassert(alloc_new_height >= new_height); - xassert(alloc_new_height - new_height < 6); + xassert(alloc_new_height - new_height < sixel_row_height); uint32_t *new_data = NULL; - uint32_t bg = term->sixel.default_bg; + 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, alloc_new_width * alloc_new_height * sizeof(uint32_t)); + 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; @@ -1233,80 +1619,176 @@ resize(struct terminal *term, int new_width, int new_height) } else { /* Width (and thus stride) change - need to allocate a new buffer */ xassert(new_width > old_width); - new_data = xmalloc(alloc_new_width * alloc_new_height * sizeof(uint32_t)); + 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 */ - for (int r = 0; r < min(old_height, new_height); r++) { - memcpy(&new_data[r * new_width], &old_data[r * old_width], old_width * sizeof(uint32_t)); + const int row_copy_count = min(old_height, alloc_new_height); + const uint32_t *end = &new_data[row_copy_count * new_width]; - for (int c = old_width; c < new_width; c++) - new_data[r * new_width + c] = bg; + 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); } - /* Initialize new rows to background color */ - for (int r = old_height; r < new_height; r++) { - for (int c = 0; c < new_width; c++) - new_data[r * new_width + c] = bg; + 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.row_byte_ofs = term->sixel.pos.row * new_width; + 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(struct terminal *term, int col, int width, uint32_t color, uint8_t sixel) +sixel_add_generic(struct terminal *term, uint32_t *data, int stride, uint32_t color, + uint8_t sixel) { - xassert(term->sixel.pos.col < term->sixel.image.width); - xassert(term->sixel.pos.row < term->sixel.image.height); + const int pan = term->sixel.pan; - size_t ofs = term->sixel.row_byte_ofs + col; - uint32_t *data = &term->sixel.image.data[ofs]; - - int max_non_empty_row = -1; - int row = term->sixel.pos.row; - - for (int i = 0; i < 6; i++, sixel >>= 1, data += width) { + for (int i = 0; i < 6; i++, sixel >>= 1) { if (sixel & 1) { - *data = color; - max_non_empty_row = row + i; - } + for (int r = 0; r < pan; r++, data += stride) + *data = color; + } else + data += stride * pan; } xassert(sixel == 0); +} - term->sixel.max_non_empty_row_no = max( - term->sixel.max_non_empty_row_no, - max_non_empty_row); +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(struct terminal *term, uint8_t c, unsigned count) +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; - for (unsigned i = 0; i < count; i++, col++) - sixel_add(term, col, width, color, c); + 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); - term->sixel.pos.col = col; } +IGNORE_WARNING("-Wpedantic") + static void -decsixel(struct terminal *term, uint8_t c) +decsixel_generic(struct terminal *term, uint8_t c) { switch (c) { case '"': @@ -1319,6 +1801,7 @@ decsixel(struct terminal *term, uint8_t c) term->sixel.state = SIXEL_DECGRI; term->sixel.param = 0; term->sixel.param_idx = 0; + term->sixel.repeat_count = 1; break; case '#': @@ -1331,37 +1814,30 @@ decsixel(struct terminal *term, uint8_t c) 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 + * 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 '-': - term->sixel.pos.row += 6; + case '-': /* GNL - Graphical New Line */ + term->sixel.pos.row += 6 * term->sixel.pan; term->sixel.pos.col = 0; - term->sixel.row_byte_ofs += term->sixel.image.width * 6; + 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.height) { - if (!resize_vertically(term, term->sixel.pos.row + 6)) - term->sixel.pos.col = term->sixel.max_width + 1; + 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 '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': - case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': - case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': - case '[': case '\\': case ']': case '^': case '_': case '`': case 'a': - case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': - case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': - case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': - case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': - case '~': - sixel_add_many(term, c - 63, 1); + case '?' ... '~': + sixel_add_many_generic(term, c - 63, 1); break; case ' ': @@ -1375,6 +1851,17 @@ decsixel(struct terminal *term, uint8_t c) } } +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) { @@ -1404,63 +1891,141 @@ decgra(struct terminal *term, uint8_t c) pan = pan > 0 ? pan : 1; pad = pad > 0 ? pad : 1; - LOG_DBG("pan=%u, pad=%u (aspect ratio = %u), size=%ux%u", - pan, pad, pan / pad, ph, pv); + 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); - - /* This ensures the sixel’s final image size is *at least* - * this large */ - term->sixel.max_non_empty_row_no = - min(pv, term->sixel.image.height) - 1; } term->sixel.state = SIXEL_DECSIXEL; - decsixel(term, c); + + /* 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(struct terminal *term, uint8_t c) +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': - term->sixel.param *= 10; - term->sixel.param += c - '0'; + 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 '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': - case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': - case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': - case '[': case '\\': case ']': case '^': case '_': case '`': case 'a': - case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': - case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': - case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': - case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': - case '~': { - unsigned count = term->sixel.param; - if (likely(count > 0)) - sixel_add_many(term, c - 63, count); - else if (unlikely(count == 0)) - sixel_add_many(term, c - 63, 1); + 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; - sixel_put(term, c); + 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) { @@ -1499,12 +2064,12 @@ decgci(struct terminal *term, uint8_t c) int sat = min(c3, 100); /* - * Sixel’s HLS use the following primary color hues: + * Sixel's HLS use the following primary color hues: * blue: 0° * red: 120° * green: 240° * - * While “standard” HSL uses: + * While "standard" HSL uses: * red: 0° * green: 120° * blue: 240° @@ -1521,15 +2086,14 @@ decgci(struct terminal *term, uint8_t c) } case 2: { /* RGB */ - uint8_t r = 255 * min(c1, 100) / 100; - uint8_t g = 255 * min(c2, 100) / 100; - uint8_t b = 255 * min(c3, 100) / 100; + 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 %hhu/%hhu/%hhu", + LOG_DBG("setting palette #%d = RGB %hu/%hu/%hu", term->sixel.color_idx, r, g, b); - term->sixel.palette[term->sixel.color_idx] = - 0xffu << 24 | r << 16 | g << 8 | b; + term->sixel.palette[term->sixel.color_idx] = color_decode_srgb(term, r, g, b); break; } } @@ -1537,19 +2101,36 @@ decgci(struct terminal *term, uint8_t c) term->sixel.color = term->sixel.palette[term->sixel.color_idx]; term->sixel.state = SIXEL_DECSIXEL; - decsixel(term, c); + + if (likely(term->sixel.pan == 1 && term->sixel.pad == 1)) + decsixel_ar_11(term, c); + else + decsixel_generic(term, c); break; } } } -void -sixel_put(struct terminal *term, uint8_t c) +static void +sixel_put_generic(struct terminal *term, uint8_t c) { switch (term->sixel.state) { - case SIXEL_DECSIXEL: decsixel(term, c); break; + case SIXEL_DECSIXEL: decsixel_generic(term, c); break; case SIXEL_DECGRA: decgra(term, c); break; - case SIXEL_DECGRI: decgri(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; } diff --git a/sixel.h b/sixel.h index f72b4dc4..ab8a5050 100644 --- a/sixel.h +++ b/sixel.h @@ -6,10 +6,11 @@ #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); -void sixel_init(struct terminal *term, int p1, int p2, int p3); -void sixel_put(struct terminal *term, uint8_t c); +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); @@ -19,6 +20,7 @@ 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); diff --git a/slave.c b/slave.c index 2f23e996..62899372 100644 --- a/slave.c +++ b/slave.c @@ -19,14 +19,18 @@ #include "debug.h" #include "macros.h" -#include "terminal.h" #include "tokenize.h" -#include "version.h" +#include "util.h" #include "xmalloc.h" extern char **environ; -#if defined(__FreeBSD__) +struct environ { + size_t count; + char **envp; +}; + +#if !defined(EXECVPE) static char * find_file_in_path(const char *file) { @@ -51,7 +55,7 @@ find_file_in_path(const char *file) path != NULL; path = strtok(NULL, ":")) { - char *full = xasprintf("%s/%s", path, file); + char *full = xstrjoin3(path, "/", file); if (access(full, F_OK) == 0) { free(path_list); return full; @@ -77,11 +81,11 @@ foot_execvpe(const char *file, char *const argv[], char *const envp[]) return ret; } -#else /* !__FreeBSD__ */ +#else /* EXECVPE */ #define foot_execvpe(file, argv, envp) execvpe(file, argv, envp) -#endif /* !__FreeBSD__ */ +#endif /* EXECVPE */ static bool is_valid_shell(const char *shell) @@ -117,7 +121,7 @@ is_valid_shell(const char *shell) if (line[0] == '#') continue; - if (strcmp(line, shell) == 0) { + if (streq(line, shell)) { fclose(f); return true; } @@ -154,6 +158,7 @@ emit_one_notification(int fd, const struct user_notification *notif) 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) { @@ -176,7 +181,8 @@ emit_one_notification(int fd, const struct user_notification *notif) } static bool -emit_notifications_of_kind(int fd, const user_notifications_t *notifications, +emit_notifications_of_kind(int fd, + const user_notifications_t *notifications, enum user_notification_kind kind) { tll_foreach(*notifications, it) { @@ -304,9 +310,69 @@ err: _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, - char *const *envp, const env_var_list_t *extra_env_vars, + 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) { @@ -351,14 +417,76 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, _exit(errno_copy); } - setenv("TERM", term_env, 1); - setenv("TERM_PROGRAM", "foot", 1); - setenv("TERM_PROGRAM_VERSION", FOOT_VERSION_SHORT, 1); - setenv("COLORTERM", "truecolor", 1); - setenv("PWD", cwd, 1); + /* 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) - setenv("TERMINFO", FOOT_TERMINFO_PATH, 1); + add_to_env(&custom_env, "TERMINFO", FOOT_TERMINFO_PATH); #endif if (extra_env_vars != NULL) { @@ -367,9 +495,9 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, const char *value = it->item.value; if (strlen(value) == 0) - unsetenv(name); + del_from_env(&custom_env, name); else - setenv(name, value, 1); + add_to_env(&custom_env, name, value); } } @@ -393,14 +521,23 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, } if (is_valid_shell(shell_argv[0])) - setenv("SHELL", shell_argv[0], 1); + add_to_env(&custom_env, "SHELL", shell_argv[0]); - slave_exec(ptmx, shell_argv, envp != NULL ? envp : environ, + 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); diff --git a/slave.h b/slave.h index b1c08f14..26d93abb 100644 --- a/slave.h +++ b/slave.h @@ -7,7 +7,7 @@ #include "user-notification.h" pid_t slave_spawn( - int ptmx, int argc, const char *cwd, char *const *argv, char *const *envp, + 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 index 90b892f3..17c821b5 100644 --- a/spawn.c +++ b/spawn.c @@ -15,9 +15,9 @@ #include "debug.h" #include "xmalloc.h" -bool +pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[], - int stdin_fd, int stdout_fd, int stderr_fd, + 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}; @@ -104,16 +104,16 @@ spawn(struct reaper *reaper, const char *cwd, char *const argv[], close(pipe_fds[0]); if (ret == 0) { - reaper_add(reaper, pid, NULL, NULL); - return true; + reaper_add(reaper, pid, cb, cb_data); + return pid; } else if (ret < 0) { LOG_ERRNO("failed to read from pipe"); - return false; + return -1; } else { LOG_ERRNO_P(errno_copy, "%s: failed to spawn", argv[0]); errno = errno_copy; waitpid(pid, NULL, 0); - return false; + return -1; } err: @@ -121,7 +121,7 @@ err: close(pipe_fds[0]); if (pipe_fds[1] != -1) close(pipe_fds[1]); - return false; + return -1; } bool @@ -145,7 +145,7 @@ spawn_expand_template(const struct config_spawn_template *template, expanded[len] = '\0'; \ } while (0) - *argv = malloc((*argc + 1) * sizeof((*argv)[0])); + *argv = xmalloc((*argc + 1) * sizeof((*argv)[0])); /* Expand the provided keys */ for (size_t i = 0; i < *argc; i++) { diff --git a/spawn.h b/spawn.h index 0fc95041..1693f1a8 100644 --- a/spawn.h +++ b/spawn.h @@ -1,12 +1,14 @@ #pragma once #include <stdbool.h> +#include <unistd.h> + #include "config.h" #include "reaper.h" -bool spawn(struct reaper *reaper, const char *cwd, char *const argv[], - int stdin_fd, int stdout_fd, int stderr_fd, - const char *xdg_activation_token); +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, diff --git a/subprojects/wayland-protocols.wrap b/subprojects/wayland-protocols.wrap new file mode 100644 index 00000000..74e5e913 --- /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 index 04153513..8eafbcbe 100644 --- a/terminal.c +++ b/terminal.c @@ -27,6 +27,7 @@ #include "commands.h" #include "config.h" #include "debug.h" +#include "emoji-variation-sequences.h" #include "extract.h" #include "grid.h" #include "ime.h" @@ -44,32 +45,16 @@ #include "util.h" #include "vt.h" #include "xmalloc.h" +#include "xsnprintf.h" #define PTMX_TIMING 0 -const char *const XCURSOR_HIDDEN = "hidden"; -const char *const XCURSOR_LEFT_PTR = "left_ptr"; -const char *const XCURSOR_TEXT = "text"; -const char *const XCURSOR_TEXT_FALLBACK = "xterm"; -//const char *const XCURSOR_HAND2 = "hand2"; -const char *const XCURSOR_TOP_LEFT_CORNER = "top_left_corner"; -const char *const XCURSOR_TOP_RIGHT_CORNER = "top_right_corner"; -const char *const XCURSOR_BOTTOM_LEFT_CORNER = "bottom_left_corner"; -const char *const XCURSOR_BOTTOM_RIGHT_CORNER = "bottom_right_corner"; -const char *const XCURSOR_LEFT_SIDE = "left_side"; -const char *const XCURSOR_RIGHT_SIDE = "right_side"; -const char *const XCURSOR_TOP_SIDE = "top_side"; -const char *const XCURSOR_BOTTOM_SIDE = "bottom_side"; - static void enqueue_data_for_slave(const void *data, size_t len, size_t offset, ptmx_buffer_list_t *buffer_list) { - void *copy = xmalloc(len); - memcpy(copy, data, len); - struct ptmx_buffer queued = { - .data = copy, + .data = xmemdup(data, len), .len = len, .idx = offset, }; @@ -135,7 +120,10 @@ term_to_slave(struct terminal *term, const void *data, size_t len) return false; } - if (tll_length(term->ptmx_buffers) > 0 || term->is_sending_paste_data) { + 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 @@ -207,25 +195,41 @@ fdm_ptmx_out(struct fdm *fdm, int fd, int events, void *data) 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->utempter_path == NULL) + if (conf->utmp_helper_path == NULL) return true; - char *const argv[] = {conf->utempter_path, "add", getenv("WAYLAND_DISPLAY"), NULL}; - return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); + 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->utempter_path == NULL) + if (conf->utmp_helper_path == NULL) return true; - char *const argv[] = {conf->utempter_path, "del", getenv("WAYLAND_DISPLAY"), NULL}; - return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); + 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 @@ -258,8 +262,8 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) 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 + * 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. */ @@ -365,6 +369,20 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) 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; @@ -373,12 +391,16 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) 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); } @@ -405,12 +427,12 @@ fdm_flash(struct fdm *fdm, int fd, int events, void *data) (unsigned long long)expiration_count); term->flash.active = false; - render_refresh(term); + render_overlay(term); - /* Work around Sway bug - unmapping a sub-surface does not damage - * the underlying surface */ - term_damage_margins(term); - term_damage_view(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; } @@ -501,6 +523,9 @@ term_arm_blink_timer(struct terminal *term) 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); @@ -625,12 +650,59 @@ fdm_title_update_timeout(struct fdm *fdm, int fd, int events, void *data) struct itimerspec reset = {{0}}; timerfd_settime(term->render.title.timer_fd, 0, &reset, NULL); - term->render.title.is_armed = false; 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) { @@ -650,6 +722,9 @@ initialize_render_workers(struct terminal *term) 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])); @@ -736,7 +811,8 @@ term_line_height_update(struct terminal *term) } static bool -term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4]) +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); @@ -751,9 +827,8 @@ term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4]) &term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); free_custom_glyphs( &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); - - const int old_cell_width = term->cell_width; - const int old_cell_height = term->cell_height; + free_custom_glyphs( + &term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT); const struct config *conf = term->conf; @@ -779,33 +854,26 @@ term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4]) 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); - if (term->cell_width < old_cell_width || - term->cell_height < old_cell_height) - { - /* - * The cell size has decreased. - * - * This means sixels, which we cannot resize, no longer fit - * into their "allocated" grid space. - * - * To be able to fit them, we would have to change the grid - * content. Inserting empty lines _might_ seem acceptable, but - * we'd also need to insert empty columns, which would break - * existing layout completely. - * - * So we delete them. - */ - sixel_destroy_all(term); - } else if (term->cell_width != old_cell_width || - term->cell_height != old_cell_height) - { - sixel_cell_size_changed(term); - } + sixel_cell_size_changed(term); - /* Use force, since cell-width/height may have changed */ - render_resize_force(term, term->width / term->scale, term->height / term->scale); + /* 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; } @@ -820,41 +888,51 @@ get_font_dpi(const struct terminal *term) * Conceptually, we use the physical monitor specs to calculate * the DPI, and we ignore the output's scaling factor. * - * However, to deal with 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. + * 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 fractional scaling factors we'll get a DPI *larger* than - * the physical DPI, that ends up being right when later + * 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). */ - /* Use highest DPI from outputs we're mapped on */ - double dpi = 0.0; - xassert(term->window != NULL); - tll_foreach(term->window->on_outputs, it) { - if (it->item->dpi > dpi) - dpi = it->item->dpi; - } + const struct wl_window *win = term->window; + const struct monitor *mon = NULL; - /* If we're not mapped, use DPI from first monitor. Hopefully this is where we'll get mapped later... */ - if (dpi == 0.) { - tll_foreach(term->wl->monitors, it) { - dpi = it->item.dpi; - break; + 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); } - if (dpi == 0) { - /* No monitors? */ - dpi = 96.; - } + const float monitor_dpi = mon != NULL + ? term_fractional_scaling(term) + ? mon->dpi.physical + : mon->dpi.scaled + : 96.; - return dpi; + return monitor_dpi > 0. ? monitor_dpi : 96.; } static enum fcft_subpixel @@ -873,7 +951,8 @@ get_font_subpixel(const struct terminal *term) * output or not. * * Thus, when determining which subpixel mode to use, we can't do - * much but select *an* output. So, we pick the first one. + * 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. @@ -883,7 +962,7 @@ get_font_subpixel(const struct terminal *term) */ if (tll_length(term->window->on_outputs) > 0) - wl_subpixel = tll_front(term->window->on_outputs)->subpixel; + 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 @@ -901,52 +980,16 @@ get_font_subpixel(const struct terminal *term) return FCFT_SUBPIXEL_DEFAULT; } -static bool -term_font_size_by_dpi(const struct terminal *term) -{ - switch (term->conf->dpi_aware) { - case DPI_AWARE_YES: return true; - case DPI_AWARE_NO: return false; - - case DPI_AWARE_AUTO: - /* - * Scale using DPI if all monitors have a scaling factor or 1. - * - * The idea is this: if a user, with multiple monitors, have - * enabled scaling on at least one monitor, then he/she has - * most likely done so to match the size of his/hers other - * monitors. - * - * I.e. if the user has one monitor with a scaling factor of - * one, and another with a scaling factor of two, he/she - * expects things to be twice as large on the second - * monitor. - * - * If we (foot) scale using DPI on the first monitor, and - * using the scaling factor on the second monitor, foot will - * *not* look twice as big on the second monitor. - */ - tll_foreach(term->wl->monitors, it) { - const struct monitor *mon = &it->item; - if (mon->scale > 1) - return false; - } - return true; - } - - BUG("unhandled DPI awareness value"); -} - int term_pt_or_px_as_pixels(const struct terminal *term, const struct pt_or_px *pt_or_px) { - double scale = !term->font_is_sized_by_dpi ? term->scale : 1.; - double dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + 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 - ? round(pt_or_px->pt * scale * dpi / 72) - : pt_or_px->px * scale; + ? (int)roundf(pt_or_px->pt * scale * dpi / 72) + : (int)roundf(pt_or_px->px * scale); } struct font_load_data { @@ -954,6 +997,7 @@ struct font_load_data { const char **names; const char *attrs; + const struct fcft_font_options *options; struct fcft_font **font; }; @@ -961,12 +1005,13 @@ static int font_loader_thread(void *_data) { struct font_load_data *data = _data; - *data->font = fcft_from_name(data->count, data->names, data->attrs); + *data->font = fcft_from_name2( + data->count, data->names, data->attrs, data->options); return *data->font != NULL; } static bool -reload_fonts(struct terminal *term) +reload_fonts(struct terminal *term, bool resize_grid) { const struct config *conf = term->conf; @@ -989,20 +1034,16 @@ reload_fonts(struct terminal *term) bool use_px_size = term->font_sizes[i][j].px_size > 0; char size[64]; - const int scale = term->font_is_sized_by_dpi ? 1 : term->scale; + const float scale = term->font_is_sized_by_dpi ? 1. : term->scale; if (use_px_size) snprintf(size, sizeof(size), ":pixelsize=%d", - term->font_sizes[i][j].px_size * scale); + (int)roundf(term->font_sizes[i][j].px_size * scale)); else snprintf(size, sizeof(size), ":size=%.2f", - term->font_sizes[i][j].pt_size * (double)scale); + term->font_sizes[i][j].pt_size * scale); - size_t len = strlen(font->pattern) + strlen(size) + 1; - names[i][j] = xmalloc(len); - - strcpy(names[i][j], font->pattern); - strcat(names[i][j], size); + names[i][j] = xstrjoin(font->pattern, size); } } @@ -1025,37 +1066,41 @@ reload_fonts(struct terminal *term) 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] = {NULL}; - int attr_len[4] = {-1, -1, -1, -1}; /* -1, so that +1 (below) results in 0 */ + 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" : ""), + }; - for (size_t i = 0; i < 2; i++) { - attr_len[0] = snprintf( - attrs[0], attr_len[0] + 1, "dpi=%.2f", - use_dpi ? term->font_dpi : 96); - attr_len[1] = snprintf( - attrs[1], attr_len[1] + 1, "dpi=%.2f:%s", - use_dpi ? term->font_dpi : 96, !custom_bold ? "weight=bold" : ""); - attr_len[2] = snprintf( - attrs[2], attr_len[2] + 1, "dpi=%.2f:%s", - use_dpi ? term->font_dpi : 96, !custom_italic ? "slant=italic" : ""); - attr_len[3] = snprintf( - attrs[3], attr_len[3] + 1, "dpi=%.2f:%s", - use_dpi ? term->font_dpi : 96, !custom_bold_italic ? "weight=bold:slant=italic" : ""); + struct fcft_font_options *options = fcft_font_options_create(); - if (i > 0) - continue; + 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); - for (size_t i = 0; i < 4; i++) - attrs[i] = xmalloc(attr_len[i] + 1); + 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], &fonts[0]}, - {count_bold, names_bold, attrs[1], &fonts[1]}, - {count_italic, names_italic, attrs[2], &fonts[2]}, - {count_bold_italic, names_bold_italic, attrs[3], &fonts[3]}, + {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}; @@ -1072,12 +1117,16 @@ reload_fonts(struct terminal *term) for (size_t i = 0; i < 4; i++) { if (tids[i] != 0) { int ret; - thrd_join(tids[i], &ret); - success = success && 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]); @@ -1093,7 +1142,7 @@ reload_fonts(struct terminal *term) } } - return success ? term_set_fonts(term, fonts) : success; + return success ? term_set_fonts(term, fonts, resize_grid) : success; } static bool @@ -1111,16 +1160,19 @@ load_fonts_from_conf(struct terminal *term) } } - return reload_fonts(term); + 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, int argc, char *const *argv, char *const *envp, + 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; @@ -1129,6 +1181,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, 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)) { @@ -1136,7 +1190,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, return NULL; } - if ((ptmx = posix_openpt(O_RDWR | O_NOCTTY)) < 0) { + 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; } @@ -1163,6 +1218,18 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, 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) { @@ -1170,8 +1237,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, 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 + /* 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); @@ -1193,16 +1260,32 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, !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, 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(), @@ -1213,7 +1296,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), }, .font_dpi = 0., - .font_subpixel = (conf->colors.alpha == 0xffff /* Can't do subpixel rendering on transparent background */ + .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, @@ -1221,32 +1305,36 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .reverse_wrap = true, .auto_margin = true, .window_title_stack = tll_init(), - .scale = 1, + .scale = 1., + .scale_before_unmap = -1, .flash = {.fd = flash_fd}, .blink = {.fd = -1}, .vt = { .state = 0, /* STATE_GROUND */ }, .colors = { - .fg = conf->colors.fg, - .bg = conf->colors.bg, - .alpha = conf->colors.alpha, - .selection_fg = conf->colors.selection_fg, - .selection_bg = conf->colors.selection_bg, - .use_custom_selection = conf->colors.use_custom.selection, + .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, + .deccsusr = conf->cursor.blink.enabled, .state = CURSOR_BLINK_ON, .fd = -1, }, - .cursor_color = { - .text = conf->cursor.color.text, - .cursor = conf->cursor.color.cursor, - }, .selection = { .coords = { .start = {-1, -1}, @@ -1275,20 +1363,26 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .wl = wayl, .render = { .chains = { - .grid = shm_chain_new(wayl->shm, true, 1 + conf->render_worker_count), - .search = shm_chain_new(wayl->shm, false, 1), - .scrollback_indicator = shm_chain_new(wayl->shm, false, 1), - .render_timer = shm_chain_new(wayl->shm, false, 1), - .url = shm_chain_new(wayl->shm, false, 1), - .csd = shm_chain_new(wayl->shm, false, 1), - .overlay = shm_chain_new(wayl->shm, false, 1), + .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), }, .scrollback_lines = conf->scrollback.lines, .app_sync_updates.timer_fd = app_sync_updates_fd, .title = { - .is_armed = false, .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(), @@ -1313,14 +1407,17 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, }, .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]; @@ -1331,34 +1428,36 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, } } + 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); - /* 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; - } + 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); + 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. Pick highest scale we find for now */ - tll_foreach(term->wl->monitors, it) { - if (it->item.scale > term->scale) - term->scale = it->item.scale; - } - - memcpy(term->colors.table, term->conf->colors.table, sizeof(term->colors.table)); + * 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)) + if (!term_font_dpi_changed(term, 0.)) goto err; term->font_subpixel = get_font_subpixel(term); @@ -1398,6 +1497,8 @@ close_fds: 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; @@ -1410,6 +1511,9 @@ term_window_configured(struct terminal *term) 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"); } } @@ -1418,11 +1522,11 @@ term_window_configured(struct terminal *term) * * A foot instance can be terminated in two ways: * - * - the client application terminates (user types ‘exit’, or pressed C-d in the + * - 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 + * 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. * @@ -1439,7 +1543,7 @@ term_window_configured(struct terminal *term) * - fdm_client_terminated(): reaper callback, called when the client * application has terminated. * - * + Kills the “terminate” timeout timer + * + 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- @@ -1447,18 +1551,18 @@ term_window_configured(struct terminal *term) * 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 + * 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 + * 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 + * - 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. * @@ -1469,7 +1573,7 @@ term_window_configured(struct terminal *term) * 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 + * 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(). * @@ -1565,10 +1669,36 @@ fdm_terminate_timeout(struct fdm *fdm, int fd, int events, void *data) struct terminal *term = data; xassert(!term->shutdown.client_has_terminated); - LOG_DBG("slave (PID=%u) has not terminated, sending SIGKILL (%d)", - term->slave, SIGKILL); + 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" + : "<unknown>", + 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; + } - kill(-term->slave, SIGKILL); return true; } @@ -1590,6 +1720,8 @@ term_shutdown(struct terminal *term) 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); @@ -1604,30 +1736,44 @@ term_shutdown(struct terminal *term) close(term->ptmx); if (!term->shutdown.client_has_terminated) { - LOG_DBG("initiating asynchronous terminate of slave; " - "sending SIGTERM to PID=%u", term->slave); + 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, SIGTERM); + kill(-term->slave, SIGHUP); - const struct itimerspec timeout = {.it_value = {.tv_sec = 60}}; + /* + * 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; + 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; } - - xassert(term->shutdown.terminate_timeout_fd < 0); - term->shutdown.terminate_timeout_fd = timeout_fd; } 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; @@ -1670,8 +1816,6 @@ term_destroy(struct terminal *term) if (term == NULL) return 0; - key_binding_unref(term->wl->key_binding_manager, term->conf); - tll_foreach(term->wl->terms, it) { if (it->item == term) { tll_remove(term->wl->terms, it); @@ -1683,6 +1827,8 @@ term_destroy(struct terminal *term) 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); @@ -1717,6 +1863,8 @@ term_destroy(struct terminal *term) } mtx_unlock(&term->render.workers.lock); + key_binding_unref(term->wl->key_binding_manager, term->conf); + urls_reset(term); free(term->vt.osc.data); @@ -1724,6 +1872,7 @@ term_destroy(struct terminal *term) composed_free(term->composed); + free(term->app_id); free(term->window_title); tll_free_and_free(term->window_title_stack, free); @@ -1739,6 +1888,8 @@ term_destroy(struct terminal *term) &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); @@ -1750,6 +1901,8 @@ term_destroy(struct terminal *term) } } 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); @@ -1777,6 +1930,15 @@ term_destroy(struct terminal *term) 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); @@ -1789,11 +1951,12 @@ term_destroy(struct terminal *term) 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 */ + /* We'll deal with this explicitly */ reaper_del(term->reaper, term->slave); int exit_status; @@ -1802,12 +1965,12 @@ term_destroy(struct terminal *term) exit_status = term->shutdown.exit_status; else { LOG_DBG("initiating blocking terminate of slave; " - "sending SIGTERM to PID=%u", term->slave); + "sending SIGHUP to PID=%u", term->slave); - kill(-term->slave, SIGTERM); + kill(-term->slave, SIGHUP); /* - * we’ve closed the ptxm, and sent SIGTERM to the client + * 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 @@ -1825,7 +1988,12 @@ term_destroy(struct terminal *term) struct sigaction action = {.sa_handler = &sig_alarm}; sigemptyset(&action.sa_mask); sigaction(SIGALRM, &action, NULL); - alarm(60); + + /* 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); @@ -1837,11 +2005,16 @@ term_destroy(struct terminal *term) xassert(errno == EINTR); if (alarm_raised) { - LOG_DBG( - "slave (PID=%u) has not terminate yet, " - "sending: SIGKILL (%d)", term->slave, SIGKILL); + LOG_DBG("slave (PID=%u) has not terminated yet, " + "sending: %s (%d)", term->slave, + next_signal == SIGTERM ? "SIGTERM" : "SIGKILL", + next_signal); - kill(-term->slave, SIGKILL); + kill(-term->slave, next_signal); + next_signal = SIGKILL; + + alarm_raised = 0; + alarm(30); } } } @@ -1894,21 +2067,40 @@ erase_cell_range(struct terminal *term, struct row *row, int start, int end) } else memset(&row->cells[start], 0, (end - start + 1) * sizeof(row->cells[0])); - if (unlikely(row->extra != NULL)) + 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 = false; - row->prompt_marker = false; + 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; @@ -1930,9 +2122,11 @@ term_reset(struct terminal *term, bool hard) 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; @@ -1964,26 +2158,45 @@ term_reset(struct terminal *term, bool hard) 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->colors.fg = term->conf->colors.fg; - term->colors.bg = term->conf->colors.bg; - term->colors.alpha = term->conf->colors.alpha; - term->colors.selection_fg = term->conf->colors.selection_fg; - term->colors.selection_bg = term->conf->colors.selection_bg; - term->colors.use_custom_selection = term->conf->colors.use_custom.selection; - memcpy(term->colors.table, term->conf->colors.table, - sizeof(term->colors.table)); + 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; @@ -1993,10 +2206,8 @@ term_reset(struct terminal *term, bool hard) 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; + term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; term_cursor_blink_update(term); - term->cursor_color.text = term->conf->cursor.color.text; - term->cursor_color.cursor = term->conf->cursor.color.cursor; selection_cancel(term); term->normal.offset = term->normal.view = 0; term->alt.offset = term->alt.view = 0; @@ -2055,7 +2266,7 @@ term_font_size_adjust_by_points(struct terminal *term, float amount) } } - return reload_fonts(term); + return reload_fonts(term, true); } static bool @@ -2078,7 +2289,7 @@ term_font_size_adjust_by_pixels(struct terminal *term, int amount) } } - return reload_fonts(term); + return reload_fonts(term, true); } static bool @@ -2102,7 +2313,7 @@ term_font_size_adjust_by_percent(struct terminal *term, bool increment, float pe } } - return reload_fonts(term); + return reload_fonts(term, true); } bool @@ -2140,13 +2351,68 @@ term_font_size_reset(struct terminal *term) } bool -term_font_dpi_changed(struct terminal *term, int old_scale) +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); + xassert(term->scale > 0.); bool was_scaled_using_dpi = term->font_is_sized_by_dpi; - bool will_scale_using_dpi = term_font_size_by_dpi(term); + bool will_scale_using_dpi = term->conf->dpi_aware; bool need_font_reload = was_scaled_using_dpi != will_scale_using_dpi || @@ -2155,22 +2421,22 @@ term_font_dpi_changed(struct terminal *term, int old_scale) : old_scale != term->scale); if (need_font_reload) { - LOG_DBG("DPI/scale change: DPI-awareness=%s, " - "DPI: %.2f -> %.2f, scale: %d -> %d, " + 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 == DPI_AWARE_AUTO ? "auto" : - term->conf->dpi_aware == DPI_AWARE_YES ? "yes" : "no", + 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 true; + return false; - return reload_fonts(term); + return reload_fonts(term, false); } void @@ -2199,6 +2465,25 @@ term_font_subpixel_changed(struct terminal *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) { @@ -2248,6 +2533,96 @@ 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) @@ -2260,7 +2635,7 @@ term_damage_scroll(struct terminal *term, enum damage_type damage_type, dmg->region.start == region.start && dmg->region.end == region.end)) { - /* Make sure we don’t overflow... */ + /* 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; @@ -2312,6 +2687,10 @@ term_erase_scrollback(struct terminal *term) 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; @@ -2324,14 +2703,14 @@ term_erase_scrollback(struct terminal *term) if (sel_end >= 0) { /* * Cancel selection if it touches any of the rows in the - * scrollback, since we can’t have the selection reference + * 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 + * 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. @@ -2378,6 +2757,13 @@ term_erase_scrollback(struct terminal *term) } 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); } @@ -2407,13 +2793,11 @@ UNITTEST }, .kind = SELECTION_NONE, .auto_scroll = { - .fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK), + .fd = -1, }, }, }; - xassert(term.selection.auto_scroll.fd >= 0); - #define populate_scrollback() do { \ for (int i = 0; i < scrollback_rows; i++) { \ if (term.normal.rows[i] == NULL) { \ @@ -2503,7 +2887,7 @@ UNITTEST /* Cleanup */ tll_free(term.normal.sixel_images); - close(term.selection.auto_scroll.fd); + 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); @@ -2536,6 +2920,8 @@ term_cursor_to(struct terminal *term, int row, int col) 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); } @@ -2545,6 +2931,16 @@ 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) { @@ -2552,6 +2948,7 @@ term_cursor_left(struct terminal *term, int 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 @@ -2561,6 +2958,7 @@ term_cursor_right(struct terminal *term, int 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 @@ -2601,9 +2999,13 @@ cursor_blink_rearm_timer(struct terminal *term) term->cursor_blink.fd = fd; } - static const struct itimerspec timer = { - .it_value = {.tv_sec = 0, .tv_nsec = 500000000}, - .it_interval = {.tv_sec = 0, .tv_nsec = 500000000}, + 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) { @@ -2672,7 +3074,7 @@ term_scroll_partial(struct terminal *term, struct scroll_region region, int rows /* * Selection is (partly) inside either the top or bottom * scrolling regions, or on (at least one) of the lines - * scrolled in (i.e. re-used lines). + * scrolled in (i.e. reused lines). */ if (selection_on_top_region(term, region) || selection_on_bottom_region(term, region)) @@ -2693,6 +3095,7 @@ term_scroll_partial(struct terminal *term, struct scroll_region region, int 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)) { @@ -2716,7 +3119,6 @@ term_scroll_partial(struct terminal *term, struct scroll_region region, int rows erase_line(term, row); } - term_damage_scroll(term, DAMAGE_SCROLL, region, rows); term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); #if defined(_DEBUG) @@ -2746,7 +3148,7 @@ term_scroll_reverse_partial(struct terminal *term, /* * Selection is (partly) inside either the top or bottom * scrolling regions, or on (at least one) of the lines - * scrolled in (i.e. re-used lines). + * scrolled in (i.e. reused lines). */ if (selection_on_top_region(term, region) || selection_on_bottom_region(term, region)) @@ -2770,17 +3172,29 @@ term_scroll_reverse_partial(struct terminal *term, sixel_scroll_down(term, rows); - bool view_follows = term->grid->view == term->grid->offset; + 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 */ @@ -2797,12 +3211,16 @@ term_scroll_reverse_partial(struct terminal *term, erase_line(term, row); } - term_damage_scroll(term, DAMAGE_SCROLL_REVERSE, region, rows); + 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 } @@ -2821,13 +3239,14 @@ term_carriage_return(struct terminal *term) void term_linefeed(struct terminal *term) { - term->grid->cur_row->linebreak = true; 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 @@ -2868,6 +3287,9 @@ term_restore_cursor(struct terminal *term, const struct cursor *cursor) 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); } @@ -2937,17 +3359,23 @@ term_kbd_focus_out(struct terminal *term) 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_BACK: return 4; - case BTN_FORWARD: return 5; - 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_TASK: return -1; /* TODO: ??? */ + 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); @@ -2999,10 +3427,13 @@ report_mouse_click(struct terminal *term, int encoded_button, int row, int col, encoded_button, col + 1, row + 1, release ? 'm' : 'M'); break; - case MOUSE_SGR_PIXELS: + 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, col_pixels + 1, row_pixels + 1, release ? 'm' : 'M'); + encoded_button, bounded_col + 1, bounded_row + 1, release ? 'm' : 'M'); break; + } case MOUSE_URXVT: snprintf(response, sizeof(response), "\033[%d;%d;%dM", @@ -3032,7 +3463,7 @@ term_mouse_grabbed(const struct terminal *term, const struct seat *seat) */ xkb_mod_mask_t mods; - get_current_modifiers(seat, &mods, NULL, 0); + 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); @@ -3176,44 +3607,51 @@ term_mouse_motion(struct terminal *term, int button, int row, int col, void term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) { - const char *xcursor = NULL; + enum cursor_shape shape = CURSOR_SHAPE_NONE; switch (term->active_surface) { - case TERM_SURF_GRID: { - bool have_custom_cursor = - render_xcursor_is_valid(seat, term->mouse_user_cursor); + case TERM_SURF_GRID: + if (seat->pointer.hidden) + shape = CURSOR_SHAPE_HIDDEN; - xcursor = seat->pointer.hidden ? XCURSOR_HIDDEN - : have_custom_cursor ? term->mouse_user_cursor - : term->is_searching ? XCURSOR_LEFT_PTR - : (seat->mouse.col >= 0 && - seat->mouse.row >= 0 && - term_mouse_grabbed(term, seat)) ? XCURSOR_TEXT - : XCURSOR_LEFT_PTR; + 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: - xcursor = XCURSOR_LEFT_PTR; + shape = CURSOR_SHAPE_LEFT_PTR; break; case TERM_SURF_BORDER_LEFT: case TERM_SURF_BORDER_RIGHT: case TERM_SURF_BORDER_TOP: case TERM_SURF_BORDER_BOTTOM: - xcursor = xcursor_for_csd_border(term, seat->mouse.x, seat->mouse.y); + shape = xcursor_for_csd_border(term, seat->mouse.x, seat->mouse.y); break; case TERM_SURF_NONE: return; } - if (xcursor == NULL) + if (shape == CURSOR_SHAPE_NONE) BUG("xcursor not set"); - render_xcursor_set(seat, term, xcursor); + render_xcursor_set(seat, term, shape); } void @@ -3229,15 +3667,79 @@ 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 && strcmp(term->window_title, title) == 0) + 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); 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) { @@ -3257,6 +3759,7 @@ term_flash(struct terminal *term, unsigned duration_ms) void term_bell(struct terminal *term) { + if (!term->bell_action_enabled) return; @@ -3264,7 +3767,7 @@ term_bell(struct terminal *term) if (!wayl_win_set_urgent(term->window)) { /* * Urgency (xdg-activation) is relatively new in - * Wayland. Fallback to our old, “faked”, urgency - + * Wayland. Fallback to our old, "faked", urgency - * rendering our window margins in red */ term->render.urgency = true; @@ -3272,15 +3775,27 @@ term_bell(struct terminal *term) } } - if (term->conf->bell.notify) - notify_notify(term, "Bell", "Bell in terminal"); + 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); + devnull, -1, -1, NULL, NULL, NULL); if (devnull >= 0) close(devnull); @@ -3290,9 +3805,19 @@ term_bell(struct terminal *term) 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, (char *const []){term->foot_exe, NULL}, - -1, -1, -1, NULL); + term->reaper, term->cwd, argv, + -1, -1, -1, NULL, NULL, NULL) >= 0; } void @@ -3399,11 +3924,77 @@ print_spacer(struct terminal *term, int col, int remaining) struct cell *cell = &row->cells[col]; cell->wc = CELL_SPACER + remaining; - cell->attrs = term->vt.attrs; + 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) +term_print(struct terminal *term, char32_t wc, int width, bool insert_mode_disable) { xassert(width > 0); @@ -3425,7 +4016,8 @@ term_print(struct terminal *term, char32_t wc, int width) } print_linewrap(term); - print_insert(term, width); + if (!insert_mode_disable) + print_insert(term, width); int col = grid->cursor.point.col; @@ -3454,9 +4046,11 @@ term_print(struct terminal *term, char32_t wc, int width) cell->wc = term->vt.last_printed = wc; cell->attrs = term->vt.attrs; - if (term->vt.osc8.uri != NULL) { - grid_row_uri_range_put( - row, col, term->vt.osc8.uri, term->vt.osc8.id); + 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: @@ -3469,12 +4063,21 @@ term_print(struct terminal *term, char32_t wc, int width) } 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 < term->cols - 1; i++) { + 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; @@ -3488,7 +4091,7 @@ term_print(struct terminal *term, char32_t wc, int width) static void ascii_printer_generic(struct terminal *term, char32_t wc) { - term_print(term, wc, 1); + term_print(term, wc, 1, false); } static void @@ -3516,6 +4119,7 @@ ascii_printer_fast(struct terminal *term, char32_t wc) /* Advance cursor */ if (unlikely(++col >= term->cols)) { + xassert(col == term->cols); grid->cursor.lcf = true; col--; } else @@ -3523,8 +4127,10 @@ ascii_printer_fast(struct terminal *term, char32_t wc) grid->cursor.point.col = col; - if (unlikely(row->extra != NULL)) + 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 @@ -3532,23 +4138,25 @@ 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(tll_length(term->grid->sixel_images) > 0 || - term->vt.osc8.uri != NULL || - term->charsets.set[term->charsets.selected] == CHARSET_GRAPHIC || - term->insert_mode) - ? &ascii_printer_generic - : &ascii_printer_fast; + 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", + LOG_DBG("switching ASCII printer %s -> %s", term->ascii_printer == &ascii_printer_fast ? "fast" : "generic", new_printer == &ascii_printer_fast ? "fast" : "generic"); } @@ -3565,26 +4173,280 @@ term_single_shift(struct terminal *term, enum charset_designator 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)) + if (likely(surface == term->window->surface.surf)) return TERM_SURF_GRID; - else if (surface == term->window->csd.surface[CSD_SURF_TITLE].surf) + 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].surf) + 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].surf) + 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].surf) + 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].surf) + 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].surf) + 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].surf) + 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].surf) + else if (surface == term->window->csd.surface[CSD_SURF_CLOSE].surface.surf) return TERM_SURF_BUTTON_CLOSE; else return TERM_SURF_NONE; @@ -3592,7 +4454,7 @@ term_surface_kind(const struct terminal *term, const struct wl_surface *surface) static bool rows_to_text(const struct terminal *term, int start, int end, - char **text, size_t *len) + int col_start, int col_end, char **text, size_t *len) { struct extraction_context *ctx = extract_begin(SELECTION_NONE, true); if (ctx == NULL) @@ -3605,15 +4467,20 @@ rows_to_text(const struct terminal *term, int start, int end, const struct row *row = term->grid->rows[r]; xassert(row != NULL); - for (int c = 0; c < term->cols; c++) + 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: @@ -3645,7 +4512,7 @@ term_scrollback_to_text(const struct terminal *term, char **text, size_t *len) end += term->grid->num_rows; } - return rows_to_text(term, start, end, text, len); + return rows_to_text(term, start, end, 0, term->cols, text, len); } bool @@ -3653,7 +4520,91 @@ 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, text, len); + 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 @@ -3748,6 +4699,8 @@ term_osc8_open(struct terminal *term, uint64_t id, const char *uri) term->vt.osc8.id = id; term->vt.osc8.uri = xstrdup(uri); + + term->bits_affecting_ascii_printer.osc8 = true; term_update_ascii_printer(term); } @@ -3757,6 +4710,7 @@ 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); } @@ -3764,6 +4718,115 @@ void term_set_user_mouse_cursor(struct terminal *term, const char *cursor) { free(term->mouse_user_cursor); - term->mouse_user_cursor = cursor != NULL ? xstrdup(cursor) : NULL; + 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 index d2762a5a..5a2a57aa 100644 --- a/terminal.h +++ b/terminal.h @@ -20,6 +20,7 @@ #include "fdm.h" #include "key-binding.h" #include "macros.h" +#include "notify.h" #include "reaper.h" #include "shm.h" #include "wayland.h" @@ -87,7 +88,7 @@ struct range { struct cursor { struct coord point; - bool lcf; + bool lcf; /* Last Column Flag; https://github.com/mattiase/wraptest#basic-vt-line-wrapping-rules */ }; enum damage_type {DAMAGE_SCROLL, DAMAGE_SCROLL_REVERSE, @@ -99,19 +100,58 @@ struct damage { uint16_t lines; }; -struct row_uri_range { - int start; - int end; +struct uri_range_data { uint64_t id; char *uri; }; +enum underline_style { + UNDERLINE_NONE, + UNDERLINE_SINGLE, /* Legacy underline */ + UNDERLINE_DOUBLE, + UNDERLINE_CURLY, + UNDERLINE_DOTTED, + UNDERLINE_DASHED, +}; + +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 { - struct row_uri_range *v; - uint32_t size; - uint32_t count; - } uri_ranges; + struct row_ranges uri_ranges; + struct row_ranges underline_ranges; }; struct row { @@ -121,19 +161,56 @@ struct row { bool dirty; bool linebreak; - /* Shell integration */ - bool prompt_marker; + struct { + bool prompt_marker; + int cmd_start; /* Column, -1 if unset */ + int cmd_end; /* Column, -1 if unset */ + } shell_integration; }; struct sixel { - void *data; + /* + * 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 { @@ -180,8 +257,10 @@ struct grid { }; struct vt_subparams { - unsigned value[16]; uint8_t idx; + unsigned *cur; + unsigned value[16]; + unsigned dummy; }; struct vt_param { @@ -197,8 +276,10 @@ struct vt { #endif char32_t utf8; struct { - struct vt_param v[16]; uint8_t idx; + struct vt_param *cur; + struct vt_param v[16]; + struct vt_param dummy; } params; uint32_t private; /* LSB=priv0, MSB=priv3 */ @@ -219,6 +300,8 @@ struct vt { char *uri; } osc8; + struct underline_range_data underline; + struct { uint8_t *data; size_t size; @@ -262,6 +345,7 @@ enum selection_kind { SELECTION_NONE, SELECTION_CHAR_WISE, SELECTION_WORD_WISE, + SELECTION_QUOTE_WISE, SELECTION_LINE_WISE, SELECTION_BLOCK }; @@ -304,18 +388,42 @@ struct 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 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; 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; @@ -340,7 +448,7 @@ struct terminal { bool bracketed_paste; bool focus_events; bool alt_scrolling; - bool modify_other_keys_2; /* True when modifyOtherKeys=2 (i.e. “CSI >4;2m”) */ + 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; @@ -364,14 +472,17 @@ struct terminal { 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 @@ -384,6 +495,11 @@ struct terminal { #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 \ @@ -401,6 +517,7 @@ struct terminal { bool num_lock_modifier; bool bell_action_enabled; + bool report_theme_changes; /* Saved DECSET modes - we save the SET state */ struct { @@ -430,6 +547,10 @@ struct terminal { 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; @@ -439,6 +560,9 @@ struct terminal { bool window_title_has_been_set; char *window_title; tll(char *) window_title_stack; + //char *window_icon; /* No escape sequence available to set the icon */ + //tll(char *)window_icon_stack; + char *app_id; struct { bool active; @@ -450,7 +574,8 @@ struct terminal { int fd; } blink; - int scale; + float scale; + float scale_before_unmap; /* Last scaling factor used */ int width; /* pixels */ int height; /* pixels */ int stashed_width; @@ -464,15 +589,13 @@ struct terminal { int cell_width; /* pixels per cell, x-wise */ int cell_height; /* pixels per cell, y-wise */ + struct colors colors; + struct { - uint32_t fg; - uint32_t bg; - uint32_t table[256]; - uint16_t alpha; - uint32_t selection_fg; - uint32_t selection_bg; - bool use_custom_selection; - } colors; + struct colors *stack; + size_t idx; + size_t size; + } color_stack; enum cursor_style cursor_style; struct { @@ -481,10 +604,6 @@ struct terminal { int fd; enum { CURSOR_BLINK_ON, CURSOR_BLINK_OFF } state; } cursor_blink; - struct { - uint32_t text; - uint32_t cursor; - } cursor_color; struct { enum selection_kind kind; @@ -558,10 +677,19 @@ struct terminal { struct { struct timespec last_update; - bool is_armed; 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 { @@ -578,6 +706,14 @@ struct terminal { 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 */ @@ -588,6 +724,8 @@ struct terminal { } 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; @@ -599,7 +737,7 @@ struct terminal { } render; struct { - struct grid *grid; /* Original ‘normal’ grid, before resize started */ + 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 */ @@ -616,8 +754,6 @@ struct terminal { } state; struct coord pos; /* Current sixel coordinate */ - int max_non_empty_row_no; - size_t row_byte_ofs; /* Byte position into image, for current row */ 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 */ @@ -626,10 +762,22 @@ struct terminal { 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 */ @@ -637,9 +785,13 @@ struct terminal { 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; - uint32_t default_bg; + + bool linear_blending; + bool use_10bit; + pixman_format_code_t pixman_fmt; /* Application configurable */ unsigned palette_size; /* Number of colors in palette */ @@ -652,44 +804,51 @@ struct terminal { 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; 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; -}; -extern const char *const XCURSOR_HIDDEN; -extern const char *const XCURSOR_LEFT_PTR; -extern const char *const XCURSOR_TEXT; -extern const char *const XCURSOR_TEXT_FALLBACK; -//extern const char *const XCURSOR_HAND2; -extern const char *const XCURSOR_TOP_LEFT_CORNER; -extern const char *const XCURSOR_TOP_RIGHT_CORNER; -extern const char *const XCURSOR_BOTTOM_LEFT_CORNER; -extern const char *const XCURSOR_BOTTOM_RIGHT_CORNER; -extern const char *const XCURSOR_LEFT_SIDE; -extern const char *const XCURSOR_RIGHT_SIDE; -extern const char *const XCURSOR_TOP_SIDE; -extern const char *const XCURSOR_BOTTOM_SIDE; + 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, int argc, char *const *argv, char *const *envp, + 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); bool term_shutdown(struct terminal *term); @@ -703,11 +862,15 @@ 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, int old_scale); +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); @@ -723,6 +886,7 @@ 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); @@ -739,13 +903,18 @@ 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_print(struct terminal *term, char32_t wc, int width); +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); @@ -786,6 +955,8 @@ 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); @@ -800,6 +971,8 @@ 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); @@ -817,6 +990,15 @@ 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) diff --git a/tests/meson.build b/tests/meson.build index 2b7768b0..9baa064c 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,8 +1,8 @@ config_test = executable( 'test-config', - 'test-config.c', '../tokenize.c', + 'test-config.c', wl_proto_headers, - link_with: [common], + 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 index 4736a46b..9774cba9 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -60,7 +60,7 @@ test_string(struct context *ctx, bool (*parse_fun)(struct context *ctx), BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } - if (strcmp(*ptr, input[i].value) != 0) { + 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); @@ -221,7 +221,7 @@ test_uint32(struct context *ctx, bool (*parse_fun)(struct context *ctx), } static void -test_double(struct context *ctx, bool (*parse_fun)(struct context *ctx), +test_float(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, const float *ptr) { ctx->key = key; @@ -313,9 +313,7 @@ test_spawn_template(struct context *ctx, bool (*parse_fun)(struct context *ctx), 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 || - strcmp(ptr->argv.args[i], args[i]) != 0) - { + 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, @@ -382,6 +380,10 @@ test_color(struct context *ctx, bool (*parse_fun)(struct context *ctx), {"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}, }; @@ -397,6 +399,16 @@ test_color(struct context *ctx, bool (*parse_fun)(struct context *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); + } } } } @@ -443,6 +455,18 @@ test_two_colors(struct context *ctx, bool (*parse_fun)(struct context *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); + } } } } @@ -458,14 +482,17 @@ test_section_main(void) 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, "utempter", &conf.utempter_path); + 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, "notify-focus-inhibit", &conf.notify_focus_inhibit); + 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); @@ -473,23 +500,11 @@ test_section_main(void) 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_spawn_template(&ctx, &parse_section_main, "notify", &conf.notify); - - test_enum( - &ctx, &parse_section_main, "dpi-aware", - 9, - (const char *[]){"on", "true", "yes", "1", - "off", "false", "no", "0", - "auto"}, - (int []){DPI_AWARE_YES, DPI_AWARE_YES, DPI_AWARE_YES, DPI_AWARE_YES, - DPI_AWARE_NO, DPI_AWARE_NO, DPI_AWARE_NO, DPI_AWARE_NO, - DPI_AWARE_AUTO}, - (int *)&conf.dpi_aware); - test_enum(&ctx, &parse_section_main, "selection-target", 4, (const char *[]){"none", "primary", "clipboard", "both"}, @@ -506,6 +521,14 @@ test_section_main(void) (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) */ @@ -516,6 +539,22 @@ test_section_main(void) 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) { @@ -526,6 +565,7 @@ test_section_bell(void) 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", @@ -534,6 +574,22 @@ test_section_bell(void) 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) { @@ -545,7 +601,7 @@ test_section_scrollback(void) test_uint32(&ctx, &parse_section_scrollback, "lines", &conf.scrollback.lines); - test_double(&ctx, parse_section_scrollback, "multiplier", &conf.scrollback.multiplier); + test_float(&ctx, parse_section_scrollback, "multiplier", &conf.scrollback.multiplier); test_enum( &ctx, &parse_section_scrollback, "indicator-position", @@ -578,9 +634,6 @@ test_section_url(void) (int *)&conf.url.osc8_underline); test_c32string(&ctx, &parse_section_url, "label-letters", &conf.url.label_letters); - /* TODO: protocols (list of wchars) */ - /* TODO: uri-characters (wchar string, but sorted) */ - config_free(&conf); } @@ -599,7 +652,14 @@ test_section_cursor(void) (const char *[]){"block", "beam", "underline"}, (int []){CURSOR_BLOCK, CURSOR_BEAM, CURSOR_UNDERLINE}, (int *)&conf.cursor.style); - test_boolean(&ctx, &parse_section_cursor, "blink", &conf.cursor.blink); + 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", @@ -628,64 +688,176 @@ test_section_mouse(void) } static void -test_section_colors(void) +test_section_touch(void) { struct config conf = {0}; struct context ctx = { - .conf = &conf, .section = "colors", .path = "unittest"}; + .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, "foreground", false, &conf.colors.fg); - test_color(&ctx, &parse_section_colors, "background", false, &conf.colors.bg); - test_color(&ctx, &parse_section_colors, "regular0", false, &conf.colors.table[0]); - test_color(&ctx, &parse_section_colors, "regular1", false, &conf.colors.table[1]); - test_color(&ctx, &parse_section_colors, "regular2", false, &conf.colors.table[2]); - test_color(&ctx, &parse_section_colors, "regular3", false, &conf.colors.table[3]); - test_color(&ctx, &parse_section_colors, "regular4", false, &conf.colors.table[4]); - test_color(&ctx, &parse_section_colors, "regular5", false, &conf.colors.table[5]); - test_color(&ctx, &parse_section_colors, "regular6", false, &conf.colors.table[6]); - test_color(&ctx, &parse_section_colors, "regular7", false, &conf.colors.table[7]); - test_color(&ctx, &parse_section_colors, "bright0", false, &conf.colors.table[8]); - test_color(&ctx, &parse_section_colors, "bright1", false, &conf.colors.table[9]); - test_color(&ctx, &parse_section_colors, "bright2", false, &conf.colors.table[10]); - test_color(&ctx, &parse_section_colors, "bright3", false, &conf.colors.table[11]); - test_color(&ctx, &parse_section_colors, "bright4", false, &conf.colors.table[12]); - test_color(&ctx, &parse_section_colors, "bright5", false, &conf.colors.table[13]); - test_color(&ctx, &parse_section_colors, "bright6", false, &conf.colors.table[14]); - test_color(&ctx, &parse_section_colors, "bright7", false, &conf.colors.table[15]); - test_color(&ctx, &parse_section_colors, "dim0", false, &conf.colors.dim[0]); - test_color(&ctx, &parse_section_colors, "dim1", false, &conf.colors.dim[1]); - test_color(&ctx, &parse_section_colors, "dim2", false, &conf.colors.dim[2]); - test_color(&ctx, &parse_section_colors, "dim3", false, &conf.colors.dim[3]); - test_color(&ctx, &parse_section_colors, "dim4", false, &conf.colors.dim[4]); - test_color(&ctx, &parse_section_colors, "dim5", false, &conf.colors.dim[5]); - test_color(&ctx, &parse_section_colors, "dim6", false, &conf.colors.dim[6]); - test_color(&ctx, &parse_section_colors, "dim7", false, &conf.colors.dim[7]); - test_color(&ctx, &parse_section_colors, "selection-foreground", false, &conf.colors.selection_fg); - test_color(&ctx, &parse_section_colors, "selection-background", false, &conf.colors.selection_bg); - test_color(&ctx, &parse_section_colors, "urls", false, &conf.colors.url); - test_two_colors(&ctx, &parse_section_colors, "jump-labels", false, - &conf.colors.jump_label.fg, - &conf.colors.jump_label.bg); - test_two_colors(&ctx, &parse_section_colors, "scrollback-indicator", false, - &conf.colors.scrollback_indicator.fg, - &conf.colors.scrollback_indicator.bg); - test_two_colors(&ctx, &parse_section_colors, "search-box-no-match", false, - &conf.colors.search_box.no_match.fg, - &conf.colors.search_box.no_match.bg); - test_two_colors(&ctx, &parse_section_colors, "search-box-match", false, - &conf.colors.search_box.match.fg, - &conf.colors.search_box.match.bg); + 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, key_name, false, - &conf.colors.table[i]); + test_color(&ctx, &parse_section_colors_dark, key_name, false, + &conf.colors_dark.table[i]); } - test_invalid_key(&ctx, &parse_section_colors, "256"); + 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) */ @@ -727,6 +899,8 @@ test_section_csd(void) &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 */ @@ -734,11 +908,22 @@ test_section_csd(void) 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) + enum key_binding_type type, bool need_argv, bool need_section_id) { xassert(map[action] != NULL); xassert(bindings->count == 0); @@ -750,7 +935,10 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), const bool alt = action % 3; const bool shift = action % 4; const bool super = action % 5; - const bool argv = action % 6; + 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"}; @@ -789,7 +977,7 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); snprintf(value, sizeof(value), "%s%s%s", - argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : "", + argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : section_id ? "[foobar]" : "", modifier_string, sym_name); break; } @@ -798,7 +986,7 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), 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\"] " : "", + argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : section_id ? "[foobar]" : "", modifier_string, button_name); xassert(click_count > 0); @@ -824,7 +1012,7 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), for (size_t i = 0; i < ALEN(args); i++) { if (binding->aux.pipe.args[i] == NULL || - strcmp(binding->aux.pipe.args[i], args[i]) != 0) + !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\"", @@ -839,6 +1027,18 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), 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", @@ -851,17 +1051,19 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), ctx->section, ctx->key, ctx->value, binding->action, action); } - if (binding->modifiers.ctrl != ctrl || - binding->modifiers.alt != alt || - binding->modifiers.shift != shift || - binding->modifiers.super != super) + 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, - binding->modifiers.ctrl, binding->modifiers.alt, - binding->modifiers.shift, binding->modifiers.super, + have_ctrl, have_alt, have_shift, have_super, ctrl, alt, shift, super); } @@ -917,14 +1119,17 @@ _test_binding_collisions(struct context *ctx, bindings.arr[0] = (struct config_key_binding){ .action = (test_mode == FAIL_DIFFERENT_ACTION ? max_action - 1 : max_action), - .modifiers = {.ctrl = true}, + .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 = {.ctrl = true}, + .modifiers = tll_init(), .path = "unittest", }; + tll_push_back(bindings.arr[1].modifiers, xstrdup(XKB_MOD_NAME_CTRL)); switch (type) { case KEY_BINDING: @@ -945,7 +1150,8 @@ _test_binding_collisions(struct context *ctx, break; case FAIL_MOUSE_OVERRIDE: - ctx->conf->mouse.selection_override_modifiers.ctrl = true; + 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: @@ -1028,7 +1234,9 @@ test_section_key_bindings(void) test_key_binding( &ctx, &parse_section_key_bindings, action, BIND_ACTION_KEY_COUNT - 1, - binding_action_map, &conf.bindings.key, KEY_BINDING); + 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); @@ -1063,7 +1271,8 @@ test_section_search_bindings(void) test_key_binding( &ctx, &parse_section_search_bindings, action, BIND_ACTION_SEARCH_COUNT - 1, - search_binding_action_map, &conf.bindings.search, KEY_BINDING); + search_binding_action_map, &conf.bindings.search, KEY_BINDING, + false, false); } config_free(&conf); @@ -1099,7 +1308,8 @@ test_section_url_bindings(void) test_key_binding( &ctx, &parse_section_url_bindings, action, BIND_ACTION_URL_COUNT - 1, - url_binding_action_map, &conf.bindings.url, KEY_BINDING); + url_binding_action_map, &conf.bindings.url, KEY_BINDING, + false, false); } config_free(&conf); @@ -1135,7 +1345,8 @@ test_section_mouse_bindings(void) test_key_binding( &ctx, &parse_section_mouse_bindings, action, BIND_ACTION_COUNT - 1, - binding_action_map, &conf.bindings.mouse, MOUSE_BINDING); + binding_action_map, &conf.bindings.mouse, MOUSE_BINDING, + false, false); } config_free(&conf); @@ -1184,10 +1395,13 @@ test_section_text_bindings(void) 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); } @@ -1203,26 +1417,26 @@ test_section_environment(void) ctx.value = "bar"; xassert(parse_section_environment(&ctx)); xassert(tll_length(conf.env_vars) == 1); - xassert(strcmp(tll_front(conf.env_vars).name, "FOO") == 0); - xassert(strcmp(tll_front(conf.env_vars).value, "bar") == 0); + 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(strcmp(tll_back(conf.env_vars).name, "BAR") == 0); - xassert(strcmp(tll_back(conf.env_vars).value, "123") == 0); + 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(strcmp(tll_front(conf.env_vars).name, "FOO") == 0); - xassert(strcmp(tll_front(conf.env_vars).value, "456") == 0); - xassert(strcmp(tll_back(conf.env_vars).name, "BAR") == 0); - xassert(strcmp(tll_back(conf.env_vars).value, "123") == 0); + 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); } @@ -1260,7 +1474,7 @@ test_section_tweak(void) RENDER_TIMER_BOTH}, (int *)&conf.tweak.render_timer); - test_double(&ctx, &parse_section_tweak, "box-drawing-base-thickness", + 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); @@ -1293,6 +1507,12 @@ test_section_tweak(void) 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); @@ -1307,12 +1527,16 @@ 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_colors(); + test_section_touch(); + test_section_colors_dark(); + test_section_colors_light(); test_section_csd(); test_section_key_bindings(); test_section_key_bindings_collisions(); diff --git a/themes/aeroroot b/themes/aeroroot new file mode 100644 index 00000000..dbeb2e81 --- /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 00000000..f05683ba --- /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 index 941a27b4..291ab8db 100644 --- a/themes/apprentice +++ b/themes/apprentice @@ -1,10 +1,8 @@ # -*- conf -*- # https://github.com/romainl/Apprentice -[cursor] -color=262626 6c6c6c - -[colors] +[colors-dark] +cursor=262626 6c6c6c foreground=bcbcbc background=262626 regular0=1c1c1c diff --git a/themes/ayu-mirage b/themes/ayu-mirage new file mode 100644 index 00000000..2d9b6b54 --- /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 b/themes/catppuccin deleted file mode 100644 index 4ccfabec..00000000 --- a/themes/catppuccin +++ /dev/null @@ -1,25 +0,0 @@ -# -*- conf -*- -# Catppuccin - -[cursor] -color=1A1826 D9E0EE - -[colors] -foreground=D9E0EE -background=1E1D2F -regular0=6E6C7E # black -regular1=F28FAD # red -regular2=ABE9B3 # green -regular3=FAE3B0 # yellow -regular4=96CDFB # blue -regular5=F5C2E7 # magenta -regular6=89DCEB # cyan -regular7=D9E0EE # white -bright0=988BA2 # bright black -bright1=F28FAD # bright red -bright2=ABE9B3 # bright green -bright3=FAE3B0 # bright yellow -bright4=96CDFB # bright blue -bright5=F5C2E7 # bright magenta -bright6=89DCEB # bright cyan -bright7=D9E0EE # bright white diff --git a/themes/catppuccin-frappe b/themes/catppuccin-frappe new file mode 100644 index 00000000..3acae600 --- /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 00000000..ca7a7aae --- /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 00000000..8f5ea36e --- /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 00000000..7d98dc0f --- /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 00000000..ffaf6cb2 --- /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 index 0925d2c2..42af3377 100644 --- a/themes/derp +++ b/themes/derp @@ -1,10 +1,8 @@ # -*- conf -*- # Derp -[cursor] -color=000000 ffffff - -[colors] +[colors-dark] +cursor=000000 ffffff foreground=ffffff background=000000 regular0=111111 diff --git a/themes/deus b/themes/deus index 8fb37f75..69c44944 100644 --- a/themes/deus +++ b/themes/deus @@ -2,10 +2,8 @@ # Deus # Color palette based on: https://github.com/ajmwagar/vim-deus -[cursor] -color=2c323b eaeaea - -[colors] +[colors-dark] +cursor=2c323b eaeaea background=2c323b foreground=eaeaea regular0=242a32 diff --git a/themes/dracula b/themes/dracula index 8b6ab542..82994203 100644 --- a/themes/dracula +++ b/themes/dracula @@ -1,10 +1,8 @@ # -*- conf -*- # Dracula -[cursor] -color=282a36 f8f8f2 - -[colors] +[colors-dark] +cursor=282a36 f8f8f2 foreground=f8f8f2 background=282a36 regular0=000000 # black diff --git a/themes/dracula-iterm b/themes/dracula-iterm new file mode 100644 index 00000000..b75ddd9c --- /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 00000000..8bc022ea --- /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 <eugen@rah.ro> + +[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 00000000..e44f3ea9 --- /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 index 73207199..c5dadcc5 100644 --- a/themes/gruvbox-dark +++ b/themes/gruvbox-dark @@ -1,7 +1,7 @@ # -*- conf -*- # Gruvbox -[colors] +[colors-dark] background=282828 foreground=ebdbb2 regular0=282828 diff --git a/themes/gruvbox-light b/themes/gruvbox-light index 6a7a2416..6b616612 100644 --- a/themes/gruvbox-light +++ b/themes/gruvbox-light @@ -1,7 +1,10 @@ # -*- conf -*- # Gruvbox - Light -[colors] +[main] +initial-color-theme=light + +[colors-light] background=fbf1c7 foreground=3c3836 regular0=fbf1c7 diff --git a/themes/hacktober b/themes/hacktober index acb6c0b1..ecdb18fb 100644 --- a/themes/hacktober +++ b/themes/hacktober @@ -1,8 +1,7 @@ # -*- conf -*- -[cursor] -color=141414 c9c9c9 -[colors] +[colors-dark] +cursor=141414 c9c9c9 foreground=c9c9c9 background=141414 regular0=191918 # black diff --git a/themes/iterm b/themes/iterm new file mode 100644 index 00000000..c5ffc190 --- /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 index 82528498..0092b795 100644 --- a/themes/jetbrains-darcula +++ b/themes/jetbrains-darcula @@ -2,10 +2,8 @@ # JetBrains Darcula # Palette based on the same theme from https://github.com/dexpota/kitty-themes -[cursor] -color=202020 ffffff - -[colors] +[colors-dark] +cursor=202020 ffffff background=202020 foreground=adadad regular0=000000 # black diff --git a/themes/kitty b/themes/kitty index b5b813cc..81fd003e 100644 --- a/themes/kitty +++ b/themes/kitty @@ -1,9 +1,7 @@ # -*- conf -*- -[cursor] -color=111111 cccccc - -[colors] +[colors-dark] +cursor=111111 cccccc foreground=dddddd background=000000 regular0=000000 # black diff --git a/themes/material-amber b/themes/material-amber index ee2c21b5..69126aa0 100644 --- a/themes/material-amber +++ b/themes/material-amber @@ -2,10 +2,11 @@ # Material Amber # Based on material.io guidelines with Amber 50 background -# [cursor] -# color=fff8e1 21201d +[main] +initial-color-theme=light -[colors] +[colors-light] +cursor=fff8e1 21201d foreground = 21201d background = fff8e1 diff --git a/themes/material-design b/themes/material-design index 4a9e008a..bf1d0a6b 100644 --- a/themes/material-design +++ b/themes/material-design @@ -2,7 +2,7 @@ # Material # From https://github.com/MartinSeeler/iterm2-material-design -[colors] +[colors-dark] foreground=ECEFF1 background=263238 regular0=546E7A # black diff --git a/themes/modus-operandi b/themes/modus-operandi index 5e3a9fd6..6baca2f7 100644 --- a/themes/modus-operandi +++ b/themes/modus-operandi @@ -3,7 +3,11 @@ # modus-operandi # See: https://protesilaos.com/emacs/modus-themes # -[colors] + +[main] +initial-color-theme=light + +[colors-light] background=ffffff foreground=000000 regular0=000000 @@ -22,3 +26,5 @@ bright4=2544bb bright5=5317ac bright6=005a5f bright7=ffffff + +jump-labels=dce0e8 0000ff diff --git a/themes/modus-vivendi b/themes/modus-vivendi index 82b1075d..9ee670ec 100644 --- a/themes/modus-vivendi +++ b/themes/modus-vivendi @@ -4,7 +4,7 @@ # See: https://protesilaos.com/emacs/modus-themes # -[colors] +[colors-dark] background=000000 foreground=ffffff regular0=000000 diff --git a/themes/modus-vivendi-tinted b/themes/modus-vivendi-tinted new file mode 100644 index 00000000..6a61fc79 --- /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 00000000..19e1b6fa --- /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 index 5d9f31a9..3044da91 100644 --- a/themes/monokai-pro +++ b/themes/monokai-pro @@ -1,7 +1,7 @@ # -*- conf -*- # Monokai Pro -[colors] +[colors-dark] background=2D2A2E foreground=FCFCFA regular0=403E41 diff --git a/themes/moonfly b/themes/moonfly index 870de9d0..b30e3156 100644 --- a/themes/moonfly +++ b/themes/moonfly @@ -2,10 +2,8 @@ # moonfly # Based on https://github.com/bluz71/vim-moonfly-colors -[cursor] -color = 080808 9e9e9e - -[colors] +[colors-dark] +cursor = 080808 9e9e9e foreground = b2b2b2 background = 080808 diff --git a/themes/neon b/themes/neon new file mode 100644 index 00000000..74884e03 --- /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 00000000..e9e40404 --- /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 index 2a27fb2d..ccdd183a 100644 --- a/themes/nightfly +++ b/themes/nightfly @@ -2,10 +2,8 @@ # nightfly # Based on https://github.com/bluz71/vim-nightfly-guicolors -[cursor] -color = 080808 9ca1aa - -[colors] +[colors-dark] +cursor = 080808 9ca1aa foreground = acb4c2 background = 011627 diff --git a/themes/noirblaze b/themes/noirblaze new file mode 100644 index 00000000..b21055a4 --- /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 index 4ce3a53e..eb2fdf0f 100644 --- a/themes/nord +++ b/themes/nord @@ -6,10 +6,8 @@ # this specific foot theme is based on nord-alacritty: # https://github.com/arcticicestudio/nord-alacritty/blob/develop/src/nord.yml -[cursor] -color = 2e3440 d8dee9 - -[colors] +[colors-dark] +cursor = 2e3440 d8dee9 foreground = d8dee9 background = 2e3440 diff --git a/themes/nordiq b/themes/nordiq index f309de23..1efccba6 100644 --- a/themes/nordiq +++ b/themes/nordiq @@ -1,10 +1,8 @@ # -*- conf -*- # Nordiq -[cursor] -color=eeeeee 9f515a - -[colors] +[colors-dark] +cursor=eeeeee 9f515a foreground=dbdee9 background=0e1420 regular0=5b6272 diff --git a/themes/nvim b/themes/nvim new file mode 100644 index 00000000..74dd1ac6 --- /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 00000000..fe3afb74 --- /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 00000000..fd8943b1 --- /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 index ac5cc834..6d66e87e 100644 --- a/themes/onedark +++ b/themes/onedark @@ -1,10 +1,8 @@ # OneDark # Palette based on the same theme from https://github.com/dexpota/kitty-themes -[cursor] -color=111111 cccccc - -[colors] +[colors-dark] +cursor=111111 cccccc foreground=979eab background=282c34 regular0=282c34 # black diff --git a/themes/onehalf-dark b/themes/onehalf-dark new file mode 100644 index 00000000..1faca455 --- /dev/null +++ b/themes/onehalf-dark @@ -0,0 +1,37 @@ +# theme: One Half - dark version +# author: Son A. Pham <sp@sonpham.me> +# 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 00000000..2c1dc7c5 --- /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 00000000..09934925 --- /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 index 18cd7f17..26260c6f 100644 --- a/themes/paper-color-dark +++ b/themes/paper-color-dark @@ -2,10 +2,8 @@ # PaperColorDark # Palette based on https://github.com/NLKNguyen/papercolor-theme -[cursor] -color=1c1c1c eeeeee - -[colors] +[colors-dark] +cursor=1c1c1c eeeeee background=1c1c1c foreground=eeeeee regular0=1c1c1c # black diff --git a/themes/paper-color-light b/themes/paper-color-light index b08ea707..554aabc0 100644 --- a/themes/paper-color-light +++ b/themes/paper-color-light @@ -2,10 +2,11 @@ # PaperColor Light # Palette based on https://github.com/NLKNguyen/papercolor-theme -[cursor] -color=eeeeee 444444 +[main] +initial-color-theme=light -[colors] +[colors-light] +cursor=eeeeee 444444 background=eeeeee foreground=444444 regular0=eeeeee # black diff --git a/themes/poimandres b/themes/poimandres new file mode 100644 index 00000000..a2123ac5 --- /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 index 56814a77..62a08cc2 100644 --- a/themes/rezza +++ b/themes/rezza @@ -13,7 +13,7 @@ # and also posted here: # https://forums.debian.net/viewtopic.php?t=29981 -[colors] +[colors-dark] foreground = cccccc background = 191911 diff --git a/themes/rose-pine b/themes/rose-pine index 6b58a66c..b9aa7e2a 100644 --- a/themes/rose-pine +++ b/themes/rose-pine @@ -1,26 +1,28 @@ # -*- conf -*- -# Rose-Piné +# Rosé Pine -[cursor] -color=191724 e0def4 - -[colors] +[colors-dark] +cursor=191724 e0def4 background=191724 foreground=e0def4 -regular0=26233a # black -regular1=eb6f92 # red -regular2=31748f # green -regular3=f6c177 # yellow -regular4=9ccfd8 # blue -regular5=c4a7e7 # magenta -regular6=ebbcba # cyan -regular7=e0def4 # white -bright0=6e6a86 # bright black -bright1=eb6f92 # bright red -bright2=31748f # bright green -bright3=f6c177 # bright yellow -bright4=9ccfd8 # bright blue -bright5=c4a7e7 # bright magenta -bright6=ebbcba # bright cyan -bright7=e0def4 # bright white \ No newline at end of file +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 00000000..d2742c72 --- /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 00000000..51b9a33a --- /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 00000000..83fea617 --- /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 index 28392add..8a93187e 100644 --- a/themes/selenized-black +++ b/themes/selenized-black @@ -1,10 +1,8 @@ # -*- conf -*- # Selenized black -[cursor] -color = 181818 56d8c9 - -[colors] +[colors-dark] +cursor = 181818 56d8c9 background= 181818 foreground= b9b9b9 diff --git a/themes/selenized-dark b/themes/selenized-dark index ed74cdfc..8ace1c05 100644 --- a/themes/selenized-dark +++ b/themes/selenized-dark @@ -1,10 +1,8 @@ # -*- conf -*- # Selenized dark -[cursor] -color = 103c48 53d6c7 - -[colors] +[colors-dark] +cursor = 103c48 53d6c7 background= 103c48 foreground= adbcbc diff --git a/themes/selenized-light b/themes/selenized-light index 7e599d8e..c842fc3c 100644 --- a/themes/selenized-light +++ b/themes/selenized-light @@ -1,10 +1,11 @@ # -*- conf -*- # Selenized light -[cursor] -color=fbf3db 00978a +[main] +initial-color-theme=light -[colors] +[colors-light] +cursor=fbf3db 00978a background= fbf3db foreground= 53676d diff --git a/themes/selenized-white b/themes/selenized-white index b4d25315..659bf814 100644 --- a/themes/selenized-white +++ b/themes/selenized-white @@ -1,10 +1,11 @@ # -*- conf -*- # Selenized white -[cursor] -color=ffffff 009a8a +[main] +initial-color-theme=light -[colors] +[colors-light] +cursor=ffffff 009a8a background= ffffff foreground= 474747 diff --git a/themes/solarized b/themes/solarized new file mode 100644 index 00000000..f1844b3c --- /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 index cad2945e..6335fa0f 100644 --- a/themes/solarized-dark +++ b/themes/solarized-dark @@ -1,10 +1,8 @@ # -*- conf -*- # Solarized dark -[cursor] -color= 002b36 93a1a1 - -[colors] +[colors-dark] +cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 regular0= 073642 diff --git a/themes/solarized-dark-normal-brights b/themes/solarized-dark-normal-brights index 1ab7d375..7b608110 100644 --- a/themes/solarized-dark-normal-brights +++ b/themes/solarized-dark-normal-brights @@ -1,10 +1,8 @@ # -*- conf -*- # Solarized dark -[cursor] -color= 002b36 93a1a1 - -[colors] +[colors-dark] +cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 regular0= 073642 diff --git a/themes/solarized-light b/themes/solarized-light index 74474573..db27be43 100644 --- a/themes/solarized-light +++ b/themes/solarized-light @@ -1,10 +1,11 @@ # -*- conf -*- # Solarized light -[cursor] -color=fdf6e3 586e75 +[main] +initial-color-theme=light -[colors] +[colors-light] +cursor= fdf6e3 586e75 background= fdf6e3 foreground= 657b83 regular0= eee8d5 diff --git a/themes/solarized-normal-brights b/themes/solarized-normal-brights new file mode 100644 index 00000000..3bd3c189 --- /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 00000000..612c82cc --- /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 00000000..81ce1a5f --- /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 index a326f8ad..5ea43f63 100644 --- a/themes/tango +++ b/themes/tango @@ -1,10 +1,8 @@ # -*- conf -*- # Tango -[cursor] -color=000000 babdb6 - -[colors] +[colors-dark] +cursor=000000 babdb6 foreground=babdb6 background=000000 regular0=2e3436 diff --git a/themes/tempus-autumn b/themes/tempus-autumn index 9c1f8797..214478bb 100644 --- a/themes/tempus-autumn +++ b/themes/tempus-autumn @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by earthly colours (WCAG AA compliant) -#[cursor] -#color = 302420 a9a2a6 - -[colors] +[colors-dark] +#cursor = 302420 a9a2a6 foreground = a9a2a6 background = 302420 regular0 = 302420 diff --git a/themes/tempus-classic b/themes/tempus-classic index 0164605b..95b37b76 100644 --- a/themes/tempus-classic +++ b/themes/tempus-classic @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with warm hues (WCAG AA compliant) -#[cursor] -#color = 232323 aeadaf - -[colors] +[colors-dark] +#cursor = 232323 aeadaf foreground = aeadaf background = 232323 regular0 = 232323 diff --git a/themes/tempus-dawn b/themes/tempus-dawn index cf143fba..c288544e 100644 --- a/themes/tempus-dawn +++ b/themes/tempus-dawn @@ -3,10 +3,12 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with a soft, slightly desaturated palette (WCAG AA compliant) -#[cursor] -#color = eff0f2 4a4b4e +[main] +initial-color-theme=light -[colors] + +[colors-light] +#cursor = eff0f2 4a4b4e foreground = 4a4b4e background = eff0f2 regular0 = 4a4b4e diff --git a/themes/tempus-day b/themes/tempus-day index b287d45c..03454f04 100644 --- a/themes/tempus-day +++ b/themes/tempus-day @@ -3,10 +3,11 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with warm colours (WCAG AA compliant) -#[cursor] -#color = f8f2e5 464340 +[main] +initial-color-theme=light -[colors] +[colors-light] +#cursor = f8f2e5 464340 foreground = 464340 background = f8f2e5 regular0 = 464340 diff --git a/themes/tempus-dusk b/themes/tempus-dusk index 2c0308e1..cd27aaaa 100644 --- a/themes/tempus-dusk +++ b/themes/tempus-dusk @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a deep blue-ish, slightly desaturated palette (WCAG AA compliant) -#[cursor] -#color = 1f252d a2a8ba - -[colors] +[colors-dark] +#cursor = 1f252d a2a8ba foreground = a2a8ba background = 1f252d regular0 = 1f252d diff --git a/themes/tempus-fugit b/themes/tempus-fugit index 9ebbcee7..b9dce351 100644 --- a/themes/tempus-fugit +++ b/themes/tempus-fugit @@ -3,10 +3,11 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light, pleasant theme optimised for long writing/coding sessions (WCAG AA compliant) -#[cursor] -#color = fff5f3 4d595f +[main] +initial-color-theme=light -[colors] +[colors-light] +#cursor = fff5f3 4d595f foreground = 4d595f background = fff5f3 regular0 = 4d595f diff --git a/themes/tempus-future b/themes/tempus-future index 3dd8c7a6..1f8c3c79 100644 --- a/themes/tempus-future +++ b/themes/tempus-future @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by concept art of outer space (WCAG AAA compliant) -#[cursor] -#color = 090a18 b4abac - -[colors] +[colors-dark] +#cursor = 090a18 b4abac foreground = b4abac background = 090a18 regular0 = 090a18 diff --git a/themes/tempus-night b/themes/tempus-night index de7be5ff..aae80f02 100644 --- a/themes/tempus-night +++ b/themes/tempus-night @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: High contrast dark theme with bright colours (WCAG AAA compliant) -#[cursor] -#color = 1a1a1a e0e0e0 - -[colors] +[colors-dark] +#cursor = 1a1a1a e0e0e0 foreground = e0e0e0 background = 1a1a1a regular0 = 1a1a1a diff --git a/themes/tempus-past b/themes/tempus-past index 8c66f54d..5f90ddf1 100644 --- a/themes/tempus-past +++ b/themes/tempus-past @@ -3,10 +3,11 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme inspired by old vaporwave concept art (WCAG AA compliant) -#[cursor] -#color = f3f2f4 53545b +[main] +initial-color-theme=light -[colors] +[colors-light] +#cursor = f3f2f4 53545b foreground = 53545b background = f3f2f4 regular0 = 53545b diff --git a/themes/tempus-rift b/themes/tempus-rift index 3657a7fe..8add657a 100644 --- a/themes/tempus-rift +++ b/themes/tempus-rift @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a subdued palette on the green side of the spectrum (WCAG AA compliant) -#[cursor] -#color = 162c22 bbbcbc - -[colors] +[colors-dark] +#cursor = 162c22 bbbcbc foreground = bbbcbc background = 162c22 regular0 = 162c22 diff --git a/themes/tempus-spring b/themes/tempus-spring index d50e6d06..eb15a1be 100644 --- a/themes/tempus-spring +++ b/themes/tempus-spring @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by early spring colours (WCAG AA compliant) -#[cursor] -#color = 283a37 b5b8b7 - -[colors] +[colors-dark] +#cursor = 283a37 b5b8b7 foreground = b5b8b7 background = 283a37 regular0 = 283a37 diff --git a/themes/tempus-summer b/themes/tempus-summer index 7da1d8c4..74c8faa2 100644 --- a/themes/tempus-summer +++ b/themes/tempus-summer @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by summer evenings by the sea (WCAG AA compliant) -#[cursor] -#color = 202c3d a0abae - -[colors] +[colors-dark] +#cursor = 202c3d a0abae foreground = a0abae background = 202c3d regular0 = 202c3d diff --git a/themes/tempus-tempest b/themes/tempus-tempest index 57c300aa..f1cf55bf 100644 --- a/themes/tempus-tempest +++ b/themes/tempus-tempest @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: A green-scale, subtle theme for late night hackers (WCAG AAA compliant) -#[cursor] -#color = 282b2b b6e0ca - -[colors] +[colors-dark] +#cursor = 282b2b b6e0ca foreground = b6e0ca background = 282b2b regular0 = 282b2b diff --git a/themes/tempus-totus b/themes/tempus-totus index 01e84692..fae6ede3 100644 --- a/themes/tempus-totus +++ b/themes/tempus-totus @@ -3,10 +3,11 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme for prose or for coding in an open space (WCAG AAA compliant) -#[cursor] -#color = ffffff 4a484d +[main] +initial-color-theme=light -[colors] +[colors-light] +#cursor = ffffff 4a484d foreground = 4a484d background = ffffff regular0 = 4a484d diff --git a/themes/tempus-warp b/themes/tempus-warp index fa8c21c2..906b3f37 100644 --- a/themes/tempus-warp +++ b/themes/tempus-warp @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a vibrant palette (WCAG AA compliant) -#[cursor] -#color = 001514 a29fa0 - -[colors] +[colors-dark] +#cursor = 001514 a29fa0 foreground = a29fa0 background = 001514 regular0 = 001514 diff --git a/themes/tempus-winter b/themes/tempus-winter index 8db97057..dc95128b 100644 --- a/themes/tempus-winter +++ b/themes/tempus-winter @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by winter nights at the city (WCAG AA compliant) -#[cursor] -#color = 202427 8da3b8 - -[colors] +[colors-dark] +#cursor = 202427 8da3b8 foreground = 8da3b8 background = 202427 regular0 = 202427 diff --git a/themes/tokyonight-day b/themes/tokyonight-day deleted file mode 100644 index 5143aa07..00000000 --- a/themes/tokyonight-day +++ /dev/null @@ -1,21 +0,0 @@ -# -*- conf -*- - -[colors] -background=e1e2e7 -foreground=3760bf -regular0=e9e9ed -regular1=f52a65 -regular2=587539 -regular3=8c6c3e -regular4=2e7de9 -regular5=9854f1 -regular6=007197 -regular7=6172b0 -bright0=a1a6c5 -bright1=f52a65 -bright2=587539 -bright3=8c6c3e -bright4=2e7de9 -bright5=9854f1 -bright6=007197 -bright7=3760bf \ No newline at end of file diff --git a/themes/tokyonight-light b/themes/tokyonight-light new file mode 100644 index 00000000..359a31b9 --- /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 index f789e1bd..58037f72 100644 --- a/themes/tokyonight-night +++ b/themes/tokyonight-night @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] background=1a1b26 foreground=c0caf5 regular0=15161E diff --git a/themes/tokyonight-storm b/themes/tokyonight-storm index 074b4697..4dbbf6c6 100644 --- a/themes/tokyonight-storm +++ b/themes/tokyonight-storm @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] background=24283b foreground=c0caf5 regular0=1D202F diff --git a/themes/visibone b/themes/visibone index 3ee665d0..b989b36b 100644 --- a/themes/visibone +++ b/themes/visibone @@ -1,10 +1,8 @@ # -*- conf -*- # VisiBone -[cursor] -color=010101 ffffff - -[colors] +[colors-dark] +cursor=010101 ffffff foreground=ffffff background=010101 regular0=666666 diff --git a/themes/xterm b/themes/xterm new file mode 100644 index 00000000..a9382fd8 --- /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 index bace080c..37a26812 100644 --- a/themes/zenburn +++ b/themes/zenburn @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] foreground=dcdccc background=111111 diff --git a/tokenize.c b/tokenize.c index 77cc3f1a..70ceb39b 100644 --- a/tokenize.c +++ b/tokenize.c @@ -45,7 +45,7 @@ tokenize_cmdline(const char *cmdline, char ***argv) size_t idx = 0; while (*p != '\0') { - char *end = strchr(search_start, delim); + char *end = (char *)strchr(search_start, delim); if (end == NULL) { if (delim != ' ') { LOG_ERR("unterminated %s quote", delim == '"' ? "double" : "single"); diff --git a/unicode-mode.c b/unicode-mode.c index a69601ec..1acdc664 100644 --- a/unicode-mode.c +++ b/unicode-mode.c @@ -7,31 +7,30 @@ #include "search.h" void -unicode_mode_activate(struct seat *seat) +unicode_mode_activate(struct terminal *term) { - if (seat->unicode_mode.active) + if (term->unicode_mode.active) return; - seat->unicode_mode.active = true; - seat->unicode_mode.character = u'\0'; - seat->unicode_mode.count = 0; - unicode_mode_updated(seat); + 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 seat *seat) +unicode_mode_deactivate(struct terminal *term) { - if (!seat->unicode_mode.active) + if (!term->unicode_mode.active) return; - seat->unicode_mode.active = false; - unicode_mode_updated(seat); + term->unicode_mode.active = false; + unicode_mode_updated(term); } void -unicode_mode_updated(struct seat *seat) +unicode_mode_updated(struct terminal *term) { - struct terminal *term = seat->kbd_focus; if (term == NULL) return; @@ -52,10 +51,10 @@ unicode_mode_input(struct seat *seat, struct terminal *term, { char utf8[MB_CUR_MAX]; size_t chars = c32rtomb( - utf8, seat->unicode_mode.character, &(mbstate_t){0}); + utf8, term->unicode_mode.character, &(mbstate_t){0}); LOG_DBG("Unicode input: 0x%06x -> %.*s", - seat->unicode_mode.character, (int)chars, utf8); + term->unicode_mode.character, (int)chars, utf8); if (chars != (size_t)-1) { if (term->is_searching) @@ -64,7 +63,7 @@ unicode_mode_input(struct seat *seat, struct terminal *term, term_to_slave(term, utf8, chars); } - unicode_mode_deactivate(seat); + unicode_mode_deactivate(term); } else if (sym == XKB_KEY_Escape || @@ -73,23 +72,25 @@ unicode_mode_input(struct seat *seat, struct terminal *term, sym == XKB_KEY_d || sym == XKB_KEY_g))) { - unicode_mode_deactivate(seat); + unicode_mode_deactivate(term); } else if (sym == XKB_KEY_BackSpace) { - if (seat->unicode_mode.count > 0) { - seat->unicode_mode.character >>= 4; - seat->unicode_mode.count--; - unicode_mode_updated(seat); + if (term->unicode_mode.count > 0) { + term->unicode_mode.character >>= 4; + term->unicode_mode.count--; + unicode_mode_updated(term); } } - else if (seat->unicode_mode.count < 6) { + 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) @@ -97,10 +98,10 @@ unicode_mode_input(struct seat *seat, struct terminal *term, if (digit >= 0) { xassert(digit >= 0 && digit <= 0xf); - seat->unicode_mode.character <<= 4; - seat->unicode_mode.character |= digit; - seat->unicode_mode.count++; - unicode_mode_updated(seat); + 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 index e7c75b9b..2f8d2b35 100644 --- a/unicode-mode.h +++ b/unicode-mode.h @@ -2,10 +2,10 @@ #include <xkbcommon/xkbcommon-keysyms.h> -#include "wayland.h" +#include "terminal.h" -void unicode_mode_activate(struct seat *seat); -void unicode_mode_deactivate(struct seat *seat); -void unicode_mode_updated(struct seat *seat); +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 00000000..43738353 --- /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 index 7214a479..e09f7a97 100644 --- a/uri.c +++ b/uri.c @@ -144,6 +144,21 @@ uri_parse(const char *uri, size_t len, 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 : @@ -250,7 +265,7 @@ hostname_is_localhost(const char *hostname) this_host[0] = '\0'; return (hostname != NULL && ( - strcmp(hostname, "") == 0 || - strcmp(hostname, "localhost") == 0 || - strcmp(hostname, this_host) == 0)); + streq(hostname, "") || + streq(hostname, "localhost") || + streq(hostname, this_host))); } diff --git a/url-mode.c b/url-mode.c index 7d7ffd81..44809f5f 100644 --- a/url-mode.c +++ b/url-mode.c @@ -4,6 +4,7 @@ #include <string.h> #include <wctype.h> #include <unistd.h> +#include <regex.h> #include <sys/stat.h> #include <fcntl.h> @@ -14,6 +15,7 @@ #include "char32.h" #include "grid.h" #include "key-binding.h" +#include "quirks.h" #include "render.h" #include "selection.h" #include "spawn.h" @@ -65,16 +67,18 @@ spawn_url_launcher_with_token(struct terminal *term, return false; } + xassert(term->url_launch != NULL); bool ret = false; if (spawn_expand_template( - &term->conf->url.launch, 1, - (const char *[]){"url"}, - (const char *[]){url}, + 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, xdg_activation_token); + 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]); @@ -85,7 +89,6 @@ spawn_url_launcher_with_token(struct terminal *term, return ret; } -#if defined(HAVE_XDG_ACTIVATION) struct spawn_activation_context { struct terminal *term; char *url; @@ -100,13 +103,13 @@ activation_token_done(const char *token, void *data) free(ctx->url); free(ctx); } -#endif static bool spawn_url_launcher(struct seat *seat, struct terminal *term, const char *url, uint32_t serial) { -#if defined(HAVE_XDG_ACTIVATION) + xassert(term->url_launch != NULL); + struct spawn_activation_context *ctx = xmalloc(sizeof(*ctx)); *ctx = (struct spawn_activation_context){ .term = term, @@ -122,14 +125,13 @@ spawn_url_launcher(struct seat *seat, struct terminal *term, const char *url, free(ctx->url); free(ctx); -#endif 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) + uint32_t serial, bool paste_url_to_self) { char *url_string = NULL; @@ -157,6 +159,15 @@ activate_url(struct seat *seat, struct terminal *term, const struct 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; @@ -177,28 +188,17 @@ 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, - xkb_mod_mask_t locked, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial) { - const xkb_mod_mask_t bind_mods = - mods & seat->kbd.bind_significant & ~locked; - const xkb_mod_mask_t bind_consumed = - consumed & seat->kbd.bind_significant & ~locked; + /* + * Key bindings + */ - /* Key bindings */ + /* Match untranslated symbols */ tll_foreach(bindings->url, it) { const struct key_binding *bind = &it->item; - - /* Match translated symbol */ - if (bind->k.sym == sym && - bind->mods == (bind_mods & ~bind_consumed)) - { - execute_binding(seat, term, bind, serial); - return; - } - - if (bind->mods != bind_mods || bind_mods != (mods & ~locked)) + if (bind->mods != mods || bind->mods == 0) continue; for (size_t i = 0; i < raw_count; i++) { @@ -207,6 +207,26 @@ urls_input(struct seat *seat, struct terminal *term, 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) { @@ -228,13 +248,13 @@ urls_input(struct seat *seat, struct terminal *term, return; } - if (mods & ~consumed & ~locked) + 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 + * 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. */ @@ -262,7 +282,9 @@ urls_input(struct seat *seat, struct terminal *term, } if (match) { - activate_url(seat, term, match, serial); + // 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: @@ -284,218 +306,147 @@ urls_input(struct seat *seat, struct terminal *term, } } -static int -c32cmp_single(const void *_a, const void *_b) -{ - const char32_t *a = _a; - const char32_t *b = _b; - return *a - *b; -} +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 -auto_detected(const struct terminal *term, enum url_action action, - url_list_t *urls) +regex_detected(const struct terminal *term, enum url_action action, + const regex_t *preg, url_list_t *urls) { - const struct config *conf = term->conf; + /* + * 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. + */ - const char32_t *uri_characters = conf->url.uri_characters; - if (uri_characters == NULL) - return; + /* There is *at most* term->rows logical lines */ + struct vline vlines[term->rows]; + size_t vline_idx = 0; - const size_t uri_characters_count = c32len(uri_characters); - if (uri_characters_count == 0) - return; + memset(vlines, 0, sizeof(vlines)); + struct vline *vline = &vlines[vline_idx]; - size_t max_prot_len = conf->url.max_prot_len; - char32_t proto_chars[max_prot_len]; - struct coord proto_start[max_prot_len]; - size_t proto_char_count = 0; - - enum { - STATE_PROTOCOL, - STATE_URL, - } state = STATE_PROTOCOL; - - struct coord start = {-1, -1}; - char32_t url[term->cols * term->rows + 1]; - size_t len = 0; - - ssize_t parenthesis = 0; - ssize_t brackets = 0; - ssize_t ltgts = 0; + 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]; - char32_t wc = cell->wc; + const char32_t *wc = &cell->wc; + size_t wc_count = 1; - switch (state) { - case STATE_PROTOCOL: - for (size_t i = 0; i < max_prot_len - 1; i++) { - proto_chars[i] = proto_chars[i + 1]; - proto_start[i] = proto_start[i + 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); - if (proto_char_count >= max_prot_len) - proto_char_count = max_prot_len - 1; - - proto_chars[max_prot_len - 1] = wc; - proto_start[max_prot_len - 1] = (struct coord){c, r}; - proto_char_count++; - - for (size_t i = 0; i < conf->url.prot_count; i++) { - size_t prot_len = c32len(conf->url.protocols[i]); - - if (proto_char_count < prot_len) - continue; - - const char32_t *proto = &proto_chars[max_prot_len - prot_len]; - - if (c32ncasecmp(conf->url.protocols[i], proto, prot_len) == 0) { - state = STATE_URL; - start = proto_start[max_prot_len - prot_len]; - - c32ncpy(url, proto, prot_len); - len = prot_len; - - parenthesis = brackets = ltgts = 0; - break; - } - } - break; - - case STATE_URL: { - const char32_t *match = bsearch( - &wc, - uri_characters, - uri_characters_count, - sizeof(uri_characters[0]), - &c32cmp_single); - - bool emit_url = false; - - if (match == NULL) { - /* - * Character is not a valid URI character. Emit - * the URL we’ve collected so far, *without* - * including _this_ character. - */ - emit_url = true; - } else { - xassert(*match == wc); - - switch (wc) { - default: - url[len++] = wc; - break; - - case U'(': - parenthesis++; - url[len++] = wc; - break; - - case U'[': - brackets++; - url[len++] = wc; - break; - - case U'<': - ltgts++; - url[len++] = wc; - break; - - case U')': - if (--parenthesis < 0) - emit_url = true; - else - url[len++] = wc; - break; - - case U']': - if (--brackets < 0) - emit_url = true; - else - url[len++] = wc; - break; - - case U'>': - if (--ltgts < 0) - emit_url = true; - else - url[len++] = wc; - break; - } - } - - if (c >= term->cols - 1 && row->linebreak) { - /* - * Endpoint is inclusive, and we’ll be subtracting - * 1 from the column when emitting the URL. - */ - c++; - emit_url = true; - } - - if (emit_url) { - struct coord end = {c, r}; - - if (--end.col < 0) { - end.row--; - end.col = term->cols - 1; - } - - /* Heuristic to remove trailing characters that - * are valid URL characters, but typically not at - * the end of the URL */ - bool done = false; - do { - switch (url[len - 1]) { - case U'.': case U',': case U':': case U';': case U'?': - case U'!': case U'"': case U'\'': case U'%': - len--; - end.col--; - if (end.col < 0) { - end.row--; - end.col = term->cols - 1; - } - break; - - default: - done = true; - break; - } - } while (!done); - - url[len] = U'\0'; - - start.row += term->grid->view; - end.row += term->grid->view; - - char *url_utf8 = ac32tombs(url); - if (url_utf8 != NULL) { - tll_push_back( - *urls, - ((struct url){ - .id = (uint64_t)rand() << 32 | rand(), - .url = url_utf8, - .range = { - .start = start, - .end = end, - }, - .action = action, - .osc8 = false})); - } - - state = STATE_PROTOCOL; - len = 0; - parenthesis = brackets = ltgts = 0; - } - break; + 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); } } @@ -522,7 +473,7 @@ osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) continue; for (size_t i = 0; i < extra->uri_ranges.count; i++) { - const struct row_uri_range *range = &extra->uri_ranges.v[i]; + const struct row_range *range = &extra->uri_ranges.v[i]; struct coord start = { .col = range->start, @@ -535,8 +486,8 @@ osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) tll_push_back( *urls, ((struct url){ - .id = range->id, - .url = xstrdup(range->uri), + .id = range->uri.id, + .url = xstrdup(range->uri.uri), .range = { .start = start, .end = end, @@ -570,7 +521,7 @@ remove_overlapping(url_list_t *urls, int cols) (in_start >= out_start && in_end <= out_end)) { /* - * OSC-8 URLs can’t overlap with each + * OSC-8 URLs can't overlap with each * other. * * Similarly, auto-detected URLs cannot overlap with @@ -597,22 +548,16 @@ remove_overlapping(url_list_t *urls, int cols) } void -urls_collect(const struct terminal *term, enum url_action action, url_list_t *urls) +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); - osc8_uris(term, action, urls); - auto_detected(term, action, urls); + if (osc8) + osc8_uris(term, action, urls); + regex_detected(term, action, preg, urls); remove_overlapping(urls, term->grid->num_cols); } -static int -c32cmp_qsort_wrapper(const void *_a, const void *_b) -{ - const char32_t *a = *(const char32_t **)_a; - const char32_t *b = *(const char32_t **)_b; - return c32cmp(a, b); -} - static void generate_key_combos(const struct config *conf, size_t count, char32_t *combos[static count]) @@ -646,7 +591,7 @@ generate_key_combos(const struct config *conf, xassert(hints_count - offset >= count); - /* Copy slice of ‘hints’ array to the caller provided array */ + /* 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]; @@ -655,10 +600,6 @@ generate_key_combos(const struct config *conf, } free(hints); - /* Sorting is a kind of shuffle, since we’re sorting on the - * *reversed* strings */ - qsort(combos, count, sizeof(char32_t *), &c32cmp_qsort_wrapper); - /* Reverse all strings */ for (size_t i = 0; i < count; i++) { const size_t len = c32len(combos[i]); @@ -682,17 +623,17 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) size_t combo_idx = 0; - tll_foreach(*urls, it) { + tll_rforeach(*urls, it) { bool id_already_seen = false; /* Look for already processed URLs where both the URI and the * ID matches */ - tll_foreach(*urls, it2) { + tll_rforeach(*urls, it2) { if (&it->item == &it2->item) break; if (it->item.id == it2->item.id && - strcmp(it->item.url, it2->item.url) == 0) + streq(it->item.url, it2->item.url)) { id_already_seen = true; break; @@ -704,14 +645,14 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) /* * Scan previous URLs, and check if *this* URL matches any of - * them; if so, re-use the *same* key combo. + * them; if so, reuse the *same* key combo. */ bool url_already_seen = false; - tll_foreach(*urls, it2) { + tll_rforeach(*urls, it2) { if (&it->item == &it2->item) break; - if (strcmp(it->item.url, it2->item.url) == 0) { + if (streq(it->item.url, it2->item.url)) { it->item.key = xc32dup(it2->item.key); url_already_seen = true; break; @@ -722,12 +663,12 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) it->item.key = combos[combo_idx++]; } - /* Free combos we didn’t use up */ + /* 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_foreach(*urls, it) { + tll_rforeach(*urls, it) { if (it->item.key == NULL) continue; @@ -784,13 +725,19 @@ tag_cells_for_url(struct terminal *term, const struct url *url, bool value) } void -urls_render(struct terminal *term) +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; @@ -802,7 +749,7 @@ urls_render(struct terminal *term) } term->render.last_cursor.row = NULL; - /* Clear scroll damage, to ensure we don’t apply it twice (once on + /* 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); @@ -813,6 +760,9 @@ urls_render(struct terminal *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}; @@ -846,10 +796,10 @@ urls_reset(struct terminal *term) term->url_grid_snapshot = NULL; /* - * Make sure “last cursor” doesn’t point to a row in the just + * 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), + * Note that it will still be erased properly (if hasn't already), * since we marked the cell as dirty *before* taking the grid * snapshot. */ @@ -870,5 +820,11 @@ urls_reset(struct terminal *term) 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 index abfcb57b..758cd92f 100644 --- a/url-mode.h +++ b/url-mode.h @@ -14,15 +14,15 @@ static inline bool urls_mode_is_active(const struct terminal *term) } void urls_collect( - const struct terminal *term, enum url_action action, url_list_t *urls); + 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); +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, - xkb_mod_mask_t locked, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial); diff --git a/util.h b/util.h index 683dbd4a..3746e269 100644 --- a/util.h +++ b/util.h @@ -1,12 +1,20 @@ #pragma once +#include <stdbool.h> #include <stdint.h> +#include <string.h> #include <threads.h> #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) { diff --git a/utils/xtgettcap.c b/utils/xtgettcap.c index b3ab712a..82ee0085 100644 --- a/utils/xtgettcap.c +++ b/utils/xtgettcap.c @@ -103,7 +103,7 @@ main(int argc, const char *const *argv) if (isprint(buf[i])) printf("%c", buf[i]); else if (buf[i] == '\033') - printf("\033[1;31m\\E\033[m"); + printf("\033[1;31m<ESC>\033[m"); else printf("%02x", (uint8_t)buf[i]); } @@ -141,6 +141,9 @@ main(int argc, const char *const *argv) const char *key = strtok(key_value, "="); const char *value = strtok(NULL, "="); + if (key == NULL) + continue; + #if 0 assert((success && value != NULL) || (!success && value == NULL)); @@ -158,12 +161,30 @@ main(int argc, const char *const *argv) printf(" \033[%dm", color); for (size_t i = 0 ; i < len; i++) { - if (isprint(decoded[i])) + if (isprint(decoded[i])) { + /* All printable characters */ printf("%c", decoded[i]); - else if (decoded[i] == '\033') - printf("\033[1;31m\\E\033[22;%dm", color); - else + } + + else if (decoded[i] == '\033') { + /* ESC */ + printf("\033[1;31m<ESC>\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++; diff --git a/vt.c b/vt.c index 91f00e6f..1d8297be 100644 --- a/vt.c +++ b/vt.c @@ -16,8 +16,8 @@ #include "csi.h" #include "dcs.h" #include "debug.h" -#include "grid.h" #include "osc.h" +#include "sixel.h" #include "util.h" #include "xmalloc.h" @@ -137,12 +137,12 @@ action_execute(struct terminal *term, uint8_t c) /* backspace */ #if 0 /* - * This is the “correct” BS behavior. However, it doesn’t play + * 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” + * Note that it breaks vttest "1. Test of cursor movements -> + * Test of autowrap" */ term_cursor_left(term, 1); #else @@ -154,7 +154,7 @@ action_execute(struct terminal *term, uint8_t c) 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(?) */ + /* Don't wrap past, or inside, the scrolling region(?) */ } else term_cursor_to( term, @@ -241,12 +241,16 @@ action_execute(struct terminal *term, uint8_t c) 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; @@ -294,74 +298,31 @@ action_print(struct terminal *term, uint8_t c) } static void -action_param(struct terminal *term, uint8_t c) +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; } +} - xassert(term->vt.params.idx > 0); +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]); - const size_t max_sub_params - = sizeof(term->vt.params.v[0].sub.value) / sizeof(term->vt.params.v[0].sub.value[0]); - /* New parameter */ - if (c == ';') { - if (unlikely(term->vt.params.idx >= max_params)) - goto excess_params; + struct vt_param *param; - struct vt_param *param = &term->vt.params.v[term->vt.params.idx++]; - param->value = 0; - param->sub.idx = 0; - } - - /* New sub-parameter */ - else if (c == ':') { - if (unlikely(term->vt.params.idx - 1 >= max_params)) - goto excess_params; - - struct vt_param *param = &term->vt.params.v[term->vt.params.idx - 1]; - if (unlikely(param->sub.idx >= max_sub_params)) - goto excess_sub_params; - - param->sub.value[param->sub.idx++] = 0; - } - - /* New digit for current parameter/sub-parameter */ - else { - if (unlikely(term->vt.params.idx - 1 >= max_params)) - goto excess_params; - - struct vt_param *param = &term->vt.params.v[term->vt.params.idx - 1]; - unsigned *value; - - if (param->sub.idx > 0) { - if (unlikely(param->sub.idx - 1 >= max_sub_params)) - goto excess_sub_params; - value = ¶m->sub.value[param->sub.idx - 1]; - } else - value = ¶m->value; - - *value *= 10; - *value += c - '0'; - } - -#if defined(_DEBUG) - /* The rest of the code assumes 'idx' *never* points outside the array */ - xassert(term->vt.params.idx <= max_params); - for (size_t i = 0; i < term->vt.params.idx; i++) - xassert(term->vt.params.v[i].sub.idx <= max_sub_params); -#endif - - return; - -excess_params: - { + if (unlikely(term->vt.params.idx >= max_params)) { static bool have_warned = false; if (!have_warned) { have_warned = true; @@ -370,11 +331,29 @@ excess_params: "(will not warn again)", sizeof(term->vt.params.v) / sizeof(term->vt.params.v[0])); } - } - return; + param = &term->vt.params.dummy; + } else + param = &term->vt.params.v[term->vt.params.idx++]; -excess_sub_params: - { + 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; @@ -383,8 +362,33 @@ excess_sub_params: "(will not warn again)", sizeof(term->vt.params.v[0].sub.value) / sizeof(term->vt.params.v[0].sub.value[0])); } - } - return; + + 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 @@ -398,7 +402,7 @@ action_collect(struct terminal *term, uint8_t c) * more. * * As such, we optimize *reading* the private(s), and *resetting* - * them (in action_clear()). Writing is ok if it’s a bit slow. + * them (in action_clear()). Writing is ok if it's a bit slow. */ if ((term->vt.private & 0xff) == 0) @@ -436,6 +440,27 @@ UNITTEST 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) { @@ -459,12 +484,16 @@ action_esc_dispatch(struct terminal *term, uint8_t final) 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; @@ -478,14 +507,7 @@ action_esc_dispatch(struct terminal *term, uint8_t final) break; case 'H': - tll_foreach(term->tab_stops, it) { - if (it->item >= term->grid->cursor.point.col) { - tll_insert_before(term->tab_stops, it, term->grid->cursor.point.col); - break; - } - } - - tll_push_back(term->tab_stops, term->grid->cursor.point.col); + tab_set(term); break; case 'M': @@ -530,6 +552,8 @@ action_esc_dispatch(struct terminal *term, uint8_t final) 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; } @@ -538,6 +562,8 @@ action_esc_dispatch(struct terminal *term, uint8_t final) 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; } @@ -546,15 +572,16 @@ action_esc_dispatch(struct terminal *term, uint8_t final) case '#': switch (final) { - case '8': - for (int r = 0; r < term->rows; r++) { - struct row *row = grid_row(term->grid, r); - for (int c = 0; c < term->cols; c++) { - row->cells[c].wc = U'E'; - row->cells[c].attrs = (struct attributes){0}; - } - row->dirty = true; - } + 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] == '#' */ @@ -619,250 +646,10 @@ action_put(struct terminal *term, uint8_t c) dcs_put(term, c); } -static inline uint32_t -chain_key(uint32_t old_key, uint32_t new_wc) -{ - unsigned bits = 32 - __builtin_clz(CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO); - - /* Rotate old key 8 bits */ - uint32_t new_key = (old_key << 8) | (old_key >> (bits - 8)); - - /* xor with new char */ - new_key ^= new_wc; - - /* Multiply with magic hash constant */ - new_key *= 2654435761; - - /* And mask, to ensure the new value is within range */ - new_key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; - - return new_key; -} - static void action_utf8_print(struct terminal *term, char32_t wc) { - int width = c32width(wc); - const bool grapheme_clustering = term->conf->tweak.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 = chain_key(composed->key, wc); - } else - key = chain_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; - - if (composed == NULL) { - bool base_from_primary; - bool comb_from_primary; - bool pre_from_primary; - - char32_t precomposed = fcft_precompose( - term->fonts[0], base, wc, &base_from_primary, - &comb_from_primary, &pre_from_primary); - - 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); - - size_t collision_count = 0; - - /* Look for existing combining chain */ - while (true) { - if (unlikely(collision_count > 128)) { - static bool have_warned = false; - if (!have_warned) { - have_warned = true; - LOG_WARN("ignoring composed character: " - "too many collisions in hash table"); - } - return; - } - - const struct composed *cc = composed_lookup(term->composed, key); - if (cc == NULL) - break; - - /* - * We may have a key collisison, so need to check that - * it’s a true match. If not, bump the key and try - * again. - */ - - xassert(key == cc->key); - if (cc->chars[0] != base || - cc->count != wanted_count || - cc->chars[wanted_count - 1] != wc) - { -#if 0 - LOG_WARN("COLLISION: base: %04x/%04x, count: %d/%zu, last: %04x/%04x", - cc->chars[0], base, cc->count, wanted_count, cc->chars[wanted_count - 1], wc); -#endif - key++; - collision_count++; - continue; - } - - bool match = composed != NULL - ? memcmp(&cc->chars[1], &composed->chars[1], - (wanted_count - 2) * sizeof(cc->chars[0])) == 0 - : true; - - if (!match) { - key++; - collision_count++; - continue; - } - - wc = CELL_COMB_CHARS_LO + cc->key; - width = cc->width; - goto out; - } - - 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; - - 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: - if (unlikely(wc == 0xfe0f)) - width = 2; - new_cc->width = min(grapheme_width + width, 2); - 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 + key; - 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); + term_process_and_print_non_ascii(term, wc); } static void @@ -899,6 +686,16 @@ 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); } @@ -928,6 +725,17 @@ 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); } @@ -1024,7 +832,9 @@ state_csi_entry_switch(struct terminal *term, uint8_t data) case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE; case 0x30 ... 0x39: action_param(term, data); return STATE_CSI_PARAM; - case 0x3a ... 0x3b: 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; @@ -1044,8 +854,9 @@ state_csi_param_switch(struct terminal *term, uint8_t data) case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE; - case 0x30 ... 0x39: - case 0x3a ... 0x3b: action_param(term, data); return STATE_CSI_PARAM; + 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; @@ -1126,7 +937,7 @@ state_dcs_entry_switch(struct terminal *term, uint8_t data) 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(term, data); return STATE_DCS_PARAM; + 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; @@ -1147,7 +958,7 @@ state_dcs_param_switch(struct terminal *term, uint8_t data) 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(term, data); return STATE_DCS_PARAM; + 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; @@ -1230,7 +1041,7 @@ 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: return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1240,7 +1051,7 @@ 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: return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1250,7 +1061,7 @@ 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: return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1260,7 +1071,7 @@ 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: return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1270,7 +1081,7 @@ 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: return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1280,7 +1091,7 @@ 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: return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } diff --git a/wayland.c b/wayland.c index 68a7a4f1..1ffd62a6 100644 --- a/wayland.c +++ b/wayland.c @@ -1,17 +1,21 @@ #include "wayland.h" +#include <errno.h> +#include <fcntl.h> +#include <locale.h> +#include <poll.h> #include <stdlib.h> #include <string.h> #include <unistd.h> -#include <errno.h> -#include <poll.h> -#include <fcntl.h> #include <sys/timerfd.h> #include <sys/epoll.h> +#include <cursor-shape-v1.h> #include <wayland-client.h> #include <wayland-cursor.h> +#include <xkbcommon/xkbcommon.h> +#include <xkbcommon/xkbcommon-keysyms.h> #include <xkbcommon/xkbcommon-compose.h> #include <tllist.h> @@ -32,12 +36,12 @@ #include "xmalloc.h" static void -csd_reload_font(struct wl_window *win, int old_scale) +csd_reload_font(struct wl_window *win, float old_scale) { struct terminal *term = win->term; const struct config *conf = term->conf; - const int scale = term->scale; + const float scale = term->scale; bool enable_csd = win->csd_mode == CSD_YES && !win->is_fullscreen; if (!enable_csd) @@ -52,10 +56,10 @@ csd_reload_font(struct wl_window *win, int old_scale) patterns[i] = conf->csd.font.arr[i].pattern; char pixelsize[32]; - snprintf(pixelsize, sizeof(pixelsize), - "pixelsize=%u", conf->csd.title_height * scale * 1 / 2); + snprintf(pixelsize, sizeof(pixelsize), "pixelsize=%u", + (int)roundf(conf->csd.title_height * scale * 1 / 2)); - LOG_DBG("loading CSD font \"%s:%s\" (old-scale=%d, scale=%d)", + 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); @@ -74,12 +78,12 @@ csd_instantiate(struct wl_window *win) 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].surf, &win->csd.surface[i], + win, win->csd.surface[CSD_SURF_TITLE].surface.surf, &win->csd.surface[i], true); xassert(ret); } - csd_reload_font(win, -1); + csd_reload_font(win, -1.); } static void @@ -187,8 +191,10 @@ seat_destroy(struct seat *seat) if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); - if (seat->pointer.surface != NULL) - wl_surface_destroy(seat->pointer.surface); + 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); @@ -204,11 +210,14 @@ seat_destroy(struct seat *seat) 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) @@ -221,6 +230,7 @@ seat_destroy(struct seat *seat) ime_reset_pending(seat); free(seat->clipboard.text); free(seat->primary.text); + free(seat->pointer.last_custom_xcursor); free(seat->name); } @@ -229,6 +239,12 @@ 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) bool have_description = false; @@ -243,9 +259,6 @@ shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) if (!have_description) LOG_DBG("shm: 0x%08x: unknown", format); #endif - - if (format == WL_SHM_FORMAT_ARGB8888) - wayl->have_argb8888 = true; } static const struct wl_shm_listener shm_listener = { @@ -270,9 +283,10 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, struct seat *seat = data; xassert(seat->wl_seat == wl_seat); - LOG_DBG("%s: keyboard=%s, pointer=%s", seat->name, + 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_POINTER) ? "yes" : "no", + (caps & WL_SEAT_CAPABILITY_TOUCH) ? "yes" : "no"); if (caps & WL_SEAT_CAPABILITY_KEYBOARD) { if (seat->wl_keyboard == NULL) { @@ -288,31 +302,83 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, if (caps & WL_SEAT_CAPABILITY_POINTER) { if (seat->wl_pointer == NULL) { - xassert(seat->pointer.surface == NULL); - seat->pointer.surface = wl_compositor_create_surface(seat->wayl->compositor); + xassert(seat->pointer.surface.surf == NULL); + seat->pointer.surface.surf = + wl_compositor_create_surface(seat->wayl->compositor); - if (seat->pointer.surface == NULL) { + 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); + 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 = 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 @@ -331,15 +397,48 @@ static const struct wl_seat_listener seat_listener = { static void update_term_for_output_change(struct terminal *term) { - if (tll_length(term->window->on_outputs) == 0) - return; + const float old_scale = term->scale; + const float logical_width = term->width / old_scale; + const float logical_height = term->height / old_scale; - int old_scale = term->scale; - - render_resize(term, term->width / term->scale, term->height / term->scale); - term_font_dpi_changed(term, 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 @@ -350,11 +449,6 @@ update_terms_on_monitor(struct monitor *mon) tll_foreach(wayl->terms, it) { struct terminal *term = it->item; - if (term->conf->dpi_aware == DPI_AWARE_AUTO) { - update_term_for_output_change(term); - continue; - } - tll_foreach(term->window->on_outputs, it2) { if (it2->item == mon) { update_term_for_output_change(term); @@ -373,6 +467,9 @@ output_update_ppi(struct monitor *mon) 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; @@ -395,27 +492,36 @@ output_update_ppi(struct monitor *mon) break; } - int scaled_width = mon->dim.px_scaled.width; - int scaled_height = mon->dim.px_scaled.height; - - if (scaled_width == 0 && scaled_height == 0 && mon->scale > 0) { - /* Estimate scaled width/height if none has been provided */ - scaled_width = mon->dim.px_real.width / mon->scale; - scaled_height = mon->dim.px_real.height / mon->scale; - } + 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; - double px_diag = sqrt(pow(scaled_width, 2) + pow(scaled_height, 2)); - mon->dpi = px_diag / mon->inch * mon->scale; + 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; - if (mon->dpi > 1000) { + 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 is unreasonable, using 96 instead", - mon->name, mon->dpi); + LOG_WARN("%s: DPI=%f (physical) is unreasonable, using 96 instead", + mon->name, mon->dpi.physical); } - mon->dpi = 96; + 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; } } @@ -526,6 +632,8 @@ xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, 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 @@ -566,6 +674,88 @@ 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) { @@ -616,9 +806,37 @@ surface_leave(void *data, struct wl_surface *wl_surface, 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 @@ -633,6 +851,11 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, 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]; @@ -647,29 +870,47 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, [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_ACTIVATED: is_activated = true; break; - case XDG_TOPLEVEL_STATE_FULLSCREEN: is_fullscreen = true; break; 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; - case XDG_TOPLEVEL_STATE_RESIZING: is_resizing = 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 >= XDG_TOPLEVEL_STATE_MAXIMIZED && - *state <= XDG_TOPLEVEL_STATE_TILED_BOTTOM) - { + if (*state >= 0 && *state < ALEN(strings)) { state_chars += snprintf( &state_str[state_chars], sizeof(state_str) - state_chars, - "%s, ", strings[*state]); + "%s, ", + strings[*state] != NULL ? strings[*state] : "<unknown>"); } #endif } @@ -701,6 +942,10 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, 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; } @@ -735,23 +980,58 @@ xdg_toplevel_wm_capabilities(void *data, win->wm_capabilities.maximize = false; win->wm_capabilities.minimize = false; - uint32_t *cap_ptr; - wl_array_for_each(cap_ptr, caps) { - switch (*cap_ptr) { +#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] : "<unknown>"); + } +#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’... */ + /*.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 @@ -781,6 +1061,7 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, 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; @@ -789,14 +1070,22 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, 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; @@ -817,9 +1106,11 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, 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 + * 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 @@ -830,25 +1121,39 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, * Note: if we also disable content centering while resizing, then * the last, forced, resize *is* necessary. */ - bool resized = was_resizing && !win->is_resizing - ? render_resize_force(term, new_width, new_height) - : render_resize(term, new_width, new_height); -#else - bool resized = render_resize(term, new_width, new_height); + 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); - if (!resized) { + /* + * 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, we won't be committing a new surface - * anytime soon. Some compositors require a commit in - * combination with an ack - make them happy. + * 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); + wl_surface_commit(win->surface.surf); } if (wasnt_configured) @@ -888,6 +1193,27 @@ static const struct zxdg_toplevel_decoration_v1_listener xdg_toplevel_decoration .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) { @@ -921,16 +1247,21 @@ handle_global(void *data, struct wl_registry *registry, LOG_DBG("global: 0x%08x, interface=%s, version=%u", name, interface, version); struct wayland *wayl = data; - if (strcmp(interface, wl_compositor_interface.name) == 0) { + 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, required); + wayl->registry, name, &wl_compositor_interface, min(version, preferred)); } - else if (strcmp(interface, wl_subcompositor_interface.name) == 0) { + else if (streq(interface, wl_subcompositor_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -939,29 +1270,50 @@ handle_global(void *data, struct wl_registry *registry, wayl->registry, name, &wl_subcompositor_interface, required); } - else if (strcmp(interface, wl_shm_interface.name) == 0) { + 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, required); + 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 (strcmp(interface, xdg_wm_base_interface.name) == 0) { + 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 5. Version 2 - * adds 'tiled' window states. We use that information to - * restore the window size when window is un-tiled. Version 5 - * adds 'wm_capabilities'. We use that information to draw - * window decorations. + * 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_WM_CAPABILITIES_SINCE_VERSION) +#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; @@ -974,7 +1326,7 @@ handle_global(void *data, struct wl_registry *registry, xdg_wm_base_add_listener(wayl->shell, &xdg_wm_base_listener, wayl); } - else if (strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0) { + else if (streq(interface, zxdg_decoration_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -983,11 +1335,17 @@ handle_global(void *data, struct wl_registry *registry, wayl->registry, name, &zxdg_decoration_manager_v1_interface, required); } - else if (strcmp(interface, wl_seat_interface.name) == 0) { + 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"); @@ -995,7 +1353,7 @@ handle_global(void *data, struct wl_registry *registry, } struct wl_seat *wl_seat = wl_registry_bind( - wayl->registry, name, &wl_seat_interface, required); + wayl->registry, name, &wl_seat_interface, min(version, preferred)); tll_push_back(wayl->seats, ((struct seat){ .wayl = wayl, @@ -1016,6 +1374,19 @@ handle_global(void *data, struct wl_registry *registry, 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); @@ -1023,7 +1394,7 @@ handle_global(void *data, struct wl_registry *registry, wl_seat_add_listener(wl_seat, &seat_listener, seat); } - else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { + else if (streq(interface, zxdg_output_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1040,7 +1411,7 @@ handle_global(void *data, struct wl_registry *registry, } } - else if (strcmp(interface, wl_output_interface.name) == 0) { + else if (streq(interface, wl_output_interface.name)) { const uint32_t required = 2; if (!verify_iface_version(interface, version, required)) return; @@ -1072,7 +1443,7 @@ handle_global(void *data, struct wl_registry *registry, } } - else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) { + else if (streq(interface, wl_data_device_manager_interface.name)) { const uint32_t required = 3; if (!verify_iface_version(interface, version, required)) return; @@ -1084,7 +1455,7 @@ handle_global(void *data, struct wl_registry *registry, seat_add_data_device(&it->item); } - else if (strcmp(interface, zwp_primary_selection_device_manager_v1_interface.name) == 0) { + 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; @@ -1097,7 +1468,7 @@ handle_global(void *data, struct wl_registry *registry, seat_add_primary_selection(&it->item); } - else if (strcmp(interface, wp_presentation_interface.name) == 0) { + else if (streq(interface, wp_presentation_interface.name)) { if (wayl->presentation_timings) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) @@ -1110,8 +1481,7 @@ handle_global(void *data, struct wl_registry *registry, } } -#if defined(HAVE_XDG_ACTIVATION) - else if (strcmp(interface, xdg_activation_v1_interface.name) == 0) { + else if (streq(interface, xdg_activation_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1119,10 +1489,110 @@ handle_global(void *data, struct wl_registry *registry, 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 (strcmp(interface, zwp_text_input_manager_v3_interface.name) == 0) { + else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1134,6 +1604,7 @@ handle_global(void *data, struct wl_registry *registry, seat_add_text_input(&it->item); } #endif + } static void @@ -1204,7 +1675,7 @@ handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) if (seat->wl_keyboard != NULL) keyboard_listener.leave( - seat, seat->wl_keyboard, -1, seat->kbd_focus->window->surface); + seat, seat->wl_keyboard, -1, seat->kbd_focus->window->surface.surf); } if (seat->mouse_focus != NULL) { @@ -1214,7 +1685,7 @@ handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) if (seat->wl_pointer != NULL) pointer_listener.leave( - seat, seat->wl_pointer, -1, seat->mouse_focus->window->surface); + seat, seat->wl_pointer, -1, seat->mouse_focus->window->surface.surf); } seat_destroy(seat); @@ -1249,6 +1720,8 @@ fdm_wayl(struct fdm *fdm, int fd, int events, void *data) 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"); @@ -1334,24 +1807,37 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, LOG_ERR("no seats available (wl_seat interface too old?)"); goto out; } - if (wayl->primary_selection_device_manager == NULL) - LOG_WARN("no primary selection available"); - -#if defined(HAVE_XDG_ACTIVATION) - if (wayl->xdg_activation == NULL) { -#else - if (true) { -#endif - LOG_WARN( - "no XDG activation support; " - "bell.urgent will fall back to coloring the window margins red"); + if (tll_length(wayl->monitors) == 0) { + LOG_ERR("no monitors available"); + goto out; } if (presentation_timings && wayl->presentation == NULL) { - LOG_ERR("presentation time interface not implemented by compositor"); + 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; " @@ -1362,21 +1848,14 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, /* Trigger listeners registered when handling globals */ wl_display_roundtrip(wayl->display); - if (!wayl->have_argb8888) { - LOG_ERR("compositor does not support ARGB surfaces"); - goto out; - } - tll_foreach(wayl->monitors, it) { LOG_INFO( - "%s: %dx%d+%dx%d@%dHz %s %.2f\" scale=%d PPI=%dx%d (physical) PPI=%dx%d (logical), DPI=%.2f", + "%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)round(it->item.refresh), + 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.ppi.real.x, it->item.ppi.real.y, - it->item.ppi.scaled.x, it->item.ppi.scaled.y, - it->item.dpi); + it->item.dpi.physical, it->item.dpi.scaled); } wayl->fd = wl_display_get_fd(wayl->display); @@ -1435,10 +1914,33 @@ wayl_destroy(struct wayland *wayl) zwp_text_input_manager_v3_destroy(wayl->text_input_manager); #endif -#if defined(HAVE_XDG_ACTIVATION) +#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); -#endif if (wayl->xdg_output_manager != NULL) zxdg_output_manager_v1_destroy(wayl->xdg_output_manager); if (wayl->shell != NULL) @@ -1451,8 +1953,14 @@ wayl_destroy(struct wayland *wayl) 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) - wl_shm_destroy(wayl->shm); + 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) @@ -1469,6 +1977,28 @@ wayl_destroy(struct wayland *wayl) 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) { @@ -1485,30 +2015,40 @@ wayl_win_init(struct terminal *term, const char *token) 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 = wl_compositor_create_surface(wayl->compositor); - if (win->surface == NULL) { + win->surface.surf = wl_compositor_create_surface(wayl->compositor); + if (win->surface.surf == NULL) { LOG_ERR("failed to create wayland surface"); goto out; } - if (term->colors.alpha == 0xffff) { - struct wl_region *region = wl_compositor_create_region( - term->wl->compositor); + wl_surface_add_listener(win->surface.surf, &surface_listener, win); - if (region != NULL) { - wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX); - wl_surface_set_opaque_region(win->surface, region); - wl_region_destroy(region); - } + 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); } - wl_surface_add_listener(win->surface, &surface_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 - win->xdg_surface = xdg_wm_base_get_xdg_surface(wayl->shell, win->surface); + 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); @@ -1516,6 +2056,62 @@ wayl_win_init(struct terminal *term, const char *token) 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; @@ -1530,8 +2126,8 @@ wayl_win_init(struct terminal *term, const char *token) 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_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); @@ -1541,13 +2137,10 @@ wayl_win_init(struct terminal *term, const char *token) LOG_WARN("no decoration manager available - using CSDs unconditionally"); } - wl_surface_commit(win->surface); + wl_surface_commit(win->surface.surf); -#if defined(HAVE_XDG_ACTIVATION) /* Complete XDG startup notification */ - if (token) - xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface); -#endif + wayl_activate(wayl, win, token); if (!wayl_win_subsurface_new(win, &win->overlay, false)) { LOG_ERR("failed to create overlay surface"); @@ -1596,33 +2189,33 @@ wayl_win_destroy(struct wl_window *win) * nor mouse focus). */ - if (win->render_timer.surf != NULL) { - wl_surface_attach(win->render_timer.surf, NULL, 0, 0); - wl_surface_commit(win->render_timer.surf); + 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.surf != NULL) { - wl_surface_attach(win->scrollback_indicator.surf, NULL, 0, 0); - wl_surface_commit(win->scrollback_indicator.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.surf != NULL) { - wl_surface_attach(win->search.surf, NULL, 0, 0); - wl_surface_commit(win->search.surf); + if (win->search.surface.surf != NULL) { + wl_surface_attach(win->search.surface.surf, NULL, 0, 0); + wl_surface_commit(win->search.surface.surf); } /* URLs */ tll_foreach(win->urls, it) { - wl_surface_attach(it->item.surf.surf, NULL, 0, 0); - wl_surface_commit(it->item.surf.surf); + 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].surf != NULL) { - wl_surface_attach(win->csd.surface[i].surf, NULL, 0, 0); - wl_surface_commit(win->csd.surface[i].surf); + 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); } } @@ -1630,8 +2223,8 @@ wayl_win_destroy(struct wl_window *win) /* Main window */ win->unmapped = true; - wl_surface_attach(win->surface, NULL, 0, 0); - wl_surface_commit(win->surface); + 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); @@ -1641,6 +2234,8 @@ wayl_win_destroy(struct wl_window *win) 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); @@ -1654,14 +2249,24 @@ wayl_win_destroy(struct wl_window *win) shm_purge(term->render.chains.url); shm_purge(term->render.chains.csd); -#if defined(HAVE_XDG_ACTIVATION) 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) @@ -1670,8 +2275,8 @@ wayl_win_destroy(struct wl_window *win) xdg_toplevel_destroy(win->xdg_toplevel); if (win->xdg_surface != NULL) xdg_surface_destroy(win->xdg_surface); - if (win->surface != NULL) - wl_surface_destroy(win->surface); + if (win->surface.surf != NULL) + wl_surface_destroy(win->surface.surf); wayl_roundtrip(win->term->wl); @@ -1681,7 +2286,7 @@ wayl_win_destroy(struct wl_window *win) } bool -wayl_reload_xcursor_theme(struct seat *seat, int new_scale) +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 */ @@ -1695,6 +2300,11 @@ wayl_reload_xcursor_theme(struct seat *seat, int new_scale) seat->pointer.cursor = NULL; } + if (seat->pointer.shape_device != NULL) { + /* Using server side cursors */ + return true; + } + int xcursor_size = 24; { @@ -1714,7 +2324,7 @@ wayl_reload_xcursor_theme(struct seat *seat, int new_scale) const char *xcursor_theme = getenv("XCURSOR_THEME"); - LOG_INFO("cursor theme: %s, size: %d, scale: %d", + LOG_INFO("cursor theme: %s, size: %d, scale: %.2f", xcursor_theme ? xcursor_theme : "(null)", xcursor_size, new_scale); @@ -1746,7 +2356,14 @@ wayl_flush(struct wayland *wayl) } if (errno != EAGAIN) { - LOG_ERRNO("failed to flush wayland socket"); + 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; } @@ -1798,7 +2415,142 @@ wayl_roundtrip(struct wayland *wayl) wayl_flush(wayl); } -#if defined(HAVE_XDG_ACTIVATION) +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) { @@ -1806,16 +2558,14 @@ activation_token_for_urgency_done(const char *token, void *data) struct wayland *wayl = win->term->wl; win->urgency_token_is_pending = false; - xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface); + xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); } -#endif /* HAVE_XDG_ACTIVATION */ bool wayl_win_set_urgent(struct wl_window *win) { -#if defined(HAVE_XDG_ACTIVATION) if (win->urgency_token_is_pending) { - /* We already have a pending token. Don’t request another one, + /* We already have a pending token. Don't request another one, * to avoid flooding the Wayland socket */ return true; } @@ -1827,11 +2577,28 @@ wayl_win_set_urgent(struct wl_window *win) win->urgency_token_is_pending = true; return true; } -#endif 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) { @@ -1851,27 +2618,56 @@ wayl_win_csd_borders_visible(const struct wl_window *win) bool wayl_win_subsurface_new_with_custom_parent( struct wl_window *win, struct wl_surface *parent, - struct wl_surf_subsurf *surf, bool allow_pointer_input) + struct wayl_sub_surface *surf, bool allow_pointer_input) { struct wayland *wayl = win->term->wl; - surf->surf = NULL; + 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) + 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); @@ -1883,35 +2679,46 @@ wayl_win_subsurface_new_with_custom_parent( wl_region_destroy(empty); } - surf->surf = main_surface; + surf->surface.surf = main_surface; surf->sub = sub; + surf->surface.viewport = viewport; return true; } bool -wayl_win_subsurface_new(struct wl_window *win, struct wl_surf_subsurf *surf, +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, allow_pointer_input); + win, win->surface.surf, surf, allow_pointer_input); } void -wayl_win_subsurface_destroy(struct wl_surf_subsurf *surf) +wayl_win_subsurface_destroy(struct wayl_sub_surface *surf) { if (surf == NULL) return; - if (surf->sub != NULL) + + 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); - if (surf->surf != NULL) - wl_surface_destroy(surf->surf); - - surf->surf = NULL; - surf->sub = NULL; + surf->sub = NULL; + } + if (surf->surface.surf != NULL) { + wl_surface_destroy(surf->surface.surf); + surf->surface.surf = NULL; + } } -#if defined(HAVE_XDG_ACTIVATION) - static void activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, const char *token) @@ -1972,9 +2779,27 @@ wayl_get_activation_token( 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); + 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; } -#endif + +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 index 4b6939ab..9cbd1023 100644 --- a/wayland.h +++ b/wayland.h @@ -9,24 +9,37 @@ #include <xkbcommon/xkbcommon.h> /* Wayland protocols */ +#include <color-management-v1.h> +#include <fractional-scale-v1.h> #include <presentation-time.h> #include <primary-selection-unstable-v1.h> +#include <single-pixel-buffer-v1.h> #include <text-input-unstable-v3.h> +#include <viewporter.h> +#include <xdg-activation-v1.h> #include <xdg-decoration-unstable-v1.h> #include <xdg-output-unstable-v1.h> #include <xdg-shell.h> +#include <xdg-system-bell-v1.h> +#include <xdg-toplevel-icon-v1.h> -#if defined(HAVE_XDG_ACTIVATION) - #include <xdg-activation-v1.h> +#if defined(HAVE_XDG_TOPLEVEL_TAG) + #include <xdg-toplevel-tag-v1.h> +#endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + #include <ext-background-effect-v1.h> #endif #include <fcft/fcft.h> #include <tllist.h> +#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 { @@ -40,6 +53,29 @@ enum data_offer_mime_type { 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 */ @@ -109,8 +145,10 @@ struct seat { xkb_mod_index_t mod_caps; xkb_mod_index_t mod_num; - xkb_mod_mask_t bind_significant; - xkb_mod_mask_t kitty_significant; + 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; @@ -120,6 +158,8 @@ struct seat { bool alt; bool ctrl; bool super; + + xkb_keysym_t last_shortcut_sym; } kbd; /* Pointer state */ @@ -127,17 +167,35 @@ struct seat { struct { uint32_t serial; - struct wl_surface *surface; + /* Client-side cursor */ + struct wayl_surface surface; struct wl_cursor_theme *theme; struct wl_cursor *cursor; - int scale; - bool hidden; - const char *xcursor; + /* 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; @@ -154,6 +212,7 @@ struct seat { /* We used a discrete axis event in the current pointer frame */ double aggregated[2]; + double aggregated_120[2]; bool have_discrete; } mouse; @@ -206,12 +265,6 @@ struct seat { uint32_t serial; } ime; #endif - - struct { - bool active; - int count; - char32_t character; - } unicode_mode; }; enum csd_surface { @@ -269,7 +322,10 @@ struct monitor { } scaled; } ppi; - float dpi; + struct { + float scaled; + float physical; + } dpi; int scale; float refresh; @@ -289,19 +345,13 @@ struct monitor { bool use_output_release; }; -struct wl_surf_subsurf { - struct wl_surface *surf; - struct wl_subsurface *sub; -}; - struct wl_url { const struct url *url; - struct wl_surf_subsurf surf; + struct wayl_sub_surface surf; }; enum csd_mode {CSD_UNKNOWN, CSD_NO, CSD_YES}; -#if defined(HAVE_XDG_ACTIVATION) typedef void (*activation_token_cb_t)(const char *token, void *data); /* @@ -315,26 +365,28 @@ struct xdg_activation_token_context { activation_token_cb_t cb; /* User provided callback */ void *cb_data; /* Callback user pointer */ }; -#endif struct wayland; struct wl_window { struct terminal *term; - struct wl_surface *surface; + struct wayl_surface surface; struct xdg_surface *xdg_surface; struct xdg_toplevel *xdg_toplevel; -#if defined(HAVE_XDG_ACTIVATION) + struct wp_fractional_scale_v1 *fractional_scale; + tll(struct xdg_activation_token_context *) xdg_tokens; bool urgency_token_is_pending; -#endif + bool unmapped; + float scale; + int preferred_buffer_scale; struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; enum csd_mode csd_mode; struct { - struct wl_surf_subsurf surface[CSD_SURF_COUNT]; + struct wayl_sub_surface surface[CSD_SURF_COUNT]; struct fcft_font *font; int move_timeout_fd; uint32_t serial; @@ -345,10 +397,10 @@ struct wl_window { bool minimize:1; } wm_capabilities; - struct wl_surf_subsurf search; - struct wl_surf_subsurf scrollback_indicator; - struct wl_surf_subsurf render_timer; - struct wl_surf_subsurf overlay; + struct wayl_sub_surface search; + struct wayl_sub_surface scrollback_indicator; + struct wayl_sub_surface render_timer; + struct wayl_sub_surface overlay; struct wl_callback *frame_callback; @@ -364,6 +416,12 @@ struct wl_window { 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; @@ -371,10 +429,17 @@ struct wl_window { 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; @@ -402,23 +467,56 @@ struct wayland { struct wl_data_device_manager *data_device_manager; struct zwp_primary_selection_device_manager_v1 *primary_selection_device_manager; -#if defined(HAVE_XDG_ACTIVATION) struct xdg_activation_v1 *xdg_activation; -#endif + + 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 - bool have_argb8888; 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( @@ -426,29 +524,41 @@ struct wayland *wayl_init( bool presentation_timings); void wayl_destroy(struct wayland *wayl); -bool wayl_reload_xcursor_theme(struct seat *seat, int new_scale); +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 wl_surf_subsurf *surf, + 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 wl_surf_subsurf *surf, bool allow_pointer_input); -void wayl_win_subsurface_destroy(struct wl_surf_subsurf *surf); + struct wayl_sub_surface *surf, bool allow_pointer_input); +void wayl_win_subsurface_destroy(struct wayl_sub_surface *surf); -#if defined(HAVE_XDG_ACTIVATION) 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); -#endif +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 00000000..44d818ec --- /dev/null +++ b/xkbcommon-vmod.h @@ -0,0 +1,18 @@ +#pragma once + +#include <xkbcommon/xkbcommon-names.h> + +/* 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 index 5d1fc997..ccfb5c48 100644 --- a/xmalloc.c +++ b/xmalloc.c @@ -1,8 +1,6 @@ #include <errno.h> -#include <stdarg.h> #include <stdio.h> #include <stdlib.h> -#include <string.h> #include "xmalloc.h" #include "debug.h" @@ -34,8 +32,17 @@ xcalloc(size_t nmemb, size_t size) void * xrealloc(void *ptr, size_t size) { + xassert(size != 0); void *alloc = realloc(ptr, size); - return unlikely(size == 0) ? alloc : check_alloc(alloc); + 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 * diff --git a/xmalloc.h b/xmalloc.h index 8a3098b8..03e6eb0d 100644 --- a/xmalloc.h +++ b/xmalloc.h @@ -2,6 +2,7 @@ #include <stdarg.h> #include <stddef.h> +#include <string.h> #include <wchar.h> #include <uchar.h> @@ -11,8 +12,39 @@ 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 index b0e17741..2f6f8493 100644 --- a/xsnprintf.c +++ b/xsnprintf.c @@ -1,32 +1,51 @@ #include "xsnprintf.h" +#include <errno.h> #include <limits.h> #include <stdio.h> #include "debug.h" +#include "macros.h" -size_t -xvsnprintf(char *buf, size_t n, const char *format, va_list ap) +/* + * 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) { - xassert(n <= INT_MAX); int len = vsnprintf(buf, n, format, ap); - - /* - * ISO C11 §7.21.6.5 states: - * "The snprintf 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." - */ - xassert(len >= 0); - xassert(len < (int)n); - + if (unlikely(len < 0 || len >= (int)n)) { + FATAL_ERROR(__func__, (len < 0) ? errno : ENOBUFS); + } return (size_t)len; } size_t -xsnprintf(char *buf, size_t n, const char *format, ...) +xsnprintf(char *restrict buf, size_t n, const char *restrict format, ...) { va_list ap; va_start(ap, format); diff --git a/xsnprintf.h b/xsnprintf.h index c745ef3e..9463a34a 100644 --- a/xsnprintf.h +++ b/xsnprintf.h @@ -4,5 +4,4 @@ #include <stddef.h> #include "macros.h" -size_t xsnprintf(char *buf, size_t len, const char *fmt, ...) PRINTF(3) NONNULL_ARGS; -size_t xvsnprintf(char *buf, size_t len, const char *fmt, va_list ap) VPRINTF(3) NONNULL_ARGS; +size_t xsnprintf(char *restrict buf, size_t n, const char *restrict format, ...) PRINTF(3) NONNULL_ARGS;